Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support ?module suffix for compatibility #23

Merged
merged 6 commits into from Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
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
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
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
@@ -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
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