Skip to content

Commit a4e5f63

Browse files
authored
Merge pull request webpack#7638 from webpack/feature/wasm-initial-error
add helpful error when importing wasm in initial chunk
2 parents e8dc361 + 0bb917b commit a4e5f63

20 files changed

+208
-3
lines changed

lib/Chunk.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ class Chunk {
298298
}
299299

300300
/**
301-
* @returns {SortableSet} the chunkGroups that said chunk is referenced in
301+
* @returns {SortableSet<ChunkGroup>} the chunkGroups that said chunk is referenced in
302302
*/
303303
get groupsIterable() {
304304
return this._groups;

lib/ChunkTemplate.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module.exports = class ChunkTemplate extends Tapable {
2626
super();
2727
this.outputOptions = outputOptions || {};
2828
this.hooks = {
29+
/** @type {SyncWaterfallHook<TODO[], RenderManifestOptions>} */
2930
renderManifest: new SyncWaterfallHook(["result", "options"]),
3031
modules: new SyncWaterfallHook([
3132
"source",

lib/Compilation.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,11 @@ class Compilation extends Tapable {
232232
/** @type {SyncHook} */
233233
seal: new SyncHook([]),
234234

235+
/** @type {SyncHook} */
236+
beforeChunks: new SyncHook([]),
237+
/** @type {SyncHook<Chunk[]>} */
238+
afterChunks: new SyncHook(["chunks"]),
239+
235240
/** @type {SyncBailHook<Module[]>} */
236241
optimizeDependenciesBasic: new SyncBailHook(["modules"]),
237242
/** @type {SyncBailHook<Module[]>} */
@@ -1150,6 +1155,7 @@ class Compilation extends Tapable {
11501155
}
11511156
this.hooks.afterOptimizeDependencies.call(this.modules);
11521157

1158+
this.hooks.beforeChunks.call();
11531159
for (const preparedEntrypoint of this._preparedEntrypoints) {
11541160
const module = preparedEntrypoint.module;
11551161
const name = preparedEntrypoint.name;
@@ -1171,6 +1177,8 @@ class Compilation extends Tapable {
11711177
}
11721178
this.processDependenciesBlocksForChunkGroups(this.chunkGroups.slice());
11731179
this.sortModules(this.modules);
1180+
this.hooks.afterChunks.call(this.chunks);
1181+
11741182
this.hooks.optimize.call();
11751183

11761184
while (

lib/MainTemplate.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const {
1919
const Template = require("./Template");
2020

2121
/** @typedef {import("webpack-sources").ConcatSource} ConcatSource */
22+
/** @typedef {import("webpack-sources").Source} Source */
2223
/** @typedef {import("./ModuleTemplate")} ModuleTemplate */
2324
/** @typedef {import("./Chunk")} Chunk */
2425
/** @typedef {import("./Module")} Module} */
@@ -93,7 +94,9 @@ module.exports = class MainTemplate extends Tapable {
9394
localVars: new SyncWaterfallHook(["source", "chunk", "hash"]),
9495
require: new SyncWaterfallHook(["source", "chunk", "hash"]),
9596
requireExtensions: new SyncWaterfallHook(["source", "chunk", "hash"]),
97+
/** @type {SyncWaterfallHook<string, Chunk, string>} */
9698
beforeStartup: new SyncWaterfallHook(["source", "chunk", "hash"]),
99+
/** @type {SyncWaterfallHook<string, Chunk, string>} */
97100
startup: new SyncWaterfallHook(["source", "chunk", "hash"]),
98101
render: new SyncWaterfallHook([
99102
"source",
@@ -448,7 +451,7 @@ module.exports = class MainTemplate extends Tapable {
448451
/**
449452
*
450453
* @param {string} hash string hash
451-
* @param {number} length length
454+
* @param {number=} length length
452455
* @returns {string} call hook return
453456
*/
454457
renderCurrentHashCode(hash, length) {

lib/Module.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,9 @@ class Module extends DependenciesBlock {
182182
);
183183
}
184184

185+
/**
186+
* @returns {Chunk[]} all chunks which contain the module
187+
*/
185188
getChunks() {
186189
return Array.from(this._chunks);
187190
}

lib/ModuleReason.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44
*/
55
"use strict";
66

7+
/** @typedef {import("./Module")} Module */
8+
/** @typedef {import("./Dependency")} Dependency */
9+
710
class ModuleReason {
11+
/**
12+
* @param {Module} module the referencing module
13+
* @param {Dependency} dependency the referencing dependency
14+
* @param {string=} explanation some extra detail
15+
*/
816
constructor(module, dependency, explanation) {
917
this.module = module;
1018
this.dependency = dependency;

lib/wasm/WasmMainTemplatePlugin.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const Template = require("../Template");
88
const WebAssemblyUtils = require("./WebAssemblyUtils");
99

1010
/** @typedef {import("../Module")} Module */
11+
/** @typedef {import("../MainTemplate")} MainTemplate */
1112

1213
// Get all wasm modules
1314
const getAllWasmModules = chunk => {
@@ -159,6 +160,11 @@ class WasmMainTemplatePlugin {
159160
this.supportsStreaming = supportsStreaming;
160161
this.mangleImports = mangleImports;
161162
}
163+
164+
/**
165+
* @param {MainTemplate} mainTemplate main template
166+
* @returns {void}
167+
*/
162168
apply(mainTemplate) {
163169
mainTemplate.hooks.localVars.tap(
164170
"WasmMainTemplatePlugin",
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
*/
4+
"use strict";
5+
6+
const WebpackError = require("../WebpackError");
7+
8+
/** @typedef {import("../Module")} Module */
9+
/** @typedef {import("../RequestShortener")} RequestShortener */
10+
11+
/**
12+
* @param {Module} module module to get chains from
13+
* @param {RequestShortener} requestShortener to make readable identifiers
14+
* @returns {string[]} all chains to the module
15+
*/
16+
const getInitialModuleChains = (module, requestShortener) => {
17+
const queue = [
18+
{ head: module, message: module.readableIdentifier(requestShortener) }
19+
];
20+
/** @type {Set<string>} */
21+
const results = new Set();
22+
/** @type {Set<string>} */
23+
const incompleteResults = new Set();
24+
/** @type {Set<Module>} */
25+
const visitedModules = new Set();
26+
27+
for (const chain of queue) {
28+
const { head, message } = chain;
29+
let final = true;
30+
/** @type {Set<Module>} */
31+
const alreadyReferencedModules = new Set();
32+
for (const reason of head.reasons) {
33+
const newHead = reason.module;
34+
if (newHead) {
35+
if (!newHead.getChunks().some(c => c.canBeInitial())) continue;
36+
final = false;
37+
if (alreadyReferencedModules.has(newHead)) continue;
38+
alreadyReferencedModules.add(newHead);
39+
const moduleName = newHead.readableIdentifier(requestShortener);
40+
const detail = reason.explanation ? ` (${reason.explanation})` : "";
41+
const newMessage = `${moduleName}${detail} --> ${message}`;
42+
if (visitedModules.has(newHead)) {
43+
incompleteResults.add(`... --> ${newMessage}`);
44+
continue;
45+
}
46+
visitedModules.add(newHead);
47+
queue.push({
48+
head: newHead,
49+
message: newMessage
50+
});
51+
} else {
52+
final = false;
53+
const newMessage = reason.explanation
54+
? `(${reason.explanation}) --> ${message}`
55+
: message;
56+
results.add(newMessage);
57+
}
58+
}
59+
if (final) {
60+
results.add(message);
61+
}
62+
}
63+
for (const result of incompleteResults) {
64+
results.add(result);
65+
}
66+
return Array.from(results);
67+
};
68+
69+
module.exports = class WebAssemblyInInitialChunkError extends WebpackError {
70+
/**
71+
* @param {Module} module WASM module
72+
* @param {RequestShortener} requestShortener request shortener
73+
*/
74+
constructor(module, requestShortener) {
75+
const moduleChains = getInitialModuleChains(module, requestShortener);
76+
const message = `WebAssembly module is included in initial chunk.
77+
This is not allowed, because WebAssembly download and compilation must happen asynchronous.
78+
Add an async splitpoint (i. e. import()) somewhere between your entrypoint and the WebAssembly module:
79+
${moduleChains.map(s => `* ${s}`).join("\n")}`;
80+
81+
super(message);
82+
this.name = "WebAssemblyInInitialChunkError";
83+
this.hideStack = true;
84+
this.module = module;
85+
86+
Error.captureStackTrace(this, this.constructor);
87+
}
88+
};

lib/wasm/WebAssemblyModulesPlugin.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,19 @@ const WebAssemblyGenerator = require("./WebAssemblyGenerator");
1010
const WebAssemblyJavascriptGenerator = require("./WebAssemblyJavascriptGenerator");
1111
const WebAssemblyImportDependency = require("../dependencies/WebAssemblyImportDependency");
1212
const WebAssemblyExportImportedDependency = require("../dependencies/WebAssemblyExportImportedDependency");
13+
const WebAssemblyInInitialChunkError = require("./WebAssemblyInInitialChunkError");
14+
15+
/** @typedef {import("../Compiler")} Compiler */
1316

1417
class WebAssemblyModulesPlugin {
1518
constructor(options) {
1619
this.options = options;
1720
}
1821

22+
/**
23+
* @param {Compiler} compiler compiler
24+
* @returns {void}
25+
*/
1926
apply(compiler) {
2027
compiler.hooks.compilation.tap(
2128
"WebAssemblyModulesPlugin",
@@ -78,6 +85,27 @@ class WebAssemblyModulesPlugin {
7885
return result;
7986
}
8087
);
88+
89+
compilation.hooks.afterChunks.tap("WebAssemblyModulesPlugin", () => {
90+
const initialWasmModules = new Set();
91+
for (const chunk of compilation.chunks) {
92+
if (chunk.canBeInitial()) {
93+
for (const module of chunk.modulesIterable) {
94+
if (module.type.startsWith("webassembly")) {
95+
initialWasmModules.add(module);
96+
}
97+
}
98+
}
99+
}
100+
for (const module of initialWasmModules) {
101+
compilation.errors.push(
102+
new WebAssemblyInInitialChunkError(
103+
module,
104+
compilation.requestShortener
105+
)
106+
);
107+
}
108+
});
81109
}
82110
);
83111
}

test/StatsTestCases.test.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,16 @@ const tests = fs
1515
testName =>
1616
fs.existsSync(path.join(base, testName, "index.js")) ||
1717
fs.existsSync(path.join(base, testName, "webpack.config.js"))
18-
);
18+
)
19+
.filter(testName => {
20+
const testDirectory = path.join(base, testName);
21+
const filterPath = path.join(testDirectory, "test.filter.js");
22+
if (fs.existsSync(filterPath) && !require(filterPath)()) {
23+
describe.skip(testName, () => it("filtered"));
24+
return false;
25+
}
26+
return true;
27+
});
1928

2029
describe("StatsTestCases", () => {
2130
tests.forEach(testName => {

0 commit comments

Comments
 (0)