Skip to content

Commit d2b14ed

Browse files
sapphi-redCopilot
andauthored
feat(commonjs)!: revert #1909 and add requireNodeBuiltins option (#1937)
* Initial plan * Add requireNodeBuiltins option to control createRequire injection Co-authored-by: sapphi-red <[email protected]> * Address code review feedback - use explicit boolean checks Co-authored-by: sapphi-red <[email protected]> * Add requireNodeBuiltins: false variant tests for 3 fixtures Co-authored-by: sapphi-red <[email protected]> * Generate snapshots for requireNodeBuiltins: false variant tests Co-authored-by: sapphi-red <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]>
1 parent 4dfad64 commit d2b14ed

File tree

15 files changed

+210
-20
lines changed

15 files changed

+210
-20
lines changed

packages/commonjs/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,34 @@ You can also provide a [picomatch pattern](https://github.com/micromatch/picomat
6363

6464
`"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"`.
6565

66+
### `requireNodeBuiltins`
67+
68+
Type: `boolean`<br>
69+
Default: `false`
70+
71+
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()`.
72+
73+
**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.
74+
75+
Example:
76+
77+
```js
78+
commonjs({
79+
strictRequires: true,
80+
requireNodeBuiltins: true
81+
});
82+
```
83+
84+
With `requireNodeBuiltins: true`, code like:
85+
86+
```js
87+
if (condition) {
88+
require('node:fs');
89+
}
90+
```
91+
92+
will generate output using `createRequire` instead of hoisting the import to the top of the file.
93+
6694
### `dynamicRequireTargets`
6795

6896
Type: `string | string[]`<br>

packages/commonjs/src/index.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ export default function commonjs(options = {}) {
4242
ignoreDynamicRequires,
4343
requireReturnsDefault: requireReturnsDefaultOption,
4444
defaultIsModuleExports: defaultIsModuleExportsOption,
45-
esmExternals
45+
esmExternals,
46+
requireNodeBuiltins = false
4647
} = options;
4748
const extensions = options.extensions || ['.js'];
4849
const filter = createFilter(options.include, options.exclude);
@@ -215,7 +216,8 @@ export default function commonjs(options = {}) {
215216
requireResolver = getRequireResolver(
216217
extensions,
217218
detectCyclesAndConditional,
218-
currentlyResolving
219+
currentlyResolving,
220+
requireNodeBuiltins
219221
);
220222
},
221223

@@ -263,7 +265,7 @@ export default function commonjs(options = {}) {
263265

264266
if (isWrappedId(id, EXTERNAL_SUFFIX)) {
265267
const actualId = unwrapId(id, EXTERNAL_SUFFIX);
266-
if (actualId.startsWith('node:')) {
268+
if (requireNodeBuiltins === true && actualId.startsWith('node:')) {
267269
return getExternalBuiltinRequireProxy(actualId);
268270
}
269271
return getUnknownRequireProxy(

packages/commonjs/src/resolve-require-sources.js

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import {
1010
} from './helpers';
1111
import { resolveExtensions } from './resolve-id';
1212

13-
export function getRequireResolver(extensions, detectCyclesAndConditional, currentlyResolving) {
13+
export function getRequireResolver(
14+
extensions,
15+
detectCyclesAndConditional,
16+
currentlyResolving,
17+
requireNodeBuiltins
18+
) {
1419
const knownCjsModuleTypes = Object.create(null);
1520
const requiredIds = Object.create(null);
1621
const unconditionallyRequiredIds = Object.create(null);
@@ -195,21 +200,24 @@ export function getRequireResolver(extensions, detectCyclesAndConditional, curre
195200
getTypeForFullyAnalyzedModule(dependencyId));
196201
// Special-case external Node built-ins to be handled via a lazy __require
197202
// helper instead of hoisted ESM imports when strict wrapping is used.
203+
// Only apply this when requireNodeBuiltins option is enabled.
198204
const isExternalWrapped = isWrappedId(dependencyId, EXTERNAL_SUFFIX);
199205
let resolvedDependencyId = dependencyId;
200-
if (parentMeta.isCommonJS === IS_WRAPPED_COMMONJS && !allowProxy && isExternalWrapped) {
201-
const actualExternalId = unwrapId(dependencyId, EXTERNAL_SUFFIX);
202-
if (actualExternalId.startsWith('node:')) {
203-
isCommonJS = IS_WRAPPED_COMMONJS;
204-
parentMeta.isRequiredCommonJS[dependencyId] = isCommonJS;
205-
}
206-
} else if (isExternalWrapped && !allowProxy) {
207-
// If the parent is not wrapped but the dependency is a node: builtin external,
208-
// unwrap the EXTERNAL_SUFFIX so it's treated as a normal external.
209-
// This avoids trying to load the lazy __require proxy for non-wrapped contexts.
210-
const actualExternalId = unwrapId(dependencyId, EXTERNAL_SUFFIX);
211-
if (actualExternalId.startsWith('node:')) {
212-
resolvedDependencyId = actualExternalId;
206+
if (requireNodeBuiltins === true) {
207+
if (parentMeta.isCommonJS === IS_WRAPPED_COMMONJS && !allowProxy && isExternalWrapped) {
208+
const actualExternalId = unwrapId(dependencyId, EXTERNAL_SUFFIX);
209+
if (actualExternalId.startsWith('node:')) {
210+
isCommonJS = IS_WRAPPED_COMMONJS;
211+
parentMeta.isRequiredCommonJS[dependencyId] = isCommonJS;
212+
}
213+
} else if (isExternalWrapped && !allowProxy) {
214+
// If the parent is not wrapped but the dependency is a node: builtin external,
215+
// unwrap the EXTERNAL_SUFFIX so it's treated as a normal external.
216+
// This avoids trying to load the lazy __require proxy for non-wrapped contexts.
217+
const actualExternalId = unwrapId(dependencyId, EXTERNAL_SUFFIX);
218+
if (actualExternalId.startsWith('node:')) {
219+
resolvedDependencyId = actualExternalId;
220+
}
213221
}
214222
}
215223
const isWrappedCommonJS = isCommonJS === IS_WRAPPED_COMMONJS;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module.exports = {
2+
description:
3+
'does not crash and does not mark external node: builtins as pure when strictRequires is true and requireNodeBuiltins is false (default)',
4+
pluginOptions: {
5+
strictRequires: true,
6+
requireNodeBuiltins: false
7+
},
8+
context: {
9+
__filename: __filename
10+
}
11+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Top-level require of a Node builtin ensures the transform computes
2+
// wrappedModuleSideEffects for an external wrapped dependency.
3+
function unused() {
4+
// External Node builtin require; not executed at runtime
5+
require('node:crypto');
6+
}
7+
8+
module.exports = 1;

packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped/_config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ module.exports = {
22
description:
33
'does not crash and does not mark external node: builtins as pure when strictRequires is true',
44
pluginOptions: {
5-
strictRequires: true
5+
strictRequires: true,
6+
requireNodeBuiltins: true
67
},
78
context: {
89
__filename: __filename
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module.exports = {
2+
description: 'handles node: builtins correctly with strictRequires: auto and requireNodeBuiltins: false (default)',
3+
pluginOptions: {
4+
strictRequires: 'auto',
5+
requireNodeBuiltins: false
6+
},
7+
exports: (exports, t) => {
8+
// Should be able to access properties of node:stream
9+
t.truthy(exports.Readable);
10+
t.is(typeof exports.Readable, 'function');
11+
// Should be able to instantiate
12+
t.truthy(exports.readable);
13+
}
14+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const stream = require('node:stream');
2+
const readable = new stream.Readable({});
3+
4+
module.exports = { Readable: stream.Readable, readable };

packages/commonjs/test/fixtures/function/strict-requires-auto-external-node-builtin/_config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
module.exports = {
22
description: 'handles node: builtins correctly with strictRequires: auto',
33
pluginOptions: {
4-
strictRequires: 'auto'
4+
strictRequires: 'auto',
5+
requireNodeBuiltins: true
56
},
67
exports: (exports, t) => {
78
// Should be able to access properties of node:stream
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = {
2+
description: "hoists external node built-in requires when requireNodeBuiltins is false (default)",
3+
pluginOptions: {
4+
strictRequires: true,
5+
requireNodeBuiltins: false
6+
},
7+
exports: (exports, t) => {
8+
t.is(exports, 42);
9+
}
10+
};

0 commit comments

Comments
 (0)