feat: preserve defer/source import phase on external dependencies#20934
Conversation
Adds two configCases under test/configCases/externals to document the runtime code webpack emits when a static `import defer` or `import source` targets an external module: - phase-imports-defer asserts that a sync `var` external is wrapped with `__webpack_require__.zO(...)` (the optimized deferred namespace helper) while an already-async `promise` external falls back to a regular harmony import (defer is a no-op). - phase-imports-source asserts that source-phase static imports of externals emit a regular harmony import and are not lowered to the deferred helper. https://claude.ai/code/session_01BKSJuH4X5zjxsszYXHRqy2
When an external is imported via a phase-aware import statement, the phase keyword is now propagated through ExternalModule's emitted source the same way import attributes are. - Capture `dependency.phase` alongside `attributes` and `externalType` in ImportDependencyMeta when wiring up an ExternalModule. - For static module-type externals, emit `import defer * as <id> from "x"` for namespace defer imports and `import source <id> from "x"` for single-binding source imports in the ModuleExternalInitFragment output. - For dynamic import-type externals, emit `import.defer(…)` / `import.source(…)` instead of `import(…)` when the importFunctionName is the default `"import"`. Adds a configCase under externals/phase-imports-esm covering both static and dynamic phase forms; the existing phase-imports-defer / -source tests continue to assert the runtime-bundle (non-ESM) behaviour. https://claude.ai/code/session_01BKSJuH4X5zjxsszYXHRqy2
Extends phase-imports-esm to assert phase keyword emission across all three external types that resolve to native ESM: - `module` externals at static sites → `import defer * as …` / `import source … from …`. - `import` externals at dynamic sites → `import.defer(…)` / `import.source(…)`. - `module-import` externals dispatch to the `module` form for static imports and the `import` form for dynamic imports based on the consumer dependency. https://claude.ai/code/session_01BKSJuH4X5zjxsszYXHRqy2
🦋 Changeset detectedLatest commit: 337223f 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 (b60f7bd). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@b60f7bd
yarn add -D webpack@https://pkg.pr.new/webpack@b60f7bd
pnpm add -D webpack@https://pkg.pr.new/webpack@b60f7bd |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #20934 +/- ##
==========================================
+ Coverage 91.34% 91.38% +0.04%
==========================================
Files 566 568 +2
Lines 56130 56519 +389
Branches 14904 15022 +118
==========================================
+ Hits 51274 51652 +378
- Misses 4856 4867 +11
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 extends external-module handling to carry import “phase” metadata (defer / source) through to code generation, and adds configCases to lock in the emitted runtime/code patterns for phase imports targeting externals.
Changes:
- Propagate
dependency.phaseintoExternalModuledependency metadata (similar to import attributes). - Emit phase-aware forms for externals: native
import defer/import sourceformoduleexternals, andimport.defer(...)/import.source(...)forimportexternals when applicable. - Add new externals configCases covering
deferandsourcephase imports (including an ESM-output scenario).
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
types.d.ts |
Adds ImportDependencyMeta.phase to the published typings. |
lib/ExternalModuleFactoryPlugin.js |
Threads dependency.phase into dependencyMeta for external module creation. |
lib/ExternalModule.js |
Uses phase metadata to influence emitted import statements / dynamic import call forms for externals. |
.changeset/external-module-phase-imports.md |
Changeset documenting the new phase-preservation behavior for externals. |
test/configCases/externals/phase-imports-source/webpack.config.js |
New configCase enabling sourceImport for externals. |
test/configCases/externals/phase-imports-source/test.config.js |
Selects the built test bundle for the new source-phase case. |
test/configCases/externals/phase-imports-source/index.js |
Asserts generated bundle content for source-phase static imports of externals. |
test/configCases/externals/phase-imports-source/bundle.js |
Entry containing import source statements against externals. |
test/configCases/externals/phase-imports-esm/webpack.config.js |
New ESM-output configCase covering module, import, and module-import externals with phase usage. |
test/configCases/externals/phase-imports-esm/test.config.js |
Selects the built test bundle for the ESM phase-imports case. |
test/configCases/externals/phase-imports-esm/index.js |
Validates emitted native phase import syntax / import.defer / import.source strings in output. |
test/configCases/externals/phase-imports-esm/bundle.js |
Entry exercising phase import syntax for different external types. |
test/configCases/externals/phase-imports-defer/webpack.config.js |
New configCase enabling deferImport for externals. |
test/configCases/externals/phase-imports-defer/test.config.js |
Selects the built test bundle for the defer-phase case. |
test/configCases/externals/phase-imports-defer/infrastructure-log.js |
Filters expected infrastructure log noise when FS cache is enabled. |
test/configCases/externals/phase-imports-defer/index.js |
Asserts deferred runtime helper usage for sync externals and no-op behavior for async externals. |
test/configCases/externals/phase-imports-defer/bundle.js |
Entry using /* webpackDefer: true */ imports against externals. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const phase = dependencyMeta && dependencyMeta.phase; | ||
| let content = ""; | ||
| if (imported === true) { | ||
| // namespace | ||
| content = `import * as ${identifier} from ${JSON.stringify(request)}${ | ||
| attributes | ||
| };\n`; | ||
| const phaseKeyword = ImportPhaseUtils.isDefer(phase) ? "defer " : ""; | ||
| content = `import ${phaseKeyword}* as ${identifier} from ${JSON.stringify( | ||
| request | ||
| )}${attributes};\n`; | ||
| } else if (imported.length === 0) { | ||
| // just import, no use | ||
| content = `import ${JSON.stringify(request)}${attributes};\n`; |
| /** | ||
| * Returns the unique identifier used to reference this module. | ||
| * @returns {string} a unique identifier of the module | ||
| */ | ||
| identifier() { | ||
| return `external ${this._resolveExternalType(this.externalType)} ${JSON.stringify(this.request)}`; | ||
| return `external ${this._resolveExternalType( | ||
| this.externalType | ||
| )} ${JSON.stringify(this.request)}`; | ||
| } |
| --- | ||
| "webpack": minor | ||
| --- | ||
|
|
||
| Preserve `defer` / `source` import phase keywords on external dependencies in ESM output, the same way import attributes are preserved. Static `import defer * as ns from "x"` and `import source v from "x"` against a `module` external are now emitted as native `import defer * as …` / `import source … from …` statements at the top of the bundle, and dynamic `import.defer("x")` / `import.source("x")` against an `import` external is emitted as `import.defer(…)` / `import.source(…)`. |
- ExternalModule.identifier() and updateHash() now include `phase` and `attributes` from `dependencyMeta`, so the same external imported twice with different phases no longer collapses into a single ExternalModule and silently loses one phase keyword. - Force `imported = true` (namespace form) in getSourceForModuleExternal when phase is `defer`, since `import defer` only has a valid namespace native syntax — concatenation can no longer silently drop the keyword by narrowing the import shape. - Drop the duplicated `_isLegacyAssert && _isLegacyAssert` ternary in ModuleExternalInitFragment.getContent(). - Run prettier/eslint over the touched lib files. Adds a regression test under externals/phase-imports-esm: importing ext-both-phases with both `import defer * as` and `import source` must emit both native statements in the bundle. https://claude.ai/code/session_01BKSJuH4X5zjxsszYXHRqy2
Types CoverageCoverage after merging claude/test-deferred-imports-TR1Md into main will be
Coverage Report
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
…rnals Webpack 5.107 preserves the defer and source import phase keywords on external dependencies the same way it preserves import attributes. Adds subsections under externalsType.module and externalsType.import showing the static and dynamic forms emitted in the output bundle. Refs: webpack/webpack#20934
…rnals (#8239) * docs(externals): document defer/source phase preservation in ESM externals Webpack 5.107 preserves the defer and source import phase keywords on external dependencies the same way it preserves import attributes. Adds subsections under externalsType.module and externalsType.import showing the static and dynamic forms emitted in the output bundle. Refs: webpack/webpack#20934 * docs(externals): fix broken anchor for import attributes link The reference link pointed to /configuration/module/#ruleparserimportattributes, which does not exist (the fragment-check on PR #8239 failed). Webpack docs have no dedicated "import attributes" page; the closest internal anchor is #rulewith, but that documents Rule.with as a matcher rather than import attributes themselves. Replace the internal link with the TC39 proposal URL so readers can learn what import attributes are.
Summary
Phase imports (
import defer * as ns from "x",import source x from "x",import.defer("x"),import.source("x")) are now respected on external dependencies the same wayimport attributesalready are.moduleexternals in ESM output,ModuleExternalInitFragmentnow emitsimport defer * as <id> from "x"for namespace defer imports andimport source <id> from "x"for single-default source imports — instead of stripping the phase keyword.importexternals,getSourceForImportExternalemitsimport.defer("x")/import.source("x")when theimportFunctionNameis the default"import"and a phase is set on the dependency.ImportDependencyMetanow carriesphasealongsideattributesandexternalType.ExternalModule.identifier()andupdateHash()includephaseandattributes, so the same external imported with two different phases (or attribute sets) no longer collapses into a singleExternalModuleand silently drops one phase.getSourceForModuleExternalkeepsimported = true(namespace form) when the dependency phase isdefer, sinceimport deferonly has a valid namespace native syntax.Three
configCasescover the new behaviour:externals/phase-imports-defer— runtime case (var/promiseexternals, nooutput.module): asserts the optimized deferred runtime helper (__webpack_require__.zO(...)) is used for sync externals and skipped for already-async ones.externals/phase-imports-source— same shape forsourcephase against runtime-form externals.externals/phase-imports-esm— ESM output case (output.module: true) covering all three external types —module,import,module-import— at static and dynamic sites, plus a regression test for the same external imported with two different phases.What kind of change does this PR introduce?
feat
Did you add tests for your changes?
yes —
test/configCases/externals/phase-imports-defer/,test/configCases/externals/phase-imports-source/,test/configCases/externals/phase-imports-esm/.Does this PR introduce a breaking change?
no — externals that were not imported with a phase keyword are emitted exactly as before. The added
phase/attributesmaterial inExternalModule.identifier()only changes the identifier when those fields are non-default, so caches built before this change for non-phase externals stay valid.If relevant, what needs to be documented…
n/a — the existing externals docs already describe
import attributesflowing through unchanged; phase imports now flow through the same way and don't need a separate doc entry.Use of AI
Claude Code was used to draft the implementation and tests under human review.
https://claude.ai/code/session_01BKSJuH4X5zjxsszYXHRqy2