1
0
mirror of https://github.com/lancedikson/bowser synced 2026-10-30 15:26:39 +00:00

fix: expose the constant maps on the ESM namespace (#642)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Denis Demchenko
2026-10-26 21:16:03 +02:00
committed by GitHub
parent 459d9ea318
commit 6ad397ee41
7 changed files with 67 additions and 21 deletions

View File

@@ -39,8 +39,9 @@ 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:
The ES module build also exposes `parse`, `getParser`, `BROWSER_MAP`,
`ENGINE_MAP`, `OS_MAP` and `PLATFORMS_MAP` 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";

View File

@@ -5,8 +5,9 @@
// 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
// export *and* every member of the CommonJS namespace as a named export, so
// `import * as Bowser from 'bowser'` sees what `require('bowser')` does. 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.
@@ -16,14 +17,25 @@ 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 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;
// `Parser` is both the parser type and, as in `index.d.ts`, a namespace of the
// result types, so `Bowser.Parser.ParsedResult` works on a namespace import.
export type Parser = Bowser.Parser.Parser;
export declare namespace Parser {
type Parser = Bowser.Parser.Parser;
type ParsedResult = Bowser.Parser.ParsedResult;
type Details = Bowser.Parser.Details;
type BrowserDetails = Bowser.Parser.BrowserDetails;
type EngineDetails = Bowser.Parser.EngineDetails;
type OSDetails = Bowser.Parser.OSDetails;
type PlatformDetails = Bowser.Parser.PlatformDetails;
type checkTree = Bowser.Parser.checkTree;
}
export type ParsedResult = Bowser.Parser.ParsedResult;
export type Details = Bowser.Parser.Details;
export type BrowserDetails = Bowser.Parser.BrowserDetails;

View File

@@ -92,3 +92,10 @@ class Bowser {
export default Bowser;
export const { parse, getParser } = Bowser;
// Named too, so `import * as Bowser` has every member the UMD build has.
export {
BROWSER_MAP,
ENGINE_MAP,
OS_MAP,
PLATFORMS_MAP,
};

View File

@@ -218,6 +218,13 @@ if (!nodeAtLeast(12, 17)) {
assert.strictEqual(parse(UA).browser.name, 'Chrome');
assert.strictEqual(getParser(UA).getBrowserName(), 'Chrome');
});
// `import * as Bowser` must see every member `require('bowser')` has.
check('import("bowser") has the constant maps as named exports', function () {
['BROWSER_MAP', 'ENGINE_MAP', 'OS_MAP', 'PLATFORMS_MAP'].forEach(function (key) {
assert.strictEqual(typeof mod[key], 'object', key);
assert.strictEqual(mod[key], mod.default[key], key);
});
});
console.log('\n' + passed + ' checks passed');
})
.catch(function (err) {

View File

@@ -16,9 +16,9 @@
* export declared in `index.d.mts` that `bowser.mjs` does not actually have.
*
* Both directions are asserted. The negative cases matter as much as the
* positive ones: `index.d.mts` deliberately omits `BROWSER_MAP` and friends as
* named exports because importing them that way throws at runtime, and only a
* compile that is expected to *fail* can hold that line.
* positive ones: `Parser` is only a type, and constructing it from an import
* would throw at runtime. Only a compile that is expected to *fail* can hold
* that line.
*
* Either way the package is installed into a real `node_modules` and resolved
* by the normal upward walk, so the exports map, the `types` field and the
@@ -57,11 +57,8 @@ const MODES = [
* promising something `bowser.mjs` will not deliver.
*/
const MUST_NOT_COMPILE = [
['BROWSER_MAP is not a named export', 'import { BROWSER_MAP } from "bowser"; export default BROWSER_MAP;'],
['OS_MAP is not a named export', 'import { OS_MAP } from "bowser"; export default OS_MAP;'],
['ENGINE_MAP is not a named export', 'import { ENGINE_MAP } from "bowser"; export default ENGINE_MAP;'],
['PLATFORMS_MAP is not a named export', 'import { PLATFORMS_MAP } from "bowser"; export default PLATFORMS_MAP;'],
['Parser is a type, not a value', 'import { Parser } from "bowser"; export default new Parser("x");'],
['Parser is a type, not a value, on the namespace', 'import * as B from "bowser"; export default new B.Parser("x");'],
];
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'bowser-types-'));

View File

@@ -1,7 +1,10 @@
// An ES module consumer: default plus named imports, types imported as types.
// This is `moduleResolution: node16` (from ESM), `nodenext` and `bundler`.
import Bowser, { parse, getParser } from 'bowser';
import Bowser, { parse, getParser, BROWSER_MAP, ENGINE_MAP, OS_MAP, PLATFORMS_MAP } from 'bowser';
import type { ParsedResult, Parser, ClientHints, checkTree } from 'bowser';
// The namespace import the README documents for TypeScript: it must offer
// everything the CommonJS `export =` namespace does.
import * as BowserNS from 'bowser';
const UA = 'Mozilla/5.0 (Macintosh) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
@@ -10,8 +13,15 @@ const parser: Parser = getParser(UA);
const tree: checkTree = { chrome: '>100' };
const satisfies: boolean | undefined = parser.satisfies(tree);
const hints: ClientHints = { mobile: false };
// BROWSER_MAP is deliberately not a named export of bowser.mjs — it only
// exists as a static on the class. Reaching it any other way must not compile.
const maps: Record<string, string> = Bowser.BROWSER_MAP;
const maps: Array<Record<string, string>> = [
Bowser.BROWSER_MAP, BROWSER_MAP, ENGINE_MAP, OS_MAP, PLATFORMS_MAP,
];
export { result, satisfies, hints, maps };
const nsResult: BowserNS.Parser.ParsedResult = BowserNS.parse(UA);
const nsParser: BowserNS.Parser.Parser = BowserNS.getParser(UA);
const nsHints: BowserNS.ClientHints = { mobile: false };
const nsMaps: Array<Record<string, string>> = [
BowserNS.BROWSER_MAP, BowserNS.ENGINE_MAP, BowserNS.OS_MAP, BowserNS.PLATFORMS_MAP,
];
export { result, satisfies, hints, maps, nsResult, nsParser, nsHints, nsMaps };

View File

@@ -1,5 +1,6 @@
import test from 'ava';
import Bowser from '../../src/bowser';
import * as BowserNS from '../../src/bowser';
import Parser from '../../src/parser';
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 OPR/43.0.2442.1165';
@@ -16,3 +17,14 @@ test('Bowser`s constructor fails if UA is empty', (t) => {
test('Bowser.parse parses UA and returns result', (t) => {
t.deepEqual(Bowser.parse(UA), browser.getResult());
});
test('every static member of Bowser is also a named export', (t) => {
// `require('bowser')` is the class itself, so `import * as Bowser` has to
// expose the same members for code moving between the two to keep working.
const statics = Object.getOwnPropertyNames(Bowser)
.filter(key => ['length', 'name', 'prototype'].indexOf(key) === -1);
t.true(statics.length > 0);
statics.forEach((key) => {
t.is(BowserNS[key], Bowser[key], key);
});
});