Skip to content

Commit

Permalink
feat: support ?module suffix for compatibility (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Mar 14, 2024
1 parent e7d94ae commit 06da6be
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 12 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?module";
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.
Expand Down
41 changes: 30 additions & 11 deletions src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<UnwasmPluginOptions>((opts) => {
const assets: Record<string, WasmAsset> = Object.create(null);

Expand Down Expand Up @@ -63,7 +69,7 @@ const unplugin = createUnplugin<UnwasmPluginOptions>((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 {
Expand Down Expand Up @@ -91,23 +97,34 @@ const unplugin = createUnplugin<UnwasmPluginOptions>((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;
}

const buff = Buffer.from(code, "binary");
const name = `wasm/${basename(id, ".wasm")}-${sha1(buff)}.wasm`;
const parsed = parse(name, buff);

const isModule = id.endsWith("?module");

const name = `wasm/${basename(id.split("?")[0], ".wasm")}-${sha1(buff)}.wasm`;

const parsed = isModule
? { imports: [], exports: ["default"] }
: parse(name, buff);

const asset = (assets[name] = <WasmAsset>{
name,
Expand All @@ -118,7 +135,9 @@ const unplugin = createUnplugin<UnwasmPluginOptions>((opts) => {
});

return {
code: await getWasmBinding(asset, opts),
code: isModule
? await getWasmModuleBinding(asset, opts)
: await getWasmESMBinding(asset, opts),
map: { mappings: "" },
};
},
Expand All @@ -129,8 +148,8 @@ const unplugin = createUnplugin<UnwasmPluginOptions>((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;
Expand Down
25 changes: 24 additions & 1 deletion src/plugin/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
Expand Down Expand Up @@ -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 = ${opts.lazy === true ? "" : `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`
Expand Down
10 changes: 10 additions & 0 deletions test/fixture/module-import.mjs
Original file line number Diff line number Diff line change
@@ -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";
}
11 changes: 11 additions & 0 deletions test/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---
Expand Down

0 comments on commit 06da6be

Please sign in to comment.