WasmChunkLoadingRuntimeModule.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. */
  4. "use strict";
  5. const RuntimeGlobals = require("../RuntimeGlobals");
  6. const RuntimeModule = require("../RuntimeModule");
  7. const Template = require("../Template");
  8. const { compareModulesByIdentifier } = require("../util/comparators");
  9. const WebAssemblyUtils = require("./WebAssemblyUtils");
  10. /** @typedef {import("@webassemblyjs/ast").Signature} Signature */
  11. /** @typedef {import("../Chunk")} Chunk */
  12. /** @typedef {import("../ChunkGraph")} ChunkGraph */
  13. /** @typedef {import("../Compilation")} Compilation */
  14. /** @typedef {import("../Module")} Module */
  15. /** @typedef {import("../Module").ReadOnlyRuntimeRequirements} ReadOnlyRuntimeRequirements */
  16. /** @typedef {import("../ModuleGraph")} ModuleGraph */
  17. /** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
  18. // TODO webpack 6 remove the whole folder
  19. // Get all wasm modules
  20. /**
  21. * @param {ModuleGraph} moduleGraph the module graph
  22. * @param {ChunkGraph} chunkGraph the chunk graph
  23. * @param {Chunk} chunk the chunk
  24. * @returns {Module[]} all wasm modules
  25. */
  26. const getAllWasmModules = (moduleGraph, chunkGraph, chunk) => {
  27. const wasmModules = chunk.getAllAsyncChunks();
  28. const array = [];
  29. for (const chunk of wasmModules) {
  30. for (const m of chunkGraph.getOrderedChunkModulesIterable(
  31. chunk,
  32. compareModulesByIdentifier
  33. )) {
  34. if (m.type.startsWith("webassembly")) {
  35. array.push(m);
  36. }
  37. }
  38. }
  39. return array;
  40. };
  41. /**
  42. * generates the import object function for a module
  43. * @param {ChunkGraph} chunkGraph the chunk graph
  44. * @param {Module} module the module
  45. * @param {boolean | undefined} mangle mangle imports
  46. * @param {string[]} declarations array where declarations are pushed to
  47. * @param {RuntimeSpec} runtime the runtime
  48. * @returns {string} source code
  49. */
  50. const generateImportObject = (
  51. chunkGraph,
  52. module,
  53. mangle,
  54. declarations,
  55. runtime
  56. ) => {
  57. const moduleGraph = chunkGraph.moduleGraph;
  58. /** @type {Map<string, string | number>} */
  59. const waitForInstances = new Map();
  60. const properties = [];
  61. const usedWasmDependencies = WebAssemblyUtils.getUsedDependencies(
  62. moduleGraph,
  63. module,
  64. mangle
  65. );
  66. for (const usedDep of usedWasmDependencies) {
  67. const dep = usedDep.dependency;
  68. const importedModule = moduleGraph.getModule(dep);
  69. const exportName = dep.name;
  70. const usedName =
  71. importedModule &&
  72. moduleGraph
  73. .getExportsInfo(importedModule)
  74. .getUsedName(exportName, runtime);
  75. const description = dep.description;
  76. const direct = dep.onlyDirectImport;
  77. const module = usedDep.module;
  78. const name = usedDep.name;
  79. if (direct) {
  80. const instanceVar = `m${waitForInstances.size}`;
  81. waitForInstances.set(
  82. instanceVar,
  83. chunkGraph.getModuleId(/** @type {Module} */ (importedModule))
  84. );
  85. properties.push({
  86. module,
  87. name,
  88. value: `${instanceVar}[${JSON.stringify(usedName)}]`
  89. });
  90. } else {
  91. const params =
  92. /** @type {Signature} */
  93. (description.signature).params.map(
  94. (param, k) => "p" + k + param.valtype
  95. );
  96. const mod = `${RuntimeGlobals.moduleCache}[${JSON.stringify(
  97. chunkGraph.getModuleId(/** @type {Module} */ (importedModule))
  98. )}]`;
  99. const modExports = `${mod}.exports`;
  100. const cache = `wasmImportedFuncCache${declarations.length}`;
  101. declarations.push(`var ${cache};`);
  102. const modCode =
  103. /** @type {Module} */
  104. (importedModule).type.startsWith("webassembly")
  105. ? `${mod} ? ${modExports}[${JSON.stringify(usedName)}] : `
  106. : "";
  107. properties.push({
  108. module,
  109. name,
  110. value: Template.asString([
  111. modCode + `function(${params}) {`,
  112. Template.indent([
  113. `if(${cache} === undefined) ${cache} = ${modExports};`,
  114. `return ${cache}[${JSON.stringify(usedName)}](${params});`
  115. ]),
  116. "}"
  117. ])
  118. });
  119. }
  120. }
  121. let importObject;
  122. if (mangle) {
  123. importObject = [
  124. "return {",
  125. Template.indent([
  126. properties.map(p => `${JSON.stringify(p.name)}: ${p.value}`).join(",\n")
  127. ]),
  128. "};"
  129. ];
  130. } else {
  131. /** @type {Map<string, Array<{ name: string, value: string }>>} */
  132. const propertiesByModule = new Map();
  133. for (const p of properties) {
  134. let list = propertiesByModule.get(p.module);
  135. if (list === undefined) {
  136. propertiesByModule.set(p.module, (list = []));
  137. }
  138. list.push(p);
  139. }
  140. importObject = [
  141. "return {",
  142. Template.indent([
  143. Array.from(propertiesByModule, ([module, list]) => {
  144. return Template.asString([
  145. `${JSON.stringify(module)}: {`,
  146. Template.indent([
  147. list.map(p => `${JSON.stringify(p.name)}: ${p.value}`).join(",\n")
  148. ]),
  149. "}"
  150. ]);
  151. }).join(",\n")
  152. ]),
  153. "};"
  154. ];
  155. }
  156. const moduleIdStringified = JSON.stringify(chunkGraph.getModuleId(module));
  157. if (waitForInstances.size === 1) {
  158. const moduleId = Array.from(waitForInstances.values())[0];
  159. const promise = `installedWasmModules[${JSON.stringify(moduleId)}]`;
  160. const variable = Array.from(waitForInstances.keys())[0];
  161. return Template.asString([
  162. `${moduleIdStringified}: function() {`,
  163. Template.indent([
  164. `return promiseResolve().then(function() { return ${promise}; }).then(function(${variable}) {`,
  165. Template.indent(importObject),
  166. "});"
  167. ]),
  168. "},"
  169. ]);
  170. } else if (waitForInstances.size > 0) {
  171. const promises = Array.from(
  172. waitForInstances.values(),
  173. id => `installedWasmModules[${JSON.stringify(id)}]`
  174. ).join(", ");
  175. const variables = Array.from(
  176. waitForInstances.keys(),
  177. (name, i) => `${name} = array[${i}]`
  178. ).join(", ");
  179. return Template.asString([
  180. `${moduleIdStringified}: function() {`,
  181. Template.indent([
  182. `return promiseResolve().then(function() { return Promise.all([${promises}]); }).then(function(array) {`,
  183. Template.indent([`var ${variables};`, ...importObject]),
  184. "});"
  185. ]),
  186. "},"
  187. ]);
  188. } else {
  189. return Template.asString([
  190. `${moduleIdStringified}: function() {`,
  191. Template.indent(importObject),
  192. "},"
  193. ]);
  194. }
  195. };
  196. /**
  197. * @typedef {object} WasmChunkLoadingRuntimeModuleOptions
  198. * @property {(path: string) => string} generateLoadBinaryCode
  199. * @property {boolean} [supportsStreaming]
  200. * @property {boolean} [mangleImports]
  201. * @property {ReadOnlyRuntimeRequirements} runtimeRequirements
  202. */
  203. class WasmChunkLoadingRuntimeModule extends RuntimeModule {
  204. /**
  205. * @param {WasmChunkLoadingRuntimeModuleOptions} options options
  206. */
  207. constructor({
  208. generateLoadBinaryCode,
  209. supportsStreaming,
  210. mangleImports,
  211. runtimeRequirements
  212. }) {
  213. super("wasm chunk loading", RuntimeModule.STAGE_ATTACH);
  214. this.generateLoadBinaryCode = generateLoadBinaryCode;
  215. this.supportsStreaming = supportsStreaming;
  216. this.mangleImports = mangleImports;
  217. this._runtimeRequirements = runtimeRequirements;
  218. }
  219. /**
  220. * @returns {string | null} runtime code
  221. */
  222. generate() {
  223. const fn = RuntimeGlobals.ensureChunkHandlers;
  224. const withHmr = this._runtimeRequirements.has(
  225. RuntimeGlobals.hmrDownloadUpdateHandlers
  226. );
  227. const compilation = /** @type {Compilation} */ (this.compilation);
  228. const { moduleGraph, outputOptions } = compilation;
  229. const chunkGraph = /** @type {ChunkGraph} */ (this.chunkGraph);
  230. const chunk = /** @type {Chunk} */ (this.chunk);
  231. const wasmModules = getAllWasmModules(moduleGraph, chunkGraph, chunk);
  232. const { mangleImports } = this;
  233. /** @type {string[]} */
  234. const declarations = [];
  235. const importObjects = wasmModules.map(module => {
  236. return generateImportObject(
  237. chunkGraph,
  238. module,
  239. mangleImports,
  240. declarations,
  241. chunk.runtime
  242. );
  243. });
  244. const chunkModuleIdMap = chunkGraph.getChunkModuleIdMap(chunk, m =>
  245. m.type.startsWith("webassembly")
  246. );
  247. /**
  248. * @param {string} content content
  249. * @returns {string} created import object
  250. */
  251. const createImportObject = content =>
  252. mangleImports
  253. ? `{ ${JSON.stringify(WebAssemblyUtils.MANGLED_MODULE)}: ${content} }`
  254. : content;
  255. const wasmModuleSrcPath = compilation.getPath(
  256. JSON.stringify(outputOptions.webassemblyModuleFilename),
  257. {
  258. hash: `" + ${RuntimeGlobals.getFullHash}() + "`,
  259. hashWithLength: length =>
  260. `" + ${RuntimeGlobals.getFullHash}}().slice(0, ${length}) + "`,
  261. module: {
  262. id: '" + wasmModuleId + "',
  263. hash: `" + ${JSON.stringify(
  264. chunkGraph.getChunkModuleRenderedHashMap(chunk, m =>
  265. m.type.startsWith("webassembly")
  266. )
  267. )}[chunkId][wasmModuleId] + "`,
  268. hashWithLength(length) {
  269. return `" + ${JSON.stringify(
  270. chunkGraph.getChunkModuleRenderedHashMap(
  271. chunk,
  272. m => m.type.startsWith("webassembly"),
  273. length
  274. )
  275. )}[chunkId][wasmModuleId] + "`;
  276. }
  277. },
  278. runtime: chunk.runtime
  279. }
  280. );
  281. const stateExpression = withHmr
  282. ? `${RuntimeGlobals.hmrRuntimeStatePrefix}_wasm`
  283. : undefined;
  284. return Template.asString([
  285. "// object to store loaded and loading wasm modules",
  286. `var installedWasmModules = ${
  287. stateExpression ? `${stateExpression} = ${stateExpression} || ` : ""
  288. }{};`,
  289. "",
  290. // This function is used to delay reading the installed wasm module promises
  291. // by a microtask. Sorting them doesn't help because there are edge cases where
  292. // sorting is not possible (modules splitted into different chunks).
  293. // So we not even trying and solve this by a microtask delay.
  294. "function promiseResolve() { return Promise.resolve(); }",
  295. "",
  296. Template.asString(declarations),
  297. "var wasmImportObjects = {",
  298. Template.indent(importObjects),
  299. "};",
  300. "",
  301. `var wasmModuleMap = ${JSON.stringify(
  302. chunkModuleIdMap,
  303. undefined,
  304. "\t"
  305. )};`,
  306. "",
  307. "// object with all WebAssembly.instance exports",
  308. `${RuntimeGlobals.wasmInstances} = {};`,
  309. "",
  310. "// Fetch + compile chunk loading for webassembly",
  311. `${fn}.wasm = function(chunkId, promises) {`,
  312. Template.indent([
  313. "",
  314. `var wasmModules = wasmModuleMap[chunkId] || [];`,
  315. "",
  316. "wasmModules.forEach(function(wasmModuleId, idx) {",
  317. Template.indent([
  318. "var installedWasmModuleData = installedWasmModules[wasmModuleId];",
  319. "",
  320. '// a Promise means "currently loading" or "already loaded".',
  321. "if(installedWasmModuleData)",
  322. Template.indent(["promises.push(installedWasmModuleData);"]),
  323. "else {",
  324. Template.indent([
  325. `var importObject = wasmImportObjects[wasmModuleId]();`,
  326. `var req = ${this.generateLoadBinaryCode(wasmModuleSrcPath)};`,
  327. "var promise;",
  328. this.supportsStreaming
  329. ? Template.asString([
  330. "if(importObject && typeof importObject.then === 'function' && typeof WebAssembly.compileStreaming === 'function') {",
  331. Template.indent([
  332. "promise = Promise.all([WebAssembly.compileStreaming(req), importObject]).then(function(items) {",
  333. Template.indent([
  334. `return WebAssembly.instantiate(items[0], ${createImportObject(
  335. "items[1]"
  336. )});`
  337. ]),
  338. "});"
  339. ]),
  340. "} else if(typeof WebAssembly.instantiateStreaming === 'function') {",
  341. Template.indent([
  342. `promise = WebAssembly.instantiateStreaming(req, ${createImportObject(
  343. "importObject"
  344. )});`
  345. ])
  346. ])
  347. : Template.asString([
  348. "if(importObject && typeof importObject.then === 'function') {",
  349. Template.indent([
  350. "var bytesPromise = req.then(function(x) { return x.arrayBuffer(); });",
  351. "promise = Promise.all([",
  352. Template.indent([
  353. "bytesPromise.then(function(bytes) { return WebAssembly.compile(bytes); }),",
  354. "importObject"
  355. ]),
  356. "]).then(function(items) {",
  357. Template.indent([
  358. `return WebAssembly.instantiate(items[0], ${createImportObject(
  359. "items[1]"
  360. )});`
  361. ]),
  362. "});"
  363. ])
  364. ]),
  365. "} else {",
  366. Template.indent([
  367. "var bytesPromise = req.then(function(x) { return x.arrayBuffer(); });",
  368. "promise = bytesPromise.then(function(bytes) {",
  369. Template.indent([
  370. `return WebAssembly.instantiate(bytes, ${createImportObject(
  371. "importObject"
  372. )});`
  373. ]),
  374. "});"
  375. ]),
  376. "}",
  377. "promises.push(installedWasmModules[wasmModuleId] = promise.then(function(res) {",
  378. Template.indent([
  379. `return ${RuntimeGlobals.wasmInstances}[wasmModuleId] = (res.instance || res).exports;`
  380. ]),
  381. "}));"
  382. ]),
  383. "}"
  384. ]),
  385. "});"
  386. ]),
  387. "};"
  388. ]);
  389. }
  390. }
  391. module.exports = WasmChunkLoadingRuntimeModule;