Skip to content

Commit 6861425

Browse files
committed
feat!: support custom init
1 parent d03a1cb commit 6861425

15 files changed

+276
-116
lines changed

README.md

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22
[![npm downloads][npm-downloads-src]][npm-downloads-href]
33
[![Codecov][codecov-src]][codecov-href]
44

5-
# 🇼 unwasm
5+
# unwasm
66

77
Universal [WebAssembly](https://webassembly.org/) tools for JavaScript.
88

99
## Goal
1010

11-
This project aims to make a common and future-proof solution for WebAssembly modules support suitable for various JavaScript runtimes, frameworks, and build Tools following [WebAssembly/ES Module Integration](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration) proposal from WebAssembly Community Group as much as possible.
11+
This project aims to make a common and future-proof solution for WebAssembly modules support suitable for various JavaScript runtimes, frameworks, and build Tools following [WebAssembly/ES Module Integration](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration) proposal from WebAssembly Community Group as much as possible while also trying to keep compatibility with current ecosystem libraries.
1212

1313
## Roadmap
1414

1515
The development will be split into multiple stages.
1616

1717
> [!IMPORTANT]
18-
> This Project is under development! Join the linked discussions to be involved!
18+
> This Project is under development! See the linked discussions to be involved!
1919
2020
- [ ] Universal builder plugins built with [unjs/unplugin](https://github.com/unjs/unplugin) ([unjs/unwasm#2](https://github.com/unjs/unwasm/issues/2))
2121
- [x] Rollup
@@ -25,29 +25,73 @@ The development will be split into multiple stages.
2525
- [ ] Integration with [Wasmer](https://github.com/wasmerio) ([unjs/unwasm#6](https://github.com/unjs/unwasm/issues/6))
2626
- [ ] Convention and tools for library authors exporting wasm modules ([unjs/unwasm#7](https://github.com/unjs/unwasm/issues/7))
2727

28-
## Install
28+
## Bindings API
2929

30-
Install package from [npm](https://www.npmjs.com/package/unwasm):
30+
When importing a `.wasm` module using unwasm, it will take steps to transform the binary and finally resolve to an ESM module that allows you to interact with the WASM module. The returned result is a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) object. This proxy allows to use an elegant API while also having both backward and forward compatibility with WASM modules as ecosystem evolves.
31+
32+
WebAssembly modules that don't require any imports, can be imported simply like you import any other esm module:
33+
34+
**Using static import:**
35+
36+
```js
37+
import { func } from "lib/module.wasm";
38+
```
39+
40+
**Using dynamic import:**
41+
42+
```js
43+
const { func } = await import("lib/module.wasm").then((mod) => mod.default);
44+
```
45+
46+
In case of your WebAssembly module requires imports object (which is likely!), the usage syntax would be slightly different as we need to initial module with imports object first:
47+
48+
**Using static import with imports object:**
49+
50+
```js
51+
import { func, $init } from "lib/module.wasm";
52+
53+
await $init({ env: {} });
54+
```
55+
56+
**Using dynamic import with imports object:**
57+
58+
```js
59+
const { func } = await import("lib/module.wasm").then((mod) => mod.$init(env));
60+
```
61+
62+
> [!NOTE] > **When using static import syntax**, and before calling `$init`, the named exports will be wrapped into a function by proxy that waits for the module initialization and before that if called, will immediately try to call `$init()` and return a Promise that calls function after init.
63+
64+
> [!NOTE]
65+
> Named exports with `$` prefix are reserved for unwasm. In case your module uses them, you can access from `$exports` property.
66+
67+
## Usage
68+
69+
Unwasm needs to transform the `.wasm` imports to the compatible bindings. Currently only method is using a rollup plugin. In the future more usage methods will be introduced.
70+
71+
### Install
72+
73+
First, install the [`unwasm` npm package](https://www.npmjs.com/package/unwasm):
3174

3275
```sh
3376
# npm
34-
npm install unwasm
77+
npm install --dev unwasm
3578

3679
# yarn
37-
yarn add unwasm
80+
yarn add -D unwasm
3881

3982
# pnpm
40-
pnpm install unwasm
83+
pnpm i -D unwasm
4184

4285
# bun
43-
bun install unwasm
86+
bun i -D unwasm
4487
```
4588

46-
## Using build plugin
89+
### Builder Plugins
4790

4891
###### Rollup
4992

5093
```js
94+
// rollup.config.js
5195
import unwasmPlugin from "unwasm/plugin";
5296

5397
export default {
@@ -59,7 +103,7 @@ export default {
59103
};
60104
```
61105

62-
### Options
106+
### Plugin Options
63107

64108
- `esmImport`: Direct import the wasm file instead of bundling, required in Cloudflare Workers (default is `false`)
65109
- `lazy`: Import `.wasm` files using a lazily evaluated promise for compatibility with runtimes without top-level await support (default is `false`)
@@ -71,6 +115,7 @@ export default {
71115
- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
72116
- Install dependencies using `pnpm install`
73117
- Run interactive tests using `pnpm dev`
118+
- Optionally install [es6-string-html](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) extension to make it easier working with string templates.
74119

75120
## License
76121

build.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ import { defineBuildConfig } from "unbuild";
22

33
export default defineBuildConfig({
44
declaration: true,
5-
entries: ["src/plugin"],
5+
entries: ["src/plugin/index"],
66
externals: ["unwasm", "rollup"],
77
});

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
"type": "module",
99
"exports": {
1010
"./plugin": {
11-
"types": "./dist/plugin.d.mts",
12-
"import": "./dist/plugin.mjs"
11+
"types": "./dist/plugin/index.d.mts",
12+
"import": "./dist/plugin/index.mjs"
1313
}
1414
},
1515
"files": [
16-
"dist"
16+
"dist",
17+
"*.d.ts"
1718
],
1819
"scripts": {
1920
"build": "unbuild",
@@ -48,4 +49,4 @@
4849
"vitest": "^1.1.0"
4950
},
5051
"packageManager": "pnpm@8.12.1"
51-
}
52+
}

plugin.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./dist/plugin";

src/_utils.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* eslint-disable unicorn/no-empty-file */ // <-- WTF!
2+
3+
// No runtime utils yet!
4+
// https://github.com/unjs/unwasm/issues/4
5+
6+
// See plugin/ for real stuff!

src/plugin.ts renamed to src/plugin/index.ts

Lines changed: 11 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,17 @@ import { basename } from "pathe";
33
import MagicString from "magic-string";
44
import type { RenderedChunk, Plugin as RollupPlugin } from "rollup";
55
import { createUnplugin } from "unplugin";
6-
import { sha1 } from "./_utils";
7-
8-
const UNWASM_EXTERNAL_PREFIX = "\0unwasm:external:";
9-
const UMWASM_HELPERS_ID = "\0unwasm:helpers";
10-
11-
export interface UnwasmPluginOptions {
12-
/**
13-
* Direct import the wasm file instead of bundling, required in Cloudflare Workers
14-
*
15-
* @default false
16-
*/
17-
esmImport?: boolean;
18-
19-
/**
20-
* Import `.wasm` files using a lazily evaluated promise for compatibility with runtimes without top-level await support
21-
*
22-
* @default false
23-
*/
24-
lazy?: boolean;
25-
}
6+
import {
7+
sha1,
8+
UMWASM_HELPERS_ID,
9+
UNWASM_EXTERNAL_PREFIX,
10+
UnwasmPluginOptions,
11+
WasmAsset,
12+
} from "./shared";
13+
import { getPluginUtils, getWasmBinding } from "./runtime";
2614

2715
const unplugin = createUnplugin<UnwasmPluginOptions>((opts) => {
28-
type WasmAsset = {
29-
name: string;
30-
source: Buffer;
31-
};
32-
3316
const assets: Record<string, WasmAsset> = Object.create(null);
34-
3517
return {
3618
name: "unwasm",
3719
rollup: {
@@ -78,9 +60,9 @@ const unplugin = createUnplugin<UnwasmPluginOptions>((opts) => {
7860
}
7961
const source = await fs.readFile(id);
8062
const name = `wasm/${basename(id, ".wasm")}-${sha1(source)}.wasm`;
81-
assets[id] = <WasmAsset>{ name, source };
63+
assets[id] = <WasmAsset>{ name, id, source };
8264
// TODO: Can we parse wasm to extract exports and avoid syntheticNamedExports?
83-
return `export default "WASM";`; // dummy
65+
return `export default "UNWASM DUMMY EXPORT";`;
8466
},
8567
transform(_code, id) {
8668
if (!id.endsWith(".wasm")) {
@@ -91,46 +73,8 @@ const unplugin = createUnplugin<UnwasmPluginOptions>((opts) => {
9173
return;
9274
}
9375

94-
const envCode: string = opts.esmImport
95-
? `
96-
async function _instantiate(imports) {
97-
const _mod = await import("${UNWASM_EXTERNAL_PREFIX}${id}").then(r => r.default || r);
98-
return WebAssembly.instantiate(_mod, imports)
99-
}
100-
`
101-
: `
102-
import { base64ToUint8Array } from "${UMWASM_HELPERS_ID}";
103-
104-
function _instantiate(imports) {
105-
const _mod = base64ToUint8Array("${asset.source.toString("base64")}")
106-
return WebAssembly.instantiate(_mod, imports)
107-
}
108-
`;
109-
110-
const code = `${envCode}
111-
const _defaultImports = Object.create(null);
112-
113-
// TODO: For testing only
114-
Object.assign(_defaultImports, { env: { "seed": () => () => Date.now() * Math.random() } })
115-
116-
const instancePromises = new WeakMap();
117-
function instantiate(imports = _defaultImports) {
118-
let p = instancePromises.get(imports);
119-
if (!p) {
120-
p = _instantiate(imports);
121-
instancePromises.set(imports, p);
122-
}
123-
return p;
124-
}
125-
126-
const _instance = instantiate();
127-
const _exports = _instance.then(r => r?.instance?.exports || r?.exports || r);
128-
129-
export default ${opts.lazy ? "" : "await "} _exports;
130-
`;
131-
13276
return {
133-
code,
77+
code: getWasmBinding(asset, opts),
13478
map: { mappings: "" },
13579
syntheticNamedExports: true,
13680
};
@@ -147,7 +91,6 @@ export default ${opts.lazy ? "" : "await "} _exports;
14791
) ||
14892
!code.includes(UNWASM_EXTERNAL_PREFIX)
14993
) {
150-
console.log(chunk);
15194
return;
15295
}
15396
const s = new MagicString(code);
@@ -190,20 +133,6 @@ export default ${opts.lazy ? "" : "await "} _exports;
190133
};
191134
});
192135

193-
export function getPluginUtils() {
194-
return `
195-
export function base64ToUint8Array(str) {
196-
const data = atob(str);
197-
const size = data.length;
198-
const bytes = new Uint8Array(size);
199-
for (let i = 0; i < size; i++) {
200-
bytes[i] = data.charCodeAt(i);
201-
}
202-
return bytes;
203-
}
204-
`;
205-
}
206-
207136
const rollup = unplugin.rollup as (opts: UnwasmPluginOptions) => RollupPlugin;
208137

209138
export default {

0 commit comments

Comments
 (0)