WasmMainTemplatePlugin.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const Template = require("../Template");
  7. const WebAssemblyUtils = require("./WebAssemblyUtils");
  8. /** @typedef {import("../Module")} Module */
  9. /** @typedef {import("../MainTemplate")} MainTemplate */
  10. // Get all wasm modules
  11. const getAllWasmModules = chunk => {
  12. const wasmModules = chunk.getAllAsyncChunks();
  13. const array = [];
  14. for (const chunk of wasmModules) {
  15. for (const m of chunk.modulesIterable) {
  16. if (m.type.startsWith("webassembly")) {
  17. array.push(m);
  18. }
  19. }
  20. }
  21. return array;
  22. };
  23. /**
  24. * generates the import object function for a module
  25. * @param {Module} module the module
  26. * @param {boolean} mangle mangle imports
  27. * @returns {string} source code
  28. */
  29. const generateImportObject = (module, mangle) => {
  30. const waitForInstances = new Map();
  31. const properties = [];
  32. const usedWasmDependencies = WebAssemblyUtils.getUsedDependencies(
  33. module,
  34. mangle
  35. );
  36. for (const usedDep of usedWasmDependencies) {
  37. const dep = usedDep.dependency;
  38. const importedModule = dep.module;
  39. const exportName = dep.name;
  40. const usedName = importedModule && importedModule.isUsed(exportName);
  41. const description = dep.description;
  42. const direct = dep.onlyDirectImport;
  43. const module = usedDep.module;
  44. const name = usedDep.name;
  45. if (direct) {
  46. const instanceVar = `m${waitForInstances.size}`;
  47. waitForInstances.set(instanceVar, importedModule.id);
  48. properties.push({
  49. module,
  50. name,
  51. value: `${instanceVar}[${JSON.stringify(usedName)}]`
  52. });
  53. } else {
  54. const params = description.signature.params.map(
  55. (param, k) => "p" + k + param.valtype
  56. );
  57. const mod = `installedModules[${JSON.stringify(importedModule.id)}]`;
  58. const func = `${mod}.exports[${JSON.stringify(usedName)}]`;
  59. properties.push({
  60. module,
  61. name,
  62. value: Template.asString([
  63. (importedModule.type.startsWith("webassembly")
  64. ? `${mod} ? ${func} : `
  65. : "") + `function(${params}) {`,
  66. Template.indent([`return ${func}(${params});`]),
  67. "}"
  68. ])
  69. });
  70. }
  71. }
  72. let importObject;
  73. if (mangle) {
  74. importObject = [
  75. "return {",
  76. Template.indent([
  77. properties.map(p => `${JSON.stringify(p.name)}: ${p.value}`).join(",\n")
  78. ]),
  79. "};"
  80. ];
  81. } else {
  82. const propertiesByModule = new Map();
  83. for (const p of properties) {
  84. let list = propertiesByModule.get(p.module);
  85. if (list === undefined) {
  86. propertiesByModule.set(p.module, (list = []));
  87. }
  88. list.push(p);
  89. }
  90. importObject = [
  91. "return {",
  92. Template.indent([
  93. Array.from(propertiesByModule, ([module, list]) => {
  94. return Template.asString([
  95. `${JSON.stringify(module)}: {`,
  96. Template.indent([
  97. list.map(p => `${JSON.stringify(p.name)}: ${p.value}`).join(",\n")
  98. ]),
  99. "}"
  100. ]);
  101. }).join(",\n")
  102. ]),
  103. "};"
  104. ];
  105. }
  106. if (waitForInstances.size === 1) {
  107. const moduleId = Array.from(waitForInstances.values())[0];
  108. const promise = `installedWasmModules[${JSON.stringify(moduleId)}]`;
  109. const variable = Array.from(waitForInstances.keys())[0];
  110. return Template.asString([
  111. `${JSON.stringify(module.id)}: function() {`,
  112. Template.indent([
  113. `return promiseResolve().then(function() { return ${promise}; }).then(function(${variable}) {`,
  114. Template.indent(importObject),
  115. "});"
  116. ]),
  117. "},"
  118. ]);
  119. } else if (waitForInstances.size > 0) {
  120. const promises = Array.from(
  121. waitForInstances.values(),
  122. id => `installedWasmModules[${JSON.stringify(id)}]`
  123. ).join(", ");
  124. const variables = Array.from(
  125. waitForInstances.keys(),
  126. (name, i) => `${name} = array[${i}]`
  127. ).join(", ");
  128. return Template.asString([
  129. `${JSON.stringify(module.id)}: function() {`,
  130. Template.indent([
  131. `return promiseResolve().then(function() { return Promise.all([${promises}]); }).then(function(array) {`,
  132. Template.indent([`var ${variables};`, ...importObject]),
  133. "});"
  134. ]),
  135. "},"
  136. ]);
  137. } else {
  138. return Template.asString([
  139. `${JSON.stringify(module.id)}: function() {`,
  140. Template.indent(importObject),
  141. "},"
  142. ]);
  143. }
  144. };
  145. class WasmMainTemplatePlugin {
  146. constructor({ generateLoadBinaryCode, supportsStreaming, mangleImports }) {
  147. this.generateLoadBinaryCode = generateLoadBinaryCode;
  148. this.supportsStreaming = supportsStreaming;
  149. this.mangleImports = mangleImports;
  150. }
  151. /**
  152. * @param {MainTemplate} mainTemplate main template
  153. * @returns {void}
  154. */
  155. apply(mainTemplate) {
  156. mainTemplate.hooks.localVars.tap(
  157. "WasmMainTemplatePlugin",
  158. (source, chunk) => {
  159. const wasmModules = getAllWasmModules(chunk);
  160. if (wasmModules.length === 0) return source;
  161. const importObjects = wasmModules.map(module => {
  162. return generateImportObject(module, this.mangleImports);
  163. });
  164. return Template.asString([
  165. source,
  166. "",
  167. "// object to store loaded and loading wasm modules",
  168. "var installedWasmModules = {};",
  169. "",
  170. // This function is used to delay reading the installed wasm module promises
  171. // by a microtask. Sorting them doesn't help because there are egdecases where
  172. // sorting is not possible (modules splitted into different chunks).
  173. // So we not even trying and solve this by a microtask delay.
  174. "function promiseResolve() { return Promise.resolve(); }",
  175. "",
  176. "var wasmImportObjects = {",
  177. Template.indent(importObjects),
  178. "};"
  179. ]);
  180. }
  181. );
  182. mainTemplate.hooks.requireEnsure.tap(
  183. "WasmMainTemplatePlugin",
  184. (source, chunk, hash) => {
  185. const webassemblyModuleFilename =
  186. mainTemplate.outputOptions.webassemblyModuleFilename;
  187. const chunkModuleMaps = chunk.getChunkModuleMaps(m =>
  188. m.type.startsWith("webassembly")
  189. );
  190. if (Object.keys(chunkModuleMaps.id).length === 0) return source;
  191. const wasmModuleSrcPath = mainTemplate.getAssetPath(
  192. JSON.stringify(webassemblyModuleFilename),
  193. {
  194. hash: `" + ${mainTemplate.renderCurrentHashCode(hash)} + "`,
  195. hashWithLength: length =>
  196. `" + ${mainTemplate.renderCurrentHashCode(hash, length)} + "`,
  197. module: {
  198. id: '" + wasmModuleId + "',
  199. hash: `" + ${JSON.stringify(
  200. chunkModuleMaps.hash
  201. )}[wasmModuleId] + "`,
  202. hashWithLength(length) {
  203. const shortChunkHashMap = Object.create(null);
  204. for (const wasmModuleId of Object.keys(chunkModuleMaps.hash)) {
  205. if (typeof chunkModuleMaps.hash[wasmModuleId] === "string") {
  206. shortChunkHashMap[wasmModuleId] = chunkModuleMaps.hash[
  207. wasmModuleId
  208. ].substr(0, length);
  209. }
  210. }
  211. return `" + ${JSON.stringify(
  212. shortChunkHashMap
  213. )}[wasmModuleId] + "`;
  214. }
  215. }
  216. }
  217. );
  218. const createImportObject = content =>
  219. this.mangleImports
  220. ? `{ ${JSON.stringify(
  221. WebAssemblyUtils.MANGLED_MODULE
  222. )}: ${content} }`
  223. : content;
  224. return Template.asString([
  225. source,
  226. "",
  227. "// Fetch + compile chunk loading for webassembly",
  228. "",
  229. `var wasmModules = ${JSON.stringify(
  230. chunkModuleMaps.id
  231. )}[chunkId] || [];`,
  232. "",
  233. "wasmModules.forEach(function(wasmModuleId) {",
  234. Template.indent([
  235. "var installedWasmModuleData = installedWasmModules[wasmModuleId];",
  236. "",
  237. '// a Promise means "currently loading" or "already loaded".',
  238. "if(installedWasmModuleData)",
  239. Template.indent(["promises.push(installedWasmModuleData);"]),
  240. "else {",
  241. Template.indent([
  242. `var importObject = wasmImportObjects[wasmModuleId]();`,
  243. `var req = ${this.generateLoadBinaryCode(wasmModuleSrcPath)};`,
  244. "var promise;",
  245. this.supportsStreaming
  246. ? Template.asString([
  247. "if(importObject instanceof Promise && typeof WebAssembly.compileStreaming === 'function') {",
  248. Template.indent([
  249. "promise = Promise.all([WebAssembly.compileStreaming(req), importObject]).then(function(items) {",
  250. Template.indent([
  251. `return WebAssembly.instantiate(items[0], ${createImportObject(
  252. "items[1]"
  253. )});`
  254. ]),
  255. "});"
  256. ]),
  257. "} else if(typeof WebAssembly.instantiateStreaming === 'function') {",
  258. Template.indent([
  259. `promise = WebAssembly.instantiateStreaming(req, ${createImportObject(
  260. "importObject"
  261. )});`
  262. ])
  263. ])
  264. : Template.asString([
  265. "if(importObject instanceof Promise) {",
  266. Template.indent([
  267. "var bytesPromise = req.then(function(x) { return x.arrayBuffer(); });",
  268. "promise = Promise.all([",
  269. Template.indent([
  270. "bytesPromise.then(function(bytes) { return WebAssembly.compile(bytes); }),",
  271. "importObject"
  272. ]),
  273. "]).then(function(items) {",
  274. Template.indent([
  275. `return WebAssembly.instantiate(items[0], ${createImportObject(
  276. "items[1]"
  277. )});`
  278. ]),
  279. "});"
  280. ])
  281. ]),
  282. "} else {",
  283. Template.indent([
  284. "var bytesPromise = req.then(function(x) { return x.arrayBuffer(); });",
  285. "promise = bytesPromise.then(function(bytes) {",
  286. Template.indent([
  287. `return WebAssembly.instantiate(bytes, ${createImportObject(
  288. "importObject"
  289. )});`
  290. ]),
  291. "});"
  292. ]),
  293. "}",
  294. "promises.push(installedWasmModules[wasmModuleId] = promise.then(function(res) {",
  295. Template.indent([
  296. `return ${mainTemplate.requireFn}.w[wasmModuleId] = (res.instance || res).exports;`
  297. ]),
  298. "}));"
  299. ]),
  300. "}"
  301. ]),
  302. "});"
  303. ]);
  304. }
  305. );
  306. mainTemplate.hooks.requireExtensions.tap(
  307. "WasmMainTemplatePlugin",
  308. (source, chunk) => {
  309. if (!chunk.hasModuleInGraph(m => m.type.startsWith("webassembly"))) {
  310. return source;
  311. }
  312. return Template.asString([
  313. source,
  314. "",
  315. "// object with all WebAssembly.instance exports",
  316. `${mainTemplate.requireFn}.w = {};`
  317. ]);
  318. }
  319. );
  320. mainTemplate.hooks.hash.tap("WasmMainTemplatePlugin", hash => {
  321. hash.update("WasmMainTemplatePlugin");
  322. hash.update("2");
  323. });
  324. }
  325. }
  326. module.exports = WasmMainTemplatePlugin;