Skip to content

Commit 7455053

Browse files
authored
fix(html): skip script tags for shared chunks without JavaScript (#22779)
1 parent d5569e2 commit 7455053

9 files changed

Lines changed: 141 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Skip HTML script tags for shared chunks that emit no JavaScript.

‎lib/dependencies/html/HtmlEntryDependency.js‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -956,10 +956,7 @@ HtmlEntryDependency.Template = class HtmlEntryDependencyTemplate extends (
956956
if (hasCss) {
957957
cssChunks.push(chunk);
958958
}
959-
// Anything not CSS-only stays on the JS lane in `orderedChunks` order,
960-
// preserving the runtime-first invariant. A chunk emitting neither
961-
// `.js` nor `.css` still gets a clone, as it always has.
962-
if (hasJs || !hasCss) jsChunks.push(chunk);
959+
if (hasJs) jsChunks.push(chunk);
963960
}
964961
// When the entry chunk carries CSS itself, fold it into the same
965962
// ordering so its `<link>` lands in the right cascade position
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import first from "./first";
2+
import second from "./second";
3+
import vendor from "./vendor";
4+
5+
self.sharedValues = [first, second, vendor];
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = "first";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = "second";
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"use strict";
2+
3+
module.exports = {
4+
/** @returns {string[]} test bundles */
5+
findBundle() {
6+
return ["./test.js"];
7+
}
8+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const crypto = require("crypto");
2+
const fs = require("fs");
3+
const path = require("path");
4+
const vm = require("vm");
5+
6+
for (const name of ["virtual", "mixed"]) {
7+
it(`should load ${name} shared chunks with valid integrity and no missing scripts`, () => {
8+
const html = fs.readFileSync(path.join(__dirname, `${name}.html`), "utf8");
9+
const tags = html.match(/<script\b[^>]*>/g) || [];
10+
expect(tags).toHaveLength(name === "virtual" ? 2 : 3);
11+
expect(tags[0]).toContain(`${name}-runtime.`);
12+
if (name === "virtual") {
13+
expect(html).not.toContain("virtual-shared.");
14+
} else {
15+
expect(tags[1]).toContain("mixed-shared.");
16+
}
17+
expect(html).not.toContain("__WEBPACK_HTML_INTEGRITY__");
18+
const context = vm.createContext({ self: {} });
19+
for (const tag of tags) {
20+
const filename = tag.match(/\bsrc="([^"]+)"/)[1];
21+
const source = fs.readFileSync(path.join(__dirname, filename));
22+
const integrity = tag.match(/\bintegrity="([^"]+)"/);
23+
expect(integrity).not.toBeNull();
24+
expect(integrity[1]).toBe(
25+
`sha384-${crypto.createHash("sha384").update(source).digest("base64")}`
26+
);
27+
vm.runInContext(source.toString("utf8"), context, { filename });
28+
}
29+
expect(context.self.sharedValues).toEqual(["first", "second", "vendor"]);
30+
});
31+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = "vendor";
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"use strict";
2+
3+
const fs = require("fs");
4+
const path = require("path");
5+
const webpack = require("../../../../");
6+
7+
/** @type {import("../../../../").Configuration[]} */
8+
module.exports = [false, true].map((includeJavaScript) => {
9+
const name = includeJavaScript ? "mixed" : "virtual";
10+
return {
11+
mode: "production",
12+
target: "web",
13+
entry: { [name]: "./entry.js" },
14+
experiments: { html: true },
15+
output: {
16+
filename: "[name].[contenthash].js",
17+
htmlFilename: `${name}.html`,
18+
publicPath: "",
19+
crossOriginLoading: "anonymous",
20+
html: { integrity: true }
21+
},
22+
optimization: {
23+
minimize: false,
24+
runtimeChunk: { name: `${name}-runtime` },
25+
splitChunks: {
26+
cacheGroups: {
27+
shared: {
28+
/**
29+
* @param {import("../../../../").Module} module module
30+
* @returns {boolean} whether the module belongs to the shared chunk
31+
*/
32+
test: (module) =>
33+
module.type === "consume-shared-module" ||
34+
(includeJavaScript &&
35+
module.nameForCondition() ===
36+
path.join(__dirname, "vendor.js")),
37+
chunks: "all",
38+
enforce: true,
39+
name: `${name}-shared`
40+
}
41+
}
42+
}
43+
},
44+
plugins: [
45+
new webpack.container.ModuleFederationPlugin({
46+
name,
47+
shared: {
48+
"./first": { eager: true, requiredVersion: false },
49+
"./second": { eager: true, requiredVersion: false }
50+
}
51+
}),
52+
{
53+
/**
54+
* @param {import("../../../../").Compiler} compiler compiler
55+
* @returns {void}
56+
*/
57+
apply(compiler) {
58+
compiler.hooks.afterEmit.tap("CheckSharedChunks", (compilation) => {
59+
const chunk = /** @type {import("../../../../").Chunk} */ (
60+
compilation.namedChunks.get(`${name}-shared`)
61+
);
62+
expect(chunk).toBeDefined();
63+
expect(chunk.files.size).toBe(includeJavaScript ? 1 : 0);
64+
expect(compilation.chunkGraph.getNumberOfChunkModules(chunk)).toBe(
65+
includeJavaScript ? 3 : 2
66+
);
67+
});
68+
compiler.hooks.thisCompilation.tap("CopyTest", (compilation) => {
69+
compilation.hooks.processAssets.tap(
70+
{
71+
name: "CopyTest",
72+
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL
73+
},
74+
() => {
75+
compilation.emitAsset(
76+
"test.js",
77+
new webpack.sources.RawSource(
78+
fs.readFileSync(path.join(__dirname, "test.js"))
79+
)
80+
);
81+
}
82+
);
83+
});
84+
}
85+
}
86+
]
87+
};
88+
});

0 commit comments

Comments
 (0)