Skip to content

fix(concatenation): keep wrapped module names and side effects intact - #21593

Merged
alexander-akait merged 3 commits into
mainfrom
fix/concatenate-wrapped-module-names
Aug 3, 2026
Merged

alexander-akait merged 3 commits into
mainfrom
fix/concatenate-wrapped-module-names

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Aug 3, 2026 •

Copy link
Copy Markdown
Member

Summary

Three correctness bugs in the wrapped-CommonJS concatenation added in #21519, found by compiling the same fixtures under concatenateModules: false, { commonjs: false } and { commonjs: true } and diffing the runtime behaviour. Refs #21519.

A wrapped body is emitted verbatim and never scope-analysed, so its identifiers never entered allUsedNames: a local declaration could shadow the wrapper accessor it calls (var dep_namespaceFn = … next to require("./dep.js") throws dep_namespaceFn is not a function, and the function form fails silently), and a free global could be captured by another member's top-level binding. ConcatenatedModule now claims those identifiers with a cached lexical scan, in place of the scope analysis that is deliberately skipped. Separately, an unused export of a wrapped module collapsed to undefined, dropping the accessor call and with it the module's side effects — a three-way require cycle evaluated [a, b] instead of [a, b, c]; it now renders as (x_namespaceFn(), undefined).

The code asserted an invariant that was never implemented ("the reserved __webpack_ prefix is what keeps a local in that body from shadowing the accessor" — findNewName produces dep_namespaceFn); those comments are corrected.

What kind of change does this PR introduce?

fix

Did you add tests for your changes?

Yes — four test/configCases/concatenate-modules/ cases: commonjs-wrapped-name-collision, commonjs-wrapped-global-capture, commonjs-unused-export-side-effects, and commonjs-require-shapes (template-literal, destructured, this-binding, computed-request, alias and both new require forms). Each was verified to fail with the lib/ change reverted.

Does this PR introduce a breaking change?

No — it only corrects output for modules wrapped by the opt-in optimization.concatenateModules.commonjs path.

If relevant, what needs to be documented once your changes are merged or what have you already documented?

n/a

Use of AI

Yes. Claude Code was used to build the differential harness that surfaced the three divergences, to write the fix and the test cases, and to verify each test fails without the fix. Every finding was reproduced in a real build and reviewed by me before committing.


Generated by Claude Code


Note

Medium Risk
Changes affect production bundle codegen on the opt-in CommonJS concatenation path in ConcatenatedModule.js; incorrect identifier reservation or unused-export expressions could alter runtime behavior, though the added config cases target the reported failure modes.

Overview
Fixes three correctness bugs in opt-in optimization.concatenateModules.commonjs wrapped-module output: verbatim CJS bodies were never scope-analyzed, so generated names could collide with body identifiers and unused exports could skip lazy accessor evaluation.

Identifier reservation: Adds findIdentifiers and, during the RESERVE phase, registers every identifier in a wrapped body in allUsedNames so findNewName does not pick names like dep_namespaceFn that locals or hoisted declarations inside the body would shadow.

Unused wrapped exports: Adds getUnusedExportExpr so unused exports on wrapped modules render as (accessor(), undefined) instead of bare undefined, preserving evaluation and side effects when nothing reads the export.

Tests: Four concatenate-modules config cases cover name collision, global capture, unused-export side effects, and require() shapes; smaller harness tweaks clear shared globals on Bun and skip a flaky library case there.

Reviewed by Cursor Bugbot for commit ef2a86b. Bugbot is set up for automated code reviews on this repo. Configure here.

A wrapped CommonJS body is emitted verbatim and never scope-analysed, so its
identifiers were unknown when the concatenation allocated its own top-level
names. A local declaration could shadow the wrapper accessor it calls, and a
free global could be captured by another member's top-level binding. Claim the
body's identifiers instead, in place of the scope analysis that is skipped.

An unused export of a wrapped module also collapsed to `undefined`, dropping
the accessor call with it — and with it the module's side effects, which a
hoisted body would still have run at its own slot.
@changeset-bot

changeset-bot Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ef2a86b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

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

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actions Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

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

Install it locally:

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

@codecov

codecov Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 94.08%. Comparing base (f2462c2) to head (ef2a86b).

Files with missing lines Patch % Lines
lib/optimize/ConcatenatedModule.js 95.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #21593      +/-   ##
==========================================
- Coverage   94.09%   94.08%   -0.01%     
==========================================
  Files         627      627              
  Lines       77209    77229      +20     
  Branches    22656    22660       +4     
==========================================
+ Hits        72646    72663      +17     
- Misses       4563     4566       +3     
Flag Coverage Δ
css-parsing 24.92% <20.00%> (-0.01%) ⬇️
html5lib 26.25% <20.00%> (-0.01%) ⬇️
integration 89.67% <95.00%> (-0.01%) ⬇️
test262 42.97% <20.00%> (-0.01%) ⬇️
unit 48.79% <20.00%> (-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.

Coverage showed the WeakMap around the identifier scan never hit: each
_analyseModule builds a fresh Source, so the key is always new. It cost an
allocation and a lookup per wrapped body for nothing.

The unused-export change at the second getFinalBinding site is reverted: no
build reaches it, because an unused ESM re-export of a wrapped module is
dropped before code generation. The require()-shaped site keeps the fix and
is the one a build actually hits.
@codspeed

codspeed Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 40.93%

⚠️ 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
✅ 219 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
⚡ Memory benchmark "many-modules-interop-runtime", scenario '{"name":"mode-production","mode":"production"}', measure 'exec' 24.6 KB 17.5 KB +40.93%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fix/concatenate-wrapped-module-names (ef2a86b) with main (f2462c2)

Open in CodSpeed

ConfigTestCases and ConfigCacheTestCases run the same cases, and a worker that
shares one global between test files (Bun's --workerThreads) carries their
leftovers into the second run. The harness only clears globals whose name
contains "webpack", so a plain counter survives: the deferred-evaluation case
then saw a non-zero count and the inline-exports case saw its side effects
twice. Both now clear their own counter.

The amd-runtimeChunk library variants are a timing difference, not a leftover:
the `promise` external is still settling when the harness deletes the chunk
loading globals, which only survives because Node gives each test file a fresh
global. Filtered out under Bun until the harness can await those externals.
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging fix/concatenate-wrapped-module-names 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.js99.12%100%100%99.12%300
   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%480, 527
   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.67%100%100%98.67%1216, 1219, 515–519, 521, 667
   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.71%100%100%99.71%687
   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%

@alexander-akait
alexander-akait merged commit c593dea into main Aug 3, 2026
63 checks passed
@alexander-akait
alexander-akait deleted the fix/concatenate-wrapped-module-names branch August 3, 2026 17:53
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