fix: entryChunk depends on the runtimeChunk hash

This commit is contained in:
xiaoxiaojx 2025-08-11 00:04:08 +08:00
parent 67379fe33d
commit 5a72fb9bec
10 changed files with 166 additions and 56 deletions

View File

@ -8,13 +8,16 @@
const { ConcatSource } = require("webpack-sources");
const { HotUpdateChunk, RuntimeGlobals } = require("..");
const Template = require("../Template");
const {
createChunkHashHandler,
getChunkInfo
} = require("../javascript/ChunkFormatHelpers");
const { getAllChunks } = require("../javascript/ChunkHelpers");
const {
chunkHasJs,
getChunkFilenameTemplate,
getCompilationHooks
} = require("../javascript/JavascriptModulesPlugin");
const { updateHashForEntryStartup } = require("../javascript/StartupHelpers");
const { getUndoPath } = require("../util/identifier");
/** @typedef {import("webpack-sources").Source} Source */
@ -27,28 +30,6 @@ const { getUndoPath } = require("../util/identifier");
/** @typedef {import("../Module")} Module */
/** @typedef {import("../javascript/JavascriptModulesPlugin").RenderContext} RenderContext */
/**
* Gets information about a chunk including its entries and runtime chunk
* @param {Chunk} chunk The chunk to get information for
* @param {ChunkGraph} chunkGraph The chunk graph containing the chunk
* @returns {{entries: Array<[Module, Entrypoint | undefined]>, runtimeChunk: Chunk|null}} Object containing chunk entries and runtime chunk
*/
function getChunkInfo(chunk, chunkGraph) {
const entries = [
...chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)
];
const runtimeChunk =
entries.length > 0
? /** @type {Entrypoint[][]} */
(entries)[0][1].getRuntimeChunk()
: null;
return {
entries,
runtimeChunk
};
}
/**
* @param {Compilation} compilation the compilation instance
* @param {Chunk} chunk the chunk
@ -287,19 +268,7 @@ class ModuleChunkFormatPlugin {
}
return source;
});
hooks.chunkHash.tap(PLUGIN_NAME, (chunk, hash, { chunkGraph }) => {
if (chunk.hasRuntime()) return;
const { entries, runtimeChunk } = getChunkInfo(chunk, chunkGraph);
hash.update(PLUGIN_NAME);
hash.update("1");
if (runtimeChunk && runtimeChunk.hash) {
// Any change to runtimeChunk should trigger a hash update,
// we shouldn't depend on or inspect its internal implementation.
// import __webpack_require__ from "./runtime-main.e9400aee33633a3973bd.js";
hash.update(runtimeChunk.hash);
}
updateHashForEntryStartup(hash, chunkGraph, entries, chunk);
});
hooks.chunkHash.tap(PLUGIN_NAME, createChunkHashHandler(PLUGIN_NAME));
});
}
}

View File

@ -0,0 +1,70 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Natsu @xiaoxiaojx
*/
"use strict";
const { updateHashForEntryStartup } = require("./StartupHelpers");
/** @typedef {import("../ChunkGraph")} ChunkGraph */
/** @typedef {import("../Module")} Module */
/** @typedef {import("../Chunk")} Chunk */
/** @typedef {import("../Entrypoint")} Entrypoint */
/** @typedef {import("../util/Hash")} Hash */
/** @typedef {import("../Compilation").ChunkHashContext} ChunkHashContext */
/**
* Gets information about a chunk including its entries and runtime chunk
* @param {Chunk} chunk The chunk to get information for
* @param {ChunkGraph} chunkGraph The chunk graph containing the chunk
* @returns {{entries: Array<[Module, Entrypoint | undefined]>, runtimeChunk: Chunk|null}} Object containing chunk entries and runtime chunk
*/
function getChunkInfo(chunk, chunkGraph) {
const entries = [
...chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)
];
const runtimeChunk =
entries.length > 0
? /** @type {Entrypoint[][]} */
(entries)[0][1].getRuntimeChunk()
: null;
return {
entries,
runtimeChunk
};
}
/**
* Creates a chunk hash handler
* @param {string} name The name of the chunk
* @returns {(chunk: Chunk, hash: Hash, { chunkGraph }: ChunkHashContext) => void} The chunk hash handler
*/
function createChunkHashHandler(name) {
/**
* @param {Chunk} chunk The chunk to get information for
* @param {Hash} hash The hash to update
* @param {ChunkHashContext} chunkHashContext The chunk hash context
* @returns {void}
*/
return (chunk, hash, { chunkGraph }) => {
if (chunk.hasRuntime()) return;
const { entries, runtimeChunk } = getChunkInfo(chunk, chunkGraph);
hash.update(name);
hash.update("1");
if (runtimeChunk && runtimeChunk.hash) {
// https://github.com/webpack/webpack/issues/19439
// Any change to runtimeChunk should trigger a hash update,
// we shouldn't depend on or inspect its internal implementation.
// import __webpack_require__ from "./runtime-main.e9400aee33633a3973bd.js";
hash.update(runtimeChunk.hash);
}
updateHashForEntryStartup(hash, chunkGraph, entries, chunk);
};
}
module.exports = {
createChunkHashHandler,
getChunkInfo
};

View File

@ -9,14 +9,15 @@ const { ConcatSource, RawSource } = require("webpack-sources");
const RuntimeGlobals = require("../RuntimeGlobals");
const Template = require("../Template");
const { getUndoPath } = require("../util/identifier");
const {
createChunkHashHandler,
getChunkInfo
} = require("./ChunkFormatHelpers");
const {
getChunkFilenameTemplate,
getCompilationHooks
} = require("./JavascriptModulesPlugin");
const {
generateEntryStartup,
updateHashForEntryStartup
} = require("./StartupHelpers");
const { generateEntryStartup } = require("./StartupHelpers");
/** @typedef {import("../Chunk")} Chunk */
/** @typedef {import("../Compiler")} Compiler */
@ -59,13 +60,8 @@ class CommonJsChunkFormatPlugin {
Template.renderChunkRuntimeModules(runtimeModules, renderContext)
);
}
const entries = [
...chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)
];
if (entries.length > 0) {
const runtimeChunk =
/** @type {Entrypoint} */
(entries[0][1]).getRuntimeChunk();
const { entries, runtimeChunk } = getChunkInfo(chunk, chunkGraph);
if (runtimeChunk) {
const currentOutputName = compilation
.getPath(
getChunkFilenameTemplate(chunk, compilation.outputOptions),
@ -144,15 +140,8 @@ class CommonJsChunkFormatPlugin {
}
return source;
});
hooks.chunkHash.tap(PLUGIN_NAME, (chunk, hash, { chunkGraph }) => {
if (chunk.hasRuntime()) return;
hash.update(PLUGIN_NAME);
hash.update("1");
const entries = [
...chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)
];
updateHashForEntryStartup(hash, chunkGraph, entries, chunk);
});
hooks.chunkHash.tap(PLUGIN_NAME, createChunkHashHandler(PLUGIN_NAME));
});
}
}

View File

@ -0,0 +1 @@
export var value = "0";

View File

@ -0,0 +1,2 @@
export var value = "0";

View File

@ -0,0 +1,36 @@
import { react } from "./react";
it("should work where an ESM entryChunk depends on the runtimeChunk", async function (done) {
const mainChunk = STATS_JSON.chunks.find((chunk) => chunk.id === "main");
const runtimeChunk = STATS_JSON.chunks.find((chunk) => chunk.id === "runtime-main");
const dynamic1Chunk = STATS_JSON.chunks.find((chunk) => chunk.id === "dynamic-1_js");
const dynamic2Chunk = STATS_JSON.chunks.find((chunk) => chunk.id === "dynamic-2_js");
const reactChunk = STATS_JSON.chunks.find((chunk) => chunk.id === "react");
expect(mainChunk).toBeDefined();
expect(react).toBe("react");
await import('./dynamic-1').then(console.log)
await import('./dynamic-2').then(console.log)
if (WATCH_STEP === "0") {
STATE.mainChunkHash = mainChunk.hash;
STATE.dynamic1ChunkHash = dynamic1Chunk.hash;
STATE.dynamic2ChunkHash = dynamic2Chunk.hash;
STATE.runtimeChunkHash = runtimeChunk.hash;
STATE.reactChunkHash = reactChunk.hash;
} else {
// async dynamic2Chunk needn't be updated
expect(dynamic2Chunk.hash).toBe(STATE.dynamic2ChunkHash);
// initial reactChunk is needn't be updated
expect(reactChunk.hash).toBe(STATE.reactChunkHash);
// initial mainChunk need to be updated
expect(mainChunk.hash).not.toBe(STATE.mainChunkHash);
// async dynamic1Chunk need to be updated
expect(dynamic1Chunk.hash).not.toBe(STATE.dynamic1ChunkHash);
// runtime runtimeChunk need to be updated
expect(runtimeChunk.hash).not.toBe(STATE.runtimeChunkHash);
}
done()
});

View File

@ -0,0 +1 @@
export const react = "react";

View File

@ -0,0 +1,3 @@
export var value = "1";
import("./dynamic-2").then(console.log)

View File

@ -0,0 +1,5 @@
"use strict";
module.exports = {
bundlePath: /^main\./
};

View File

@ -0,0 +1,34 @@
"use strict";
/** @type {import("../../../../").Configuration} */
module.exports = {
devtool: false,
mode: "development",
target: "node",
optimization: {
minimize: false,
splitChunks: {
chunks: "all",
minSize: 1,
cacheGroups: {
react: {
test: /react/,
name: "react",
chunks: "all",
priority: 100
}
}
},
runtimeChunk: {
/**
* @param {import("../../../../").Entrypoint} entrypoint The entrypoint to generate runtime chunk name for
* @returns {string} The generated runtime chunk name
*/
name: (entrypoint) => `runtime-${entrypoint.name}`
}
},
output: {
filename: "[name].[contenthash].js",
chunkFilename: "[name].[contenthash].js"
}
};