Skip to content

Commit

Permalink
Merge pull request #7638 from webpack/feature/wasm-initial-error
Browse files Browse the repository at this point in the history
add helpful error when importing wasm in initial chunk
  • Loading branch information
sokra committed Jul 2, 2018
2 parents e8dc361 + 0bb917b commit a4e5f63
Show file tree
Hide file tree
Showing 20 changed files with 208 additions and 3 deletions.
2 changes: 1 addition & 1 deletion lib/Chunk.js
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions lib/ChunkTemplate.js
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions lib/Compilation.js
Expand Up @@ -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[]>} */
Expand Down Expand Up @@ -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;
Expand All @@ -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 (
Expand Down
5 changes: 4 additions & 1 deletion lib/MainTemplate.js
Expand Up @@ -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} */
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions lib/Module.js
Expand Up @@ -182,6 +182,9 @@ class Module extends DependenciesBlock {
);
}

/**
* @returns {Chunk[]} all chunks which contain the module
*/
getChunks() {
return Array.from(this._chunks);
}
Expand Down
8 changes: 8 additions & 0 deletions lib/ModuleReason.js
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions lib/wasm/WasmMainTemplatePlugin.js
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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",
Expand Down
88 changes: 88 additions & 0 deletions lib/wasm/WebAssemblyInInitialChunkError.js
@@ -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);
}
};
28 changes: 28 additions & 0 deletions lib/wasm/WebAssemblyModulesPlugin.js
Expand Up @@ -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",
Expand Down Expand Up @@ -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
)
);
}
});
}
);
}
Expand Down
11 changes: 10 additions & 1 deletion test/StatsTestCases.test.js
Expand Up @@ -15,7 +15,16 @@ const tests = fs
testName =>
fs.existsSync(path.join(base, testName, "index.js")) ||
fs.existsSync(path.join(base, testName, "webpack.config.js"))
);
)
.filter(testName => {
const testDirectory = path.join(base, testName);
const filterPath = path.join(testDirectory, "test.filter.js");
if (fs.existsSync(filterPath) && !require(filterPath)()) {
describe.skip(testName, () => it("filtered"));
return false;
}
return true;
});

describe("StatsTestCases", () => {
tests.forEach(testName => {
Expand Down
Empty file.
14 changes: 14 additions & 0 deletions test/configCases/wasm/wasm-in-initial-chunk-error/errors.js
@@ -0,0 +1,14 @@
module.exports = [
[
/\.\/wasm.wat/,
/WebAssembly module is included in initial chunk/,
/\* \.\/index.js --> \.\/module.js --> \.\/wasm.wat/,
/\* \.\.\. --> \.\/module.js --> \.\/module2.js --> \.\/wasm.wat/,
/\* \.\.\. --> \.\/module2.js --> \.\/module3.js --> \.\/wasm.wat/
],
[
/\.\/wasm2\.wat/,
/WebAssembly module is included in initial chunk/,
/\* \.\/index.js --> \.\/module.js --> \.\/module2.js --> \.\/module3.js --> \.\/wasm2.wat/
]
];
1 change: 1 addition & 0 deletions test/configCases/wasm/wasm-in-initial-chunk-error/index.js
@@ -0,0 +1 @@
import "./module";
7 changes: 7 additions & 0 deletions test/configCases/wasm/wasm-in-initial-chunk-error/module.js
@@ -0,0 +1,7 @@
import { getNumber } from "./wasm.wat";

import("./async.js");

require("./module2");

getNumber();
2 changes: 2 additions & 0 deletions test/configCases/wasm/wasm-in-initial-chunk-error/module2.js
@@ -0,0 +1,2 @@
require("./wasm.wat");
require("./module3");
2 changes: 2 additions & 0 deletions test/configCases/wasm/wasm-in-initial-chunk-error/module3.js
@@ -0,0 +1,2 @@
require("./wasm.wat");
require("./wasm2.wat");
@@ -0,0 +1,5 @@
var supportsWebAssembly = require("../../../helpers/supportsWebAssembly");

module.exports = function() {
return supportsWebAssembly();
};
4 changes: 4 additions & 0 deletions test/configCases/wasm/wasm-in-initial-chunk-error/wasm.wat
@@ -0,0 +1,4 @@
(module
(func $getNumber (export "getNumber") (result i32)
(i32.const 42)))

4 changes: 4 additions & 0 deletions test/configCases/wasm/wasm-in-initial-chunk-error/wasm2.wat
@@ -0,0 +1,4 @@
(module
(func $getNumber (export "getNumber") (result i32)
(i32.const 42)))

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

0 comments on commit a4e5f63

Please sign in to comment.