├── .babelrc
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .npmignore
├── .prettierignore
├── .prettierrc.json
├── .yarnrc.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── index.ts
├── logo.svg
├── package.json
├── tests
└── index.test.ts
├── tsconfig.json
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [ ["@babel/preset-env", {"targets": {"node": "current"}}], "@babel/preset-typescript"]
3 | }
4 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push]
3 |
4 | jobs:
5 | build:
6 | runs-on: ubuntu-latest
7 |
8 | strategy:
9 | matrix:
10 | node-version: [14.x, 16.x, 18.x]
11 |
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Use Node.js ${{ matrix.node-version }}
15 | uses: actions/setup-node@v2
16 | with:
17 | node-version: ${{ matrix.node-version }}
18 | - run: yarn install
19 | - run: yarn typecheck
20 | - run: yarn build
21 | - run: yarn test
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | yarn-error.log
3 | lib
4 | .yarn/cache
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/browserslist/browserslist-useragent/b767e4d53dc712715ca9fe2d0e440b755da652c2/.npmignore
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .yarn/cache
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": false,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | plugins:
2 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
3 | spec: "@yarnpkg/plugin-interactive-tools"
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Please use [github releases page](https://github.com/browserslist/browserslist-useragent/releases) to see changes for future versions.
2 |
3 | # v3.0.0
4 |
5 | - **Fix**: Don't crash when `options` parameter is not specified
6 | - **Fix**: Use `loose` option to parse browsers versions. Doesn't crash on versions like `6.00.1` or 6.0.0005` etc.
7 |
8 | - **Feature**: Added support for Samsung Internet
9 |
10 | - **Breaking**: Safari on desktop and on iOS are now considered as separate browsers.
11 | You'll need to use separate browserslist queries to target both of them - `['Safari >= 10', 'iOS >= 10.1']`. See #23
12 | - **Breaking**: Removing support for undocumented option to `matchesUA` - `all`.
13 | This was used internally, and shouldn't be breaking change for you unless you poked around code to discover it.
14 | - **Breaking**: Remove deprecated `_allowHigherVersions` flag. This has been replaced by `allowHigherVersions` in the past.
15 |
16 | # v2.0.1
17 |
18 | - Remove deprecated \_allowHigherVersions option from README
19 |
20 | # v2.0.0
21 |
22 | - **Feature**: Added Yandex to list of supported browsers
23 | - **Feature**: Added support for Yandex Browser
24 | - **Deprecation**: Deprecated `_allowHigherVersions`
25 |
26 | #v1.2.1
27 |
28 | - Fixed build link
29 |
30 | #v1.2.0
31 |
32 | - **Feature**: Updated packages and added support for headless chrome and chromium
33 | - Fixed links in badges
34 |
35 | #v1.1.0
36 |
37 | - **Feature**: Added support for selecting environment
38 | - Updated browserslist version
39 |
40 | #v1.0.4
41 |
42 | - delete gitignore
43 |
44 | #v1.0.3
45 |
46 | - Update README.md
47 |
48 | #v1.0.2
49 |
50 | - Second try at fixing logo
51 |
52 | #v1.0.1
53 |
54 | - Updated browserslist version
55 |
56 | #v1.0.0
57 |
58 | - Create LICENSE
59 | - /browserlist/ => browserslist
60 | - Refactored code and rewrote Readme
61 |
62 | #v0.0.2
63 |
64 | - Added install instructions
65 | - Initial commit
66 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Shubham Kanodia
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Browserslist Useragent
2 |
3 | 
4 | [](https://www.npmjs.com/package/browserslist-useragent)
5 |
6 |
8 |
9 | Find if a given user agent string satisfies a [browserslist](https://github.com/ai/browserslist) query.
10 |
11 | It automatically reads the browserslist configuration specified in your project,
12 | but you can also specify the same using the `options` parameter.
13 |
14 | **If you wish to target modern browsers, read [this](#when-querying-for-modern-browsers).**
15 |
16 | ## Installation
17 |
18 | Note, `browserslist` is a peer dependency, so make sure you have that installed in your project.
19 |
20 | ```bash
21 | npm install browserslist-useragent
22 | # or
23 | yarn add browserslist-useragent
24 | ```
25 |
26 | ## Usage
27 |
28 | ```js
29 | const { matchesUA } = require('browserslist-useragent')
30 |
31 | matchesUA(userAgentString, options)
32 |
33 | // with browserslist config inferred
34 | matchesUA('Mozilla/5.0 (Windows NT 10.0; rv:54.0) Gecko/20100101 Firefox/54.0')
35 | //returns boolean
36 |
37 | // with explicit browserslist
38 | matchesUA(
39 | 'Mozilla/5.0 (Windows NT 10.0; rv:54.0) Gecko/20100101 Firefox/54.0',
40 | { browsers: ['Firefox > 53'] }
41 | )
42 | // returns true
43 | ```
44 |
45 | | Option | Default Value | Description |
46 | | ------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
47 | | browsers | — | Manually provide a browserslist query (or an array of queries). Specifying this overrides the browserslist configuration specified in your project. |
48 | | env | — | When multiple browserslist [environments](https://github.com/ai/browserslist#environments) are specified, pick the config belonging to this environment. |
49 | | path | `process.cwd()` | Specify a folder to search for the browserslist config (if it differs from the current working directory) |
50 | | ignorePatch | `true` | Ignore differences in patch browser numbers |
51 | | ignoreMinor | `false` | Ignore differences in minor browser versions |
52 | | allowHigherVersions | `false` | For all the browsers in the browserslist query, return a match if the user agent version is equal to or higher than the one specified in browserslist. See [why](#when-querying-for-modern-browsers) this might be useful. |
53 |
54 | ## Supported browsers
55 |
56 | - Chrome (Chrome / Chromium / Yandex) as `and_chr` | `ChromeAndroid` | `Chrome`
57 | - Samsung Internet as `Samsung`
58 | - Firefox as `ff` | `and_ff` | `FirefoxAndroid` | `Firefox`
59 | - Safari iOS as `ios_saf` | `iOS`
60 | - Safari Desktop as `Safari`
61 | - IE as `ie` | `ie_mob`
62 | - Edge as `Edge`
63 | - Electron as `Electron`
64 |
65 | PRs to add more _browserslist supported_ browsers are welcome 👋
66 |
67 | ## Notes
68 |
69 | - All browsers on iOS (Chrome, Firefox etc) use Safari's WebKit as the underlying engine, and hence will be resolved to Safari. Since `browserslist` is usually used for
70 | transpiling / autoprefixing for browsers, this behaviour is what's intended in most cases, but might surprise you otherwise.
71 |
72 | - Right now, Chrome for Android and Firefox for Android are resolved to their desktop equivalents. The `caniuse` database does not currently store historical data for these browsers separately (except the last version) See [#156](https://github.com/ai/browserslist/issues/156). However,
73 | safari for iOS and desktop can be matched separately, since this data is available for both.
74 |
75 | ## When querying for modern browsers
76 |
77 | - It is a good idea to update this package often so that browser definitions are upto date.
78 | - It is also a good idea to add `unreleased versions` to your browserslist query, and set `ignoreMinor` and `ignorePatch` to true so that alpha / beta / canary versions of browsers are matched.
79 | - In case you're unable to keep this package up-to-date, you can set the `allowHigherVersions` to `true`. For all the browsers specified in your browserslist query, this will return a match if the user agent version is equal to or higher than those specified in your browserslist query. Use this with care though, since it's a wildcard, and only lightly tested.
80 |
81 | ## Further reads
82 |
83 | - [Smart Bundling: Shipping legacy code to only legacy browsers](https://www.smashingmagazine.com/2018/10/smart-bundling-legacy-code-browsers/)
84 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | import browserslist from 'browserslist';
2 | import semver from 'semver';
3 | import UAParser from 'ua-parser-js';
4 |
5 | // @see https://github.com/ai/browserslist#browsers
6 |
7 | // map of equivalent browsers,
8 | // see https://github.com/ai/browserslist/issues/156
9 |
10 | const browserNameMap: Record = {
11 | bb: 'BlackBerry',
12 | and_chr: 'Chrome',
13 | ChromeAndroid: 'Chrome',
14 | FirefoxAndroid: 'Firefox',
15 | ff: 'Firefox',
16 | ie_mob: 'ExplorerMobile',
17 | ie: 'Explorer',
18 | and_ff: 'Firefox',
19 | ios_saf: 'iOS',
20 | op_mini: 'OperaMini',
21 | op_mob: 'OperaMobile',
22 | and_qq: 'QQAndroid',
23 | and_uc: 'UCAndroid',
24 | }
25 |
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)
31 |
32 | // Case A: For Safari on iOS, the use the browser version
33 | if (
34 | parsedUA.browser.name === 'Safari' && parsedUA.os.name === 'iOS') {
35 | return {
36 | family: 'iOS',
37 | version: parsedBrowserVersion,
38 | }
39 | }
40 |
41 | // Case B: The browser on iOS didn't report as safari,
42 | // so we use the iOS version as a proxy to the browser
43 | // version. This is based on the assumption that the
44 | // underlying Safari Engine used will be *atleast* equal
45 | // to the iOS version it's running on.
46 | if (parsedUA.os.name === 'iOS') {
47 | return {
48 | family: 'iOS',
49 | version: parsedOSVersion
50 | }
51 | }
52 |
53 | if (
54 | (parsedUA.browser.name === 'Opera' && parsedUA.device.type === 'mobile') ||
55 | parsedUA.browser.name === 'Opera Mobi'
56 | ) {
57 | return {
58 | family: 'OperaMobile',
59 | version: parsedBrowserVersion
60 | }
61 | }
62 |
63 | if (parsedUA.browser.name === 'Samsung Browser') {
64 | return {
65 | family: 'Samsung',
66 | version: parsedBrowserVersion
67 | }
68 | }
69 |
70 | if (parsedUA.browser.name === 'IE') {
71 | return {
72 | family: 'Explorer',
73 | version: parsedBrowserVersion
74 | }
75 | }
76 |
77 | if (parsedUA.browser.name === 'IEMobile') {
78 | return {
79 | family: 'ExplorerMobile',
80 | version: parsedBrowserVersion
81 | }
82 | }
83 |
84 | // Use engine version for gecko-based browsers
85 | if (parsedUA.engine.name === 'Gecko') {
86 | return {
87 | family: 'Firefox',
88 | version: parsedEngineVersion
89 | }
90 | }
91 |
92 | // Use engine version for blink-based browsers
93 | if (parsedUA.engine.name === 'Blink') {
94 | return {
95 | family: 'Chrome',
96 | version: parsedEngineVersion
97 | }
98 | }
99 |
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 | ) {
105 | return {
106 | family: 'Chrome',
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
118 | }
119 | }
120 |
121 | return {
122 | family: parsedUA.browser.name || null,
123 | version: parsedBrowserVersion
124 | }
125 | }
126 |
127 | // Convert version to a semver value.
128 | // 2.5 -> 2.5.0; 1 -> 1.0.0;
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
138 | }
139 |
140 | // 10.0-10.2 -> 10.0, 10.1, 10.2
141 | function generateSemversInRange(versionRange: string) {
142 | const [start, end] = versionRange.split('-')
143 | const startSemver = semverify(start)
144 | const endSemver = semverify(end)
145 |
146 | if (!startSemver || !endSemver) {
147 | return []
148 | }
149 | const versionsInRange = []
150 | let curVersion = startSemver
151 |
152 | while (semver.gte(endSemver, curVersion)) {
153 | versionsInRange.push(curVersion)
154 | curVersion = semver.inc(curVersion, 'minor') as string
155 | }
156 |
157 | return versionsInRange
158 | }
159 |
160 | function normalizeQuery(query: string) {
161 | let normalizedQuery = query
162 | const regex = `(${Object.keys(browserNameMap).join('|')})`
163 | const match = query.match(new RegExp(regex))
164 |
165 | if (match) {
166 | normalizedQuery = query.replace(match[0], browserNameMap[match[0]])
167 | }
168 |
169 | return normalizedQuery
170 | }
171 |
172 | const parseBrowsersList = (browsersList: string[]): { family: string, version: string | null }[] => {
173 | const browsers = browsersList
174 | .map((browser) => {
175 | const [name, version] = browser.split(' ')
176 | return { name, version }
177 | })
178 | // #38 Filter out non-numerical browser versions
179 | .filter((browser) => browser.version !== 'TP')
180 | .map((browser) => {
181 | let normalizedName = browser.name
182 | let normalizedVersion = browser.version
183 |
184 | if (browser.name in browserNameMap) {
185 | normalizedName = browserNameMap[browser.name]
186 | }
187 |
188 | // browserslist might return ranges (9.0-9.2), unwrap them
189 | // see https://github.com/browserslist/browserslist-useragent/issues/41
190 | if (browser.version.indexOf('-') > 0) {
191 | return generateSemversInRange(browser.version).map((version) => ({
192 | family: normalizedName,
193 | version,
194 | }))
195 | } else {
196 | return {
197 | family: normalizedName,
198 | version: normalizedVersion,
199 | }
200 | }
201 | })
202 |
203 | return browsers.flat()
204 | }
205 |
206 | const compareBrowserSemvers = (versionA: string, versionB: string, options: Options) => {
207 | const semverifiedA = semverify(versionA)
208 | const semverifiedB = semverify(versionB)
209 |
210 | if (!semverifiedA || !semverifiedB) {
211 | return false
212 | }
213 | let referenceVersion = semverifiedB
214 |
215 | if (options.ignorePatch) {
216 | referenceVersion = `~${semverifiedB}`
217 | }
218 |
219 | if (options.ignoreMinor) {
220 | referenceVersion = `^${semverifiedB}`
221 | }
222 |
223 | if (options.allowHigherVersions) {
224 | return semver.gte(semverifiedA, semverifiedB)
225 | } else {
226 | return semver.satisfies(semverifiedA, referenceVersion)
227 | }
228 | }
229 |
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 = {}) => {
240 | // bail out early if the user agent is invalid
241 | if (!uaString) {
242 | return false
243 | }
244 |
245 | let normalizedQuery
246 | if (opts.browsers) {
247 | normalizedQuery = opts.browsers.map(normalizeQuery)
248 | }
249 | const browsers = browserslist(normalizedQuery, {
250 | env: opts.env,
251 | path: opts.path || process.cwd(),
252 | })
253 |
254 |
255 | const parsedBrowsers = parseBrowsersList(browsers)
256 |
257 |
258 | const resolvedUserAgent = resolveUserAgent(uaString)
259 |
260 | const options = {
261 | ignoreMinor: false,
262 | ignorePatch: true,
263 | ...opts,
264 | }
265 |
266 | return parsedBrowsers.some((browser) => {
267 | if (!resolvedUserAgent.family) return false
268 | if (!resolvedUserAgent.version) return false
269 | if (!browser.version) return false
270 |
271 |
272 | return (
273 | browser.family.toLowerCase() ===
274 | resolvedUserAgent.family.toLocaleLowerCase() &&
275 | compareBrowserSemvers(resolvedUserAgent.version, browser.version, options)
276 | )
277 | })
278 | }
279 |
280 | export {
281 | matchesUA,
282 | resolveUserAgent,
283 | normalizeQuery,
284 | }
285 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "browserslist-useragent",
3 | "version": "4.0.0",
4 | "description": "A utility to match a browselist query to browser user agents",
5 | "main": "lib/index.js",
6 | "repository": "https://github.com/pastelsky/browserslist-useragent",
7 | "author": "Shubham Kanodia ",
8 | "publishConfig": {
9 | "registry": "https://registry.npmjs.org"
10 | },
11 | "license": "MIT",
12 | "files": [
13 | "./lib/index.js",
14 | "./lib/index.d.ts",
15 | "tsconfig.json"
16 | ],
17 | "engines": {
18 | "node": ">= 6.x.x"
19 | },
20 | "scripts": {
21 | "test": "jest",
22 | "test:watch": "jest --watch",
23 | "typecheck": "tsc --noEmit",
24 | "prepublish": "yarn run build",
25 | "build": "mkdir -p lib && tsc"
26 | },
27 | "dependencies": {
28 | "semver": "^7.3.5",
29 | "ua-parser-js": "^1.0.32"
30 | },
31 | "peerDependencies": {
32 | "browserslist": "^4.0.0"
33 | },
34 | "devDependencies": {
35 | "@babel/preset-env": "^7.19.4",
36 | "@babel/preset-typescript": "^7.18.6",
37 | "@types/jest": "^29.1.2",
38 | "@types/node": "^18.11.0",
39 | "@types/semver": "^7.3.12",
40 | "@types/ua-parser-js": "^0.7.36",
41 | "@types/useragent": "^2.3.1",
42 | "babel-cli": "^6.26.0",
43 | "browserslist": "^4.19.1",
44 | "jest": "^29.2.0",
45 | "prettier": "^2.5.1",
46 | "typescript": "^4.8.4",
47 | "useragent-generator": "^1.1.1-amkt-22079-finish.0"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/index.test.ts:
--------------------------------------------------------------------------------
1 | const ua = require('useragent-generator')
2 |
3 | const { resolveUserAgent, matchesUA, normalizeQuery } = require('../index')
4 |
5 | const CustomUserAgentString = {
6 | YANDEX:
7 | 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 YaBrowser/18.1.1.839 Yowser/2.5 Safari/537.36',
8 | YANDEX_SEARCH:
9 | 'Mozilla/5.0 (Linux; Android 4.4.4; GT-I9300I Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/33.0.0.0 Mobile Safari/537.36 YandexSearch/7.16',
10 | SAMSUNG_BROWSER_7_2:
11 | 'Mozilla/5.0 (Linux; Android 6.0.1; SAMSUNG SM-N930F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/7.2 Chrome/63.0.3239.111 Mobile Safari/537.36',
12 | FACEBOOK_WEBVIEW_CHROME_ANDROID:
13 | 'Mozilla/5.0 (Linux; Android 8.0.0; LG-H930 Build/OPR1.170623.026; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36 [FB_IAB/Orca-Android;FBAV/189.0.0.27.99;]',
14 | FACEBOOK_WEBVIEW_IOS:
15 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12D508 [FBAN/FBIOS;FBAV/27.0.0.10.12;FBBV/8291884;FBDV/iPhone7,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/8.2;FBSS/3; FBCR/vodafoneIE;FBID/phone;FBLC/en_US;FBOP/5]',
16 | OPERA_MOBILE:
17 | 'Opera/9.80 (Android 2.3.3; Linux; Opera Mobi/ADR-1111101157; U; es-ES) Presto/2.9.201 Version/11.50',
18 | ELECTRON:
19 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_4_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Electron/12.0.13 Safari/537.36',
20 | }
21 |
22 | it('normalizes queries properly', () => {
23 | expect(normalizeQuery('and_chr >= 61')).toBe('Chrome >= 61')
24 |
25 | expect(normalizeQuery('ChromeAndroid >= 61')).toBe('Chrome >= 61')
26 |
27 | expect(normalizeQuery('FirefoxAndroid < 54')).toBe('Firefox < 54')
28 |
29 | expect(normalizeQuery('ff < 54')).toBe('Firefox < 54')
30 |
31 | expect(normalizeQuery('ios_saf < 10.1.0')).toBe('iOS < 10.1.0')
32 |
33 | expect(normalizeQuery('last 10 and_chr versions')).toBe(
34 | 'last 10 Chrome versions'
35 | )
36 |
37 | expect(normalizeQuery('>= 5%')).toBe('>= 5%')
38 | })
39 |
40 | it('resolves all browsers in iOS to safari with correct platform version', () => {
41 | expect(resolveUserAgent(ua.chrome.iOS('10.3.0'))).toEqual({
42 | family: 'iOS',
43 | version: '10.3.0',
44 | })
45 |
46 | expect(resolveUserAgent(ua.firefox.iOS('10.3.0'))).toEqual({
47 | family: 'iOS',
48 | version: '10.3.0',
49 | })
50 |
51 | expect(resolveUserAgent(ua.safari.iOSWebview('10.3.0'))).toEqual({
52 | family: 'iOS',
53 | version: '10.3.0',
54 | })
55 |
56 | expect(resolveUserAgent(ua.safari.iOS('8.3.0'))).toEqual({
57 | family: 'iOS',
58 | version: '8.3.0',
59 | })
60 | })
61 |
62 | it('resolves desktop safari on osx properly', () => {
63 | expect(resolveUserAgent(ua.safari('10.1.0'))).toEqual({
64 | family: 'Safari',
65 | version: '10.1.0',
66 | })
67 | })
68 |
69 | it('resolves IE/Edge properly', () => {
70 | expect(resolveUserAgent(ua.ie('11.0.0'))).toEqual({
71 | family: 'Explorer',
72 | version: '11.0.0',
73 | })
74 |
75 | expect(resolveUserAgent(ua.ie.windowsPhone('10.0.0'))).toEqual({
76 | family: 'ExplorerMobile',
77 | version: '10.0.0',
78 | })
79 |
80 | expect(resolveUserAgent(ua.edge('14.1.0'))).toEqual({
81 | family: 'Edge',
82 | version: '14.1.0',
83 | })
84 | })
85 |
86 | it('resolves chrome/android properly', () => {
87 | expect(resolveUserAgent(ua.chrome('41.0.228.90'))).toEqual({
88 | family: 'Chrome',
89 | version: '41.0.228',
90 | })
91 |
92 | expect(resolveUserAgent(ua.chrome.androidWebview('2.3.3'))).toEqual({
93 | family: 'Android',
94 | version: '2.3.3',
95 | })
96 |
97 |
98 | expect(
99 | resolveUserAgent(
100 | ua.chrome.androidWebview({
101 | androidVersion: '4.4.1',
102 | chromeVersion: '44.0.0',
103 | })
104 | )
105 | ).toEqual({
106 | family: 'Chrome',
107 | version: '44.0.0',
108 | })
109 |
110 | expect(
111 | resolveUserAgent(
112 | ua.chrome.androidWebview({
113 | androidVersion: '6.0.0',
114 | chromeVersion: '60.0.0',
115 | })
116 | )
117 | ).toEqual({
118 | family: 'Chrome',
119 | version: '60.0.0',
120 | })
121 |
122 | expect(
123 | resolveUserAgent(
124 | ua.chrome('41.0.228.90').replace('Chrome', 'HeadlessChrome')
125 | )
126 | ).toEqual({
127 | family: 'Chrome',
128 | version: '41.0.228',
129 | })
130 |
131 | expect(resolveUserAgent(ua.chromium('41.0.228.90'))).toEqual({
132 | family: 'Chrome',
133 | version: '41.0.228',
134 | })
135 |
136 | expect(
137 | matchesUA(CustomUserAgentString.YANDEX, { browsers: ['Chrome >= 63'] })
138 | ).toBeTruthy()
139 |
140 | expect(
141 | matchesUA(CustomUserAgentString.YANDEX_SEARCH, {
142 | browsers: ['Chrome >= 33'],
143 | })
144 | ).toBeTruthy()
145 |
146 | expect(
147 | resolveUserAgent(CustomUserAgentString.FACEBOOK_WEBVIEW_CHROME_ANDROID)
148 | ).toEqual({
149 | family: 'Chrome',
150 | version: '69.0.3497',
151 | })
152 |
153 | expect(
154 | matchesUA(CustomUserAgentString.FACEBOOK_WEBVIEW_CHROME_ANDROID, {
155 | browsers: ['Chrome >= 63'],
156 | })
157 | ).toBeTruthy()
158 |
159 | expect(resolveUserAgent(CustomUserAgentString.FACEBOOK_WEBVIEW_IOS)).toEqual({
160 | family: 'iOS',
161 | version: '8.2.0',
162 | })
163 |
164 | expect(
165 | matchesUA(CustomUserAgentString.FACEBOOK_WEBVIEW_IOS, {
166 | browsers: ['iOS >= 8'],
167 | })
168 | ).toBeTruthy()
169 | })
170 |
171 | it('resolves firefox properly', () => {
172 | expect(resolveUserAgent(ua.firefox('41.0.0'))).toEqual({
173 | family: 'Firefox',
174 | version: '41.0.0',
175 | })
176 |
177 | expect(resolveUserAgent(ua.firefox.androidPhone('44.0.0'))).toEqual({
178 | family: 'Firefox',
179 | version: '44.0.0',
180 | })
181 | })
182 |
183 | it('resolves samsung browser properly', () => {
184 | expect(resolveUserAgent(CustomUserAgentString.SAMSUNG_BROWSER_7_2)).toEqual({
185 | family: 'Samsung',
186 | version: '7.2.0',
187 | })
188 | })
189 |
190 | it('resolves electron properly', () => {
191 | // Electron 12 -> Chrome 89
192 | expect(resolveUserAgent(CustomUserAgentString.ELECTRON)).toEqual({
193 | family: 'Chrome',
194 | version: '89.0.4389',
195 | })
196 | })
197 |
198 | it('detects if browserslist matches UA', () => {
199 | expect(
200 | matchesUA(ua.firefox.androidPhone('40.0.1'), {
201 | browsers: ['Firefox >= 40'],
202 | })
203 | ).toBeTruthy()
204 |
205 | expect(
206 | matchesUA(ua.firefox('30.0.0'), { browsers: ['Firefox >= 10.0.0'] })
207 | ).toBeTruthy()
208 |
209 | expect(
210 | matchesUA(ua.chrome.iOS('11.0.0'), { browsers: ['iOS >= 10.3.0'] })
211 | ).toBeTruthy()
212 |
213 | expect(
214 | matchesUA(ua.safari.iOS('11.0.0'), { browsers: ['iOS >= 10.3.0'] })
215 | ).toBeTruthy()
216 |
217 | expect(
218 | matchesUA(CustomUserAgentString.SAMSUNG_BROWSER_7_2, {
219 | browsers: ['Samsung >= 7'],
220 | })
221 | ).toBeTruthy()
222 |
223 | expect(
224 | matchesUA(CustomUserAgentString.OPERA_MOBILE, {
225 | browsers: ['OperaMobile >= 7'],
226 | ignoreMinor: true,
227 | })
228 | ).toBeTruthy()
229 |
230 | const modernList = [
231 | 'Firefox >= 53',
232 | 'Edge >= 15',
233 | 'Chrome >= 58',
234 | 'iOS >= 10',
235 | 'Safari >= 11',
236 | 'Samsung >= 7',
237 | ]
238 |
239 | expect(matchesUA(ua.safari.iOS(9), { browsers: modernList })).toBeFalsy()
240 |
241 | expect(
242 | matchesUA(ua.chrome.androidPhone(57), { browsers: modernList })
243 | ).toBeFalsy()
244 |
245 | expect(
246 | matchesUA(ua.firefox.androidPhone(52), { browsers: modernList })
247 | ).toBeFalsy()
248 |
249 | expect(matchesUA(ua.firefox(56), { browsers: modernList })).toBeTruthy()
250 |
251 | expect(matchesUA(ua.edge(14), { browsers: modernList })).toBeFalsy()
252 |
253 | expect(matchesUA(ua.chrome(64), { browsers: modernList })).toBeTruthy()
254 |
255 | expect(
256 | matchesUA(ua.chrome.androidWebview('4.3.3'), { browsers: modernList })
257 | ).toBeFalsy()
258 |
259 | expect(matchesUA(ua.safari('12.0.0'), { browsers: modernList })).toBeTruthy()
260 |
261 | expect(matchesUA(ua.safari('10.0.0'), { browsers: modernList })).toBeFalsy()
262 |
263 | expect(
264 | matchesUA(CustomUserAgentString.SAMSUNG_BROWSER_7_2, {
265 | browsers: modernList,
266 | })
267 | ).toBeTruthy()
268 | })
269 |
270 | it('can interpret various variations in specifying browser names', () => {
271 | expect(matchesUA(ua.chrome(49), { browsers: ['and_chr >= 49'] })).toBeTruthy()
272 |
273 | expect(
274 | matchesUA(ua.safari.iOS('10.3.0'), { browsers: ['ios_saf >= 10.1.0'] })
275 | ).toBeTruthy()
276 |
277 | expect(
278 | matchesUA(ua.firefox.androidPhone('46.0.0'), {
279 | browsers: ['FirefoxAndroid >= 41.1.0'],
280 | })
281 | ).toBeTruthy()
282 | })
283 |
284 | it('ignorePatch option works correctly', () => {
285 | expect(
286 | matchesUA(ua.firefox('49.0.1'), {
287 | browsers: ['ff >= 44'],
288 | ignorePatch: false,
289 | })
290 | ).toBeFalsy()
291 |
292 | expect(
293 | matchesUA(ua.firefox('49.0.1'), {
294 | browsers: ['ff >= 44'],
295 | ignorePatch: true,
296 | })
297 | ).toBeTruthy()
298 |
299 | expect(
300 | matchesUA(ua.firefox('49.1.1'), {
301 | browsers: ['ff >= 44'],
302 | ignorePatch: true,
303 | ignoreMinor: false,
304 | })
305 | ).toBeFalsy()
306 | })
307 |
308 | it('ignoreMinor option works correctly', () => {
309 | expect(
310 | matchesUA(ua.firefox('49.1.0'), {
311 | browsers: ['ff >= 44'],
312 | ignoreMinor: false,
313 | })
314 | ).toBeFalsy()
315 |
316 | expect(
317 | matchesUA(ua.firefox('49.1.0'), {
318 | browsers: ['ff >= 44'],
319 | ignoreMinor: true,
320 | })
321 | ).toBeTruthy()
322 |
323 | expect(
324 | matchesUA(ua.firefox('49.1.3'), {
325 | browsers: ['ff >= 44'],
326 | ignoreMinor: true,
327 | ignorePatch: false,
328 | })
329 | ).toBeTruthy()
330 | })
331 |
332 | it('allowHigherVersions works correctly', () => {
333 | expect(
334 | matchesUA(ua.chrome('1000'), {
335 | browsers: ['chrome >= 60'],
336 | allowHigherVersions: false,
337 | })
338 | ).toBeFalsy()
339 |
340 | expect(
341 | matchesUA(ua.chrome('1000'), {
342 | browsers: ['chrome >= 60'],
343 | allowHigherVersions: true,
344 | })
345 | ).toBeTruthy()
346 | })
347 |
348 | it('parses semvers liberally', () => {
349 | expect(
350 | matchesUA(
351 | 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00',
352 | { browsers: ['opera >= 12'] }
353 | )
354 | ).toBeTruthy()
355 |
356 | expect(
357 | matchesUA(
358 | 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.000.1',
359 | { browsers: ['opera >= 12'] }
360 | )
361 | ).toBeTruthy()
362 |
363 | expect(
364 | matchesUA(
365 | 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.0.0001',
366 | { browsers: ['opera >= 12'] }
367 | )
368 | ).toBeTruthy()
369 | })
370 |
371 | it('can deal with version ranges (if returned by browserslist)', () => {
372 | expect(
373 | matchesUA(ua.safari.iOS('9.1.0'), { browsers: ['ios_saf >= 9'] })
374 | ).toBeTruthy()
375 |
376 | expect(
377 | matchesUA(ua.safari.iOS('9.0.0'), { browsers: ['ios_saf >= 9'] })
378 | ).toBeTruthy()
379 |
380 | // This should fail
381 | // see https://github.com/browserslist/browserslist/issues/402
382 | expect(
383 | matchesUA(ua.safari.iOS('9.1.0'), { browsers: ['ios_saf >= 9.2'] })
384 | ).toBeTruthy() // <-- should actually be falsy
385 | })
386 |
387 | it('can deal with non-numerical version numbers returned by browserslist for safari technology preview', () => {
388 | expect(
389 | matchesUA(ua.safari('18.1.0'), {
390 | browsers: ['unreleased Safari versions'],
391 | ignorePatch: true,
392 | ignoreMinor: true,
393 | allowHigherVersions: true,
394 | })
395 | ).toBeTruthy()
396 | })
397 |
398 | it('gracefully fails on invalid inputs', () => {
399 | expect(matchesUA(undefined)).toBeFalsy()
400 |
401 | expect(matchesUA(null)).toBeFalsy()
402 | })
403 |
404 | it('deals with minor safari', () => {
405 | const UA_ios_saf_10_3_1 = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E8301 Safari/602.1';
406 |
407 | expect(matchesUA(UA_ios_saf_10_3_1, { browsers: ['ios_saf >= 10.1'] }))
408 | .toBeTruthy()
409 |
410 | })
411 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "commonjs", /* Specify what module code is generated. */
29 | // "rootDir": "./", /* Specify the root folder within your source files. */
30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "resolveJsonModule": true, /* Enable importing .json files. */
39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
40 |
41 | /* JavaScript Support */
42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
45 |
46 | /* Emit */
47 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
52 | "outDir": "lib", /* Specify an output folder for all emitted files. */
53 | // "removeComments": true, /* Disable emitting comments. */
54 | // "noEmit": true, /* Disable emitting files from a compilation. */
55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
63 | // "newLine": "crlf", /* Set the newline character for emitting files. */
64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
70 |
71 | /* Interop Constraints */
72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
77 |
78 | /* Type Checking */
79 | "strict": true, /* Enable all strict type-checking options. */
80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
98 |
99 | /* Completeness */
100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
102 | },
103 | "include": ["./index.ts"]
104 | }
105 |
--------------------------------------------------------------------------------