Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/commonjs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that this is not what I expected. There should be an option that has the behavior before #1909, which is to hoist the require like non node: requires are done.
While createRequire is not supported, some builtin modules are supported in those runtimes. So stubbing all builtin modules would break some codes that were working.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sapphi-red could you please elaborate some more? the more information you can write down, the better the agent will understand the intent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1909 should be reverted and reapplied with the new option to ensure that the behavior is same when the option is set.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not what I had in mind. Charlie isn't likely to understand what that means in this context, and I'm not going to be doing that manually. Additionally that'll require bringing tests forward.

If you're not specific in a way an agent can understand, this isn't likely to move forward and the current behavior will stand. Please leave specifics on how Charlie can move in the direction you would like.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. Copilot can understand it at least.
sapphi-red#1


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)`
Expand Down
14 changes: 13 additions & 1 deletion packages/commonjs/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export default function commonjs(options = {}) {
defaultIsModuleExports: defaultIsModuleExportsOption,
esmExternals
} = options;
const rawExternalBuiltinsRequire = options.externalBuiltinsRequire;
const externalBuiltinsRequireStrategy =
rawExternalBuiltinsRequire === 'stub' ? 'stub' : 'create-require';
Comment on lines +47 to +49
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The allowed values ('create-require' | 'stub') and default are defined in multiple places (normalization here, validation later, and default in proxies.js). This duplication risks drift. Centralizing the strategies and default into constants would improve maintainability and keep behavior and warnings in sync.

Suggestion

Introduce shared constants and use them for normalization here (and for validation below):

// near the top
const EXTERNAL_BUILTINS_REQUIRE_DEFAULT = 'create-require';
const EXTERNAL_BUILTINS_REQUIRE_STRATEGIES = new Set(['create-require', 'stub']);

// ...
const rawExternalBuiltinsRequire = options.externalBuiltinsRequire;
const externalBuiltinsRequireStrategy =
  EXTERNAL_BUILTINS_REQUIRE_STRATEGIES.has(rawExternalBuiltinsRequire)
    ? rawExternalBuiltinsRequire
    : EXTERNAL_BUILTINS_REQUIRE_DEFAULT;

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this refactor.

const extensions = options.extensions || ['.js'];
const filter = createFilter(options.include, options.exclude);
const isPossibleCjsId = (id) => {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 17 additions & 1 deletion packages/commonjs/src/proxies.js
Original file line number Diff line number Diff line change
Expand Up @@ -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` +
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
};
Original file line number Diff line number Diff line change
@@ -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;
49 changes: 49 additions & 0 deletions packages/commonjs/test/snapshots/function.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file modified packages/commonjs/test/snapshots/function.js.snap
Binary file not shown.
12 changes: 12 additions & 0 deletions packages/commonjs/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}

/**
Expand Down
Loading