├── .clean-publish
├── .commitlintrc.json
├── .czrc
├── .editorconfig
├── .eslintrc.json
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug-report.yml
│ ├── config.yml
│ ├── feature-request.yml
│ └── question.yml
├── renovate.json
└── workflows
│ ├── checks.yml
│ ├── commit.yml
│ ├── release.yml
│ ├── tests.yml
│ └── website.yml
├── .gitignore
├── .nano-staged.json
├── .npmrc
├── .simple-git-hooks.json
├── .size-limit.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── examples
├── .eslintrc.json
├── EXAMPLES.md
├── buildDemo.js
└── demojs
│ ├── index.actual.js
│ ├── index.modern.js
│ └── index.old.js
├── package.json
├── pnpm-lock.yaml
├── rollup.config.js
├── src
├── browsers
│ ├── browserslist.spec.ts
│ ├── browserslist.ts
│ ├── index.ts
│ ├── optimize.spec.ts
│ ├── optimize.ts
│ ├── types.ts
│ ├── utils.spec.ts
│ └── utils.ts
├── cli.ts
├── index.ts
├── numbers
│ ├── index.ts
│ ├── range.spec.ts
│ ├── range.ts
│ ├── ray.spec.ts
│ ├── ray.ts
│ ├── segment.spec.ts
│ ├── segment.ts
│ └── utils.ts
├── regex
│ ├── index.ts
│ ├── nodes.ts
│ ├── optimize.spec.ts
│ ├── optimize.ts
│ ├── regex.ts
│ └── utils.ts
├── semver
│ ├── index.ts
│ ├── semver.spec.ts
│ ├── semver.ts
│ └── types.ts
├── useragent
│ ├── index.ts
│ ├── types.ts
│ ├── useragent.spec.ts
│ ├── useragent.ts
│ ├── utils.spec.ts
│ └── utils.ts
├── useragentRegex
│ ├── index.ts
│ ├── types.ts
│ ├── useragentRegex.spec.ts
│ ├── useragentRegex.ts
│ └── utils.ts
├── utils
│ ├── index.ts
│ └── utils.spec.ts
└── versions
│ ├── index.ts
│ ├── utils.spec.ts
│ ├── utils.ts
│ ├── versions.spec.ts
│ └── versions.ts
├── test
├── tsconfig.json
└── useragents.ts
├── tsconfig.json
└── vite.config.js
/.clean-publish:
--------------------------------------------------------------------------------
1 | {
2 | "withoutPublish": true,
3 | "tempDir": "package"
4 | }
5 |
--------------------------------------------------------------------------------
/.commitlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@commitlint/config-conventional"],
3 | "rules": {
4 | "body-max-line-length": [0]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.czrc:
--------------------------------------------------------------------------------
1 | {
2 | "path": "@commitlint/cz-commitlint"
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | end_of_line = lf
10 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "@trigen/eslint-config",
4 | "@trigen/eslint-config/esm",
5 | "@trigen/eslint-config/tsm",
6 | "@trigen/eslint-config/typescript",
7 | "@trigen/eslint-config/typescript-requiring-type-checking",
8 | "@trigen/eslint-config/jest"
9 | ],
10 | "env": {
11 | "node": true
12 | },
13 | "parserOptions": {
14 | "tsconfigRootDir": "./",
15 | "project": ["./tsconfig.json", "./test/tsconfig.json"]
16 | },
17 | "rules": {
18 | "@typescript-eslint/no-magic-numbers": "off",
19 | "@typescript-eslint/no-unsafe-argument": "off",
20 | "@typescript-eslint/no-misused-promises": "off",
21 | "array-element-newline": "off",
22 | "prefer-destructuring": "off",
23 | "@typescript-eslint/keyword-spacing": ["error", {}]
24 | },
25 | "ignorePatterns": [
26 | "rollup.config.js",
27 | "dist"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | open_collective: browserslist-useragent-regexp
2 | ko_fi: dangreen
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.yml:
--------------------------------------------------------------------------------
1 | name: "🐛 Bug Report"
2 | description: "If something isn't working as expected."
3 | title: "[Bug]: "
4 | labels: ["bug"]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible.
9 |
10 | - type: checkboxes
11 | id: input1
12 | attributes:
13 | label: Would you like to work on a fix?
14 | options:
15 | - label: Check this if you would like to implement a PR, we are more than happy to help you go through the process.
16 |
17 | - type: textarea
18 | attributes:
19 | label: Current and expected behavior
20 | description: A clear and concise description of what the library is doing and what you would expect.
21 | validations:
22 | required: true
23 |
24 | - type: input
25 | attributes:
26 | label: Reproduction
27 | description: |
28 | Please provide issue reproduction.
29 | You can give a link to a repository with the reproduction or make a [sandbox](https://codesandbox.io/) and reproduce the issue there.
30 | validations:
31 | required: true
32 |
33 | - type: input
34 | attributes:
35 | label: browserslist-useragent-regexp version
36 | description: Which version of `browserslist-useragent-regexp` are you using?
37 | placeholder: v0.0.0
38 | validations:
39 | required: true
40 |
41 | - type: textarea
42 | attributes:
43 | label: Possible solution
44 | description: If you have suggestions on a fix for the bug.
45 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.yml:
--------------------------------------------------------------------------------
1 | name: "🚀 Feature Request"
2 | description: "I have a specific suggestion!"
3 | labels: ["enhancement"]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: Thanks for taking the time to suggest a new feature! Please fill out this form as completely as possible.
8 |
9 | - type: checkboxes
10 | id: input1
11 | attributes:
12 | label: Would you like to work on this feature?
13 | options:
14 | - label: Check this if you would like to implement a PR, we are more than happy to help you go through the process.
15 |
16 | - type: textarea
17 | attributes:
18 | label: What problem are you trying to solve?
19 | description: |
20 | A concise description of what the problem is.
21 | placeholder: |
22 | I have an issue when [...]
23 | validations:
24 | required: true
25 |
26 | - type: textarea
27 | attributes:
28 | label: Describe the solution you'd like
29 | validations:
30 | required: true
31 |
32 | - type: textarea
33 | attributes:
34 | label: Describe alternatives you've considered
35 |
36 | - type: textarea
37 | attributes:
38 | label: Documentation, Adoption, Migration Strategy
39 | description: |
40 | If you can, explain how users will be able to use this and how it might be documented. Maybe a mock-up?
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.yml:
--------------------------------------------------------------------------------
1 | name: "❓ Question"
2 | description: "Have a Question?"
3 | labels: ["question"]
4 | body:
5 | - type: textarea
6 | attributes:
7 | label: Describe your question
8 | validations:
9 | required: true
10 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base",
4 | ":preserveSemverRanges"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.github/workflows/checks.yml:
--------------------------------------------------------------------------------
1 | name: Checks
2 | on:
3 | pull_request:
4 | branches:
5 | - master
6 | jobs:
7 | size:
8 | runs-on: ubuntu-latest
9 | name: size-limit
10 | steps:
11 | - name: Checkout the repository
12 | uses: actions/checkout@v4
13 | - name: Install pnpm
14 | uses: pnpm/action-setup@v2
15 | with:
16 | version: 8
17 | - name: Install Node.js
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: 16
21 | cache: 'pnpm'
22 | - name: Check size
23 | uses: andresz1/size-limit-action@master
24 | with:
25 | github_token: ${{ secrets.GITHUB_TOKEN }}
26 | editorconfig:
27 | runs-on: ubuntu-latest
28 | name: editorconfig
29 | steps:
30 | - name: Checkout the repository
31 | uses: actions/checkout@v4
32 | - name: Create config
33 | run: |
34 | echo '{"Exclude": ["docs"]}' > .ecrc
35 | - name: Check editorconfig
36 | uses: editorconfig-checker/action-editorconfig-checker@main
37 | package-json:
38 | runs-on: ubuntu-latest
39 | name: package.json
40 | steps:
41 | - name: Checkout the repository
42 | uses: actions/checkout@v4
43 | - name: Install pnpm
44 | uses: pnpm/action-setup@v2
45 | with:
46 | version: 8
47 | - name: Install Node.js
48 | uses: actions/setup-node@v4
49 | with:
50 | node-version: 16
51 | cache: 'pnpm'
52 | - name: Check package.json files
53 | run: pnpm dlx @trigen/lint-package-json
54 |
--------------------------------------------------------------------------------
/.github/workflows/commit.yml:
--------------------------------------------------------------------------------
1 | name: Commit
2 | on:
3 | push:
4 | jobs:
5 | commitlint:
6 | runs-on: ubuntu-latest
7 | name: commitlint
8 | steps:
9 | - name: Checkout the repository
10 | uses: actions/checkout@v4
11 | with:
12 | fetch-depth: 0
13 | - name: Install pnpm
14 | uses: pnpm/action-setup@v2
15 | with:
16 | version: 8
17 | - name: Install Node.js
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: 16
21 | cache: 'pnpm'
22 | - name: Install dependencies
23 | run: pnpm install
24 | - name: Run commitlint
25 | run: pnpm commitlint --from=HEAD~1
26 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | release:
4 | types: [created]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | name: Publish package
9 | steps:
10 | - name: Checkout the repository
11 | uses: actions/checkout@v4
12 | - name: Install pnpm
13 | uses: pnpm/action-setup@v2
14 | with:
15 | version: 8
16 | - name: Install Node.js
17 | uses: actions/setup-node@v4
18 | with:
19 | node-version: 16
20 | cache: 'pnpm'
21 | registry-url: 'https://registry.npmjs.org'
22 | - name: Install dependencies
23 | run: pnpm install
24 | - name: Publish
25 | run: pnpm publish --no-git-checks --tag $NPM_TAG
26 | env:
27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
28 | NPM_TAG: ${{ contains(github.ref_name, '-beta') && 'next' || 'latest' }}
29 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on:
3 | workflow_dispatch:
4 | pull_request:
5 | push:
6 | branches:
7 | - master
8 | jobs:
9 | types:
10 | runs-on: ubuntu-latest
11 | name: types
12 | steps:
13 | - name: Checkout the repository
14 | uses: actions/checkout@v4
15 | - name: Install pnpm
16 | uses: pnpm/action-setup@v2
17 | with:
18 | version: 8
19 | - name: Install Node.js
20 | uses: actions/setup-node@v4
21 | with:
22 | node-version: 16
23 | cache: 'pnpm'
24 | - name: Install dependencies
25 | run: pnpm install
26 | - name: Check types
27 | run: pnpm test:types
28 | unit:
29 | runs-on: ubuntu-latest
30 | name: unit
31 | steps:
32 | - name: Checkout the repository
33 | uses: actions/checkout@v4
34 | - name: Install pnpm
35 | uses: pnpm/action-setup@v2
36 | with:
37 | version: 8
38 | - name: Install Node.js
39 | uses: actions/setup-node@v4
40 | with:
41 | node-version: 16
42 | cache: 'pnpm'
43 | - name: Install dependencies
44 | run: pnpm install
45 | - name: Run tests
46 | run: pnpm test
47 | - name: Collect coverage
48 | uses: codecov/codecov-action@v4
49 | if: success()
50 | with:
51 | files: ./coverage/lcov.info
52 |
--------------------------------------------------------------------------------
/.github/workflows/website.yml:
--------------------------------------------------------------------------------
1 | name: Website
2 | on:
3 | push:
4 | branches:
5 | - master
6 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
7 | permissions:
8 | contents: read
9 | pages: write
10 | id-token: write
11 | # Allow one concurrent deployment
12 | concurrency:
13 | group: "pages"
14 | cancel-in-progress: true
15 | jobs:
16 | deploy:
17 | environment:
18 | name: github-pages
19 | url: ${{ steps.deployment.outputs.page_url }}
20 | runs-on: ubuntu-latest
21 | name: deploy website
22 | steps:
23 | - name: Checkout the repository
24 | uses: actions/checkout@v4
25 | - name: Install pnpm
26 | uses: pnpm/action-setup@v2
27 | with:
28 | version: 8
29 | - name: Install Node.js
30 | uses: actions/setup-node@v4
31 | with:
32 | node-version: 16
33 | cache: 'pnpm'
34 | - name: Install dependencies
35 | run: pnpm install
36 | - name: Build website
37 | run: |
38 | pnpm build
39 | pnpm build:docs
40 | pnpm build:demo
41 | - name: Setup Pages
42 | uses: actions/configure-pages@v5
43 | - name: Upload artifact
44 | uses: actions/upload-pages-artifact@v3
45 | with:
46 | path: './docs'
47 | - name: Deploy to GitHub Pages
48 | id: deployment
49 | uses: actions/deploy-pages@v4
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Coverage directory used by tools like istanbul
7 | coverage
8 |
9 | # Dependency directory
10 | node_modules
11 |
12 | # Optional npm cache directory
13 | .npm
14 |
15 | # OS stuff
16 | ._*
17 | .DS_Store
18 |
19 | # Some caches
20 | .*cache
21 |
22 | # Compiled dist
23 | dist
24 | package
25 | build
26 |
27 | # Env files
28 | .env
29 |
--------------------------------------------------------------------------------
/.nano-staged.json:
--------------------------------------------------------------------------------
1 | {
2 | "*.{js,ts,tsx}": "eslint --fix"
3 | }
4 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | auto-install-peers=false
2 |
--------------------------------------------------------------------------------
/.simple-git-hooks.json:
--------------------------------------------------------------------------------
1 | {
2 | "commit-msg": "pnpm commitlint --edit \"$1\"",
3 | "pre-commit": "pnpm nano-staged",
4 | "pre-push": "pnpm test"
5 | }
6 |
--------------------------------------------------------------------------------
/.size-limit.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "path": "dist/index.js",
4 | "limit": "8 KB"
5 | },
6 | {
7 | "path": "dist/cli.js",
8 | "limit": "1.45 KB"
9 | }
10 | ]
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### [4.1.3](https://github.com/browserslist/browserslist-useragent-regexp/compare/v4.1.2...v4.1.3) (2024-04-10)
6 |
7 |
8 | ### Bug Fixes
9 |
10 | * restore types field in package.json ([#1546](https://github.com/browserslist/browserslist-useragent-regexp/issues/1546)) ([3fdde6a](https://github.com/browserslist/browserslist-useragent-regexp/commit/3fdde6a8a8b5a2c634e4812da069678152d8f047)), closes [#1545](https://github.com/browserslist/browserslist-useragent-regexp/issues/1545)
11 |
12 | ### [4.1.2](https://github.com/browserslist/browserslist-useragent-regexp/compare/v4.1.1...v4.1.2) (2024-04-05)
13 |
14 |
15 | ### Bug Fixes
16 |
17 | * fix splitCommonDiff function to work correctly on common digits after first difference ([#1543](https://github.com/browserslist/browserslist-useragent-regexp/issues/1543)) ([ae3981c](https://github.com/browserslist/browserslist-useragent-regexp/commit/ae3981ce92408c4a04175f1ad0ee446024ddbbc1))
18 |
19 | ### [4.1.1](https://github.com/browserslist/browserslist-useragent-regexp/compare/v4.1.0...v4.1.1) (2023-12-24)
20 |
21 |
22 | ### Bug Fixes
23 |
24 | * fix Edge browser regex, minor version is required ([#1533](https://github.com/browserslist/browserslist-useragent-regexp/issues/1533)) ([f6108cf](https://github.com/browserslist/browserslist-useragent-regexp/commit/f6108cfb3921bf2822f4235eeb4ba893409e4bb8)), closes [#1530](https://github.com/browserslist/browserslist-useragent-regexp/issues/1530)
25 |
26 | ## [4.1.0](https://github.com/browserslist/browserslist-useragent-regexp/compare/v4.0.0...v4.1.0) (2023-06-16)
27 |
28 |
29 | ### Features
30 |
31 | * allow all browserslist options via JS API ([#1489](https://github.com/browserslist/browserslist-useragent-regexp/issues/1489)) ([38d1e23](https://github.com/browserslist/browserslist-useragent-regexp/commit/38d1e23fe8e59875208426f9a32b9a4d06577e28))
32 |
33 | ## [4.0.0](https://github.com/browserslist/browserslist-useragent-regexp/compare/v4.0.0-beta.1...v4.0.0) (2022-11-15)
34 |
35 | ## [4.0.0-beta.1](https://github.com/browserslist/browserslist-useragent-regexp/compare/v4.0.0-beta.0...v4.0.0-beta.1) (2022-11-06)
36 |
37 | ## [4.0.0-beta.0](https://github.com/browserslist/browserslist-useragent-regexp/compare/v3.0.0...v4.0.0-beta.0) (2022-10-15)
38 |
39 |
40 | ### ⚠ BREAKING CHANGES
41 |
42 | * regexp -> regex in JS API naming, new regexes from ua-regexes-lite
43 | * now browserslist is peer dependency
44 | * NodeJS >= 14 is required, no commonjs support
45 |
46 | ### Features
47 |
48 | * `bluare` binary alias ([288b473](https://github.com/browserslist/browserslist-useragent-regexp/commit/288b4732490977e7e70038b72d94476d735214da))
49 | * browserslist as peer dependency ([eedbcc5](https://github.com/browserslist/browserslist-useragent-regexp/commit/eedbcc58794cb8cbf491027ffd651abadd27d5ed))
50 | * move to ESM ([#1450](https://github.com/browserslist/browserslist-useragent-regexp/issues/1450)) ([41456bc](https://github.com/browserslist/browserslist-useragent-regexp/commit/41456bc22b789fee57384a00abb64e0690ded08a))
51 |
52 |
53 | ### Bug Fixes
54 |
55 | * typo in cli option ([e11f219](https://github.com/browserslist/browserslist-useragent-regexp/commit/e11f2196b5b291f31f81057fa5d468c51f48e1a6))
56 |
57 |
58 | * rename js api, ua-regexes-lite instead of useragents ([#1454](https://github.com/browserslist/browserslist-useragent-regexp/issues/1454)) ([332b7d8](https://github.com/browserslist/browserslist-useragent-regexp/commit/332b7d87cc83e749109f973671239eddcd026bff))
59 |
60 | ## [3.0.0] - 2021-02-03
61 | ### Breaking
62 | - Requires `Node 12+`
63 |
64 | ## [2.1.1] - 2020-10-17
65 | ### Fixed
66 | - [#472](https://github.com/browserslist/browserslist-useragent-regexp/issues/472)
67 |
68 | ## [2.1.0] - 2020-06-11
69 | ### Fixed
70 | - Extracting browser family from regexp fix.
71 |
72 | ### Changed
73 | - `HeadlessChrome` regexp was removed, works with regular Chrome regexp.
74 |
75 | ## [2.0.5] - 2020-05-12
76 | ### Fixed
77 | - [#434](https://github.com/browserslist/browserslist-useragent-regexp/issues/434)
78 |
79 | ## [2.0.4] - 2020-04-23
80 | ### Fixed
81 | - Desktop Safari regexp.
82 |
83 | ## [2.0.3] - 2020-04-22
84 | ### Fixed
85 | - [#420](https://github.com/browserslist/browserslist-useragent-regexp/issues/420)
86 |
87 | ## [2.0.2] - 2020-04-16
88 | ### Fixed
89 | - [#409](https://github.com/browserslist/browserslist-useragent-regexp/issues/409)
90 |
91 | ## [2.0.0] - 2020-01-25
92 | ### Breaking
93 | - Requires `Node 10+`
94 |
95 | ## [1.3.1-beta] - 2019-07-30
96 | ### Changed
97 | - Dependencies update.
98 |
99 | ## [1.3.0-beta] - 2019-06-09
100 | ### Added
101 | - `trigen-scripts` dev tool.
102 |
103 | ### Changed
104 | - Dependencies update.
105 |
106 | ## [1.2.0-beta] - 2019-05-09
107 | ### Fixed
108 | - [#23](https://github.com/browserslist/browserslist-useragent-regexp/issues/23): Patch Chrome regexp to ignore Edge useragent.
109 |
110 | ## [1.1.1-beta] - 2019-05-08
111 | ### Fixed
112 | - [#11](https://github.com/browserslist/browserslist-useragent-regexp/issues/11): `rollup-plugin-shebang` -> `rollup-plugin-add-shebang`
113 |
114 | ## [1.1.0-beta] - 2019-05-01
115 | ### Summary
116 | The size of RegExp for `defaults` has decreased by ~63%.
117 | ### Changed
118 | - Result RegExps optimizations;
119 | - Removing of useless RegExps from result.
120 |
121 | ## [1.0.3-beta] - 2019-04-07
122 | ### Fixed
123 | - [#11](https://github.com/browserslist/browserslist-useragent-regexp/issues/11)
124 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 - present, TrigenSoftware
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-regexp
2 |
3 | [![ESM-only package][package]][package-url]
4 | [![NPM version][npm]][npm-url]
5 | [![Node version][node]][node-url]
6 | [![Dependencies status][deps]][deps-url]
7 | [![Install size][size]][size-url]
8 | [![Build status][build]][build-url]
9 | [![Coverage status][coverage]][coverage-url]
10 | [![Documentation badge][documentation]][documentation-url]
11 |
12 | [package]: https://img.shields.io/badge/package-ESM--only-ffe536.svg
13 | [package-url]: https://nodejs.org/api/esm.html
14 |
15 | [npm]: https://img.shields.io/npm/v/browserslist-useragent-regexp.svg
16 | [npm-url]: https://npmjs.com/package/browserslist-useragent-regexp
17 |
18 | [node]: https://img.shields.io/node/v/browserslist-useragent-regexp.svg
19 | [node-url]: https://nodejs.org
20 |
21 | [deps]: https://img.shields.io/librariesio/release/npm/browserslist-useragent-regexp
22 | [deps-url]: https://libraries.io/npm/browserslist-useragent-regexp/tree
23 |
24 | [size]: https://packagephobia.com/badge?p=browserslist-useragent-regexp
25 | [size-url]: https://packagephobia.com/result?p=browserslist-useragent-regexp
26 |
27 | [build]: https://img.shields.io/github/actions/workflow/status/browserslist/browserslist-useragent-regexp/tests.yml?branch=master
28 | [build-url]: https://github.com/browserslist/browserslist-useragent-regexp/actions
29 |
30 | [coverage]: https://img.shields.io/codecov/c/github/browserslist/browserslist-useragent-regexp.svg
31 | [coverage-url]: https://app.codecov.io/gh/browserslist/browserslist-useragent-regexp
32 |
33 | [documentation]: https://img.shields.io/badge/API-Documentation-2b7489.svg
34 | [documentation-url]: https://browserslist.github.io/browserslist-useragent-regexp
35 |
36 | A utility to compile [browserslist query](https://github.com/browserslist/browserslist#queries) to a regex to test browser useragent. Simplest example: you can detect supported browsers on client-side.
37 |
38 | 1) Create `.browserslistrc` config, for example like this:
39 |
40 | ```
41 | last 2 versions
42 | not dead
43 | ```
44 |
45 | 2) Add script to `package.json`:
46 |
47 | ```json
48 | {
49 | "scripts": {
50 | "supportedBrowsers": "echo \"export default $(browserslist-useragent-regexp --allowHigherVersions);\" > supportedBrowsers.js"
51 | }
52 | }
53 | ```
54 |
55 |
56 | for Windows users
57 |
58 | ```json
59 | {
60 | "scripts": {
61 | "supportedBrowsers": "(echo export default && browserslist-useragent-regexp --allowHigherVersions) > supportedBrowsers.js"
62 | }
63 | }
64 | ```
65 |
66 |
67 |
68 | 3) Run this script, to compile regex:
69 |
70 | ```bash
71 | pnpm supportedBrowsers
72 | # or
73 | npm run supportedBrowsers
74 | # or
75 | yarn supportedBrowsers
76 | ```
77 |
78 | `supportedBrowsers.js`:
79 |
80 | ```js
81 | export default /Edge?\/(10[5-9]|1[1-9]\d|[2-9]\d\d|\d{4,})(\.\d+|)(\.\d+|)|Firefox\/(10[4-9]|1[1-9]\d|[2-9]\d\d|\d{4,})\.\d+(\.\d+|)|Chrom(ium|e)\/(10[5-9]|1[1-9]\d|[2-9]\d\d|\d{4,})\.\d+(\.\d+|)|Maci.* Version\/(15\.([6-9]|\d{2,})|(1[6-9]|[2-9]\d|\d{3,})\.\d+)([,.]\d+|)( Mobile\/\w+|) Safari\/|Chrome.+OPR\/(9\d|\d{3,})\.\d+\.\d+|(CPU[ +]OS|iPhone[ +]OS|CPU[ +]iPhone|CPU IPhone OS|CPU iPad OS)[ +]+(15[._]([6-9]|\d{2,})|(1[6-9]|[2-9]\d|\d{3,})[._]\d+)([._]\d+|)|Opera Mini|Android:?[ /\-](10[6-9]|1[1-9]\d|[2-9]\d{2}|\d{4,})(\.\d+|)(\.\d+|)|Mobile Safari.+OPR\/(6[4-9]|[7-9]\d|\d{3,})\.\d+\.\d+|Android.+Firefox\/(10[5-9]|1[1-9]\d|[2-9]\d\d|\d{4,})\.\d+(\.\d+|)|Android.+Chrom(ium|e)\/(10[6-9]|1[1-9]\d|[2-9]\d\d|\d{4,})\.\d+(\.\d+|)|Android.+(UC? ?Browser|UCWEB|U3)[ /]?(13\.([4-9]|\d{2,})|(1[4-9]|[2-9]\d|\d{3,})\.\d+)\.\d+|SamsungBrowser\/(1[7-9]|[2-9]\d|\d{3,})\.\d+|Android.+MQQBrowser\/(13(\.([1-9]|\d{2,})|)|(1[4-9]|[2-9]\d|\d{3,})(\.\d+|))(\.\d+|)|K[Aa][Ii]OS\/(2\.([5-9]|\d{2,})|([3-9]|\d{2,})\.\d+)(\.\d+|)/;
82 | ```
83 |
84 | 4) Import regex from created file:
85 |
86 | ```js
87 | import supportedBrowsers from './supportedBrowsers.js';
88 |
89 | if (supportedBrowsers.test(navigator.userAgent)) {
90 | alert('Your browser is supported.');
91 | }
92 | ```
93 |
94 | ## Install
95 |
96 | ```bash
97 | pnpm add -D browserslist-useragent-regexp
98 | # or
99 | npm i -D browserslist-useragent-regexp
100 | # or
101 | yarn add -D browserslist-useragent-regexp
102 | ```
103 |
104 | ## Why?
105 |
106 | As was written in article ["Smart Bundling: Shipping legacy code to only legacy browsers"](https://www.smashingmagazine.com/2018/10/smart-bundling-legacy-code-browsers/): you can determinate, which bundle you should give to browser from server with [`browserslist-useragent`](https://github.com/browserslist/browserslist-useragent). But in this case you must have your own server with special logic. Now, with `browserslist-useragent-regexp`, you can move that to client-side.
107 |
108 | Development was inspired by [this proposal from Mathias Bynens](https://twitter.com/mathias/status/1105857829393653761).
109 |
110 | How to make differential resource loading and other optimizations with `browserslist-useragent-regexp` you can read in article ["Speed up with Browserslist"](https://dev.to/dangreen/speed-up-with-browserslist-30lh).
111 |
112 | [Demo](https://browserslist.github.io/browserslist-useragent-regexp/demo.html) ([sources](https://github.com/browserslist/browserslist-useragent-regexp/blob/7cf6afb7da2b6c77179abb8b8bd1bbcb61cf376a/docs/demo.html#L17-L29), [build script](https://github.com/browserslist/browserslist-useragent-regexp/blob/7cf6afb7da2b6c77179abb8b8bd1bbcb61cf376a/examples/buildDemo.js#L61-L74)).
113 |
114 | Also, testing useragents using generated regex [is faster](https://gist.github.com/dangreen/55c41072d8891efd3a772a4739d6cd9d) than using the `matchesUA` method from browserslist-useragent.
115 |
116 | ## CLI
117 |
118 | ```bash
119 | pnpm browserslist-useragent-regexp [query] [...options]
120 | # or
121 | npx browserslist-useragent-regexp [query] [...options]
122 | # or
123 | yarn exec -- browserslist-useragent-regexp [query] [...options]
124 | ```
125 |
126 | Also, short alias is available:
127 |
128 | ```bash
129 | pnpm bluare [query] [...options]
130 | ```
131 |
132 | | Option | Description | Default |
133 | |--------|-------------|---------|
134 | | query | Manually provide a browserslist query. Specifying this overrides the browserslist configuration specified in your project. | |
135 | | ‑‑help, -h | Print this message. | |
136 | | ‑‑verbose, -v | Print additional info about regexes. | |
137 | | ‑‑ignorePatch | Ignore differences in patch browser numbers. | `true` |
138 | | ‑‑ignoreMinor | Ignore differences in minor browser versions. | `false` |
139 | | ‑‑allowHigherVersions | For all the browsers in the browserslist query, return a match if the useragent version is equal to or higher than the one specified in browserslist. | `false` |
140 | | ‑‑allowZeroSubversions | Ignore match of patch or patch and minor, if they are 0. | `false` |
141 |
142 | ## JS API basics
143 |
144 | Module exposes two main methods:
145 |
146 | ### [getUserAgentRegexes(options)](https://browserslist.github.io/browserslist-useragent-regexp/functions/getUserAgentRegexes.html)
147 |
148 | Compile browserslist query to [regexes for each browser](#regex-info-object).
149 |
150 | ### [getUserAgentRegex(options)](https://browserslist.github.io/browserslist-useragent-regexp/functions/getUserAgentRegex.html)
151 |
152 | Compile browserslist query to one regex.
153 |
154 | > [Description of all methods you can find in Documentation.](https://browserslist.github.io/browserslist-useragent-regexp/index.html)
155 |
156 | #### Options
157 |
158 | | Option | Type | Default | Description |
159 | |--------|------|---------|-------------|
160 | | browsers | `string \| string[]` | — | Manually provide a browserslist query (or an array of queries). Specifying this overrides the browserslist configuration specified in your project. |
161 | | ignorePatch | `boolean` | `true` | Ignore differences in patch browser numbers. |
162 | | ignoreMinor | `boolean` | `false` | Ignore differences in minor browser versions. |
163 | | allowHigherVersions | `boolean` | `false` | For all the browsers in the browserslist query, return a match if the useragent version is equal to or higher than the one specified in browserslist. |
164 | | allowZeroSubversions | `boolean` | `false` | Ignore match of patch or patch and minor, if they are 0. |
165 |
166 | Any of the [`browserslist` API options](https://github.com/browserslist/browserslist#js-api) may also be provided.
167 |
168 | #### Regex info object
169 |
170 | | Property | Type | Description |
171 | |----------|------|-------------|
172 | | family | `string` | Browser family. |
173 | | requestVersions | `[number, number, number][]` | Versions provided by browserslist. |
174 | | regex | `RegExp` | Regex to match useragent with family and versions. |
175 | | sourceRegex | `RegExp` | Original useragent regex, without versions. |
176 | | version | `[number, number, number] \| null` | Useragent version of regex. |
177 | | minVersion | `[number, number, number] \| null` | Useragent min version of regex. |
178 | | maxVersion | `[number, number, number] \| null` | Useragent max version of regex. |
179 |
180 | ## Other
181 |
182 | - [Supported browsers](https://github.com/browserslist/browserslist-useragent#supported-browsers)
183 | - [Notes](https://github.com/browserslist/browserslist-useragent#notes)
184 | - [When querying for modern browsers](https://github.com/browserslist/browserslist-useragent#when-querying-for-modern-browsers)
185 |
--------------------------------------------------------------------------------
/examples/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "no-console": "off"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/examples/EXAMPLES.md:
--------------------------------------------------------------------------------
1 | # EXAMPLES
2 |
3 | ## Supported browsers
4 |
5 | 1) Create `.browserslistrc` config, for example like this:
6 |
7 | ```
8 | last 2 versions
9 | not dead
10 | ```
11 |
12 | 2) Add script to `package.json`:
13 |
14 | ```json
15 | {
16 | "scripts": {
17 | "supportedBrowsers": "echo \"export default $(browserslist-useragent-regexp --allowHigherVersions);\" > supportedBrowsers.js"
18 | }
19 | }
20 | ```
21 |
22 | 3) Run this script, to compile regex:
23 |
24 | ```bash
25 | npm run supportedBrowsers
26 | # or
27 | yarn supportedBrowsers
28 | ```
29 |
30 | 4) Import regex from created file:
31 |
32 | ```js
33 | import supportedBrowsers from './supportedBrowsers.js';
34 |
35 | if (supportedBrowsers.test(navigator.userAgent)) {
36 | alert('Your browser is supported.');
37 | }
38 | ```
39 |
40 | ## Unsupported browsers
41 |
42 | 1) Create `.browserslistrc` config, for example with [environments](https://github.com/browserslist/browserslist#environments), like this:
43 |
44 | ```
45 | [production]
46 | last 2 versions
47 | not dead
48 |
49 | [unsupported]
50 | dead
51 | ```
52 |
53 | 2) Add script to `package.json`:
54 |
55 | ```json
56 | {
57 | "scripts": {
58 | "supportedBrowsers": "echo \"export default $(BROWSERSLIST_ENV=unsupported browserslist-useragent-regexp);\" > supportedBrowsers.js"
59 | }
60 | }
61 | ```
62 |
63 | 3) Run this script, to compile regex:
64 |
65 | ```bash
66 | npm run supportedBrowsers
67 | # or
68 | yarn supportedBrowsers
69 | ```
70 |
71 | 4) Import regex from created file:
72 |
73 | ```js
74 | import unsupportedBrowsers from './unsupportedBrowsers.js';
75 |
76 | if (unsupportedBrowsers.test(navigator.userAgent)) {
77 | alert('Your browser is unsupported.');
78 | }
79 | ```
80 |
--------------------------------------------------------------------------------
/examples/buildDemo.js:
--------------------------------------------------------------------------------
1 | import {
2 | getUserAgentRegexes,
3 | getUserAgentRegex
4 | } from '../dist/index.js'
5 |
6 | function versionsToString(versions) {
7 | return versions.map(_ => (
8 | _[0] === Infinity
9 | ? 'all'
10 | : _.join('.')
11 | )).join(' ')
12 | }
13 |
14 | function renderStyles() {
15 | return ``
64 | }
65 |
66 | function renderScript() {
67 | const modernBrowsers = getUserAgentRegex({
68 | browsers: 'last 3 years and not dead',
69 | allowHigherVersions: true
70 | })
71 | const actualBrowsers = getUserAgentRegex({
72 | browsers: 'last 6 years and not dead',
73 | allowHigherVersions: true
74 | })
75 |
76 | return ``
111 | }
112 |
113 | function renderHtml(body) {
114 | return `
115 |
116 |
117 |
118 |
119 |
120 |
121 | DEMO
122 |
136 | ${renderScript()}
137 | ${renderStyles()}
138 |
139 |
140 | ${body}
141 |
142 | `
143 | }
144 |
145 | function renderUserAgentRegex({
146 | family,
147 | sourceRegex,
148 | regex,
149 | requestVersions,
150 | matchedVersions,
151 | version,
152 | minVersion,
153 | maxVersion
154 | }, query) {
155 | const regexString = regex.toString()
156 | const sourceRegexString = sourceRegex.toString()
157 | const requestVersionsString = versionsToString(requestVersions)
158 | const matchedVersionsString = versionsToString(matchedVersions)
159 | let regexBrowsersVersion = ''
160 |
161 | if (minVersion) {
162 | regexBrowsersVersion = minVersion.filter(isFinite).join('.')
163 | } else {
164 | regexBrowsersVersion = '...'
165 | }
166 |
167 | regexBrowsersVersion += ' - '
168 |
169 | if (maxVersion) {
170 | regexBrowsersVersion += maxVersion.filter(isFinite).join('.')
171 | } else {
172 | regexBrowsersVersion += '...'
173 | }
174 |
175 | return `
176 |
177 |
178 | |
179 | Family: |
180 | ${family} |
181 |
182 |
183 | Versions: | ${requestVersionsString} |
184 |
185 |
186 | Matched versions: | ${matchedVersionsString} |
187 |
188 |
189 | Source regex: | ${sourceRegexString} |
190 |
191 |
192 | ${version ? 'Source regex fixed browser version' : 'Source regex browsers versions'}: | ${version ? version.join('.') : regexBrowsersVersion} |
193 |
194 |
195 | Versioned regex: | ${regexString} |
196 |
`
197 | }
198 |
199 | function renderQuery(query) {
200 | const result = getUserAgentRegexes({
201 | browsers: query,
202 | allowHigherVersions: true
203 | })
204 |
205 | return `
206 |
207 |
208 |
209 | |
210 |
211 |
212 | ${query}
213 |
214 | |
215 |
216 |
217 |
218 | ${result.map(_ => renderUserAgentRegex(_, query)).join('\n')}
219 | `
220 | }
221 |
222 | function render(queries) {
223 | return renderHtml(`browserslist-useragent-regexp demo
224 |
225 |
226 | Useragent: |
227 | |
228 |
229 | Options: |
230 |
231 | {
232 | allowHigherVersions: true
233 | }
234 | |
235 |
236 |
237 |
238 | ${queries.map(_ => renderQuery(_)).join('\n')}
239 |
`)
240 | }
241 |
242 | console.log(render(['dead', 'defaults']))
243 |
--------------------------------------------------------------------------------
/examples/demojs/index.actual.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | function elevateElements(elements) {
4 | var firstElement = elements[0]
5 | var root = elements[0].parentElement
6 | var firstRootElement = root.firstElementChild
7 |
8 | if (firstElement === firstRootElement) {
9 | return
10 | }
11 |
12 | var fragment = document.createDocumentFragment()
13 |
14 | elements.forEach(function (element) {
15 | fragment.append(element)
16 | })
17 |
18 | root.insertBefore(fragment, firstRootElement)
19 | }
20 |
21 | document.getElementById('useragent').innerText = navigator.userAgent
22 |
23 | Array.from(document.querySelectorAll('[data-query]')).forEach(function (input) {
24 | var query = input.dataset.query
25 | var some = false
26 |
27 | Array.from(document.querySelectorAll('[data-for-query="' + query + '"]')).forEach(function (input) {
28 | var regex = input.dataset.regex
29 | var family = input.dataset.family
30 | var checked = new RegExp(regex).test(navigator.userAgent)
31 |
32 | input.checked = checked
33 | some = some || checked
34 |
35 | if (checked) {
36 | elevateElements(
37 | document.querySelectorAll('[data-group-family="' + query + ' ' + family + '"]')
38 | )
39 | }
40 | })
41 |
42 | input.checked = some
43 |
44 | if (some) {
45 | elevateElements(
46 | document.querySelectorAll('[data-group-query="' + query + '"]')
47 | )
48 | }
49 | })
50 |
--------------------------------------------------------------------------------
/examples/demojs/index.modern.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | function elevateElements(elements) {
4 | const firstElement = elements[0]
5 | const root = elements[0].parentElement
6 | const firstRootElement = root.firstElementChild
7 |
8 | if (firstElement === firstRootElement) {
9 | return
10 | }
11 |
12 | const fragment = document.createDocumentFragment()
13 |
14 | elements.forEach((element) => {
15 | fragment.append(element)
16 | })
17 |
18 | root.insertBefore(fragment, firstRootElement)
19 | }
20 |
21 | document.getElementById('useragent').innerText = navigator.userAgent
22 |
23 | document.querySelectorAll('[data-query]').forEach((input) => {
24 | const { query } = input.dataset
25 | let some = false
26 |
27 | document.querySelectorAll(`[data-for-query='${query}']`).forEach((input) => {
28 | const { regex, family } = input.dataset
29 | const checked = new RegExp(regex).test(navigator.userAgent)
30 |
31 | input.checked = checked
32 | some = some || checked
33 |
34 | if (checked) {
35 | elevateElements(
36 | document.querySelectorAll(`[data-group-family='${query} ${family}']`)
37 | )
38 | }
39 | })
40 |
41 | input.checked = some
42 |
43 | if (some) {
44 | elevateElements(
45 | document.querySelectorAll(`[data-group-query='${query}']`)
46 | )
47 | }
48 | })
49 |
--------------------------------------------------------------------------------
/examples/demojs/index.old.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | function forEach(elements, handler) {
4 | for (var i = 0, len = elements.length; i < len; i++) {
5 | handler(elements[i]);
6 | }
7 | }
8 |
9 | function findByAttribute(attribute, value) {
10 | var hasValue = typeof value !== 'undefined';
11 |
12 | if (typeof document.querySelectorAll === 'function') {
13 | return document.querySelectorAll(
14 | hasValue
15 | ? '[' + attribute + '=' + value + ']'
16 | : '[' + attribute + ']'
17 | );
18 | }
19 |
20 | var result = [];
21 |
22 | forEach(document.all, function (element) {
23 | if (!hasValue && element.hasAttribute(attribute)
24 | || hasValue && element.getAttribute(attribute) === value
25 | ) {
26 | result.push(element);
27 | }
28 | });
29 |
30 | return result;
31 | }
32 |
33 | function elevateElements(elements) {
34 | var firstElement = elements[0]
35 | var root = elements[0].parentElement
36 | var firstRootElement = root.children[0]
37 |
38 | if (firstElement === firstRootElement) {
39 | return
40 | }
41 |
42 | var fragment = document.createDocumentFragment()
43 |
44 | forEach(elements, function (element) {
45 | fragment.appendChild(element)
46 | })
47 |
48 | root.insertBefore(fragment, firstRootElement)
49 | }
50 |
51 | document.getElementById('useragent').innerText = navigator.userAgent;
52 |
53 | forEach(findByAttribute('data-query'), function (input) {
54 | var query = input.getAttribute('data-query');
55 | var some = false;
56 |
57 | forEach(findByAttribute('data-for-query', query), function (input) {
58 | var regex = input.getAttribute('data-regex')
59 | var family = input.getAttribute('data-family')
60 | var checked = new RegExp(regex).test(navigator.userAgent);
61 |
62 | input.checked = checked;
63 | some = some || checked;
64 |
65 | if (checked) {
66 | elevateElements(
67 | findByAttribute('data-group-family', query + ' ' + family)
68 | )
69 | }
70 | });
71 |
72 | input.checked = some;
73 |
74 | if (some) {
75 | elevateElements(
76 | findByAttribute('data-group-query', query)
77 | )
78 | }
79 | });
80 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "browserslist-useragent-regexp",
3 | "type": "module",
4 | "version": "4.1.3",
5 | "description": "A utility to compile browserslist query to a RegExp to test browser useragent.",
6 | "author": "dangreen",
7 | "license": "MIT",
8 | "funding": [
9 | "https://opencollective.com/browserslist-useragent-regexp",
10 | "https://ko-fi.com/dangreen"
11 | ],
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/browserslist/browserslist-useragent-regexp"
15 | },
16 | "bugs": {
17 | "url": "https://github.com/browserslist/browserslist-useragent-regexp/issues"
18 | },
19 | "keywords": [
20 | "browserslist",
21 | "useragent",
22 | "regexp"
23 | ],
24 | "engines": {
25 | "node": ">=14.0.0"
26 | },
27 | "bin": {
28 | "browserslist-useragent-regexp": "./dist/cli.js",
29 | "bluare": "./dist/cli.js"
30 | },
31 | "exports": "./src/index.ts",
32 | "publishConfig": {
33 | "types": "./dist/index.d.ts",
34 | "exports": {
35 | "types": "./dist/index.d.ts",
36 | "import": "./dist/index.js"
37 | },
38 | "directory": "package"
39 | },
40 | "files": [
41 | "dist"
42 | ],
43 | "scripts": {
44 | "clear:package": "del ./package",
45 | "clear": "del ./package ./dist ./coverage",
46 | "prepublishOnly": "run test build clear:package clean-publish",
47 | "postpublish": "pnpm clear:package",
48 | "emitDeclarations": "tsc --emitDeclarationOnly",
49 | "build": "run -p [ rollup -c ] emitDeclarations",
50 | "lint": "eslint './*{js,ts}' './src/**/*.{js,ts}' './examples/**/*.{js,ts}'",
51 | "test:unit": "vitest run --coverage",
52 | "test:unit:watch": "vitest watch",
53 | "test:types": "tsc --noEmit",
54 | "test:size": "size-limit",
55 | "test": "run -p lint test:unit",
56 | "build:demo": "node examples/buildDemo > ./docs/demo.html && cp -R examples/demojs/ docs/demojs/",
57 | "build:docs": "typedoc ./src --out ./docs --excludeExternals && touch docs/.nojekyll",
58 | "commit": "cz",
59 | "bumpVersion": "standard-version",
60 | "createGithubRelease": "simple-github-release",
61 | "release": "run bumpVersion [ git push origin master --tags ] createGithubRelease",
62 | "release:beta": "run [ bumpVersion --prerelease beta ] [ git push origin master --tags ] [ createGithubRelease --prerelease ]",
63 | "updateGitHooks": "simple-git-hooks"
64 | },
65 | "peerDependencies": {
66 | "browserslist": ">=4.0.0"
67 | },
68 | "dependencies": {
69 | "argue-cli": "^2.1.0",
70 | "easy-table": "^1.2.0",
71 | "picocolors": "^1.0.0",
72 | "regexp-tree": "^0.1.24",
73 | "ua-regexes-lite": "^1.2.1"
74 | },
75 | "devDependencies": {
76 | "@commitlint/cli": "^18.4.3",
77 | "@commitlint/config-conventional": "^18.4.3",
78 | "@commitlint/cz-commitlint": "^18.4.3",
79 | "@rollup/plugin-node-resolve": "^15.0.1",
80 | "@size-limit/file": "^11.0.0",
81 | "@swc/core": "^1.3.20",
82 | "@swc/helpers": "^0.5.1",
83 | "@trigen/browserslist-config": "8.0.0-alpha.30",
84 | "@trigen/eslint-config": "8.0.0-alpha.32",
85 | "@trigen/scripts": "8.0.0-alpha.30",
86 | "@types/node": "^20.10.1",
87 | "@vitest/coverage-v8": "^1.0.0",
88 | "browserslist": "^4.22.2",
89 | "clean-publish": "^4.0.1",
90 | "commitizen": "^4.2.5",
91 | "del-cli": "^5.0.0",
92 | "eslint": "^8.28.0",
93 | "nano-staged": "^0.8.0",
94 | "rollup": "^4.6.1",
95 | "rollup-plugin-add-shebang": "^0.3.1",
96 | "rollup-plugin-swc3": "^0.11.0",
97 | "simple-git-hooks": "^2.8.1",
98 | "simple-github-release": "^1.0.0",
99 | "size-limit": "^11.0.0",
100 | "standard-version": "^9.5.0",
101 | "typedoc": "^0.25.1",
102 | "typescript": "^5.1.3",
103 | "vite": "^5.0.4",
104 | "vitest": "^1.0.0"
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { swc } from 'rollup-plugin-swc3'
2 | import { nodeResolve } from '@rollup/plugin-node-resolve'
3 | import nodeEsm from '@trigen/browserslist-config/node-esm'
4 | import shebang from 'rollup-plugin-add-shebang'
5 | import pkg from './package.json' assert { type: 'json' }
6 |
7 | const extensions = ['.js', '.ts']
8 | const external = _ => /node_modules/.test(_) && !/@swc\/helpers/.test(_)
9 | const plugins = targets => [
10 | nodeResolve({
11 | extensions
12 | }),
13 | swc({
14 | tsconfig: false,
15 | jsc: {
16 | parser: {
17 | syntax: 'typescript'
18 | },
19 | externalHelpers: true
20 | },
21 | env: {
22 | targets
23 | },
24 | module: {
25 | type: 'es6'
26 | },
27 | sourceMaps: true
28 | })
29 | ]
30 |
31 | export default [
32 | {
33 | input: pkg.exports,
34 | plugins: plugins(nodeEsm.join(', ')),
35 | external,
36 | output: {
37 | file: pkg.publishConfig.exports.import,
38 | format: 'es',
39 | sourcemap: true
40 | }
41 | },
42 | {
43 | input: 'src/cli.ts',
44 | plugins: [...plugins(nodeEsm.join(', ')), shebang()],
45 | external: _ => !_.endsWith('src/cli.ts'),
46 | output: {
47 | file: 'dist/cli.js',
48 | format: 'es',
49 | sourcemap: true
50 | }
51 | }
52 | ]
53 |
--------------------------------------------------------------------------------
/src/browsers/browserslist.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import { parseBrowsersList } from './browserslist.js'
3 |
4 | describe('Browsers', () => {
5 | describe('browserslist', () => {
6 | describe('parseBrowsersList', () => {
7 | it('should correct parse browsers list', () => {
8 | const browsersList = [
9 | 'ie 10',
10 | 'chrome 11.12',
11 | 'opera 13.14.15',
12 | 'ios_saf 14.2-14.4',
13 | 'android 4.4.2-4.4.4'
14 | ]
15 | const browsers = [
16 | {
17 | family: 'ie',
18 | version: [
19 | 10,
20 | 0,
21 | 0
22 | ]
23 | },
24 | {
25 | family: 'chrome',
26 | version: [
27 | 11,
28 | 12,
29 | 0
30 | ]
31 | },
32 | {
33 | family: 'opera',
34 | version: [
35 | 13,
36 | 14,
37 | 15
38 | ]
39 | },
40 | {
41 | family: 'ios_saf',
42 | version: [
43 | 14,
44 | 2,
45 | 0
46 | ]
47 | },
48 | {
49 | family: 'ios_saf',
50 | version: [
51 | 14,
52 | 3,
53 | 0
54 | ]
55 | },
56 | {
57 | family: 'ios_saf',
58 | version: [
59 | 14,
60 | 4,
61 | 0
62 | ]
63 | },
64 | {
65 | family: 'android',
66 | version: [
67 | 4,
68 | 4,
69 | 2
70 | ]
71 | },
72 | {
73 | family: 'android',
74 | version: [
75 | 4,
76 | 4,
77 | 3
78 | ]
79 | },
80 | {
81 | family: 'android',
82 | version: [
83 | 4,
84 | 4,
85 | 4
86 | ]
87 | }
88 | ]
89 |
90 | expect(
91 | parseBrowsersList(browsersList)
92 | ).toEqual(
93 | browsers
94 | )
95 | })
96 | })
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/src/browsers/browserslist.ts:
--------------------------------------------------------------------------------
1 | import browserslist from 'browserslist'
2 | import {
3 | semverify,
4 | rangeSemver
5 | } from '../semver/index.js'
6 | import type {
7 | Browser,
8 | BrowserslistRequest
9 | } from './types.js'
10 |
11 | /**
12 | * Browsers strings to info objects.
13 | * @param browsersList - Browsers strings with family and version.
14 | * @returns Browser info objects.
15 | */
16 | export function parseBrowsersList(browsersList: string[]) {
17 | return browsersList.reduce((browsers, browser) => {
18 | const [family, versionString, versionStringTo] = browser.split(/ |-/)
19 | const version = semverify(versionString)
20 | const versions = !version
21 | ? []
22 | : versionStringTo
23 | ? rangeSemver(version, semverify(versionStringTo))
24 | : [version]
25 |
26 | return versions.reduce((browsers, semver) => {
27 | if (semver) {
28 | browsers.push({
29 | family,
30 | version: semver
31 | })
32 | }
33 |
34 | return browsers
35 | }, browsers)
36 | }, [])
37 | }
38 |
39 | /**
40 | * Request browsers list.
41 | * @param options - Options to get browsers list.
42 | * @returns Browser info objects.
43 | */
44 | export function getBrowsersList(options: BrowserslistRequest = {}) {
45 | const {
46 | browsers,
47 | ...browserslistOptions
48 | } = options
49 | const browsersList = browserslist(browsers, browserslistOptions)
50 | const parsedBrowsers = parseBrowsersList(browsersList)
51 |
52 | return parsedBrowsers
53 | }
54 |
--------------------------------------------------------------------------------
/src/browsers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types.js'
2 | export * from './utils.js'
3 | export * from './browserslist.js'
4 | export * from './optimize.js'
5 |
--------------------------------------------------------------------------------
/src/browsers/optimize.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import type { Browser } from './types.js'
3 | import type { Semver } from '../semver/types.js'
4 | import {
5 | mergeBrowserVersions,
6 | versionsListToRanges
7 | } from './optimize.js'
8 |
9 | describe('Browsers', () => {
10 | describe('optimize', () => {
11 | describe('mergeBrowserVersions', () => {
12 | it('should merge browsers versions', () => {
13 | const browsers: Browser[] = [
14 | {
15 | family: 'firefox',
16 | version: [
17 | 10,
18 | 0,
19 | 0
20 | ]
21 | },
22 | {
23 | family: 'firefox',
24 | version: [
25 | 11,
26 | 0,
27 | 0
28 | ]
29 | },
30 | {
31 | family: 'chrome',
32 | version: [
33 | 11,
34 | 12,
35 | 0
36 | ]
37 | }
38 | ]
39 | const mergedBrowsers = mergeBrowserVersions(browsers)
40 |
41 | expect(
42 | mergedBrowsers.get('firefox')?.length
43 | ).toBe(
44 | 2
45 | )
46 |
47 | expect(
48 | mergedBrowsers.get('chrome')?.length
49 | ).toBe(
50 | 1
51 | )
52 | })
53 | })
54 |
55 | describe('versionsListToRanges', () => {
56 | it('should collapse ranges', () => {
57 | const versions: Semver[] = [
58 | [
59 | 11,
60 | 0,
61 | 0
62 | ],
63 | [
64 | 12,
65 | 0,
66 | 0
67 | ],
68 | [
69 | 13,
70 | 0,
71 | 0
72 | ],
73 | [
74 | 14,
75 | 1,
76 | 1
77 | ],
78 | [
79 | 15,
80 | 1,
81 | 0
82 | ],
83 | [
84 | 15,
85 | 2,
86 | 0
87 | ],
88 | [
89 | 15,
90 | 3,
91 | 1
92 | ],
93 | [
94 | 15,
95 | 3,
96 | 2
97 | ],
98 | [
99 | 15,
100 | 3,
101 | 3
102 | ]
103 | ]
104 | const ranges = [
105 | [
106 | [11, 13],
107 | 0,
108 | 0
109 | ],
110 | [
111 | 14,
112 | 1,
113 | 1
114 | ],
115 | [
116 | 15,
117 | [1, 2],
118 | 0
119 | ],
120 | [
121 | 15,
122 | 3,
123 | [1, 3]
124 | ]
125 | ]
126 |
127 | expect(
128 | versionsListToRanges(versions)
129 | ).toEqual(
130 | ranges
131 | )
132 |
133 | expect(
134 | versionsListToRanges([
135 | [
136 | 10,
137 | 0,
138 | 0
139 | ]
140 | ])
141 | ).toEqual([
142 | [
143 | 10,
144 | 0,
145 | 0
146 | ]
147 | ])
148 |
149 | expect(
150 | versionsListToRanges([
151 | [
152 | 10,
153 | 0,
154 | 0
155 | ],
156 | [
157 | 14,
158 | 0,
159 | 0
160 | ]
161 | ])
162 | ).toEqual([
163 | [
164 | 10,
165 | 0,
166 | 0
167 | ],
168 | [
169 | 14,
170 | 0,
171 | 0
172 | ]
173 | ])
174 |
175 | expect(
176 | versionsListToRanges([
177 | [
178 | 10,
179 | 0,
180 | 0
181 | ],
182 | [
183 | 14,
184 | 1,
185 | 0
186 | ]
187 | ])
188 | ).toEqual([
189 | [
190 | 10,
191 | 0,
192 | 0
193 | ],
194 | [
195 | 14,
196 | 1,
197 | 0
198 | ]
199 | ])
200 |
201 | expect(
202 | versionsListToRanges([
203 | [
204 | 4,
205 | 0,
206 | 0
207 | ],
208 | [
209 | 10,
210 | 1,
211 | 0
212 | ],
213 | [
214 | 11,
215 | 1,
216 | 0
217 | ]
218 | ])
219 | ).toEqual([
220 | [
221 | 4,
222 | 0,
223 | 0
224 | ],
225 | [
226 | [10, 11],
227 | 1,
228 | 0
229 | ]
230 | ])
231 |
232 | expect(
233 | versionsListToRanges([
234 | [
235 | 10,
236 | 1,
237 | 0
238 | ],
239 | [
240 | 11,
241 | 1,
242 | 0
243 | ],
244 | [
245 | 15,
246 | 0,
247 | 0
248 | ],
249 | [
250 | 20,
251 | 0,
252 | 1
253 | ],
254 | [
255 | 21,
256 | 0,
257 | 1
258 | ]
259 | ])
260 | ).toEqual([
261 | [
262 | [10, 11],
263 | 1,
264 | 0
265 | ],
266 | [
267 | 15,
268 | 0,
269 | 0
270 | ],
271 | [
272 | [20, 21],
273 | 0,
274 | 1
275 | ]
276 | ])
277 |
278 | expect(
279 | versionsListToRanges([
280 | [
281 | 15,
282 | 1,
283 | 0
284 | ],
285 | [
286 | 15,
287 | 2,
288 | 1
289 | ],
290 | [
291 | 15,
292 | 3,
293 | 2
294 | ]
295 | ])
296 | ).toEqual([
297 | [
298 | 15,
299 | 1,
300 | 0
301 | ],
302 | [
303 | 15,
304 | 2,
305 | 1
306 | ],
307 | [
308 | 15,
309 | 3,
310 | 2
311 | ]
312 | ])
313 |
314 | expect(
315 | versionsListToRanges([
316 | [
317 | 15,
318 | 1,
319 | 0
320 | ],
321 | [
322 | 15,
323 | 2,
324 | 0
325 | ]
326 | ])
327 | ).toEqual([
328 | [
329 | 15,
330 | [1, 2],
331 | 0
332 | ]
333 | ])
334 |
335 | expect(
336 | versionsListToRanges([
337 | [
338 | 4,
339 | 0,
340 | 0
341 | ],
342 | [
343 | 5,
344 | 0,
345 | 0
346 | ],
347 | [
348 | 5,
349 | 1,
350 | 0
351 | ]
352 | ])
353 | ).toEqual([
354 | [
355 | [4, 5],
356 | 0,
357 | 0
358 | ],
359 | [
360 | 5,
361 | 1,
362 | 0
363 | ]
364 | ])
365 |
366 | expect(
367 | versionsListToRanges([
368 | [
369 | 4,
370 | 1,
371 | 0
372 | ],
373 | [
374 | 4,
375 | 1,
376 | 1
377 | ],
378 | [
379 | 4,
380 | 2,
381 | 0
382 | ]
383 | ])
384 | ).toEqual([
385 | [
386 | 4,
387 | 1,
388 | [0, 1]
389 | ],
390 | [
391 | 4,
392 | 2,
393 | 0
394 | ]
395 | ])
396 |
397 | expect(
398 | versionsListToRanges([
399 | [
400 | 4,
401 | 1,
402 | 0
403 | ],
404 | [
405 | 4,
406 | 1,
407 | 1
408 | ],
409 | [
410 | 4,
411 | 1,
412 | 3
413 | ]
414 | ])
415 | ).toEqual([
416 | [
417 | 4,
418 | 1,
419 | [0, 1]
420 | ],
421 | [
422 | 4,
423 | 1,
424 | 3
425 | ]
426 | ])
427 |
428 | expect(
429 | versionsListToRanges([
430 | [
431 | 4,
432 | 1,
433 | 0
434 | ],
435 | [
436 | 4,
437 | 1,
438 | 1
439 | ],
440 | [
441 | 4,
442 | 2,
443 | 1
444 | ]
445 | ])
446 | ).toEqual([
447 | [
448 | 4,
449 | 1,
450 | [0, 1]
451 | ],
452 | [
453 | 4,
454 | 2,
455 | 1
456 | ]
457 | ])
458 |
459 | expect(
460 | versionsListToRanges([
461 | [
462 | 4,
463 | 1,
464 | 0
465 | ],
466 | [
467 | 4,
468 | 1,
469 | 1
470 | ],
471 | [
472 | 4,
473 | 2,
474 | 2
475 | ]
476 | ])
477 | ).toEqual([
478 | [
479 | 4,
480 | 1,
481 | [0, 1]
482 | ],
483 | [
484 | 4,
485 | 2,
486 | 2
487 | ]
488 | ])
489 | })
490 |
491 | it('should do not touch single versions', () => {
492 | expect(
493 | versionsListToRanges([
494 | [
495 | 10,
496 | 0,
497 | 0
498 | ]
499 | ])
500 | ).toEqual([
501 | [
502 | 10,
503 | 0,
504 | 0
505 | ]
506 | ])
507 |
508 | expect(
509 | versionsListToRanges([
510 | [
511 | 10,
512 | 0,
513 | 0
514 | ],
515 | [
516 | 14,
517 | 0,
518 | 0
519 | ]
520 | ])
521 | ).toEqual([
522 | [
523 | 10,
524 | 0,
525 | 0
526 | ],
527 | [
528 | 14,
529 | 0,
530 | 0
531 | ]
532 | ])
533 | })
534 | })
535 | })
536 | })
537 |
--------------------------------------------------------------------------------
/src/browsers/optimize.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | Semver,
3 | RangedSemver
4 | } from '../semver/index.js'
5 | import { SemverPart } from '../semver/index.js'
6 | import { compareArrays } from '../utils/index.js'
7 | import type {
8 | Browser,
9 | BrowsersVersions
10 | } from './types.js'
11 | import { numbersToRanges } from './utils.js'
12 |
13 | /**
14 | * Merge browser info object to map with versions.
15 | * @param browsers - Browser info object to merge.
16 | * @returns Merged browsers map.
17 | */
18 | export function mergeBrowserVersions(browsers: Browser[]) {
19 | const merge: BrowsersVersions = new Map()
20 |
21 | browsers.forEach(({
22 | family,
23 | version
24 | }) => {
25 | const versions = merge.get(family)
26 |
27 | if (versions) {
28 | const strVersion = version.join('.')
29 |
30 | if (versions.every(_ => _.join('.') !== strVersion)) {
31 | versions.push(version)
32 | }
33 |
34 | return
35 | }
36 |
37 | merge.set(family, [version])
38 | })
39 |
40 | merge.forEach((versions) => {
41 | versions.sort((a, b) => {
42 | for (const i in a) {
43 | if (a[i] !== b[i]) {
44 | return a[i] - b[i]
45 | }
46 | }
47 |
48 | return 0
49 | })
50 | })
51 |
52 | return merge
53 | }
54 |
55 | /**
56 | * Versions to ranged versions.
57 | * @param versions - Semver versions list.
58 | * @returns Ranged versions list.
59 | */
60 | export function versionsListToRanges(versions: Semver[]) {
61 | if (versions.length < 2) {
62 | return versions
63 | }
64 |
65 | const max = versions.length + 1
66 | const ranges: RangedSemver[] = []
67 | let prev: number[] = null
68 | let current: number[] = versions[0]
69 | let major: number | number[] = [current[SemverPart.Major]]
70 | let minor: number | number[] = [current[SemverPart.Minor]]
71 | let patch: number | number[] = [current[SemverPart.Patch]]
72 | let part: SemverPart = null
73 |
74 | for (let i = 1; i < max; i++) {
75 | prev = versions[i - 1]
76 | current = versions[i] || []
77 |
78 | for (let p = SemverPart.Major; p <= SemverPart.Patch; p++) {
79 | if ((p === part || part === null)
80 | && prev[p] + 1 === current[p]
81 | && compareArrays(prev, current, p + 1)
82 | ) {
83 | part = p
84 |
85 | if (p === SemverPart.Major) {
86 | (major as number[]).push(current[SemverPart.Major])
87 | } else {
88 | major = current[SemverPart.Major]
89 | }
90 |
91 | if (p === SemverPart.Minor) {
92 | (minor as number[]).push(current[SemverPart.Minor])
93 | } else {
94 | minor = current[SemverPart.Minor]
95 | }
96 |
97 | if (p === SemverPart.Patch) {
98 | (patch as number[]).push(current[SemverPart.Patch])
99 | } else {
100 | patch = current[SemverPart.Patch]
101 | }
102 |
103 | break
104 | }
105 |
106 | if (part === p || prev[p] !== current[p]) {
107 | ranges.push([
108 | numbersToRanges(major),
109 | numbersToRanges(minor),
110 | numbersToRanges(patch)
111 | ])
112 | major = [current[SemverPart.Major]]
113 | minor = [current[SemverPart.Minor]]
114 | patch = [current[SemverPart.Patch]]
115 | part = null
116 | break
117 | }
118 | }
119 | }
120 |
121 | return ranges
122 | }
123 |
--------------------------------------------------------------------------------
/src/browsers/types.ts:
--------------------------------------------------------------------------------
1 | import type browserslist from 'browserslist'
2 | import type { Options } from 'browserslist'
3 | import type {
4 | Semver,
5 | RangedSemver
6 | } from '../semver/types.js'
7 |
8 | export interface Browser {
9 | family: string
10 | version: Semver
11 | }
12 |
13 | export interface BrowserslistRequest extends Options {
14 | browsers?: Parameters[0]
15 | }
16 |
17 | export type BrowsersVersions = Map
18 |
19 | export type RangedBrowsersVersions = Map
20 |
--------------------------------------------------------------------------------
/src/browsers/utils.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import { numbersToRanges } from './utils.js'
3 |
4 | describe('Browsers', () => {
5 | describe('utils', () => {
6 | describe('numbersToRanges', () => {
7 | it('should get first and last elements form array', () => {
8 | expect(
9 | numbersToRanges([
10 | 5,
11 | 6,
12 | 7
13 | ])
14 | ).toEqual(
15 | [5, 7]
16 | )
17 |
18 | expect(
19 | numbersToRanges([5, 6])
20 | ).toEqual(
21 | [5, 6]
22 | )
23 | })
24 |
25 | it('should get first element form one-element-array', () => {
26 | expect(
27 | numbersToRanges([8])
28 | ).toBe(
29 | 8
30 | )
31 | })
32 |
33 | it('should return number', () => {
34 | expect(
35 | numbersToRanges(10)
36 | ).toBe(
37 | 10
38 | )
39 | })
40 | })
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/src/browsers/utils.ts:
--------------------------------------------------------------------------------
1 | import type { SemverRange } from '../semver/index.js'
2 |
3 | /**
4 | * Array of numbers to array of first and last elements.
5 | * @param numbers - Array of numbers.
6 | * @returns Number or two numbers.
7 | */
8 | export function numbersToRanges(numbers: number | number[]): SemverRange {
9 | if (typeof numbers === 'number') {
10 | return numbers
11 | }
12 |
13 | if (numbers.length === 1) {
14 | return numbers[0]
15 | }
16 |
17 | return [numbers[0], numbers[numbers.length - 1]]
18 | }
19 |
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import {
3 | argv,
4 | read,
5 | end,
6 | alias,
7 | option,
8 | readOptions
9 | } from 'argue-cli'
10 | import colors from 'picocolors'
11 | import Table from 'easy-table'
12 | import type { Semver } from './index.js'
13 | import {
14 | getUserAgentRegex,
15 | getBrowsersList,
16 | mergeBrowserVersions,
17 | getRegexesForBrowsers,
18 | applyVersionsToRegexes,
19 | defaultOptions,
20 | compileRegexes,
21 | compileRegex
22 | } from './index.js'
23 |
24 | function versionsToString(versions: Semver[]) {
25 | return versions.map(_ => (
26 | _[0] === Infinity
27 | ? 'all'
28 | : _.join('.')
29 | )).join(' ')
30 | }
31 |
32 | const {
33 | help,
34 | verbose,
35 | ...regexOptions
36 | } = readOptions(
37 | option(alias('help', 'h'), Boolean),
38 | option(alias('verbose', 'v'), Boolean),
39 | option('ignorePatch', Boolean),
40 | option('ignoreMinor', Boolean),
41 | option('allowHigherVersions', Boolean),
42 | option('allowZeroSubversions', Boolean)
43 | )
44 |
45 | if (help) {
46 | end()
47 |
48 | const optionsTable = new Table()
49 |
50 | optionsTable.cell('Option', 'query')
51 | optionsTable.cell(
52 | 'Description',
53 | 'Manually provide a browserslist query.'
54 | + ' Specifying this overrides the browserslist configuration specified in your project.'
55 | )
56 | optionsTable.newRow()
57 |
58 | optionsTable.cell('Option', '--help, -h')
59 | optionsTable.cell('Description', 'Print this message.')
60 | optionsTable.newRow()
61 |
62 | optionsTable.cell('Option', '--verbose, -v')
63 | optionsTable.cell('Description', 'Print additional info about regexes.')
64 | optionsTable.newRow()
65 |
66 | optionsTable.cell('Option', '--ignorePatch')
67 | optionsTable.cell('Description', 'Ignore differences in patch browser numbers.')
68 | optionsTable.cell('Default', 'true')
69 | optionsTable.newRow()
70 |
71 | optionsTable.cell('Option', '--ignoreMinor')
72 | optionsTable.cell('Description', 'Ignore differences in minor browser versions.')
73 | optionsTable.cell('Default', 'false')
74 | optionsTable.newRow()
75 |
76 | optionsTable.cell('Option', '--allowHigherVersions')
77 | optionsTable.cell(
78 | 'Description',
79 | 'For all the browsers in the browserslist query,'
80 | + ' return a match if the user agent version is equal to or higher than the one specified in browserslist.'
81 | )
82 | optionsTable.cell('Default', 'false')
83 | optionsTable.newRow()
84 |
85 | optionsTable.cell('Option', '--allowZeroSubversions')
86 | optionsTable.cell('Description', 'Ignore match of patch or patch and minor, if they are 0.')
87 | optionsTable.cell('Default', 'false')
88 | optionsTable.newRow()
89 |
90 | console.log(`\nbrowserslist-useragent-regexp [query] [...options]\n\n${optionsTable.toString()}`)
91 | process.exit(0)
92 | }
93 |
94 | const query = argv.length
95 | ? read()
96 | : undefined
97 | const options = {
98 | browsers: query,
99 | ...defaultOptions,
100 | ...regexOptions
101 | }
102 |
103 | end()
104 |
105 | if (verbose) {
106 | const browsersList = getBrowsersList(options)
107 | const mergedBrowsers = mergeBrowserVersions(browsersList)
108 |
109 | console.log(
110 | colors.blue('\n> Browserslist\n')
111 | )
112 |
113 | const browsersTable = new Table()
114 |
115 | mergedBrowsers.forEach((versions, browser) => {
116 | browsersTable.cell('Browser', colors.yellow(browser))
117 |
118 | versions.forEach((version, i) => {
119 | if (version[0] === Infinity) {
120 | browsersTable.cell(`Version ${i}`, 'all')
121 | } else {
122 | browsersTable.cell(`Version ${i}`, version.join('.'))
123 | }
124 | })
125 |
126 | browsersTable.newRow()
127 | })
128 |
129 | console.log(browsersTable.print())
130 |
131 | const sourceRegexes = getRegexesForBrowsers(mergedBrowsers, options)
132 | const versionedRegexes = applyVersionsToRegexes(sourceRegexes, options)
133 | const regexes = compileRegexes(versionedRegexes)
134 | const regexesTable = new Table()
135 |
136 | console.log(
137 | colors.blue('\n> Regexes\n')
138 | )
139 |
140 | regexes.forEach(({
141 | family,
142 | requestVersions,
143 | matchedVersions,
144 | sourceRegex,
145 | version,
146 | minVersion,
147 | maxVersion,
148 | regex
149 | }, i) => {
150 | if (i > 0) {
151 | regexesTable.cell('', '')
152 | regexesTable.newRow()
153 | }
154 |
155 | const requestVersionsString = versionsToString(requestVersions)
156 | const matchedVersionsString = versionsToString(matchedVersions)
157 |
158 | regexesTable.cell('Name', colors.yellow('Family:'))
159 | regexesTable.cell('Value', family)
160 | regexesTable.newRow()
161 |
162 | regexesTable.cell('Name', colors.yellow('Versions:'))
163 | regexesTable.cell('Value', requestVersionsString)
164 | regexesTable.newRow()
165 |
166 | regexesTable.cell('Name', colors.yellow('Matched versions:'))
167 | regexesTable.cell('Value', matchedVersionsString)
168 | regexesTable.newRow()
169 |
170 | regexesTable.cell('Name', colors.yellow('Source regex:'))
171 | regexesTable.cell('Value', sourceRegex)
172 | regexesTable.newRow()
173 |
174 | if (version) {
175 | regexesTable.cell('Name', colors.yellow('Source regex fixed browser version:'))
176 | regexesTable.cell('Value', version.join('.'))
177 | regexesTable.newRow()
178 | } else {
179 | let regexBrowsersVersion = ''
180 |
181 | if (minVersion) {
182 | regexBrowsersVersion = minVersion.filter(isFinite).join('.')
183 | } else {
184 | regexBrowsersVersion = '...'
185 | }
186 |
187 | regexBrowsersVersion += ' - '
188 |
189 | if (maxVersion) {
190 | regexBrowsersVersion += maxVersion.filter(isFinite).join('.')
191 | } else {
192 | regexBrowsersVersion += '...'
193 | }
194 |
195 | regexesTable.cell('Name', colors.yellow('Source regex browsers versions:'))
196 | regexesTable.cell('Value', regexBrowsersVersion)
197 | regexesTable.newRow()
198 | }
199 |
200 | regexesTable.cell('Name', colors.yellow('Versioned regex:'))
201 | regexesTable.cell('Value', regex)
202 | regexesTable.newRow()
203 | })
204 |
205 | console.log(`${regexesTable.print()}\n`)
206 |
207 | const regex = compileRegex(regexes)
208 |
209 | console.log(regex)
210 | process.exit(0)
211 | }
212 |
213 | console.log(
214 | getUserAgentRegex(options)
215 | )
216 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './browsers/index.js'
2 | export * from './numbers/index.js'
3 | export * from './regex/index.js'
4 | export * from './semver/index.js'
5 | export * from './useragent/index.js'
6 | export * from './useragentRegex/index.js'
7 | export * from './utils/index.js'
8 | export * from './versions/index.js'
9 |
--------------------------------------------------------------------------------
/src/numbers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './range.js'
2 | export * from './ray.js'
3 | export * from './segment.js'
4 | export * from './utils.js'
5 |
--------------------------------------------------------------------------------
/src/numbers/range.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import { toString } from '../regex/index.js'
3 | import { rangeToRegex } from './range.js'
4 |
5 | describe('Numbers', () => {
6 | describe('range', () => {
7 | describe('rangeToRegex', () => {
8 | it('should return number pattern for Infinity (\'all\') version', () => {
9 | expect(
10 | toString(rangeToRegex(Infinity))
11 | ).toBe(
12 | '\\d+'
13 | )
14 | })
15 |
16 | it('should return ray pattern', () => {
17 | expect(
18 | toString(rangeToRegex(6))
19 | ).toBe(
20 | '([6-9]|\\d{2,})'
21 | )
22 | })
23 |
24 | it('should return segment pattern', () => {
25 | expect(
26 | toString(rangeToRegex(6, 8))
27 | ).toBe(
28 | '[6-8]'
29 | )
30 | })
31 | })
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/src/numbers/range.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DisjunctionCapturingGroupNode,
3 | NumberPatternNode
4 | } from '../regex/index.js'
5 | import { rayToNumberPatterns } from './ray.js'
6 | import { segmentToNumberPatterns } from './segment.js'
7 |
8 | /**
9 | * Get regex for given numeric range.
10 | * @param from - Range start.
11 | * @param to - Range end.
12 | * @returns Range pattern.
13 | */
14 | export function rangeToRegex(from: number, to = Infinity) {
15 | if (from === Infinity) {
16 | return NumberPatternNode()
17 | }
18 |
19 | const numberPatterns = to === Infinity
20 | ? rayToNumberPatterns(from)
21 | : segmentToNumberPatterns(from, to)
22 | const regex = DisjunctionCapturingGroupNode(numberPatterns)
23 |
24 | return regex
25 | }
26 |
--------------------------------------------------------------------------------
/src/numbers/ray.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import { toString } from '../regex/index.js'
3 | import {
4 | rayRangeDigitPattern,
5 | rayToNumberPatterns
6 | } from './ray.js'
7 |
8 | describe('Numbers', () => {
9 | describe('ray', () => {
10 | describe('rayRangeNumberPattern', () => {
11 | it('should return digit pattern', () => {
12 | expect(
13 | toString(rayRangeDigitPattern(0, true))
14 | ).toBe(
15 | '\\d'
16 | )
17 | })
18 |
19 | it('should return max digit', () => {
20 | expect(
21 | toString(rayRangeDigitPattern(9, true))
22 | ).toBe(
23 | '9'
24 | )
25 | })
26 |
27 | it('should return digits range', () => {
28 | expect(
29 | toString(rayRangeDigitPattern(1, true))
30 | ).toBe(
31 | '[1-9]'
32 | )
33 | })
34 |
35 | it('should start from next digit', () => {
36 | expect(
37 | toString(rayRangeDigitPattern(1, false))
38 | ).toBe(
39 | '[2-9]'
40 | )
41 | })
42 |
43 | it('should not return more than 9', () => {
44 | expect(
45 | toString(rayRangeDigitPattern(9, false))
46 | ).toBe(
47 | ''
48 | )
49 | })
50 | })
51 |
52 | describe('rayToNumberPatterns', () => {
53 | it('should return correct ray pattern', () => {
54 | expect(
55 | rayToNumberPatterns(0).map(toString)
56 | ).toEqual(['\\d+'])
57 |
58 | expect(
59 | rayToNumberPatterns(1).map(toString)
60 | ).toEqual(['[1-9]', '\\d{2,}'])
61 |
62 | expect(
63 | rayToNumberPatterns(9).map(toString)
64 | ).toEqual(['9', '\\d{2,}'])
65 |
66 | expect(
67 | rayToNumberPatterns(10).map(toString)
68 | ).toEqual(['[1-9]\\d', '\\d{3,}'])
69 |
70 | expect(
71 | rayToNumberPatterns(11).map(toString)
72 | ).toEqual([
73 | '1[1-9]',
74 | '[2-9]\\d',
75 | '\\d{3,}'
76 | ])
77 |
78 | expect(
79 | rayToNumberPatterns(19).map(toString)
80 | ).toEqual([
81 | '19',
82 | '[2-9]\\d',
83 | '\\d{3,}'
84 | ])
85 |
86 | expect(
87 | rayToNumberPatterns(20).map(toString)
88 | ).toEqual(['[2-9]\\d', '\\d{3,}'])
89 |
90 | expect(
91 | rayToNumberPatterns(21).map(toString)
92 | ).toEqual([
93 | '2[1-9]',
94 | '[3-9]\\d',
95 | '\\d{3,}'
96 | ])
97 |
98 | expect(
99 | rayToNumberPatterns(29).map(toString)
100 | ).toEqual([
101 | '29',
102 | '[3-9]\\d',
103 | '\\d{3,}'
104 | ])
105 |
106 | expect(
107 | rayToNumberPatterns(99).map(toString)
108 | ).toEqual(['99', '\\d{3,}'])
109 |
110 | expect(
111 | rayToNumberPatterns(100).map(toString)
112 | ).toEqual(['[1-9]\\d\\d', '\\d{4,}'])
113 |
114 | expect(
115 | rayToNumberPatterns(101).map(toString)
116 | ).toEqual([
117 | '10[1-9]',
118 | '1[1-9]\\d',
119 | '[2-9]\\d\\d',
120 | '\\d{4,}'
121 | ])
122 |
123 | expect(
124 | rayToNumberPatterns(111).map(toString)
125 | ).toEqual([
126 | '11[1-9]',
127 | '1[2-9]\\d',
128 | '[2-9]\\d\\d',
129 | '\\d{4,}'
130 | ])
131 |
132 | expect(
133 | rayToNumberPatterns(199).map(toString)
134 | ).toEqual([
135 | '199',
136 | '[2-9]\\d\\d',
137 | '\\d{4,}'
138 | ])
139 |
140 | expect(
141 | rayToNumberPatterns(200).map(toString)
142 | ).toEqual(['[2-9]\\d\\d', '\\d{4,}'])
143 |
144 | expect(
145 | rayToNumberPatterns(299).map(toString)
146 | ).toEqual([
147 | '299',
148 | '[3-9]\\d\\d',
149 | '\\d{4,}'
150 | ])
151 |
152 | expect(
153 | rayToNumberPatterns(999).map(toString)
154 | ).toEqual(['999', '\\d{4,}'])
155 |
156 | expect(
157 | rayToNumberPatterns(1000).map(toString)
158 | ).toEqual(['[1-9]\\d\\d\\d', '\\d{5,}'])
159 |
160 | expect(
161 | rayToNumberPatterns(56).map(toString)
162 | ).toEqual([
163 | '5[6-9]',
164 | '[6-9]\\d',
165 | '\\d{3,}'
166 | ])
167 | })
168 | })
169 | })
170 | })
171 |
--------------------------------------------------------------------------------
/src/numbers/ray.ts:
--------------------------------------------------------------------------------
1 | import type { Expression } from 'regexp-tree/ast'
2 | import {
3 | SimpleCharNode,
4 | CharacterClassNode,
5 | ClassRangeNode,
6 | RangeQuantifierNode,
7 | AlternativeNode,
8 | DigitPatternNode,
9 | NumberPatternNode
10 | } from '../regex/index.js'
11 | import { numberToDigits } from './utils.js'
12 |
13 | /**
14 | * Get digit pattern.
15 | * @param digit - Ray start.
16 | * @param includes - Include start digit or use next.
17 | * @returns Digit pattern.
18 | */
19 | export function rayRangeDigitPattern(digit: number, includes: boolean) {
20 | const rangeStart = digit + Number(!includes)
21 |
22 | if (rangeStart === 0) {
23 | return DigitPatternNode()
24 | }
25 |
26 | if (rangeStart === 9) {
27 | return SimpleCharNode('9')
28 | }
29 |
30 | if (rangeStart > 9) {
31 | return null
32 | }
33 |
34 | return CharacterClassNode(
35 | ClassRangeNode(
36 | SimpleCharNode(rangeStart),
37 | SimpleCharNode('9')
38 | )
39 | )
40 | }
41 |
42 | /**
43 | * Create numeric ray pattern.
44 | * @param from - Start from this number.
45 | * @returns Numeric ray pattern parts.
46 | */
47 | export function rayToNumberPatterns(from: number) {
48 | if (from === 0) {
49 | return [NumberPatternNode()]
50 | }
51 |
52 | const digits = numberToDigits(from)
53 | const digitsCount = digits.length
54 | const other = NumberPatternNode(
55 | RangeQuantifierNode(digitsCount + 1)
56 | )
57 | const zeros = digitsCount - 1
58 |
59 | if (from / Math.pow(10, zeros) === digits[0]) {
60 | return [
61 | AlternativeNode(
62 | rayRangeDigitPattern(digits[0], true),
63 | Array.from({
64 | length: zeros
65 | }, DigitPatternNode)
66 | ),
67 | other
68 | ]
69 | }
70 |
71 | const raysNumberPatterns = digits.reduce((topNodes, _, i) => {
72 | const ri = digitsCount - i - 1
73 | const d = i === 0
74 | let prev: Expression = SimpleCharNode('')
75 | const nodes = digits.reduce((nodes, digit, j) => {
76 | if (j < ri) {
77 | nodes.push(SimpleCharNode(digit))
78 | } else
79 | if (prev) {
80 | if (j > ri) {
81 | nodes.push(DigitPatternNode())
82 | } else {
83 | prev = rayRangeDigitPattern(digit, d)
84 |
85 | if (prev) {
86 | nodes.push(prev)
87 | } else {
88 | return []
89 | }
90 | }
91 | }
92 |
93 | return nodes
94 | }, [])
95 |
96 | if (nodes.length) {
97 | topNodes.push(nodes)
98 | }
99 |
100 | return topNodes
101 | }, [])
102 | const numberPatterns: Expression[] = raysNumberPatterns.map(_ => AlternativeNode(_))
103 |
104 | numberPatterns.push(other)
105 |
106 | return numberPatterns
107 | }
108 |
--------------------------------------------------------------------------------
/src/numbers/segment.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import { toString } from '../regex/index.js'
3 | import {
4 | segmentRangeNumberPattern,
5 | splitToDecadeRanges,
6 | splitCommonDiff,
7 | segmentToNumberPatterns
8 | } from './segment.js'
9 |
10 | describe('Numbers', () => {
11 | describe('segment', () => {
12 | describe('segmentRangeNumberPattern', () => {
13 | it('should return digit pattern', () => {
14 | expect(
15 | toString(segmentRangeNumberPattern(0, 9))
16 | ).toBe(
17 | '\\d'
18 | )
19 | })
20 |
21 | it('should return digit', () => {
22 | expect(
23 | toString(segmentRangeNumberPattern(7, 7))
24 | ).toBe(
25 | '7'
26 | )
27 | })
28 |
29 | it('should return digit enum', () => {
30 | expect(
31 | toString(segmentRangeNumberPattern(7, 8))
32 | ).toBe(
33 | '[78]'
34 | )
35 | })
36 |
37 | it('should return digit range', () => {
38 | expect(
39 | toString(segmentRangeNumberPattern(1, 8))
40 | ).toBe(
41 | '[1-8]'
42 | )
43 | })
44 |
45 | it('should not return invalid range', () => {
46 | expect(
47 | toString(segmentRangeNumberPattern(9, 6))
48 | ).toBe(
49 | ''
50 | )
51 | })
52 |
53 | it('should add zeros', () => {
54 | expect(
55 | toString(segmentRangeNumberPattern(3, 4, 3))
56 | ).toBe(
57 | '000[34]'
58 | )
59 | })
60 | })
61 |
62 | describe('splitToDecadeRanges', () => {
63 | it('should split to decades', () => {
64 | expect(
65 | splitToDecadeRanges(0, 9)
66 | ).toEqual([[0, 9]])
67 |
68 | expect(
69 | splitToDecadeRanges(0, 10)
70 | ).toEqual([[0, 9], [10, 10]])
71 |
72 | expect(
73 | splitToDecadeRanges(0, 99)
74 | ).toEqual([[0, 9], [10, 99]])
75 |
76 | expect(
77 | splitToDecadeRanges(0, 199)
78 | ).toEqual([
79 | [0, 9],
80 | [10, 99],
81 | [100, 199]
82 | ])
83 | })
84 | })
85 |
86 | describe('splitCommonDiff', () => {
87 | it('should not get common and diff', () => {
88 | expect(
89 | splitCommonDiff([1, 2], [3])
90 | ).toEqual(
91 | null
92 | )
93 |
94 | expect(
95 | splitCommonDiff([1], [3])
96 | ).toEqual(
97 | null
98 | )
99 | })
100 |
101 | it('should get common and diff', () => {
102 | expect(
103 | splitCommonDiff([1, 2], [1, 3])
104 | ).toEqual([
105 | '1',
106 | 2,
107 | 3
108 | ])
109 |
110 | expect(
111 | splitCommonDiff([
112 | 1,
113 | 6,
114 | 7,
115 | 1
116 | ], [
117 | 1,
118 | 6,
119 | 1,
120 | 2
121 | ])
122 | ).toEqual([
123 | '16',
124 | 71,
125 | 12
126 | ])
127 | })
128 |
129 | it('should work correctly, if there are common digits after first difference', () => {
130 | expect(
131 | splitCommonDiff([1, 0, 3], [1, 2, 3])
132 | ).toEqual(['1', 3, 23])
133 |
134 | expect(
135 | splitCommonDiff([1, 2, 5, 2], [1, 4, 5, 3])
136 | ).toEqual(['1', 252, 453])
137 | })
138 | })
139 |
140 | describe('segmentToNumberPatterns', () => {
141 | it('should return digit range', () => {
142 | expect(
143 | segmentToNumberPatterns(0, 7).map(toString)
144 | ).toEqual(
145 | ['[0-7]']
146 | )
147 | })
148 |
149 | it('should return digit pattern', () => {
150 | expect(
151 | segmentToNumberPatterns(7, 7).map(toString)
152 | ).toEqual(
153 | ['7']
154 | )
155 | })
156 |
157 | it('should return digit enum', () => {
158 | expect(
159 | segmentToNumberPatterns(7, 8).map(toString)
160 | ).toEqual(
161 | ['[78]']
162 | )
163 | })
164 |
165 | it('should correct handle range', () => {
166 | expect(
167 | segmentToNumberPatterns(11, 13).map(toString)
168 | ).toEqual(
169 | ['1[1-3]']
170 | )
171 |
172 | expect(
173 | segmentToNumberPatterns(32, 65).map(toString)
174 | ).toEqual(
175 | [
176 | '3[2-9]',
177 | '[45]\\d',
178 | '6[0-5]'
179 | ]
180 | )
181 |
182 | expect(
183 | segmentToNumberPatterns(32, 99).map(toString)
184 | ).toEqual(
185 | [
186 | '3[2-9]',
187 | '[4-9]\\d'
188 | ]
189 | )
190 | })
191 |
192 | it('should correct handle big range', () => {
193 | expect(
194 | segmentToNumberPatterns(32, 256).map(toString)
195 | ).toEqual([
196 | '3[2-9]',
197 | '[4-9]\\d',
198 | '1[0-9]\\d',
199 | '25[0-6]',
200 | '2[0-4]\\d'
201 | ])
202 | })
203 | })
204 | })
205 | })
206 |
--------------------------------------------------------------------------------
/src/numbers/segment.ts:
--------------------------------------------------------------------------------
1 | import type { Expression } from 'regexp-tree/ast'
2 | import { concat } from '../utils/index.js'
3 | import {
4 | AlternativeNode,
5 | SimpleCharNode,
6 | CharacterClassNode,
7 | ClassRangeNode,
8 | DisjunctionCapturingGroupNode,
9 | DigitPatternNode,
10 | optimizeSegmentNumberPatterns
11 | } from '../regex/index.js'
12 | import { numberToDigits } from './utils.js'
13 |
14 | /**
15 | * Get digit pattern.
16 | * @param from - Segment start.
17 | * @param to - Segment end.
18 | * @param zeros - Zeros to add as prefix.
19 | * @returns Digit pattern.
20 | */
21 | export function segmentRangeNumberPattern(from: number, to: number, zeros?: number) {
22 | if (to < from) {
23 | return null
24 | }
25 |
26 | const fromNode = SimpleCharNode(from)
27 | const toNode = SimpleCharNode(to)
28 | const zerosPrefix = typeof zeros === 'number' && zeros > 0
29 | ? Array.from({
30 | length: zeros
31 | }, () => SimpleCharNode(0))
32 | : []
33 | const addPrefix = zerosPrefix.length
34 | ? (node: Expression) => AlternativeNode(zerosPrefix, node)
35 | : (node: Expression) => node
36 |
37 | if (from === to) {
38 | return addPrefix(fromNode)
39 | }
40 |
41 | if (from === 0 && to === 9) {
42 | return addPrefix(DigitPatternNode())
43 | }
44 |
45 | if (to - from === 1) {
46 | return addPrefix(CharacterClassNode(
47 | fromNode,
48 | toNode
49 | ))
50 | }
51 |
52 | return addPrefix(CharacterClassNode(
53 | ClassRangeNode(fromNode, toNode)
54 | ))
55 | }
56 |
57 | /**
58 | * Split segment range to decade ranges.
59 | * @param from - Segment start.
60 | * @param to - Segment end.
61 | * @returns Ranges.
62 | */
63 | export function splitToDecadeRanges(from: number, to: number) {
64 | const ranges: [number, number][] = []
65 | let num = from
66 | let decade = 1
67 |
68 | do {
69 | decade *= 10
70 |
71 | if (num < decade) {
72 | ranges.push([num, Math.min(decade - 1, to)])
73 | num = decade
74 | }
75 | } while (decade <= to)
76 |
77 | return ranges
78 | }
79 |
80 | /**
81 | * Get common and diffs of two numbers (arrays of digits).
82 | * @param a - Digits.
83 | * @param b - Other digits.
84 | * @returns Common part and diffs.
85 | */
86 | export function splitCommonDiff(a: number[], b: number[]): [string, number, number] {
87 | const len = a.length
88 |
89 | if (len !== b.length || a[0] !== b[0]) {
90 | return null
91 | }
92 |
93 | let common = a[0].toString()
94 | let currA = 0
95 | let currB = 0
96 | let diffA = ''
97 | let diffB = ''
98 |
99 | for (let i = 1; i < len; i++) {
100 | currA = a[i]
101 | currB = b[i]
102 |
103 | if (currA === currB && diffA === '' && diffB === '') {
104 | common += currA
105 | } else {
106 | diffA += currA
107 | diffB += currB
108 | }
109 | }
110 |
111 | return [
112 | common,
113 | parseInt(diffA, 10),
114 | parseInt(diffB, 10)
115 | ]
116 | }
117 |
118 | /**
119 | * Get segment patterns.
120 | * @param from - Segment start.
121 | * @param to - Segment end.
122 | * @param digitsInNumber - How many digits should be en number. Will be filled by zeros.
123 | * @returns Segment patterns.
124 | */
125 | export function segmentToNumberPatterns(from: number, to: number, digitsInNumber = 0): Expression[] {
126 | const fromDigits = numberToDigits(from)
127 | const digitsCount = fromDigits.length
128 |
129 | if (from < 10 && to < 10 || from === to) {
130 | const zeros = digitsInNumber - digitsCount
131 |
132 | return [segmentRangeNumberPattern(from, to, zeros)]
133 | }
134 |
135 | const toDigits = numberToDigits(to)
136 |
137 | if (digitsCount !== toDigits.length) {
138 | const decadeRanges = splitToDecadeRanges(from, to)
139 | const parts = concat(
140 | decadeRanges.map(([from, to]) => segmentToNumberPatterns(from, to, digitsInNumber))
141 | )
142 |
143 | return parts
144 | }
145 |
146 | const commonStart = splitCommonDiff(fromDigits, toDigits)
147 |
148 | if (Array.isArray(commonStart)) {
149 | const [
150 | common,
151 | from,
152 | to
153 | ] = commonStart
154 | const digitsInNumber = digitsCount - common.length
155 | const diffParts = segmentToNumberPatterns(from, to, digitsInNumber)
156 |
157 | return [
158 | AlternativeNode(
159 | Array.from(common, SimpleCharNode),
160 | DisjunctionCapturingGroupNode(diffParts)
161 | )
162 | ]
163 | }
164 |
165 | const range = Array.from({
166 | length: digitsCount - 1
167 | })
168 | const middleSegment = segmentRangeNumberPattern(
169 | fromDigits[0] + 1,
170 | toDigits[0] - 1
171 | )
172 | const parts = [
173 | ...range.map((_, i) => {
174 | const ri = digitsCount - i - 1
175 | const d = Number(i > 0)
176 |
177 | return AlternativeNode(
178 | fromDigits.map((digit, j) => {
179 | if (j < ri) {
180 | return SimpleCharNode(digit)
181 | }
182 |
183 | if (j > ri) {
184 | return segmentRangeNumberPattern(0, 9)
185 | }
186 |
187 | return segmentRangeNumberPattern(digit + d, 9)
188 | })
189 | )
190 | }),
191 | // but output more readable
192 | ...middleSegment
193 | ? [
194 | AlternativeNode(
195 | middleSegment,
196 | Array.from({
197 | length: digitsCount - 1
198 | }, () => DigitPatternNode())
199 | )
200 | ]
201 | : [],
202 | ...range.map((_, i) => {
203 | const ri = digitsCount - i - 1
204 | const d = Number(i > 0)
205 |
206 | return AlternativeNode(
207 | toDigits.map((digit, j) => {
208 | if (j < ri) {
209 | return SimpleCharNode(digit)
210 | }
211 |
212 | if (j > ri) {
213 | return segmentRangeNumberPattern(0, 9)
214 | }
215 |
216 | return segmentRangeNumberPattern(0, digit - d)
217 | })
218 | )
219 | })
220 | ]
221 |
222 | return optimizeSegmentNumberPatterns(parts)
223 | }
224 |
--------------------------------------------------------------------------------
/src/numbers/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Transform number to digits array.
3 | * @param num - Target number.
4 | * @returns Digits array.
5 | */
6 | export function numberToDigits(num: string | number) {
7 | return Array.from(num.toString(), Number)
8 | }
9 |
--------------------------------------------------------------------------------
/src/regex/index.ts:
--------------------------------------------------------------------------------
1 | export * from './nodes.js'
2 | export * from './regex.js'
3 | export * from './utils.js'
4 | export * from './optimize.js'
5 |
--------------------------------------------------------------------------------
/src/regex/nodes.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | AstRegExp,
3 | Alternative,
4 | Expression,
5 | Char,
6 | SimpleChar,
7 | SpecialChar,
8 | ClassRange,
9 | CharacterClass,
10 | Quantifier,
11 | SimpleQuantifier,
12 | RangeQuantifier,
13 | CapturingGroup,
14 | Repetition,
15 | Disjunction
16 | } from 'regexp-tree/ast'
17 | import { concat } from '../utils/index.js'
18 |
19 | export function AstRegExpNode(body: Expression): AstRegExp {
20 | return {
21 | type: 'RegExp',
22 | body,
23 | flags: ''
24 | }
25 | }
26 |
27 | export function AlternativeNode(
28 | ...expressions: (Expression | Expression[])[]
29 | ): Alternative | Expression {
30 | const exps = concat(expressions).filter(Boolean)
31 |
32 | if (exps.length === 1) {
33 | return exps[0]
34 | }
35 |
36 | return {
37 | type: 'Alternative',
38 | expressions: exps
39 | }
40 | }
41 |
42 | export function SimpleCharNode(value: string | number): SimpleChar {
43 | return {
44 | type: 'Char',
45 | kind: 'simple',
46 | value: String(value),
47 | codePoint: NaN
48 | }
49 | }
50 |
51 | export function MetaCharNode(value: string): SpecialChar {
52 | return {
53 | type: 'Char',
54 | kind: 'meta',
55 | value,
56 | codePoint: NaN
57 | }
58 | }
59 |
60 | export function ClassRangeNode(from: Char, to: Char): ClassRange {
61 | return {
62 | type: 'ClassRange',
63 | from,
64 | to
65 | }
66 | }
67 |
68 | export function CharacterClassNode(
69 | ...expressions: (Char | ClassRange | (Char | ClassRange)[])[]
70 | ): CharacterClass {
71 | return {
72 | type: 'CharacterClass',
73 | expressions: concat(expressions).filter(Boolean)
74 | }
75 | }
76 |
77 | export function SimpleQuantifierNode(kind: SimpleQuantifier['kind']): SimpleQuantifier {
78 | return {
79 | type: 'Quantifier',
80 | kind,
81 | greedy: true
82 | }
83 | }
84 |
85 | export function RangeQuantifierNode(from: number, to?: number): RangeQuantifier {
86 | return {
87 | type: 'Quantifier',
88 | kind: 'Range',
89 | from,
90 | to,
91 | greedy: true
92 | }
93 | }
94 |
95 | export function CapturingGroupNode(expression: Expression): CapturingGroup {
96 | return {
97 | type: 'Group',
98 | capturing: true,
99 | expression,
100 | number: null
101 | }
102 | }
103 |
104 | export function RepetitionNode(expression: Expression, quantifier: Quantifier): Repetition {
105 | return {
106 | type: 'Repetition',
107 | expression,
108 | quantifier
109 | }
110 | }
111 |
112 | export function DisjunctionNode(...expressions: (Expression | Expression[])[]): Disjunction | Expression {
113 | const exprs = concat(expressions).filter(Boolean)
114 |
115 | if (exprs.length === 1) {
116 | return exprs[0]
117 | }
118 |
119 | const disjunction: Disjunction = {
120 | type: 'Disjunction',
121 | left: null,
122 | right: exprs.pop()
123 | }
124 |
125 | exprs.reduceRight((disjunction, expr, i) => {
126 | if (i === 0) {
127 | disjunction.left = expr
128 |
129 | return disjunction
130 | }
131 |
132 | disjunction.left = {
133 | type: 'Disjunction',
134 | left: null,
135 | right: expr
136 | }
137 |
138 | return disjunction.left
139 | }, disjunction)
140 |
141 | return disjunction
142 | }
143 |
144 | export function DisjunctionCapturingGroupNode(...expressions: (Expression | Expression[])[]) {
145 | const expr = DisjunctionNode(...expressions)
146 |
147 | if (expr.type === 'Disjunction') {
148 | return CapturingGroupNode(expr)
149 | }
150 |
151 | return expr
152 | }
153 |
154 | export function DigitPatternNode() {
155 | return MetaCharNode('\\d')
156 | }
157 |
158 | export function NumberPatternNode(
159 | quantifier: Quantifier = SimpleQuantifierNode('+')
160 | ) {
161 | const numberPattern = RepetitionNode(
162 | DigitPatternNode(),
163 | quantifier
164 | )
165 |
166 | return numberPattern
167 | }
168 |
169 | export function NumberCharsNode(value: number) {
170 | return AlternativeNode(
171 | Array.from(String(value), SimpleCharNode)
172 | )
173 | }
174 |
--------------------------------------------------------------------------------
/src/regex/optimize.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import { toString } from './regex.js'
3 | import { optimizeRegex } from './optimize.js'
4 |
5 | describe('Regex', () => {
6 | describe('optimizeRegex', () => {
7 | it('should remove braces', () => {
8 | expect(
9 | toString(optimizeRegex('(Family)foo(NotFamily)(bar)'))
10 | ).toBe(
11 | '/FamilyfooNotFamilybar/'
12 | )
13 |
14 | expect(
15 | toString(optimizeRegex('(?:Family)'))
16 | ).toBe(
17 | '/Family/'
18 | )
19 |
20 | expect(
21 | toString(optimizeRegex('(Family)foo(NotFamily|bar)'))
22 | ).toBe(
23 | '/Familyfoo(NotFamily|bar)/'
24 | )
25 |
26 | expect(
27 | toString(optimizeRegex('(Family)(foo)?(?=NotFamily)'))
28 | ).toBe(
29 | '/Family(foo)?(?=NotFamily)/'
30 | )
31 | })
32 |
33 | it('should remove top braces', () => {
34 | expect(
35 | toString(optimizeRegex('/(chrome|safari)/'))
36 | ).toBe(
37 | '/chrome|safari/'
38 | )
39 |
40 | expect(
41 | toString(optimizeRegex('/(chrome|safari) post/'))
42 | ).toBe(
43 | '/(chrome|safari) post/'
44 | )
45 | })
46 |
47 | it('should make groups uncapturable', () => {
48 | expect(
49 | toString(optimizeRegex('(?:Family)+'))
50 | ).toBe(
51 | '/(Family)+/'
52 | )
53 | })
54 |
55 | it('should merge disjunctions', () => {
56 | expect(
57 | toString(optimizeRegex('/chrome|(safari|firefox)|opera/'))
58 | ).toBe(
59 | '/chrome|safari|firefox|opera/'
60 | )
61 |
62 | expect(
63 | toString(optimizeRegex('/(chrome|(safari|firefox)|opera)/'))
64 | ).toBe(
65 | '/chrome|safari|firefox|opera/'
66 | )
67 |
68 | expect(
69 | toString(optimizeRegex('/(chrome|(safari|(firefox|edge))|opera)/'))
70 | ).toBe(
71 | '/chrome|safari|firefox|edge|opera/'
72 | )
73 | })
74 |
75 | it('should remove unnecessary escapes in ranges', () => {
76 | expect(
77 | toString(optimizeRegex('[\\.\\[]'))
78 | ).toBe(
79 | '/[.[]/'
80 | )
81 | })
82 | })
83 | })
84 |
--------------------------------------------------------------------------------
/src/regex/optimize.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | AstRegExp,
3 | AstNode,
4 | Expression
5 | } from 'regexp-tree/ast'
6 | import RegexpTree from 'regexp-tree'
7 | import {
8 | isCharNode,
9 | isDigitRangeNode,
10 | parseRegex,
11 | toString
12 | } from './regex.js'
13 |
14 | /**
15 | * Optimize regex.
16 | * @param regex - Regex to optimize.
17 | * @returns Optimized regex string.
18 | */
19 | export function optimizeRegex(regex: string | RegExp | AstRegExp): AstRegExp
20 | export function optimizeRegex(regex: T): T
21 |
22 | export function optimizeRegex(regex: string | RegExp | AstNode) {
23 | // Optimization requires filled codePoints
24 | const regexAst = RegexpTree.optimize(parseRegex(toString(regex))).getAST()
25 |
26 | RegexpTree.traverse(regexAst, {
27 | Group(nodePath) {
28 | const {
29 | parent,
30 | node
31 | } = nodePath
32 | const { expression } = node
33 |
34 | node.capturing = true
35 |
36 | if (parent.type === 'RegExp'
37 | || expression.type !== 'Disjunction' && parent.type !== 'Repetition'
38 | || expression.type === 'Disjunction' && parent.type === 'Disjunction'
39 | ) {
40 | nodePath.replace(nodePath.node.expression)
41 | }
42 | }
43 | })
44 |
45 | return regexAst
46 | }
47 |
48 | /**
49 | * Merge digits patterns if possible.
50 | * @param a
51 | * @param b
52 | * @returns Merged node.
53 | */
54 | export function mergeDigits(a: Expression, b: Expression) {
55 | if (isCharNode(a) && isCharNode(b) && a.value === b.value) {
56 | return b
57 | }
58 |
59 | if (
60 | isCharNode(a, /\d/) && isDigitRangeNode(b)
61 | && Number(b.expressions[0].from.value) - Number(a.value) === 1
62 | ) {
63 | return {
64 | ...b,
65 | expressions: [
66 | {
67 | ...b.expressions[0],
68 | from: a
69 | }
70 | ]
71 | }
72 | }
73 |
74 | if (
75 | isDigitRangeNode(a) && isCharNode(b, /\d/)
76 | && Number(b.value) - Number(a.expressions[0].to.value) === 1
77 | ) {
78 | return {
79 | ...a,
80 | expressions: [
81 | {
82 | ...a.expressions[0],
83 | to: b
84 | }
85 | ]
86 | }
87 | }
88 |
89 | return null
90 | }
91 |
92 | /**
93 | * Optimize segment number patterns.
94 | * @param patterns
95 | * @returns Optimized segment number patterns.
96 | */
97 | export function optimizeSegmentNumberPatterns(patterns: Expression[]) {
98 | return patterns.reduce((patterns, node) => {
99 | const prevNode = patterns[patterns.length - 1]
100 |
101 | if (prevNode
102 | && node.type === 'Alternative' && prevNode.type === 'Alternative'
103 | && node.expressions.length === prevNode.expressions.length
104 | ) {
105 | const merged = prevNode.expressions.reduceRight((exps, exp, i) => {
106 | if (!exps) {
107 | return exps
108 | }
109 |
110 | const merged = mergeDigits(exp, node.expressions[i])
111 |
112 | if (merged) {
113 | exps.unshift(merged)
114 | } else {
115 | return null
116 | }
117 |
118 | return exps
119 | }, [])
120 |
121 | if (merged) {
122 | node.expressions = merged
123 | patterns.pop()
124 | }
125 | }
126 |
127 | patterns.push(node)
128 |
129 | return patterns
130 | }, [])
131 | }
132 |
--------------------------------------------------------------------------------
/src/regex/regex.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | AstRegExp,
3 | AstNode,
4 | Expression,
5 | Char,
6 | CharacterClass,
7 | ClassRange
8 | } from 'regexp-tree/ast'
9 | import RegexpTree from 'regexp-tree'
10 |
11 | /**
12 | * Check node whether is number pattern.
13 | * @param node - AST node to check.
14 | * @returns Is number pattern or not.
15 | */
16 | export function isNumberPatternNode(node: AstNode) {
17 | if (node.type === 'Group' && node.expression.type === 'Repetition') {
18 | const {
19 | expression,
20 | quantifier
21 | } = node.expression
22 |
23 | return expression.type === 'Char' && expression.value === '\\d'
24 | && quantifier.kind === '+' && quantifier.greedy
25 | }
26 |
27 | return false
28 | }
29 |
30 | /**
31 | * Check node whether is char node.
32 | * @param node - AST node to check.
33 | * @param value - Value to compare.
34 | * @returns Is char node or not.
35 | */
36 | export function isCharNode(node: AstNode, value?: string | number | RegExp): node is Char {
37 | if (node && node.type === 'Char') {
38 | return typeof value === 'undefined'
39 | || value instanceof RegExp && value.test(node.value)
40 | || String(value) === node.value
41 | }
42 |
43 | return false
44 | }
45 |
46 | /**
47 | * Check node whether is digit range.
48 | * @param node - AST node to check.
49 | * @returns Is digit range or not.
50 | */
51 | export function isDigitRangeNode(node: AstNode): node is CharacterClass & { expressions: [ClassRange] } {
52 | if (node.type === 'CharacterClass' && node.expressions.length === 1) {
53 | const [expression] = node.expressions
54 |
55 | return expression.type === 'ClassRange'
56 | && isCharNode(expression.from, /\d/)
57 | && isCharNode(expression.to, /\d/)
58 | }
59 |
60 | return false
61 | }
62 |
63 | /**
64 | * Check node whether is expression.
65 | * @param node - AST node to check.
66 | * @returns Is expression node or not.
67 | */
68 | export function isExpressionNode(node: AstNode): node is Expression {
69 | return node.type !== 'RegExp' && node.type !== 'ClassRange' && node.type !== 'Quantifier'
70 | }
71 |
72 | /**
73 | * Parse regex from string or regex.
74 | * @param regex - Target regex or string.
75 | * @returns Parsed regex.
76 | */
77 | export function parseRegex(regex: string | RegExp | AstRegExp): AstRegExp
78 | export function parseRegex(regex: string | RegExp | AstNode): AstNode
79 | export function parseRegex(regex: T): T
80 |
81 | export function parseRegex(regex: string | RegExp | AstNode) {
82 | return typeof regex === 'string'
83 | ? RegexpTree.parse(regex.replace(/^([^/])/, '/$1').replace(/([^/])$/, '$1/'))
84 | : regex instanceof RegExp
85 | ? RegexpTree.parse(regex)
86 | : regex
87 | }
88 |
89 | /**
90 | * Get regex from string or AST.
91 | * @param src - String or AST.
92 | * @returns RegExp.
93 | */
94 | export function toRegex(src: string | AstRegExp) {
95 | return typeof src === 'string'
96 | ? new RegExp(src)
97 | : new RegExp(RegexpTree.generate(src.body), src.flags)
98 | }
99 |
100 | /**
101 | * Get string from regex or AST.
102 | * @param src - RegExp or AST.
103 | * @returns String.
104 | */
105 | export function toString(src: string | RegExp | AstNode | null) {
106 | return typeof src === 'string'
107 | ? src
108 | : src instanceof RegExp
109 | ? src.toString()
110 | : RegexpTree.generate(src)
111 | }
112 |
--------------------------------------------------------------------------------
/src/regex/utils.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | SpecificTraversalHandlers,
3 | TraversalCallbacks,
4 | TraversalCallback,
5 | TraversalHandlers
6 | } from 'regexp-tree'
7 |
8 | export interface Visitors extends SpecificTraversalHandlers {
9 | every?: TraversalCallback | TraversalCallbacks
10 | }
11 |
12 | const classes = [
13 | 'RegExp',
14 | 'Disjunction',
15 | 'Alternative',
16 | 'Assertion',
17 | 'Char',
18 | 'CharacterClass',
19 | 'ClassRange',
20 | 'Backreference',
21 | 'Group',
22 | 'Repetition',
23 | 'Quantifier'
24 | ] as const
25 |
26 | /**
27 | * Create traversal visitors.
28 | * @param visitors
29 | * @returns Traversal handlers.
30 | */
31 | export function visitors(visitors: Visitors): TraversalHandlers {
32 | const { every } = visitors
33 |
34 | if (!every) {
35 | return visitors
36 | }
37 |
38 | if (typeof every === 'function') {
39 | return {
40 | // eslint-disable-next-line @typescript-eslint/naming-convention
41 | '*': every,
42 | ...visitors
43 | }
44 | }
45 |
46 | return classes.reduce>((newVisitors, className) => {
47 | const visitor = visitors[className] as TraversalCallback | TraversalCallbacks
48 | const visitorPre = visitor
49 | ? 'pre' in visitor
50 | ? visitor.pre
51 | : visitor as TraversalCallback
52 | : null
53 | const visitorPost = visitor
54 | ? 'post' in visitor
55 | ? visitor.post
56 | : null
57 | : null
58 |
59 | newVisitors[className] = {
60 | pre(nodePath) {
61 | if (every.pre(nodePath) !== false && visitorPre) {
62 | return visitorPre(nodePath)
63 | }
64 |
65 | return true
66 | },
67 | post(nodePath) {
68 | if (every.post(nodePath) !== false && visitorPost) {
69 | return visitorPost(nodePath)
70 | }
71 |
72 | return true
73 | }
74 | }
75 |
76 | return newVisitors
77 | }, {})
78 | }
79 |
--------------------------------------------------------------------------------
/src/semver/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types.js'
2 | export * from './semver.js'
3 |
--------------------------------------------------------------------------------
/src/semver/semver.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import type { Semver } from './types.js'
3 | import {
4 | semverify,
5 | compareSemvers,
6 | getRequiredSemverPartsCount,
7 | rangeSemver
8 | } from './semver.js'
9 |
10 | describe('Semver', () => {
11 | describe('semverify', () => {
12 | it('should semverify strings', () => {
13 | expect(
14 | semverify('10')
15 | ).toEqual(
16 | [
17 | 10,
18 | 0,
19 | 0
20 | ]
21 | )
22 |
23 | expect(
24 | semverify('11.1')
25 | ).toEqual(
26 | [
27 | 11,
28 | 1,
29 | 0
30 | ]
31 | )
32 |
33 | expect(
34 | semverify('13.0.1')
35 | ).toEqual(
36 | [
37 | 13,
38 | 0,
39 | 1
40 | ]
41 | )
42 | })
43 |
44 | it('should semverify array', () => {
45 | expect(
46 | semverify([10])
47 | ).toEqual(
48 | [
49 | 10,
50 | 0,
51 | 0
52 | ]
53 | )
54 |
55 | expect(
56 | semverify(['11', '1'])
57 | ).toEqual(
58 | [
59 | 11,
60 | 1,
61 | 0
62 | ]
63 | )
64 |
65 | expect(
66 | semverify([
67 | 13,
68 | '0',
69 | '1'
70 | ])
71 | ).toEqual(
72 | [
73 | 13,
74 | 0,
75 | 1
76 | ]
77 | )
78 | })
79 |
80 | it('should return Infinity for \'all\' version', () => {
81 | expect(
82 | semverify('all')
83 | ).toEqual(
84 | [
85 | Infinity,
86 | 0,
87 | 0
88 | ]
89 | )
90 |
91 | expect(
92 | semverify(['all'])
93 | ).toEqual(
94 | [
95 | Infinity,
96 | 0,
97 | 0
98 | ]
99 | )
100 | })
101 |
102 | it('should return null for non-number versions', () => {
103 | expect(
104 | semverify('TP')
105 | ).toBeNull()
106 |
107 | expect(
108 | semverify(['TP'])
109 | ).toBeNull()
110 | })
111 | })
112 |
113 | describe('compareSemvers', () => {
114 | it('should handle Infinity (\'all\') version', () => {
115 | expect(
116 | compareSemvers(
117 | [
118 | 10,
119 | 1,
120 | 1
121 | ],
122 | [
123 | Infinity,
124 | 0,
125 | 0
126 | ],
127 | {}
128 | )
129 | ).toBe(
130 | true
131 | )
132 | })
133 |
134 | describe('allowHigherVersions: false', () => {
135 | const options = {
136 | ignoreMinor: false,
137 | ignorePatch: false,
138 | allowHigherVersions: false
139 | }
140 |
141 | it('should compare all parts', () => {
142 | expect(
143 | compareSemvers(
144 | [
145 | 10,
146 | 1,
147 | 1
148 | ],
149 | [
150 | 10,
151 | 1,
152 | 1
153 | ],
154 | options
155 | )
156 | ).toBe(
157 | true
158 | )
159 |
160 | expect(
161 | compareSemvers(
162 | [
163 | 10,
164 | 1,
165 | 2
166 | ],
167 | [
168 | 10,
169 | 1,
170 | 1
171 | ],
172 | options
173 | )
174 | ).toBe(
175 | false
176 | )
177 |
178 | expect(
179 | compareSemvers(
180 | [
181 | 10,
182 | 1,
183 | 1
184 | ],
185 | [
186 | 10,
187 | 2,
188 | 1
189 | ],
190 | options
191 | )
192 | ).toBe(
193 | false
194 | )
195 | })
196 |
197 | it('should compare ignoring patch', () => {
198 | const ignorePatchOptions = {
199 | ...options,
200 | ignorePatch: true
201 | }
202 |
203 | expect(
204 | compareSemvers(
205 | [
206 | 10,
207 | 1,
208 | 1
209 | ],
210 | [
211 | 10,
212 | 1,
213 | 1
214 | ],
215 | ignorePatchOptions
216 | )
217 | ).toBe(
218 | true
219 | )
220 |
221 | expect(
222 | compareSemvers(
223 | [
224 | 10,
225 | 1,
226 | 0
227 | ],
228 | [
229 | 10,
230 | 1,
231 | 1
232 | ],
233 | ignorePatchOptions
234 | )
235 | ).toBe(
236 | true
237 | )
238 |
239 | expect(
240 | compareSemvers(
241 | [
242 | 10,
243 | 1,
244 | 2
245 | ],
246 | [
247 | 10,
248 | 2,
249 | 1
250 | ],
251 | ignorePatchOptions
252 | )
253 | ).toBe(
254 | false
255 | )
256 |
257 | expect(
258 | compareSemvers(
259 | [
260 | 10,
261 | 1,
262 | 2
263 | ],
264 | [
265 | 12,
266 | 2,
267 | 1
268 | ],
269 | ignorePatchOptions
270 | )
271 | ).toBe(
272 | false
273 | )
274 | })
275 |
276 | it('should compare ignoring minor', () => {
277 | const ignoreMinorOptions = {
278 | ...options,
279 | ignoreMinor: true
280 | }
281 |
282 | expect(
283 | compareSemvers(
284 | [
285 | 10,
286 | 1,
287 | 1
288 | ],
289 | [
290 | 10,
291 | 1,
292 | 1
293 | ],
294 | ignoreMinorOptions
295 | )
296 | ).toBe(
297 | true
298 | )
299 |
300 | expect(
301 | compareSemvers(
302 | [
303 | 10,
304 | 1,
305 | 0
306 | ],
307 | [
308 | 10,
309 | 2,
310 | 1
311 | ],
312 | ignoreMinorOptions
313 | )
314 | ).toBe(
315 | true
316 | )
317 |
318 | expect(
319 | compareSemvers(
320 | [
321 | 10,
322 | 1,
323 | 2
324 | ],
325 | [
326 | 11,
327 | 2,
328 | 1
329 | ],
330 | ignoreMinorOptions
331 | )
332 | ).toBe(
333 | false
334 | )
335 | })
336 | })
337 |
338 | describe('allowHigherVersions: true', () => {
339 | const options = {
340 | ignoreMinor: false,
341 | ignorePatch: false,
342 | allowHigherVersions: true
343 | }
344 |
345 | it('should compare all parts', () => {
346 | expect(
347 | compareSemvers(
348 | [
349 | 10,
350 | 1,
351 | 1
352 | ],
353 | [
354 | 10,
355 | 1,
356 | 1
357 | ],
358 | options
359 | )
360 | ).toBe(
361 | true
362 | )
363 |
364 | expect(
365 | compareSemvers(
366 | [
367 | 10,
368 | 2,
369 | 3
370 | ],
371 | [
372 | 10,
373 | 1,
374 | 1
375 | ],
376 | options
377 | )
378 | ).toBe(
379 | true
380 | )
381 |
382 | expect(
383 | compareSemvers(
384 | [
385 | 10,
386 | 1,
387 | 2
388 | ],
389 | [
390 | 10,
391 | 1,
392 | 1
393 | ],
394 | options
395 | )
396 | ).toBe(
397 | true
398 | )
399 |
400 | expect(
401 | compareSemvers(
402 | [
403 | 9,
404 | 1,
405 | 1
406 | ],
407 | [
408 | 10,
409 | 2,
410 | 1
411 | ],
412 | options
413 | )
414 | ).toBe(
415 | false
416 | )
417 |
418 | expect(
419 | compareSemvers(
420 | [
421 | 10,
422 | 1,
423 | 0
424 | ],
425 | [
426 | 10,
427 | 1,
428 | 1
429 | ],
430 | options
431 | )
432 | ).toBe(
433 | false
434 | )
435 | })
436 |
437 | it('should compare ignoring patch', () => {
438 | const ignorePatchOptions = {
439 | ...options,
440 | ignorePatch: true
441 | }
442 |
443 | expect(
444 | compareSemvers(
445 | [
446 | 10,
447 | 1,
448 | 1
449 | ],
450 | [
451 | 10,
452 | 1,
453 | 1
454 | ],
455 | ignorePatchOptions
456 | )
457 | ).toBe(
458 | true
459 | )
460 |
461 | expect(
462 | compareSemvers(
463 | [
464 | 10,
465 | 0,
466 | 2
467 | ],
468 | [
469 | 10,
470 | 2,
471 | 1
472 | ],
473 | ignorePatchOptions
474 | )
475 | ).toBe(
476 | false
477 | )
478 |
479 | expect(
480 | compareSemvers(
481 | [
482 | 9,
483 | 1,
484 | 2
485 | ],
486 | [
487 | 12,
488 | 2,
489 | 1
490 | ],
491 | ignorePatchOptions
492 | )
493 | ).toBe(
494 | false
495 | )
496 | })
497 |
498 | it('should compare ignoring minor', () => {
499 | const ignoreMinorOptions = {
500 | ...options,
501 | ignoreMinor: true
502 | }
503 |
504 | expect(
505 | compareSemvers(
506 | [
507 | 10,
508 | 1,
509 | 1
510 | ],
511 | [
512 | 10,
513 | 1,
514 | 1
515 | ],
516 | ignoreMinorOptions
517 | )
518 | ).toBe(
519 | true
520 | )
521 |
522 | expect(
523 | compareSemvers(
524 | [
525 | 10,
526 | 1,
527 | 0
528 | ],
529 | [
530 | 10,
531 | 2,
532 | 1
533 | ],
534 | ignoreMinorOptions
535 | )
536 | ).toBe(
537 | true
538 | )
539 |
540 | expect(
541 | compareSemvers(
542 | [
543 | 9,
544 | 1,
545 | 2
546 | ],
547 | [
548 | 11,
549 | 2,
550 | 1
551 | ],
552 | ignoreMinorOptions
553 | )
554 | ).toBe(
555 | false
556 | )
557 | })
558 | })
559 | })
560 |
561 | describe('getRequiredSemverPartsCount', () => {
562 | describe('allowZeroSubversions: false', () => {
563 | const options = {
564 | ignoreMinor: false,
565 | ignorePatch: false,
566 | allowZeroSubversions: false
567 | }
568 | const version: Semver = [
569 | 1,
570 | 2,
571 | 3
572 | ]
573 |
574 | it('should require all parts', () => {
575 | expect(
576 | getRequiredSemverPartsCount(version, options)
577 | ).toBe(
578 | 3
579 | )
580 | })
581 |
582 | it('should require major and minor parts', () => {
583 | const ignorePatchOptions = {
584 | ...options,
585 | ignorePatch: true
586 | }
587 |
588 | expect(
589 | getRequiredSemverPartsCount(version, ignorePatchOptions)
590 | ).toBe(
591 | 2
592 | )
593 | })
594 |
595 | it('should require major part', () => {
596 | const ignoreMinorOptions = {
597 | ...options,
598 | ignoreMinor: true
599 | }
600 |
601 | expect(
602 | getRequiredSemverPartsCount(version, ignoreMinorOptions)
603 | ).toBe(
604 | 1
605 | )
606 | })
607 | })
608 |
609 | describe('allowZeroSubversions: true', () => {
610 | const options = {
611 | ignoreMinor: false,
612 | ignorePatch: false,
613 | allowZeroSubversions: true
614 | }
615 | const version: Semver = [
616 | 1,
617 | 2,
618 | 3
619 | ]
620 | const zeroVersion: Semver = [
621 | 1,
622 | 1,
623 | 0
624 | ]
625 | const zerosVersion: Semver = [
626 | 1,
627 | 0,
628 | 0
629 | ]
630 |
631 | it('should require all parts', () => {
632 | expect(
633 | getRequiredSemverPartsCount(version, options)
634 | ).toBe(
635 | 3
636 | )
637 |
638 | expect(
639 | getRequiredSemverPartsCount(zeroVersion, options)
640 | ).toBe(
641 | 2
642 | )
643 |
644 | expect(
645 | getRequiredSemverPartsCount(zerosVersion, options)
646 | ).toBe(
647 | 1
648 | )
649 | })
650 |
651 | it('should require major and minor parts', () => {
652 | const ignorePatchOptions = {
653 | ...options,
654 | ignorePatch: true
655 | }
656 |
657 | expect(
658 | getRequiredSemverPartsCount(version, ignorePatchOptions)
659 | ).toBe(
660 | 2
661 | )
662 |
663 | expect(
664 | getRequiredSemverPartsCount(zeroVersion, ignorePatchOptions)
665 | ).toBe(
666 | 2
667 | )
668 |
669 | expect(
670 | getRequiredSemverPartsCount(zerosVersion, ignorePatchOptions)
671 | ).toBe(
672 | 1
673 | )
674 | })
675 |
676 | it('should require major part', () => {
677 | const ignoreMinorOptions = {
678 | ...options,
679 | ignoreMinor: true
680 | }
681 |
682 | expect(
683 | getRequiredSemverPartsCount(version, ignoreMinorOptions)
684 | ).toBe(
685 | 1
686 | )
687 |
688 | expect(
689 | getRequiredSemverPartsCount(zeroVersion, ignoreMinorOptions)
690 | ).toBe(
691 | 1
692 | )
693 |
694 | expect(
695 | getRequiredSemverPartsCount(zerosVersion, ignoreMinorOptions)
696 | ).toBe(
697 | 1
698 | )
699 | })
700 | })
701 | })
702 |
703 | describe('rangeSemver', () => {
704 | it('should range patch', () => {
705 | expect(
706 | rangeSemver(
707 | [4, 4, 3],
708 | [4, 4, 5]
709 | )
710 | ).toEqual([
711 | [4, 4, 3],
712 | [4, 4, 4],
713 | [4, 4, 5]
714 | ])
715 | })
716 |
717 | it('should range minor', () => {
718 | expect(
719 | rangeSemver(
720 | [15, 4, 0],
721 | [15, 6, 0]
722 | )
723 | ).toEqual([
724 | [15, 4, 0],
725 | [15, 5, 0],
726 | [15, 6, 0]
727 | ])
728 | })
729 |
730 | it('should range minor', () => {
731 | expect(
732 | rangeSemver(
733 | [100, 0, 0],
734 | [102, 0, 0]
735 | )
736 | ).toEqual([
737 | [100, 0, 0],
738 | [101, 0, 0],
739 | [102, 0, 0]
740 | ])
741 | })
742 | })
743 | })
744 |
--------------------------------------------------------------------------------
/src/semver/semver.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | SemverLike,
3 | Semver,
4 | RangedSemver,
5 | SemverCompareOptions
6 | } from './types.js'
7 |
8 | /**
9 | * Get semver from string or array.
10 | * @param version - Target to convert.
11 | * @returns Array with semver parts.
12 | */
13 | export function semverify(version: SemverLike): Semver | null {
14 | const versionParts = Array.isArray(version)
15 | ? version
16 | : version.toString().split('.')
17 |
18 | if (versionParts[0] === 'all') {
19 | return [
20 | Infinity,
21 | 0,
22 | 0
23 | ]
24 | }
25 |
26 | let versionPart: number | string = null
27 | let semverPart: number = null
28 | const semver: Semver = [
29 | 0,
30 | 0,
31 | 0
32 | ]
33 |
34 | for (let i = 0; i < 3; i++) {
35 | versionPart = versionParts[i]
36 |
37 | if (typeof versionPart === 'undefined') {
38 | continue
39 | }
40 |
41 | semverPart = typeof versionPart === 'number'
42 | ? versionPart
43 | : parseInt(versionPart, 10)
44 |
45 | if (isNaN(semverPart)) {
46 | return null
47 | }
48 |
49 | semver[i] = semverPart
50 | }
51 |
52 | return semver
53 | }
54 |
55 | /**
56 | * Get semver range.
57 | * @param from
58 | * @param to
59 | * @returns Semver range.
60 | */
61 | export function rangeSemver(from: Semver, to: Semver) {
62 | let partIndex = 0
63 | const range: Semver[] = []
64 |
65 | for (let i = 2; i >= 0; i--) {
66 | if (from[i] !== to[i]) {
67 | partIndex = i
68 | break
69 | }
70 | }
71 |
72 | for (let i = from[partIndex], max = to[partIndex]; i <= max; i++) {
73 | range.push(
74 | from.map((v, j) => (j === partIndex ? i : v)) as Semver
75 | )
76 | }
77 |
78 | return range
79 | }
80 |
81 | /**
82 | * Compare semvers.
83 | * @param a - Semver to compare.
84 | * @param b - Semver to compare with.
85 | * @param options - Compare options.
86 | * @returns Equals or not.
87 | */
88 | export function compareSemvers(a: Semver, b: Semver, options: SemverCompareOptions) {
89 | const [
90 | major,
91 | minor,
92 | patch
93 | ] = a
94 | const [
95 | majorBase,
96 | minorBase,
97 | patchBase
98 | ] = b
99 | const {
100 | ignoreMinor,
101 | ignorePatch,
102 | allowHigherVersions
103 | } = options
104 |
105 | if (majorBase === Infinity) {
106 | return true
107 | }
108 |
109 | const compareMinor = !ignoreMinor
110 | const comparePatch = compareMinor && !ignorePatch
111 |
112 | if (allowHigherVersions) {
113 | if (
114 | comparePatch && patch < patchBase
115 | || compareMinor && minor < minorBase
116 | ) {
117 | return false
118 | }
119 |
120 | return major >= majorBase
121 | }
122 |
123 | if (
124 | comparePatch && patch !== patchBase
125 | || compareMinor && minor !== minorBase
126 | ) {
127 | return false
128 | }
129 |
130 | return major === majorBase
131 | }
132 |
133 | /**
134 | * Get required semver parts count.
135 | * @param version - Semver parts or ranges.
136 | * @param options - Semver compare options.
137 | * @returns Required semver parts count.
138 | */
139 | export function getRequiredSemverPartsCount(version: Semver | RangedSemver, options: SemverCompareOptions) {
140 | const {
141 | ignoreMinor,
142 | ignorePatch,
143 | allowZeroSubversions
144 | } = options
145 | let shouldRepeatCount = ignoreMinor
146 | ? 1
147 | : ignorePatch
148 | ? 2
149 | : 3
150 |
151 | if (allowZeroSubversions) {
152 | for (let i = shouldRepeatCount - 1; i > 0; i--) {
153 | if (version[i] !== 0 || shouldRepeatCount === 1) {
154 | break
155 | }
156 |
157 | shouldRepeatCount--
158 | }
159 | }
160 |
161 | return shouldRepeatCount
162 | }
163 |
--------------------------------------------------------------------------------
/src/semver/types.ts:
--------------------------------------------------------------------------------
1 | export interface SemverCompareOptions {
2 | ignoreMinor?: boolean
3 | ignorePatch?: boolean
4 | allowZeroSubversions?: boolean
5 | allowHigherVersions?: boolean
6 | }
7 |
8 | export type Semver = [
9 | number,
10 | number,
11 | number
12 | ]
13 |
14 | export type SemverRange = number | [number, number]
15 |
16 | export type RangedSemver = [
17 | SemverRange,
18 | SemverRange,
19 | SemverRange
20 | ]
21 |
22 | export type SemverLike = string | (number | string)[]
23 |
24 | export enum SemverPart {
25 | Major = 0,
26 | Minor,
27 | Patch
28 | }
29 |
--------------------------------------------------------------------------------
/src/useragent/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types.js'
2 | export * from './utils.js'
3 | export * from './useragent.js'
4 |
--------------------------------------------------------------------------------
/src/useragent/types.ts:
--------------------------------------------------------------------------------
1 | import type { UserAgentRegex } from 'ua-regexes-lite'
2 | import type { AstRegExp } from 'regexp-tree/ast'
3 | import type { Semver } from '../semver/types.js'
4 |
5 | export interface BrowserRegex extends UserAgentRegex {
6 | requestVersions: Semver[]
7 | matchedVersions: Semver[]
8 | }
9 |
10 | export interface BrowserVersionedRegex extends BrowserRegex {
11 | sourceRegex: RegExp
12 | regexAst: AstRegExp
13 | }
14 |
--------------------------------------------------------------------------------
/src/useragent/useragent.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import type { UserAgentRegex } from 'ua-regexes-lite'
3 | import type { BrowsersVersions } from '../browsers/types.js'
4 | import { getRegexesForBrowsers } from './useragent.js'
5 |
6 | describe('UserAgent', () => {
7 | describe('getRegexesForBrowsers', () => {
8 | it('should find regexes by family name', () => {
9 | const browsers: BrowsersVersions = new Map([['chrome', [[98, 0, 0], [99, 0, 0], [100, 0, 0]]]])
10 | const regexes = [
11 | {
12 | regex: /chrome/,
13 | family: 'chrome'
14 | },
15 | {
16 | regex: /firefox/,
17 | family: 'firefox'
18 | }
19 | ]
20 |
21 | expect(getRegexesForBrowsers(browsers, {}, regexes)).toEqual([
22 | {
23 | regex: /chrome/,
24 | family: 'chrome',
25 | requestVersions: [[98, 0, 0], [99, 0, 0], [100, 0, 0]],
26 | matchedVersions: [[98, 0, 0], [99, 0, 0], [100, 0, 0]]
27 | }
28 | ])
29 | })
30 |
31 | it('should find regexes by version', () => {
32 | const browsers: BrowsersVersions = new Map([['chrome', [[98, 0, 0], [99, 0, 0], [100, 0, 0]]]])
33 | const regexes: UserAgentRegex[] = [
34 | {
35 | regex: /chrome 97/,
36 | family: 'chrome',
37 | version: [97, 0, 0]
38 | },
39 | {
40 | regex: /chrome 99/,
41 | family: 'chrome',
42 | version: [99, 0, 0]
43 | },
44 | {
45 | regex: /chrome 101/,
46 | family: 'chrome',
47 | version: [101, 0, 0]
48 | }
49 | ]
50 |
51 | expect(getRegexesForBrowsers(browsers, {}, regexes)).toEqual([
52 | {
53 | regex: /chrome 99/,
54 | family: 'chrome',
55 | version: [99, 0, 0],
56 | minVersion: [99, 0, 0],
57 | maxVersion: [99, 0, 0],
58 | requestVersions: [[98, 0, 0], [99, 0, 0], [100, 0, 0]],
59 | matchedVersions: [[99, 0, 0]]
60 | }
61 | ])
62 | })
63 |
64 | it('should find regexes by version ranges', () => {
65 | const browsers: BrowsersVersions = new Map([['chrome', [[98, 0, 0], [99, 0, 0], [100, 0, 0]]]])
66 | const regexes: UserAgentRegex[] = [
67 | {
68 | regex: /chrome x/,
69 | family: 'chrome',
70 | maxVersion: [9, 0, 0]
71 | },
72 | {
73 | regex: /chrome xx/,
74 | family: 'chrome',
75 | minVersion: [10, 0, 0],
76 | maxVersion: [99, 0, 0]
77 | },
78 | {
79 | regex: /chrome xxx/,
80 | family: 'chrome',
81 | minVersion: [100, 0, 0]
82 | }
83 | ]
84 |
85 | expect(getRegexesForBrowsers(browsers, {}, regexes)).toEqual([
86 | {
87 | regex: /chrome xx/,
88 | family: 'chrome',
89 | minVersion: [10, 0, 0],
90 | maxVersion: [99, 0, 0],
91 | requestVersions: [[98, 0, 0], [99, 0, 0], [100, 0, 0]],
92 | matchedVersions: [[98, 0, 0], [99, 0, 0]]
93 | },
94 | {
95 | regex: /chrome xxx/,
96 | family: 'chrome',
97 | minVersion: [100, 0, 0],
98 | requestVersions: [[98, 0, 0], [99, 0, 0], [100, 0, 0]],
99 | matchedVersions: [[100, 0, 0]]
100 | }
101 | ])
102 | })
103 |
104 | it('should apply patches', () => {
105 | const browsers: BrowsersVersions = new Map([['chrome', [[98, 0, 0], [99, 0, 0], [100, 0, 0]]]])
106 | const regexes: UserAgentRegex[] = [
107 | {
108 | regex: /chrome/,
109 | family: 'chrome'
110 | },
111 | {
112 | regex: /chrome patch/,
113 | family: 'chrome',
114 | maxVersion: [98, 0, 0]
115 | }
116 | ]
117 |
118 | expect(getRegexesForBrowsers(browsers, {}, regexes)).toEqual([
119 | {
120 | regex: /chrome patch/,
121 | family: 'chrome',
122 | requestVersions: [[98, 0, 0], [99, 0, 0], [100, 0, 0]],
123 | matchedVersions: [[98, 0, 0], [99, 0, 0], [100, 0, 0]]
124 | }
125 | ])
126 | })
127 |
128 | it('should not apply patches', () => {
129 | const browsers: BrowsersVersions = new Map([['chrome', [[99, 0, 0], [100, 0, 0]]]])
130 | const regexes: UserAgentRegex[] = [
131 | {
132 | regex: /chrome/,
133 | family: 'chrome'
134 | },
135 | {
136 | regex: /chrome patch/,
137 | family: 'chrome',
138 | maxVersion: [98, 0, 0]
139 | }
140 | ]
141 |
142 | expect(getRegexesForBrowsers(browsers, {}, regexes)).toEqual([
143 | {
144 | regex: /chrome/,
145 | family: 'chrome',
146 | requestVersions: [[99, 0, 0], [100, 0, 0]],
147 | matchedVersions: [[99, 0, 0], [100, 0, 0]]
148 | }
149 | ])
150 | })
151 |
152 | it('should not apply patches for different family', () => {
153 | const browsers: BrowsersVersions = new Map([['chrome', [[98, 0, 0], [99, 0, 0], [100, 0, 0]]]])
154 | const regexes: UserAgentRegex[] = [
155 | {
156 | regex: /firefox/,
157 | family: 'firefox'
158 | },
159 | {
160 | regex: /chrome patch/,
161 | family: 'chrome',
162 | maxVersion: [98, 0, 0]
163 | }
164 | ]
165 |
166 | expect(getRegexesForBrowsers(browsers, {}, regexes)).toEqual([
167 | {
168 | regex: /chrome patch/,
169 | family: 'chrome',
170 | maxVersion: [98, 0, 0],
171 | requestVersions: [[98, 0, 0], [99, 0, 0], [100, 0, 0]],
172 | matchedVersions: [[98, 0, 0]]
173 | }
174 | ])
175 | })
176 | })
177 | })
178 |
--------------------------------------------------------------------------------
/src/useragent/useragent.ts:
--------------------------------------------------------------------------------
1 | import { regexes } from 'ua-regexes-lite'
2 | import type { SemverCompareOptions } from '../semver/index.js'
3 | import type { BrowsersVersions } from '../browsers/types.js'
4 | import type { BrowserRegex } from './types.js'
5 | import { findMatchedVersions } from './utils.js'
6 |
7 | /**
8 | * Get useragent regexes for given browsers.
9 | * @param browsers - Browsers.
10 | * @param options - Semver compare options.
11 | * @param targetRegexes - Override default regexes.
12 | * @returns User agent regexes.
13 | */
14 | export function getRegexesForBrowsers(browsers: BrowsersVersions, options: SemverCompareOptions, targetRegexes = regexes) {
15 | const result: BrowserRegex[] = []
16 | let prevFamily = ''
17 | let prevRegexIsGlobal = false
18 |
19 | targetRegexes.forEach((regex) => {
20 | const requestVersions = browsers.get(regex.family)
21 |
22 | if (!requestVersions) {
23 | return
24 | }
25 |
26 | let {
27 | version,
28 | minVersion,
29 | maxVersion
30 | } = regex
31 |
32 | if (version) {
33 | minVersion = version
34 | maxVersion = version
35 | }
36 |
37 | let matchedVersions = findMatchedVersions(minVersion, maxVersion, requestVersions, options)
38 |
39 | if (matchedVersions.length) {
40 | // regex contains global patch
41 | if (prevFamily === regex.family && prevRegexIsGlobal) {
42 | version = undefined
43 | minVersion = undefined
44 | maxVersion = undefined
45 | matchedVersions = requestVersions
46 | result.pop()
47 | }
48 |
49 | result.push({
50 | ...regex,
51 | version,
52 | minVersion,
53 | maxVersion,
54 | requestVersions,
55 | matchedVersions
56 | })
57 | }
58 |
59 | prevRegexIsGlobal = !version && !minVersion && !maxVersion
60 | prevFamily = regex.family
61 | })
62 |
63 | return result
64 | }
65 |
--------------------------------------------------------------------------------
/src/useragent/utils.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import type { Semver } from '../semver/types.js'
3 | import { findMatchedVersions } from './utils.js'
4 |
5 | describe('UserAgent', () => {
6 | describe('utils', () => {
7 | describe('someSemverMatched', () => {
8 | const versions: Semver[] = [
9 | [
10 | 10,
11 | 0,
12 | 0
13 | ],
14 | [
15 | 11,
16 | 0,
17 | 0
18 | ],
19 | [
20 | 12,
21 | 0,
22 | 0
23 | ]
24 | ]
25 |
26 | it('should correct match semver', () => {
27 | expect(
28 | findMatchedVersions(
29 | [
30 | 9,
31 | 0,
32 | 0
33 | ],
34 | null,
35 | versions,
36 | {}
37 | )
38 | ).toEqual(
39 | versions
40 | )
41 |
42 | expect(
43 | findMatchedVersions(
44 | null,
45 | [
46 | 11,
47 | 0,
48 | 0
49 | ],
50 | versions,
51 | {}
52 | )
53 | ).toEqual(
54 | [
55 | [
56 | 10,
57 | 0,
58 | 0
59 | ],
60 | [
61 | 11,
62 | 0,
63 | 0
64 | ]
65 | ]
66 | )
67 |
68 | expect(
69 | findMatchedVersions(
70 | [
71 | 10,
72 | 0,
73 | 0
74 | ],
75 | [
76 | 16,
77 | 0,
78 | 0
79 | ],
80 | versions,
81 | {}
82 | )
83 | ).toEqual(
84 | versions
85 | )
86 |
87 | expect(
88 | findMatchedVersions(
89 | [
90 | 16,
91 | 0,
92 | 0
93 | ],
94 | null,
95 | versions,
96 | {}
97 | )
98 | ).toEqual(
99 | []
100 | )
101 |
102 | expect(
103 | findMatchedVersions(
104 | null,
105 | [
106 | 9,
107 | 0,
108 | 0
109 | ],
110 | versions,
111 | {}
112 | )
113 | ).toEqual(
114 | []
115 | )
116 | })
117 | })
118 | })
119 | })
120 |
--------------------------------------------------------------------------------
/src/useragent/utils.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | Semver,
3 | SemverCompareOptions
4 | } from '../semver/index.js'
5 | import { compareSemvers } from '../semver/index.js'
6 |
7 | /**
8 | * Find matched versions.
9 | * @param minVersion - Semver version.
10 | * @param maxVersion - Semver version.
11 | * @param bases - Base semver versions.
12 | * @param options - Semver compare options.
13 | * @returns Matched versions.
14 | */
15 | export function findMatchedVersions(
16 | minVersion: Semver | null,
17 | maxVersion: Semver | null,
18 | bases: Semver[],
19 | options: SemverCompareOptions
20 | ) {
21 | const compareOptions = {
22 | ...options,
23 | allowHigherVersions: true
24 | }
25 | const minComparator = (ver: Semver) => compareSemvers(ver, minVersion, compareOptions)
26 | const maxComparator = (ver: Semver) => compareSemvers(maxVersion, ver, compareOptions)
27 | const comparator = minVersion && maxVersion
28 | ? (ver: Semver) => minComparator(ver) && maxComparator(ver)
29 | : minVersion
30 | ? minComparator
31 | : maxVersion
32 | ? maxComparator
33 | : () => true
34 |
35 | return bases.filter(comparator)
36 | }
37 |
--------------------------------------------------------------------------------
/src/useragentRegex/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types.js'
2 | export * from './utils.js'
3 | export * from './useragentRegex.js'
4 |
--------------------------------------------------------------------------------
/src/useragentRegex/types.ts:
--------------------------------------------------------------------------------
1 | import type { BrowserslistRequest } from '../browsers/types.js'
2 | import type { SemverCompareOptions } from '../semver/types.js'
3 |
4 | export type UserAgentRegexOptions = BrowserslistRequest & SemverCompareOptions
5 |
--------------------------------------------------------------------------------
/src/useragentRegex/useragentRegex.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from 'vitest'
2 | import { useragents } from '../../test/useragents.js'
3 | import {
4 | getUserAgentRegex,
5 | getUserAgentRegexes
6 | } from './useragentRegex.js'
7 |
8 | function* getUserAgents() {
9 | const regexesCache = new Map()
10 | const getRegex = (query: string, allowHigherVersions = true) => {
11 | const key = `${query}:${allowHigherVersions}`
12 | let regex = regexesCache.get(key)
13 |
14 | if (!regex) {
15 | regex = getUserAgentRegex({
16 | browsers: query,
17 | allowHigherVersions,
18 | allowZeroSubversions: true
19 | })
20 | regexesCache.set(key, regex)
21 | }
22 |
23 | return regex
24 | }
25 |
26 | for (const useragent of useragents) {
27 | if (useragent.yes) {
28 | for (const query of useragent.yes) {
29 | yield {
30 | ua: useragent.ua,
31 | regex: getRegex(query, useragent.allowHigherVersions),
32 | query,
33 | allowHigherVersions: useragent.allowHigherVersions,
34 | should: true
35 | }
36 | }
37 | }
38 |
39 | if (useragent.no) {
40 | for (const query of useragent.no) {
41 | yield {
42 | ua: useragent.ua,
43 | regex: getRegex(query, useragent.allowHigherVersions),
44 | allowHigherVersions: useragent.allowHigherVersions,
45 | query,
46 | should: false
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
53 | interface UserAgentTest {
54 | ua: string
55 | regex: RegExp
56 | query: string
57 | allowHigherVersions?: boolean
58 | should: boolean
59 | }
60 |
61 | function inspect({ query, ua, should, allowHigherVersions }: UserAgentTest) {
62 | const info = getUserAgentRegexes({
63 | browsers: query,
64 | allowHigherVersions,
65 | allowZeroSubversions: true
66 | })
67 | const message = `${should ? 'Should' : 'Should not'} matches:
68 |
69 | Useragent: ${ua}
70 | Query: ${query}
71 | ${info.map(_ => `
72 | Source Regex: ${String(_.sourceRegex)}
73 | Regex: ${String(_.regex)}`).join('')}
74 | `
75 |
76 | throw new Error(message)
77 | }
78 |
79 | describe('UserAgentRegex', () => {
80 | it('should create correct regexes', () => {
81 | const userAgents = getUserAgents()
82 | let res: boolean
83 |
84 | for (const ua of userAgents) {
85 | res = ua.regex.test(ua.ua)
86 |
87 | if (res !== ua.should) {
88 | inspect(ua)
89 | }
90 | }
91 | })
92 | }, 5 * 60 * 1000)
93 |
--------------------------------------------------------------------------------
/src/useragentRegex/useragentRegex.ts:
--------------------------------------------------------------------------------
1 | import type { SemverCompareOptions } from '../semver/index.js'
2 | import { getRegexesForBrowsers } from '../useragent/index.js'
3 | import {
4 | getBrowsersList,
5 | mergeBrowserVersions
6 | } from '../browsers/index.js'
7 | import { applyVersionsToRegexes } from '../versions/index.js'
8 | import type { UserAgentRegexOptions } from './types.js'
9 | import {
10 | compileRegexes,
11 | compileRegex
12 | } from './utils.js'
13 |
14 | export const defaultOptions = {
15 | ignoreMinor: false,
16 | ignorePatch: true,
17 | allowZeroSubversions: false,
18 | allowHigherVersions: false
19 | } as const satisfies Required
20 |
21 | /**
22 | * Get source regexes objects from browserslist query.
23 | * @param options - Browserslist and semver compare options.
24 | * @returns Source regexes objects.
25 | */
26 | export function getPreUserAgentRegexes(options: UserAgentRegexOptions = {}) {
27 | const finalOptions = {
28 | ...defaultOptions,
29 | ...options
30 | }
31 | const browsersList = getBrowsersList(finalOptions)
32 | const mergedBrowsers = mergeBrowserVersions(browsersList)
33 | const sourceRegexes = getRegexesForBrowsers(mergedBrowsers, finalOptions)
34 | const versionedRegexes = applyVersionsToRegexes(sourceRegexes, finalOptions)
35 |
36 | return versionedRegexes
37 | }
38 |
39 | /**
40 | * Compile browserslist query to regexes.
41 | * @param options - Browserslist and semver compare options.
42 | * @returns Objects with info about compiled regexes.
43 | */
44 | export function getUserAgentRegexes(options: UserAgentRegexOptions = {}) {
45 | return compileRegexes(
46 | getPreUserAgentRegexes(options)
47 | )
48 | }
49 |
50 | /**
51 | * Compile browserslist query to regex.
52 | * @param options - Browserslist and semver compare options.
53 | * @returns Compiled regex.
54 | */
55 | export function getUserAgentRegex(options: UserAgentRegexOptions = {}) {
56 | return compileRegex(
57 | getPreUserAgentRegexes(options)
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/src/useragentRegex/utils.ts:
--------------------------------------------------------------------------------
1 | import type { BrowserVersionedRegex } from '../useragent/types.js'
2 | import {
3 | optimizeRegex,
4 | toRegex,
5 | CapturingGroupNode,
6 | AstRegExpNode,
7 | DisjunctionCapturingGroupNode
8 | } from '../regex/index.js'
9 |
10 | /**
11 | * Compile regexes.
12 | * @param regexes - Objects with info about compiled regexes.
13 | * @returns Objects with info about compiled regexes.
14 | */
15 | export function compileRegexes(regexes: BrowserVersionedRegex[]) {
16 | return regexes.map(({
17 | regexAst,
18 | ...regex
19 | }) => {
20 | const optimizedRegexAst = optimizeRegex(regexAst)
21 |
22 | return {
23 | ...regex,
24 | regexAst: optimizedRegexAst,
25 | regex: toRegex(optimizedRegexAst)
26 | }
27 | })
28 | }
29 |
30 | /**
31 | * Compile regex.
32 | * @param regexes - Objects with info about compiled regexes.
33 | * @returns Compiled common regex.
34 | */
35 | export function compileRegex(regexes: BrowserVersionedRegex[]) {
36 | const partsRegexes = regexes.map(
37 | ({ regexAst }) => CapturingGroupNode(regexAst.body)
38 | )
39 | const regexAst = optimizeRegex(
40 | AstRegExpNode(
41 | DisjunctionCapturingGroupNode(partsRegexes)
42 | )
43 | )
44 |
45 | return toRegex(regexAst)
46 | }
47 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Compare two arrays.
3 | * @param a - Array to compare.
4 | * @param b - Array to compare.
5 | * @param from - Index to start compare from.
6 | * @returns Equals or not.
7 | */
8 | export function compareArrays(a: unknown[], b: unknown[], from = 0) {
9 | const len = a.length
10 |
11 | for (let i = from; i < len; i++) {
12 | if (a[i] !== b[i]) {
13 | return false
14 | }
15 | }
16 |
17 | return true
18 | }
19 |
20 | /**
21 | * Clone simple object.
22 | * @param value
23 | * @returns Object clone.
24 | */
25 | export function clone(value: T): T {
26 | if (value === null || typeof value !== 'object') {
27 | return value
28 | }
29 |
30 | /* eslint-disable */
31 | const copy = Array.isArray(value)
32 | ? []
33 | : {}
34 | let i
35 |
36 | for (i in value) {
37 | copy[i] = clone(value[i])
38 | }
39 | /* eslint-enable */
40 |
41 | return copy as T
42 | }
43 |
44 | /**
45 | * Concat arrays.
46 | * @param items
47 | * @returns Concatinated arrays.
48 | */
49 | export function concat(items: (T | T[])[]) {
50 | return ([] as T[]).concat(...items)
51 | }
52 |
--------------------------------------------------------------------------------
/src/utils/utils.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import { compareArrays } from './index.js'
3 |
4 | describe('Utils', () => {
5 | describe('compareArrays', () => {
6 | it('should correct compare arrays', () => {
7 | expect(
8 | compareArrays([], [])
9 | ).toBe(
10 | true
11 | )
12 |
13 | expect(
14 | compareArrays([
15 | 1,
16 | 2,
17 | 3
18 | ], [
19 | 1,
20 | 2,
21 | 3
22 | ])
23 | ).toBe(
24 | true
25 | )
26 |
27 | expect(
28 | compareArrays([
29 | 3,
30 | 2,
31 | 3
32 | ], [
33 | 1,
34 | 2,
35 | 3
36 | ])
37 | ).toBe(
38 | false
39 | )
40 |
41 | expect(
42 | compareArrays([
43 | 3,
44 | 2,
45 | 3
46 | ], [
47 | 1,
48 | 2,
49 | 3
50 | ], 1)
51 | ).toBe(
52 | true
53 | )
54 |
55 | expect(
56 | compareArrays([
57 | 3,
58 | 2,
59 | 3
60 | ], [
61 | 1,
62 | 2,
63 | 3
64 | ], 2)
65 | ).toBe(
66 | true
67 | )
68 |
69 | expect(
70 | compareArrays([
71 | 3,
72 | 2,
73 | 3
74 | ], [
75 | 1,
76 | 2,
77 | 3
78 | ], 3)
79 | ).toBe(
80 | true
81 | )
82 |
83 | expect(
84 | compareArrays([3, 1], [1, 2], 3)
85 | ).toBe(
86 | true
87 | )
88 | })
89 | })
90 | })
91 |
--------------------------------------------------------------------------------
/src/versions/index.ts:
--------------------------------------------------------------------------------
1 | export * from './versions.js'
2 | export * from './utils.js'
3 |
--------------------------------------------------------------------------------
/src/versions/utils.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import {
3 | AlternativeNode,
4 | SimpleCharNode,
5 | toString
6 | } from '../regex/index.js'
7 | import {
8 | getNumberPatternsCount,
9 | replaceNumberPatterns,
10 | getNumberPatternsPart,
11 | rangedSemverToRegex
12 | } from './utils.js'
13 |
14 | describe('Versions', () => {
15 | describe('utils', () => {
16 | describe('getNumberPatternsCount', () => {
17 | it('should find correct number patterns count', () => {
18 | expect(
19 | getNumberPatternsCount('__(\\d+)__')
20 | ).toBe(
21 | 1
22 | )
23 |
24 | expect(
25 | getNumberPatternsCount(/__(\d+)__/)
26 | ).toBe(
27 | 1
28 | )
29 |
30 | expect(
31 | getNumberPatternsCount('__(\\d+)__(\\d+)')
32 | ).toBe(
33 | 2
34 | )
35 |
36 | expect(
37 | getNumberPatternsCount(/__(\d+)__(\d+)/)
38 | ).toBe(
39 | 2
40 | )
41 |
42 | expect(
43 | getNumberPatternsCount('__')
44 | ).toBe(
45 | 0
46 | )
47 |
48 | expect(
49 | getNumberPatternsCount(/__/)
50 | ).toBe(
51 | 0
52 | )
53 | })
54 | })
55 |
56 | describe('replaceNumberPatterns', () => {
57 | const numbers = [
58 | SimpleCharNode('1'),
59 | SimpleCharNode('2'),
60 | SimpleCharNode('3')
61 | ]
62 |
63 | it('should replace in RegExp', () => {
64 | expect(
65 | toString(replaceNumberPatterns(/__(\d+)__/, numbers))
66 | ).toBe(
67 | '/__1__/'
68 | )
69 |
70 | expect(
71 | toString(replaceNumberPatterns(/__(\d+)__(\d+)/, numbers))
72 | ).toBe(
73 | '/__1__2/'
74 | )
75 |
76 | expect(
77 | toString(replaceNumberPatterns(/__(\d+)__(\d+)__(\d+)/, numbers))
78 | ).toBe(
79 | '/__1__2__3/'
80 | )
81 |
82 | expect(
83 | toString(replaceNumberPatterns(/__/, numbers))
84 | ).toBe(
85 | '/__/'
86 | )
87 | })
88 |
89 | it('should replace in string', () => {
90 | expect(
91 | toString(replaceNumberPatterns('__(\\d+)__', numbers))
92 | ).toBe(
93 | '/__1__/'
94 | )
95 |
96 | expect(
97 | toString(replaceNumberPatterns('__(\\d+)__(\\d+)', numbers))
98 | ).toBe(
99 | '/__1__2/'
100 | )
101 |
102 | expect(
103 | toString(replaceNumberPatterns('__(\\d+)__(\\d+)__(\\d+)', numbers))
104 | ).toBe(
105 | '/__1__2__3/'
106 | )
107 |
108 | expect(
109 | toString(replaceNumberPatterns('__', numbers))
110 | ).toBe(
111 | '/__/'
112 | )
113 | })
114 |
115 | it('should replace only given count', () => {
116 | expect(
117 | toString(replaceNumberPatterns('__(\\d+)__(\\d+)__(\\d+)', numbers, 3))
118 | ).toBe(
119 | '/__1__2__3/'
120 | )
121 |
122 | expect(
123 | toString(replaceNumberPatterns('__(\\d+)__(\\d+)__(\\d+)', numbers, 2))
124 | ).toBe(
125 | '/__1__2__(\\d+)/'
126 | )
127 |
128 | expect(
129 | toString(replaceNumberPatterns('__(\\d+)__(\\d+)__(\\d+)', numbers, 1))
130 | ).toBe(
131 | '/__1__(\\d+)__(\\d+)/'
132 | )
133 |
134 | expect(
135 | toString(replaceNumberPatterns('__(\\d+)__(\\d+)__(\\d+)', numbers, 0))
136 | ).toBe(
137 | '/__(\\d+)__(\\d+)__(\\d+)/'
138 | )
139 | })
140 | })
141 |
142 | describe('getNumberPatternsPart', () => {
143 | it('should work with RegExp', () => {
144 | expect(
145 | toString(AlternativeNode(getNumberPatternsPart(/Chrome(\d+)/)))
146 | ).toBe(
147 | '(\\d+)'
148 | )
149 | })
150 |
151 | it('should work with string', () => {
152 | expect(
153 | toString(AlternativeNode(getNumberPatternsPart('Chrome(\\d+)')))
154 | ).toBe(
155 | '(\\d+)'
156 | )
157 |
158 | expect(
159 | toString(AlternativeNode(getNumberPatternsPart('Chrome\\/(\\d+)\\.(\\d+)')))
160 | ).toBe(
161 | '(\\d+)\\.(\\d+)'
162 | )
163 | })
164 |
165 | it('should ignore square braces', () => {
166 | expect(
167 | toString(AlternativeNode(getNumberPatternsPart('[(\\d+)](\\d+)')))
168 | ).toBe(
169 | '(\\d+)'
170 | )
171 | })
172 |
173 | it('should handle escaped braces', () => {
174 | expect(
175 | toString(AlternativeNode(getNumberPatternsPart('\\(\\d+\\) (not a number) (\\d+)')))
176 | ).toBe(
177 | '(\\d+)'
178 | )
179 | })
180 |
181 | it('should handle nested groups', () => {
182 | expect(
183 | toString(AlternativeNode(getNumberPatternsPart('Chrome (Canary(\\d+)) (\\d+)(test)')))
184 | ).toBe(
185 | '(Canary(\\d+)) (\\d+)'
186 | )
187 |
188 | expect(
189 | toString(AlternativeNode(getNumberPatternsPart('Browser (a (b (\\d+) (\\d+)))')))
190 | ).toBe(
191 | '(\\d+) (\\d+)'
192 | )
193 | })
194 |
195 | it('should handle only given number patterns count', () => {
196 | expect(
197 | toString(AlternativeNode(getNumberPatternsPart('(\\d+)1(\\d+)2(\\d+)3', 2)))
198 | ).toBe(
199 | '(\\d+)1(\\d+)'
200 | )
201 |
202 | expect(
203 | toString(AlternativeNode(getNumberPatternsPart('(\\d+)1(\\d+)2(\\d+)3', 1)))
204 | ).toBe(
205 | '(\\d+)'
206 | )
207 |
208 | expect(
209 | toString(AlternativeNode(getNumberPatternsPart('Chrome (Canary(\\d+)) (\\d+)', 1)))
210 | ).toBe(
211 | '(\\d+)'
212 | )
213 | })
214 |
215 | it('should not capture usless part if passed patterns count is more', () => {
216 | expect(
217 | toString(AlternativeNode(getNumberPatternsPart('pre (\\d+) post', 2)))
218 | ).toBe(
219 | '(\\d+)'
220 | )
221 | })
222 | })
223 |
224 | describe('rangedSemverToRegex', () => {
225 | describe('disallow higher versions', () => {
226 | const options = {
227 | ignoreMinor: false,
228 | ignorePatch: false,
229 | allowHigherVersions: false
230 | }
231 |
232 | it('should return only numbers', () => {
233 | expect(
234 | rangedSemverToRegex(
235 | [
236 | 11,
237 | 12,
238 | 0
239 | ],
240 | options
241 | ).map(_ => _.map(toString))
242 | ).toEqual([
243 | [
244 | '11',
245 | '12',
246 | '0'
247 | ]
248 | ])
249 | })
250 |
251 | it('should return only numbers patterns', () => {
252 | expect(
253 | rangedSemverToRegex(
254 | [
255 | Infinity,
256 | 0,
257 | 0
258 | ],
259 | options
260 | ).map(_ => _.map(toString))
261 | ).toEqual([
262 | [
263 | '\\d+',
264 | '\\d+',
265 | '\\d+'
266 | ]
267 | ])
268 | })
269 |
270 | it('should return number pattern at patch', () => {
271 | const ignorePatchOptions = {
272 | ...options,
273 | ignorePatch: true
274 | }
275 |
276 | expect(
277 | rangedSemverToRegex(
278 | [
279 | 11,
280 | 12,
281 | 0
282 | ],
283 | ignorePatchOptions
284 | ).map(_ => _.map(toString))
285 | ).toEqual([
286 | [
287 | '11',
288 | '12',
289 | '\\d+'
290 | ]
291 | ])
292 | })
293 |
294 | it('should return number patterns at minor and patch', () => {
295 | const ignoreMinorOptions = {
296 | ...options,
297 | ignoreMinor: true
298 | }
299 |
300 | expect(
301 | rangedSemverToRegex(
302 | [
303 | 11,
304 | 12,
305 | 0
306 | ],
307 | ignoreMinorOptions
308 | ).map(_ => _.map(toString))
309 | ).toEqual([
310 | [
311 | '11',
312 | '\\d+',
313 | '\\d+'
314 | ]
315 | ])
316 | })
317 |
318 | it('should return ranged major', () => {
319 | expect(
320 | rangedSemverToRegex(
321 | [
322 | [11, 13],
323 | 12,
324 | 0
325 | ],
326 | options
327 | ).map(_ => _.map(toString))
328 | ).toEqual([
329 | [
330 | '1[1-3]',
331 | '12',
332 | '0'
333 | ]
334 | ])
335 | expect(
336 | rangedSemverToRegex(
337 | [
338 | 11,
339 | [12, 15],
340 | 2
341 | ],
342 | options
343 | ).map(_ => _.map(toString))
344 | ).toEqual([
345 | [
346 | '11',
347 | '1[2-5]',
348 | '2'
349 | ]
350 | ])
351 | expect(
352 | rangedSemverToRegex(
353 | [
354 | 11,
355 | 12,
356 | [2, 5]
357 | ],
358 | options
359 | ).map(_ => _.map(toString))
360 | ).toEqual([
361 | [
362 | '11',
363 | '12',
364 | '[2-5]'
365 | ]
366 | ])
367 | })
368 | })
369 |
370 | describe('allow higher versions', () => {
371 | const allowHigherOptions = {
372 | allowHigherVersions: true
373 | }
374 |
375 | it('should return ranged major ray', () => {
376 | expect(
377 | rangedSemverToRegex(
378 | [
379 | [11, 13],
380 | 12,
381 | 1
382 | ],
383 | allowHigherOptions
384 | ).map(_ => _.map(toString))
385 | ).toEqual([
386 | [
387 | '11',
388 | '12',
389 | '([1-9]|\\d{2,})'
390 | ],
391 | [
392 | '11',
393 | '(1[3-9]|[2-9]\\d|\\d{3,})',
394 | '\\d+'
395 | ],
396 | [
397 | '(1[2-9]|[2-9]\\d|\\d{3,})',
398 | '\\d+',
399 | '\\d+'
400 | ]
401 | ])
402 | })
403 |
404 | it('should collapse zero tails', () => {
405 | expect(
406 | rangedSemverToRegex(
407 | [
408 | [11, 13],
409 | 12,
410 | 0
411 | ],
412 | allowHigherOptions
413 | ).map(_ => _.map(toString))
414 | ).toEqual([
415 | [
416 | '11',
417 | '(1[2-9]|[2-9]\\d|\\d{3,})',
418 | '\\d+'
419 | ],
420 | [
421 | '(1[2-9]|[2-9]\\d|\\d{3,})',
422 | '\\d+',
423 | '\\d+'
424 | ]
425 | ])
426 | expect(
427 | rangedSemverToRegex(
428 | [
429 | [11, 13],
430 | 0,
431 | 0
432 | ],
433 | allowHigherOptions
434 | ).map(_ => _.map(toString))
435 | ).toEqual([
436 | [
437 | '(1[1-9]|[2-9]\\d|\\d{3,})',
438 | '\\d+',
439 | '\\d+'
440 | ]
441 | ])
442 | })
443 |
444 | it('should respect ignore options', () => {
445 | expect(
446 | rangedSemverToRegex(
447 | [
448 | [11, 13],
449 | 12,
450 | 11
451 | ],
452 | {
453 | allowHigherVersions: true,
454 | ignorePatch: true
455 | }
456 | ).map(_ => _.map(toString))
457 | ).toEqual([
458 | [
459 | '11',
460 | '(1[2-9]|[2-9]\\d|\\d{3,})',
461 | '\\d+'
462 | ],
463 | [
464 | '(1[2-9]|[2-9]\\d|\\d{3,})',
465 | '\\d+',
466 | '\\d+'
467 | ]
468 | ])
469 | expect(
470 | rangedSemverToRegex(
471 | [
472 | [11, 13],
473 | 1,
474 | 2
475 | ],
476 | {
477 | allowHigherVersions: true,
478 | ignoreMinor: true
479 | }
480 | ).map(_ => _.map(toString))
481 | ).toEqual([
482 | [
483 | '(1[1-9]|[2-9]\\d|\\d{3,})',
484 | '\\d+',
485 | '\\d+'
486 | ]
487 | ])
488 | })
489 | })
490 | })
491 | })
492 | })
493 |
--------------------------------------------------------------------------------
/src/versions/utils.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | AstRegExp,
3 | AstNode,
4 | Expression
5 | } from 'regexp-tree/ast'
6 | import type { NodePath } from 'regexp-tree'
7 | import RegexpTree from 'regexp-tree'
8 | import type {
9 | SemverRange,
10 | RangedSemver,
11 | SemverCompareOptions
12 | } from '../semver/index.js'
13 | import {
14 | parseRegex,
15 | isNumberPatternNode,
16 | isExpressionNode,
17 | visitors,
18 | NumberPatternNode,
19 | NumberCharsNode
20 | } from '../regex/index.js'
21 | import { rangeToRegex } from '../numbers/index.js'
22 |
23 | /**
24 | * Get number patterns count from the regex.
25 | * @param regex - Target regex.
26 | * @returns Number patterns count.
27 | */
28 | export function getNumberPatternsCount(regex: string | RegExp | AstNode) {
29 | const regexAst = parseRegex(regex)
30 | let count = 0
31 |
32 | RegexpTree.traverse(regexAst, {
33 | Group(nodePath: NodePath) {
34 | if (isNumberPatternNode(nodePath.node)) {
35 | count++
36 | }
37 | }
38 | })
39 |
40 | return count
41 | }
42 |
43 | /**
44 | * Replace number patterns.
45 | * @param regex - Target regex.
46 | * @param numbers - Number patterns to paste.
47 | * @param numberPatternsCount - Number patterns count to replace.
48 | * @returns Regex with replaced number patterns.
49 | */
50 | export function replaceNumberPatterns(
51 | regex: string | RegExp | AstRegExp,
52 | numbers: Expression[],
53 | numberPatternsCount?: number
54 | ): AstRegExp
55 | export function replaceNumberPatterns(
56 | regex: T,
57 | numbers: Expression[],
58 | numberPatternsCount?: number
59 | ): T
60 |
61 | export function replaceNumberPatterns(
62 | regex: string | RegExp | AstNode,
63 | numbers: Expression[],
64 | numberPatternsCount?: number
65 | ) {
66 | let regexAst = parseRegex(regex)
67 | const numbersToReplace = typeof numberPatternsCount === 'number'
68 | && numberPatternsCount < numbers.length
69 | ? numbers.slice(0, numberPatternsCount)
70 | : numbers.slice()
71 |
72 | RegexpTree.traverse(regexAst, visitors({
73 | every() {
74 | return Boolean(numbersToReplace.length)
75 | },
76 | Group(nodePath: NodePath) {
77 | if (isNumberPatternNode(nodePath.node) && numbersToReplace.length) {
78 | if (regexAst === nodePath.node) {
79 | regexAst = numbersToReplace.shift()
80 | } else {
81 | nodePath.replace(numbersToReplace.shift())
82 | }
83 |
84 | return false
85 | }
86 |
87 | return true
88 | }
89 | }))
90 |
91 | return regexAst
92 | }
93 |
94 | /**
95 | * Get from regex part with number patterns.
96 | * @param regex - Target regex.
97 | * @param numberPatternsCount - Number patterns to extract.
98 | * @returns Regex part with number patterns.
99 | */
100 | export function getNumberPatternsPart(regex: string | RegExp | AstNode, numberPatternsCount?: number): Expression[] {
101 | const regexAst = parseRegex(regex)
102 | const maxNumbersCount = Math.min(
103 | getNumberPatternsCount(regexAst),
104 | numberPatternsCount || Infinity
105 | )
106 | const expressions: Expression[] = []
107 | let numbersCounter = 0
108 | let containsNumberPattern = false
109 |
110 | RegexpTree.traverse(regexAst, visitors({
111 | every: {
112 | pre({ node, parent }) {
113 | if (node === regexAst) {
114 | return true
115 | }
116 |
117 | if (!isExpressionNode(node)) {
118 | return false
119 | }
120 |
121 | if (parent === regexAst) {
122 | containsNumberPattern = false
123 | }
124 |
125 | return numbersCounter < maxNumbersCount
126 | },
127 | post({ node, parent }) {
128 | if (node !== regexAst && parent === regexAst
129 | && isExpressionNode(node)
130 | && (containsNumberPattern || numbersCounter > 0 && numbersCounter < maxNumbersCount)
131 | ) {
132 | expressions.push(node)
133 | }
134 | }
135 | },
136 | Group(nodePath: NodePath) {
137 | if (isNumberPatternNode(nodePath.node) && numbersCounter < maxNumbersCount) {
138 | containsNumberPattern = true
139 | numbersCounter++
140 |
141 | return false
142 | }
143 |
144 | return true
145 | }
146 | }))
147 |
148 | if (expressions.length === 1 && !isNumberPatternNode(expressions[0])) {
149 | return getNumberPatternsPart(expressions[0], maxNumbersCount)
150 | }
151 |
152 | return expressions
153 | }
154 |
155 | /**
156 | * Ranged semver to regex patterns.
157 | * @param rangedVersion - Ranged semver.
158 | * @param options - Semver compare options.
159 | * @returns Array of regex pattern.
160 | */
161 | export function rangedSemverToRegex(rangedVersion: RangedSemver, options: SemverCompareOptions) {
162 | const {
163 | ignoreMinor,
164 | ignorePatch,
165 | allowHigherVersions
166 | } = options
167 | const ignoreIndex = rangedVersion[0] === Infinity
168 | ? 0
169 | : ignoreMinor
170 | ? 1
171 | : ignorePatch
172 | ? 2
173 | : 3
174 |
175 | if (allowHigherVersions) {
176 | const numberPatterns: Expression[][] = []
177 | let prevWasZero = true
178 | let d = 0
179 | let start = 0
180 | const createMapper = (i: number) => (range: SemverRange, j: number) => {
181 | if (j >= ignoreIndex) {
182 | return NumberPatternNode()
183 | }
184 |
185 | start = Array.isArray(range)
186 | ? range[0]
187 | : range
188 |
189 | if (j < i) {
190 | return NumberCharsNode(start)
191 | }
192 |
193 | if (j > i) {
194 | return NumberPatternNode()
195 | }
196 |
197 | return rangeToRegex(start + d)
198 | }
199 |
200 | for (let i = ignoreIndex - 1; i >= 0; i--) {
201 | if (prevWasZero && !rangedVersion[i]) {
202 | continue
203 | }
204 |
205 | prevWasZero = false
206 | numberPatterns.push(rangedVersion.map(createMapper(i)))
207 | d = 1
208 | }
209 |
210 | return numberPatterns
211 | }
212 |
213 | const numberPatterns = rangedVersion.map((range, i) => {
214 | if (i >= ignoreIndex) {
215 | return NumberPatternNode()
216 | }
217 |
218 | if (Array.isArray(range)) {
219 | return rangeToRegex(
220 | range[0],
221 | range[1]
222 | )
223 | }
224 |
225 | return NumberCharsNode(range)
226 | })
227 |
228 | return [numberPatterns]
229 | }
230 |
--------------------------------------------------------------------------------
/src/versions/versions.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import type { BrowserRegex } from '../useragent/types.js'
3 | import {
4 | toString,
5 | toRegex
6 | } from '../regex/index.js'
7 | import {
8 | applyVersionsToRegex,
9 | applyVersionsToRegexes
10 | } from './versions.js'
11 |
12 | describe('Versions', () => {
13 | describe('versions', () => {
14 | describe('applyVersionsToRegex', () => {
15 | it('should work with RegExp', () => {
16 | expect(
17 | toString(applyVersionsToRegex(
18 | /Chrome v(\d+) (\d+) (\d+)/,
19 | [
20 | [
21 | [10, 11],
22 | 0,
23 | 0
24 | ],
25 | [
26 | 64,
27 | 0,
28 | 0
29 | ]
30 | ],
31 | {}
32 | ))
33 | ).toBe(
34 | '/Chrome v(1[01] 0 0|64 0 0)/'
35 | )
36 | })
37 |
38 | it('should work with string', () => {
39 | expect(
40 | toString(applyVersionsToRegex(
41 | 'Chrome v(\\d+) (\\d+) (\\d+)?',
42 | [
43 | [
44 | [10, 11],
45 | 0,
46 | 0
47 | ],
48 | [
49 | 64,
50 | 0,
51 | 0
52 | ]
53 | ],
54 | {}
55 | ))
56 | ).toBe(
57 | '/Chrome v(1[01] 0 0?|64 0 0?)/'
58 | )
59 | })
60 |
61 | it('should apply semver compare options', () => {
62 | expect(
63 | toString(applyVersionsToRegex(
64 | 'Chrome v(\\d+)',
65 | [
66 | [
67 | [10, 11],
68 | 0,
69 | 0
70 | ],
71 | [
72 | 64,
73 | 2,
74 | 2
75 | ],
76 | [
77 | 64,
78 | 0,
79 | 0
80 | ]
81 | ],
82 | {
83 | ignorePatch: true,
84 | allowZeroSubversions: true,
85 | allowHigherVersions: true
86 | }
87 | ))
88 | ).toBe(
89 | '/Chrome v([1-9]\\d|\\d{3,})/'
90 | )
91 | })
92 |
93 | it('should apply semver to match higher version ', () => {
94 | expect(
95 | toString(applyVersionsToRegex(
96 | 'Chrome v(\\d+) (\\d+)',
97 | [
98 | [
99 | 8,
100 | 2,
101 | 0
102 | ]
103 | ],
104 | {
105 | ignorePatch: true,
106 | allowZeroSubversions: true,
107 | allowHigherVersions: true
108 | }
109 | ))
110 | ).toBe(
111 | '/Chrome v(8 ([2-9]|\\d{2,})|(9|\\d{2,}) \\d+)/'
112 | )
113 | })
114 | })
115 |
116 | describe('applyVersionsToRegexes', () => {
117 | const regexes: BrowserRegex[] = [
118 | {
119 | family: 'chrome',
120 | regex: /Chrome (\d+) (\d+) (\d+)/,
121 | requestVersions: [[64, 0, 0], [73, 0, 0]],
122 | matchedVersions: [[64, 0, 0], [73, 0, 0]]
123 | },
124 | {
125 | family: 'firefox',
126 | regex: /FF/,
127 | requestVersions: [[1, 2, 3]],
128 | matchedVersions: [[1, 2, 3]]
129 | },
130 | {
131 | family: 'ie',
132 | regex: /lol serious?/,
133 | requestVersions: [[5, 0, 0]],
134 | version: [5, 0, 0],
135 | matchedVersions: [[5, 0, 0]]
136 | }
137 | ]
138 |
139 | it('should return versioned RegExp objects with info', () => {
140 | expect(
141 | applyVersionsToRegexes(
142 | regexes,
143 | {
144 | allowZeroSubversions: true,
145 | allowHigherVersions: true
146 | }
147 | ).map(({ regexAst, ...regex }) => ({
148 | ...regex,
149 | regex: toRegex(regexAst)
150 | }))
151 | ).toEqual([
152 | {
153 | ...regexes[0],
154 | sourceRegex: regexes[0].regex,
155 | regex: /Chrome (6[4-9]|[7-9]\d|\d{3,}) (\d+) (\d+)/
156 | },
157 | {
158 | ...regexes[1],
159 | sourceRegex: regexes[1].regex,
160 | regex: /FF/
161 | },
162 | {
163 | ...regexes[2],
164 | sourceRegex: regexes[2].regex,
165 | regex: /lol serious?/
166 | }
167 | ])
168 | })
169 | })
170 | })
171 | })
172 |
--------------------------------------------------------------------------------
/src/versions/versions.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | AstRegExp,
3 | AstNode
4 | } from 'regexp-tree/ast'
5 | import RegexpTree from 'regexp-tree'
6 | import type {
7 | BrowserRegex,
8 | BrowserVersionedRegex
9 | } from '../useragent/types.js'
10 | import { clone } from '../utils/index.js'
11 | import type {
12 | RangedSemver,
13 | SemverCompareOptions
14 | } from '../semver/index.js'
15 | import { getRequiredSemverPartsCount } from '../semver/index.js'
16 | import { versionsListToRanges } from '../browsers/index.js'
17 | import {
18 | parseRegex,
19 | AlternativeNode,
20 | DisjunctionCapturingGroupNode,
21 | visitors
22 | } from '../regex/index.js'
23 | import {
24 | getNumberPatternsPart,
25 | replaceNumberPatterns,
26 | rangedSemverToRegex
27 | } from './utils.js'
28 |
29 | /**
30 | * Apply ranged sevmers to the regex.
31 | * @param regex - Target regex.
32 | * @param versions - Ranged semvers.
33 | * @param options - Semver compare options.
34 | * @returns Regex with given versions.
35 | */
36 | export function applyVersionsToRegex(
37 | regex: string | RegExp | AstRegExp,
38 | versions: RangedSemver[],
39 | options: SemverCompareOptions
40 | ): AstRegExp
41 | export function applyVersionsToRegex(
42 | regex: T,
43 | versions: RangedSemver[],
44 | options: SemverCompareOptions
45 | ): T
46 |
47 | export function applyVersionsToRegex(
48 | regex: string | RegExp | AstNode,
49 | versions: RangedSemver[],
50 | options: SemverCompareOptions
51 | ) {
52 | const { allowHigherVersions } = options
53 | const regexAst = parseRegex(regex)
54 | const finalVersions = allowHigherVersions && versions.length
55 | ? [versions[0]]
56 | : versions
57 | const maxRequiredPartsCount = finalVersions.reduce(
58 | (maxRequiredPartsCount, version) => Math.max(
59 | maxRequiredPartsCount,
60 | getRequiredSemverPartsCount(version, options)
61 | ),
62 | 1
63 | )
64 | const numberPatternsPart = getNumberPatternsPart(regexAst, maxRequiredPartsCount)
65 | const versionsPart = DisjunctionCapturingGroupNode(
66 | ...finalVersions.map(
67 | version => rangedSemverToRegex(version, options)
68 | .map(parts => replaceNumberPatterns(
69 | AlternativeNode(clone(numberPatternsPart)),
70 | parts,
71 | maxRequiredPartsCount
72 | ))
73 | )
74 | )
75 |
76 | RegexpTree.traverse(regexAst, visitors({
77 | every(nodePath) {
78 | if (!numberPatternsPart.length) {
79 | return false
80 | }
81 |
82 | if (nodePath.node === numberPatternsPart[0]) {
83 | if (numberPatternsPart.length === 1) {
84 | nodePath.replace(versionsPart)
85 | } else {
86 | nodePath.remove()
87 | }
88 |
89 | numberPatternsPart.shift()
90 | }
91 |
92 | return true
93 | }
94 | }))
95 |
96 | return regexAst
97 | }
98 |
99 | /**
100 | * Apply browser versions to info objects.
101 | * @param browserRegexes - Objects with requested browser version and regex.
102 | * @param options - Semver compare options.
103 | * @returns Objects with requested browser version and regex special for this version.
104 | */
105 | export function applyVersionsToRegexes(
106 | browserRegexes: BrowserRegex[],
107 | options: SemverCompareOptions
108 | ): BrowserVersionedRegex[] {
109 | return browserRegexes.map(({
110 | regex: sourceRegex,
111 | version,
112 | maxVersion,
113 | matchedVersions,
114 | ...other
115 | }) => {
116 | let regexAst = parseRegex(sourceRegex)
117 |
118 | if (!version) {
119 | regexAst = applyVersionsToRegex(
120 | regexAst,
121 | versionsListToRanges(matchedVersions),
122 | {
123 | ...options,
124 | allowHigherVersions: !maxVersion && options.allowHigherVersions
125 | }
126 | )
127 | }
128 |
129 | return {
130 | regex: null,
131 | sourceRegex,
132 | regexAst,
133 | version,
134 | maxVersion,
135 | matchedVersions,
136 | ...other
137 | }
138 | })
139 | }
140 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "include": [
4 | "../src/**/*.spec.ts",
5 | "."
6 | ],
7 | "exclude": []
8 | }
9 |
--------------------------------------------------------------------------------
/test/useragents.ts:
--------------------------------------------------------------------------------
1 | export const useragents = [
2 | /**
3 | * IE 6
4 | */
5 | {
6 | ua: 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)',
7 | yes: ['ie 6', 'ie >= 5', 'ie 6-7']
8 | },
9 | /**
10 | * IE 7
11 | */
12 | {
13 | ua: 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)',
14 | yes: ['ie 7', 'ie >= 7', 'ie 6-7']
15 | },
16 | /**
17 | * IE 8
18 | */
19 | {
20 | ua: 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)',
21 | yes: ['ie 8', 'ie >= 8', 'ie 8-9']
22 | },
23 | /**
24 | * IE 8 Compatability Mode
25 | */
26 | {
27 | ua: 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)',
28 | yes: ['ie 8', 'ie >= 8', 'ie 8-9']
29 | },
30 | /**
31 | * IE 9
32 | */
33 | {
34 | ua: 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
35 | yes: ['ie 9', 'ie >= 9', 'ie 8-9']
36 | },
37 | /**
38 | * IE 9 Compatability Mode
39 | */
40 | {
41 | ua: 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E)',
42 | yes: ['ie 9', 'ie >= 9', 'ie 8-9']
43 | },
44 | /**
45 | * IE 10
46 | */
47 | {
48 | ua: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)',
49 | yes: ['ie 10', 'ie >= 10', 'ie 10-11']
50 | },
51 | {
52 | ua: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Win64; x64; Trident/6.0)',
53 | yes: ['ie 10', 'ie >= 10', 'ie 10-11']
54 | },
55 | /**
56 | * IE 10 Compatability Mode
57 | */
58 | {
59 | ua: 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E)',
60 | yes: ['ie 10', 'ie >= 10', 'ie 10-11']
61 | },
62 | /**
63 | * IE 11
64 | */
65 | {
66 | ua: 'Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; rv:11.0) like Gecko',
67 | yes: ['ie 11', 'ie >= 11', 'ie 10-11']
68 | },
69 | /**
70 | * IE 11 Compatability Mode
71 | */
72 | {
73 | ua: 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729)',
74 | yes: ['ie 11', 'ie >= 11', 'ie 10-11']
75 | },
76 | /**
77 | * Edge on EdgeHTML
78 | */
79 | {
80 | ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063',
81 | yes: ['edge 15', 'edge >= 15', 'edge 15-18'],
82 | no: ['chrome >= 50']
83 | },
84 | {
85 | ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18363',
86 | yes: ['edge 18', 'edge >= 18', 'edge 15-18'],
87 | no: ['chrome >= 50']
88 | },
89 | /**
90 | * Edge on Chromium
91 | */
92 | {
93 | ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36 Edg/80.0.361.62',
94 | yes: ['edge 80', 'edge >= 80', 'chrome 80']
95 | },
96 | {
97 | ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 Edg/105.0.1343.25',
98 | yes: ['edge 105', 'edge >= 105', 'chrome 105']
99 | },
100 | /**
101 | * Edge bug #1530
102 | */
103 | {
104 | ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 Edge/120.0',
105 | allowHigherVersions: false,
106 | yes: ['edge 120'],
107 | no: ['edge 12']
108 | },
109 | /**
110 | * Firefox Desktop
111 | */
112 | {
113 | ua: 'Mozilla/5.0 (Windows NT 5.2; rv:42.0) Gecko/20100101 Firefox/42.0',
114 | yes: ['firefox >= 40']
115 | },
116 | /**
117 | * Firefox Desktop bug #1530
118 | */
119 | {
120 | ua: 'Mozilla/5.0 (Windows NT 5.2; rv:42.0) Gecko/20100101 Firefox/120.0',
121 | allowHigherVersions: false,
122 | yes: ['firefox 120'],
123 | no: ['firefox 12']
124 | },
125 | /**
126 | * Chrome Desktop
127 | */
128 | {
129 | ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36',
130 | yes: ['chrome >= 60']
131 | },
132 | /**
133 | * Chrome Desktop bug #1530
134 | */
135 | {
136 | ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36',
137 | allowHigherVersions: false,
138 | yes: ['chrome 120'],
139 | no: ['chrome 12']
140 | },
141 | /**
142 | * Safari Desktop
143 | */
144 | {
145 | ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Safari/605.1.15',
146 | yes: ['safari 15']
147 | },
148 | /**
149 | * Safari iPad Desktop-like
150 | */
151 | {
152 | ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Safari/605.1.15',
153 | yes: ['safari 14']
154 | },
155 | {
156 | // Weird case with comma
157 | ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6,2 Safari/605.1.15',
158 | yes: ['safari 15']
159 | },
160 | /**
161 | * Opera Desktop on Presto
162 | */
163 | {
164 | ua: 'Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11',
165 | yes: ['opera 11']
166 | },
167 | {
168 | ua: 'Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8) Presto/2.12.388 Version/12.15',
169 | yes: ['opera 12']
170 | },
171 | /**
172 | * Opera Desktop on Chromium
173 | */
174 | {
175 | ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36 OPR/15.0.1147.153',
176 | yes: ['opera 15']
177 | },
178 | {
179 | ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 OPR/91.0.4516.16',
180 | yes: ['opera 91', 'chrome 105']
181 | },
182 | /**
183 | * iOS iPhone Safari
184 | */
185 | {
186 | ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1',
187 | yes: ['ios_saf 15']
188 | },
189 | /**
190 | * iOS iPhone App (WKWebView)
191 | */
192 | {
193 | ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148',
194 | yes: ['ios_saf 15']
195 | },
196 | /**
197 | * Opera Mini
198 | */
199 | {
200 | ua: 'Opera/9.80 (Android; Opera Mini/12.0.1987/37.7327; U; pl) Presto/2.12.423 Version/12.16',
201 | yes: ['op_mini all']
202 | },
203 | /**
204 | * Android
205 | */
206 | {
207 | ua: 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; SCH-I535 Build/KOT49H) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
208 | yes: ['android 4']
209 | },
210 | /**
211 | * Opera Mobile
212 | */
213 | {
214 | ua: 'Opera/9.80 (S60; SymbOS; Opera Mobi/SYB-1107071606; U; en) Presto/2.8.149 Version/11.10',
215 | yes: ['op_mob 11']
216 | },
217 | {
218 | ua: 'Opera/12.02 (Android 4.1; Linux; Opera Mobi/ADR-1111101157; U; en-US) Presto/2.9.201 Version/12.02',
219 | yes: ['op_mob 12']
220 | },
221 | /**
222 | * IE Mobile
223 | */
224 | {
225 | ua: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 920)',
226 | yes: ['ie_mob >= 1']
227 | },
228 | {
229 | ua: 'Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 635) like iPhone OS 7_0_3 Mac OS X AppleWebKit/537 (KHTML, like Gecko) Mobile Safari/537',
230 | yes: ['ie_mob >= 1'],
231 | no: ['android 4', 'ios 7']
232 | }
233 | ]
234 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Type Checking */
4 | "strict": false, /* @todo Enable */
5 | "strictBindCallApply": true,
6 | "noFallthroughCasesInSwitch": true,
7 | "noImplicitOverride": true,
8 | "noImplicitReturns": true,
9 | "noUnusedLocals": true,
10 | "noUnusedParameters": true,
11 | /* Modules */
12 | "module": "NodeNext",
13 | "moduleResolution": "NodeNext",
14 | "resolveJsonModule": true,
15 | /* Emit */
16 | "declaration": true,
17 | "declarationMap": true,
18 | "inlineSourceMap": true,
19 | "outDir": "dist",
20 | /* Interop Constraints */
21 | "allowSyntheticDefaultImports": true,
22 | "isolatedModules": true,
23 | "verbatimModuleSyntax": true,
24 | /* Language and Environment */
25 | "lib": [
26 | "esnext"
27 | ],
28 | "target": "ESNext",
29 | /* Completeness */
30 | "skipLibCheck": true
31 | },
32 | "include": [
33 | "src"
34 | ],
35 | "exclude": [
36 | "**/*.spec.ts"
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 |
3 | export default defineConfig({
4 | test: {
5 | coverage: {
6 | include: ['src/**/*'],
7 | reporter: ['lcovonly', 'text']
8 | }
9 | }
10 | })
11 |
--------------------------------------------------------------------------------