add helpful error when importing wasm in initial chunk

This commit is contained in:
Tobias Koppers 2018-07-02 16:18:49 +02:00
parent e8dc36196f
commit 1ad71e01f9
18 changed files with 215 additions and 2 deletions

View File

@ -298,7 +298,7 @@ class Chunk {
}
/**
* @returns {SortableSet} the chunkGroups that said chunk is referenced in
* @returns {SortableSet<ChunkGroup>} the chunkGroups that said chunk is referenced in
*/
get groupsIterable() {
return this._groups;

View File

@ -26,6 +26,7 @@ module.exports = class ChunkTemplate extends Tapable {
super();
this.outputOptions = outputOptions || {};
this.hooks = {
/** @type {SyncWaterfallHook<TODO[], RenderManifestOptions>} */
renderManifest: new SyncWaterfallHook(["result", "options"]),
modules: new SyncWaterfallHook([
"source",

View File

@ -232,6 +232,11 @@ class Compilation extends Tapable {
/** @type {SyncHook} */
seal: new SyncHook([]),
/** @type {SyncHook} */
beforeChunks: new SyncHook([]),
/** @type {SyncHook<Chunk[]>} */
afterChunks: new SyncHook(["chunks"]),
/** @type {SyncBailHook<Module[]>} */
optimizeDependenciesBasic: new SyncBailHook(["modules"]),
/** @type {SyncBailHook<Module[]>} */
@ -1150,6 +1155,7 @@ class Compilation extends Tapable {
}
this.hooks.afterOptimizeDependencies.call(this.modules);
this.hooks.beforeChunks.call();
for (const preparedEntrypoint of this._preparedEntrypoints) {
const module = preparedEntrypoint.module;
const name = preparedEntrypoint.name;
@ -1171,6 +1177,8 @@ class Compilation extends Tapable {
}
this.processDependenciesBlocksForChunkGroups(this.chunkGroups.slice());
this.sortModules(this.modules);
this.hooks.afterChunks.call(this.chunks);
this.hooks.optimize.call();
while (

View File

@ -19,6 +19,7 @@ const {
const Template = require("./Template");
/** @typedef {import("webpack-sources").ConcatSource} ConcatSource */
/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("./ModuleTemplate")} ModuleTemplate */
/** @typedef {import("./Chunk")} Chunk */
/** @typedef {import("./Module")} Module} */
@ -93,7 +94,9 @@ module.exports = class MainTemplate extends Tapable {
localVars: new SyncWaterfallHook(["source", "chunk", "hash"]),
require: new SyncWaterfallHook(["source", "chunk", "hash"]),
requireExtensions: new SyncWaterfallHook(["source", "chunk", "hash"]),
/** @type {SyncWaterfallHook<string, Chunk, string>} */
beforeStartup: new SyncWaterfallHook(["source", "chunk", "hash"]),
/** @type {SyncWaterfallHook<string, Chunk, string>} */
startup: new SyncWaterfallHook(["source", "chunk", "hash"]),
render: new SyncWaterfallHook([
"source",
@ -448,7 +451,7 @@ module.exports = class MainTemplate extends Tapable {
/**
*
* @param {string} hash string hash
* @param {number} length length
* @param {number=} length length
* @returns {string} call hook return
*/
renderCurrentHashCode(hash, length) {

View File

@ -182,6 +182,9 @@ class Module extends DependenciesBlock {
);
}
/**
* @returns {Chunk[]} all chunks which contain the module
*/
getChunks() {
return Array.from(this._chunks);
}

View File

@ -4,7 +4,15 @@
*/
"use strict";
/** @typedef {import("./Module")} Module */
/** @typedef {import("./Dependency")} Dependency */
class ModuleReason {
/**
* @param {Module} module the referencing module
* @param {Dependency} dependency the referencing dependency
* @param {string=} explanation some extra detail
*/
constructor(module, dependency, explanation) {
this.module = module;
this.dependency = dependency;

View File

@ -8,6 +8,7 @@ const Template = require("../Template");
const WebAssemblyUtils = require("./WebAssemblyUtils");
/** @typedef {import("../Module")} Module */
/** @typedef {import("../MainTemplate")} MainTemplate */
// Get all wasm modules
const getAllWasmModules = chunk => {
@ -159,6 +160,11 @@ class WasmMainTemplatePlugin {
this.supportsStreaming = supportsStreaming;
this.mangleImports = mangleImports;
}
/**
* @param {MainTemplate} mainTemplate main template
* @returns {void}
*/
apply(mainTemplate) {
mainTemplate.hooks.localVars.tap(
"WasmMainTemplatePlugin",

View File

@ -0,0 +1,88 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
const WebpackError = require("../WebpackError");
/** @typedef {import("../Module")} Module */
/** @typedef {import("../RequestShortener")} RequestShortener */
/**
* @param {Module} module module to get chains from
* @param {RequestShortener} requestShortener to make readable identifiers
* @returns {string[]} all chains to the module
*/
const getInitialModuleChains = (module, requestShortener) => {
const queue = [
{ head: module, message: module.readableIdentifier(requestShortener) }
];
/** @type {Set<string>} */
const results = new Set();
/** @type {Set<string>} */
const incompleteResults = new Set();
/** @type {Set<Module>} */
const visitedModules = new Set();
for (const chain of queue) {
const { head, message } = chain;
let final = true;
/** @type {Set<Module>} */
const alreadyReferencedModules = new Set();
for (const reason of head.reasons) {
const newHead = reason.module;
if (newHead) {
if (!newHead.getChunks().some(c => c.canBeInitial())) continue;
final = false;
if (alreadyReferencedModules.has(newHead)) continue;
alreadyReferencedModules.add(newHead);
const moduleName = newHead.readableIdentifier(requestShortener);
const detail = reason.explanation ? ` (${reason.explanation})` : "";
const newMessage = `${moduleName}${detail} --> ${message}`;
if (visitedModules.has(newHead)) {
incompleteResults.add(`... --> ${newMessage}`);
continue;
}
visitedModules.add(newHead);
queue.push({
head: newHead,
message: newMessage
});
} else {
final = false;
const newMessage = reason.explanation
? `(${reason.explanation}) --> ${message}`
: message;
results.add(newMessage);
}
}
if (final) {
results.add(message);
}
}
for (const result of incompleteResults) {
results.add(result);
}
return Array.from(results);
};
module.exports = class WebAssemblyInInitialChunkError extends WebpackError {
/**
* @param {Module} module WASM module
* @param {RequestShortener} requestShortener request shortener
*/
constructor(module, requestShortener) {
const moduleChains = getInitialModuleChains(module, requestShortener);
const message = `WebAssembly module is included in initial chunk.
This is not allowed, because WebAssembly download and compilation must happen asynchronous.
Add an async splitpoint (i. e. import()) somewhere between your entrypoint and the WebAssembly module:
${moduleChains.map(s => `* ${s}`).join("\n")}`;
super(message);
this.name = "WebAssemblyInInitialChunkError";
this.hideStack = true;
this.module = module;
Error.captureStackTrace(this, this.constructor);
}
};

View File

@ -10,12 +10,19 @@ const WebAssemblyGenerator = require("./WebAssemblyGenerator");
const WebAssemblyJavascriptGenerator = require("./WebAssemblyJavascriptGenerator");
const WebAssemblyImportDependency = require("../dependencies/WebAssemblyImportDependency");
const WebAssemblyExportImportedDependency = require("../dependencies/WebAssemblyExportImportedDependency");
const WebAssemblyInInitialChunkError = require("./WebAssemblyInInitialChunkError");
/** @typedef {import("../Compiler")} Compiler */
class WebAssemblyModulesPlugin {
constructor(options) {
this.options = options;
}
/**
* @param {Compiler} compiler compiler
* @returns {void}
*/
apply(compiler) {
compiler.hooks.compilation.tap(
"WebAssemblyModulesPlugin",
@ -78,6 +85,27 @@ class WebAssemblyModulesPlugin {
return result;
}
);
compilation.hooks.afterChunks.tap("WebAssemblyModulesPlugin", () => {
const initialWasmModules = new Set();
for (const chunk of compilation.chunks) {
if (chunk.canBeInitial()) {
for (const module of chunk.modulesIterable) {
if (module.type.startsWith("webassembly")) {
initialWasmModules.add(module);
}
}
}
}
for (const module of initialWasmModules) {
compilation.errors.push(
new WebAssemblyInInitialChunkError(
module,
compilation.requestShortener
)
);
}
});
}
);
}

View File

@ -2854,3 +2854,39 @@ WARNING in UglifyJs Plugin: Dropping unused function someUnRemoteUsedFunction4 [
WARNING in UglifyJs Plugin: Dropping unused function someUnRemoteUsedFunction5 [./a.js:7,0] in bundle.js"
`;
exports[`StatsTestCases should print correct stats for wasm-in-initial-chunk-error 1`] = `
"Hash: 9f32353d97d5973caae9
Time: Xms
Built at: Thu Jan 01 1970 00:00:00 GMT
Asset Size Chunks Chunk Names
0.js 130 bytes 0
main.js 9.54 KiB 1 main
Entrypoint main = main.js
[0] ./wasm.wat 42 bytes {1} [built]
[1] ./module2.js 45 bytes {1} [built]
[2] ./module3.js 47 bytes {1} [built]
[3] ./wasm2.wat 42 bytes {1} [built]
[4] ./index.js + 1 modules 124 bytes {1} [built]
| ./index.js 19 bytes [built]
| ./module.js 100 bytes [built]
[5] ./async.js 0 bytes {0} [built]
WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/
ERROR in ./wasm2.wat
WebAssembly module is included in initial chunk.
This is not allowed, because WebAssembly download and compilation must happen asynchronous.
Add an async splitpoint (i. e. import()) somewhere between your entrypoint and the WebAssembly module:
* ./index.js --> ./module.js --> ./module2.js --> ./module3.js --> ./wasm2.wat
ERROR in ./wasm.wat
WebAssembly module is included in initial chunk.
This is not allowed, because WebAssembly download and compilation must happen asynchronous.
Add an async splitpoint (i. e. import()) somewhere between your entrypoint and the WebAssembly module:
* ./index.js --> ./module.js --> ./wasm.wat
* ... --> ./module.js --> ./module2.js --> ./wasm.wat
* ... --> ./module2.js --> ./module3.js --> ./wasm.wat"
`;

View File

@ -0,0 +1 @@
import "./module";

View File

@ -0,0 +1,7 @@
import { getNumber } from "./wasm.wat";
import("./async.js");
require("./module2");
getNumber();

View File

@ -0,0 +1,2 @@
require("./wasm.wat");
require("./module3");

View File

@ -0,0 +1,2 @@
require("./wasm.wat");
require("./wasm2.wat");

View File

@ -0,0 +1,4 @@
(module
(func $getNumber (export "getNumber") (result i32)
(i32.const 42)))

View File

@ -0,0 +1,4 @@
(module
(func $getNumber (export "getNumber") (result i32)
(i32.const 42)))

View File

@ -0,0 +1,12 @@
module.exports = {
entry: "./index",
module: {
rules: [
{
test: /\.wat$/,
loader: "wast-loader",
type: "webassembly/experimental"
}
]
}
};