You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

414 lines
12 KiB

/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
const RuntimeGlobals = require("../RuntimeGlobals");
const RuntimeModule = require("../RuntimeModule");
const Template = require("../Template");
const { compareModulesByIdentifier } = require("../util/comparators");
const WebAssemblyUtils = require("./WebAssemblyUtils");
/** @typedef {import("@webassemblyjs/ast").Signature} Signature */
/** @typedef {import("../Chunk")} Chunk */
/** @typedef {import("../ChunkGraph")} ChunkGraph */
/** @typedef {import("../ChunkGraph").ModuleId} ModuleId */
/** @typedef {import("../Compilation")} Compilation */
/** @typedef {import("../Module")} Module */
/** @typedef {import("../Module").ReadOnlyRuntimeRequirements} ReadOnlyRuntimeRequirements */
/** @typedef {import("../ModuleGraph")} ModuleGraph */
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
// TODO webpack 6 remove the whole folder
// Get all wasm modules
/**
* @param {ModuleGraph} moduleGraph the module graph
* @param {ChunkGraph} chunkGraph the chunk graph
* @param {Chunk} chunk the chunk
* @returns {Module[]} all wasm modules
*/
const getAllWasmModules = (moduleGraph, chunkGraph, chunk) => {
const wasmModules = chunk.getAllAsyncChunks();
const array = [];
for (const chunk of wasmModules) {
for (const m of chunkGraph.getOrderedChunkModulesIterable(
chunk,
compareModulesByIdentifier
)) {
if (m.type.startsWith("webassembly")) {
array.push(m);
}
}
}
return array;
};
/**
* generates the import object function for a module
* @param {ChunkGraph} chunkGraph the chunk graph
* @param {Module} module the module
* @param {boolean | undefined} mangle mangle imports
* @param {string[]} declarations array where declarations are pushed to
* @param {RuntimeSpec} runtime the runtime
* @returns {string} source code
*/
const generateImportObject = (
chunkGraph,
module,
mangle,
declarations,
runtime
) => {
const moduleGraph = chunkGraph.moduleGraph;
/** @type {Map<string, string | number>} */
const waitForInstances = new Map();
const properties = [];
const usedWasmDependencies = WebAssemblyUtils.getUsedDependencies(
moduleGraph,
module,
mangle
);
for (const usedDep of usedWasmDependencies) {
const dep = usedDep.dependency;
const importedModule = moduleGraph.getModule(dep);
const exportName = dep.name;
const usedName =
importedModule &&
moduleGraph
.getExportsInfo(importedModule)
.getUsedName(exportName, runtime);
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,
/** @type {ModuleId} */
(chunkGraph.getModuleId(/** @type {Module} */ (importedModule)))
);
properties.push({
module,
name,
value: `${instanceVar}[${JSON.stringify(usedName)}]`
});
} else {
const params =
/** @type {Signature} */
(description.signature).params.map(
(param, k) => `p${k}${param.valtype}`
);
const mod = `${RuntimeGlobals.moduleCache}[${JSON.stringify(
chunkGraph.getModuleId(/** @type {Module} */ (importedModule))
)}]`;
const modExports = `${mod}.exports`;
const cache = `wasmImportedFuncCache${declarations.length}`;
declarations.push(`var ${cache};`);
const modCode =
/** @type {Module} */
(importedModule).type.startsWith("webassembly")
? `${mod} ? ${modExports}[${JSON.stringify(usedName)}] : `
: "";
properties.push({
module,
name,
value: Template.asString([
`${modCode}function(${params}) {`,
Template.indent([
`if(${cache} === undefined) ${cache} = ${modExports};`,
`return ${cache}[${JSON.stringify(usedName)}](${params});`
]),
"}"
])
});
}
}
let importObject;
if (mangle) {
importObject = [
"return {",
Template.indent([
properties.map(p => `${JSON.stringify(p.name)}: ${p.value}`).join(",\n")
]),
"};"
];
} else {
/** @type {Map<string, Array<{ name: string, value: string }>>} */
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]) =>
Template.asString([
`${JSON.stringify(module)}: {`,
Template.indent([
list.map(p => `${JSON.stringify(p.name)}: ${p.value}`).join(",\n")
]),
"}"
])
).join(",\n")
]),
"};"
];
}
const moduleIdStringified = JSON.stringify(chunkGraph.getModuleId(module));
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([
`${moduleIdStringified}: 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([
`${moduleIdStringified}: function() {`,
Template.indent([
`return promiseResolve().then(function() { return Promise.all([${promises}]); }).then(function(array) {`,
Template.indent([`var ${variables};`, ...importObject]),
"});"
]),
"},"
]);
}
return Template.asString([
`${moduleIdStringified}: function() {`,
Template.indent(importObject),
"},"
]);
};
/**
* @typedef {object} WasmChunkLoadingRuntimeModuleOptions
* @property {(path: string) => string} generateLoadBinaryCode
* @property {boolean} [supportsStreaming]
* @property {boolean} [mangleImports]
* @property {ReadOnlyRuntimeRequirements} runtimeRequirements
*/
class WasmChunkLoadingRuntimeModule extends RuntimeModule {
/**
* @param {WasmChunkLoadingRuntimeModuleOptions} options options
*/
constructor({
generateLoadBinaryCode,
supportsStreaming,
mangleImports,
runtimeRequirements
}) {
super("wasm chunk loading", RuntimeModule.STAGE_ATTACH);
this.generateLoadBinaryCode = generateLoadBinaryCode;
this.supportsStreaming = supportsStreaming;
this.mangleImports = mangleImports;
this._runtimeRequirements = runtimeRequirements;
}
/**
* @returns {string | null} runtime code
*/
generate() {
const fn = RuntimeGlobals.ensureChunkHandlers;
const withHmr = this._runtimeRequirements.has(
RuntimeGlobals.hmrDownloadUpdateHandlers
);
const compilation = /** @type {Compilation} */ (this.compilation);
const { moduleGraph, outputOptions } = compilation;
const chunkGraph = /** @type {ChunkGraph} */ (this.chunkGraph);
const chunk = /** @type {Chunk} */ (this.chunk);
const wasmModules = getAllWasmModules(moduleGraph, chunkGraph, chunk);
const { mangleImports } = this;
/** @type {string[]} */
const declarations = [];
const importObjects = wasmModules.map(module =>
generateImportObject(
chunkGraph,
module,
mangleImports,
declarations,
chunk.runtime
)
);
const chunkModuleIdMap = chunkGraph.getChunkModuleIdMap(chunk, m =>
m.type.startsWith("webassembly")
);
/**
* @param {string} content content
* @returns {string} created import object
*/
const createImportObject = content =>
mangleImports
? `{ ${JSON.stringify(WebAssemblyUtils.MANGLED_MODULE)}: ${content} }`
: content;
const wasmModuleSrcPath = compilation.getPath(
JSON.stringify(outputOptions.webassemblyModuleFilename),
{
hash: `" + ${RuntimeGlobals.getFullHash}() + "`,
hashWithLength: length =>
`" + ${RuntimeGlobals.getFullHash}}().slice(0, ${length}) + "`,
module: {
id: '" + wasmModuleId + "',
hash: `" + ${JSON.stringify(
chunkGraph.getChunkModuleRenderedHashMap(chunk, m =>
m.type.startsWith("webassembly")
)
)}[chunkId][wasmModuleId] + "`,
hashWithLength(length) {
return `" + ${JSON.stringify(
chunkGraph.getChunkModuleRenderedHashMap(
chunk,
m => m.type.startsWith("webassembly"),
length
)
)}[chunkId][wasmModuleId] + "`;
}
},
runtime: chunk.runtime
}
);
const stateExpression = withHmr
? `${RuntimeGlobals.hmrRuntimeStatePrefix}_wasm`
: undefined;
return Template.asString([
"// object to store loaded and loading wasm modules",
`var installedWasmModules = ${
stateExpression ? `${stateExpression} = ${stateExpression} || ` : ""
}{};`,
"",
// This function is used to delay reading the installed wasm module promises
// by a microtask. Sorting them doesn't help because there are edge cases 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(); }",
"",
Template.asString(declarations),
"var wasmImportObjects = {",
Template.indent(importObjects),
"};",
"",
`var wasmModuleMap = ${JSON.stringify(
chunkModuleIdMap,
undefined,
"\t"
)};`,
"",
"// object with all WebAssembly.instance exports",
`${RuntimeGlobals.wasmInstances} = {};`,
"",
"// Fetch + compile chunk loading for webassembly",
`${fn}.wasm = function(chunkId, promises) {`,
Template.indent([
"",
"var wasmModules = wasmModuleMap[chunkId] || [];",
"",
"wasmModules.forEach(function(wasmModuleId, idx) {",
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 && typeof importObject.then === 'function' && 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 && typeof importObject.then === 'function') {",
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 ${RuntimeGlobals.wasmInstances}[wasmModuleId] = (res.instance || res).exports;`
]),
"}));"
]),
"}"
]),
"});"
]),
"};"
]);
}
}
module.exports = WasmChunkLoadingRuntimeModule;