From 297a5e1d67fd0b34be3365b83a8196e7b302f041 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 14 Mar 2024 15:42:44 +0100 Subject: [PATCH 1/6] feat: support `?module` import for compatibility --- README.md | 12 ++++++++++++ src/plugin/index.ts | 31 ++++++++++++++++++++++--------- src/plugin/runtime.ts | 25 ++++++++++++++++++++++++- test/fixture/module-import.mjs | 10 ++++++++++ test/plugin.test.ts | 11 +++++++++++ 5 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 test/fixture/module-import.mjs diff --git a/README.md b/README.md index cf0be2b..6671824 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,18 @@ await initRand({ > [!NOTE] > When using **static import syntax**, and before initializing the module, the named exports will be wrapped into a function by proxy that waits for the module initialization and if called before init, will immediately try to call init without imports and return a Promise that calls a function after init. +### Module compatibility + +There are situations where libraries require a [`WebAssembly.Module`](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Module) instance to initialize [`WebAssembly.Instance`](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Instance/Instance) themselves. In order to maximize compatibility, unwasm allows a specific import suffix `?module` to import `.wasm` files as a Module directly. + +```js +import _sumMod from "unwasm/examples/sum.wasm"; +const { sum } = await WebAssembly.instantiate(_sumMod).then((i) => i.exports); +``` + +> [!NOTE] +> Open [an issue](https://github.com/unjs/unwasm/issues/new/choose) to us! We would love to help those libraries to migrate! + ## Integration Unwasm needs to transform the `.wasm` imports to the compatible bindings. Currently, the only method is using a rollup plugin. In the future, more usage methods will be introduced. diff --git a/src/plugin/index.ts b/src/plugin/index.ts index b4a24c6..3b0fae2 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -12,10 +12,16 @@ import { UnwasmPluginOptions, WasmAsset, } from "./shared"; -import { getPluginUtils, getWasmBinding } from "./runtime"; +import { + getPluginUtils, + getWasmESMBinding, + getWasmModuleBinding, +} from "./runtime"; export type { UnwasmPluginOptions } from "./shared"; +const WASM_ID_RE = /\.wasm\??.*$/i; + const unplugin = createUnplugin((opts) => { const assets: Record = Object.create(null); @@ -63,7 +69,7 @@ const unplugin = createUnplugin((opts) => { external: true, }; } - if (id.endsWith(".wasm")) { + if (WASM_ID_RE.test(id)) { const r = await this.resolve(id, importer, { skipSelf: true }); if (r?.id && r.id !== id) { return { @@ -91,17 +97,22 @@ const unplugin = createUnplugin((opts) => { return getPluginUtils(); } - if (!id.endsWith(".wasm") || !existsSync(id)) { + if (!WASM_ID_RE.test(id)) { + return; + } + + const idPath = id.split("?")[0]; + if (!existsSync(idPath)) { return; } - this.addWatchFile(id); + this.addWatchFile(idPath); - const buff = await fs.readFile(id); + const buff = await fs.readFile(idPath); return buff.toString("binary"); }, async transform(code, id) { - if (!id.endsWith(".wasm")) { + if (!WASM_ID_RE.test(id)) { return; } @@ -118,7 +129,9 @@ const unplugin = createUnplugin((opts) => { }); return { - code: await getWasmBinding(asset, opts), + code: id.endsWith("?module") + ? await getWasmModuleBinding(asset, opts) + : await getWasmESMBinding(asset, opts), map: { mappings: "" }, }; }, @@ -129,8 +142,8 @@ const unplugin = createUnplugin((opts) => { if ( !( - chunk.moduleIds.some((id) => id.endsWith(".wasm")) || - chunk.imports.some((id) => id.endsWith(".wasm")) + chunk.moduleIds.some((id) => WASM_ID_RE.test(id)) || + chunk.imports.some((id) => WASM_ID_RE.test(id)) ) ) { return; diff --git a/src/plugin/runtime.ts b/src/plugin/runtime.ts index 867be01..70d6f55 100644 --- a/src/plugin/runtime.ts +++ b/src/plugin/runtime.ts @@ -9,7 +9,10 @@ import { // https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html const js = String.raw; -export async function getWasmBinding( +/** + * Returns ESM compatible exports binding + */ +export async function getWasmESMBinding( asset: WasmAsset, opts: UnwasmPluginOptions, ) { @@ -73,6 +76,26 @@ export default _mod; } } +/** + * Returns WebAssembly.Module binding for compatibility + */ +export function getWasmModuleBinding( + asset: WasmAsset, + opts: UnwasmPluginOptions, +) { + return opts.esmImport + ? js` +const _mod = await import("${UNWASM_EXTERNAL_PREFIX}${asset.name}").then(r => r.default || r); +export default _mod; + ` + : js` +import { base64ToUint8Array } from "${UMWASM_HELPERS_ID}"; +const _data = base64ToUint8Array("${asset.source.toString("base64")}"); +const _mod = new WebAssembly.Module(_data); +export default _mod; + `; +} + export function getPluginUtils() { // --- Shared utils for the generated code --- return js` diff --git a/test/fixture/module-import.mjs b/test/fixture/module-import.mjs new file mode 100644 index 0000000..6e11cd4 --- /dev/null +++ b/test/fixture/module-import.mjs @@ -0,0 +1,10 @@ +import _sumMod from "@fixture/wasm/sum.wasm?module"; + +const { sum } = await WebAssembly.instantiate(_sumMod).then(i => i.exports); + +export function test() { + if (sum(1, 2) !== 3) { + return "FALED: sum"; + } + return "OK"; +} diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 1704e30..8f5bb31 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -37,6 +37,17 @@ describe("plugin:rollup", () => { const resText = await _evalCloudflare(name).then((r) => r.text()); expect(resText).toBe("OK"); }); + + it("module", async () => { + const { output } = await _rollupBuild( + "fixture/module-import.mjs", + "rollup-module", + {}, + ); + const code = output[0].code; + const mod = await evalModule(code, { url: r("fixture/rollup-module.mjs") }); + expect(mod.test()).toBe("OK"); + }); }); // --- Utils --- From 642f3aca363245bc5968c8e8312a18c226474730 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 14 Mar 2024 15:42:49 +0100 Subject: [PATCH 2/6] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6671824..239d700 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ await initRand({ There are situations where libraries require a [`WebAssembly.Module`](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Module) instance to initialize [`WebAssembly.Instance`](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Instance/Instance) themselves. In order to maximize compatibility, unwasm allows a specific import suffix `?module` to import `.wasm` files as a Module directly. ```js -import _sumMod from "unwasm/examples/sum.wasm"; +import _sumMod from "unwasm/examples/sum.wasm?module"; const { sum } = await WebAssembly.instantiate(_sumMod).then((i) => i.exports); ``` From 189a64a4beb09bd5149c4a650d23d5829e94e5b0 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 14 Mar 2024 15:43:21 +0100 Subject: [PATCH 3/6] lint --- test/fixture/module-import.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fixture/module-import.mjs b/test/fixture/module-import.mjs index 6e11cd4..401eca4 100644 --- a/test/fixture/module-import.mjs +++ b/test/fixture/module-import.mjs @@ -1,6 +1,6 @@ import _sumMod from "@fixture/wasm/sum.wasm?module"; -const { sum } = await WebAssembly.instantiate(_sumMod).then(i => i.exports); +const { sum } = await WebAssembly.instantiate(_sumMod).then((i) => i.exports); export function test() { if (sum(1, 2) !== 3) { From 0dd7722830fc5a1d118faf8e16a663d7188ce1cc Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 14 Mar 2024 15:54:28 +0100 Subject: [PATCH 4/6] respect `lazy` option for top level await --- src/plugin/runtime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugin/runtime.ts b/src/plugin/runtime.ts index 70d6f55..5cad162 100644 --- a/src/plugin/runtime.ts +++ b/src/plugin/runtime.ts @@ -85,7 +85,7 @@ export function getWasmModuleBinding( ) { return opts.esmImport ? js` -const _mod = await import("${UNWASM_EXTERNAL_PREFIX}${asset.name}").then(r => r.default || r); +const _mod = ${opts.lazy === true ? "" : `await`} import("${UNWASM_EXTERNAL_PREFIX}${asset.name}").then(r => r.default || r); export default _mod; ` : js` From 64613b35c066d01c3117f61dfccd7a49db618df5 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 14 Mar 2024 16:07:41 +0100 Subject: [PATCH 5/6] update transform --- src/plugin/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/plugin/index.ts b/src/plugin/index.ts index 3b0fae2..c251054 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -116,9 +116,13 @@ const unplugin = createUnplugin((opts) => { return; } + const isModule = id.endsWith("?module"); + const buff = Buffer.from(code, "binary"); - const name = `wasm/${basename(id, ".wasm")}-${sha1(buff)}.wasm`; - const parsed = parse(name, buff); + const name = `wasm/${basename(id.split("?")[0], ".wasm")}-${sha1(buff)}.wasm`; + const parsed = isModule + ? { imports: [], exports: ["default"] } + : parse(name, buff); const asset = (assets[name] = { name, @@ -129,7 +133,7 @@ const unplugin = createUnplugin((opts) => { }); return { - code: id.endsWith("?module") + code: isModule ? await getWasmModuleBinding(asset, opts) : await getWasmESMBinding(asset, opts), map: { mappings: "" }, From 512f686002aef028003271ea86c9cf67dbc4be3f Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 14 Mar 2024 16:08:12 +0100 Subject: [PATCH 6/6] fmt --- src/plugin/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugin/index.ts b/src/plugin/index.ts index c251054..0543258 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -116,10 +116,12 @@ const unplugin = createUnplugin((opts) => { return; } + const buff = Buffer.from(code, "binary"); + const isModule = id.endsWith("?module"); - const buff = Buffer.from(code, "binary"); const name = `wasm/${basename(id.split("?")[0], ".wasm")}-${sha1(buff)}.wasm`; + const parsed = isModule ? { imports: [], exports: ["default"] } : parse(name, buff);