Skip to content

fix: pre-populate externals in lazy proxy entry chunk for closure libraries - #21013

Merged
alexander-akait merged 11 commits into
mainfrom
claude/fix-issue-19134-J1Iyo
May 22, 2026
Merged

fix: pre-populate externals in lazy proxy entry chunk for closure libraries#21013
alexander-akait merged 11 commits into
mainfrom
claude/fix-issue-19134-J1Iyo

Conversation

@alexander-akait

@alexander-akait alexander-akait commented May 21, 2026

Copy link
Copy Markdown
Member

Summary

Closes #19134.

When output.library.type produces a closure-wrapped bundle (umd, umd2, amd, amd-require, system), external modules of those types reference closure-bound identifiers like __WEBPACK_EXTERNAL_MODULE_react__ that the library wrapper supplies as function arguments. With lazyCompilation enabled, the initial entry chunk only contains the LazyCompilationProxyModule, so the wrapper is rendered without those arguments. When the proxy activates for the first time, the externals it pulls in arrive in a hot-update chunk that lives outside the original wrapper closure, and their factory bodies throw ReferenceError at runtime; only a manual page refresh recovers.

The inactive LazyCompilationProxyModule now declares the statically-enumerable externals (string and object forms of externals, recursing into arrays, skipping byLayer) as its own dependencies. They land in the initial entry chunk, so the library wrapper exposes their closure identifiers from the first build. When activation later imports them through the lazily-compiled module, they resolve to the already-installed factories instead of throwing. Function and RegExp externals are not pre-populated because their effective request set isn't statically knowable.

What kind of change does this PR introduce?

fix

Did you add tests for your changes?

Yes:

  • test/hotCases/lazy-compilation/umd-library-externals/ — minimal UMD-library + closure-bound external reproduction; fails without the fix with ReferenceError: __WEBPACK_EXTERNAL_MODULE_util__ is not defined, passes with it.
  • test/hotCases/lazy-compilation/issue-19134/ — mirrors the reporter's webpack.config.js (UMD library + multiple closure externals + lazyCompilation); fails without the fix with the same shape as the issue's __WEBPACK_EXTERNAL_MODULE_react__ error.
  • test/hotCases/lazy-compilation/import-externals/, module-externals/, module-import-externals/ — regression coverage for non-closure external types so the new code path doesn't disturb them.

Does this PR introduce a breaking change?

No. Only the inactive LazyCompilationProxyModule gains extra dependencies, and only when output.library.type is one of the closure-wrapped library types. Other library types and non-lazyCompilation builds are unaffected.

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 drafted the implementation, the regression tests, and the changeset under human review. The maintainer reviewed and approved each step (including pivoting from an earlier reload-based approach to this dependency-reservation approach after feedback).

…library output

When `output.library.type` is one of `umd`, `umd2`, `amd`, `amd-require`, or `system`,
the library wrapper passes external modules as closure arguments
(e.g. `__WEBPACK_EXTERNAL_MODULE_react__`). With `lazyCompilation`, the
initial entry chunk only contains the `LazyCompilationProxyModule`, so the
wrapper is generated without those arguments. When the proxy activates, any
external dependencies it pulls in arrive in a hot-update chunk that lives
outside the original wrapper closure, so their factory bodies cannot resolve
the closure identifier and throw `ReferenceError` at runtime - only a manual
refresh recovers.

The `LazyCompilationProxyModule` now reloads the page when it transitions
from inactive to active under one of these library types. The inactive
proxy stamps a `wasInactive` flag on its dispose data; the new active proxy
sees the flag, calls `window.location.reload()`, and bails out before the
async chunk that holds the missing externals is requested. After the
reload, the freshly initialized bundle is built with the externals baked
into its wrapper, so the closure identifiers resolve correctly.

Other library types (`var`, `commonjs2`, `module`, `global`, `window`,
etc.) are unaffected since they don't rely on closure-bound externals.

Closes #19134
…raries

When `lazyCompilation` activates a proxy for the first time under a
closure-wrapped library type (umd, umd2, amd, amd-require, system), any
external dependency the lazily-built module pulls in lands in a hot update
chunk that lives outside the original library wrapper closure. Its factory
references closure identifiers like `__WEBPACK_EXTERNAL_MODULE_react__`
that the original entry-chunk wrapper never declared, so the activation
update throws `ReferenceError` at runtime and only a manual page refresh
recovers (issue #19134).

Reserve statically-enumerable externals as dependencies of the inactive
`LazyCompilationProxyModule` so the initial entry chunk's wrapper already
exposes those closure identifiers. Activation then resolves them against
the existing wrapper instead of an undeclared name. Function and RegExp
externals are skipped because their effective request set is only known
once something asks for the request.
…externals

These external types resolve via dynamic `import()` (`import`,
`module-import`) or static `import` statements (`module`) rather than
closure identifiers, so the closure-wrapper fix from the previous commit
doesn't apply to them. Adding regression coverage to confirm the fix
doesn't disturb their existing behavior and to lock in that lazy
compilation continues to work for each form.
…tend config

Mirrors the reporter's webpack config from the original issue (UMD
library output with multiple closure-bound externals and lazyCompilation
enabled). Without the fix the test reproduces
`ReferenceError: __WEBPACK_EXTERNAL_MODULE_fs__ is not defined`, the
same shape as the issue's `__WEBPACK_EXTERNAL_MODULE_react__` failure;
with the fix the lazy 'page' activates cleanly and exposes both
externals.
Copilot AI review requested due to automatic review settings May 21, 2026 19:56
@changeset-bot

changeset-bot Bot commented May 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 639c7e0

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

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

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

Install it locally:

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

@codecov

codecov Bot commented May 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.32258% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.57%. Comparing base (294197c) to head (639c7e0).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
lib/hmr/LazyCompilationPlugin.js 90.32% 3 Missing ⚠️

❌ Your changes status has failed because you have indirect coverage changes. Learn more about Unexpected Coverage Changes and reasons for indirect coverage changes.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #21013      +/-   ##
==========================================
+ Coverage   90.94%   91.57%   +0.62%     
==========================================
  Files         573      573              
  Lines       58986    59491     +505     
  Branches    15898    16064     +166     
==========================================
+ Hits        53647    54480     +833     
+ Misses       5339     5011     -328     
Flag Coverage Δ
integration 89.49% <90.32%> (-0.23%) ⬇️
test262 45.31% <ø> (-0.07%) ⬇️
unit 37.95% <ø> (+1.32%) ⬆️

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.

Copilot AI 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.

Pull request overview

This PR addresses a runtime ReferenceError that occurs when lazyCompilation is used with closure-wrapped library outputs (e.g. UMD/AMD/System) and the first activation pulls in externals via a hot-update chunk. The fix ensures statically enumerable externals are present in the initial entry chunk so closure-bound external identifiers are available when the lazy activation update arrives.

Changes:

  • Teach LazyCompilationProxyModule (inactive state) to add statically enumerable externals as dependencies when using closure-wrapped output.library.type.
  • Add hot test cases covering the reported UMD scenario (issue #19134) and a minimal UMD external reproduction.
  • Add additional hot test coverage for ESM-based externals types (import, module, module-import) under lazy compilation, plus a changeset entry.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
lib/hmr/LazyCompilationPlugin.js Adds closure-library detection + collects static externals to reserve them on inactive lazy proxies.
.changeset/lazy-compilation-reload-for-closure-libraries.md Documents the fix in the release notes.
test/hotCases/lazy-compilation/umd-library-externals/webpack.config.js New UMD-library + externals + lazyCompilation hot case config.
test/hotCases/lazy-compilation/umd-library-externals/module.js Imports an external to trigger the historical failure mode.
test/hotCases/lazy-compilation/umd-library-externals/index.js Verifies first lazy activation doesn’t throw and resolves expected value.
test/hotCases/lazy-compilation/issue-19134/webpack.config.js Regression config mirroring issue #19134 (UMD + multiple externals + lazyCompilation).
test/hotCases/lazy-compilation/issue-19134/page.js Simulated “page” module that depends on multiple externals.
test/hotCases/lazy-compilation/issue-19134/index.js Assertion test ensuring first activation under the issue scenario does not throw.
test/hotCases/lazy-compilation/import-externals/webpack.config.js Adds lazyCompilation coverage for externalsType: "import" in ESM output.
test/hotCases/lazy-compilation/import-externals/module.js ESM module that consumes an external via import-form externals.
test/hotCases/lazy-compilation/import-externals/index.js Test ensuring first activation doesn’t throw for import-form externals.
test/hotCases/lazy-compilation/module-externals/webpack.config.js Adds lazyCompilation coverage for externalsType: "module" in ESM output.
test/hotCases/lazy-compilation/module-externals/module.js ESM module that consumes an external via module-form externals.
test/hotCases/lazy-compilation/module-externals/index.js Test ensuring first activation doesn’t throw for module-form externals.
test/hotCases/lazy-compilation/module-import-externals/webpack.config.js Adds lazyCompilation coverage for externalsType: "module-import" in ESM output.
test/hotCases/lazy-compilation/module-import-externals/module.js ESM module that consumes an external via module-import externals.
test/hotCases/lazy-compilation/module-import-externals/index.js Test ensuring first activation doesn’t throw for module-import externals.

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

Comment thread lib/hmr/LazyCompilationPlugin.js Outdated
Comment on lines +91 to +92
} else if (item && typeof item === "object" && !(item instanceof RegExp)) {
for (const request of Object.keys(item)) {
Comment on lines +5 to +9
Fix runtime `ReferenceError` on the first activation of a lazy-compiled module when `output.library.type` produces a closure-wrapped bundle (`umd`, `umd2`, `amd`, `amd-require`, `system`).

External modules of these types reference closure-bound identifiers like `__WEBPACK_EXTERNAL_MODULE_react__`, supplied by the library wrapper that is generated once per chunk. When `lazyCompilation` activates an entry or import for the first time, any external dependency the lazily-built module pulls in arrived in a hot-update chunk that lives outside the original wrapper closure, so its factory body could not resolve the closure identifier and only a manual page refresh recovered.

The inactive `LazyCompilationProxyModule` now declares statically-enumerable externals (string and object forms of `externals`) as its own dependencies, so the initial entry chunk's library wrapper already exposes their closure identifiers. When activation later pulls in those externals through the lazily-compiled module, they resolve to the already-installed factories instead of throwing. Function and RegExp externals are not pre-populated because their effective request set isn't knowable up front.

Fix runtime `ReferenceError` on the first activation of a lazy-compiled module when `output.library.type` produces a closure-wrapped bundle (`umd`, `umd2`, `amd`, `amd-require`, `system`).

External modules of these types reference closure-bound identifiers like `__WEBPACK_EXTERNAL_MODULE_react__`, supplied by the library wrapper that is generated once per chunk. When `lazyCompilation` activates an entry or import for the first time, any external dependency the lazily-built module pulls in arrived in a hot-update chunk that lives outside the original wrapper closure, so its factory body could not resolve the closure identifier and only a manual page refresh recovered.
@codspeed-hq

codspeed-hq Bot commented May 21, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 32.09%

⚡ 12 improved benchmarks
❌ 2 regressed benchmarks
✅ 130 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 "asset-modules-source", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 400.1 KB 168 KB ×2.4
Memory benchmark "side-effects-reexport", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 409.2 KB 1,029.8 KB -60.26%
Memory benchmark "future-defaults", scenario '{"name":"mode-production","mode":"production"}' 11.8 MB 7.8 MB +51.4%
Memory benchmark "many-modules-esm", scenario '{"name":"mode-production","mode":"production"}' 9.1 MB 7.5 MB +20.91%
Memory benchmark "devtool-eval-source-map", scenario '{"name":"mode-production","mode":"production"}' 7.9 MB 6.5 MB +22.7%
Memory benchmark "asset-modules-resource", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 352.5 KB 209.5 KB +68.23%
Memory benchmark "cache-filesystem", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 351.1 KB 843.6 KB -58.38%
Memory benchmark "css-modules", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 504.4 KB 412.9 KB +22.17%
Memory benchmark "concatenate-modules", scenario '{"name":"mode-development","mode":"development"}' 1,115.7 KB 791.9 KB +40.89%
Memory benchmark "concatenate-modules", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 161.4 KB 131.2 KB +23%
Memory benchmark "context-esm", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 668.7 KB 149.5 KB ×4.5
Memory benchmark "many-chunks-esm", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 246.4 KB 171.1 KB +44.01%
Memory benchmark "many-chunks-commonjs", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 252.1 KB 168.8 KB +49.37%
Memory benchmark "many-modules-esm", scenario '{"name":"mode-development","mode":"development"}' 1,362.9 KB 839.3 KB +62.39%

Tip

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


Comparing claude/fix-issue-19134-J1Iyo (639c7e0) with main (c755747)

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.

Addresses Copilot review feedback on #21013:
- `collectStaticExternalRequests` was reserving every key of an object
  external as a request name, including the special `byLayer` key that
  holds layer-specific overrides rather than a real request. It would
  have produced a bogus `require("byLayer")` dependency on the inactive
  proxy. Skip `byLayer` so only actual request names are reserved.
- Replace past-tense "arrived"/"could not"/"recovered" with present-tense
  "arrives"/"cannot"/"recovers" in the changeset description.
@alexander-akait alexander-akait changed the title fix: reload page on lazy compilation activation with closure-wrapped library output fix: pre-populate externals in lazy proxy entry chunk for closure libraries May 22, 2026
The filename still referenced the earlier reload-based design that was
replaced before the PR settled. Rename to reflect what's actually
shipped.
Copilot AI review requested due to automatic review settings May 22, 2026 08:47
Earlier pass just skipped the `byLayer` key; this misses externals that
are only declared per-layer (`externals: { byLayer: { foo: { react: "React" } } }`).
Now collect requests nested under each layer too, so the inactive proxy
still reserves them. Layer-resolution itself is intentionally not done
- we err on the side of reserving more than needed since the lazy
proxy is unaware of its eventual issuer layer.

Adds a smoke test (`byLayer-externals`) confirming the configuration
keeps building under `lazyCompilation` + UMD.