Skip to content

Commit 927d87d

Browse files
fix: keep the loader's url fallback for ids its map cannot hold
A chunk carrying a runtime of its own is reached by a static import and is never named in the map, so a runtime that can reach one keeps building a url from the chunk id; a hot update can force-load an id this build never saw, so HMR keeps it too. Everything else imports the literal the map holds and emits no id table. Naming an initial chunk there was the other half of this: it made that chunk's hash and the loader's chase each other, and the pair settled with the entry shipping new bytes under the content hash it already had.
1 parent c256cbb commit 927d87d

15 files changed

Lines changed: 47 additions & 27 deletions

File tree

‎lib/RuntimeTemplate.js‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3769,6 +3769,22 @@ class RuntimeTemplate {
37693769
}
37703770
}
37713771

3772+
/**
3773+
* Whether the loader still needs to build a url from a chunk id: a chunk carrying
3774+
* a runtime of its own is reached by a static import and so is never named in the
3775+
* map, and only a runtime that can reach one has to keep the fallback.
3776+
* @param {Chunk} runtimeChunk the chunk holding the chunk loader
3777+
* @param {ChunkGraph} chunkGraph the chunk graph
3778+
* @returns {boolean} true when an id the map does not hold can reach the loader
3779+
*/
3780+
chunkImportsNeedRuntimeUrl(runtimeChunk, chunkGraph) {
3781+
for (const chunk of runtimeChunk.getAllReferencedChunks()) {
3782+
if (chunk === runtimeChunk || !chunk.hasRuntime()) continue;
3783+
if (JavascriptModulesPlugin.chunkHasJs(chunk, chunkGraph)) return true;
3784+
}
3785+
return false;
3786+
}
3787+
37723788
/**
37733789
* A literal `import()` specifier for every javascript chunk a runtime loads on
37743790
* demand, keyed by chunk id, or `null` when one of them cannot be named here. The
@@ -3798,13 +3814,14 @@ class RuntimeTemplate {
37983814
) {
37993815
return null;
38003816
}
3801-
// An initial chunk is installed before the loader runs and a chunk carrying a
3802-
// runtime is reached by a static import, so neither is ever looked up here.
3817+
// An initial chunk and one carrying a runtime are both reached by a static
3818+
// import, so naming either here would only make two hashes chase each other.
38033819
const installed = new Set(runtimeChunk.getAllInitialChunks());
38043820
/** @type {Chunk[]} */
38053821
const chunks = [];
38063822
for (const chunk of runtimeChunk.getAllReferencedChunks()) {
38073823
if (
3824+
chunk === runtimeChunk ||
38083825
chunk.hasRuntime() ||
38093826
installed.has(chunk) ||
38103827
!JavascriptModulesPlugin.chunkHasJs(chunk, chunkGraph)

‎lib/esm/ModuleChunkLoadingPlugin.js‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,15 @@ class ModuleChunkLoadingPlugin {
144144
);
145145
return;
146146
}
147+
147148
// Asked as the runtime module asks, so the two always agree on whether
148149
// a chunk id is ever turned into a url here.
150+
const { runtimeTemplate } = compilation;
149151
if (
150-
compilation.runtimeTemplate.analyzableChunkImports(
151-
chunk,
152-
chunkGraph,
153-
set
154-
) !== null
152+
runtimeTemplate.analyzableChunkImports(chunk, chunkGraph, set) !==
153+
null &&
154+
!set.has(RuntimeGlobals.hmrDownloadUpdateHandlers) &&
155+
!runtimeTemplate.chunkImportsNeedRuntimeUrl(chunk, chunkGraph)
155156
) {
156157
return;
157158
}

‎lib/esm/ModuleChunkLoadingRuntimeModule.js‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,16 @@ class ModuleChunkLoadingRuntimeModule extends RuntimeModule {
195195
? `${RuntimeGlobals.publicPath} + ${RuntimeGlobals.getChunkScriptFilename}(${id})`
196196
: `chunkUrls[${id}]()`;
197197
const runtimeImport = `${importFunctionName}(${chunkImportBase} + ${RuntimeGlobals.getChunkScriptFilename}(chunkId))`;
198-
// A hot update can force-load a chunk id this build never saw, which no baked
199-
// map holds; `getChunkScriptFilename` is asked for alongside the hot handlers.
198+
// A hot update can force-load an id this build never saw, and a chunk with a
199+
// runtime of its own is never named, so only those keep the url form.
200+
const withRuntimeUrl =
201+
chunkImports === null ||
202+
withHmr ||
203+
runtimeTemplate.chunkImportsNeedRuntimeUrl(chunk, chunkGraph);
200204
const chunkImport =
201205
chunkImports === null
202206
? runtimeImport
203-
: withHmr
207+
: withRuntimeUrl
204208
? `(${RuntimeGlobals.hasOwnProperty}(chunkImports, chunkId) ? chunkImports[chunkId]() : ${runtimeImport})`
205209
: "chunkImports[chunkId]()";
206210
return Template.asString([

‎test/configCases/analyzable/block-origin-modules/index.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ it("should bake the import even though the block names no module of its own", ()
4242
const region = bundle.slice(start, bundle.indexOf("};", start));
4343

4444
// One per emitter: `require.ensure`, AMD `require([...])` and the lazy-once context.
45-
expect(region.split("import(")).toHaveLength(4);
45+
for (const name of ["ensured", "amd_js", "ctx"]) {
46+
expect([name, region.includes(name)]).toEqual([name, true]);
47+
}
4648
expect(bundle).toContain(`${"__webpack_require__"}.e(`);
4749
});

‎test/configCases/analyzable/context-module/defer-entry.js‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,4 @@ it("should keep the loaders ahead of the deferred slot", () => {
2626
expect(bundle).toContain(`import("./${__NAME__}-defer_async_js.mjs")`);
2727
expect(bundle).toContain("], null]");
2828
expect(bundle).toContain(`${"__webpack_require__"}.e =`);
29-
expect(bundle).not.toContain(`${"__webpack_require__"}.u =`);
3029
});

‎test/configCases/analyzable/context-module/fake-entry.js‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,4 @@ it("should keep the loaders behind the fake-map slot", () => {
2424
// The exports type sits at 1, so the loaders moved one along.
2525
expect(bundle).toContain("return ids[2][0]()");
2626
expect(bundle).toContain(`${"__webpack_require__"}.e =`);
27-
expect(bundle).not.toContain(`${"__webpack_require__"}.u =`);
2827
});

‎test/configCases/analyzable/context-module/plain-entry.js‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,5 @@ it("should write a static import next to each request", () => {
2626
expect(bundle).toContain(`import("./${__NAME__}-locales_en_js.mjs")`);
2727
// A single chunk per request needs no `Promise.all` around it.
2828
expect(bundle).toContain(`return ${"__webpack_require__"}.e(ids[1][0])`);
29-
// The request map holds chunk ids, so the loader reads them — but it imports a
30-
// name from its own map, so no id is ever turned into a filename.
3129
expect(bundle).toContain(`${"__webpack_require__"}.e =`);
32-
expect(bundle).not.toContain(`${"__webpack_require__"}.u =`);
3330
});

‎test/configCases/analyzable/context-module/split-entry.js‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,4 @@ it("should write one static import per chunk of the request", () => {
2727
expect(bundle).toContain(`import("./${__NAME__}-split_de_js.mjs")`);
2828
expect(bundle).toContain(`Promise.all(ids[1].map(${"__webpack_require__"}.e))`);
2929
expect(bundle).toContain(`${"__webpack_require__"}.e =`);
30-
expect(bundle).not.toContain(`${"__webpack_require__"}.u =`);
3130
});

‎test/configCases/analyzable/css-chunk-stays-analyzable/index.js‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ it("should name that chunk with a specifier a foreign bundler can follow", () =>
1313
);
1414
expect(bundle).toContain(`${"chunkImports"} = {`);
1515
expect(bundle).toContain('import("./lazy-css.mjs")');
16-
// The name is in the loader's map, so no chunk id is turned into one.
17-
expect(bundle).not.toContain(`${"__webpack_require__"}.u =`);
1816
});
1917

2018
it("should bake the href of the css that chunk carries", () => {

‎test/configCases/analyzable/dynamic-import/index.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ it("should emit an analyzable literal import() for module output", () => {
2121
expect(bundle).toContain(importMap);
2222
expect(bundle).toContain('import("./dynamic.mjs")');
2323
expect(bundle).toContain(`${"__webpack_require__"}.e(`);
24-
// The runtime builds no name from a chunk id, so it ships no map of them.
24+
// Every chunk the loader can be asked for is named here, so it builds no url from
25+
// a chunk id and the table mapping one to a filename is not emitted.
2526
expect(bundle).not.toContain(`${"__webpack_require__"}.u =`);
2627
});

0 commit comments

Comments
 (0)