mirror of
https://github.com/lancedikson/bowser
synced 2026-09-23 12:34:58 +00:00
fix: make dual packaging non-breaking for existing consumers
Keeps the published surface of bowser@2.14.1 intact while adding a real ESM build, and rewrites CI so it can actually run the new toolchain. Packaging - Restore the flat artifact layout. es5.js and bundled.js stay at the tarball root next to src/, so unpkg.com/bowser/es5.js and require('bowser/bundled') keep working. bowser.mjs is added alongside. - Build the UMD bundles from dedicated single-default-export entries in build/entries/. src/bowser.js gained named exports (parse, getParser) to fix #511, but a UMD bundle with named exports makes require('bowser') a namespace object instead of the class: typeof flips from 'function' to 'object' and BROWSER_MAP / ENGINE_MAP / OS_MAP / PLATFORMS_MAP disappear, since they are static getters rather than exports. - globalName back to lowercase 'bowser', matching the shipped es5.js. Renaming it to 'Bowser' would break every script-tag consumer. - Enumerate every legacy subpath in the exports map. Conditional exports are honoured from Node 12.16.0 onward, so a "."-only map turns paths that resolve today into ERR_PACKAGE_PATH_NOT_EXPORTED. Subpath patterns ("./src/*") need Node 12.20.0+ and the trailing-slash folder form was removed in Node 17, so explicit per-file keys are the only spelling that works across the whole supported range. Extension-less aliases included: the README documents require('bowser/bundled'). - main, browser and module keep their existing values. No engines field (npm warns EBADENGINE, pnpm fails under engine-strict) and no type field (it would reclassify es5.js as ESM). Build - Minify the UMD bundles with terser instead of rolldown's built-in oxc minifier. oxc prints every string literal as a template literal and rejects any compress.target below es2015, so it cannot emit ES5 and was silently undoing babel's lowering. terser is what webpack 4 used. - Wire up the copyright banner, which was declared but never passed to a config, and restore bundled.js as the polyfilled build via core-js/stable. - Add index.d.mts so the import condition has ESM types. Reusing the export = declarations for both conditions describes an ES module with CommonJS types. CI - Replace npm ci with pnpm across all workflows. This is what was failing: the lockfile was swapped for pnpm-lock.yaml but the workflows still ran npm ci, pinned to Node 12.16.3 and 16. Build now runs on Node 24, which tsdown requires. - Add a pack-smoke job that installs the packed tarball on Node 12.16.3, 14, 18, 20 and 24 and exercises every documented entry point, plus publint and attw on the tarball. This is what makes "non-breaking" a tested claim rather than an argument; it catches all of the above. - Restore test-list-of-ua.js to asserting src against the built es5.js. It had been collapsed to comparing Bowser.parse with itself.
This commit is contained in:
3
.babelrc
3
.babelrc
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"presets": [["@babel/preset-env", {
|
||||
"useBuiltIns": "usage",
|
||||
"corejs": "3",
|
||||
"useBuiltIns": "entry",
|
||||
"modules": "cjs",
|
||||
"loose": true,
|
||||
"targets": {
|
||||
|
||||
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
|
||||
|
||||
134
.github/workflows/pull-request.yml
vendored
134
.github/workflows/pull-request.yml
vendored
@@ -3,42 +3,122 @@ 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
|
||||
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"
|
||||
|
||||
lint:
|
||||
name: "ESLint"
|
||||
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
|
||||
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;
|
||||
35
index.d.mts
Normal file
35
index.d.mts
Normal file
@@ -0,0 +1,35 @@
|
||||
// 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;
|
||||
|
||||
export declare const BROWSER_MAP: typeof Bowser.BROWSER_MAP;
|
||||
export declare const ENGINE_MAP: typeof Bowser.ENGINE_MAP;
|
||||
export declare const OS_MAP: typeof Bowser.OS_MAP;
|
||||
export declare const PLATFORMS_MAP: typeof Bowser.PLATFORMS_MAP;
|
||||
|
||||
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;
|
||||
|
||||
export { Bowser };
|
||||
60
package.json
60
package.json
@@ -3,8 +3,11 @@
|
||||
"description": "Lightweight browser detector",
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"index.d.ts"
|
||||
"es5.js",
|
||||
"bundled.js",
|
||||
"bowser.mjs",
|
||||
"index.d.ts",
|
||||
"index.d.mts"
|
||||
],
|
||||
"keywords": [
|
||||
"browser",
|
||||
@@ -28,27 +31,51 @@
|
||||
"url": "https://github.com/naorpeled"
|
||||
}
|
||||
],
|
||||
"main": "./dist/umd/bowser.umd.js",
|
||||
"browser": "./dist/esm/bowser.mjs",
|
||||
"module": "./dist/esm/bowser.mjs",
|
||||
"main": "es5.js",
|
||||
"browser": "es5.js",
|
||||
"module": "src/bowser.js",
|
||||
"types": "index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./index.d.ts",
|
||||
"default": "./dist/esm/bowser.mjs"
|
||||
"types": "./index.d.mts",
|
||||
"default": "./bowser.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./index.d.ts",
|
||||
"default": "./dist/umd/bowser.umd.js"
|
||||
}
|
||||
}
|
||||
"default": "./es5.js"
|
||||
},
|
||||
"default": "./es5.js"
|
||||
},
|
||||
"./es5.js": "./es5.js",
|
||||
"./es5": "./es5.js",
|
||||
"./bundled.js": "./bundled.js",
|
||||
"./bundled": "./bundled.js",
|
||||
"./bowser.mjs": "./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": {
|
||||
"@arethetypeswrong/cli": "^0.18.5",
|
||||
"@babel/cli": "^7.29.7",
|
||||
"@babel/core": "^7.29.7",
|
||||
"@babel/preset-env": "^7.29.7",
|
||||
@@ -58,9 +85,9 @@
|
||||
"ava": "^3.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",
|
||||
"core-js": "^3.49.0",
|
||||
"eslint": "^10.8.0",
|
||||
"eslint-config-airbnb-extended": "^3.1.0",
|
||||
"eslint-plugin-ava": "^17.0.1",
|
||||
@@ -68,15 +95,22 @@
|
||||
"gh-pages": "^3.0.0",
|
||||
"jsdoc": "^3.6.3",
|
||||
"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",
|
||||
"tsdown": "^0.22.14",
|
||||
"typescript": "^5.9.3",
|
||||
"yamljs": "^0.3.0"
|
||||
},
|
||||
"ava": {
|
||||
"require": [
|
||||
"@babel/register"
|
||||
],
|
||||
"files": [
|
||||
"test/**/*.js",
|
||||
"!test/package/**"
|
||||
]
|
||||
},
|
||||
"bugs": {
|
||||
@@ -94,8 +128,10 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
1284
pnpm-lock.yaml
generated
1284
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -209,7 +209,6 @@ class Parser {
|
||||
return this.getBrowser().name || '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get browser's version
|
||||
* @return {String} version of browser
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import test from 'ava';
|
||||
import yaml from 'yamljs';
|
||||
import path from 'path';
|
||||
import Bowser from '../../dist/esm/bowser.mjs';
|
||||
import Bowser from '../../src/bowser';
|
||||
import BowserBuild from '../../es5';
|
||||
|
||||
const listOfUA = yaml.load(path.join(__dirname, 'useragentstrings.yml'));
|
||||
|
||||
@@ -11,7 +12,7 @@ browserNames.forEach((browserName) => {
|
||||
listOfUA[browserName].forEach((browser, index) => {
|
||||
test(`Test ${browserName} ${index}`, (t) => {
|
||||
const parsed = Bowser.parse(browser.ua);
|
||||
const parsedBuild = Bowser.parse(browser.ua);
|
||||
const parsedBuild = BowserBuild.parse(browser.ua);
|
||||
t.deepEqual(parsed, browser.spec, `${browser.ua}`);
|
||||
t.deepEqual(parsedBuild, browser.spec, `${browser.ua}`);
|
||||
t.is(parsed.browser.name, browserName, `${browser.ua}`);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
62
test/package/smoke.cjs
Normal file
62
test/package/smoke.cjs
Normal file
@@ -0,0 +1,62 @@
|
||||
/* 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 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);
|
||||
|
||||
// `npm install <tarball>` would work too, but plain tar keeps this dependency
|
||||
// free and identical across the whole Node matrix. npm tarballs always unpack
|
||||
// to a single top-level `package/` directory.
|
||||
cp.execFileSync('tar', ['-xzf', tarball, '-C', modules], { stdio: 'inherit' });
|
||||
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; rimraf-by-hand keeps Node 12 happy.
|
||||
cp.execFileSync('rm', ['-rf', tmp]);
|
||||
} catch (e) { /* best effort */ }
|
||||
|
||||
if (result.status !== 0) {
|
||||
console.error('\npackage smoke test FAILED on Node ' + process.versions.node);
|
||||
process.exit(result.status || 1);
|
||||
}
|
||||
130
tsdown.config.ts
130
tsdown.config.ts
@@ -1,42 +1,104 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
import babel from '@rolldown/plugin-babel'
|
||||
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`;
|
||||
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) {
|
||||
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: /^!/,
|
||||
},
|
||||
});
|
||||
return { code: result.code as string };
|
||||
},
|
||||
});
|
||||
|
||||
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'),
|
||||
{
|
||||
entry: ['src/bowser.js'],
|
||||
format: ['umd'],
|
||||
plugins:[
|
||||
babel({
|
||||
presets: [['@babel/preset-env', {
|
||||
useBuiltIns: 'usage',
|
||||
corejs: '3',
|
||||
modules: 'cjs',
|
||||
loose: true,
|
||||
targets: {
|
||||
ie: '8',
|
||||
browsers: '>2%'
|
||||
}
|
||||
}]],
|
||||
"plugins": [
|
||||
"add-module-exports"
|
||||
]
|
||||
})
|
||||
],
|
||||
outDir: 'dist/umd', // universal module definition
|
||||
globalName: 'Bowser',
|
||||
dts: false,
|
||||
clean: true,
|
||||
},
|
||||
{
|
||||
entry: ['src/bowser.js'],
|
||||
// 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: 'dist/esm', // es module
|
||||
outDir: '.',
|
||||
outExtensions: () => ({ js: '.mjs' }),
|
||||
platform: 'browser',
|
||||
banner,
|
||||
dts: false,
|
||||
clean: true
|
||||
clean: false,
|
||||
},
|
||||
])
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user