diff --git a/etc/rollup/rollup-plugin-require-rewriter/require_rewriter.mjs b/etc/rollup/rollup-plugin-require-rewriter/require_rewriter.mjs index 796ba449..68161963 100644 --- a/etc/rollup/rollup-plugin-require-rewriter/require_rewriter.mjs +++ b/etc/rollup/rollup-plugin-require-rewriter/require_rewriter.mjs @@ -1,44 +1,46 @@ import MagicString from 'magic-string'; -const CRYPTO_IMPORT_ESM_SRC = `const nodejsRandomBytes = await (async () => { +const CRYPTO_IMPORT_ESM_SRC = `import { randomBytes as nodejsRandomBytes } from 'crypto';`; +const BROWSER_ESM_SRC = `const nodejsRandomBytes = nodejsMathRandomBytes;`; +const CODE_TO_REPLACE = `const nodejsRandomBytes = (() => { try { - return (await import('crypto')).randomBytes;`; - -export class RequireRewriter { - /** - * Take the compiled source code input; types are expected to already have been removed - * Look for the function that depends on crypto, replace it with a top-level await - * and dynamic import for the crypto module. - * - * @param {string} code - source code of the module being transformed - * @param {string} id - module id (usually the source file name) - * @returns {{ code: string; map: import('magic-string').SourceMap }} - */ - transform(code, id) { - if (!id.includes('node_byte_utils')) { - return; + return require('crypto').randomBytes; } - if (!code.includes('const nodejsRandomBytes')) { - throw new Error(`Unexpected! 'const nodejsRandomBytes' is missing from ${id}`); + catch { + return nodejsMathRandomBytes; } +})();`; - const start = code.indexOf('const nodejsRandomBytes'); - const endString = `return require('crypto').randomBytes;`; - const end = code.indexOf(endString) + endString.length; +export function requireRewriter({ isBrowser = false } = {}) { + return { + /** + * Take the compiled source code input; types are expected to already have been removed + * Look for the function that depends on crypto, replace it with a top-level await + * and dynamic import for the crypto module. + * + * @param {string} code - source code of the module being transformed + * @param {string} id - module id (usually the source file name) + * @returns {{ code: string; map: import('magic-string').SourceMap }} + */ + transform(code, id) { + if (!id.includes('node_byte_utils')) { + return; + } + const start = code.indexOf(CODE_TO_REPLACE); + if (start === -1) { + throw new Error(`Unexpected! Code meant to be replaced is missing from ${id}`); + } - if (start < 0 || end < 0) { - throw new Error( - `Unexpected! 'const nodejsRandomBytes' or 'return require('crypto').randomBytes;' not found` - ); - } + const end = start + CODE_TO_REPLACE.length; - // MagicString lets us edit the source code and still generate an accurate source map - const magicString = new MagicString(code); - magicString.overwrite(start, end, CRYPTO_IMPORT_ESM_SRC); + // MagicString lets us edit the source code and still generate an accurate source map + const magicString = new MagicString(code); + magicString.overwrite(start, end, isBrowser ? BROWSER_ESM_SRC : CRYPTO_IMPORT_ESM_SRC); - return { - code: magicString.toString(), - map: magicString.generateMap({ hires: true }) - }; - } + return { + code: magicString.toString(), + map: magicString.generateMap({ hires: true }) + }; + } + }; } diff --git a/package.json b/package.json index 2399e2c7..727c4b12 100644 --- a/package.json +++ b/package.json @@ -75,18 +75,18 @@ "native": false }, "main": "./lib/bson.cjs", - "module": "./lib/bson.mjs", + "module": "./lib/bson.node.mjs", "exports": { - "import": { + "browser": { "types": "./bson.d.ts", - "default": "./lib/bson.mjs" - }, - "require": { - "types": "./bson.d.ts", - "default": "./lib/bson.cjs" + "default": "./lib/bson.browser.mjs" }, "react-native": "./lib/bson.rn.cjs", - "browser": "./lib/bson.mjs" + "default": { + "types": "./bson.d.ts", + "import": "./lib/bson.node.mjs", + "require": "./lib/bson.cjs" + } }, "compass:exports": { "import": "./lib/bson.cjs", diff --git a/rollup.config.mjs b/rollup.config.mjs index 16a0376d..9e25c0ad 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -1,6 +1,6 @@ import { nodeResolve } from '@rollup/plugin-node-resolve'; import typescript from '@rollup/plugin-typescript'; -import { RequireRewriter } from './etc/rollup/rollup-plugin-require-rewriter/require_rewriter.mjs'; +import { requireRewriter } from './etc/rollup/rollup-plugin-require-rewriter/require_rewriter.mjs'; import { RequireVendor } from './etc/rollup/rollup-plugin-require-vendor/require_vendor.mjs'; /** @type {typescript.RollupTypescriptOptions} */ @@ -55,9 +55,22 @@ const config = [ }, { input, - plugins: [typescript(tsConfig), new RequireRewriter(), nodeResolve({ resolveOnly: [] })], + plugins: [ + typescript(tsConfig), + requireRewriter({ isBrowser: true }), + nodeResolve({ resolveOnly: [] }) + ], output: { - file: 'lib/bson.mjs', + file: 'lib/bson.browser.mjs', + format: 'esm', + sourcemap: true + } + }, + { + input, + plugins: [typescript(tsConfig), requireRewriter(), nodeResolve({ resolveOnly: [] })], + output: { + file: 'lib/bson.node.mjs', format: 'esm', sourcemap: true } diff --git a/test/bundling/webpack/readme.md b/test/bundling/webpack/readme.md index 099c64e4..69da7365 100644 --- a/test/bundling/webpack/readme.md +++ b/test/bundling/webpack/readme.md @@ -1,7 +1,6 @@ # Webpack BSON setup example -In order to use BSON with webpack there are two changes beyond the default config file needed: -- Set `experiments: { topLevelAwait: true }` in the top-level config object +In order to use BSON with webpack there is one change beyond the default config file needed: - Set `resolve: { fallback: { crypto: false } }` in the top-level config object ## Testing diff --git a/test/bundling/webpack/webpack.config.js b/test/bundling/webpack/webpack.config.js index dfd35e18..d3009ff5 100644 --- a/test/bundling/webpack/webpack.config.js +++ b/test/bundling/webpack/webpack.config.js @@ -14,7 +14,6 @@ const config = { // Add your plugins here // Learn more about plugins from https://webpack.js.org/configuration/plugins/ ], - experiments: { topLevelAwait: true }, module: { rules: [ { diff --git a/test/load_bson.js b/test/load_bson.js index 6951d346..b63c0c71 100644 --- a/test/load_bson.js +++ b/test/load_bson.js @@ -52,19 +52,28 @@ function loadCJSModuleBSON(globals) { return { context, exports: context.exports }; } -async function loadESModuleBSON(globals) { - const filename = path.resolve(__dirname, `../lib/bson.mjs`); +async function loadESModuleBSON() { + const filename = path.resolve(__dirname, `../lib/bson.node.mjs`); const code = await fs.promises.readFile(filename, { encoding: 'utf8' }); - const context = vm.createContext({ - ...commonGlobals, - // Putting this last to allow caller to override default globals - ...globals - }); + const context = vm.createContext(commonGlobals); const bsonMjs = new vm.SourceTextModule(code, { context }); + const cryptoModule = new vm.SyntheticModule( + ['randomBytes'], + function () { + this.setExport('randomBytes', crypto.randomBytes); + }, + { context } + ); + + await cryptoModule.link(() => {}); - await bsonMjs.link(() => {}); + await bsonMjs.link(specifier => { + if (specifier === 'crypto') { + return cryptoModule; + } + }); await bsonMjs.evaluate(); return { context, exports: bsonMjs.namespace }; diff --git a/test/node/byte_utils.test.ts b/test/node/byte_utils.test.ts index df1fed0c..d0c34d21 100644 --- a/test/node/byte_utils.test.ts +++ b/test/node/byte_utils.test.ts @@ -694,11 +694,11 @@ describe('ByteUtils', () => { }); }); - describe('nodejs es module environment dynamically imports crypto', function () { + describe('nodejs es module environment imports crypto', function () { let bsonImportedFromESMMod; beforeEach(async function () { - const { exports } = await loadESModuleBSON({}); + const { exports } = await loadESModuleBSON(); bsonImportedFromESMMod = exports; }); diff --git a/test/node/exports.test.ts b/test/node/exports.test.ts index f3f08a54..55e1bd24 100644 --- a/test/node/exports.test.ts +++ b/test/node/exports.test.ts @@ -69,31 +69,31 @@ describe('bson entrypoint', () => { it('maintains the order of keys in exports conditions', async () => { expect(pkg).property('exports').is.a('object'); - expect(pkg).nested.property('exports.import').is.a('object'); - expect(pkg).nested.property('exports.require').is.a('object'); + expect(pkg).nested.property('exports.browser').is.a('object'); + expect(pkg).nested.property('exports.default').is.a('object'); expect( Object.keys(pkg.exports), 'Order matters in the exports fields. import/require need to proceed the "bundler" targets (RN/browser) and react-native MUST proceed browser' - ).to.deep.equal(['import', 'require', 'react-native', 'browser']); + ).to.deep.equal(['browser', 'react-native', 'default']); // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#packagejson-exports-imports-and-self-referencing expect( - Object.keys(pkg.exports.import), + Object.keys(pkg.exports.browser), 'TS docs say that `types` should ALWAYS proceed `default`' ).to.deep.equal(['types', 'default']); expect( - Object.keys(pkg.exports.require), + Object.keys(pkg.exports.default), 'TS docs say that `types` should ALWAYS proceed `default`' - ).to.deep.equal(['types', 'default']); + ).to.deep.equal(['types', 'import', 'require']); expect(Object.keys(pkg['compass:exports'])).to.deep.equal(['import', 'require']); }); it('has the equivalent "bson.d.ts" value for all "types" specifiers', () => { expect(pkg).property('types', 'bson.d.ts'); - expect(pkg).nested.property('exports.import.types', './bson.d.ts'); - expect(pkg).nested.property('exports.require.types', './bson.d.ts'); + expect(pkg).nested.property('exports.browser.types', './bson.d.ts'); + expect(pkg).nested.property('exports.default.types', './bson.d.ts'); }); }); });