fix: fix the case where an ESM entryChunk depends on the runtimeChunk hash (#19570)
Github Actions / lint (push) Has been cancelled Details
Github Actions / validate-legacy-node (push) Has been cancelled Details
Github Actions / benchmark (1/4) (push) Has been cancelled Details
Github Actions / benchmark (2/4) (push) Has been cancelled Details
Github Actions / benchmark (3/4) (push) Has been cancelled Details
Github Actions / benchmark (4/4) (push) Has been cancelled Details
Github Actions / basic (push) Has been cancelled Details
Github Actions / unit (push) Has been cancelled Details
Github Actions / integration (10.x, macos-latest, a) (push) Has been cancelled Details
Github Actions / integration (10.x, macos-latest, b) (push) Has been cancelled Details
Github Actions / integration (10.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (10.x, ubuntu-latest, b) (push) Has been cancelled Details
Github Actions / integration (10.x, windows-latest, a) (push) Has been cancelled Details
Github Actions / integration (10.x, windows-latest, b) (push) Has been cancelled Details
Github Actions / integration (12.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (14.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (16.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (18.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (20.x, macos-latest, a) (push) Has been cancelled Details
Github Actions / integration (20.x, macos-latest, b) (push) Has been cancelled Details
Github Actions / integration (20.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (20.x, ubuntu-latest, b) (push) Has been cancelled Details
Github Actions / integration (20.x, windows-latest, a) (push) Has been cancelled Details
Github Actions / integration (20.x, windows-latest, b) (push) Has been cancelled Details
Github Actions / integration (22.x, macos-latest, a) (push) Has been cancelled Details
Github Actions / integration (22.x, macos-latest, b) (push) Has been cancelled Details
Github Actions / integration (22.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (22.x, ubuntu-latest, b) (push) Has been cancelled Details
Github Actions / integration (22.x, windows-latest, a) (push) Has been cancelled Details
Github Actions / integration (22.x, windows-latest, b) (push) Has been cancelled Details
Github Actions / integration (24.x, macos-latest, a) (push) Has been cancelled Details
Github Actions / integration (24.x, macos-latest, b) (push) Has been cancelled Details
Github Actions / integration (24.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (24.x, ubuntu-latest, b) (push) Has been cancelled Details
Github Actions / integration (24.x, windows-latest, a) (push) Has been cancelled Details
Github Actions / integration (24.x, windows-latest, b) (push) Has been cancelled Details
Github Actions / integration (lts/*, ubuntu-latest, a, 1) (push) Has been cancelled Details
Github Actions / integration (lts/*, ubuntu-latest, b, 1) (push) Has been cancelled Details
Update examples / examples (push) Has been cancelled Details

This commit is contained in:
Xiao 2025-05-30 20:30:09 +08:00 committed by GitHub
parent 1b34181550
commit e805dc1b09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 265 additions and 34 deletions

View File

@ -4366,16 +4366,21 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o
/** @type {Chunk[]} */
const unorderedRuntimeChunks = [];
/** @type {Chunk[]} */
const otherChunks = [];
const initialChunks = [];
/** @type {Chunk[]} */
const asyncChunks = [];
for (const c of this.chunks) {
if (c.hasRuntime()) {
unorderedRuntimeChunks.push(c);
} else if (c.canBeInitial()) {
initialChunks.push(c);
} else {
otherChunks.push(c);
asyncChunks.push(c);
}
}
unorderedRuntimeChunks.sort(byId);
otherChunks.sort(byId);
initialChunks.sort(byId);
asyncChunks.sort(byId);
/** @typedef {{ chunk: Chunk, referencedBy: RuntimeChunkInfo[], remaining: number }} RuntimeChunkInfo */
/** @type {Map<Chunk, RuntimeChunkInfo>} */
@ -4541,8 +4546,9 @@ This prevents using hashes of each other and should be avoided.`);
}
this.logger.timeAggregate("hashing: hash chunks");
};
for (const chunk of otherChunks) processChunk(chunk);
for (const chunk of asyncChunks) processChunk(chunk);
for (const chunk of runtimeChunks) processChunk(chunk);
for (const chunk of initialChunks) processChunk(chunk);
if (errors.length > 0) {
errors.sort(compareSelect(err => err.module, compareModulesByIdentifier));
for (const error of errors) {

View File

@ -19,8 +19,33 @@ const { updateHashForEntryStartup } = require("../javascript/StartupHelpers");
const { getUndoPath } = require("../util/identifier");
/** @typedef {import("../Chunk")} Chunk */
/** @typedef {import("../ChunkGraph")} ChunkGraph */
/** @typedef {import("../ChunkGroup")} ChunkGroup */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../Entrypoint")} Entrypoint */
/** @typedef {import("../Module")} Module */
/**
* 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 = Array.from(
chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)
);
const runtimeChunk =
entries.length > 0
? /** @type {Entrypoint[][]} */
(entries)[0][1].getRuntimeChunk()
: null;
return {
entries,
runtimeChunk
};
}
class ModuleChunkFormatPlugin {
/**
@ -76,13 +101,8 @@ class ModuleChunkFormatPlugin {
)
);
}
const entries = Array.from(
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),
@ -207,11 +227,15 @@ class ModuleChunkFormatPlugin {
"ModuleChunkFormatPlugin",
(chunk, hash, { chunkGraph, runtimeTemplate }) => {
if (chunk.hasRuntime()) return;
const { entries, runtimeChunk } = getChunkInfo(chunk, chunkGraph);
hash.update("ModuleChunkFormatPlugin");
hash.update("1");
const entries = Array.from(
chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)
);
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);
}
);

View File

@ -9,12 +9,14 @@ const path = require("path");
const fs = require("graceful-fs");
const vm = require("vm");
const rimraf = require("rimraf");
const { pathToFileURL, fileURLToPath } = require("url");
const checkArrayExpectation = require("./checkArrayExpectation");
const createLazyTestEnv = require("./helpers/createLazyTestEnv");
const { remove } = require("./helpers/remove");
const prepareOptions = require("./helpers/prepareOptions");
const deprecationTracking = require("./helpers/deprecationTracking");
const FakeDocument = require("./helpers/FakeDocument");
const asModule = require("./helpers/asModule");
/**
* @param {string} src src
@ -200,7 +202,7 @@ const describeCases = config => {
{
aggregateTimeout: 1000
},
(err, stats) => {
async (err, stats) => {
if (err) return compilationFinished(err);
if (!stats) {
return compilationFinished(
@ -273,29 +275,122 @@ const describeCases = config => {
document: new FakeDocument()
};
const baseModuleScope = {
console,
it: run.it,
beforeEach: _beforeEach,
afterEach: _afterEach,
expect,
jest,
STATS_JSON: jsonStats,
nsObj: m => {
Object.defineProperty(m, Symbol.toStringTag, {
value: "Module"
});
return m;
},
window: globalContext,
self: globalContext,
WATCH_STEP: run.name,
STATE: state
};
const esmCache = new Map();
const esmIdentifier = `${category.name}-${testName}`;
const esmContext = vm.createContext(baseModuleScope, {
name: "context for esm"
});
// ESM
const isModule =
options.experiments && options.experiments.outputModule;
/**
* @param {string} currentDirectory the current directory
* @param {TODO} module a module
* @param {string} currentDirectory The directory to resolve relative paths from
* @param {string} module The module to require
* @param {("unlinked"|"evaluated")} esmMode The mode for ESM module handling
* @returns {EXPECTED_ANY} required module
* @private
*/
function _require(currentDirectory, module) {
if (Array.isArray(module) || /^\.\.?\//.test(module)) {
function _require(currentDirectory, module, esmMode) {
if (/^\.\.?\//.test(module) || path.isAbsolute(module)) {
let fn;
let content;
let p;
if (Array.isArray(module)) {
p = path.join(currentDirectory, module[0]);
content = module
.map(arg => {
p = path.join(currentDirectory, arg);
return fs.readFileSync(p, "utf-8");
})
.join("\n");
} else {
p = path.join(currentDirectory, module);
content = fs.readFileSync(p, "utf-8");
const p = path.isAbsolute(module)
? module
: path.join(currentDirectory, module);
const content = fs.readFileSync(p, "utf-8");
if (isModule) {
if (!vm.SourceTextModule)
throw new Error(
"Running this test requires '--experimental-vm-modules'.\nRun with 'node --experimental-vm-modules node_modules/jest-cli/bin/jest'."
);
let esm = esmCache.get(p);
if (!esm) {
esm = new vm.SourceTextModule(content, {
identifier: `${esmIdentifier}-${p}`,
url: `${pathToFileURL(p).href}?${esmIdentifier}`,
context: esmContext,
initializeImportMeta: (meta, module) => {
meta.url = pathToFileURL(p).href;
},
importModuleDynamically: async (
specifier,
module
) => {
const normalizedSpecifier =
specifier.startsWith("file:")
? `./${path.relative(
path.dirname(p),
fileURLToPath(specifier)
)}`
: specifier.replace(
/https:\/\/test.cases\/path\//,
"./"
);
const result = await _require(
currentDirectory,
normalizedSpecifier,
"evaluated"
);
return await asModule(result, module.context);
}
});
esmCache.set(p, esm);
}
if (esmMode === "unlinked") return esm;
return (async () => {
if (esmMode === "unlinked") return esm;
if (esm.status !== "evaluated") {
await esm.link(
async (specifier, referencingModule) =>
await asModule(
await _require(
path.dirname(
referencingModule.identifier
? referencingModule.identifier.slice(
esmIdentifier.length + 1
)
: fileURLToPath(referencingModule.url)
),
specifier,
"unlinked"
),
referencingModule.context,
true
)
);
// node.js 10 needs instantiate
if (esm.instantiate) esm.instantiate();
await esm.evaluate();
}
if (esmMode === "evaluated") return esm;
const ns = esm.namespace;
return ns.default && ns.default instanceof Promise
? ns.default
: ns;
})();
}
if (
options.target === "web" ||
options.target === "webworker"
@ -358,10 +453,33 @@ const describeCases = config => {
if (testConfig.noTests)
return process.nextTick(compilationFinished);
_require(
const getBundle = (outputDirectory, module) => {
if (Array.isArray(module)) {
return module.map(arg =>
path.join(outputDirectory, arg)
);
} else if (module instanceof RegExp) {
return fs
.readdirSync(outputDirectory)
.filter(f => module.test(f))
.map(f => path.join(outputDirectory, f));
}
return [path.join(outputDirectory, module)];
};
const promises = [];
for (const p of getBundle(
outputDirectory,
testConfig.bundlePath || "./bundle.js"
);
)) {
promises.push(
Promise.resolve().then(() =>
_require(outputDirectory, p)
)
);
}
await Promise.all(promises);
if (run.getNumberOfTests() < 1)
return compilationFinished(
@ -431,6 +549,12 @@ const describeCases = config => {
afterAll(() => {
remove(tempDirectory);
});
const {
it: _it,
beforeEach: _beforeEach,
afterEach: _afterEach
} = createLazyTestEnv(10000);
});
}
});

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,3 @@
module.exports = {
bundlePath: /^main\./
};

View File

@ -0,0 +1,31 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
experiments: {
outputModule: true
},
optimization: {
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"
}
};