feat: add externalsPresets.nodeModules preset - #21569
Conversation
Externalize installed packages (requests resolving into a node_modules directory) instead of bundling them, loading them via require()/import at runtime. Opt-in only; relative/absolute and CSS/url requests are never treated as package externals. Useful for server-side rendering builds.
🦋 Changeset detectedLatest commit: d2add04 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
This PR is packaged and the instant preview is available (5d3d043). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@5d3d043
yarn add -D webpack@https://pkg.pr.new/webpack@5d3d043
pnpm add -D webpack@https://pkg.pr.new/webpack@5d3d043 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21569 +/- ##
==========================================
+ Coverage 93.93% 93.95% +0.01%
==========================================
Files 623 623
Lines 75624 75711 +87
Branches 22015 22055 +40
==========================================
+ Hits 71037 71132 +95
+ Misses 4587 4579 -8
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Only externalize a request that resolves to a runtime-loadable JS/JSON file, so a package's stylesheet or asset imported from JS stays bundled for webpack to process instead of being require()'d/import-ed at runtime. Also refresh the ecmaVersion browserslist inline snapshots for the new externalsPresets.nodeModules key.
Merging this PR will degrade performance by 5.87%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Memory | benchmark "asset-modules-source", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
645.2 KB | 1,369.7 KB | -52.89% |
| ⚡ | Memory | benchmark "asset-modules-resource", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
1,237.2 KB | 657.7 KB | +88.1% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing feat/externals-node-modules-preset (d2add04) with main (2d18aa6)
Extend externalsPresets.nodeModules to accept `true | { allowlist }`,
where allowlist items are an exact request string, a RegExp, or a
(request) => boolean filter. Matching requests stay bundled while the
rest are externalized, matching webpack-node-externals' allowlist so the
preset is a drop-in built-in replacement.
…xternals Point users at the built-in preset as the replacement for the webpack-node-externals plugin, and add the ordering prefix the changeset naming convention requires.
A `#` specifier only resolves against the importing package's own `imports` field, so it can never be emitted as an external request. A bundled package's `#internal` import resolved into node_modules and was externalized, making node throw MODULE_NOT_FOUND at runtime.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c5dacb6. Configure here.
The external keeps the original request, so a `resolve.alias` remapping a bare request to a different package was externalized under its original name and loaded the unaliased package at runtime. Externalize only when the request resolves into the package it names.

Summary
Adds an opt-in
externalsPresets.nodeModulespreset that externalizes installed packages — any request resolving into anode_modulesdirectory — loading them viarequire()/importat runtime instead of bundling them (handy for server-side rendering builds where dependencies stay on disk).This is a built-in, out-of-the-box replacement for the commonly-used
webpack-node-externalsplugin, so this class of build no longer needs a third-party plugin. Compared to the plugin it:node_modulesandresolve.aliasare handled correctly (the plugin scans a singlenode_modulesdir);node-commonjsvsmodule-importfromoutput.module, and keeps CommonJS-category deps asrequire()even in module output (equivalent to the plugin'simportType);allowlist: [/\.css$/]use — here it's automatic);nodeModules: { allowlist: [...] }— exact string,RegExp, or(request) => boolean— to keep specific requests bundled while externalizing the rest, matching the plugin'sallowlist.Split out from #21444 as the first of several focused PRs.
What kind of change does this PR introduce?
feat
Did you add tests for your changes?
Yes —
test/configCases/externals/node-modules-preset/covers CommonJS (node-commonjs) and module (module-import) output, relative/alias-resolved-outside-node_modulesrequests staying bundled, a package asset (.svg) staying bundled rather than externalized, and theallowlistin all three forms (RegExp, string, function).Defaults.unittest.jsand theCli.basictestsnapshot are updated for the new option.Does this PR introduce a breaking change?
No. The preset defaults to
falseand is never auto-enabled, so existing builds are unaffected.If relevant, what needs to be documented once your changes are merged or what have you already documented?
The new
externalsPresets.nodeModulesoption (including itsallowlist) should be added to the externals documentation, ideally noting it as the built-in equivalent ofwebpack-node-externals.Use of AI
AI (Claude) was used to implement the preset, compare it against
webpack-node-externalsfor parity, write the integration tests, and regenerate the types/validators; all changes were reviewed and verified locally (yarn tsc, targeted config cases, snapshot suites).Note
Medium Risk
Touches core externals resolution for server/SSR builds; behavior is opt-in but misconfiguration could omit deps from the bundle or break runtime requires.
Overview
Adds an opt-in
externalsPresets.nodeModulespreset (defaults tofalse) as a built-in alternative towebpack-node-externals: when enabled, dependencies that resolve to runtime-loadable files undernode_modulesare left external and loaded viarequire()/importat runtime instead of being bundled.The preset wires an
ExternalsPluginhandler that resolves each request before externalizing, so pnpm/monorepo layouts and symlinked installs behave correctly. It skips relative/absolute/#imports, CSS/url dependency types, and non-JS resolutions (e.g. package assets stay bundled).resolve.aliasthat redirects a request to a different package keeps that request bundled. External type followsoutput.module(module-importvsnode-commonjs), with CommonJS-category deps still usingnode-commonjs. Optionalallowlist(string,RegExp, or function) keeps named requests bundled.Types and a minor changeset document the new option.
Reviewed by Cursor Bugbot for commit d2add04. Bugbot is set up for automated code reviews on this repo. Configure here.