8000 fix: fix the case where an ESM entryChunk depends on the runtimeChunk… · webpack/webpack@e805dc1 · GitHub
[go: up one dir, main page]

Skip to content

Commit e805dc1

Browse files
authored
fix: fix the case where an ESM entryChunk depends on the runtimeChunk hash (#19570)
1 parent 1b34181 commit e805dc1

File tree

10 files changed

+265
-34
lines changed

10 files changed

+265
-34
lines changed

lib/Compilation.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4366,16 +4366,21 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o
43664366
/** @type {Chunk[]} */
43674367
const unorderedRuntimeChunks = [];
43684368
/** @type {Chunk[]} */
4369-
const otherChunks = [];
4369+
const initialChunks = [];
4370+
/** @type {Chunk[]} */
4371+
const asyncChunks = [];
43704372
for (const c of this.chunks) {
43714373
if (c.hasRuntime()) {
43724374
unorderedRuntimeChunks.push(c);
4375+
} else if (c.canBeInitial()) {
4376+
initialChunks.push(c);
43734377
} else {
4374-
otherChunks.push(c);
4378+
asyncChunks.push(c);
43754379
}
43764380
}
43774381
unorderedRuntimeChunks.sort(byId);
4378-
otherChunks.sort(byId);
4382+
initialChunks.sort(byId);
4383+
asyncChunks.sort(byId);
43794384

43804385
/** @typedef {{ chunk: Chunk, referencedBy: RuntimeChunkInfo[], remaining: number }} RuntimeChunkInfo */
43814386
/** @type {Map<Chunk, RuntimeChunkInfo>} */
@@ -4541,8 +4546,9 @@ This prevents using hashes of each other and should be avoided.`);
45414546
}
45424547
this.logger.timeAggregate("hashing: hash chunks");
45434548
};
4544-
for (const chunk of otherChunks) processChunk(chunk);
4549+
for (const chunk of asyncChunks) processChunk(chunk);
45454550
for (const chunk of runtimeChunks) processChunk(chunk);
4551+
for (const chunk of initialChunks) processChunk(chunk);
45464552
if (errors.length > 0) {
45474553
errors.sort(compareSelect(err => err.module, compareModulesByIdentifier));
45484554
for (const error of errors) {

lib/esm/ModuleChunkFormatPlugin.js

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,33 @@ const { updateHashForEntryStartup } = require("../javascript/StartupHelpers");
1919
const { getUndoPath } = require("../util/identifier");
2020

2121
/** @typedef {import("../Chunk")} Chunk */
22+
/** @typedef {import("../ChunkGraph")} ChunkGraph */
23+
/** @typedef {import("../ChunkGroup")} ChunkGroup */
2224
/** @typedef {import("../Compiler")} Compiler */
2325
/** @typedef {import("../Entrypoint")} Entrypoint */
26+
/** @typedef {import("../Module")} Module */
27+
28+
/**
29+
* Gets information about a chunk including its entries and runtime chunk
30+
* @param {Chunk} chunk The chunk to get information for
31+
* @param {ChunkGraph} chunkGraph The chunk graph containing the chunk
32+
* @returns {{entries: Array<[Module, Entrypoint | undefined]>, runtimeChunk: Chunk|null}} Object containing chunk entries and runtime chunk
33+
*/
34+
function getChunkInfo(chunk, chunkGraph) {
< 10000 code>35+
const entries = Array.from(
36+
chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)
37+
);
38+
const runtimeChunk =
39+
entries.length > 0
40+
? /** @type {Entrypoint[][]} */
41+
(entries)[0][1].getRuntimeChunk()
42+
: null;
43+
44+
return {
45+
entries,
46+
runtimeChunk
47+
};
48+
}
2449

2550
class ModuleChunkFormatPlugin {
2651
/**
@@ -76,13 +101,8 @@ class ModuleChunkFormatPlugin {
76101
)
77102
);
78103
}
79-
const entries = Array.from(
80-
chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)
81-
);
82-
if (entries.length > 0) {
83-
const runtimeChunk =
84-
/** @type {Entrypoint[][]} */
85-
(entries)[0][1].getRuntimeChunk();
104+
const { entries, runtimeChunk } = getChunkInfo(chunk, chunkGraph);
105+
if (runtimeChunk) {
86106
const currentOutputName = compilation
87107
.getPath(
88108
getChunkFilenameTemplate(chunk, compilation.outputOptions),
@@ -207,11 +227,15 @@ class ModuleChunkFormatPlugin {
207227
"ModuleChunkFormatPlugin",
208228
(chunk, hash, { chunkGraph, runtimeTemplate }) => {
209229
if (chunk.hasRuntime()) return;
230+
const { entries, runtimeChunk } = getChunkInfo(chunk, chunkGraph);
210231
hash.update("ModuleChunkFormatPlugin");
211232
hash.update("1");
212-
const entries = Array.from(
213-
chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)
214-
);
233+
if (runtimeChunk && runtimeChunk.hash) {
234+
// Any change to runtimeChunk should trigger a hash update,
235+
// we shouldn't depend on or inspect its internal implementation.
236+
// import __webpack_require__ from "./runtime-main.e9400aee33633a3973bd.js";
237+
hash.update(runtimeChunk.hash);
238+
}
215239
updateHashForEntryStartup(hash, chunkGraph, entries, chunk);
216240
}
217241
);

test/WatchTestCases.template.js

Lines changed: 144 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ const path = require("path");
99
const fs = require("graceful-fs");
1010
const vm = require("vm");
1111
const rimraf = require("rimraf");
12+
const { pathToFileURL, fileURLToPath } = require("url");
1213
const checkArrayExpectation = require("./checkArrayExpectation");
1314
const createLazyTestEnv = require("./helpers/createLazyTestEnv");
1415
const { remove } = require("./helpers/remove");
1516
const prepareOptions = require("./helpers/prepareOptions");
1617
const deprecationTracking = require("./helpers/deprecationTracking");
1718
const FakeDocument = require("./helpers/FakeDocument");
19+
const asModule = require("./helpers/asModule");
1820

1921
/**
2022
* @param {string} src src
@@ -200,7 +202,7 @@ const describeCases = config => {
200202
{
201203
aggregateTimeout: 1000
202204
},
203-
(err, stats) => {
205+
async (err, stats) => {
204206
if (err) return compilationFinished(err);
205207
if (!stats) {
206208
return compilationFinished(
@@ -273,29 +275,122 @@ const describeCases = config => {
273275
document: new FakeDocument()
274276
};
275277

278+
const baseModuleScope = {
279+
console,
280+
it: run.it,
281+
beforeEach: _beforeEach,
282+
afterEach: _afterEach,
283+
expect,
284+
jest,
285+
STATS_JSON: jsonStats,
286+
nsObj: m => {
287+
Object.defineProperty(m, Symbol.toStringTag, {
288+
value: "Module"
289+
});
290+
return m;
291+
},
292+
window: globalContext,
293+
self: globalContext,
294+
WATCH_STEP: run.name,
295+
STATE: state
296+
};
297+
298+
const esmCache = new Map();
299+
const esmIdentifier = `${category.name}-${testName}`;
300+
const esmContext = vm.createContext(baseModuleScope, {
301+
name: "context for esm"
302+
});
303+
// ESM
304+
const isModule =
305+
options.experiments && options.experiments.outputModule;
306+
276307
/**
277-
* @param {string} currentDirectory the current directory
278-
* @param {TODO} module a module
308+
* @param {string} currentDirectory The directory to resolve relative paths from
309+
* @param {string} module The module to require
310+
* @param {("unlinked"|"evaluated")} esmMode The mode for ESM module handling
279311
* @returns {EXPECTED_ANY} required module
280312
* @private
281313
*/
282-
function _require(currentDirectory, module) {
283-
if (Array.isArray(module) || /^\.\.?\//.test(module)) {
314+
function _require(currentDirectory, module, esmMode) {
315+
if (/^\.\.?\//.test(module) || path.isAbsolute(module)) {
284316
let fn;
285-
let content;
286-
let p;
287-
if (Array.isArray(module)) {
288-
p = path.join(currentDirectory, module[0]);
289-
content = module
290-
.map(arg => {
291-
p = path.join(currentDirectory, arg);
292-
return fs.readFileSync(p, "utf-8");
293-
})
294-
.join("\n");
295-
} else {
296-
p = path.join(currentDirectory, module);
297-
content = fs.readFileSync(p, "utf-8");
317+
const p = path.isAbsolute(module)
318+
? module
319+
: path.join(currentDirectory, module);
320+
const content = fs.readFileSync(p, "utf-8");
321+
322+
if (isModule) {
323+
if (!vm.SourceTextModule)
324+
throw new Error(
325+
"Running this test requires '--experimental-vm-modules'.\nRun with 'node --experimental-vm-modules node_modules/jest-cli/bin/jest'."
326+
);
327+
let esm = esmCache.get(p);
328+
if (!esm) {
329+
esm = new vm.SourceTextModule(content, {
330+
identifier: `${esmIdentifier}-${p}`,
331+
url: `${pathToFileURL(p).href}?${esmIdentifier}`,
332+
context: esmContext,
333+
initializeImportMeta: (meta, module) => {
334+
meta.url = pathToFileURL(p).href;
335+
},
336+
importModuleDynamically: async (
337+
specifier,
338+
module
339+
) => {
340+
const normalizedSpecifier =
341+
specifier.startsWith("file:")
342+
? `./${path.relative(
343+
path.dirname(p),
344+
fileURLToPath(specifier)
345+
)}`
346+
: specifier.replace(
347+
/https:\/\/test.cases\/path\//,
348+
"./"
349+
);
350+
const result = await _require(
351+
currentDirectory,
352+
normalizedSpecifier,
353+
"evaluated"
354+
);
355+
return await asModule(result, module.context);
356+
}
357+
});
358+
esmCache.set(p, esm);
359+
}
360+
if (esmMode === "unlinked") return esm;
361+
return (async () => {
362+
if (esmMode === "unlinked") return esm;
363+
if (esm.status !== "evaluated") {
364+
await esm.link(
365+
async (specifier, referencingModule) =>
366+
await asModule(
367+
await _require(
368+
path.dirname(
369+
referencingModule.identifier
370+
? referencingModule.identifier.slice(
371+
esmIdentifier.length + 1
372+
)
373+
: fileURLToPath(referencingModule.url)
374+
),
375+
specifier,
376+
"unlinked"
377+
),
378+
referencingModule.context,
379+
true
380+
)
381+
);
382+
// node.js 10 needs instantiate
383+
if (esm.instantiate) esm.instantiate();
384+
await esm.evaluate();
385+
}
386+
if (esmMode === "evaluated") return esm;
387+
const ns = esm.namespace;
388+
return ns.default && ns.default instanceof Promise
389+
? ns.default
390+
: ns;
391+
})();
298392
}
393+
299394
if (
300395
options.target === "web" ||
301396
options.target === "webworker"
@@ -358,10 +453,33 @@ const describeCases = config => {
358453

359454
if (testConfig.noTests)
360455
return process.nextTick(compilationFinished);
361-
_require(
456+
457+
const getBundle = (outputDirectory, module) => {
458+
if (Array.isArray(module)) {
459+
return module.map(arg =>
460+
path.join(outputDirectory, arg)
461+
);
462+
} else if (module instanceof RegExp) {
463+
return fs
464+
.readdirSync(outputDirectory)
465+
.filter(f => module.test(f))
466+
.map(f => path.join(outputDirectory, f));
467+
}
468+
return [path.join(outputDirectory, module)];
469+
};
470+
471+
const promises = [];
472+
for (const p of getBundle(
362473
outputDirectory,
363474
testConfig.bundlePath || "./bundle.js"
364-
);
475+
)) {
476+
promises.push(
477+
Promise.resolve().then(() =>
478+
_require(outputDirectory, p)
479+
)
480+
);
481+
}
482+
await Promise.all(promises);
365483

366484
if (run.getNumberOfTests() < 1)
367485
return compilationFinished(
@@ -431,6 +549,12 @@ const describeCases = config => {
431549
afterAll(() => {
432550
remove(tempDirectory);
433551
});
552+
553+
const {
554+
it: _it,
555+
beforeEach: _beforeEach,
556+
afterEach: _afterEach
557+
} = createLazyTestEnv(10000);
434558
});
435559
}
436560
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export var value = "0";
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export var value = "0";
2+
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { react } from "./react";
2+
3+
it("should work where an ESM entryChunk depends on the runtimeChunk", async function (done) {
4+
const mainChunk = STATS_JSON.chunks.find((chunk) => chunk.id === "main");
5+
const runtimeChunk = STATS_JSON.chunks.find((chunk) => chunk.id === "runtime-main");
6+
const dynamic1Chunk = STATS_JSON.chunks.find((chunk) => chunk.id === "dynamic-1_js");
7+
const dynamic2Chunk = STATS_JSON.chunks.find((chunk) => chunk.id === "dynamic-2_js");
8+
const reactChunk = STATS_JSON.chunks.find((chunk) => chunk.id === "react");
9+
expect(mainChunk).toBeDefined();
10+
expect(react).toBe("react");
11+
12+
await import('./dynamic-1').then(console.log)
13+
await import('./dynamic-2').then(console.log)
14+
15+
if (WATCH_STEP === "0") {
16+
STATE.mainChunkHash = mainChunk.hash;
17+
STATE.dynamic1ChunkHash = dynamic1Chunk.hash;
18+
STATE.dynamic2ChunkHash = dynamic2Chunk.hash;
19+
STATE.runtimeChunkHash = runtimeChunk.hash;
20+
STATE.reactChunkHash = reactChunk.hash;
21+
} else {
22+
// async dynamic2Chunk needn't be updated
23+
expect(dynamic2Chunk.hash).toBe(STATE.dynamic2ChunkHash);
24+
// initial reactChunk is needn't be updated
25+
expect(reactChunk.hash).toBe(STATE.reactChunkHash);
26+
27+
28+
// initial mainChunk need to be updated
29+
expect(mainChunk.hash).not.toBe(STATE.mainChunkHash);
30+
// async dynamic1Chunk need to be updated
31+
expect(dynamic1Chunk.hash).not.toBe(STATE.dynamic1ChunkHash);
32+
// runtime runtimeChunk need to be updated
33+
expect(runtimeChunk.hash).not.toBe(STATE.runtimeChunkHash);
34+
}
35+
done()
36+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const react = "react";
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export var value = "1";
2+
3+
import("./dynamic-2").then(console.log)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
bundlePath: /^main\./
3+
};

0 commit comments

Comments
 (0)
0