unwasm 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,53 +17,42 @@ The development will be split into multiple stages.
17
17
  > [!IMPORTANT]
18
18
  > This Project is under development! See the linked discussions to be involved!
19
19
 
20
- - [ ] Universal builder plugins built with [unjs/unplugin](https://github.com/unjs/unplugin) ([unjs/unwasm#2](https://github.com/unjs/unwasm/issues/2))
20
+ - [ ] Builder plugin powered by [unjs/unplugin](https://github.com/unjs/unplugin) ([unjs/unwasm#2](https://github.com/unjs/unwasm/issues/2))
21
21
  - [x] Rollup
22
- - [ ] Tools to operate and inspect `.wasm` files ([unjs/unwasm#3](https://github.com/unjs/unwasm/issues/3))
23
- - [ ] Runtime utils ([unjs/unwasm#4](https://github.com/unjs/unwasm/issues/4))
24
- - [ ] ESM loader for Node.js and other JavaScript runtimes ([unjs/unwasm#5](https://github.com/unjs/unwasm/issues/5))
22
+ - [ ] Build Tools ([unjs/unwasm#3](https://github.com/unjs/unwasm/issues/3))
23
+ - [x] `parseWasm`
24
+ - [ ] Runtime Utils ([unjs/unwasm#4](https://github.com/unjs/unwasm/issues/4))
25
+ - [ ] ESM Loader ([unjs/unwasm#5](https://github.com/unjs/unwasm/issues/5))
25
26
  - [ ] Integration with [Wasmer](https://github.com/wasmerio) ([unjs/unwasm#6](https://github.com/unjs/unwasm/issues/6))
26
- - [ ] Convention and tools for library authors exporting wasm modules ([unjs/unwasm#7](https://github.com/unjs/unwasm/issues/7))
27
+ - [ ] Convention for library authors exporting wasm modules ([unjs/unwasm#7](https://github.com/unjs/unwasm/issues/7))
27
28
 
28
29
  ## Bindings API
29
30
 
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 of an elegant API while also having both backward and forward compatibility with WASM modules as the ecosystem evolves.
31
+ When importing a `.wasm` module, unwasm resolves, reads, and then parses the module during build process to get the information about imports and exports and generate appropriate code bindings.
31
32
 
32
- WebAssembly modules that don't require any imports, can be imported simply like you import any other ESM module.
33
+ If the target environment supports [top level `await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#top_level_await) and also the wasm module requires no imports object (auto-detected after parsing), unwasm generates bindings to allow importing wasm module like any other ESM import.
33
34
 
34
- **Using static import:**
35
+ If the target environment lacks support for top level `await` or the wasm module requires imports object or `lazy` plugin option is set to `true`, unwasm will export a wrapped [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) object which can be called as a function to lazily evaluate the module with custom imports object. This way we still have a simple syntax as close as possible to ESM modules and also we can lazily initialize modules.
36
+
37
+ **Example:** Using static import
35
38
 
36
39
  ```js
37
40
  import { sum } from "unwasm/examples/sum.wasm";
38
41
  ```
39
42
 
40
- **Using dynamic import:**
43
+ **Example:** Using dynamic import
41
44
 
42
45
  ```js
43
- const { sum } = await import("unwasm/examples/sum.wasm").then(
44
- (mod) => mod.default,
45
- );
46
+ const { sum } = await import("unwasm/examples/sum.wasm");
46
47
  ```
47
48
 
48
- In case your WebAssembly module requires an import object (which is likely!), the usage syntax would be slightly different as we need to initate the module with an import object first.
49
-
50
- **Using static import with imports object:**
51
-
52
- ```js
53
- import { rand, $init } from "unwasm/examples/rand.wasm";
54
-
55
- await $init({
56
- env: {
57
- seed: () => () => Math.random() * Date.now(),
58
- },
59
- });
60
- ```
49
+ If your WebAssembly module requires an import object (which is likely!), the usage syntax would be slightly different as we need to initiate the module with an import object first.
61
50
 
62
- **Using dynamic import with imports object:**
51
+ **Example:** Using dynamic import with imports object
63
52
 
64
53
  ```js
65
- const { rand } = await import("unwasm/examples/rand.wasm").then((mod) =>
66
- mod.$init({
54
+ const { rand } = await import("unwasm/examples/rand.wasm").then((r) =>
55
+ r.default({
67
56
  env: {
68
57
  seed: () => () => Math.random() * Date.now(),
69
58
  },
@@ -71,19 +60,28 @@ const { rand } = await import("unwasm/examples/rand.wasm").then((mod) =>
71
60
  );
72
61
  ```
73
62
 
74
- > [!NOTE]
75
- > 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 a function after init.
63
+ **Example:** Using static import with imports object
64
+
65
+ ```js
66
+ import initRand, { rand } from "unwasm/examples/rand.wasm";
67
+
68
+ await initRand({
69
+ env: {
70
+ seed: () => () => Math.random() * Date.now(),
71
+ },
72
+ });
73
+ ```
76
74
 
77
75
  > [!NOTE]
78
- > Named exports with the `$` prefix are reserved for unwasm. In case your module uses them, you can access them from the `$exports` property.
76
+ > 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.
79
77
 
80
- ## Usage
78
+ ## Integration
81
79
 
82
- 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.
80
+ 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.
83
81
 
84
82
  ### Install
85
83
 
86
- First, install the [`unwasm` npm package](https://www.npmjs.com/package/unwasm):
84
+ First, install the [`unwasm`](https://www.npmjs.com/package/unwasm) npm package.
87
85
 
88
86
  ```sh
89
87
  # npm
@@ -105,11 +103,11 @@ bun i -D unwasm
105
103
 
106
104
  ```js
107
105
  // rollup.config.js
108
- import unwasmPlugin from "unwasm/plugin";
106
+ import { rollup as unwasm } from "unwasm/plugin";
109
107
 
110
108
  export default {
111
109
  plugins: [
112
- unwasmPlugin.rollup({
110
+ unwasm({
113
111
  /* options */
114
112
  }),
115
113
  ],
@@ -118,8 +116,58 @@ export default {
118
116
 
119
117
  ### Plugin Options
120
118
 
121
- - `esmImport`: Direct import the wasm file instead of bundling, required in Cloudflare Workers (default is `false`)
122
- - `lazy`: Import `.wasm` files using a lazily evaluated promise for compatibility with runtimes without top-level await support (default is `false`)
119
+ - `esmImport`: Direct import the wasm file instead of bundling, required in Cloudflare Workers and works with environments that allow natively importing a `.wasm` module (default is `false`)
120
+ - `lazy`: Import `.wasm` files using a lazily evaluated proxy for compatibility with runtimes without top-level await support (default is `false`)
121
+
122
+ ## Tools
123
+
124
+ unwasm provides useful build tools to operate on `.wasm` modules directly.
125
+
126
+ **Note:** `unwasm/tools` subpath export is **not** meant or optimized for production runtime. Only rely on it for development and build time.
127
+
128
+ ### `parseWasm`
129
+
130
+ Parses `wasm` binary format with useful information using [webassemblyjs/wasm-parser](https://github.com/xtuc/webassemblyjs/tree/master/packages/wasm-parser).
131
+
132
+ ```js
133
+ import { readFile } from "node:fs/promises";
134
+ import { parseWasm } from "unwasm/tools";
135
+
136
+ const source = await readFile(new URL("./examples/sum.wasm", import.meta.url));
137
+ const parsed = parseWasm(source);
138
+ console.log(JSON.stringify(parsed, undefined, 2));
139
+ ```
140
+
141
+ Example parsed result:
142
+
143
+ ```json
144
+ {
145
+ "modules": [
146
+ {
147
+ "exports": [
148
+ {
149
+ "id": 5,
150
+ "name": "rand",
151
+ "type": "Func"
152
+ },
153
+ {
154
+ "id": 0,
155
+ "name": "memory",
156
+ "type": "Memory"
157
+ }
158
+ ],
159
+ "imports": [
160
+ {
161
+ "module": "env",
162
+ "name": "seed",
163
+ "params": [],
164
+ "returnType": "f64"
165
+ }
166
+ ]
167
+ }
168
+ ]
169
+ }
170
+ ```
123
171
 
124
172
  ## Development
125
173
 
@@ -0,0 +1,25 @@
1
+ import { Plugin } from 'rollup';
2
+
3
+ interface UnwasmPluginOptions {
4
+ /**
5
+ * Directly import the `.wasm` files instead of bundling as base64 string.
6
+ *
7
+ * @default false
8
+ */
9
+ esmImport?: boolean;
10
+ /**
11
+ * Avoid using top level await and always use a proxy.
12
+ *
13
+ * Useful for compatibility with environments that don't support top level await.
14
+ *
15
+ * @default false
16
+ */
17
+ lazy?: boolean;
18
+ }
19
+
20
+ declare const rollup: (opts: UnwasmPluginOptions) => Plugin;
21
+ declare const _default: {
22
+ rollup: (opts: UnwasmPluginOptions) => Plugin<any>;
23
+ };
24
+
25
+ export { _default as default, rollup };
@@ -0,0 +1,25 @@
1
+ import { Plugin } from 'rollup';
2
+
3
+ interface UnwasmPluginOptions {
4
+ /**
5
+ * Directly import the `.wasm` files instead of bundling as base64 string.
6
+ *
7
+ * @default false
8
+ */
9
+ esmImport?: boolean;
10
+ /**
11
+ * Avoid using top level await and always use a proxy.
12
+ *
13
+ * Useful for compatibility with environments that don't support top level await.
14
+ *
15
+ * @default false
16
+ */
17
+ lazy?: boolean;
18
+ }
19
+
20
+ declare const rollup: (opts: UnwasmPluginOptions) => Plugin;
21
+ declare const _default: {
22
+ rollup: (opts: UnwasmPluginOptions) => Plugin<any>;
23
+ };
24
+
25
+ export { _default as default, rollup };
@@ -3,6 +3,8 @@ import { basename } from 'pathe';
3
3
  import MagicString from 'magic-string';
4
4
  import { createUnplugin } from 'unplugin';
5
5
  import { createHash } from 'node:crypto';
6
+ import { parseWasm } from './tools.mjs';
7
+ import '@webassemblyjs/wasm-parser';
6
8
 
7
9
  const UNWASM_EXTERNAL_PREFIX = "\0unwasm:external:";
8
10
  const UMWASM_HELPERS_ID = "\0unwasm:helpers";
@@ -25,24 +27,42 @@ function _instantiate(imports) {
25
27
  return WebAssembly.instantiate(_data, imports)
26
28
  }
27
29
  `;
28
- return js`
29
- import { createUnwasmModule } from "${UMWASM_HELPERS_ID}";
30
+ const canTopAwait = opts.lazy !== true && Object.keys(asset.imports).length === 0;
31
+ if (canTopAwait) {
32
+ return js`
33
+ import { getExports } from "${UMWASM_HELPERS_ID}";
34
+ ${envCode}
35
+
36
+ const $exports = getExports(await _instantiate());
37
+
38
+ ${asset.exports.map((name) => `export const ${name} = $exports.${name};`).join("\n")}
39
+
40
+ export const $init = () => $exports;
41
+
42
+ export default $exports;
43
+ `;
44
+ } else {
45
+ return js`
46
+ import { createLazyWasmModule } from "${UMWASM_HELPERS_ID}";
30
47
  ${envCode}
31
- const _mod = createUnwasmModule(_instantiate);
48
+
49
+ const _mod = createLazyWasmModule(_instantiate);
50
+
51
+ ${asset.exports.map((name) => `export const ${name} = _mod.${name};`).join("\n")}
32
52
 
33
53
  export const $init = _mod.$init.bind(_mod);
34
- export const exports = _mod;
35
54
 
36
55
  export default _mod;
37
- `;
56
+ `;
57
+ }
38
58
  }
39
59
  function getPluginUtils() {
40
60
  return js`
41
61
  export function debug(...args) {
42
- console.log('[wasm]', ...args);
62
+ console.log('[wasm] [debug]', ...args);
43
63
  }
44
64
 
45
- function getExports(input) {
65
+ export function getExports(input) {
46
66
  return input?.instance?.exports || input?.exports || input;
47
67
  }
48
68
 
@@ -56,14 +76,14 @@ export function base64ToUint8Array(str) {
56
76
  return bytes;
57
77
  }
58
78
 
59
- export function createUnwasmModule(_instantiator) {
79
+ export function createLazyWasmModule(_instantiator) {
60
80
  const _exports = Object.create(null);
61
81
  let _loaded;
62
82
  let _promise;
63
83
 
64
- const $init = (imports) => {
84
+ const init = (imports) => {
65
85
  if (_loaded) {
66
- return Promise.resolve(_proxy);
86
+ return Promise.resolve(exportsProxy);
67
87
  }
68
88
  if (_promise) {
69
89
  return _promise;
@@ -73,16 +93,16 @@ export function createUnwasmModule(_instantiator) {
73
93
  Object.assign(_exports, getExports(r));
74
94
  _loaded = true;
75
95
  _promise = undefined;
76
- return _proxy;
96
+ return exportsProxy;
77
97
  })
78
98
  .catch(error => {
79
99
  _promise = undefined;
80
- console.error('[wasm]', error);
100
+ console.error('[wasm] [error]', error);
81
101
  throw error;
82
102
  });
83
103
  }
84
104
 
85
- const $exports = new Proxy(_exports, {
105
+ const exportsProxy = new Proxy(_exports, {
86
106
  get(_, prop) {
87
107
  if (_loaded) {
88
108
  return _exports[prop];
@@ -90,33 +110,51 @@ export function createUnwasmModule(_instantiator) {
90
110
  return (...args) => {
91
111
  return _loaded
92
112
  ? _exports[prop]?.(...args)
93
- : $init().then(() => $exports[prop]?.(...args));
113
+ : init().then(() => _exports[prop]?.(...args));
94
114
  };
95
115
  },
96
116
  });
97
117
 
98
- const _instance = {
99
- $init,
100
- $exports,
101
- };
102
118
 
103
- const _proxy = new Proxy(_instance, {
119
+ const lazyProxy = new Proxy(() => {}, {
104
120
  get(_, prop) {
105
- // Reserve all to avoid future breaking changes
106
- if (prop.startsWith('$')) {
107
- return _instance[prop];
108
- }
109
- return $exports[prop];
110
- }
121
+ return exportsProxy[prop];
122
+ },
123
+ apply(_, __, args) {
124
+ return init(args[0])
125
+ },
111
126
  });
112
127
 
113
- return _proxy;
128
+ return lazyProxy;
114
129
  }
115
130
  `;
116
131
  }
117
132
 
118
133
  const unplugin = createUnplugin((opts) => {
119
134
  const assets = /* @__PURE__ */ Object.create(null);
135
+ const _parseCache = /* @__PURE__ */ Object.create(null);
136
+ function parse(name, source) {
137
+ if (_parseCache[name]) {
138
+ return _parseCache[name];
139
+ }
140
+ const parsed = parseWasm(source);
141
+ const imports = /* @__PURE__ */ Object.create(null);
142
+ const exports = [];
143
+ for (const mod of parsed.modules) {
144
+ exports.push(...mod.exports.map((e) => e.name));
145
+ for (const imp of mod.imports) {
146
+ if (!imports[imp.module]) {
147
+ imports[imp.module] = [];
148
+ }
149
+ imports[imp.module].push(imp.name);
150
+ }
151
+ }
152
+ _parseCache[name] = {
153
+ imports,
154
+ exports
155
+ };
156
+ return _parseCache[name];
157
+ }
120
158
  return {
121
159
  name: "unwasm",
122
160
  rollup: {
@@ -136,8 +174,7 @@ const unplugin = createUnplugin((opts) => {
136
174
  return {
137
175
  id: r.id.startsWith("file://") ? r.id.slice(7) : r.id,
138
176
  external: false,
139
- moduleSideEffects: false,
140
- syntheticNamedExports: true
177
+ moduleSideEffects: false
141
178
  };
142
179
  }
143
180
  }
@@ -163,7 +200,14 @@ const unplugin = createUnplugin((opts) => {
163
200
  }
164
201
  const source = await promises.readFile(id);
165
202
  const name = `wasm/${basename(id, ".wasm")}-${sha1(source)}.wasm`;
166
- assets[id] = { name, id, source };
203
+ const parsed = parse(name, source);
204
+ assets[id] = {
205
+ name,
206
+ id,
207
+ source,
208
+ imports: parsed.imports,
209
+ exports: parsed.exports
210
+ };
167
211
  return `export default "UNWASM DUMMY EXPORT";`;
168
212
  },
169
213
  transform(_code, id) {
@@ -176,8 +220,7 @@ const unplugin = createUnplugin((opts) => {
176
220
  }
177
221
  return {
178
222
  code: getWasmBinding(asset, opts),
179
- map: { mappings: "" },
180
- syntheticNamedExports: true
223
+ map: { mappings: "" }
181
224
  };
182
225
  },
183
226
  renderChunk(code, chunk) {
@@ -230,4 +273,4 @@ const index = {
230
273
  rollup
231
274
  };
232
275
 
233
- export { index as default };
276
+ export { index as default, rollup };
@@ -0,0 +1,25 @@
1
+ type ParsedWasmModule = {
2
+ id?: string;
3
+ imports: ModuleImport[];
4
+ exports: ModuleExport[];
5
+ };
6
+ type ModuleImport = {
7
+ module: string;
8
+ name: string;
9
+ returnType?: string;
10
+ params?: {
11
+ id?: string;
12
+ type: string;
13
+ }[];
14
+ };
15
+ type ModuleExport = {
16
+ name: string;
17
+ id: string | number;
18
+ type: "Func" | "Memory";
19
+ };
20
+ type ParseResult = {
21
+ modules: ParsedWasmModule[];
22
+ };
23
+ declare function parseWasm(source: Buffer | ArrayBuffer): ParseResult;
24
+
25
+ export { type ModuleExport, type ModuleImport, type ParseResult, type ParsedWasmModule, parseWasm };
@@ -0,0 +1,25 @@
1
+ type ParsedWasmModule = {
2
+ id?: string;
3
+ imports: ModuleImport[];
4
+ exports: ModuleExport[];
5
+ };
6
+ type ModuleImport = {
7
+ module: string;
8
+ name: string;
9
+ returnType?: string;
10
+ params?: {
11
+ id?: string;
12
+ type: string;
13
+ }[];
14
+ };
15
+ type ModuleExport = {
16
+ name: string;
17
+ id: string | number;
18
+ type: "Func" | "Memory";
19
+ };
20
+ type ParseResult = {
21
+ modules: ParsedWasmModule[];
22
+ };
23
+ declare function parseWasm(source: Buffer | ArrayBuffer): ParseResult;
24
+
25
+ export { type ModuleExport, type ModuleImport, type ParseResult, type ParsedWasmModule, parseWasm };
package/dist/tools.mjs ADDED
@@ -0,0 +1,41 @@
1
+ import { decode } from '@webassemblyjs/wasm-parser';
2
+
3
+ function parseWasm(source) {
4
+ const ast = decode(source);
5
+ const modules = [];
6
+ for (const body of ast.body) {
7
+ if (body.type === "Module") {
8
+ const module = {
9
+ imports: [],
10
+ exports: []
11
+ };
12
+ modules.push(module);
13
+ for (const field of body.fields) {
14
+ if (field.type === "ModuleImport") {
15
+ module.imports.push({
16
+ module: field.module,
17
+ name: field.name,
18
+ returnType: field.descr?.signature?.results?.[0],
19
+ params: field.descr.signature.params?.map(
20
+ (p) => ({
21
+ id: p.id,
22
+ type: p.valtype
23
+ })
24
+ )
25
+ });
26
+ } else if (field.type === "ModuleExport") {
27
+ module.exports.push({
28
+ name: field.name,
29
+ id: field.descr.id.value,
30
+ type: field.descr.exportType
31
+ });
32
+ }
33
+ }
34
+ }
35
+ }
36
+ return {
37
+ modules
38
+ };
39
+ }
40
+
41
+ export { parseWasm };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unwasm",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "WebAssembly tools for JavaScript",
5
5
  "repository": "unjs/unwasm",
6
6
  "license": "MIT",
@@ -9,8 +9,12 @@
9
9
  "exports": {
10
10
  "./examples/*": "./examples/*",
11
11
  "./plugin": {
12
- "types": "./dist/plugin/index.d.mts",
13
- "import": "./dist/plugin/index.mjs"
12
+ "types": "./dist/plugin.d.mts",
13
+ "import": "./dist/plugin.mjs"
14
+ },
15
+ "./tools": {
16
+ "types": "./dist/tools.d.mts",
17
+ "import": "./dist/tools.mjs"
14
18
  }
15
19
  },
16
20
  "files": [
@@ -30,6 +34,7 @@
30
34
  "test:types": "tsc --noEmit --skipLibCheck"
31
35
  },
32
36
  "dependencies": {
37
+ "@webassemblyjs/wasm-parser": "^1.11.6",
33
38
  "magic-string": "^0.30.5",
34
39
  "mlly": "^1.4.2",
35
40
  "pathe": "^1.1.1",
package/plugin.d.ts CHANGED
@@ -1 +1,3 @@
1
1
  export * from "./dist/plugin";
2
+
3
+ export { default } from "./dist/plugin";
@@ -1,22 +0,0 @@
1
- import { Plugin } from 'rollup';
2
-
3
- interface UnwasmPluginOptions {
4
- /**
5
- * Direct import the wasm file instead of bundling, required in Cloudflare Workers
6
- *
7
- * @default false
8
- */
9
- esmImport?: boolean;
10
- /**
11
- * Import `.wasm` files using a lazily evaluated promise for compatibility with runtimes without top-level await support
12
- *
13
- * @default false
14
- */
15
- lazy?: boolean;
16
- }
17
-
18
- declare const _default: {
19
- rollup: (opts: UnwasmPluginOptions) => Plugin<any>;
20
- };
21
-
22
- export { _default as default };
@@ -1,22 +0,0 @@
1
- import { Plugin } from 'rollup';
2
-
3
- interface UnwasmPluginOptions {
4
- /**
5
- * Direct import the wasm file instead of bundling, required in Cloudflare Workers
6
- *
7
- * @default false
8
- */
9
- esmImport?: boolean;
10
- /**
11
- * Import `.wasm` files using a lazily evaluated promise for compatibility with runtimes without top-level await support
12
- *
13
- * @default false
14
- */
15
- lazy?: boolean;
16
- }
17
-
18
- declare const _default: {
19
- rollup: (opts: UnwasmPluginOptions) => Plugin<any>;
20
- };
21
-
22
- export { _default as default };