Skip to content

Commit

Permalink
add helpful error when importing wasm in initial chunk
Browse files Browse the repository at this point in the history
  • Loading branch information
sokra committed Jul 2, 2018
1 parent e8dc361 commit 1ad71e0
Show file tree
Hide file tree
Showing 18 changed files with 215 additions and 2 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
36 changes: 36 additions & 0 deletions test/__snapshots__/StatsTestCases.test.js.snap
Expand Up @@ -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"
`;
Empty file.
1 change: 1 addition & 0 deletions test/statsCases/wasm-in-initial-chunk-error/index.js
@@ -0,0 +1 @@
import "./module";
7 changes: 7 additions & 0 deletions test/statsCases/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/statsCases/wasm-in-initial-chunk-error/module2.js
@@ -0,0 +1,2 @@
require("./wasm.wat");
require("./module3");
2 changes: 2 additions & 0 deletions test/statsCases/wasm-in-initial-chunk-error/module3.js
@@ -0,0 +1,2 @@
require("./wasm.wat");
require("./wasm2.wat");
4 changes: 4 additions & 0 deletions test/statsCases/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/statsCases/wasm-in-initial-chunk-error/wasm2.wat
@@ -0,0 +1,4 @@
(module
(func $getNumber (export "getNumber") (result i32)
(i32.const 42)))

12 changes: 12 additions & 0 deletions test/statsCases/wasm-in-initial-chunk-error/webpack.config.js
@@ -0,0 +1,12 @@
module.exports = {
entry: "./index",
module: {
rules: [
{
test: /\.wat$/,
loader: "wast-loader",
type: "webassembly/experimental"
}
]
}
};

0 comments on commit 1ad71e0

Please sign in to comment.