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
414 lines
12 KiB
4 weeks ago
|
/*
|
||
|
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;
|