Skip to content

fix: tree-shake CommonJS exports through const NAME = require(LITERAL) bindings#21003

Merged
alexander-akait merged 3 commits into
mainfrom
claude/verify-issue-14054-ebbQ6
May 21, 2026
Merged

fix: tree-shake CommonJS exports through const NAME = require(LITERAL) bindings#21003
alexander-akait merged 3 commits into
mainfrom
claude/verify-issue-14054-ebbQ6

Conversation

@alexander-akait
Copy link
Copy Markdown
Member

Closes #14054.

Previously webpack treated every export of a CommonJS module as referenced
when the module was imported via a named binding (const utils = require("./utils")) — the bare CommonJsRequireDependency reports
EXPORTS_OBJECT_REFERENCED because it can't see how utils is later
used. Only the destructuring form (const { foo } = require("./utils"))
was actually tree-shakeable.

CommonJsImportsParserPlugin now tags the declared binding and forwards
later static member accesses on it — utils.foo, utils.foo(),
utils["foo"] — to the same CommonJsRequireDependency as referenced
exports. The moment utils is read in any other context (passed by
value, spread, destructured later, accessed with a dynamic key, called
directly), the dependency is restored to EXPORTS_OBJECT_REFERENCED so
no usage is silently dropped.

…L)` bindings

Closes #14054.

Previously webpack treated every export of a CommonJS module as referenced
when the module was imported via a named binding (`const utils =
require("./utils")`) — the bare `CommonJsRequireDependency` reports
`EXPORTS_OBJECT_REFERENCED` because it can't see how `utils` is later
used. Only the destructuring form (`const { foo } = require("./utils")`)
was actually tree-shakeable.

CommonJsImportsParserPlugin now tags the declared binding and forwards
later static member accesses on it — `utils.foo`, `utils.foo()`,
`utils["foo"]` — to the same `CommonJsRequireDependency` as referenced
exports. The moment `utils` is read in any other context (passed by
value, spread, destructured later, accessed with a dynamic key, called
directly), the dependency is restored to `EXPORTS_OBJECT_REFERENCED` so
no usage is silently dropped.
Copilot AI review requested due to automatic review settings May 20, 2026 22:56
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 20, 2026

🦋 Changeset detected

Latest commit: 82849d9

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

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

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
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

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

Install it locally:

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

@codecov
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.60%. Comparing base (eb14a5a) to head (82849d9).
⚠️ Report is 11 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #21003      +/-   ##
==========================================
+ Coverage   90.93%   91.60%   +0.67%     
==========================================
  Files         573      573              
  Lines       58986    59277     +291     
  Branches    15898    16012     +114     
==========================================
+ Hits        53636    54301     +665     
+ Misses       5350     4976     -374     
Flag Coverage Δ
integration 89.62% <100.00%> (-0.09%) ⬇️
test262 45.39% <32.60%> (-0.02%) ⬇️
unit 37.85% <4.34%> (+1.22%) ⬆️

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

☔ View full report in Codecov by Sentry.
📢 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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves CommonJS tree-shaking when a module is imported via a named const binding (const ns = require("./mod")). It does so by tracking that binding in the parser and forwarding subsequent static member accesses on the binding to the same CommonJsRequireDependency, enabling usedExports to drop unused exports.* assignments while still bailing out to full-namespace usage when the binding is used in non-static ways.

Changes:

  • Track const NAME = require("literal") bindings and record later static member-chain reads/calls (NAME.x, NAME["x"], NAME.x()) as referenced exports on the underlying CommonJsRequireDependency.
  • Add regression tests for issue #14054 and a dedicated cjs-tree-shaking/require-member-access case to validate partial namespace usage and bailout behavior.
  • Add a changeset documenting the patch-level behavior change.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
lib/dependencies/CommonJsImportsParserPlugin.js Tags const require bindings and forwards static member-chain usage into CommonJsRequireDependency.referencedExports, with bailout to full namespace on value-usage.
test/configCases/issues/issue-14054/webpack.config.js Production config for the regression case validating minified output markers.
test/configCases/issues/issue-14054/index.js Regression assertions that unused CommonJS exports are removed when only static members are accessed.
test/configCases/issues/issue-14054/utils.js Fixture module with used/unused exported markers.
test/configCases/issues/issue-14054/utils-fullref.js Fixture module for the “full namespace read” counter-test (ensures exports are retained).
test/cases/cjs-tree-shaking/require-member-access/test.filter.js Skips this case in development mode where usedExports analysis is not active.
test/cases/cjs-tree-shaking/require-member-access/index.js Verifies used-exports tracking through a require binding and a bailout scenario via value usage.
test/cases/cjs-tree-shaking/require-member-access/module.js Exposes __webpack_exports_info__.usedExports for assertions.
test/cases/cjs-tree-shaking/require-member-access/module-rest.js Same fixture used for the bailout (namespace read) test.
.changeset/cjs-require-binding-tree-shake.md Patch changeset describing the new tree-shaking behavior for const NAME = require(...) bindings.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +5
it("should tree-shake CommonJS exports through a const binding (call form)", () => {
const m = require("./module");
expect(m.a).toBe("a");
expect(m.usedExports).toEqual(["a", "usedExports"]);
});
Comment on lines +759 to +763
if (members.length === 0) {
// `NAME(...)` — calling the require result directly; the
// whole exports object is observable.
binding.dep.referencedExports = null;
} else {
Addresses Copilot review comments on #21003:

1. The first member-access test claimed "(call form)" but only performed
   a property read. Make the fixture export a function so the test
   actually calls `m.a()`, and add matching call-style assertions to the
   property and string-literal tests so all three trigger the same
   referenced-exports set.

2. Add explicit bailout tests for non-static uses of the binding —
   spread (already covered), dynamic-key access `m[key]`, and direct
   call `m(...)` against a `module.exports = fn` style module — so the
   `EXPORTS_OBJECT_REFERENCED` fallback path is exercised.
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 20, 2026

Merging this PR will not alter performance

⚡ 3 improved benchmarks
❌ 1 regressed benchmark
✅ 140 untouched benchmarks
⏩ 72 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "many-modules-commonjs", scenario '{"name":"mode-production","mode":"production"}' 8.5 MB 6.9 MB +22.27%
Memory benchmark "many-chunks-commonjs", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 167 KB 252.4 KB -33.82%
Memory benchmark "many-modules-esm", scenario '{"name":"mode-production","mode":"production"}' 9 MB 6.8 MB +32.65%
Memory benchmark "many-chunks-commonjs", scenario '{"name":"mode-production","mode":"production"}' 9.6 MB 7.1 MB +35.78%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/verify-issue-14054-ebbQ6 (82849d9) with main (6c5f2f8)

Open in CodSpeed

Footnotes

  1. 72 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comment thread lib/dependencies/CommonJsImportsParserPlugin.js Outdated
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Copilot AI review requested due to automatic review settings May 21, 2026 13:09
@github-actions
Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging claude/verify-issue-14054-ebbQ6 into main will be
98.96%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
bin
   webpack.js98.77%100%100%98.77%91
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.js91.30%100%100%91.30%140, 142–143, 145, 147, 153–154, 161, 168, 90
examples/markdown
   webpack.config.mjs100%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-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%
   CleanPlugin.js99.15%100%100%99.15%206, 226
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.45%100%100%98.45%1572, 1868, 1875, 1883, 1905, 2801, 3226, 3888, 3917, 3970–3971, 3975, 3980, 3996–3997, 4011–4012, 4017–4018, 4495, 4521, 511, 516, 5229, 5261, 5278, 5294, 5310, 5325, 5350–5351, 5353, 5681, 5686, 5692, 5695, 5707, 5709, 5713, 5729, 5744, 5776, 5830, 5854, 5968, 730–731
   Compiler.js99.55%100%100%99.55%1116–1117, 1125
   ConcatenationScope.js98.59%100%100%98.59%189
   ConditionalInitFragment.js100%100%100%100%
   ConstPlugin.js100%100%100%100%
   ContextExclusionPlugin.js100%100%100%100%
   ContextModule.js100%100%100%100%
   ContextModuleFactory.js97.75%100%100%97.75%258, 393, 418, 443, 447, 458
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js98.92%100%100%98.92%158–159, 175, 194, 268
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.20%100%100%98.20%379, 425
   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.97%100%100%98.97%425–429, 577
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.50%100%100%99.50%182, 2252–2253, 2256, 2267, 2278, 2289, 278, 3694, 3709, 3733
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.74%100%100%98.74%399, 401, 405
   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%
   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.50%100%100%98.50%1305, 1310, 1371, 1385, 1447, 1456
   ModuleFactory.js100%100%100%100%
   ModuleFilenameHelpers.js98.85%100%100%98.85%106, 108
   ModuleGraph.js99.73%100%100%99.73%1004
   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.69%100%100%99.69%645
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js97.83%100%100%97.83%1072, 1106, 1122, 1209, 1834, 1839–1849, 794, 797, 814, 831
   NormalModuleFactory.js99.47%100%100%99.47%1083, 1392, 486, 498
   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.js98.85%100%100%98.85%519–520, 525, 527, 591
   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.59%100%100%98.59%218, 222, 224, 398, 409, 811
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js98.86%100%100%98.86%136–137
   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%325
   cli.js98.46%100%100%98.46%10, 119, 471, 503, 545, 815
   index.js99.72%100%100%99.72%165
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js96.33%100%100%96.33%10, 198, 220, 222
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModulesPlugin.js97.32%100%100%97.32%283, 307, 310, 36, 362, 41
   AssetParser.js100%100%100%100%
   AssetSourceGenerator.js100%100%100%100%
   AssetSourceParser.js100%100%100%100%
   RawDataUrlModule.js100%100%100%100%
lib/async-modules
   AsyncModuleHelpers.js100%100%100%100%
   AwaitDependenciesInitFragment.js100%100%100%100%
   InferAsyncModulesPlugin.js100%100%100%100%
lib/cache
   AddBuildDependenciesPlugin.js100%100%100%100%
   AddManagedPathsPlugin.js100%100%100%100%
   IdleFileCachePlugin.js97.92%100%100%97.92%71, 83, 91
   MemoryCachePlugin.js95.83%100%100%95.83%33
   MemoryWithGcCachePlugin.js93.15%100%100%93.15%106, 113–114, 122, 89
   PackFileCacheStrategy.js96.40%100%100%96.40%1250, 1350, 1354, 1416, 628, 647, 657–659, 661, 677–678, 683, 686, 688, 693, 698, 722, 728, 762, 768, 774, 779, 790, 799, 804–805, 807, 824, 830–831, 833
   ResolverCachePlugin.js100%100%100%100%
   getLazyHashedEtag.js100%100%100%100%
   mergeEtags.js100%100%100%100%
lib/config
   browserslistTargetHandler.js100%100%100%100%
   defaults.js99.29%100%100%99.29%1411–1413, 1421, 271, 274, 279, 283
   normalization.js99%100%100%99%191–192, 258, 273
   target.js100%100%100%100%
lib/container
   ContainerEntryDependency.js100%100%100%100%
   ContainerEntryModule.js100%100%100%100%
   ContainerEntryModuleFactory.js100%100%100%100%
   ContainerExposedDependency.js100%100%100%100%
   ContainerPlugin.js100%100%100%100%
   ContainerReferencePlugin.js100%100%100%100%
   FallbackDependency.js100%100%100%100%
   FallbackItemDependency.js100%100%100%100%
   

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comment on lines +55 to +58
* @typedef {object} RequireBindingData
* @property {RawReferencedExports} referencedExports mutable list shared with the dependency; pushed to as `NAME.x.y` accesses are walked
* @property {InstanceType<typeof import("./CommonJsRequireDependency")> | null} dep dependency for the `require()` call (assigned during walk)
*/
@alexander-akait alexander-akait merged commit 72ef0fb into main May 21, 2026
61 of 62 checks passed
@alexander-akait alexander-akait deleted the claude/verify-issue-14054-ebbQ6 branch May 21, 2026 14:13
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.

commonjs tree shaking not work in webpack5

2 participants