Skip to content

fix: expose exports of all entry modules in library output - #21661

Closed
webdevelopersrinu wants to merge 1 commit into
webpack:mainfrom
webdevelopersrinu:fix-15936-multi-entry-library-exports
Closed

webdevelopersrinu wants to merge 1 commit into
webpack:mainfrom
webdevelopersrinu:fix-15936-multi-entry-library-exports

Conversation

@webdevelopersrinu

@webdevelopersrinu webdevelopersrinu commented Aug 10, 2026 •

Copy link
Copy Markdown

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)

  • AbstractLibraryPlugin gains an opt-in finishAllEntryModules mode 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) and ExportPropertyLibraryPlugin (amd/umd/system/jsonp) opt in.
  • Later entries win on export name conflicts — preserving the documented "last entry wins" semantics for conflicting names.
  • module/modern-module and commonjs-static intentionally keep current behavior (their export lists are generated statically from the last entry module).
  • Merging is skipped when startup is delayed by depending chunks or when entry modules are inlined, so existing output is unchanged in those cases. Single-entry output is byte-identical to before.

Testing

  • New test case test/configCases/library/issue-15936-entry-array-exports — fails on current main, passes with this change (covers multi-entry exposure + conflict semantics).
  • Full library category of ConfigTestCases: no new failures vs baseline.

Summary by CodeRabbit

  • New Features

    • Multi-entry libraries now expose exports from all entry modules.
    • Conflicting export names follow a predictable last-entry-wins behavior.
    • Export-property libraries can merge entry exports while applying configured export settings.
  • Bug Fixes

    • Improved handling of exports in multi-entry library bundles.
  • Tests

    • Added coverage for combined entry exports and conflicting export precedence.

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

changeset-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: c23dcff

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 no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

@linux-foundation-easycla

linux-foundation-easycla Bot commented Aug 10, 2026 •

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

  • ✅ login: webdevelopersrinu / name: webdevelopersrinu (c23dcff)

@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The 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

Layer / File(s) Summary
Entry export merge core
lib/library/AbstractLibraryPlugin.js
AbstractLibraryPlugin can finalize all entry dependencies, generate merged startup exports, preserve namespace semantics, add runtime requirements, and report an inline-runtime bailout.
Library plugin integration
lib/library/AssignLibraryPlugin.js, lib/library/ExportPropertyLibraryPlugin.js
Library plugins configure entry finalization, render merged exports, and register runtime dependencies according to library type.
Multi-entry export validation
test/configCases/library/issue-15936-entry-array-exports/*
The test configuration defines three entry modules and verifies all exports plus later-entry precedence for duplicate names.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title uses the required conventional-commit form and clearly describes exposing exports from all entry modules.
Linked Issues check ✅ Passed The implementation merges exports from all library entry modules and adds regression coverage for issue #15936.
Out of Scope Changes check ✅ Passed The changes are limited to library export merging and regression tests for issue #15936.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

👋 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
and leave a comment explaining why this is a mistake.

@github-actions github-actions Bot closed this Aug 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
lib/library/AssignLibraryPlugin.js (1)

135-138: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The unnamed !== "static" predicate is repeated at three sites.

The constructor, renderStartup (line 328), and runtimeRequirements each 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__.r without 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 win

Add a regression test for the options.export path combined with merging.

When options.export is set, finishEntryModule marks 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 the library.export variant 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 win

The bailout tap recomputes the entry list for every module in the chunk.

inlineInRuntimeBailout runs once per candidate module. _getMergeableEntryModules iterates all entry modules and calls getModuleSourceTypes each time, so the cost is O(modules × entries) per chunk. _renderEntryExportsMerge and _addEntryExportsMergeRuntimeRequirements repeat the same computation.

Cache the result per chunk in a WeakMap created inside the thisCompilation callback, 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 value

The 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

📥 Commits

Reviewing files that changed from the base of the PR and between e3f177c and c23dcff.

📒 Files selected for processing (7)
  • lib/library/AbstractLibraryPlugin.js
  • lib/library/AssignLibraryPlugin.js
  • lib/library/ExportPropertyLibraryPlugin.js
  • test/configCases/library/issue-15936-entry-array-exports/a.js
  • test/configCases/library/issue-15936-entry-array-exports/b.js
  • test/configCases/library/issue-15936-entry-array-exports/index.js
  • test/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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 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.js

Repository: 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 -200

Repository: 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

Comment on lines +296 to +312
_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 : [];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.js

Repository: 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 -240

Repository: 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 test

Repository: 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 -220

Repository: 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' | sort

Repository: 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 -260

Repository: 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 -220

Repository: 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"])
PY

Repository: 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"])
PY

Repository: 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.

Comment on lines +348 to +358
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__;`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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:

  1. Line 348 merges entries.slice(0, -1). This assumes RuntimeGlobals.exports holds the exports of entries[entries.length - 1]. _renderEntryExportsMerge ignores the module argument that renderStartup receives, 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 the module passed to renderStartup to exclude the correct entry.

  2. Line 356 tests __webpack_exports__.__esModule on 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.

Comment on lines +350 to +353
`__webpack_merge_entry__(${RuntimeGlobals.require}(${JSON.stringify(
chunkGraph.getModuleId(module)
)}));`
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 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.js

Repository: 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 test

Repository: 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 -250

Repository: 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 -200

Repository: 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 -250

Repository: 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.js

Repository: 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]*""' lib

Repository: 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 || true

Repository: 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 -80

Repository: 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);
JS

Repository: 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])
PY

Repository: 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.

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.

export in library works only for last entry module

1 participant