Repository navigation
fix: expose exports of all entry modules in library output - #21661
webdevelopersrinu wants to merge 1 commit into
Conversation
When an entry is an array of modules and a library output is configured, only the exports of the last entry module were flagged as used and exposed on the library. Exports of all earlier entry modules were tree-shaken away and silently dropped. The fix stays entirely in the library plugin layer: - AbstractLibraryPlugin gains an opt-in finishAllEntryModules mode that flags the exports of every entry dependency as used, and a helper that renders startup code merging the exports of all entry modules of the chunk into the final library exports object. Later entry modules win on export name conflicts, preserving the documented last-entry-wins semantics. Merging is skipped when startup is delayed by depending chunks or when entry modules are inlined. - AssignLibraryPlugin (var, assign, this, window, self, global, commonjs, commonjs2) and ExportPropertyLibraryPlugin (amd, umd, system, jsonp) opt in. - module/modern-module and commonjs-static keep their current behavior, since their export lists are generated statically from the last entry module. Fixes webpack#15936
|
|
|
📝 WalkthroughWalkthroughChangesThe library plugins now merge exports from all applicable entry modules. Later entries override duplicate export names. A configuration test covers three entry modules and validates the resulting global library exports. Multi-entry library exports
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
Comment |
|
👋 Hi there! This PR was automatically flagged and closed by our quality checks. If you believe this was a mistake, please review our contributing guidelines |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (4)
lib/library/AssignLibraryPlugin.js (1)
135-138: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe
unnamed !== "static"predicate is repeated at three sites.The constructor,
renderStartup(line 328), andruntimeRequirementseach recompute the same condition. If one site drifts, the runtime requirements and the emitted code disagree, and the bundle references__webpack_require__or__webpack_require__.rwithout those runtime modules being included.Store the decision once in the constructor and read it at the other two sites.
♻️ Suggested shape
constructor(options) { + const staticExports = options.unnamed === "static"; super({ pluginName: "AssignLibraryPlugin", type: options.type, // "static" enumerates the exports of the last entry module at // compile time, so merging all entry exports is not applicable - finishAllEntryModules: options.unnamed !== "static" + finishAllEntryModules: !staticExports }); + /** `@type` {boolean} */ + this._staticExports = staticExports;Also applies to: 436-442
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/library/AssignLibraryPlugin.js` around lines 135 - 138, Compute the `options.unnamed !== "static"` decision once in the `AssignLibraryPlugin` constructor and store it on the instance. Update `renderStartup` and `runtimeRequirements` to reuse that stored value instead of recomputing the predicate, keeping emitted code and runtime-module requirements consistent.lib/library/ExportPropertyLibraryPlugin.js (1)
122-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a regression test for the
options.exportpath combined with merging.When
options.exportis set,finishEntryModulemarks only the selected export name as used on each entry module (lines 82-89). The merge then builds__webpack_exports_merged__from the surviving exports, and the postfix narrows it to the selected property. The stack summary lists one configuration case, which uses the default export shape. Cover thelibrary.exportvariant so the interaction between per-entry export selection and merging is exercised.As per coding guidelines: "Cover every changed line and branch with tests, preferably through integration cases; do not lower patch coverage below the project target."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/library/ExportPropertyLibraryPlugin.js` around lines 122 - 129, Add a regression integration test for the library configuration where options.export selects an export while entry exports are merged. Exercise the finishEntryModule export-selection flow together with _renderEntryExportsMerge and verify the generated/runtime result contains only the selected property, covering the export postfix branch in addition to the existing default-export case.Source: Coding guidelines
lib/library/AbstractLibraryPlugin.js (2)
159-172: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winThe bailout tap recomputes the entry list for every module in the chunk.
inlineInRuntimeBailoutruns once per candidate module._getMergeableEntryModulesiterates all entry modules and callsgetModuleSourceTypeseach time, so the cost is O(modules × entries) per chunk._renderEntryExportsMergeand_addEntryExportsMergeRuntimeRequirementsrepeat the same computation.Cache the result per chunk in a
WeakMapcreated inside thethisCompilationcallback, so the per-compilation state is released with the compilation.As per coding guidelines: "Evaluate time and memory costs of changes, especially on hot paths; release per-compilation state after seal/emit when possible."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/library/AbstractLibraryPlugin.js` around lines 159 - 172, Cache each chunk’s mergeable entry modules in a WeakMap created inside the thisCompilation callback, and reuse that cached result in the inlineInRuntimeBailout tap, _renderEntryExportsMerge, and _addEntryExportsMergeRuntimeRequirements instead of calling _getMergeableEntryModules repeatedly. Preserve the existing options === false and bailout behavior while ensuring the cache remains scoped to and released with the compilation.Source: Coding guidelines
328-346: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe emitted merge helper adds three comment lines to every affected bundle.
Lines 329-331 are written into the output bundle, not into the source. They cost bytes in every non-minified library build that merges entries. Reduce to a single line, or drop them.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/library/AbstractLibraryPlugin.js` around lines 328 - 346, Reduce the emitted comments in the merge helper constructed by the `buf` array in `AbstractLibraryPlugin` from three lines to a single concise line, or remove them entirely; leave the `__webpack_exports_merged__` and `__webpack_merge_entry__` merge logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/library/AbstractLibraryPlugin.js`:
- Line 44: Regenerate types.d.ts so AbstractLibraryPluginOptions declares the
optional finishAllEntryModules boolean property, matching the documented option
in AbstractLibraryPlugin.
- Around line 348-358: Update _renderEntryExportsMerge and its renderStartup
call path to use the startup module argument when selecting entries: exclude
that module rather than blindly slicing off the last entry, then merge the
remaining entries in reverse order after the RuntimeGlobals.exports call.
Determine the namespace marker from whether any merged entry is an ES module, so
__esModule is preserved when an earlier entry provides it.
- Around line 350-353: Update the merge-entry generation around
chunkGraph.getModuleId(module) in AbstractLibraryPlugin to handle null IDs: only
emit the __webpack_merge_entry__ call when the module has a valid ID, or
otherwise skip that mergeable entry. Preserve existing output for modules with
assigned IDs.
- Around line 296-312: Update _getMergeableEntryModules so the delayed-startup
guard ignores the entrypoint’s runtime chunk: when checking entrypoint.chunks,
exclude both the current chunk and entrypoint.getRuntimeChunk(). Preserve the
existing early return for other chunks that can delay startup and keep the
JavaScript-module filtering and mergeability rules unchanged.
---
Nitpick comments:
In `@lib/library/AbstractLibraryPlugin.js`:
- Around line 159-172: Cache each chunk’s mergeable entry modules in a WeakMap
created inside the thisCompilation callback, and reuse that cached result in the
inlineInRuntimeBailout tap, _renderEntryExportsMerge, and
_addEntryExportsMergeRuntimeRequirements instead of calling
_getMergeableEntryModules repeatedly. Preserve the existing options === false
and bailout behavior while ensuring the cache remains scoped to and released
with the compilation.
- Around line 328-346: Reduce the emitted comments in the merge helper
constructed by the `buf` array in `AbstractLibraryPlugin` from three lines to a
single concise line, or remove them entirely; leave the
`__webpack_exports_merged__` and `__webpack_merge_entry__` merge logic
unchanged.
In `@lib/library/AssignLibraryPlugin.js`:
- Around line 135-138: Compute the `options.unnamed !== "static"` decision once
in the `AssignLibraryPlugin` constructor and store it on the instance. Update
`renderStartup` and `runtimeRequirements` to reuse that stored value instead of
recomputing the predicate, keeping emitted code and runtime-module requirements
consistent.
In `@lib/library/ExportPropertyLibraryPlugin.js`:
- Around line 122-129: Add a regression integration test for the library
configuration where options.export selects an export while entry exports are
merged. Exercise the finishEntryModule export-selection flow together with
_renderEntryExportsMerge and verify the generated/runtime result contains only
the selected property, covering the export postfix branch in addition to the
existing default-export case.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: b26ba73b-6c2c-46e6-87d6-108d7eda7b01
📒 Files selected for processing (7)
lib/library/AbstractLibraryPlugin.jslib/library/AssignLibraryPlugin.jslib/library/ExportPropertyLibraryPlugin.jstest/configCases/library/issue-15936-entry-array-exports/a.jstest/configCases/library/issue-15936-entry-array-exports/b.jstest/configCases/library/issue-15936-entry-array-exports/index.jstest/configCases/library/issue-15936-entry-array-exports/webpack.config.js
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
webpack/schema-utils(auto-detected)webpack/tapable(auto-detected)
| * @typedef {object} AbstractLibraryPluginOptions | ||
| * @property {string} pluginName name of the plugin | ||
| * @property {LibraryType} type used library type | ||
| * @property {boolean=} finishAllEntryModules when true, finishEntryModule is called for every entry dependency instead of only the last one, so exports of all entry modules survive optimization |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP -C4 'AbstractLibraryPluginOptions|finishAllEntryModules' types.d.ts declarations.d.ts 2>/dev/null
rg -nP -C3 'AbstractLibraryPlugin' lib/index.jsRepository: webpack/webpack
Length of output: 1070
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- source typedef and option usage ---'
rg -n -P -C8 'AbstractLibraryPluginOptions|finishAllEntryModules' lib/library/AbstractLibraryPlugin.js lib/index.js
printf '%s\n' '--- generated declaration option ---'
rg -n -P -C12 'declare interface AbstractLibraryPluginOptions|finishAllEntryModules' types.d.ts declarations.d.ts 2>/dev/null || true
printf '%s\n' '--- repository generation metadata ---'
rg -n -P -C3 'types\.d\.ts|yarn fix|declarations' package.json scripts tooling lib/index.js 2>/dev/null | head -200Repository: webpack/webpack
Length of output: 13405
Regenerate types.d.ts to include finishAllEntryModules?: boolean in AbstractLibraryPluginOptions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/library/AbstractLibraryPlugin.js` at line 44, Regenerate types.d.ts so
AbstractLibraryPluginOptions declares the optional finishAllEntryModules boolean
property, matching the documented option in AbstractLibraryPlugin.
Source: Coding guidelines
| _getMergeableEntryModules(chunkGraph, chunk) { | ||
| /** @type {Module[]} */ | ||
| const entries = []; | ||
| for (const [ | ||
| module, | ||
| entrypoint | ||
| ] of chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)) { | ||
| if (!chunkGraph.getModuleSourceTypes(module).has("javascript")) continue; | ||
| if (entrypoint && entrypoint.chunks.some((c) => c !== chunk)) { | ||
| // entry depends on other chunks, startup is delayed and | ||
| // exports are not synchronously available for merging | ||
| return []; | ||
| } | ||
| entries.push(module); | ||
| } | ||
| return entries.length > 1 ? entries : []; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect how other code distinguishes delayed startup from extra entrypoint chunks.
rg -nP -C6 'getChunkEntryModulesWithChunkGroupIterable' --type=js lib/
rg -nP -C6 'delayed|startup.*delay|getRuntimeChunk\(\)' --type=js lib/javascript/JavascriptModulesPlugin.jsRepository: webpack/webpack
Length of output: 8044
🏁 Script executed:
#!/bin/bash
sed -n '250,330p' lib/library/AbstractLibraryPlugin.js
sed -n '1490,1565p' lib/javascript/JavascriptModulesPlugin.js
rg -n -C4 'runtimeChunk|splitChunks|array.*entry|entry.*array|merge.*entry|_getMergeableEntryModules' test lib | head -240Repository: webpack/webpack
Length of output: 24287
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- library tests and fixtures ---'
rg -n -i -C3 'library|entry.*array|array.*entry|runtimeChunk|splitChunks' test/configCases test/Defaults.unittest.js test | rg -i 'library|entry|runtimeChunk|splitChunks' | head -300
printf '%s\n' '--- entrypoint and runtime-chunk implementation ---'
rg -n -C5 'getRuntimeChunk|runtimeChunk|chunks =|this\\.chunks' lib/Entrypoint.js lib | head -300
printf '%s\n' '--- merge call sites ---'
rg -n -C8 '_renderEntryExportsMerge|_getMergeableEntryModules' lib testRepository: webpack/webpack
Length of output: 50371
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- issue-15936 case ---'
cat -n test/configCases/library/issue-15936-entry-array-exports/webpack.config.js
cat -n test/configCases/library/issue-15936-entry-array-exports/index.js
find test/configCases/library/issue-15936-entry-array-exports -maxdepth 2 -type f -print
printf '%s\n' '--- runtime chunk and entrypoint chunk-group wiring ---'
rg -n -C5 'setRuntimeChunk|setEntrypointChunk|addChunk\\(' lib | rg -C5 'Entrypoint|runtime|entrypoint|chunkGroup' | head -220
printf '%s\n' '--- relevant Entrypoint methods ---'
cat -n lib/Entrypoint.js | sed -n '1,145p'
printf '%s\n' '--- chunk-group chunk ordering methods ---'
rg -n -C5 'addChunk|insertChunk|chunks' lib/ChunkGroup.js lib/Compilation.js | head -220Repository: webpack/webpack
Length of output: 16737
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- runtime chunk wiring ---'
rg -n -C6 'setRuntimeChunk|setEntrypointChunk' lib
printf '%s\n' '--- entrypoint chunk creation ---'
rg -n -C8 'new Entrypoint|entrypoint\\.pushChunk|entrypoint\\.unshiftChunk|entrypoint\\.insertChunk|chunkGroup\\.pushChunk' lib | head -260
printf '%s\n' '--- relevant library snapshots ---'
find test/configCases/library -maxdepth 3 -type f \( -name '*snap*' -o -name '*.js' \) | rg 'issue-15936|runtime-chunk|type-assign' | sortRepository: webpack/webpack
Length of output: 10820
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- initial entrypoint creation and runtime chunk insertion ---'
sed -n '3515,3685p' lib/Compilation.js
printf '%s\n' '--- library merge call context ---'
rg -n '_renderEntryExportsMerge|renderEntryExportsMerge|merge exports of all entry modules' lib/library lib
printf '%s\n' '--- split-chunk group connection ---'
rg -n -C8 'insertChunk|pushChunk|addChunkInGroup|addChunk.*chunkGroup|chunkGroup.*addChunk' lib/optimize/SplitChunksPlugin.js lib | head -260
printf '%s\n' '--- entrypoint chunk consumers ---'
rg -n -C4 'getEntrypointChunk\\(\\)|entrypoint\\.chunks' lib | head -260Repository: webpack/webpack
Length of output: 16339
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- library render hooks ---'
sed -n '280,355p' lib/library/AssignLibraryPlugin.js
sed -n '85,145p' lib/library/ExportPropertyLibraryPlugin.js
printf '%s\n' '--- chunk rendering and runtime handling ---'
rg -n -C8 'renderContext|entrypoint.*getEntrypointChunk|hasRuntime\\(\\)|getChunkInfo' lib/javascript/JavascriptModulesPlugin.js lib/library
printf '%s\n' '--- split chunk implementation around Chunk.split callers ---'
rg -n -C12 '\\.split\\(' lib/optimize/SplitChunksPlugin.js lib | head -220Repository: webpack/webpack
Length of output: 50371
🏁 Script executed:
#!/bin/bash
python3 - <<'PY'
from pathlib import Path
abstract = Path("lib/library/AbstractLibraryPlugin.js").read_text()
compilation = Path("lib/Compilation.js").read_text()
chunk = Path("lib/Chunk.js").read_text()
assert 'entrypoint.chunks.some((c) => c !== chunk)' in abstract
runtime_setup = 'entry.unshiftChunk(chunk);'
assert runtime_setup in compilation
assert 'entry.setRuntimeChunk(chunk);' in compilation
assert 'chunkGroup.insertChunk(newChunk, this);' in chunk
# Model the chunk-group shapes established by the source:
# a separate runtime chunk is unshifted; split chunks are inserted before
# the entry chunk. Both make the current predicate return true.
entry = object()
runtime = object()
split = object()
def rejects_merge(group, current):
return any(candidate is not current for candidate in group)
cases = {
"separate runtime chunk": ([runtime, entry], entry),
"runtime plus initial split chunk": ([runtime, split, entry], entry),
"single entry chunk": ([entry], entry),
}
for name, (group, current) in cases.items():
print(name, "=>", rejects_merge(group, current))
assert rejects_merge(*cases["separate runtime chunk"])
assert rejects_merge(*cases["runtime plus initial split chunk"])
assert not rejects_merge(*cases["single entry chunk"])
PYRepository: webpack/webpack
Length of output: 253
🏁 Script executed:
#!/bin/bash
python3 - <<'PY'
from pathlib import Path
abstract = Path("lib/library/AbstractLibraryPlugin.js").read_text()
compilation = Path("lib/Compilation.js").read_text()
chunk = Path("lib/Chunk.js").read_text()
assert 'entrypoint.chunks.some((c) => c !== chunk)' in abstract
assert 'entry.unshiftChunk(chunk);' in compilation
assert 'entry.setRuntimeChunk(chunk);' in compilation
assert 'chunkGroup.insertChunk(newChunk, this);' in chunk
entry = object()
runtime = object()
split = object()
def rejects_merge(group, current):
return any(candidate is not current for candidate in group)
cases = {
"separate runtime chunk": ([runtime, entry], entry),
"runtime plus initial split chunk": ([runtime, split, entry], entry),
"single entry chunk": ([entry], entry),
}
for name, (group, current) in cases.items():
print(name, "=>", rejects_merge(group, current))
assert rejects_merge(*cases["separate runtime chunk"])
assert rejects_merge(*cases["runtime plus initial split chunk"])
assert not rejects_merge(*cases["single entry chunk"])
PYRepository: webpack/webpack
Length of output: 253
Distinguish runtime chunks from delayed startup chunks.
entrypoint.chunks.some((c) => c !== chunk) treats a separate runtime chunk as delayed startup and returns [], so array-entry export merging is skipped. Exclude entrypoint.getRuntimeChunk() from this check while retaining the guard for chunks that can defer startup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/library/AbstractLibraryPlugin.js` around lines 296 - 312, Update
_getMergeableEntryModules so the delayed-startup guard ignores the entrypoint’s
runtime chunk: when checking entrypoint.chunks, exclude both the current chunk
and entrypoint.getRuntimeChunk(). Preserve the existing early return for other
chunks that can delay startup and keep the JavaScript-module filtering and
mergeability rules unchanged.
| for (const module of entries.slice(0, -1).reverse()) { | ||
| buf.push( | ||
| `__webpack_merge_entry__(${RuntimeGlobals.require}(${JSON.stringify( | ||
| chunkGraph.getModuleId(module) | ||
| )}));` | ||
| ); | ||
| } | ||
| buf.push( | ||
| `if (${RuntimeGlobals.exports}.__esModule) ${RuntimeGlobals.makeNamespaceObject}(__webpack_exports_merged__);` | ||
| ); | ||
| buf.push(`${RuntimeGlobals.exports} = __webpack_exports_merged__;`); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
The merge order assumes the startup-rendered module is the last entry, and the __esModule check reads only that module.
Two points on the generated code:
-
Line 348 merges
entries.slice(0, -1). This assumesRuntimeGlobals.exportsholds the exports ofentries[entries.length - 1]._renderEntryExportsMergeignores themoduleargument thatrenderStartupreceives, so the assumption is never checked. If the rendered startup module is not the last entry, that module's exports are merged twice and one entry module is skipped. Use themodulepassed torenderStartupto exclude the correct entry. -
Line 356 tests
__webpack_exports__.__esModuleon the last entry only. If an earlier entry is an ES module and the last entry is not, the merged object loses the namespace marker even though it carries ESM exports.
Sketch of the module-driven variant
- _renderEntryExportsMerge(renderContext) {
- const { chunk, chunkGraph, inlined } = renderContext;
+ _renderEntryExportsMerge(startupModule, renderContext) {
+ const { chunk, chunkGraph, inlined } = renderContext;
if (inlined) return "";
const entries = this._getMergeableEntryModules(chunkGraph, chunk);
if (entries.length === 0) return "";Then merge entries.filter((m) => m !== startupModule).reverse() after the RuntimeGlobals.exports call, and drive the namespace marker from whether any merged entry is an ES module.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/library/AbstractLibraryPlugin.js` around lines 348 - 358, Update
_renderEntryExportsMerge and its renderStartup call path to use the startup
module argument when selecting entries: exclude that module rather than blindly
slicing off the last entry, then merge the remaining entries in reverse order
after the RuntimeGlobals.exports call. Determine the namespace marker from
whether any merged entry is an ES module, so __esModule is preserved when an
earlier entry provides it.
| `__webpack_merge_entry__(${RuntimeGlobals.require}(${JSON.stringify( | ||
| chunkGraph.getModuleId(module) | ||
| )}));` | ||
| ); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check the declared return type of ChunkGraph#getModuleId.
ast-grep run --pattern 'getModuleId($_) { $$$ }' --lang javascript lib/ChunkGraph.js
rg -nP -B8 'getModuleId\(module\)\s*\{' lib/ChunkGraph.jsRepository: webpack/webpack
Length of output: 704
🏁 Script executed:
#!/bin/bash
sed -n '1490,1555p' lib/ChunkGraph.js
sed -n '300,375p' lib/library/AbstractLibraryPlugin.js
rg -n "getModuleId\\(|__webpack_merge_entry__|merge.*entry|entryModule" lib testRepository: webpack/webpack
Length of output: 50372
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- ChunkGraph#getModuleId ---'
sed -n '1515,1565p' lib/ChunkGraph.js
printf '%s\n' '--- AbstractLibraryPlugin call site ---'
sed -n '330,365p' lib/library/AbstractLibraryPlugin.js
printf '%s\n' '--- merge-entry references in source ---'
rg -n --glob 'lib/**/*.js' --glob 'test/**/*.js' --glob '!test/**/*.snap' '__webpack_merge_entry__|merge_entry|getModuleId\\(module\\)' lib test | head -200
printf '%s\n' '--- module ID assignment paths ---'
rg -n --glob 'lib/**/*.js' 'setModuleId|getModuleId\\(' lib/ChunkGraph.js lib | head -250Repository: webpack/webpack
Length of output: 3244
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- AbstractLibraryPlugin surrounding method ---'
sed -n '250,380p' lib/library/AbstractLibraryPlugin.js
printf '%s\n' '--- ChunkGraph ID reads and writes ---'
rg -n --glob 'lib/**/*.js' 'setModuleId|getModuleId\\(' lib | head -300
printf '%s\n' '--- entry rendering call graph ---'
rg -n --glob 'lib/**/*.js' 'renderStartup|render.*Startup|render.*Chunk|renderChunk|renderMain|startupRender' lib | head -250
printf '%s\n' '--- relevant tests ---'
rg -n --glob 'test/**/*.js' 'merge.*entry|multiple.*entry|library.*entry|export.*merge' test | head -200Repository: webpack/webpack
Length of output: 14442
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- all module ID writes ---'
rg -n --glob 'lib/**/*.js' 'setModuleId' lib | head -200
printf '%s\n' '--- all getModuleId calls ---'
rg -n --glob 'lib/**/*.js' 'getModuleId' lib | head -250
printf '%s\n' '--- startup rendering ---'
sed -n '1180,1285p' lib/javascript/JavascriptModulesPlugin.js
printf '%s\n' '--- module ID assignment hooks ---'
rg -n --glob 'lib/**/*.js' 'moduleIds|assign.*module|ModuleIds|beforeModuleIds|afterModuleIds' lib | head -250Repository: webpack/webpack
Length of output: 25971
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- module ID lifecycle ---'
sed -n '3640,3750p' lib/Compilation.js
sed -n '4325,4385p' lib/Compilation.js
sed -n '5835,5885p' lib/Compilation.js
printf '%s\n' '--- module needId definition and assignments ---'
rg -n --glob 'lib/**/*.js' 'needId' lib/Module.js lib/Compilation.js lib/ids | head -200
sed -n '280,335p' lib/Module.js
printf '%s\n' '--- ascending assignment guard ---'
sed -n '430,480p' lib/ids/IdHelpers.js
printf '%s\n' '--- default moduleIds configuration ---'
sed -n '2150,2205p' lib/config/defaults.jsRepository: webpack/webpack
Length of output: 12101
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- entry-module rendering and ID use ---'
sed -n '1545,1615p' lib/javascript/JavascriptModulesPlugin.js
printf '%s\n' '--- seal/emit order after module IDs ---'
sed -n '3720,3845p' lib/Compilation.js
rg -n --glob 'lib/**/*.js' 'getChunkEntryModulesWithChunkGroupIterable|connectChunkAndEntryModule' lib/ChunkGraph.js lib/Compilation.js
printf '%s\n' '--- all assignments that disable IDs ---'
rg -n --glob 'lib/**/*.js' 'needId\\s*=\\s*false|setModuleId\\([^\\n]*""' libRepository: webpack/webpack
Length of output: 7216
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- moduleIds option handling ---'
sed -n '690,765p' lib/WebpackOptionsApply.js
rg -n --glob 'schemas/**/*.json' --glob 'declarations/**/*.d.ts' '"moduleIds"|moduleIds' schemas declarations | head -100
printf '%s\n' '--- entry module graph methods ---'
sed -n '1160,1245p' lib/ChunkGraph.js
sed -n '1460,1515p' lib/ChunkGraph.js
printf '%s\n' '--- ID-disabled module cases ---'
rg -n --glob 'lib/**/*.js' 'needId = false|setModuleId\\([^)]*, ""\\)' lib || trueRepository: webpack/webpack
Length of output: 7216
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- runtime require handling for null IDs ---'
sed -n '585,700p' lib/RuntimeTemplate.js
sed -n '730,770p' lib/RuntimeTemplate.js
sed -n '810,870p' lib/RuntimeTemplate.js
printf '%s\n' '--- tests/configurations using moduleIds: false ---'
rg -n --glob 'test/**/*.js' --glob 'test/**/*.json' 'moduleIds\\s*:\\s*false|moduleIds.*false' test | head -100
printf '%s\n' '--- option documentation for moduleIds false ---'
rg -n --glob 'types.d.ts' --glob 'schemas/WebpackOptions.json' --glob 'declarations/**/*.d.ts' 'moduleIds' | head -80Repository: webpack/webpack
Length of output: 7522
🏁 Script executed:
#!/bin/bash
python3 - <<'PY'
from pathlib import Path
import re
apply = Path("lib/WebpackOptionsApply.js").read_text()
abstract = Path("lib/library/AbstractLibraryPlugin.js").read_text()
chunk_graph = Path("lib/ChunkGraph.js").read_text()
defaults = Path("lib/config/defaults.js").read_text()
assert 'if (moduleIds) {' in apply
assert 'moduleIds: false' in Path("declarations/WebpackOptions.d.ts").read_text()
assert '`@returns` {ModuleId | null} the id of the module' in chunk_graph
assert 'JSON.stringify(' in abstract
assert 'getModuleId(module)' in abstract
assert '=== null' not in abstract[abstract.index('for (const module of entries'):abstract.index('return `${Template.asString(buf)}')]
print("moduleIds:false is supported, core plugin registration is conditional, getModuleId is nullable, and the merge loop has no null-ID guard.")
PY
node - <<'JS'
const moduleId = null;
const generated = `__webpack_require__(${JSON.stringify(moduleId)})`;
if (generated !== "__webpack_require__(null)") process.exit(1);
console.log(generated);
JSRepository: webpack/webpack
Length of output: 268
🏁 Script executed:
#!/bin/bash
python3 - <<'PY'
from pathlib import Path
apply = Path("lib/WebpackOptionsApply.js").read_text()
abstract = Path("lib/library/AbstractLibraryPlugin.js").read_text()
chunk_graph = Path("lib/ChunkGraph.js").read_text()
declarations = Path("declarations/WebpackOptions.d.ts").read_text()
checks = {
"moduleIds conditional": "if (moduleIds) {" in apply,
"moduleIds false supported": 'moduleIds?: "natural" | "named" | "hashed" | "deterministic" | "size" | false;' in declarations,
"nullable getModuleId": "`@returns` {ModuleId | null} the id of the module" in chunk_graph,
"merge call uses getModuleId": "chunkGraph.getModuleId(module)" in abstract,
}
for name, passed in checks.items():
print(f"{name}: {passed}")
start = abstract.index("for (const module of entries")
end = abstract.index("return `${Template.asString(buf)}\\n`;", start)
print("--- merge loop ---")
print(abstract[start:end])
PYRepository: webpack/webpack
Length of output: 696
Guard against null module IDs before emitting the merge call.
When optimization.moduleIds is false and no custom plugin assigns IDs, getModuleId returns null. This loop then emits __webpack_require__(null), which fails at runtime. Ensure every mergeable entry has an ID or skip the merge before generating this code.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/library/AbstractLibraryPlugin.js` around lines 350 - 353, Update the
merge-entry generation around chunkGraph.getModuleId(module) in
AbstractLibraryPlugin to handle null IDs: only emit the __webpack_merge_entry__
call when the module has a valid ID, or otherwise skip that mergeable entry.
Preserve existing output for modules with assigned IDs.
Fixes #15936
Problem
With
entry: ["./a.js", "./b.js", "./c.js"]and a library output, only the last entry module's exports were flagged as used — exports of earlier entries were tree-shaken away and silently dropped from the library.Solution (library plugin layer only)
AbstractLibraryPlugingains an opt-infinishAllEntryModulesmode that flags exports of every entry dependency as used, plus a helper that renders startup code merging all entry modules' exports into the library exports object.AssignLibraryPlugin(var/assign/this/window/self/global/commonjs/commonjs2) andExportPropertyLibraryPlugin(amd/umd/system/jsonp) opt in.module/modern-moduleandcommonjs-staticintentionally keep current behavior (their export lists are generated statically from the last entry module).Testing
test/configCases/library/issue-15936-entry-array-exports— fails on current main, passes with this change (covers multi-entry exposure + conflict semantics).librarycategory of ConfigTestCases: no new failures vs baseline.Summary by CodeRabbit
New Features
Bug Fixes
Tests