Skip to content

Commit 1ec270a

Browse files
committed
Makes separate browser & node ESM bundles to avoid top async/await
1 parent b5ad49a commit 1ec270a

File tree

6 files changed

+87
-63
lines changed

6 files changed

+87
-63
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,46 @@
11
import MagicString from 'magic-string';
22

3-
const CRYPTO_IMPORT_ESM_SRC = `const nodejsRandomBytes = await (async () => {
3+
const CRYPTO_IMPORT_ESM_SRC = `import { randomBytes as nodejsRandomBytes } from 'crypto';`;
4+
const BROWSER_ESM_SRC = `const nodejsRandomBytes = nodejsMathRandomBytes;`;
5+
const CODE_TO_REPLACE = `const nodejsRandomBytes = (() => {
46
try {
5-
return (await import('crypto')).randomBytes;`;
6-
7-
export class RequireRewriter {
8-
/**
9-
* Take the compiled source code input; types are expected to already have been removed
10-
* Look for the function that depends on crypto, replace it with a top-level await
11-
* and dynamic import for the crypto module.
12-
*
13-
* @param {string} code - source code of the module being transformed
14-
* @param {string} id - module id (usually the source file name)
15-
* @returns {{ code: string; map: import('magic-string').SourceMap }}
16-
*/
17-
transform(code, id) {
18-
if (!id.includes('node_byte_utils')) {
19-
return;
7+
return require('crypto').randomBytes;
208
}
21-
if (!code.includes('const nodejsRandomBytes')) {
22-
throw new Error(`Unexpected! 'const nodejsRandomBytes' is missing from ${id}`);
9+
catch {
10+
return nodejsMathRandomBytes;
2311
}
12+
})();`;
2413

25-
const start = code.indexOf('const nodejsRandomBytes');
26-
const endString = `return require('crypto').randomBytes;`;
27-
const end = code.indexOf(endString) + endString.length;
14+
export function requireRewriter({ isBrowser = false } = {}) {
15+
return {
16+
/**
17+
* Take the compiled source code input; types are expected to already have been removed
18+
* Look for the function that depends on crypto, replace it with a top-level await
19+
* and dynamic import for the crypto module.
20+
*
21+
* @param {string} code - source code of the module being transformed
22+
* @param {string} id - module id (usually the source file name)
23+
* @returns {{ code: string; map: import('magic-string').SourceMap }}
24+
*/
25+
transform(code, id) {
26+
if (!id.includes('node_byte_utils')) {
27+
return;
28+
}
29+
const start = code.indexOf(CODE_TO_REPLACE);
30+
if (start === -1) {
31+
throw new Error(`Unexpected! Code meant to be replaced is missing from ${id}`);
32+
}
2833

29-
if (start < 0 || end < 0) {
30-
throw new Error(
31-
`Unexpected! 'const nodejsRandomBytes' or 'return require('crypto').randomBytes;' not found`
32-
);
33-
}
34+
const end = start + CODE_TO_REPLACE.length;
3435

35-
// MagicString lets us edit the source code and still generate an accurate source map
36-
const magicString = new MagicString(code);
37-
magicString.overwrite(start, end, CRYPTO_IMPORT_ESM_SRC);
36+
// MagicString lets us edit the source code and still generate an accurate source map
37+
const magicString = new MagicString(code);
38+
magicString.overwrite(start, end, isBrowser ? BROWSER_ESM_SRC : CRYPTO_IMPORT_ESM_SRC);
3839

39-
return {
40-
code: magicString.toString(),
41-
map: magicString.generateMap({ hires: true })
42-
};
43-
}
40+
return {
41+
code: magicString.toString(),
42+
map: magicString.generateMap({ hires: true })
43+
};
44+
}
45+
};
4446
}

package.json

+7-7
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,18 @@
7575
"native": false
7676
},
7777
"main": "./lib/bson.cjs",
78-
"module": "./lib/bson.mjs",
78+
"module": "./lib/bson.node.mjs",
7979
"exports": {
80-
"import": {
80+
"browser": {
8181
"types": "./bson.d.ts",
82-
"default": "./lib/bson.mjs"
82+
"import": "./lib/bson.browser.mjs"
8383
},
84-
"require": {
84+
"default": {
8585
"types": "./bson.d.ts",
86-
"default": "./lib/bson.cjs"
86+
"import": "./lib/bson.node.mjs",
87+
"require": "./lib/bson.cjs"
8788
},
88-
"react-native": "./lib/bson.rn.cjs",
89-
"browser": "./lib/bson.mjs"
89+
"react-native": "./lib/bson.rn.cjs"
9090
},
9191
"compass:exports": {
9292
"import": "./lib/bson.cjs",

rollup.config.mjs

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { nodeResolve } from '@rollup/plugin-node-resolve';
22
import typescript from '@rollup/plugin-typescript';
3-
import { RequireRewriter } from './etc/rollup/rollup-plugin-require-rewriter/require_rewriter.mjs';
3+
import { requireRewriter } from './etc/rollup/rollup-plugin-require-rewriter/require_rewriter.mjs';
44
import { RequireVendor } from './etc/rollup/rollup-plugin-require-vendor/require_vendor.mjs';
55

66
/** @type {typescript.RollupTypescriptOptions} */
@@ -55,9 +55,22 @@ const config = [
5555
},
5656
{
5757
input,
58-
plugins: [typescript(tsConfig), new RequireRewriter(), nodeResolve({ resolveOnly: [] })],
58+
plugins: [
59+
typescript(tsConfig),
60+
requireRewriter({ isBrowser: true }),
61+
nodeResolve({ resolveOnly: [] })
62+
],
5963
output: {
60-
file: 'lib/bson.mjs',
64+
file: 'lib/bson.browser.mjs',
65+
format: 'esm',
66+
sourcemap: true
67+
}
68+
},
69+
{
70+
input,
71+
plugins: [typescript(tsConfig), requireRewriter(), nodeResolve({ resolveOnly: [] })],
72+
output: {
73+
file: 'lib/bson.node.mjs',
6174
format: 'esm',
6275
sourcemap: true
6376
}

test/load_bson.js

+17-8
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,28 @@ function loadCJSModuleBSON(globals) {
5252
return { context, exports: context.exports };
5353
}
5454

55-
async function loadESModuleBSON(globals) {
56-
const filename = path.resolve(__dirname, `../lib/bson.mjs`);
55+
async function loadESModuleBSON() {
56+
const filename = path.resolve(__dirname, `../lib/bson.node.mjs`);
5757
const code = await fs.promises.readFile(filename, { encoding: 'utf8' });
5858

59-
const context = vm.createContext({
60-
...commonGlobals,
61-
// Putting this last to allow caller to override default globals
62-
...globals
63-
});
59+
const context = vm.createContext(commonGlobals);
6460

6561
const bsonMjs = new vm.SourceTextModule(code, { context });
62+
const cryptoModule = new vm.SyntheticModule(
63+
['randomBytes'],
64+
function () {
65+
this.setExport('randomBytes', crypto.randomBytes);
66+
},
67+
{ context }
68+
);
69+
70+
await cryptoModule.link(() => {});
6671

67-
await bsonMjs.link(() => {});
72+
await bsonMjs.link(specifier => {
73+
if (specifier === 'crypto') {
74+
return cryptoModule;
75+
}
76+
});
6877
await bsonMjs.evaluate();
6978

7079
return { context, exports: bsonMjs.namespace };

test/node/byte_utils.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -694,11 +694,11 @@ describe('ByteUtils', () => {
694694
});
695695
});
696696

697-
describe('nodejs es module environment dynamically imports crypto', function () {
697+
describe('nodejs es module environment imports crypto', function () {
698698
let bsonImportedFromESMMod;
699699

700700
beforeEach(async function () {
701-
const { exports } = await loadESModuleBSON({});
701+
const { exports } = await loadESModuleBSON();
702702
bsonImportedFromESMMod = exports;
703703
});
704704

test/node/exports.test.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -69,31 +69,31 @@ describe('bson entrypoint', () => {
6969

7070
it('maintains the order of keys in exports conditions', async () => {
7171
expect(pkg).property('exports').is.a('object');
72-
expect(pkg).nested.property('exports.import').is.a('object');
73-
expect(pkg).nested.property('exports.require').is.a('object');
72+
expect(pkg).nested.property('exports.browser').is.a('object');
73+
expect(pkg).nested.property('exports.default').is.a('object');
7474

7575
expect(
7676
Object.keys(pkg.exports),
7777
'Order matters in the exports fields. import/require need to proceed the "bundler" targets (RN/browser) and react-native MUST proceed browser'
78-
).to.deep.equal(['import', 'require', 'react-native', 'browser']);
78+
).to.deep.equal(['browser', 'default', 'react-native']);
7979

8080
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#packagejson-exports-imports-and-self-referencing
8181
expect(
82-
Object.keys(pkg.exports.import),
82+
Object.keys(pkg.exports.browser),
8383
'TS docs say that `types` should ALWAYS proceed `default`'
84-
).to.deep.equal(['types', 'default']);
84+
).to.deep.equal(['types', 'import']);
8585
expect(
86-
Object.keys(pkg.exports.require),
86+
Object.keys(pkg.exports.default),
8787
'TS docs say that `types` should ALWAYS proceed `default`'
88-
).to.deep.equal(['types', 'default']);
88+
).to.deep.equal(['types', 'import', 'require']);
8989

9090
expect(Object.keys(pkg['compass:exports'])).to.deep.equal(['import', 'require']);
9191
});
9292

9393
it('has the equivalent "bson.d.ts" value for all "types" specifiers', () => {
9494
expect(pkg).property('types', 'bson.d.ts');
95-
expect(pkg).nested.property('exports.import.types', './bson.d.ts');
96-
expect(pkg).nested.property('exports.require.types', './bson.d.ts');
95+
expect(pkg).nested.property('exports.browser.types', './bson.d.ts');
96+
expect(pkg).nested.property('exports.default.types', './bson.d.ts');
9797
});
9898
});
9999
});

0 commit comments

Comments
 (0)