Skip to content

Commit a77af35

Browse files
committed
feat(commonjs): add 'externalBuiltinsRequire' option to stub node: builtins proxy for edge runtimes; keep default as 'create-require' (#1924)
1 parent b079b59 commit a77af35

File tree

8 files changed

+107
-2
lines changed

8 files changed

+107
-2
lines changed

packages/commonjs/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,33 @@ When this option is set to `false`, the generated code will either directly thro
183183

184184
Setting this option to `true` will instead leave the `require` call in the code or use it as a fallback for `dynamicRequireTargets`.
185185

186+
### `externalBuiltinsRequire`
187+
188+
Type: `'create-require' | 'stub'`
189+
Default: `'create-require'`
190+
191+
Controls how external Node built-ins (e.g. `require('node:fs')`) that are required from wrapped CommonJS modules are handled.
192+
193+
- `'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.
194+
- `'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.
195+
196+
Example (avoid `node:module` for edge targets):
197+
198+
```js
199+
import commonjs from '@rollup/plugin-commonjs';
200+
201+
export default {
202+
input: 'src/index.js',
203+
output: { format: 'es' },
204+
plugins: [
205+
commonjs({
206+
strictRequires: true,
207+
externalBuiltinsRequire: 'stub'
208+
})
209+
]
210+
};
211+
```
212+
186213
### `esmExternals`
187214

188215
Type: `boolean | string[] | ((id: string) => boolean)`

packages/commonjs/src/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export default function commonjs(options = {}) {
4444
defaultIsModuleExports: defaultIsModuleExportsOption,
4545
esmExternals
4646
} = options;
47+
const externalBuiltinsRequireStrategy =
48+
options.externalBuiltinsRequire === 'stub' ? 'stub' : 'create-require';
4749
const extensions = options.extensions || ['.js'];
4850
const filter = createFilter(options.include, options.exclude);
4951
const isPossibleCjsId = (id) => {
@@ -264,7 +266,7 @@ export default function commonjs(options = {}) {
264266
if (isWrappedId(id, EXTERNAL_SUFFIX)) {
265267
const actualId = unwrapId(id, EXTERNAL_SUFFIX);
266268
if (actualId.startsWith('node:')) {
267-
return getExternalBuiltinRequireProxy(actualId);
269+
return getExternalBuiltinRequireProxy(actualId, externalBuiltinsRequireStrategy);
268270
}
269271
return getUnknownRequireProxy(
270272
actualId,

packages/commonjs/src/proxies.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,24 @@ export function getEsImportProxy(id, defaultIsModuleExports, moduleSideEffects)
9090
// hoist an ESM import of the built-in (which would eagerly load it). Instead,
9191
// expose a lazy `__require()` that resolves the built-in at runtime via
9292
// `createRequire(import.meta.url)`.
93-
export function getExternalBuiltinRequireProxy(id) {
93+
/**
94+
* Generate the proxy module used for external Node built-ins that are
95+
* `require()`d from wrapped CommonJS modules.
96+
*
97+
* Strategy:
98+
* - 'create-require' (default): import `createRequire` from 'node:module' and
99+
* lazily resolve the built-in at runtime. This keeps Node behaviour and
100+
* avoids hoisting, but hard-depends on Node's `module` API.
101+
* - 'stub': emit a tiny proxy that exports a throwing `__require()` without
102+
* importing from 'node:module'. This makes output parse/run in edge
103+
* runtimes when the path is dead, and fails loudly if executed.
104+
*/
105+
export function getExternalBuiltinRequireProxy(id, strategy = 'create-require') {
94106
const stringifiedId = JSON.stringify(id);
107+
if (strategy === 'stub') {
108+
const msg = `Node built-in ${stringifiedId} is not available in this environment`;
109+
return `export function __require() { throw new Error(${JSON.stringify(msg)}); }`;
110+
}
95111
return (
96112
`import { createRequire } from 'node:module';\n` +
97113
`const require = createRequire(import.meta.url);\n` +
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module.exports = {
2+
description:
3+
"uses 'stub' proxy for external node: builtins when configured, avoiding node:module import",
4+
pluginOptions: {
5+
strictRequires: true,
6+
externalBuiltinsRequire: 'stub'
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/snapshots/function.js.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6731,6 +6731,35 @@ Generated by [AVA](https://avajs.dev).
67316731
`,
67326732
}
67336733

6734+
## module-side-effects-external-node-builtin-wrapped-stub
6735+
6736+
> Snapshot 1
6737+
6738+
{
6739+
'main.js': `'use strict';␊
6740+
6741+
function getDefaultExportFromCjs (x) {␊
6742+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;␊
6743+
}␊
6744+
6745+
var main$1;␊
6746+
var hasRequiredMain;␊
6747+
6748+
function requireMain () {␊
6749+
if (hasRequiredMain) return main$1;␊
6750+
hasRequiredMain = 1;␊
6751+
6752+
main$1 = 1;␊
6753+
return main$1;␊
6754+
}␊
6755+
6756+
var mainExports = requireMain();␊
6757+
var main = /*@__PURE__*/getDefaultExportFromCjs(mainExports);␊
6758+
6759+
module.exports = main;␊
6760+
`,
6761+
}
6762+
67346763
## module-side-effects-import-wrapped
67356764

67366765
> Snapshot 1
24 Bytes
Binary file not shown.

packages/commonjs/types/index.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,18 @@ interface RollupCommonJSOptions {
225225
* home directory name. By default, it uses the current working directory.
226226
*/
227227
dynamicRequireRoot?: string;
228+
229+
/**
230+
* Controls how `require('node:*')` dependencies of wrapped CommonJS modules are
231+
* handled. The default uses Node's `module.createRequire` lazily to resolve the
232+
* built-in at runtime. Set to `'stub'` to avoid importing from `node:module` and
233+
* instead emit a tiny proxy whose `__require()` throws at runtime. This can be
234+
* useful for edge runtimes that do not support Node built-ins when those code paths
235+
* are never executed.
236+
*
237+
* @default 'create-require'
238+
*/
239+
externalBuiltinsRequire?: 'create-require' | 'stub';
228240
}
229241

230242
/**

0 commit comments

Comments
 (0)