diff --git a/packages/commonjs/README.md b/packages/commonjs/README.md
index 7eda39f93..fd4a69568 100644
--- a/packages/commonjs/README.md
+++ b/packages/commonjs/README.md
@@ -63,6 +63,34 @@ You can also provide a [picomatch pattern](https://github.com/micromatch/picomat
`"debug"` works like `"auto"` but after bundling, it will display a warning containing a list of ids that have been wrapped which can be used as picomatch pattern for fine-tuning or to avoid the potential race conditions mentioned for `"auto"`.
+### `requireNodeBuiltins`
+
+Type: `boolean`
+Default: `false`
+
+When enabled, external Node built-ins (e.g., `node:fs`, `node:path`) required from wrapped CommonJS modules will use `createRequire(import.meta.url)` instead of being hoisted as ESM imports. This prevents eager loading of Node built-ins at module initialization time and preserves the lazy execution semantics of `require()`.
+
+**Important:** Enabling this option adds a dependency on `node:module` in the output bundle, which may not be available in some environments like edge runtimes (Cloudflare Workers, Vercel Edge Runtime). Only enable this option if you are targeting Node.js environments and need the lazy loading behavior for Node built-ins.
+
+Example:
+
+```js
+commonjs({
+ strictRequires: true,
+ requireNodeBuiltins: true
+});
+```
+
+With `requireNodeBuiltins: true`, code like:
+
+```js
+if (condition) {
+ require('node:fs');
+}
+```
+
+will generate output using `createRequire` instead of hoisting the import to the top of the file.
+
### `dynamicRequireTargets`
Type: `string | string[]`
diff --git a/packages/commonjs/src/index.js b/packages/commonjs/src/index.js
index 33dcda93e..6476ab423 100644
--- a/packages/commonjs/src/index.js
+++ b/packages/commonjs/src/index.js
@@ -42,7 +42,8 @@ export default function commonjs(options = {}) {
ignoreDynamicRequires,
requireReturnsDefault: requireReturnsDefaultOption,
defaultIsModuleExports: defaultIsModuleExportsOption,
- esmExternals
+ esmExternals,
+ requireNodeBuiltins = false
} = options;
const extensions = options.extensions || ['.js'];
const filter = createFilter(options.include, options.exclude);
@@ -215,7 +216,8 @@ export default function commonjs(options = {}) {
requireResolver = getRequireResolver(
extensions,
detectCyclesAndConditional,
- currentlyResolving
+ currentlyResolving,
+ requireNodeBuiltins
);
},
@@ -263,7 +265,7 @@ export default function commonjs(options = {}) {
if (isWrappedId(id, EXTERNAL_SUFFIX)) {
const actualId = unwrapId(id, EXTERNAL_SUFFIX);
- if (actualId.startsWith('node:')) {
+ if (requireNodeBuiltins === true && actualId.startsWith('node:')) {
return getExternalBuiltinRequireProxy(actualId);
}
return getUnknownRequireProxy(
diff --git a/packages/commonjs/src/resolve-require-sources.js b/packages/commonjs/src/resolve-require-sources.js
index 5f7fe0726..8f81cb820 100644
--- a/packages/commonjs/src/resolve-require-sources.js
+++ b/packages/commonjs/src/resolve-require-sources.js
@@ -10,7 +10,12 @@ import {
} from './helpers';
import { resolveExtensions } from './resolve-id';
-export function getRequireResolver(extensions, detectCyclesAndConditional, currentlyResolving) {
+export function getRequireResolver(
+ extensions,
+ detectCyclesAndConditional,
+ currentlyResolving,
+ requireNodeBuiltins
+) {
const knownCjsModuleTypes = Object.create(null);
const requiredIds = Object.create(null);
const unconditionallyRequiredIds = Object.create(null);
@@ -195,21 +200,24 @@ export function getRequireResolver(extensions, detectCyclesAndConditional, curre
getTypeForFullyAnalyzedModule(dependencyId));
// Special-case external Node built-ins to be handled via a lazy __require
// helper instead of hoisted ESM imports when strict wrapping is used.
+ // Only apply this when requireNodeBuiltins option is enabled.
const isExternalWrapped = isWrappedId(dependencyId, EXTERNAL_SUFFIX);
let resolvedDependencyId = dependencyId;
- if (parentMeta.isCommonJS === IS_WRAPPED_COMMONJS && !allowProxy && isExternalWrapped) {
- const actualExternalId = unwrapId(dependencyId, EXTERNAL_SUFFIX);
- if (actualExternalId.startsWith('node:')) {
- isCommonJS = IS_WRAPPED_COMMONJS;
- parentMeta.isRequiredCommonJS[dependencyId] = isCommonJS;
- }
- } else if (isExternalWrapped && !allowProxy) {
- // If the parent is not wrapped but the dependency is a node: builtin external,
- // unwrap the EXTERNAL_SUFFIX so it's treated as a normal external.
- // This avoids trying to load the lazy __require proxy for non-wrapped contexts.
- const actualExternalId = unwrapId(dependencyId, EXTERNAL_SUFFIX);
- if (actualExternalId.startsWith('node:')) {
- resolvedDependencyId = actualExternalId;
+ if (requireNodeBuiltins === true) {
+ if (parentMeta.isCommonJS === IS_WRAPPED_COMMONJS && !allowProxy && isExternalWrapped) {
+ const actualExternalId = unwrapId(dependencyId, EXTERNAL_SUFFIX);
+ if (actualExternalId.startsWith('node:')) {
+ isCommonJS = IS_WRAPPED_COMMONJS;
+ parentMeta.isRequiredCommonJS[dependencyId] = isCommonJS;
+ }
+ } else if (isExternalWrapped && !allowProxy) {
+ // If the parent is not wrapped but the dependency is a node: builtin external,
+ // unwrap the EXTERNAL_SUFFIX so it's treated as a normal external.
+ // This avoids trying to load the lazy __require proxy for non-wrapped contexts.
+ const actualExternalId = unwrapId(dependencyId, EXTERNAL_SUFFIX);
+ if (actualExternalId.startsWith('node:')) {
+ resolvedDependencyId = actualExternalId;
+ }
}
}
const isWrappedCommonJS = isCommonJS === IS_WRAPPED_COMMONJS;
diff --git a/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped-default/_config.js b/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped-default/_config.js
new file mode 100644
index 000000000..4e4a4fbfd
--- /dev/null
+++ b/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped-default/_config.js
@@ -0,0 +1,11 @@
+module.exports = {
+ description:
+ 'does not crash and does not mark external node: builtins as pure when strictRequires is true and requireNodeBuiltins is false (default)',
+ pluginOptions: {
+ strictRequires: true,
+ requireNodeBuiltins: false
+ },
+ context: {
+ __filename: __filename
+ }
+};
diff --git a/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped-default/main.js b/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped-default/main.js
new file mode 100644
index 000000000..b6c0d07c2
--- /dev/null
+++ b/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped-default/main.js
@@ -0,0 +1,8 @@
+// Top-level require of a Node builtin ensures the transform computes
+// wrappedModuleSideEffects for an external wrapped dependency.
+function unused() {
+ // External Node builtin require; not executed at runtime
+ require('node:crypto');
+}
+
+module.exports = 1;
diff --git a/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped/_config.js b/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped/_config.js
index 6bc57ba61..a3e15a184 100644
--- a/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped/_config.js
+++ b/packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped/_config.js
@@ -2,7 +2,8 @@ module.exports = {
description:
'does not crash and does not mark external node: builtins as pure when strictRequires is true',
pluginOptions: {
- strictRequires: true
+ strictRequires: true,
+ requireNodeBuiltins: true
},
context: {
__filename: __filename
diff --git a/packages/commonjs/test/fixtures/function/strict-requires-auto-external-node-builtin-default/_config.js b/packages/commonjs/test/fixtures/function/strict-requires-auto-external-node-builtin-default/_config.js
new file mode 100644
index 000000000..fdbaf8f40
--- /dev/null
+++ b/packages/commonjs/test/fixtures/function/strict-requires-auto-external-node-builtin-default/_config.js
@@ -0,0 +1,14 @@
+module.exports = {
+ description: 'handles node: builtins correctly with strictRequires: auto and requireNodeBuiltins: false (default)',
+ pluginOptions: {
+ strictRequires: 'auto',
+ requireNodeBuiltins: false
+ },
+ exports: (exports, t) => {
+ // Should be able to access properties of node:stream
+ t.truthy(exports.Readable);
+ t.is(typeof exports.Readable, 'function');
+ // Should be able to instantiate
+ t.truthy(exports.readable);
+ }
+};
diff --git a/packages/commonjs/test/fixtures/function/strict-requires-auto-external-node-builtin-default/main.js b/packages/commonjs/test/fixtures/function/strict-requires-auto-external-node-builtin-default/main.js
new file mode 100644
index 000000000..d69942f74
--- /dev/null
+++ b/packages/commonjs/test/fixtures/function/strict-requires-auto-external-node-builtin-default/main.js
@@ -0,0 +1,4 @@
+const stream = require('node:stream');
+const readable = new stream.Readable({});
+
+module.exports = { Readable: stream.Readable, readable };
diff --git a/packages/commonjs/test/fixtures/function/strict-requires-auto-external-node-builtin/_config.js b/packages/commonjs/test/fixtures/function/strict-requires-auto-external-node-builtin/_config.js
index 6001fc98a..5e1b8eeb9 100644
--- a/packages/commonjs/test/fixtures/function/strict-requires-auto-external-node-builtin/_config.js
+++ b/packages/commonjs/test/fixtures/function/strict-requires-auto-external-node-builtin/_config.js
@@ -1,7 +1,8 @@
module.exports = {
description: 'handles node: builtins correctly with strictRequires: auto',
pluginOptions: {
- strictRequires: 'auto'
+ strictRequires: 'auto',
+ requireNodeBuiltins: true
},
exports: (exports, t) => {
// Should be able to access properties of node:stream
diff --git a/packages/commonjs/test/fixtures/function/strict-requires-external-node-builtin-default/_config.js b/packages/commonjs/test/fixtures/function/strict-requires-external-node-builtin-default/_config.js
new file mode 100644
index 000000000..fce880ea1
--- /dev/null
+++ b/packages/commonjs/test/fixtures/function/strict-requires-external-node-builtin-default/_config.js
@@ -0,0 +1,10 @@
+module.exports = {
+ description: "hoists external node built-in requires when requireNodeBuiltins is false (default)",
+ pluginOptions: {
+ strictRequires: true,
+ requireNodeBuiltins: false
+ },
+ exports: (exports, t) => {
+ t.is(exports, 42);
+ }
+};
diff --git a/packages/commonjs/test/fixtures/function/strict-requires-external-node-builtin-default/main.js b/packages/commonjs/test/fixtures/function/strict-requires-external-node-builtin-default/main.js
new file mode 100644
index 000000000..347fad0c2
--- /dev/null
+++ b/packages/commonjs/test/fixtures/function/strict-requires-external-node-builtin-default/main.js
@@ -0,0 +1,4 @@
+if (false) {
+ require('node:sqlite');
+}
+module.exports = 42;
diff --git a/packages/commonjs/test/fixtures/function/strict-requires-external-node-builtin/_config.js b/packages/commonjs/test/fixtures/function/strict-requires-external-node-builtin/_config.js
index f56002724..4995e9996 100644
--- a/packages/commonjs/test/fixtures/function/strict-requires-external-node-builtin/_config.js
+++ b/packages/commonjs/test/fixtures/function/strict-requires-external-node-builtin/_config.js
@@ -1,7 +1,8 @@
module.exports = {
description: "does not hoist external node built-in requires when strictRequires is true",
pluginOptions: {
- strictRequires: true
+ strictRequires: true,
+ requireNodeBuiltins: true
},
exports: (exports, t) => {
t.is(exports, 42);
diff --git a/packages/commonjs/test/snapshots/function.js.md b/packages/commonjs/test/snapshots/function.js.md
index 324a7f2ca..494b74a1b 100644
--- a/packages/commonjs/test/snapshots/function.js.md
+++ b/packages/commonjs/test/snapshots/function.js.md
@@ -6731,6 +6731,37 @@ Generated by [AVA](https://avajs.dev).
`,
}
+## module-side-effects-external-node-builtin-wrapped-default
+
+> Snapshot 1
+
+ {
+ 'main.js': `'use strict';␊
+ ␊
+ require('node:crypto');␊
+ ␊
+ function getDefaultExportFromCjs (x) {␊
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;␊
+ }␊
+ ␊
+ var main$1;␊
+ var hasRequiredMain;␊
+ ␊
+ function requireMain () {␊
+ if (hasRequiredMain) return main$1;␊
+ hasRequiredMain = 1;␊
+ ␊
+ main$1 = 1;␊
+ return main$1;␊
+ }␊
+ ␊
+ var mainExports = requireMain();␊
+ var main = /*@__PURE__*/getDefaultExportFromCjs(mainExports);␊
+ ␊
+ module.exports = main;␊
+ `,
+ }
+
## module-side-effects-import-wrapped
> Snapshot 1
@@ -8717,6 +8748,34 @@ Generated by [AVA](https://avajs.dev).
## strict-requires-auto-external-node-builtin
+> Snapshot 1
+
+ {
+ 'main.js': `'use strict';␊
+ ␊
+ var require$$0 = require('node:stream');␊
+ ␊
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }␊
+ ␊
+ var require$$0__default = /*#__PURE__*/_interopDefaultCompat(require$$0);␊
+ ␊
+ function getDefaultExportFromCjs (x) {␊
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;␊
+ }␊
+ ␊
+ const stream = require$$0__default.default;␊
+ const readable = new stream.Readable({});␊
+ ␊
+ var main = { Readable: stream.Readable, readable };␊
+ ␊
+ var main$1 = /*@__PURE__*/getDefaultExportFromCjs(main);␊
+ ␊
+ module.exports = main$1;␊
+ `,
+ }
+
+## strict-requires-auto-external-node-builtin-default
+
> Snapshot 1
{
@@ -9258,6 +9317,34 @@ Generated by [AVA](https://avajs.dev).
## strict-requires-external-node-builtin
+> Snapshot 1
+
+ {
+ 'main.js': `'use strict';␊
+ ␊
+ function getDefaultExportFromCjs (x) {␊
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;␊
+ }␊
+ ␊
+ var main$1;␊
+ var hasRequiredMain;␊
+ ␊
+ function requireMain () {␊
+ if (hasRequiredMain) return main$1;␊
+ hasRequiredMain = 1;␊
+ main$1 = 42;␊
+ return main$1;␊
+ }␊
+ ␊
+ var mainExports = requireMain();␊
+ var main = /*@__PURE__*/getDefaultExportFromCjs(mainExports);␊
+ ␊
+ module.exports = main;␊
+ `,
+ }
+
+## strict-requires-external-node-builtin-default
+
> Snapshot 1
{
diff --git a/packages/commonjs/test/snapshots/function.js.snap b/packages/commonjs/test/snapshots/function.js.snap
index 3d1906593..c05cb5899 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..2d6ad2004 100644
--- a/packages/commonjs/types/index.d.ts
+++ b/packages/commonjs/types/index.d.ts
@@ -225,6 +225,17 @@ interface RollupCommonJSOptions {
* home directory name. By default, it uses the current working directory.
*/
dynamicRequireRoot?: string;
+ /**
+ * When enabled, external Node built-ins (e.g., `node:fs`) required from wrapped CommonJS modules
+ * will use `createRequire(import.meta.url)` instead of being hoisted as ESM imports. This prevents
+ * eager loading of Node built-ins at module initialization time.
+ *
+ * Note: This option adds a dependency on `node:module` in the output, which may not be available
+ * in some environments like edge runtimes (Cloudflare Workers, Vercel Edge Runtime).
+ *
+ * @default false
+ */
+ requireNodeBuiltins?: boolean;
}
/**