342 lines
10 KiB
JavaScript
342 lines
10 KiB
JavaScript
/*
|
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
|
Author Tobias Koppers @sokra
|
|
*/
|
|
"use strict";
|
|
|
|
const Template = require("../Template");
|
|
const WebAssemblyUtils = require("./WebAssemblyUtils");
|
|
|
|
/** @typedef {import("../Module")} Module */
|
|
/** @typedef {import("../MainTemplate")} MainTemplate */
|
|
|
|
// Get all wasm modules
|
|
const getAllWasmModules = chunk => {
|
|
const wasmModules = chunk.getAllAsyncChunks();
|
|
const array = [];
|
|
for (const chunk of wasmModules) {
|
|
for (const m of chunk.modulesIterable) {
|
|
if (m.type.startsWith("webassembly")) {
|
|
array.push(m);
|
|
}
|
|
}
|
|
}
|
|
|
|
return array;
|
|
};
|
|
|
|
/**
|
|
* generates the import object function for a module
|
|
* @param {Module} module the module
|
|
* @param {boolean} mangle mangle imports
|
|
* @returns {string} source code
|
|
*/
|
|
const generateImportObject = (module, mangle) => {
|
|
const waitForInstances = new Map();
|
|
const properties = [];
|
|
const usedWasmDependencies = WebAssemblyUtils.getUsedDependencies(
|
|
module,
|
|
mangle
|
|
);
|
|
for (const usedDep of usedWasmDependencies) {
|
|
const dep = usedDep.dependency;
|
|
const importedModule = dep.module;
|
|
const exportName = dep.name;
|
|
const usedName = importedModule && importedModule.isUsed(exportName);
|
|
const description = dep.description;
|
|
const direct = dep.onlyDirectImport;
|
|
|
|
const module = usedDep.module;
|
|
const name = usedDep.name;
|
|
|
|
if (direct) {
|
|
const instanceVar = `m${waitForInstances.size}`;
|
|
waitForInstances.set(instanceVar, importedModule.id);
|
|
properties.push({
|
|
module,
|
|
name,
|
|
value: `${instanceVar}[${JSON.stringify(usedName)}]`
|
|
});
|
|
} else {
|
|
const params = description.signature.params.map(
|
|
(param, k) => "p" + k + param.valtype
|
|
);
|
|
|
|
const mod = `installedModules[${JSON.stringify(importedModule.id)}]`;
|
|
const func = `${mod}.exports[${JSON.stringify(usedName)}]`;
|
|
|
|
properties.push({
|
|
module,
|
|
name,
|
|
value: Template.asString([
|
|
(importedModule.type.startsWith("webassembly")
|
|
? `${mod} ? ${func} : `
|
|
: "") + `function(${params}) {`,
|
|
Template.indent([`return ${func}(${params});`]),
|
|
"}"
|
|
])
|
|
});
|
|
}
|
|
}
|
|
|
|
let importObject;
|
|
if (mangle) {
|
|
importObject = [
|
|
"return {",
|
|
Template.indent([
|
|
properties.map(p => `${JSON.stringify(p.name)}: ${p.value}`).join(",\n")
|
|
]),
|
|
"};"
|
|
];
|
|
} else {
|
|
const propertiesByModule = new Map();
|
|
for (const p of properties) {
|
|
let list = propertiesByModule.get(p.module);
|
|
if (list === undefined) {
|
|
propertiesByModule.set(p.module, (list = []));
|
|
}
|
|
list.push(p);
|
|
}
|
|
importObject = [
|
|
"return {",
|
|
Template.indent([
|
|
Array.from(propertiesByModule, ([module, list]) => {
|
|
return Template.asString([
|
|
`${JSON.stringify(module)}: {`,
|
|
Template.indent([
|
|
list.map(p => `${JSON.stringify(p.name)}: ${p.value}`).join(",\n")
|
|
]),
|
|
"}"
|
|
]);
|
|
}).join(",\n")
|
|
]),
|
|
"};"
|
|
];
|
|
}
|
|
|
|
if (waitForInstances.size === 1) {
|
|
const moduleId = Array.from(waitForInstances.values())[0];
|
|
const promise = `installedWasmModules[${JSON.stringify(moduleId)}]`;
|
|
const variable = Array.from(waitForInstances.keys())[0];
|
|
return Template.asString([
|
|
`${JSON.stringify(module.id)}: function() {`,
|
|
Template.indent([
|
|
`return promiseResolve().then(function() { return ${promise}; }).then(function(${variable}) {`,
|
|
Template.indent(importObject),
|
|
"});"
|
|
]),
|
|
"},"
|
|
]);
|
|
} else if (waitForInstances.size > 0) {
|
|
const promises = Array.from(
|
|
waitForInstances.values(),
|
|
id => `installedWasmModules[${JSON.stringify(id)}]`
|
|
).join(", ");
|
|
const variables = Array.from(
|
|
waitForInstances.keys(),
|
|
(name, i) => `${name} = array[${i}]`
|
|
).join(", ");
|
|
return Template.asString([
|
|
`${JSON.stringify(module.id)}: function() {`,
|
|
Template.indent([
|
|
`return promiseResolve().then(function() { return Promise.all([${promises}]); }).then(function(array) {`,
|
|
Template.indent([`var ${variables};`, ...importObject]),
|
|
"});"
|
|
]),
|
|
"},"
|
|
]);
|
|
} else {
|
|
return Template.asString([
|
|
`${JSON.stringify(module.id)}: function() {`,
|
|
Template.indent(importObject),
|
|
"},"
|
|
]);
|
|
}
|
|
};
|
|
|
|
class WasmMainTemplatePlugin {
|
|
constructor({ generateLoadBinaryCode, supportsStreaming, mangleImports }) {
|
|
this.generateLoadBinaryCode = generateLoadBinaryCode;
|
|
this.supportsStreaming = supportsStreaming;
|
|
this.mangleImports = mangleImports;
|
|
}
|
|
|
|
/**
|
|
* @param {MainTemplate} mainTemplate main template
|
|
* @returns {void}
|
|
*/
|
|
apply(mainTemplate) {
|
|
mainTemplate.hooks.localVars.tap(
|
|
"WasmMainTemplatePlugin",
|
|
(source, chunk) => {
|
|
const wasmModules = getAllWasmModules(chunk);
|
|
if (wasmModules.length === 0) return source;
|
|
const importObjects = wasmModules.map(module => {
|
|
return generateImportObject(module, this.mangleImports);
|
|
});
|
|
return Template.asString([
|
|
source,
|
|
"",
|
|
"// object to store loaded and loading wasm modules",
|
|
"var installedWasmModules = {};",
|
|
"",
|
|
// This function is used to delay reading the installed wasm module promises
|
|
// by a microtask. Sorting them doesn't help because there are egdecases where
|
|
// sorting is not possible (modules splitted into different chunks).
|
|
// So we not even trying and solve this by a microtask delay.
|
|
"function promiseResolve() { return Promise.resolve(); }",
|
|
"",
|
|
"var wasmImportObjects = {",
|
|
Template.indent(importObjects),
|
|
"};"
|
|
]);
|
|
}
|
|
);
|
|
mainTemplate.hooks.requireEnsure.tap(
|
|
"WasmMainTemplatePlugin",
|
|
(source, chunk, hash) => {
|
|
const webassemblyModuleFilename =
|
|
mainTemplate.outputOptions.webassemblyModuleFilename;
|
|
|
|
const chunkModuleMaps = chunk.getChunkModuleMaps(m =>
|
|
m.type.startsWith("webassembly")
|
|
);
|
|
if (Object.keys(chunkModuleMaps.id).length === 0) return source;
|
|
const wasmModuleSrcPath = mainTemplate.getAssetPath(
|
|
JSON.stringify(webassemblyModuleFilename),
|
|
{
|
|
hash: `" + ${mainTemplate.renderCurrentHashCode(hash)} + "`,
|
|
hashWithLength: length =>
|
|
`" + ${mainTemplate.renderCurrentHashCode(hash, length)} + "`,
|
|
module: {
|
|
id: '" + wasmModuleId + "',
|
|
hash: `" + ${JSON.stringify(
|
|
chunkModuleMaps.hash
|
|
)}[wasmModuleId] + "`,
|
|
hashWithLength(length) {
|
|
const shortChunkHashMap = Object.create(null);
|
|
for (const wasmModuleId of Object.keys(chunkModuleMaps.hash)) {
|
|
if (typeof chunkModuleMaps.hash[wasmModuleId] === "string") {
|
|
shortChunkHashMap[wasmModuleId] = chunkModuleMaps.hash[
|
|
wasmModuleId
|
|
].substr(0, length);
|
|
}
|
|
}
|
|
return `" + ${JSON.stringify(
|
|
shortChunkHashMap
|
|
)}[wasmModuleId] + "`;
|
|
}
|
|
}
|
|
}
|
|
);
|
|
const createImportObject = content =>
|
|
this.mangleImports
|
|
? `{ ${JSON.stringify(
|
|
WebAssemblyUtils.MANGLED_MODULE
|
|
)}: ${content} }`
|
|
: content;
|
|
return Template.asString([
|
|
source,
|
|
"",
|
|
"// Fetch + compile chunk loading for webassembly",
|
|
"",
|
|
`var wasmModules = ${JSON.stringify(
|
|
chunkModuleMaps.id
|
|
)}[chunkId] || [];`,
|
|
"",
|
|
"wasmModules.forEach(function(wasmModuleId) {",
|
|
Template.indent([
|
|
"var installedWasmModuleData = installedWasmModules[wasmModuleId];",
|
|
"",
|
|
'// a Promise means "currently loading" or "already loaded".',
|
|
"if(installedWasmModuleData)",
|
|
Template.indent(["promises.push(installedWasmModuleData);"]),
|
|
"else {",
|
|
Template.indent([
|
|
`var importObject = wasmImportObjects[wasmModuleId]();`,
|
|
`var req = ${this.generateLoadBinaryCode(wasmModuleSrcPath)};`,
|
|
"var promise;",
|
|
this.supportsStreaming
|
|
? Template.asString([
|
|
"if(importObject instanceof Promise && typeof WebAssembly.compileStreaming === 'function') {",
|
|
Template.indent([
|
|
"promise = Promise.all([WebAssembly.compileStreaming(req), importObject]).then(function(items) {",
|
|
Template.indent([
|
|
`return WebAssembly.instantiate(items[0], ${createImportObject(
|
|
"items[1]"
|
|
)});`
|
|
]),
|
|
"});"
|
|
]),
|
|
"} else if(typeof WebAssembly.instantiateStreaming === 'function') {",
|
|
Template.indent([
|
|
`promise = WebAssembly.instantiateStreaming(req, ${createImportObject(
|
|
"importObject"
|
|
)});`
|
|
])
|
|
])
|
|
: Template.asString([
|
|
"if(importObject instanceof Promise) {",
|
|
Template.indent([
|
|
"var bytesPromise = req.then(function(x) { return x.arrayBuffer(); });",
|
|
"promise = Promise.all([",
|
|
Template.indent([
|
|
"bytesPromise.then(function(bytes) { return WebAssembly.compile(bytes); }),",
|
|
"importObject"
|
|
]),
|
|
"]).then(function(items) {",
|
|
Template.indent([
|
|
`return WebAssembly.instantiate(items[0], ${createImportObject(
|
|
"items[1]"
|
|
)});`
|
|
]),
|
|
"});"
|
|
])
|
|
]),
|
|
"} else {",
|
|
Template.indent([
|
|
"var bytesPromise = req.then(function(x) { return x.arrayBuffer(); });",
|
|
"promise = bytesPromise.then(function(bytes) {",
|
|
Template.indent([
|
|
`return WebAssembly.instantiate(bytes, ${createImportObject(
|
|
"importObject"
|
|
)});`
|
|
]),
|
|
"});"
|
|
]),
|
|
"}",
|
|
"promises.push(installedWasmModules[wasmModuleId] = promise.then(function(res) {",
|
|
Template.indent([
|
|
`return ${mainTemplate.requireFn}.w[wasmModuleId] = (res.instance || res).exports;`
|
|
]),
|
|
"}));"
|
|
]),
|
|
"}"
|
|
]),
|
|
"});"
|
|
]);
|
|
}
|
|
);
|
|
mainTemplate.hooks.requireExtensions.tap(
|
|
"WasmMainTemplatePlugin",
|
|
(source, chunk) => {
|
|
if (!chunk.hasModuleInGraph(m => m.type.startsWith("webassembly"))) {
|
|
return source;
|
|
}
|
|
return Template.asString([
|
|
source,
|
|
"",
|
|
"// object with all WebAssembly.instance exports",
|
|
`${mainTemplate.requireFn}.w = {};`
|
|
]);
|
|
}
|
|
);
|
|
mainTemplate.hooks.hash.tap("WasmMainTemplatePlugin", hash => {
|
|
hash.update("WasmMainTemplatePlugin");
|
|
hash.update("2");
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = WasmMainTemplatePlugin;
|