From 5a72fb9bec95c1b67d1623fbbe0247803254dfcd Mon Sep 17 00:00:00 2001 From: xiaoxiaojx Date: Mon, 11 Aug 2025 00:04:08 +0800 Subject: [PATCH] fix: entryChunk depends on the runtimeChunk hash --- lib/esm/ModuleChunkFormatPlugin.js | 41 ++--------- lib/javascript/ChunkFormatHelpers.js | 70 +++++++++++++++++++ lib/javascript/CommonJsChunkFormatPlugin.js | 29 +++----- .../node-async-chunks-hmr/0/dynamic-1.js | 1 + .../node-async-chunks-hmr/0/dynamic-2.js | 2 + .../chunks/node-async-chunks-hmr/0/index.js | 36 ++++++++++ .../chunks/node-async-chunks-hmr/0/react.js | 1 + .../node-async-chunks-hmr/1/dynamic-1.js | 3 + .../node-async-chunks-hmr/test.config.js | 5 ++ .../node-async-chunks-hmr/webpack.config.js | 34 +++++++++ 10 files changed, 166 insertions(+), 56 deletions(-) create mode 100644 lib/javascript/ChunkFormatHelpers.js create mode 100644 test/watchCases/chunks/node-async-chunks-hmr/0/dynamic-1.js create mode 100644 test/watchCases/chunks/node-async-chunks-hmr/0/dynamic-2.js create mode 100644 test/watchCases/chunks/node-async-chunks-hmr/0/index.js create mode 100644 test/watchCases/chunks/node-async-chunks-hmr/0/react.js create mode 100644 test/watchCases/chunks/node-async-chunks-hmr/1/dynamic-1.js create mode 100644 test/watchCases/chunks/node-async-chunks-hmr/test.config.js create mode 100644 test/watchCases/chunks/node-async-chunks-hmr/webpack.config.js diff --git a/lib/esm/ModuleChunkFormatPlugin.js b/lib/esm/ModuleChunkFormatPlugin.js index 33c74d60e..d50776d80 100644 --- a/lib/esm/ModuleChunkFormatPlugin.js +++ b/lib/esm/ModuleChunkFormatPlugin.js @@ -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)); }); } } diff --git a/lib/javascript/ChunkFormatHelpers.js b/lib/javascript/ChunkFormatHelpers.js new file mode 100644 index 000000000..24b05a9dd --- /dev/null +++ b/lib/javascript/ChunkFormatHelpers.js @@ -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 +}; diff --git a/lib/javascript/CommonJsChunkFormatPlugin.js b/lib/javascript/CommonJsChunkFormatPlugin.js index 728b620a1..a0bbe5f93 100644 --- a/lib/javascript/CommonJsChunkFormatPlugin.js +++ b/lib/javascript/CommonJsChunkFormatPlugin.js @@ -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)); }); } } diff --git a/test/watchCases/chunks/node-async-chunks-hmr/0/dynamic-1.js b/test/watchCases/chunks/node-async-chunks-hmr/0/dynamic-1.js new file mode 100644 index 000000000..92f3c109a --- /dev/null +++ b/test/watchCases/chunks/node-async-chunks-hmr/0/dynamic-1.js @@ -0,0 +1 @@ +export var value = "0"; \ No newline at end of file diff --git a/test/watchCases/chunks/node-async-chunks-hmr/0/dynamic-2.js b/test/watchCases/chunks/node-async-chunks-hmr/0/dynamic-2.js new file mode 100644 index 000000000..034acec95 --- /dev/null +++ b/test/watchCases/chunks/node-async-chunks-hmr/0/dynamic-2.js @@ -0,0 +1,2 @@ +export var value = "0"; + diff --git a/test/watchCases/chunks/node-async-chunks-hmr/0/index.js b/test/watchCases/chunks/node-async-chunks-hmr/0/index.js new file mode 100644 index 000000000..4ff87308c --- /dev/null +++ b/test/watchCases/chunks/node-async-chunks-hmr/0/index.js @@ -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() +}); diff --git a/test/watchCases/chunks/node-async-chunks-hmr/0/react.js b/test/watchCases/chunks/node-async-chunks-hmr/0/react.js new file mode 100644 index 000000000..59fb4cdbd --- /dev/null +++ b/test/watchCases/chunks/node-async-chunks-hmr/0/react.js @@ -0,0 +1 @@ +export const react = "react"; \ No newline at end of file diff --git a/test/watchCases/chunks/node-async-chunks-hmr/1/dynamic-1.js b/test/watchCases/chunks/node-async-chunks-hmr/1/dynamic-1.js new file mode 100644 index 000000000..d0565da58 --- /dev/null +++ b/test/watchCases/chunks/node-async-chunks-hmr/1/dynamic-1.js @@ -0,0 +1,3 @@ +export var value = "1"; + +import("./dynamic-2").then(console.log) \ No newline at end of file diff --git a/test/watchCases/chunks/node-async-chunks-hmr/test.config.js b/test/watchCases/chunks/node-async-chunks-hmr/test.config.js new file mode 100644 index 000000000..ab9f34700 --- /dev/null +++ b/test/watchCases/chunks/node-async-chunks-hmr/test.config.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + bundlePath: /^main\./ +}; diff --git a/test/watchCases/chunks/node-async-chunks-hmr/webpack.config.js b/test/watchCases/chunks/node-async-chunks-hmr/webpack.config.js new file mode 100644 index 000000000..6532964b6 --- /dev/null +++ b/test/watchCases/chunks/node-async-chunks-hmr/webpack.config.js @@ -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" + } +};