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:
@@ -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";
|
||||
|
||||
26
index.d.mts
26
index.d.mts
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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-'));
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user