-
-
Notifications
You must be signed in to change notification settings - Fork 611
feat(commonjs): add externalBuiltinsRequire to stub external Node builtins when wrapped #1931
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat(commonjs): add externalBuiltinsRequire to stub external Node builtins when wrapped #1931
Conversation
…iltins proxy for edge runtimes; keep default as 'create-require' (#1924)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The feature is well-scoped and maintains backward-compatible defaults. Implementation and type definitions are consistent, and tests/docs align with behavior. Only minor messaging refinements are suggested to make warnings and runtime errors more actionable. Consider a small test to assert the invalid-option warning, but otherwise this looks solid.
Additional notes (4)
-
Readability |
packages/commonjs/src/proxies.js:105-109
Good addition overall; the stub path is minimal and avoidsnode:module. Consider making the thrown error more actionable by suggesting how to resolve it (e.g., configure'create-require'for Node targets or mark the specifier as external). This helps users who hit the error in edge runtimes understand next steps. -
Maintainability |
packages/commonjs/src/index.js:218-226
The validation is good, but the warning could be clearer by listing the valid values and linking to the README section. This reduces user confusion when they typo the option. -
Maintainability |
packages/commonjs/src/index.js:47-49
The default selection and allowed strategy literals are duplicated in multiple places. This risks drift between normalization and validation. Centralizing the allowed values and default into constants improves maintainability and keeps the warning message in-sync with behavior. -
Readability |
packages/commonjs/types/index.d.ts:229-239
Nice JSDoc. Adding a direct link to the README section will improve discoverability for users reading the type definitions in editors.
Summary of changes
- Added a new
externalBuiltinsRequireoption to@rollup/plugin-commonjswith supported values'create-require'(default) and'stub'. - Threaded the option through
src/index.jsand added runtime validation that warns on invalid values and falls back to'create-require'. - Extended
getExternalBuiltinRequireProxy()insrc/proxies.jsto accept a strategy and emit either the existingcreateRequireproxy or a minimal throwing stub that avoids importingnode:module. - Documented the new option in
README.mdwith an example. - Updated TypeScript definitions to include the new option and its default.
- Added a test fixture to verify the
'stub'strategy does not injectnode:moduleand kept existing snapshots unchanged.
| // 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fixture may be entirely tree-shaken (the require('node:crypto') call is inside an unused function), which means the stub proxy may never be emitted. That reduces confidence that the 'stub' code path is actually exercised. Consider forcing the function to be used so we can snapshot the emitted stub proxy and assert the absence of node:module.
Suggestion
Call the function so Rollup cannot tree-shake it away, ensuring the stub proxy is generated and appears in the snapshot:
// 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');
}
unused();
module.exports = 1;Reply with "@CharlieHelps yes please" if you'd like me to update the fixture accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CharlieHelps yes please
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Expand this to see my work.
- Verified fixture config at
packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped-stub/_config.jssetspluginOptions.externalBuiltinsRequire: 'stub'andpluginOptions.strictRequires: true, withcontext.__filenameand description noting stubbednode:builtins. - Confirmed test harness in
packages/commonjs/test/function.jsandpackages/commonjs/test/helpers/util.js: fixtures run viacommonjs(config.pluginOptions)andrunCodeSplitTest()captures thrown errors ({ error, exports }) for assertions with snapshots. - Traced wrapped external flow and
EXTERNAL_SUFFIXacrosspackages/commonjs/src/index.js,src/generate-imports.js,src/resolve-require-sources.js,src/resolve-id.js, andsrc/helpers.js; wrapped externals import__requirefrom<id>?commonjs-external, with try/catch behavior controlled bygetIgnoreTryCatchRequireStatementMode()gated byisWrappedId(..., EXTERNAL_SUFFIX). - Verified stub mode in
packages/commonjs/src/proxies.jsemitsexport function __require() { throw new Error(...) }andpackages/commonjs/types/index.d.tsdocuments the stub proxy behavior. - Updated
packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped-stub/main.jsto callunused()insidetry { ... } catch {}sorequire('node:crypto')exercises the stubbed proxy per the review comment. - Ran AVA tests (suite passed, 0 failures; snapshots updated) and
tsc types/index.d.ts test/types.ts --noEmitcompleted with no errors. - Re-requested review from @shellscape and @CharlieHelps on PR #1931 after applying the fixture update.
- Validated the saved snapshot for
module-side-effects-external-node-builtin-wrapped-stubinpackages/commonjs/test/snapshots/function.js.mdshows the inlined__require()stub message and thetry { unused(); } catch {}call as expected. - Confirmed the emitted entry keeps
/*@__PURE__*/getDefaultExportFromCjs(...)andmodule.exports = main;consistent with surrounding fixtures, ensuring only the external proxy stub changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. I updated the fixture to call the function so the external builtin stub proxy is definitely emitted and shows up in the snapshot, and wrapped the call in a tiny try/catch to swallow the expected stub throw at runtime.
- File:
packages/commonjs/test/fixtures/function/module-side-effects-external-node-builtin-wrapped-stub/main.js - Commits: 7dd4935 and 736caa6
- Calls
unused()to prevent tree-shaking - Catches the expected error from the
'stub'proxy so the test can execute - Snapshots updated to assert the stub is present and there’s no
node:moduleimport
- Calls
Verification
# Update snapshots and run tests (packages/commonjs): 283 tests passed
$ pnpm -C packages/commonjs test -- --update-snapshots
# Type tests: No errors
$ pnpm -C packages/commonjs test:ts
# Lint (repo): 0 errors (8 warnings unchanged)
$ pnpm lint:jsRe-requested review from @CharlieHelps (and @shellscape since you requested the change).
|
@CharlieHelps use the PR template. this is in your instructions in the repo |
|
Updated the PR description to use the repository template.
If you want any boxes toggled or additional details in the Description section, I can adjust. Separately, I’m preparing the small fixture tweak acknowledged above and will push it after local checks. |
…s emitted; update snapshots
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall the implementation is sound and backwards-compatible. The primary improvements are maintainability and usability: centralize the allowed strategy literals and default to avoid drift, improve the invalid-option warning with valid values and a README link, and make the stub error message more actionable. Optionally, consider adding a small test to assert the invalid-option warning is emitted. No functional bugs were found in the modified code.
Additional notes (3)
-
Readability |
packages/commonjs/src/index.js:218-226
The validation is good, but the warning message could be more actionable by listing valid values and linking to the README section. Also, reusing shared constants helps avoid drift if supported strategies change. -
Readability |
packages/commonjs/src/proxies.js:105-109
The stubbed proxy throws a generic error. Making the message more actionable (suggesting how to resolve) will help users encountering this in edge runtimes. -
Maintainability |
packages/commonjs/types/index.d.ts:229-239
Nice JSDoc. Adding a direct link to the README section will improve discoverability for users reading types in editors.
Summary of changes
- Introduced a new
externalBuiltinsRequireoption to@rollup/plugin-commonjswith supported values'create-require'(default) and'stub'. - Threaded this option through
src/index.jsand validated it duringbuildStart, warning and falling back to'create-require'on invalid values. - Extended
getExternalBuiltinRequireProxy()insrc/proxies.jsto accept a strategy and emit either the existingcreateRequire-based proxy or a minimal throwing stub that avoids importingnode:module. - Updated documentation in
README.mdwith a dedicated section and example configuration. - Updated TypeScript definitions in
types/index.d.tsto include the new option and its default. - Added a new test fixture to exercise the
'stub'path and updated snapshots to assert the emitted stub proxy and absence ofnode:moduleimport.
| const rawExternalBuiltinsRequire = options.externalBuiltinsRequire; | ||
| const externalBuiltinsRequireStrategy = | ||
| rawExternalBuiltinsRequire === 'stub' ? 'stub' : 'create-require'; |
There was a problem hiding this comment.
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.
|
@sapphi-red @danielroe please take a look over this one |
| 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Rollup Plugin Name:
commonjsThis PR contains:
Are tests included?
Breaking Changes?
List any relevant issue numbers:
resolves #1924
Description
Adds a new
externalBuiltinsRequireoption to@rollup/plugin-commonjsthat controls howrequire('node:*')from wrapped CJS externals is handled.'create-require'(default): current behavior; lazily resolvesrequireviamodule.createRequire(import.meta.url)so Node built-ins continue to work as today.'stub': emits a minimal proxy that exports a throwing__require()and does not importnode:module, letting edge runtimes avoid bundlingnode:modulewhen those paths are dead.The option is threaded through
src/index.jsintogetExternalBuiltinRequireProxy()with validation (invalid values warn and fall back to the default). Implementation lives insrc/proxies.js. Docs updated inREADME.md. Tests include a new fixture to cover the'stub'path; existing snapshots remain unchanged because the default stays'create-require'.Context: follows up on #1930 and addresses the ask in #1924 to avoid unconditionally injecting
createRequirefor external built-ins in wrapped modules while keeping Node defaults unchanged for now.