Skip to content

feat: add externalsPresets.nodeModules preset - #21569

Merged
alexander-akait merged 8 commits into
mainfrom
feat/externals-node-modules-preset
Aug 1, 2026
Merged

feat: add externalsPresets.nodeModules preset#21569
alexander-akait merged 8 commits into
mainfrom
feat/externals-node-modules-preset

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Jul 31, 2026

Copy link
Copy Markdown
Member

Summary

Adds an opt-in externalsPresets.nodeModules preset that externalizes installed packages — any request resolving into a node_modules directory — loading them via require()/import at 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-externals plugin, so this class of build no longer needs a third-party plugin. Compared to the plugin it:

  • resolves each request, so pnpm / monorepo / symlinked node_modules and resolve.alias are handled correctly (the plugin scans a single node_modules dir);
  • auto-selects node-commonjs vs module-import from output.module, and keeps CommonJS-category deps as require() even in module output (equivalent to the plugin's importType);
  • keeps a package's CSS / assets imported from JS bundled for webpack to process (the plugin's most common allowlist: [/\.css$/] use — here it's automatic);
  • accepts nodeModules: { allowlist: [...] } — exact string, RegExp, or (request) => boolean — to keep specific requests bundled while externalizing the rest, matching the plugin's allowlist.

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_modules requests staying bundled, a package asset (.svg) staying bundled rather than externalized, and the allowlist in all three forms (RegExp, string, function). Defaults.unittest.js and the Cli.basictest snapshot are updated for the new option.

Does this PR introduce a breaking change?

No. The preset defaults to false and 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.nodeModules option (including its allowlist) should be added to the externals documentation, ideally noting it as the built-in equivalent of webpack-node-externals.

Use of AI

AI (Claude) was used to implement the preset, compare it against webpack-node-externals for 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.nodeModules preset (defaults to false) as a built-in alternative to webpack-node-externals: when enabled, dependencies that resolve to runtime-loadable files under node_modules are left external and loaded via require() / import at runtime instead of being bundled.

The preset wires an ExternalsPlugin handler 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.alias that redirects a request to a different package keeps that request bundled. External type follows output.module (module-import vs node-commonjs), with CommonJS-category deps still using node-commonjs. Optional allowlist (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.

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-bot

changeset-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d2add04

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack Minor

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

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

This PR is packaged and the instant preview is available (5d3d043).

Install it locally:

  • npm
npm i -D webpack@https://pkg.pr.new/webpack@5d3d043
  • yarn
yarn add -D webpack@https://pkg.pr.new/webpack@5d3d043
  • pnpm
pnpm add -D webpack@https://pkg.pr.new/webpack@5d3d043

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.95%. Comparing base (1fdea24) to head (d2add04).
⚠️ Report is 2 commits behind head on main.

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     
Flag Coverage Δ
css-parsing 25.03% <4.87%> (-0.03%) ⬇️
html5lib 26.44% <4.87%> (-0.03%) ⬇️
integration 89.33% <100.00%> (+<0.01%) ⬆️
test262 43.47% <4.87%> (-0.02%) ⬇️
unit 48.27% <4.87%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread lib/WebpackOptionsApply.js Outdated
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.
@codspeed-hq

codspeed-hq Bot commented Jul 31, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 5.87%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 218 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

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)

Open in CodSpeed

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.
Comment thread lib/WebpackOptionsApply.js
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.
Comment thread .changeset/015-externals-node-modules-preset.md Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread lib/WebpackOptionsApply.js
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.
@github-actions

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging feat/externals-node-modules-preset into main will be
99.31%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
bin
   webpack.js98.82%100%100%98.82%103
examples
   build-common.js100%100%100%100%
   buildAll.js100%100%100%100%
   examples.js100%100%100%100%
   template-common.js98.21%100%100%98.21%72
examples/custom-javascript-parser
   test.filter.js100%100%100%100%
examples/custom-javascript-parser/internals
   acorn-parse.js100%100%100%100%
   meriyah-parse.js100%100%100%100%
   oxc-parse.js100%100%100%100%
examples/markdown
   webpack.config.mjs100%100%100%100%
examples/module-federation
   test.filter.js100%100%100%100%
examples/reexport-components
   test.filter.js100%100%100%100%
examples/typescript
   test.filter.js100%100%100%100%
examples/typescript-non-erasable
   test.filter.js50%100%100%50%5
examples/virtual-modules
   test.filter.js100%100%100%100%
examples/wasm-bindgen-esm
   test.filter.js100%100%100%100%
examples/wasm-complex
   test.filter.js100%100%100%100%
examples/wasm-emscripten
   test.filter.js100%100%100%100%
examples/wasm-simple
   test.filter.js100%100%100%100%
examples/wasm-simple-source-phase
   test.filter.js100%100%100%100%
lib
   APIPlugin.js100%100%100%100%
   AsyncDependenciesBlock.js100%100%100%100%
   AutomaticPrefetchPlugin.js100%100%100%100%
   BannerPlugin.js100%100%100%100%
   Cache.js98.21%100%100%98.21%101
   CacheFacade.js100%100%100%100%
   Chunk.js99.72%100%100%99.72%39
   ChunkGraph.js100%100%100%100%
   ChunkGroup.js100%100%100%100%
   ChunkTemplate.js100%100%100%100%
   CircularModulesPlugin.js98.81%100%100%98.81%136
   CleanPlugin.js99.13%100%100%99.13%214, 234
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.42%100%100%98.42%1648, 1967, 1974, 1982, 2004, 2007, 2946, 3425–3426, 3458, 4162, 4192, 4245–4246, 4250, 4255, 4271–4272, 4286–4287, 4292–4293, 4773, 4799, 527, 532, 5607, 5639, 5656, 5672, 5688, 5703, 5728–5729, 5731, 6061, 6066, 6072, 6075, 6082, 6094, 6096, 6100, 6116, 6131, 6163, 6217, 6241, 6356, 778–779
   Compiler.js99.56%100%100%99.56%1159–1160, 1168
   ConcatenationScope.js98.65%100%100%98.65%195
   ConditionalInitFragment.js100%100%100%100%
   ConstPlugin.js100%100%100%100%
   ContextExclusionPlugin.js100%100%100%100%
   ContextModule.js99.88%100%100%99.88%1461
   ContextModuleFactory.js97.20%100%100%97.20%266, 435, 456, 461, 501, 512, 514, 518, 527–528
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js99.08%100%100%99.08%1080, 176–177, 193, 212, 286
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.51%100%100%98.51%479, 525
   DependencyTemplate.js100%100%100%100%
   DependencyTemplates.js100%100%100%100%
   DotenvPlugin.js98.41%100%100%98.41%378, 391–392
   DynamicEntryPlugin.js100%100%100%100%
   EntryOptionPlugin.js100%100%100%100%
   EntryPlugin.js100%100%100%100%
   Entrypoint.js100%100%100%100%
   EnvironmentPlugin.js97.14%100%100%97.14%49
   ErrorHelpers.js100%100%100%100%
   EvalDevToolModulePlugin.js100%100%100%100%
   EvalSourceMapDevToolPlugin.js100%100%100%100%
   ExportsInfo.js100%100%100%100%
   ExportsInfoApiPlugin.js100%100%100%100%
   ExternalModule.js98.65%100%100%98.65%1202, 1205, 514–518, 520, 666
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.16%100%100%99.16%1267, 1269–1274, 1281, 1284, 182, 2502–2503, 2506, 2517, 2528, 2539, 280, 3976, 3991, 4015
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.21%100%100%98.21%448, 457, 460, 464, 476
   FlagDependencyUsagePlugin.js100%100%100%100%
   FlagEntryExportAsUsedPlugin.js100%100%100%100%
   Generator.js100%100%100%100%
   HotModuleReplacementPlugin.js100%100%100%100%
   HotUpdateChunk.js100%100%100%100%
   IgnorePlugin.js100%100%100%100%
   IgnoreWarningsPlugin.js100%100%100%100%
   InitFragment.js100%100%100%100%
   JavascriptMetaInfoPlugin.js100%100%100%100%
   LazyBarrel.js100%100%100%100%
   LibraryTemplatePlugin.js100%100%100%100%
   LoaderOptionsPlugin.js100%100%100%100%
   LoaderTargetPlugin.js100%100%100%100%
   MainTemplate.js100%100%100%100%
   ManifestPlugin.js100%100%100%100%
   Module.js98.51%100%100%98.51%1303, 1308, 1368, 1382, 1444, 1453
   ModuleFactory.js100%100%100%100%
   ModuleFilenameHelpers.js98.90%100%100%98.90%111, 113
   ModuleGraph.js99.78%100%100%99.78%1170
   ModuleGraphConnection.js100%100%100%100%
   ModuleInfoHeaderPlugin.js100%100%100%100%
   ModuleNotFoundError.js100%100%100%100%
   ModuleProfile.js100%100%100%100%
   ModuleSourceTypeConstants.js100%100%100%100%
   ModuleTemplate.js100%100%100%100%
   ModuleTypeConstants.js100%100%100%100%
   MultiCompiler.js99.70%100%100%99.70%663
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js97.98%100%100%97.98%1020, 1023, 1040, 1057, 1305, 1339, 1355, 1803, 2100, 2105–2115, 34
   NormalModuleFactory.js98.72%100%100%98.72%1123, 1391, 1402, 1412, 1463–1465, 1472, 526, 538
   NormalModuleReplacementPlugin.js100%100%100%100%
   NullFactory.js100%100%100%100%
   OptimizationStages.js100%100%100%100%
   OptionsApply.js100%100%100%100%
   Parser.js100%100%100%100%
   PlatformPlugin.js100%100%100%100%
   PrefetchPlugin.js100%100%100%100%
   ProgressPlugin.js99.80%100%100%99.80%691
   ProvidePlugin.js100%100%100%100%
   RawModule.js100%100%100%100%
   RecordIdsPlugin.js100%100%100%100%
   RequestShortener.js100%100%100%100%
   ResolverFactory.js100%100%100%100%
   RuntimeGlobals.js100%100%100%100%
   RuntimeModule.js100%100%100%100%
   RuntimePlugin.js100%100%100%100%
   RuntimeTemplate.js100%100%100%100%
   SelfModuleFactory.js100%100%100%100%
   SingleEntryPlugin.js100%100%100%100%
   SourceMapDevToolModuleOptionsPlugin.js100%100%100%100%
   SourceMapDevToolPlugin.js98.63%100%100%98.63%220, 224, 226, 420, 431, 890
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js99.43%100%100%99.43%308–309
   UseStrictPlugin.js100%100%100%100%
   WarnCaseSensitiveModulesPlugin.js100%100%100%100%
   WarnDeprecatedOptionPlugin.js100%100%100%100%
   WarnNoModeSetPlugin.js100%100%100%100%
   WatchIgnorePlugin.js100%100%100%100%
   Watching.js100%100%100%100%
   WebpackError.js100%100%100%100%
   WebpackIsIncludedPlugin.js100%100%100%100%
   WebpackOptionsApply.js100%100%100%100%
   WebpackOptionsDefaulter.js100%100%100%100%
   buildChunkGraph.js99.87%100%100%99.87%371
   cli.js98.63%100%100%98.63%10, 119, 549, 581, 631, 905
   index.js99.73%100%100%99.73%184
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js97.10%100%100%97.10%10, 263, 285, 287
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModule.js100%100%100%100%
   AssetModulesPlugin.js97.95%100%100%97.95%295, 319, 322, 42, 452, 47
   AssetParser.js100%100%100%100%
   AssetSourceGenerator.js100%100%100%100%
   AssetSourceParser.js100%100%100%100%
   RawDataUrlModule.js100%100%100%100%
   WebManifestGenerator.js100%100%100%100%
   WebManifestParser.js100%100%100%100%
lib/async-modules
   AsyncModuleHelpers.js100%100%100%100%
   AwaitDependenciesInitFragment.js100%100%100%100%
   InferAsyncModulesPlugin.js100%100%100%100%
   isGeneratorLowered.js100%100%100%100%
lib/bun
   BunTargetPlugin.js100%100%100%100%
lib/cache
   AddBuildDependenciesPlugin.js100%100%100%100%
   AddManagedPathsPlugin.js100%100%100%100%
   IdleFileCachePlugin.js97.92%100%100%97.92%75, 87, 95
   MemoryCachePlugin.js95.83%100%100%95.83%33
   MemoryWithGcCachePlugin.js93.15%100%100%93.15%107, 114–115, 123, 90
   PackFileCacheStrategy.js96.52%100%100%96.52%1310, 1410, 1414, 1476, 1712, 1796, 1819, 1851, 675, 694, 704–706, 708, 724–725, 730, 733, 735, 740, 745, 770, 776, 810, 816, 822, 827, 838, 847, 852–853, 855, 872, 878–879, 881
   ResolverCachePlugin.js100%100%100%100%
   getLazyHashedEtag.js100%100%1

@alexander-akait
alexander-akait merged commit 5d3d043 into main Aug 1, 2026
63 checks passed
@alexander-akait
alexander-akait deleted the feat/externals-node-modules-preset branch August 1, 2026 11:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Sponsor
SponsoredKunjungi sekarang
Promo