diff --git a/packages/commonjs/README.md b/packages/commonjs/README.md index 7eda39f93..2239f9a60 100644 --- a/packages/commonjs/README.md +++ b/packages/commonjs/README.md @@ -183,6 +183,33 @@ When this option is set to `false`, the generated code will either directly thro Setting this option to `true` will instead leave the `require` call in the code or use it as a fallback for `dynamicRequireTargets`. +### `externalBuiltinsRequire` + +Type: `'create-require' | 'stub'` +Default: `'create-require'` + +Controls how external Node built-ins (e.g. `require('node:fs')`) that are required from wrapped CommonJS modules are handled. + +- `'create-require'` (default): lazily resolve the built-in at runtime using `module.createRequire(import.meta.url)`. This matches Node behaviour and avoids hoisting, but introduces a hard dependency on `node:module` in the generated output. +- `'stub'`: emit a tiny proxy that exports a throwing `__require()` without importing from `node:module`. This avoids the `node:module` import so bundles can parse/run in edge runtimes when those code paths are never executed. If the path is executed at runtime, it will throw with a clear error message. + +Example (avoid `node:module` for edge targets): + +```js +import commonjs from '@rollup/plugin-commonjs'; + +export default { + input: 'src/index.js', + output: { format: 'es' }, + plugins: [ + commonjs({ + strictRequires: true, + externalBuiltinsRequire: 'stub' + }) + ] +}; +``` + ### `esmExternals` Type: `boolean | string[] | ((id: string) => boolean)` diff --git a/packages/commonjs/src/index.js b/packages/commonjs/src/index.js index 33dcda93e..9e64345f7 100644 --- a/packages/commonjs/src/index.js +++ b/packages/commonjs/src/index.js @@ -44,6 +44,9 @@ export default function commonjs(options = {}) { defaultIsModuleExports: defaultIsModuleExportsOption, esmExternals } = options; + const rawExternalBuiltinsRequire = options.externalBuiltinsRequire; + const externalBuiltinsRequireStrategy = + rawExternalBuiltinsRequire === 'stub' ? 'stub' : 'create-require'; const extensions = options.extensions || ['.js']; const filter = createFilter(options.include, options.exclude); const isPossibleCjsId = (id) => { @@ -212,6 +215,15 @@ export default function commonjs(options = {}) { 'The namedExports option from "@rollup/plugin-commonjs" is deprecated. Named exports are now handled automatically.' ); } + if ( + rawExternalBuiltinsRequire != null && + rawExternalBuiltinsRequire !== 'create-require' && + rawExternalBuiltinsRequire !== 'stub' + ) { + this.warn( + `${PLUGIN_NAME}: invalid externalBuiltinsRequire "${rawExternalBuiltinsRequire}", using "create-require"` + ); + } requireResolver = getRequireResolver( extensions, detectCyclesAndConditional, @@ -264,7 +276,7 @@ export default function commonjs(options = {}) { if (isWrappedId(id, EXTERNAL_SUFFIX)) { const actualId = unwrapId(id, EXTERNAL_SUFFIX); if (actualId.startsWith('node:')) { - return getExternalBuiltinRequireProxy(actualId); + return getExternalBuiltinRequireProxy(actualId, externalBuiltinsRequireStrategy); } return getUnknownRequireProxy( actualId, diff --git a/packages/commonjs/src/proxies.js b/packages/commonjs/src/proxies.js index 72aae3640..77b8f611e 100644 --- a/packages/commonjs/src/proxies.js +++ b/packages/commonjs/src/proxies.js @@ -90,7 +90,23 @@ export function getEsImportProxy(id, defaultIsModuleExports, moduleSideEffects) // hoist an ESM import of the built-in (which would eagerly load it). Instead, // expose a lazy `__require()` that resolves the built-in at runtime via // `createRequire(import.meta.url)`. -export function getExternalBuiltinRequireProxy(id) { +/** + * Generate the proxy module used for external Node built-ins that are + * `require()`d from wrapped CommonJS modules. + * + * Strategy: + * - 'create-require' (default): import `createRequire` from 'node:module' and + * lazily resolve the built-in at runtime. This keeps Node behaviour and + * avoids hoisting, but hard-depends on Node's `module` API. + * - 'stub': emit a tiny proxy that exports a throwing `__require()` without + * importing from 'node:module'. This makes output parse/run in edge + * runtimes when the path is dead, and fails loudly if executed. + */ +export function getExternalBuiltinRequireProxy(id, strategy = 'create-require') { + if (strategy === 'stub') { + const msg = `Node built-in ${id} is not available in this environment`; + return `export function __require() { throw new Error(${JSON.stringify(msg)}); }`; + } const stringifiedId = JSON.stringify(id); return ( `import { createRequire } from 'node:module';\n` + diff --git a/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped-stub/_config.js b/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped-stub/_config.js new file mode 100644 index 000000000..5c8abddb8 --- /dev/null +++ b/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped-stub/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: + "uses 'stub' proxy for external node: builtins when configured, avoiding node:module import", + pluginOptions: { + strictRequires: true, + externalBuiltinsRequire: 'stub' + }, + context: { + __filename: __filename + } +}; diff --git a/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped-stub/main.js b/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped-stub/main.js new file mode 100644 index 000000000..fdb88b6fc --- /dev/null +++ b/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped-stub/main.js @@ -0,0 +1,20 @@ +// Ensure the transform computes `wrappedModuleSideEffects` for an external +// wrapped dependency by including a Node builtin `require()` inside a function. +function unused() { + // External Node builtin require – converted to an external proxy. + // When `externalBuiltinsRequire: 'stub'`, calling this will throw; we + // invoke it inside a try/catch below so the test can snapshot the emitted + // stub proxy without failing at runtime. + require('node:crypto'); +} + +try { + unused(); +} catch (_err) { + // Expected: in this fixture we configure `externalBuiltinsRequire: 'stub'`, + // so calling the proxy's `__require()` throws. We swallow the error so the + // test can assert on the generated code (no `node:module` import) without + // failing at runtime. +} + +module.exports = 1; diff --git a/packages/commonjs/test/snapshots/function.js.md b/packages/commonjs/test/snapshots/function.js.md index 324a7f2ca..aba07a508 100644 --- a/packages/commonjs/test/snapshots/function.js.md +++ b/packages/commonjs/test/snapshots/function.js.md @@ -6731,6 +6731,55 @@ Generated by [AVA](https://avajs.dev). `, } +## module-side-effects-external-node-builtin-wrapped-stub + +> Snapshot 1 + + { + 'main.js': `'use strict';␊ + ␊ + function getDefaultExportFromCjs (x) {␊ + return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;␊ + }␊ + ␊ + function __require() { throw new Error("Node built-in node:crypto is not available in this environment"); }␊ + ␊ + var main$1;␊ + var hasRequiredMain;␊ + ␊ + function requireMain () {␊ + if (hasRequiredMain) return main$1;␊ + hasRequiredMain = 1;␊ + // Ensure the transform computes \`wrappedModuleSideEffects\` for an external␊ + // wrapped dependency by including a Node builtin \`require()\` inside a function.␊ + function unused() {␊ + // External Node builtin require – converted to an external proxy.␊ + // When \`externalBuiltinsRequire: 'stub'\`, calling this will throw; we␊ + // invoke it inside a try/catch below so the test can snapshot the emitted␊ + // stub proxy without failing at runtime.␊ + __require();␊ + }␊ + ␊ + try {␊ + unused();␊ + } catch (_err) {␊ + // Expected: in this fixture we configure \`externalBuiltinsRequire: 'stub'\`,␊ + // so calling the proxy's \`__require()\` throws. We swallow the error so the␊ + // test can assert on the generated code (no \`node:module\` import) without␊ + // failing at runtime.␊ + }␊ + ␊ + main$1 = 1;␊ + return main$1;␊ + }␊ + ␊ + var mainExports = requireMain();␊ + var main = /*@__PURE__*/getDefaultExportFromCjs(mainExports);␊ + ␊ + module.exports = main;␊ + `, + } + ## module-side-effects-import-wrapped > Snapshot 1 diff --git a/packages/commonjs/test/snapshots/function.js.snap b/packages/commonjs/test/snapshots/function.js.snap index 3d1906593..baaeb663e 100644 Binary files a/packages/commonjs/test/snapshots/function.js.snap and b/packages/commonjs/test/snapshots/function.js.snap differ diff --git a/packages/commonjs/types/index.d.ts b/packages/commonjs/types/index.d.ts index 37453bd92..0493b4bb5 100644 --- a/packages/commonjs/types/index.d.ts +++ b/packages/commonjs/types/index.d.ts @@ -225,6 +225,18 @@ interface RollupCommonJSOptions { * home directory name. By default, it uses the current working directory. */ dynamicRequireRoot?: string; + + /** + * Controls how `require('node:*')` dependencies of wrapped CommonJS modules are + * handled. The default uses Node's `module.createRequire` lazily to resolve the + * built-in at runtime. Set to `'stub'` to avoid importing from `node:module` and + * instead emit a tiny proxy whose `__require()` throws at runtime. This can be + * useful for edge runtimes that do not support Node built-ins when those code paths + * are never executed. + * + * @default 'create-require' + */ + externalBuiltinsRequire?: 'create-require' | 'stub'; } /**