Skip to content

Commit c256cbb

Browse files
fix: name chunk imports in the chunk loader
Baking a literal `import("./chunk.<hash>.js")` at every dynamic import site put the imported chunk's content hash into the importing module, so a change to a lazily loaded route invalidated every chunk on the path up to the initial wave — and the chunk id to filename map in the runtime chunk stored the same hashes a second time. The ESM chunk loader now holds the literal specifiers itself, keyed by chunk id, and `.f.j` imports one instead of building a url from `.p + .u(id)`. Call sites go back to `ensureChunk(id)`, so no importing module carries a chunk hash, the id to filename map is not emitted where the loader names everything, and the output stays walkable: every emitted chunk is still reached by a literal specifier a foreign bundler or a preload scanner can follow. `__webpack_require__.ei` is gone with the call-site form. Two bailout reasons moved with the decision: an output that cannot use a native `import()` is reported where the imports are generated, and a runtime reading its chunks another way reports from the chunk loader that will not serve it. Closes #22056
1 parent 3ca6196 commit c256cbb

70 files changed

Lines changed: 663 additions & 631 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Name chunk imports in the chunk loader, not at each import site.

‎lib/APIPlugin.js‎

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const getConcatenatedModule = memoize(() =>
4242
*/
4343
/** @import Compilation from "./Compilation" */
4444
/** @import ChunkGraph from "./ChunkGraph" */
45+
/** @import { RuntimeSpec } from "./util/runtime" */
4546
/** @import JavascriptParser, { Range } from "./javascript/JavascriptParser" */
4647

4748
// Modules that reassign `__webpack_public_path__` at runtime, by compilation. A baked
@@ -79,17 +80,30 @@ const usesRuntimePublicPathOverride = (compilation) =>
7980
const overriddenRuntimes = new WeakMap();
8081

8182
/**
82-
* Whether the public path is reassigned in any runtime `module` belongs to. Falls back
83-
* to the whole compilation when there is no chunk graph to place the module in.
83+
* Whether the public path is reassigned in any runtime the reference belongs to,
84+
* named either by the module emitting it or by `runtime` directly — a caller holding
85+
* a chunk rather than a module has only the latter. Falls back to the whole
86+
* compilation when neither can place it.
8487
* @param {Compilation} compilation compilation
8588
* @param {ChunkGraph=} chunkGraph the chunk graph
8689
* @param {Module=} module the module a reference is emitted into
87-
* @returns {boolean} true when a reassignment can reach this module's runtimes
90+
* @param {RuntimeSpec=} runtime the runtime it is emitted for, where no module names it
91+
* @returns {boolean} true when a reassignment can reach those runtimes
8892
*/
89-
const runtimeUsesPublicPathOverride = (compilation, chunkGraph, module) => {
93+
const runtimeUsesPublicPathOverride = (
94+
compilation,
95+
chunkGraph,
96+
module,
97+
runtime
98+
) => {
9099
const modules = runtimePublicPathOverride.get(compilation);
91100
if (modules === undefined) return false;
92-
if (chunkGraph === undefined || module === undefined) return true;
101+
if (
102+
chunkGraph === undefined ||
103+
(module === undefined && runtime === undefined)
104+
) {
105+
return true;
106+
}
93107
// Keyed by the chunk graph, not the compilation: `executeModule` asks with a
94108
// throw-away one whose answer must not outlive it.
95109
let runtimes = overriddenRuntimes.get(chunkGraph);
@@ -115,8 +129,10 @@ const runtimeUsesPublicPathOverride = (compilation, chunkGraph, module) => {
115129
if (overridden.size === 0) return false;
116130
/** @type {Set<string>} */
117131
const own = new Set();
118-
for (const runtime of chunkGraph.getModuleRuntimes(module)) {
119-
forEachRuntime(runtime, (key) => {
132+
const reached =
133+
module === undefined ? [runtime] : chunkGraph.getModuleRuntimes(module);
134+
for (const each of reached) {
135+
forEachRuntime(each, (key) => {
120136
own.add(/** @type {string} */ (key));
121137
});
122138
}

‎lib/ContextModule.js‎

Lines changed: 7 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,60 +1215,11 @@ module.exports = webpackAsyncContext;`;
12151215
}
12161216
const shortMode = hasNoChunk && hasNoModuleDeferred && !hasFakeMap;
12171217
// Sorted before anything is keyed by request, so two items sharing one leave the
1218-
// same last writer in the map and in the loaders beside it.
1218+
// same last writer in the map.
12191219
const sortedItems = items.sort((a, b) => {
12201220
if (a.userRequest === b.userRequest) return 0;
12211221
return a.userRequest < b.userRequest ? -1 : 1;
12221222
});
1223-
let needEnsureChunk = !hasNoChunk;
1224-
/** @type {Map<string, string[]> | undefined} */
1225-
let loaders;
1226-
// Only module output can name a chunk in an `import()`, so nothing else pays for
1227-
// building the loaders just to throw them away.
1228-
if (!hasNoChunk && runtimeTemplate.isModule()) {
1229-
/** @type {Map<string, string[]>} */
1230-
const perRequest = new Map();
1231-
let hasAnalyzable = false;
1232-
let hasFallback = false;
1233-
/**
1234-
* @param {Chunk} chunk one chunk a request needs
1235-
* @returns {string} a thunk loading it, by literal name where the chunk has one
1236-
*/
1237-
const loaderFor = (chunk) => {
1238-
// `.ei` always imports, where `.e` skips a chunk already loaded — the same
1239-
// filter `blockPromise` applies before reaching for it.
1240-
const analyzable =
1241-
chunk.hasRuntime() || chunk.id === null
1242-
? null
1243-
: runtimeTemplate.analyzableChunkImport(
1244-
chunk,
1245-
"",
1246-
runtimeRequirements,
1247-
this,
1248-
chunkGraph
1249-
);
1250-
if (analyzable !== null) {
1251-
hasAnalyzable = true;
1252-
return runtimeTemplate.returningFunction(analyzable);
1253-
}
1254-
hasFallback = true;
1255-
return runtimeTemplate.returningFunction(
1256-
`${RuntimeGlobals.ensureChunk}(${JSON.stringify(chunk.id)})`
1257-
);
1258-
};
1259-
for (const item of sortedItems) {
1260-
perRequest.set(
1261-
item.userRequest,
1262-
/** @type {Chunk[]} */ (item.chunks).map(loaderFor)
1263-
);
1264-
}
1265-
// Nothing gained when no chunk can be spelled out, and the plain id map is
1266-
// smaller, so the loaders are only written when at least one names its chunk.
1267-
if (hasAnalyzable) {
1268-
loaders = perRequest;
1269-
needEnsureChunk = hasFallback;
1270-
}
1271-
}
12721223
/** @type {Record<string, ModuleId | (ModuleId | FakeMapType | ChunkId[] | (ModuleId[] | undefined))[]>} */
12731224
const map = Object.create(null);
12741225
for (const item of sortedItems) {
@@ -1302,16 +1253,11 @@ module.exports = webpackAsyncContext;`;
13021253
const asyncDepsPosition = hasNoChunk ? chunksPosition : chunksPosition + 1;
13031254
const requestPrefix = hasNoChunk
13041255
? "Promise.resolve()"
1305-
: loaders !== undefined
1306-
? hasMultipleOrNoChunks
1307-
? `Promise.all(ids[${chunksPosition}].map(${runtimeTemplate.returningFunction("load()", "load")}))`
1308-
: `ids[${chunksPosition}][0]()`
1309-
: hasMultipleOrNoChunks
1310-
? `Promise.all(ids[${chunksPosition}].map(${RuntimeGlobals.ensureChunk}))`
1311-
: `${RuntimeGlobals.ensureChunk}(ids[${chunksPosition}][0])`;
1312-
// Only the id map reads it; nothing loads a chunk when there is none to load, and
1313-
// a map of loaders spells out every one it could reach for.
1314-
if (needEnsureChunk) {
1256+
: hasMultipleOrNoChunks
1257+
? `Promise.all(ids[${chunksPosition}].map(${RuntimeGlobals.ensureChunk}))`
1258+
: `${RuntimeGlobals.ensureChunk}(ids[${chunksPosition}][0])`;
1259+
// Nothing loads a chunk when there is none to load.
1260+
if (!hasNoChunk) {
13151261
runtimeRequirements.add(RuntimeGlobals.ensureChunk);
13161262
}
13171263
const returnModuleObject = this.getReturnModuleObjectSource(
@@ -1355,26 +1301,7 @@ function webpackAsyncContext(req) {
13551301
return ${requestPrefix}.then(${runtimeTemplate.returningFunction(returnModuleObject)});
13561302
}`;
13571303

1358-
// A loader is source, not data, so the map holding one is written out rather than
1359-
// serialized — one entry per line, keeping the request next to what loads it.
1360-
const mapSource =
1361-
loaders === undefined
1362-
? JSON.stringify(map, null, "\t")
1363-
: `{\n${Object.keys(map)
1364-
.map((request) => {
1365-
const slots =
1366-
/** @type {(ModuleId | FakeMapType | ChunkId[] | (ModuleId[] | undefined))[]} */
1367-
(map[request]).map((slot, index) =>
1368-
index === chunksPosition
1369-
? `[${/** @type {string[]} */ (loaders.get(request)).join(", ")}]`
1370-
: // An absent `asyncDeps` slot is `null` to `JSON.stringify`.
1371-
slot === undefined
1372-
? "null"
1373-
: JSON.stringify(slot)
1374-
);
1375-
return `\t${JSON.stringify(request)}: [${slots.join(", ")}]`;
1376-
})
1377-
.join(",\n")}\n}`;
1304+
const mapSource = JSON.stringify(map, null, "\t");
13781305

13791306
return `${cst} map = ${mapSource};
13801307
${webpackAsyncContext}

‎lib/RuntimeGlobals.js‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,6 @@ module.exports.amdDefine = "__webpack_require__.amdD";
1515
*/
1616
module.exports.amdOptions = "__webpack_require__.amdO";
1717

18-
/**
19-
* analyzable single-chunk `import()` that keeps `ensureChunk` timing and deduplication:
20-
* resolves immediately for an installed chunk, else runs the literal import and installs it
21-
* Signature: (chunkId, () => Promise) => Promise
22-
*/
23-
module.exports.analyzableChunkImport = "__webpack_require__.ei";
24-
2518
/**
2619
* Creates an async module. The body function must be a async function.
2720
* "module.exports" will be decorated with an AsyncModulePromise.

0 commit comments

Comments
 (0)