diff --git a/.eslintrc.json b/.eslintrc.json index 9b707d61..880c2791 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -21,7 +21,8 @@ "parserOptions": { "ecmaVersion": 2020, "project": [ - "./tsconfig.json" + "./tsconfig.common.json", + "./tsconfig.node.json" ] }, "plugins": [ diff --git a/.gitignore b/.gitignore index 39b98b7b..b811c49c 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ docs/public .nvmrc benchmarks.json + +*.tsbuildinfo diff --git a/etc/rollup/rollup-plugin-require-rewriter/require_rewriter.mjs b/etc/rollup/rollup-plugin-require-rewriter/require_rewriter.mjs deleted file mode 100644 index 796ba449..00000000 --- a/etc/rollup/rollup-plugin-require-rewriter/require_rewriter.mjs +++ /dev/null @@ -1,44 +0,0 @@ -import MagicString from 'magic-string'; - -const CRYPTO_IMPORT_ESM_SRC = `const nodejsRandomBytes = await (async () => { - 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; - } - if (!code.includes('const nodejsRandomBytes')) { - throw new Error(`Unexpected! 'const nodejsRandomBytes' is missing from ${id}`); - } - - const start = code.indexOf('const nodejsRandomBytes'); - const endString = `return require('crypto').randomBytes;`; - const end = code.indexOf(endString) + endString.length; - - if (start < 0 || end < 0) { - throw new Error( - `Unexpected! 'const nodejsRandomBytes' or 'return require('crypto').randomBytes;' not found` - ); - } - - // 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); - - return { - code: magicString.toString(), - map: magicString.generateMap({ hires: true }) - }; - } -} diff --git a/package.json b/package.json index 9f54a119..1cbe2da7 100644 --- a/package.json +++ b/package.json @@ -81,10 +81,12 @@ "exports": { "import": { "types": "./bson.d.ts", + "node": "./lib/bson.node.mjs", "default": "./lib/bson.mjs" }, "require": { "types": "./bson.d.ts", + "node": "./lib/bson.node.cjs", "default": "./lib/bson.cjs" }, "react-native": "./lib/bson.rn.cjs", @@ -107,7 +109,7 @@ "check:granular-bench": "npm run build:bench && node ./test/bench/etc/run_granular_benchmarks.js", "check:spec-bench": "npm run build:bench && node ./test/bench/lib/spec/bsonBench.js", "build:bench": "cd test/bench && npx tsc", - "build:ts": "node ./node_modules/typescript/bin/tsc", + "build:ts": "node ./node_modules/typescript/bin/tsc --build --force", "build:dts": "npm run build:ts && api-extractor run --typescript-compiler-folder node_modules/typescript --local && node etc/clean_definition_files.cjs", "build:bundle": "rollup -c rollup.config.mjs", "build": "npm run build:dts && npm run build:bundle", diff --git a/rollup.config.mjs b/rollup.config.mjs index 16a0376d..3e357e29 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -1,6 +1,5 @@ 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 { RequireVendor } from './etc/rollup/rollup-plugin-require-vendor/require_vendor.mjs'; /** @type {typescript.RollupTypescriptOptions} */ @@ -27,8 +26,17 @@ const tsConfig = { declaration: false, types: [], tsconfig: false, - include: ['src/**/*'] + include: ['src/**/*'], + exclude: ['src/node.ts'], }; + +/** @type {typescript.RollupTypescriptOptions} */ +const nodeTsConfig = { + ...tsConfig, + exclude: [], + types: ['node'] +}; + const input = 'src/index.ts'; /** @type {import('rollup').RollupOptions} */ @@ -43,6 +51,11 @@ const config = [ exports: 'named', sourcemap: true }, + { + file: 'lib/bson.mjs', + format: 'esm', + sourcemap: true + }, { file: 'lib/bson.bundle.js', format: 'iife', @@ -54,13 +67,18 @@ const config = [ ] }, { - input, - plugins: [typescript(tsConfig), new RequireRewriter(), nodeResolve({ resolveOnly: [] })], - output: { - file: 'lib/bson.mjs', + input: 'src/node.ts', + plugins: [typescript(nodeTsConfig), nodeResolve({ resolveOnly: ['node:crypto'] })], + output: [{ + file: 'lib/bson.node.mjs', format: 'esm', sourcemap: true - } + }, { + file: 'lib/bson.node.cjs', + format: 'commonjs', + exports: 'named', + sourcemap: true + }] }, { input, diff --git a/src/node.ts b/src/node.ts new file mode 100644 index 00000000..09061afc --- /dev/null +++ b/src/node.ts @@ -0,0 +1,6 @@ +import { randomBytes } from 'node:crypto'; + +import { nodeJsByteUtils } from './utils/node_byte_utils'; +nodeJsByteUtils.randomBytes = randomBytes; + +export * from './index'; diff --git a/src/utils/node_byte_utils.ts b/src/utils/node_byte_utils.ts index d6a641a4..9aebb5ab 100644 --- a/src/utils/node_byte_utils.ts +++ b/src/utils/node_byte_utils.ts @@ -25,37 +25,6 @@ type NodeJsBufferConstructor = Omit & { // This can be nullish, but we gate the nodejs functions on being exported whether or not this exists // Node.js global declare const Buffer: NodeJsBufferConstructor; -declare const require: (mod: 'crypto') => { randomBytes: (byteLength: number) => Uint8Array }; - -/** @internal */ -export function nodejsMathRandomBytes(byteLength: number) { - return nodeJsByteUtils.fromNumberArray( - Array.from({ length: byteLength }, () => Math.floor(Math.random() * 256)) - ); -} - -/** - * @internal - * WARNING: REQUIRE WILL BE REWRITTEN - * - * This code is carefully used by require_rewriter.mjs any modifications must be reflected in the plugin. - * - * @remarks - * "crypto" is the only dependency BSON needs. This presents a problem for creating a bundle of the BSON library - * in an es module format that can be used both on the browser and in Node.js. In Node.js when BSON is imported as - * an es module, there will be no global require function defined, making the code below fallback to the much less desireable math.random bytes. - * In order to make our es module bundle work as expected on Node.js we need to change this `require()` to a dynamic import, and the dynamic - * import must be top-level awaited since es modules are async. So we rely on a custom rollup plugin to seek out the following lines of code - * and replace `require` with `await import` and the IIFE line (`nodejsRandomBytes = (() => { ... })()`) with `nodejsRandomBytes = await (async () => { ... })()` - * when generating an es module bundle. - */ -const nodejsRandomBytes: (byteLength: number) => Uint8Array = (() => { - try { - return require('crypto').randomBytes; - } catch { - return nodejsMathRandomBytes; - } -})(); /** @internal */ export const nodeJsByteUtils = { @@ -162,5 +131,9 @@ export const nodeJsByteUtils = { return nodeJsByteUtils.toLocalBufferType(buffer).write(source, byteOffset, undefined, 'utf8'); }, - randomBytes: nodejsRandomBytes + randomBytes(byteLength: number) { + return nodeJsByteUtils.fromNumberArray( + Array.from({ length: byteLength }, () => Math.floor(Math.random() * 256)) + ); + } }; diff --git a/tsconfig.common.json b/tsconfig.common.json new file mode 100644 index 00000000..910d8f99 --- /dev/null +++ b/tsconfig.common.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "composite": true, + "allowJs": false, + "checkJs": false, + "strict": true, + "alwaysStrict": true, + "target": "es2021", + "module": "commonjs", + "moduleResolution": "node", + "skipLibCheck": true, + "lib": [ + "es2021", + "ES2022.Error" + ], + "outDir": "lib", + "importHelpers": false, + "noEmitHelpers": false, + "noEmitOnError": true, + "emitDeclarationOnly": true, + // Generate separate source maps files with sourceContent included + "sourceMap": true, + "inlineSourceMap": false, + "inlineSources": false, + // API-extractor makes use of the declarations, npm script should be cleaning these up + "declaration": true, + "declarationMap": true, + "types": [], + "isolatedModules": true, + "rootDir": "src" + }, + "ts-node": { + "transpileOnly": true, + "compiler": "typescript-cached-transpile", + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "src/node.ts" + ] + } + \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d11664fe..1ea283bb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,37 +1,7 @@ { - "compilerOptions": { - "allowJs": false, - "checkJs": false, - "strict": true, - "alwaysStrict": true, - "target": "es2021", - "module": "commonjs", - "moduleResolution": "node", - "skipLibCheck": true, - "lib": [ - "es2021", - "ES2022.Error" - ], - "outDir": "lib", - "importHelpers": false, - "noEmitHelpers": false, - "noEmitOnError": true, - "emitDeclarationOnly": true, - // Generate separate source maps files with sourceContent included - "sourceMap": true, - "inlineSourceMap": false, - "inlineSources": false, - // API-extractor makes use of the declarations, npm script should be cleaning these up - "declaration": true, - "declarationMap": true, - "types": [], - "isolatedModules": true - }, - "ts-node": { - "transpileOnly": true, - "compiler": "typescript-cached-transpile", - }, - "include": [ - "src/**/*" + "files": [], + "references": [ + { "path": "./tsconfig.common.json" }, + { "path": "./tsconfig.node.json" } ] } diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 00000000..1058af66 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.common.json", + "compilerOptions": { + "types": ["node"] + }, + "include": [], + "files": [ + "src/node.ts" + ], + "references": [ + { "path": "./tsconfig.common.json" } + ] +} \ No newline at end of file