mirror of
https://github.com/lancedikson/bowser
synced 2026-09-23 20:44:24 +00:00
Dual packaging, without breaking existing consumers (#628)
Co-authored-by: Yasumasa Ashida <ys.ashida@gmail.com>
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
parser: babel-eslint
|
||||
extends: airbnb-base
|
||||
rules:
|
||||
no-underscore-dangle: 0
|
||||
no-void: 0
|
||||
import/extensions:
|
||||
- 'error'
|
||||
- 'ignorePackages'
|
||||
- {js: 'always'}
|
||||
import/prefer-default-export: 1
|
||||
|
||||
plugins:
|
||||
- ava
|
||||
- import
|
||||
33
.github/workflows/merge-to-master.yml
vendored
33
.github/workflows/merge-to-master.yml
vendored
@@ -8,30 +8,29 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.16.3]
|
||||
|
||||
steps:
|
||||
- name: Get branch name (merge)
|
||||
if: github.event_name != 'pull_request'
|
||||
- name: Get branch name
|
||||
shell: bash
|
||||
run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/} | tr / -)" >> $GITHUB_ENV
|
||||
|
||||
- name: Get branch name (pull request)
|
||||
if: github.event_name == 'pull_request'
|
||||
shell: bash
|
||||
run: echo "BRANCH_NAME=$(echo ${GITHUB_HEAD_REF} | tr / -)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
# tsdown requires Node ^22.18.0 || >=24.11.0. The published artifacts are
|
||||
# still ES5 — see the `pack-smoke` job in pull-request.yml.
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm i -g nyc@15
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: nyc npm test && nyc report --reporter=text-lcov | ./node_modules/coveralls/bin/coveralls.js
|
||||
node-version: "24"
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
|
||||
# `pnpm test` already runs ava under nyc; this only reports the result.
|
||||
- run: pnpm test && pnpm exec nyc report --reporter=text-lcov | pnpm exec coveralls
|
||||
env:
|
||||
COVERALLS_SERVICE_NAME: GithubActions
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||
|
||||
43
.github/workflows/publish.yml
vendored
43
.github/workflows/publish.yml
vendored
@@ -21,13 +21,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
# tsdown requires Node ^22.18.0 || >=24.11.0. This only affects how the
|
||||
# package is built; the artifacts it emits are still ES5.
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 12.16.3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
node-version: "24"
|
||||
cache: pnpm
|
||||
|
||||
# Store the release version (from release tag or manual input)
|
||||
- name: Set release version
|
||||
run: |
|
||||
@@ -38,14 +42,33 @@ jobs:
|
||||
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
echo "Release triggered from tag: ${GITHUB_REF#refs/*/}"
|
||||
fi
|
||||
- run: npm ci
|
||||
- run: npm version $RELEASE_VERSION --no-git-tag-version
|
||||
- run: npm run build
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: npm version $RELEASE_VERSION --no-git-tag-version --allow-same-version
|
||||
- run: pnpm build
|
||||
|
||||
# Guard the release: the same checks the PR workflow runs, against the
|
||||
# artifacts actually about to be published.
|
||||
- run: pnpm test
|
||||
- run: pnpm exec publint
|
||||
- run: pnpm exec attw --pack . --profile node16 --entrypoints .
|
||||
|
||||
# Upload only what `files` publishes, plus the manifest. The previous
|
||||
# `path: .` also shipped node_modules through the artifact store.
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: .
|
||||
path: |
|
||||
package.json
|
||||
README.md
|
||||
LICENSE
|
||||
es5.js
|
||||
bundled.js
|
||||
bowser.mjs
|
||||
index.d.ts
|
||||
index.d.mts
|
||||
src
|
||||
|
||||
publish:
|
||||
needs: build
|
||||
@@ -61,4 +84,4 @@ jobs:
|
||||
- name: Update npm to latest (trusted publishing requires npm >= 11.5.1)
|
||||
run: npm install -g npm@latest
|
||||
- name: Publish to npm
|
||||
run: npm publish --provenance --access public
|
||||
run: npm publish --provenance --access public
|
||||
|
||||
140
.github/workflows/pull-request.yml
vendored
140
.github/workflows/pull-request.yml
vendored
@@ -3,42 +3,128 @@ on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12.16.3]
|
||||
name: Node ${{ matrix.node }}
|
||||
steps:
|
||||
- name: "Checkout latest code"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
# Every job only reads the repo and moves artifacts between jobs; none of them
|
||||
# needs write access to anything.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
lint:
|
||||
name: "ESLint"
|
||||
env:
|
||||
# tsdown requires Node ^22.18.0 || >=24.11.0. This is the *build* toolchain
|
||||
# only — see the `pack-smoke` job for the versions the published package
|
||||
# itself has to keep working on.
|
||||
BUILD_NODE_VERSION: "24"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build & test"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout latest code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "16"
|
||||
node-version: ${{ env.BUILD_NODE_VERSION }}
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run lint:check
|
||||
run: pnpm lint:check
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
|
||||
# package.json intentionally carries no `version` — it is stamped at
|
||||
# release time by publish.yml. `npm pack` needs one, so use a throwaway.
|
||||
- name: Pack
|
||||
run: |
|
||||
npm version 0.0.0-ci --no-git-tag-version --allow-same-version
|
||||
mkdir -p tarball
|
||||
npm pack --pack-destination ./tarball
|
||||
|
||||
- name: Upload package tarball
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: package-tarball
|
||||
path: tarball/*.tgz
|
||||
|
||||
pack-smoke:
|
||||
# Installs the packed tarball and exercises every documented entry point.
|
||||
# bowser publishes ES5 artifacts and has no `engines` field, so the package
|
||||
# must keep working far below the version we build on.
|
||||
name: "Consumer smoke (Node ${{ matrix.node }})"
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: ["12.16.3", "14", "18", "20", "24"]
|
||||
steps:
|
||||
- name: Checkout latest code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Download package tarball
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: package-tarball
|
||||
path: tarball
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
# No install step: bowser has no runtime dependencies and the smoke test
|
||||
# only uses Node built-ins, so this runs on every version above.
|
||||
- name: Run package smoke test
|
||||
run: node test/package/smoke.cjs tarball/*.tgz
|
||||
|
||||
package-lint:
|
||||
name: "Package manifest"
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout latest code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.BUILD_NODE_VERSION }}
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Stamp a throwaway version
|
||||
run: npm version 0.0.0-ci --no-git-tag-version --allow-same-version
|
||||
|
||||
- name: publint
|
||||
run: pnpm exec publint
|
||||
|
||||
# Scoped to the root entrypoint. The legacy `bowser/src/*` subpaths are
|
||||
# untyped ESM sources in a CJS package — true on every published version
|
||||
# of bowser, and preserved here on purpose.
|
||||
- name: Are the types wrong?
|
||||
run: pnpm exec attw --pack . --profile node16 --entrypoints .
|
||||
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -2,6 +2,13 @@ node_modules/
|
||||
.idea/
|
||||
.nyc_output
|
||||
coverage
|
||||
dist
|
||||
bundled.js*
|
||||
es5.js*
|
||||
|
||||
# Build output (see tsdown.config.ts). Anchored to the repo root on purpose —
|
||||
# unanchored these would also match the build entry points in build/entries/.
|
||||
/bundled.js*
|
||||
/es5.js*
|
||||
/bowser.mjs*
|
||||
|
||||
# `npm pack` output, used by the package smoke test
|
||||
*.tgz
|
||||
/tarball/
|
||||
|
||||
27
README.md
27
README.md
@@ -27,7 +27,9 @@ _For legacy code, check out the [1.x](https://github.com/bowser-js/bowser/tree/v
|
||||
|
||||
# Use cases
|
||||
|
||||
First of all, require the library. This is a UMD Module, so it will work for AMD, TypeScript, ES6, and CommonJS module systems.
|
||||
First of all, require the library. Bowser is a dual package: `require` resolves
|
||||
to a UMD build (which also works for AMD and as a plain `<script>` tag), and
|
||||
`import` resolves to a real ES module.
|
||||
|
||||
```javascript
|
||||
const Bowser = require("bowser"); // CommonJS
|
||||
@@ -37,11 +39,30 @@ import * as Bowser from "bowser"; // TypeScript
|
||||
import Bowser from "bowser"; // ES6 (and TypeScript with --esModuleInterop enabled)
|
||||
```
|
||||
|
||||
The ES module build also exposes `parse` and `getParser` as named exports, so
|
||||
you can import just the part you use and let your bundler drop the rest:
|
||||
|
||||
```javascript
|
||||
import { getParser, parse } from "bowser";
|
||||
|
||||
const browser = getParser(window.navigator.userAgent);
|
||||
```
|
||||
|
||||
Loaded from a CDN or a `<script>` tag, Bowser attaches itself to the global as
|
||||
`bowser` (lowercase):
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/bowser@2/es5.js"></script>
|
||||
<script>
|
||||
console.log(bowser.parse(window.navigator.userAgent));
|
||||
</script>
|
||||
```
|
||||
|
||||
By default, the exported version is the *ES5 transpiled version*, which **do not** include any polyfills.
|
||||
|
||||
In case you don't use your own `babel-polyfill` you may need to have pre-built bundle with all needed polyfills.
|
||||
In case you don't use your own polyfills you may need to have pre-built bundle with all needed polyfills.
|
||||
So, for you it's suitable to require bowser like this: `require('bowser/bundled')`.
|
||||
As the result, you get a ES5 version of bowser with `babel-polyfill` bundled together.
|
||||
As the result, you get a ES5 version of bowser with `core-js` polyfills bundled together.
|
||||
|
||||
You may need to use the source files, so they will be available in the package as well.
|
||||
|
||||
|
||||
16
build/entries/bundled.js
Normal file
16
build/entries/bundled.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* UMD entry point for `bundled.js` — same as `es5.js`, but with the polyfills
|
||||
* needed by the browser targets baked in.
|
||||
*
|
||||
* `core-js/stable` + `regenerator-runtime/runtime` is the core-js@3 equivalent
|
||||
* of the deprecated `@babel/polyfill` that this bundle used to be built from.
|
||||
* `@babel/preset-env`'s `useBuiltIns: 'entry'` rewrites the `core-js/stable`
|
||||
* import below into just the polyfills the configured targets actually need.
|
||||
*
|
||||
* See `./es5.js` for why this only re-exports the default.
|
||||
*/
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
import Bowser from '../../src/bowser.js';
|
||||
|
||||
export default Bowser;
|
||||
15
build/entries/es5.js
Normal file
15
build/entries/es5.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* UMD entry point for `es5.js`.
|
||||
*
|
||||
* `src/bowser.js` also has named exports (`parse`, `getParser`) so that ESM
|
||||
* consumers can `import { getParser } from 'bowser'`. A UMD bundle with named
|
||||
* exports would expose `module.exports = { default, parse, getParser }`, which
|
||||
* would break every existing `require('bowser')` and `window.bowser` consumer.
|
||||
*
|
||||
* Re-exporting only the default here keeps the UMD output at
|
||||
* `module.exports = Bowser` / `window.bowser = Bowser`. The class carries
|
||||
* `parse` and `getParser` as static methods, so nothing is lost.
|
||||
*/
|
||||
import Bowser from '../../src/bowser.js';
|
||||
|
||||
export default Bowser;
|
||||
36
eslint.config.mjs
Normal file
36
eslint.config.mjs
Normal file
@@ -0,0 +1,36 @@
|
||||
import js from '@eslint/js';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import { configs, plugins } from 'eslint-config-airbnb-extended';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
importPlugin.flatConfigs.recommended,
|
||||
// Stylistic plugin
|
||||
plugins.stylistic,
|
||||
// Import X plugin
|
||||
plugins.importX,
|
||||
// Airbnb base recommended config
|
||||
...configs.base.recommended,
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
'no-underscore-dangle': 0,
|
||||
'no-void': 0,
|
||||
'import/prefer-default-export': 1,
|
||||
'import-x/extensions': [
|
||||
'error',
|
||||
'always',
|
||||
{
|
||||
ignorePackages: true,
|
||||
js: 'always',
|
||||
},
|
||||
],
|
||||
'@stylistic/max-len': 0,
|
||||
'@stylistic/arrow-parens': 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
33
index.d.mts
Normal file
33
index.d.mts
Normal file
@@ -0,0 +1,33 @@
|
||||
// ESM type definitions for Bowser v2.
|
||||
//
|
||||
// `index.d.ts` uses `export =`, which describes the CommonJS/UMD shape of
|
||||
// `es5.js` (`module.exports = Bowser`). It is reached via the `require`
|
||||
// condition and stays the source of truth for the type declarations.
|
||||
//
|
||||
// This file describes `bowser.mjs`, which is a real ES module: it has a default
|
||||
// export *and* the `parse` / `getParser` named exports. It is reached via the
|
||||
// `import` condition. Declaring it separately is what keeps the types honest
|
||||
// for `moduleResolution: node16`/`bundler` consumers — reusing `index.d.ts` for
|
||||
// both conditions would describe an ES module with CommonJS types.
|
||||
|
||||
import Bowser = require('./index.js');
|
||||
|
||||
export default Bowser;
|
||||
|
||||
export declare const parse: typeof Bowser.parse;
|
||||
export declare const getParser: typeof Bowser.getParser;
|
||||
|
||||
// BROWSER_MAP / ENGINE_MAP / OS_MAP / PLATFORMS_MAP are *not* named exports of
|
||||
// bowser.mjs — they exist only as static getters on the class. Declaring them
|
||||
// here would let TypeScript accept `import { BROWSER_MAP } from 'bowser'`,
|
||||
// which throws at runtime. Reach them via the default export instead.
|
||||
|
||||
export type ClientHints = Bowser.ClientHints;
|
||||
export type Parser = Bowser.Parser.Parser;
|
||||
export type ParsedResult = Bowser.Parser.ParsedResult;
|
||||
export type Details = Bowser.Parser.Details;
|
||||
export type BrowserDetails = Bowser.Parser.BrowserDetails;
|
||||
export type EngineDetails = Bowser.Parser.EngineDetails;
|
||||
export type OSDetails = Bowser.Parser.OSDetails;
|
||||
export type PlatformDetails = Bowser.Parser.PlatformDetails;
|
||||
export type checkTree = Bowser.Parser.checkTree;
|
||||
14506
package-lock.json
generated
14506
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
98
package.json
98
package.json
@@ -2,10 +2,12 @@
|
||||
"name": "bowser",
|
||||
"description": "Lightweight browser detector",
|
||||
"files": [
|
||||
"src/",
|
||||
"src",
|
||||
"es5.js",
|
||||
"bundled.js",
|
||||
"index.d.ts"
|
||||
"bowser.mjs",
|
||||
"index.d.ts",
|
||||
"index.d.mts"
|
||||
],
|
||||
"keywords": [
|
||||
"browser",
|
||||
@@ -33,41 +35,85 @@
|
||||
"browser": "es5.js",
|
||||
"module": "src/bowser.js",
|
||||
"types": "index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./index.d.mts",
|
||||
"default": "./bowser.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./index.d.ts",
|
||||
"default": "./es5.js"
|
||||
},
|
||||
"default": "./es5.js"
|
||||
},
|
||||
"./es5.js": "./es5.js",
|
||||
"./es5": "./es5.js",
|
||||
"./bundled.js": "./bundled.js",
|
||||
"./bundled": "./bundled.js",
|
||||
"./bowser.mjs": {
|
||||
"types": "./index.d.mts",
|
||||
"default": "./bowser.mjs"
|
||||
},
|
||||
"./src/bowser.js": "./src/bowser.js",
|
||||
"./src/bowser": "./src/bowser.js",
|
||||
"./src/constants.js": "./src/constants.js",
|
||||
"./src/constants": "./src/constants.js",
|
||||
"./src/parser.js": "./src/parser.js",
|
||||
"./src/parser": "./src/parser.js",
|
||||
"./src/parser-browsers.js": "./src/parser-browsers.js",
|
||||
"./src/parser-browsers": "./src/parser-browsers.js",
|
||||
"./src/parser-engines.js": "./src/parser-engines.js",
|
||||
"./src/parser-engines": "./src/parser-engines.js",
|
||||
"./src/parser-os.js": "./src/parser-os.js",
|
||||
"./src/parser-os": "./src/parser-os.js",
|
||||
"./src/parser-platforms.js": "./src/parser-platforms.js",
|
||||
"./src/parser-platforms": "./src/parser-platforms.js",
|
||||
"./src/utils.js": "./src/utils.js",
|
||||
"./src/utils": "./src/utils.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/bowser-js/bowser.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.11.6",
|
||||
"@babel/core": "^7.8.0",
|
||||
"@babel/polyfill": "^7.8.3",
|
||||
"@babel/preset-env": "^7.8.2",
|
||||
"@babel/register": "^7.8.3",
|
||||
"@arethetypeswrong/cli": "^0.18.5",
|
||||
"@babel/cli": "^7.29.7",
|
||||
"@babel/core": "^7.29.7",
|
||||
"@babel/preset-env": "^7.29.7",
|
||||
"@babel/register": "^7.29.7",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@rolldown/plugin-babel": "0.2.3",
|
||||
"ava": "^3.0.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-add-module-exports": "^1.0.2",
|
||||
"babel-plugin-istanbul": "^6.0.0",
|
||||
"compression-webpack-plugin": "^4.0.0",
|
||||
"babel-plugin-add-module-exports": "^1.0.4",
|
||||
"babel-plugin-istanbul": "^8.0.0",
|
||||
"core-js": "^3.49.0",
|
||||
"coveralls": "^3.0.6",
|
||||
"docdash": "^1.1.1",
|
||||
"eslint": "^6.5.1",
|
||||
"eslint-config-airbnb-base": "^13.2.0",
|
||||
"eslint-plugin-ava": "^10.0.0",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint": "^10.8.0",
|
||||
"eslint-config-airbnb-extended": "^3.1.0",
|
||||
"eslint-plugin-ava": "^17.0.1",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"gh-pages": "^3.0.0",
|
||||
"jsdoc": "^3.6.3",
|
||||
"nyc": "^15.0.0",
|
||||
"sinon": "^9.0.0",
|
||||
"nyc": "^18.0.0",
|
||||
"publint": "^0.3.22",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"sinon": "^22.0.0",
|
||||
"terser": "^5.49.0",
|
||||
"testem": "^3.0.0",
|
||||
"webpack": "^4.41.0",
|
||||
"webpack-bundle-analyzer": "^3.5.2",
|
||||
"webpack-cli": "^3.3.9",
|
||||
"tsdown": "^0.22.14",
|
||||
"typescript": "^5.9.3",
|
||||
"yamljs": "^0.3.0"
|
||||
},
|
||||
"ava": {
|
||||
"require": [
|
||||
"@babel/register"
|
||||
],
|
||||
"files": [
|
||||
"test/**/*.js",
|
||||
"!test/package/**"
|
||||
]
|
||||
},
|
||||
"bugs": {
|
||||
@@ -77,16 +123,18 @@
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"generate-and-deploy-docs": "npm run generate-docs && gh-pages --dist docs --dest docs",
|
||||
"watch": "webpack --watch --config webpack.config.js",
|
||||
"build": "tsdown",
|
||||
"generate-and-deploy-docs": "pnpm generate-docs && gh-pages --dist docs --dest docs",
|
||||
"watch": "tsdown --watch",
|
||||
"lint:check": "eslint ./src",
|
||||
"lint:fix": "eslint --fix ./src",
|
||||
"testem": "testem",
|
||||
"test": "nyc --reporter=html --reporter=text ava",
|
||||
"test:watch": "ava --watch",
|
||||
"test:package": "node test/package/smoke.cjs",
|
||||
"coverage": "nyc report --reporter=text-lcov | coveralls",
|
||||
"generate-docs": "jsdoc -c jsdoc.json"
|
||||
},
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@11.18.0"
|
||||
}
|
||||
|
||||
9770
pnpm-lock.yaml
generated
Normal file
9770
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
allowBuilds:
|
||||
core-js: false
|
||||
unrs-resolver: false
|
||||
@@ -91,3 +91,4 @@ class Bowser {
|
||||
}
|
||||
|
||||
export default Bowser;
|
||||
export const { parse, getParser } = Bowser;
|
||||
|
||||
@@ -210,7 +210,6 @@ class Parser {
|
||||
return this.getBrowser().name || '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get browser's version
|
||||
* @return {String} version of browser
|
||||
|
||||
192
test/package/assertions.cjs
Normal file
192
test/package/assertions.cjs
Normal file
@@ -0,0 +1,192 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Runs *inside* a throwaway directory that has the packed bowser tarball
|
||||
* extracted into ./node_modules/bowser, so that bare specifiers resolve the way
|
||||
* they would for a real consumer.
|
||||
*
|
||||
* Deliberately written as ES5-compatible CommonJS: this file has to run on
|
||||
* every Node version bowser claims to support, down to 12.16.3. No optional
|
||||
* chaining, no nullish coalescing, no top-level await.
|
||||
*/
|
||||
var assert = require('assert');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var vm = require('vm');
|
||||
|
||||
var UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 '
|
||||
+ '(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
||||
|
||||
var passed = 0;
|
||||
function check(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok ' + name);
|
||||
}
|
||||
|
||||
/** Node version gate — ESM was unflagged in 12.17.0. */
|
||||
function nodeAtLeast(major, minor) {
|
||||
var parts = process.versions.node.split('.');
|
||||
var m = Number(parts[0]);
|
||||
var n = Number(parts[1]);
|
||||
return m > major || (m === major && n >= minor);
|
||||
}
|
||||
|
||||
console.log('bowser package smoke test on Node ' + process.versions.node);
|
||||
|
||||
// --- The CommonJS contract -------------------------------------------------
|
||||
// `require('bowser')` must be the Bowser class itself. If it ever becomes
|
||||
// `{ default, parse, getParser }`, every existing consumer breaks.
|
||||
|
||||
check('require("bowser") is the class, not a module namespace', function () {
|
||||
var Bowser = require('bowser');
|
||||
assert.strictEqual(typeof Bowser, 'function');
|
||||
assert.strictEqual(typeof Bowser.getParser, 'function');
|
||||
assert.strictEqual(typeof Bowser.parse, 'function');
|
||||
assert.strictEqual(
|
||||
Bowser.default,
|
||||
undefined,
|
||||
'require("bowser").default is set — the CJS interop unwrap was lost',
|
||||
);
|
||||
});
|
||||
|
||||
check('require("bowser") parses a user agent', function () {
|
||||
var Bowser = require('bowser');
|
||||
assert.strictEqual(Bowser.parse(UA).browser.name, 'Chrome');
|
||||
assert.strictEqual(Bowser.getParser(UA).getBrowserName(), 'Chrome');
|
||||
});
|
||||
|
||||
check('constant maps are exposed', function () {
|
||||
var Bowser = require('bowser');
|
||||
assert.strictEqual(typeof Bowser.BROWSER_MAP, 'object');
|
||||
assert.strictEqual(typeof Bowser.ENGINE_MAP, 'object');
|
||||
assert.strictEqual(typeof Bowser.OS_MAP, 'object');
|
||||
assert.strictEqual(typeof Bowser.PLATFORMS_MAP, 'object');
|
||||
});
|
||||
|
||||
// --- Legacy deep paths -----------------------------------------------------
|
||||
// These resolved before the exports map existed. Adding an exports map without
|
||||
// listing them turns them into ERR_PACKAGE_PATH_NOT_EXPORTED.
|
||||
|
||||
['bowser/es5.js', 'bowser/es5', 'bowser/bundled.js', 'bowser/bundled'].forEach(function (id) {
|
||||
check('require("' + id + '") works', function () {
|
||||
var B = require(id);
|
||||
assert.strictEqual(typeof B, 'function');
|
||||
assert.strictEqual(B.parse(UA).browser.name, 'Chrome');
|
||||
});
|
||||
});
|
||||
|
||||
// The src/*.js files are ES module sources, so they resolve but do not execute
|
||||
// under require(). Bundlers are the real consumer here. Assert resolution only.
|
||||
[
|
||||
'bowser.js', 'constants.js', 'parser.js', 'parser-browsers.js',
|
||||
'parser-engines.js', 'parser-os.js', 'parser-platforms.js', 'utils.js',
|
||||
].forEach(function (file) {
|
||||
var withExt = 'bowser/src/' + file;
|
||||
var withoutExt = withExt.replace(/\.js$/, '');
|
||||
check('resolves "' + withExt + '" and "' + withoutExt + '"', function () {
|
||||
assert.ok(fs.existsSync(require.resolve(withExt)));
|
||||
assert.ok(fs.existsSync(require.resolve(withoutExt)));
|
||||
});
|
||||
});
|
||||
|
||||
check('require.resolve("bowser/package.json") works', function () {
|
||||
assert.ok(fs.existsSync(require.resolve('bowser/package.json')));
|
||||
});
|
||||
|
||||
// --- The published manifest ------------------------------------------------
|
||||
|
||||
check('main/browser/module/types fields are unchanged', function () {
|
||||
var pkg = require('bowser/package.json');
|
||||
assert.strictEqual(pkg.main, 'es5.js');
|
||||
assert.strictEqual(pkg.browser, 'es5.js');
|
||||
assert.strictEqual(pkg.module, 'src/bowser.js');
|
||||
assert.strictEqual(pkg.types, 'index.d.ts');
|
||||
});
|
||||
|
||||
check('no "engines" field (would warn/fail installs on old Node)', function () {
|
||||
var pkg = require('bowser/package.json');
|
||||
assert.strictEqual(pkg.engines, undefined);
|
||||
});
|
||||
|
||||
check('no "type" field (would reclassify es5.js as ESM)', function () {
|
||||
var pkg = require('bowser/package.json');
|
||||
assert.strictEqual(pkg.type, undefined);
|
||||
});
|
||||
|
||||
check('published file list is exactly what we expect', function () {
|
||||
var root = path.dirname(require.resolve('bowser/package.json'));
|
||||
var actual = [];
|
||||
(function walk(dir, prefix) {
|
||||
fs.readdirSync(dir).forEach(function (name) {
|
||||
var full = path.join(dir, name);
|
||||
if (fs.statSync(full).isDirectory()) walk(full, prefix + name + '/');
|
||||
else actual.push(prefix + name);
|
||||
});
|
||||
}(root, ''));
|
||||
|
||||
var expected = [
|
||||
'LICENSE', 'README.md', 'bowser.mjs', 'bundled.js', 'es5.js',
|
||||
'index.d.mts', 'index.d.ts', 'package.json',
|
||||
'src/bowser.js', 'src/constants.js', 'src/parser-browsers.js',
|
||||
'src/parser-engines.js', 'src/parser-os.js', 'src/parser-platforms.js',
|
||||
'src/parser.js', 'src/utils.js',
|
||||
];
|
||||
assert.deepStrictEqual(actual.sort(), expected.sort());
|
||||
});
|
||||
|
||||
// --- The UMD / CDN contract ------------------------------------------------
|
||||
// Script-tag consumers get `window.bowser` (lowercase). Nothing else covers
|
||||
// this path, and a renamed global fails silently at runtime.
|
||||
|
||||
check('es5.js sets a lowercase `bowser` global when loaded as a script', function () {
|
||||
var file = require.resolve('bowser/es5.js');
|
||||
var sandbox = {};
|
||||
sandbox.self = sandbox;
|
||||
vm.runInNewContext(fs.readFileSync(file, 'utf8'), sandbox);
|
||||
assert.strictEqual(typeof sandbox.bowser, 'function', 'global `bowser` not set');
|
||||
assert.strictEqual(sandbox.bowser.parse(UA).browser.name, 'Chrome');
|
||||
assert.strictEqual(sandbox.Bowser, undefined, 'unexpected capitalised global');
|
||||
});
|
||||
|
||||
check('es5.js contains no ES6 template literals', function () {
|
||||
// rolldown's built-in (oxc) minifier rewrites every string literal as a
|
||||
// template literal, which is a syntax error in the old browsers this bundle
|
||||
// targets. Guards against the terser step being dropped from the build.
|
||||
var code = fs.readFileSync(require.resolve('bowser/es5.js'), 'utf8');
|
||||
assert.strictEqual(code.indexOf('`'), -1, 'es5.js contains a backtick');
|
||||
});
|
||||
|
||||
check('es5.js keeps the copyright banner', function () {
|
||||
var code = fs.readFileSync(require.resolve('bowser/es5.js'), 'utf8');
|
||||
assert.ok(code.indexOf('Bowser - a browser detector') !== -1);
|
||||
});
|
||||
|
||||
// --- The ESM contract ------------------------------------------------------
|
||||
|
||||
if (!nodeAtLeast(12, 17)) {
|
||||
console.log(' -- skipping ESM checks (Node ' + process.versions.node + ' < 12.17)');
|
||||
console.log('\n' + passed + ' checks passed');
|
||||
} else {
|
||||
// eslint-disable-next-line no-eval
|
||||
eval('import("bowser")')
|
||||
.then(function (mod) {
|
||||
check('import("bowser") has a working default export', function () {
|
||||
assert.strictEqual(typeof mod.default, 'function');
|
||||
assert.strictEqual(mod.default.parse(UA).browser.name, 'Chrome');
|
||||
});
|
||||
// This is issue #511: named imports must work, and must work unbound.
|
||||
check('import("bowser") has working named exports', function () {
|
||||
assert.strictEqual(typeof mod.parse, 'function');
|
||||
assert.strictEqual(typeof mod.getParser, 'function');
|
||||
var parse = mod.parse;
|
||||
var getParser = mod.getParser;
|
||||
assert.strictEqual(parse(UA).browser.name, 'Chrome');
|
||||
assert.strictEqual(getParser(UA).getBrowserName(), 'Chrome');
|
||||
});
|
||||
console.log('\n' + passed + ' checks passed');
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error('\nESM check failed: ' + err.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
100
test/package/smoke.cjs
Normal file
100
test/package/smoke.cjs
Normal file
@@ -0,0 +1,100 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Verifies the *packed* bowser package against the surface real consumers use.
|
||||
*
|
||||
* node test/package/smoke.cjs <path-to-bowser-x.y.z.tgz>
|
||||
*
|
||||
* The tarball is extracted into a throwaway directory as ./node_modules/bowser
|
||||
* and `assertions.cjs` is run next to it, so bare specifiers such as
|
||||
* `require('bowser')` and `require('bowser/es5.js')` resolve exactly as they
|
||||
* would after `npm install bowser`.
|
||||
*
|
||||
* Run this on every Node version bowser supports — that is the point of it.
|
||||
* See `.github/workflows/pull-request.yml`.
|
||||
*
|
||||
* Deliberately ES5-compatible CommonJS so it runs on Node 12.16.3.
|
||||
*/
|
||||
var cp = require('child_process');
|
||||
var fs = require('fs');
|
||||
var os = require('os');
|
||||
var path = require('path');
|
||||
var zlib = require('zlib');
|
||||
|
||||
var tarball = process.argv[2];
|
||||
|
||||
if (!tarball) {
|
||||
console.error('usage: node test/package/smoke.cjs <path-to-tarball.tgz>');
|
||||
console.error('');
|
||||
console.error('Create one with:');
|
||||
console.error(' npm version 0.0.0-ci --no-git-tag-version --allow-same-version');
|
||||
console.error(' npm pack');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
tarball = path.resolve(tarball);
|
||||
if (!fs.existsSync(tarball)) {
|
||||
console.error('no such tarball: ' + tarball);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'bowser-smoke-'));
|
||||
var modules = path.join(tmp, 'node_modules');
|
||||
fs.mkdirSync(modules);
|
||||
|
||||
/**
|
||||
* Extracts an npm tarball using only Node built-ins, so the test needs no
|
||||
* external `tar`/`npm` process and runs identically on every OS and every
|
||||
* Node version in the support matrix.
|
||||
*
|
||||
* npm tarballs are gzipped ustar archives of regular files (npm rejects
|
||||
* anything else at publish time), which is the only shape handled here:
|
||||
* pax/global extended headers and directory entries are skipped, and parent
|
||||
* directories are created per file instead.
|
||||
*/
|
||||
function extractNpmTarball(tgz, dest) {
|
||||
var buf = zlib.gunzipSync(fs.readFileSync(tgz));
|
||||
var offset = 0;
|
||||
while (offset + 512 <= buf.length) {
|
||||
var header = buf.slice(offset, offset + 512);
|
||||
offset += 512;
|
||||
if (header[0] === 0) break; // zero block: end of archive
|
||||
var name = header.slice(0, 100).toString('utf8').replace(/\0[\s\S]*$/, '');
|
||||
var prefix = header.slice(345, 500).toString('utf8').replace(/\0[\s\S]*$/, '');
|
||||
if (prefix) name = prefix + '/' + name;
|
||||
var size = parseInt(header.slice(124, 136).toString('utf8'), 8) || 0;
|
||||
var type = header[156];
|
||||
if (type === 48 /* '0' */ || type === 0) {
|
||||
var target = path.join(dest, name);
|
||||
if (target.indexOf(dest + path.sep) !== 0) {
|
||||
throw new Error('tarball entry escapes destination: ' + name);
|
||||
}
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.writeFileSync(target, buf.slice(offset, offset + size));
|
||||
}
|
||||
offset += Math.ceil(size / 512) * 512;
|
||||
}
|
||||
}
|
||||
|
||||
// npm tarballs always unpack to a single top-level `package/` directory.
|
||||
extractNpmTarball(tarball, modules);
|
||||
fs.renameSync(path.join(modules, 'package'), path.join(modules, 'bowser'));
|
||||
|
||||
var runner = path.join(tmp, 'assertions.cjs');
|
||||
fs.writeFileSync(runner, fs.readFileSync(path.join(__dirname, 'assertions.cjs')));
|
||||
|
||||
var result = cp.spawnSync(process.execPath, [runner], { cwd: tmp, stdio: 'inherit' });
|
||||
|
||||
try {
|
||||
// fs.rmSync landed in Node 14.14; recursive fs.rmdirSync (12.10+) covers the
|
||||
// bottom of the matrix. Both work on Windows, unlike shelling out to `rm`.
|
||||
if (fs.rmSync) {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
} else {
|
||||
fs.rmdirSync(tmp, { recursive: true });
|
||||
}
|
||||
} catch (e) { /* best effort */ }
|
||||
|
||||
if (result.status !== 0) {
|
||||
console.error('\npackage smoke test FAILED on Node ' + process.versions.node);
|
||||
process.exit(result.status || 1);
|
||||
}
|
||||
107
tsdown.config.ts
Normal file
107
tsdown.config.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
import babel from '@rolldown/plugin-babel';
|
||||
import { minify } from 'terser';
|
||||
|
||||
const banner = `/*!
|
||||
* Bowser - a browser detector
|
||||
* https://github.com/lancedikson/bowser
|
||||
* MIT License | (c) Dustin Diaz 2012-2015
|
||||
* MIT License | (c) Denis Demchenko 2015-2026
|
||||
*/`;
|
||||
|
||||
/**
|
||||
* The browser targets the UMD bundles are transpiled down to. Unchanged from
|
||||
* the webpack build these bundles replaced — the published `es5.js` is relied
|
||||
* on by script-tag and CDN consumers on very old browsers.
|
||||
*/
|
||||
const legacyTargets = {
|
||||
ie: '8',
|
||||
browsers: '>2%',
|
||||
};
|
||||
|
||||
/**
|
||||
* `useBuiltIns: false` for `es5.js` (syntax transpilation only) and `'entry'`
|
||||
* for `bundled.js`, which expands the `core-js/stable` import in its entry.
|
||||
*/
|
||||
const legacyBabel = (useBuiltIns: false | 'entry') => babel({
|
||||
presets: [['@babel/preset-env', {
|
||||
// Let rolldown emit the UMD wrapper; babel only lowers syntax here.
|
||||
modules: false,
|
||||
loose: true,
|
||||
useBuiltIns,
|
||||
...(useBuiltIns ? { corejs: '3' } : {}),
|
||||
targets: legacyTargets,
|
||||
}]],
|
||||
});
|
||||
|
||||
/**
|
||||
* Minifies the UMD chunks with terser instead of rolldown's built-in (oxc)
|
||||
* minifier.
|
||||
*
|
||||
* This is not a preference. oxc's minifier prints every string literal as a
|
||||
* template literal and refuses any `compress.target` below `es2015`, so it
|
||||
* cannot emit ES5 — it would silently undo babel's lowering and break the very
|
||||
* old browsers `es5.js` exists to serve. terser (`ecma: 5`) is also what the
|
||||
* webpack 4 build this replaced used, via terser-webpack-plugin.
|
||||
*/
|
||||
const terser = () => ({
|
||||
name: 'bowser:terser',
|
||||
async renderChunk(code: string, chunk: { fileName: string }) {
|
||||
const result = await minify(code, {
|
||||
ecma: 5,
|
||||
// IE 8: reserved words as property names must stay quoted.
|
||||
ie8: true,
|
||||
safari10: true,
|
||||
format: {
|
||||
// Keep the `/*!` banner.
|
||||
comments: /^!/,
|
||||
},
|
||||
});
|
||||
if (typeof result.code !== 'string') {
|
||||
throw new Error(`terser produced no output for ${chunk.fileName}`);
|
||||
}
|
||||
return { code: result.code };
|
||||
},
|
||||
});
|
||||
|
||||
const umd = (name: string, entry: string, useBuiltIns: false | 'entry') => ({
|
||||
entry: { [name]: entry },
|
||||
format: ['umd' as const],
|
||||
// Lowercase, matching the global the webpack build published. Renaming this
|
||||
// to `Bowser` would break every `<script src=".../bowser/es5.js">` consumer.
|
||||
globalName: 'bowser',
|
||||
outputOptions: {
|
||||
// `module.exports = Bowser` rather than `{ default: Bowser }`.
|
||||
exports: 'default' as const,
|
||||
// Default would be `<name>.umd.js`; these files are published paths.
|
||||
entryFileNames: '[name].js',
|
||||
},
|
||||
outDir: '.',
|
||||
platform: 'browser' as const,
|
||||
plugins: [legacyBabel(useBuiltIns), terser()],
|
||||
// webpack ran in `mode: 'production'`; minification happens in `terser()`
|
||||
// above, so rolldown's own minifier stays off. See its comment for why.
|
||||
minify: false,
|
||||
banner,
|
||||
dts: false,
|
||||
// outDir is the repo root — never let tsdown clean it.
|
||||
clean: false,
|
||||
});
|
||||
|
||||
export default defineConfig([
|
||||
umd('es5', 'build/entries/es5.js', false),
|
||||
umd('bundled', 'build/entries/bundled.js', 'entry'),
|
||||
{
|
||||
// Modern ESM build. Reached via the `import` condition of the exports map.
|
||||
// Deliberately not run through babel: `module` still points at the raw
|
||||
// `src/bowser.js`, so this must not be *more* conservative than that.
|
||||
entry: { bowser: 'src/bowser.js' },
|
||||
format: ['esm'],
|
||||
outDir: '.',
|
||||
outExtensions: () => ({ js: '.mjs' }),
|
||||
platform: 'browser',
|
||||
banner,
|
||||
dts: false,
|
||||
clean: false,
|
||||
},
|
||||
]);
|
||||
@@ -1,49 +0,0 @@
|
||||
const path = require('path');
|
||||
const CompressionPlugin = require('compression-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const banner = `Bowser - a browser detector
|
||||
https://github.com/lancedikson/bowser
|
||||
MIT License | (c) Dustin Diaz 2012-2015
|
||||
MIT License | (c) Denis Demchenko 2015-2026`;
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
new CompressionPlugin(),
|
||||
new webpack.BannerPlugin({ banner }),
|
||||
],
|
||||
mode: 'production', // "production" | "development" | "none"
|
||||
// Chosen mode tells webpack to use its built-in optimizations accordingly.
|
||||
entry: {
|
||||
bundled: ['@babel/polyfill', './src/bowser.js'],
|
||||
es5: './src/bowser.js',
|
||||
}, // string | object | array
|
||||
// defaults to ./src
|
||||
// Here the application starts executing
|
||||
// and webpack starts bundling
|
||||
output: {
|
||||
// options related to how webpack emits results
|
||||
path: path.resolve(__dirname, './'), // string
|
||||
// the target directory for all output files
|
||||
// must be an absolute path (use the Node.js path module)
|
||||
filename: '[name].js', // string
|
||||
// the filename template for entry chunks
|
||||
library: 'bowser',
|
||||
libraryTarget: 'umd', // universal module definition
|
||||
// the type of the exported library
|
||||
globalObject: 'this',
|
||||
},
|
||||
module: {
|
||||
// configuration regarding modules
|
||||
rules: [
|
||||
// rules for modules (configure loaders, parser options, etc.)
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user