fix: pre-populate externals in lazy proxy entry chunk for closure libraries - #21013
Conversation
…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.
🦋 Changeset detectedLatest commit: 639c7e0 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
This PR is packaged and the instant preview is available (7dd1824). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@7dd1824
yarn add -D webpack@https://pkg.pr.new/webpack@7dd1824
pnpm add -D webpack@https://pkg.pr.new/webpack@7dd1824 |
Codecov Report❌ Patch coverage is
❌ 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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-wrappedoutput.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.
| } else if (item && typeof item === "object" && !(item instanceof RegExp)) { | ||
| for (const request of Object.keys(item)) { |
| 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. |
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.
The filename still referenced the earlier reload-based design that was replaced before the PR settled. Rename to reflect what's actually shipped.
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.
Summary
Closes #19134.
When
output.library.typeproduces 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. WithlazyCompilationenabled, the initial entry chunk only contains theLazyCompilationProxyModule, 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 throwReferenceErrorat runtime; only a manual page refresh recovers.The inactive
LazyCompilationProxyModulenow declares the statically-enumerable externals (string and object forms ofexternals, recursing into arrays, skippingbyLayer) 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 withReferenceError: __WEBPACK_EXTERNAL_MODULE_util__ is not defined, passes with it.test/hotCases/lazy-compilation/issue-19134/— mirrors the reporter'swebpack.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
LazyCompilationProxyModulegains extra dependencies, and only whenoutput.library.typeis one of the closure-wrapped library types. Other library types and non-lazyCompilationbuilds 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).