diff --git a/README.md b/README.md index 3289a3b..42f084d 100644 --- a/README.md +++ b/README.md @@ -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"; diff --git a/index.d.mts b/index.d.mts index 756db0c..28259dc 100644 --- a/index.d.mts +++ b/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; diff --git a/src/bowser.js b/src/bowser.js index f690bfc..6dd8ff2 100644 --- a/src/bowser.js +++ b/src/bowser.js @@ -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, +}; diff --git a/test/package/assertions.cjs b/test/package/assertions.cjs index 6e71762..5a2cf4e 100644 --- a/test/package/assertions.cjs +++ b/test/package/assertions.cjs @@ -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) { diff --git a/test/types/check.mjs b/test/types/check.mjs index b532e72..f11a0fc 100644 --- a/test/types/check.mjs +++ b/test/types/check.mjs @@ -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-')); diff --git a/test/types/fixtures/consumer-esm.ts b/test/types/fixtures/consumer-esm.ts index c1a6066..924bf53 100644 --- a/test/types/fixtures/consumer-esm.ts +++ b/test/types/fixtures/consumer-esm.ts @@ -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 = Bowser.BROWSER_MAP; +const maps: Array> = [ + 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> = [ + BowserNS.BROWSER_MAP, BowserNS.ENGINE_MAP, BowserNS.OS_MAP, BowserNS.PLATFORMS_MAP, +]; + +export { result, satisfies, hints, maps, nsResult, nsParser, nsHints, nsMaps }; diff --git a/test/unit/bowser.js b/test/unit/bowser.js index a27b2c9..aba6811 100644 --- a/test/unit/bowser.js +++ b/test/unit/bowser.js @@ -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); + }); +});