Skip to content

Commit fc15b5d

Browse files
pastelskyShubham Kanodia
authored andcommitted
Merge pull request #76 from browserslist/sk/use-uaparser
Replace useragent with ua-parser-js, use TS
2 parents ee7bb63 + 9285ce3 commit fc15b5d

File tree

12 files changed

+4543
-7606
lines changed

12 files changed

+4543
-7606
lines changed

.babelrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"presets": ["es2015", "stage-2"]
2+
"presets": [ ["@babel/preset-env", {"targets": {"node": "current"}}], "@babel/preset-typescript"]
33
}

.github/workflows/main.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77

88
strategy:
99
matrix:
10-
node-version: [12.x, 14.x, 16.x]
10+
node-version: [14.x, 16.x, 18.x]
1111

1212
steps:
1313
- uses: actions/checkout@v2
@@ -16,5 +16,6 @@ jobs:
1616
with:
1717
node-version: ${{ matrix.node-version }}
1818
- run: yarn install
19+
- run: yarn typecheck
1920
- run: yarn build
2021
- run: yarn test

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
node_modules
22
yarn-error.log
3-
lib/index.js
4-
.yarn/cache
3+
lib
4+
.yarn/cache

.yarn/install-state.gz

-562 KB
Binary file not shown.

.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs

Lines changed: 0 additions & 363 deletions
This file was deleted.

.yarn/releases/yarn-3.1.1.cjs

Lines changed: 0 additions & 768 deletions
This file was deleted.

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@ but you can also specify the same using the `options` parameter.
1515

1616
## Installation
1717

18+
Note, `browserslist` is a peer dependency, so make sure you have that installed in your project.
19+
1820
```bash
1921
npm install browserslist-useragent
22+
# or
23+
yarn add browserslist-useragent
2024
```
2125

2226
## Usage

index.js renamed to index.ts

Lines changed: 96 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
const browserslist = require('browserslist')
2-
const semver = require('semver')
3-
const useragent = require('useragent')
4-
const e2c = require('electron-to-chromium/versions')
1+
import browserslist from 'browserslist';
2+
import semver from 'semver';
3+
import UAParser from 'ua-parser-js';
54

65
// @see https://github.com/ai/browserslist#browsers
76

87
// map of equivalent browsers,
98
// see https://github.com/ai/browserslist/issues/156
109

11-
const browserNameMap = {
10+
const browserNameMap: Record<string, string> = {
1211
bb: 'BlackBerry',
1312
and_chr: 'Chrome',
1413
ChromeAndroid: 'Chrome',
@@ -24,35 +23,18 @@ const browserNameMap = {
2423
and_uc: 'UCAndroid',
2524
}
2625

27-
function resolveUserAgent(uaString) {
28-
// Chrome and Opera on iOS uses a UIWebView of the underlying platform to render
29-
// content, by stripping the CriOS or OPiOS strings the useragent parser will alias the
30-
// user agent to ios_saf for the UIWebView, which is closer to the actual
31-
// renderer
32-
// @see https://github.com/Financial-Times/polyfill-service/pull/416
26+
function resolveUserAgent(uaString: string): { family: string | null, version: string | null } {
27+
const parsedUA = UAParser(uaString)
28+
const parsedBrowserVersion = semverify(parsedUA.browser.version)
29+
const parsedOSVersion = semverify(parsedUA.os.version)
30+
const parsedEngineVersion = semverify(parsedUA.engine.version)
3331

34-
let strippedUA = uaString.replace(
35-
/((CriOS|OPiOS)\/(\d+)\.(\d+)\.(\d+)\.(\d+))/,
36-
''
37-
)
38-
39-
// Yandex Browser uses Chromium as the udnerlying engine
40-
strippedUA = strippedUA.replace(/YaBrowser\/(\d+\.?)+/g, '')
41-
42-
// Yandex Search uses Chromium as the udnerlying engine
43-
strippedUA = strippedUA.replace(/YandexSearch\/(\d+\.?)+/g, '')
44-
45-
// Facebook Webview
46-
strippedUA = strippedUA.replace(/FB_IAB/g, '').replace(/FBAN\/FBIOS/g, '')
47-
48-
const parsedUA = useragent.parse(strippedUA)
49-
50-
// Case A: For Safari, Chrome and others browsers on iOS
51-
// that report as Safari after stripping tags
52-
if (parsedUA.family.includes('Safari') && parsedUA.os.family === 'iOS') {
32+
// Case A: For Safari on iOS, the use the browser version
33+
if (
34+
parsedUA.browser.name === 'Safari' && parsedUA.os.name === 'iOS') {
5335
return {
5436
family: 'iOS',
55-
version: [parsedUA.major, parsedUA.minor, parsedUA.patch].join('.'),
37+
version: parsedBrowserVersion,
5638
}
5739
}
5840

@@ -61,111 +43,121 @@ function resolveUserAgent(uaString) {
6143
// version. This is based on the assumption that the
6244
// underlying Safari Engine used will be *atleast* equal
6345
// to the iOS version it's running on.
64-
if (parsedUA.os.family === 'iOS') {
46+
if (parsedUA.os.name === 'iOS') {
6547
return {
6648
family: 'iOS',
67-
version: [parsedUA.os.major, parsedUA.os.minor, parsedUA.os.patch].join(
68-
'.'
69-
),
49+
version: parsedOSVersion
7050
}
7151
}
7252

73-
// Case C: The caniuse database does not contain
74-
// historical browser versions for so called `minor`
75-
// browsers like Chrome for Android, Firefox for Android etc
76-
// In this case, we proxy to the desktop version
77-
// @see https://github.com/Fyrd/caniuse/issues/3518
78-
7953
if (
80-
parsedUA.family.includes('Chrome Mobile') ||
81-
parsedUA.family.includes('Chrome Mobile WebView') ||
82-
parsedUA.family.includes('Chromium') ||
83-
parsedUA.family.includes('HeadlessChrome')
54+
(parsedUA.browser.name === 'Opera' && parsedUA.device.type === 'mobile') ||
55+
parsedUA.browser.name === 'Opera Mobi'
8456
) {
8557
return {
86-
family: 'Chrome',
87-
version: [parsedUA.major, parsedUA.minor, parsedUA.patch].join('.'),
58+
family: 'OperaMobile',
59+
version: parsedBrowserVersion
8860
}
8961
}
9062

91-
if (parsedUA.family === 'Opera Mobile') {
63+
if (parsedUA.browser.name === 'Samsung Browser') {
9264
return {
93-
family: 'OperaMobile',
94-
version: [parsedUA.major, parsedUA.minor, parsedUA.patch].join('.'),
65+
family: 'Samsung',
66+
version: parsedBrowserVersion
9567
}
9668
}
9769

98-
if (parsedUA.family === 'Samsung Internet') {
70+
if (parsedUA.browser.name === 'IE') {
9971
return {
100-
family: 'Samsung',
101-
version: [parsedUA.major, parsedUA.minor, parsedUA.patch].join('.'),
72+
family: 'Explorer',
73+
version: parsedBrowserVersion
10274
}
10375
}
10476

105-
if (parsedUA.family === 'Firefox Mobile') {
77+
if (parsedUA.browser.name === 'IEMobile') {
10678
return {
107-
family: 'Firefox',
108-
version: [parsedUA.major, parsedUA.minor, parsedUA.patch].join('.'),
79+
family: 'ExplorerMobile',
80+
version: parsedBrowserVersion
10981
}
11082
}
11183

112-
if (parsedUA.family === 'IE') {
84+
// Use engine version for gecko-based browsers
85+
if (parsedUA.engine.name === 'Gecko') {
11386
return {
114-
family: 'Explorer',
115-
version: [parsedUA.major, parsedUA.minor, parsedUA.patch].join('.'),
87+
family: 'Firefox',
88+
version: parsedEngineVersion
11689
}
11790
}
11891

119-
if (parsedUA.family === 'IE Mobile') {
92+
// Use engine version for blink-based browsers
93+
if (parsedUA.engine.name === 'Blink') {
12094
return {
121-
family: 'ExplorerMobile',
122-
version: [parsedUA.major, parsedUA.minor, parsedUA.patch].join('.'),
95+
family: 'Chrome',
96+
version: parsedEngineVersion
12397
}
12498
}
12599

126-
if (parsedUA.family === 'Electron') {
127-
const electronVersion = [parsedUA.major, parsedUA.minor].join('.')
100+
// Chrome based browsers pre-blink (WebKit)
101+
if (
102+
parsedUA.browser.name &&
103+
['Chrome', 'Chromium', 'Chrome WebView', 'Chrome Headless'].includes(parsedUA.browser.name)
104+
) {
128105
return {
129106
family: 'Chrome',
130-
version: e2c[electronVersion],
107+
version: parsedBrowserVersion
108+
}
109+
}
110+
111+
if (parsedUA.browser.name === 'Android Browser') {
112+
// Versions prior to Blink were based
113+
// on the OS version. Only after this
114+
// did android start using system chrome for web-views
115+
return {
116+
family: 'Android',
117+
version: parsedOSVersion
131118
}
132119
}
133120

134121
return {
135-
family: parsedUA.family,
136-
version: [parsedUA.major, parsedUA.minor, parsedUA.patch].join('.'),
122+
family: parsedUA.browser.name || null,
123+
version: parsedBrowserVersion
137124
}
138125
}
139126

140127
// Convert version to a semver value.
141128
// 2.5 -> 2.5.0; 1 -> 1.0.0;
142-
const semverify = (version) => semver.coerce(version, { loose: true }).version
143-
144-
function flatten(arr) {
145-
return arr.reduce(function (flat, toFlatten) {
146-
return flat.concat(
147-
Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten
148-
)
149-
}, [])
129+
const semverify = (version: string | undefined | null) => {
130+
if (!version) {
131+
return null
132+
}
133+
const cooerced = semver.coerce(version, { loose: true })
134+
if (!cooerced) {
135+
return null
136+
}
137+
return cooerced.version
150138
}
151139

152140
// 10.0-10.2 -> 10.0, 10.1, 10.2
153-
function generateSemversInRange(versionRange) {
141+
function generateSemversInRange(versionRange: string) {
154142
const [start, end] = versionRange.split('-')
155143
const startSemver = semverify(start)
156144
const endSemver = semverify(end)
145+
146+
if (!startSemver || !endSemver) {
147+
return []
148+
}
157149
const versionsInRange = []
158150
let curVersion = startSemver
159151

160152
while (semver.gte(endSemver, curVersion)) {
161153
versionsInRange.push(curVersion)
162-
curVersion = semver.inc(curVersion, 'minor')
154+
curVersion = semver.inc(curVersion, 'minor') as string
163155
}
164156

165157
return versionsInRange
166158
}
167159

168-
function normalizeQuery(query) {
160+
function normalizeQuery(query: string) {
169161
let normalizedQuery = query
170162
const regex = `(${Object.keys(browserNameMap).join('|')})`
171163
const match = query.match(new RegExp(regex))
@@ -177,7 +169,7 @@ function normalizeQuery(query) {
177169
return normalizedQuery
178170
}
179171

180-
const parseBrowsersList = (browsersList) => {
172+
const parseBrowsersList = (browsersList: string[]): { family: string, version: string | null }[] => {
181173
const browsers = browsersList
182174
.map((browser) => {
183175
const [name, version] = browser.split(' ')
@@ -208,12 +200,16 @@ const parseBrowsersList = (browsersList) => {
208200
}
209201
})
210202

211-
return flatten(browsers)
203+
return browsers.flat()
212204
}
213205

214-
const compareBrowserSemvers = (versionA, versionB, options) => {
206+
const compareBrowserSemvers = (versionA: string, versionB: string, options: Options) => {
215207
const semverifiedA = semverify(versionA)
216208
const semverifiedB = semverify(versionB)
209+
210+
if (!semverifiedA || !semverifiedB) {
211+
return false
212+
}
217213
let referenceVersion = semverifiedB
218214

219215
if (options.ignorePatch) {
@@ -231,7 +227,16 @@ const compareBrowserSemvers = (versionA, versionB, options) => {
231227
}
232228
}
233229

234-
const matchesUA = (uaString, opts = {}) => {
230+
type Options = {
231+
browsers?: string[],
232+
env?: string,
233+
path?: string,
234+
ignoreMinor?: boolean,
235+
ignorePatch?: boolean,
236+
allowHigherVersions?: boolean
237+
}
238+
239+
const matchesUA = (uaString: string, opts: Options = {}) => {
235240
// bail out early if the user agent is invalid
236241
if (!uaString) {
237242
return false
@@ -246,8 +251,10 @@ const matchesUA = (uaString, opts = {}) => {
246251
path: opts.path || process.cwd(),
247252
})
248253

254+
249255
const parsedBrowsers = parseBrowsersList(browsers)
250256

257+
251258
const resolvedUserAgent = resolveUserAgent(uaString)
252259

253260
const options = {
@@ -257,15 +264,20 @@ const matchesUA = (uaString, opts = {}) => {
257264
}
258265

259266
return parsedBrowsers.some((browser) => {
267+
if (!resolvedUserAgent.family) return false
268+
if (!resolvedUserAgent.version) return false
269+
if (!browser.version) return false
270+
271+
260272
return (
261273
browser.family.toLowerCase() ===
262-
resolvedUserAgent.family.toLocaleLowerCase() &&
274+
resolvedUserAgent.family.toLocaleLowerCase() &&
263275
compareBrowserSemvers(resolvedUserAgent.version, browser.version, options)
264276
)
265277
})
266278
}
267279

268-
module.exports = {
280+
export {
269281
matchesUA,
270282
resolveUserAgent,
271283
normalizeQuery,

0 commit comments

Comments
 (0)