unwasm 0.1.0 → 0.2.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
@@ -1,53 +1,110 @@
1
- # 🇼 unwasm
2
-
3
1
  [![npm version][npm-version-src]][npm-version-href]
4
2
  [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
3
  [![Codecov][codecov-src]][codecov-href]
6
4
 
5
+ # unwasm
6
+
7
7
  Universal [WebAssembly](https://webassembly.org/) tools for JavaScript.
8
8
 
9
9
  ## Goal
10
10
 
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.
12
12
 
13
13
  ## Roadmap
14
14
 
15
15
  The development will be split into multiple stages.
16
16
 
17
17
  > [!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!
19
19
 
20
- - [ ] Universal builder plugins ([unjs/unwasm#2](https://github.com/unjs/unwasm/issues/2)) built with [unjs/unplugin](https://github.com/unjs/unplugin)
20
+ - [ ] Universal builder plugins built with [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))
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
24
  - [ ] ESM loader for Node.js and other JavaScript runtimes ([unjs/unwasm#5](https://github.com/unjs/unwasm/issues/5))
25
- - [ ] Integration with [Wasmer](https://github.com/wasmerio) ([unjs/unwasm#6](https://github.com/unjs/unwasm/issues/6))
25
+ - [ ] Integration with [Wasmer](https://github.com/wasmerio) ([unjs/unwasm#6](https://github.com/unjs/unwasm/issues/6))
26
26
  - [ ] Convention and tools for library authors exporting wasm modules ([unjs/unwasm#7](https://github.com/unjs/unwasm/issues/7))
27
27
 
28
- ## Install
28
+ ## Bindings API
29
+
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
+
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 { sum } from "unwasm/examples/sum.wasm";
38
+ ```
39
+
40
+ **Using dynamic import:**
41
+
42
+ ```js
43
+ const { sum } = await import("unwasm/examples/sum.wasm").then(
44
+ (mod) => mod.default,
45
+ );
46
+ ```
47
+
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
+ ```
61
+
62
+ **Using dynamic import with imports object:**
63
+
64
+ ```js
65
+ const { rand } = await import("unwasm/examples/rand.wasm").then((mod) =>
66
+ mod.$init({
67
+ env: {
68
+ seed: () => () => Math.random() * Date.now(),
69
+ },
70
+ }),
71
+ );
72
+ ```
73
+
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.
76
+
77
+ > [!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.
79
+
80
+ ## Usage
81
+
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.
83
+
84
+ ### Install
29
85
 
30
- Install package from [npm](https://www.npmjs.com/package/unwasm):
86
+ First, install the [`unwasm` npm package](https://www.npmjs.com/package/unwasm):
31
87
 
32
88
  ```sh
33
89
  # npm
34
- npm install unwasm
90
+ npm install --dev unwasm
35
91
 
36
92
  # yarn
37
- yarn add unwasm
93
+ yarn add -D unwasm
38
94
 
39
95
  # pnpm
40
- pnpm install unwasm
96
+ pnpm i -D unwasm
41
97
 
42
98
  # bun
43
- bun install unwasm
99
+ bun i -D unwasm
44
100
  ```
45
101
 
46
- ## Using build plugin
102
+ ### Builder Plugins
47
103
 
48
104
  ###### Rollup
49
105
 
50
106
  ```js
107
+ // rollup.config.js
51
108
  import unwasmPlugin from "unwasm/plugin";
52
109
 
53
110
  export default {
@@ -59,7 +116,7 @@ export default {
59
116
  };
60
117
  ```
61
118
 
62
- ### Options
119
+ ### Plugin Options
63
120
 
64
121
  - `esmImport`: Direct import the wasm file instead of bundling, required in Cloudflare Workers (default is `false`)
65
122
  - `lazy`: Import `.wasm` files using a lazily evaluated promise for compatibility with runtimes without top-level await support (default is `false`)
@@ -71,6 +128,7 @@ export default {
71
128
  - Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
72
129
  - Install dependencies using `pnpm install`
73
130
  - Run interactive tests using `pnpm dev`
131
+ - Optionally install [es6-string-html](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) extension to make it easier to work with string templates.
74
132
 
75
133
  ## License
76
134
 
@@ -14,8 +14,9 @@ interface UnwasmPluginOptions {
14
14
  */
15
15
  lazy?: boolean;
16
16
  }
17
+
17
18
  declare const _default: {
18
19
  rollup: (opts: UnwasmPluginOptions) => Plugin<any>;
19
20
  };
20
21
 
21
- export { type UnwasmPluginOptions, _default as default };
22
+ export { _default as default };
@@ -14,8 +14,9 @@ interface UnwasmPluginOptions {
14
14
  */
15
15
  lazy?: boolean;
16
16
  }
17
+
17
18
  declare const _default: {
18
19
  rollup: (opts: UnwasmPluginOptions) => Plugin<any>;
19
20
  };
20
21
 
21
- export { type UnwasmPluginOptions, _default as default };
22
+ export { _default as default };
@@ -0,0 +1,233 @@
1
+ import { existsSync, promises } from 'node:fs';
2
+ import { basename } from 'pathe';
3
+ import MagicString from 'magic-string';
4
+ import { createUnplugin } from 'unplugin';
5
+ import { createHash } from 'node:crypto';
6
+
7
+ const UNWASM_EXTERNAL_PREFIX = "\0unwasm:external:";
8
+ const UMWASM_HELPERS_ID = "\0unwasm:helpers";
9
+ function sha1(source) {
10
+ return createHash("sha1").update(source).digest("hex").slice(0, 16);
11
+ }
12
+
13
+ const js = String.raw;
14
+ function getWasmBinding(asset, opts) {
15
+ const envCode = opts.esmImport ? js`
16
+ async function _instantiate(imports) {
17
+ const _mod = await import("${UNWASM_EXTERNAL_PREFIX}${asset.id}").then(r => r.default || r);
18
+ return WebAssembly.instantiate(_mod, imports)
19
+ }
20
+ ` : js`
21
+ import { base64ToUint8Array } from "${UMWASM_HELPERS_ID}";
22
+
23
+ function _instantiate(imports) {
24
+ const _data = base64ToUint8Array("${asset.source.toString("base64")}")
25
+ return WebAssembly.instantiate(_data, imports)
26
+ }
27
+ `;
28
+ return js`
29
+ import { createUnwasmModule } from "${UMWASM_HELPERS_ID}";
30
+ ${envCode}
31
+ const _mod = createUnwasmModule(_instantiate);
32
+
33
+ export const $init = _mod.$init.bind(_mod);
34
+ export const exports = _mod;
35
+
36
+ export default _mod;
37
+ `;
38
+ }
39
+ function getPluginUtils() {
40
+ return js`
41
+ export function debug(...args) {
42
+ console.log('[wasm]', ...args);
43
+ }
44
+
45
+ function getExports(input) {
46
+ return input?.instance?.exports || input?.exports || input;
47
+ }
48
+
49
+ export function base64ToUint8Array(str) {
50
+ const data = atob(str);
51
+ const size = data.length;
52
+ const bytes = new Uint8Array(size);
53
+ for (let i = 0; i < size; i++) {
54
+ bytes[i] = data.charCodeAt(i);
55
+ }
56
+ return bytes;
57
+ }
58
+
59
+ export function createUnwasmModule(_instantiator) {
60
+ const _exports = Object.create(null);
61
+ let _loaded;
62
+ let _promise;
63
+
64
+ const $init = (imports) => {
65
+ if (_loaded) {
66
+ return Promise.resolve(_proxy);
67
+ }
68
+ if (_promise) {
69
+ return _promise;
70
+ }
71
+ return _promise = _instantiator(imports)
72
+ .then(r => {
73
+ Object.assign(_exports, getExports(r));
74
+ _loaded = true;
75
+ _promise = undefined;
76
+ return _proxy;
77
+ })
78
+ .catch(error => {
79
+ _promise = undefined;
80
+ console.error('[wasm]', error);
81
+ throw error;
82
+ });
83
+ }
84
+
85
+ const $exports = new Proxy(_exports, {
86
+ get(_, prop) {
87
+ if (_loaded) {
88
+ return _exports[prop];
89
+ }
90
+ return (...args) => {
91
+ return _loaded
92
+ ? _exports[prop]?.(...args)
93
+ : $init().then(() => $exports[prop]?.(...args));
94
+ };
95
+ },
96
+ });
97
+
98
+ const _instance = {
99
+ $init,
100
+ $exports,
101
+ };
102
+
103
+ const _proxy = new Proxy(_instance, {
104
+ get(_, prop) {
105
+ // Reserve all to avoid future breaking changes
106
+ if (prop.startsWith('$')) {
107
+ return _instance[prop];
108
+ }
109
+ return $exports[prop];
110
+ }
111
+ });
112
+
113
+ return _proxy;
114
+ }
115
+ `;
116
+ }
117
+
118
+ const unplugin = createUnplugin((opts) => {
119
+ const assets = /* @__PURE__ */ Object.create(null);
120
+ return {
121
+ name: "unwasm",
122
+ rollup: {
123
+ async resolveId(id, importer) {
124
+ if (id === UMWASM_HELPERS_ID) {
125
+ return id;
126
+ }
127
+ if (id.startsWith(UNWASM_EXTERNAL_PREFIX)) {
128
+ return {
129
+ id,
130
+ external: true
131
+ };
132
+ }
133
+ if (id.endsWith(".wasm")) {
134
+ const r = await this.resolve(id, importer, { skipSelf: true });
135
+ if (r?.id && r.id !== id) {
136
+ return {
137
+ id: r.id.startsWith("file://") ? r.id.slice(7) : r.id,
138
+ external: false,
139
+ moduleSideEffects: false,
140
+ syntheticNamedExports: true
141
+ };
142
+ }
143
+ }
144
+ },
145
+ generateBundle() {
146
+ if (opts.esmImport) {
147
+ for (const asset of Object.values(assets)) {
148
+ this.emitFile({
149
+ type: "asset",
150
+ source: asset.source,
151
+ fileName: asset.name
152
+ });
153
+ }
154
+ }
155
+ }
156
+ },
157
+ async load(id) {
158
+ if (id === UMWASM_HELPERS_ID) {
159
+ return getPluginUtils();
160
+ }
161
+ if (!id.endsWith(".wasm") || !existsSync(id)) {
162
+ return;
163
+ }
164
+ const source = await promises.readFile(id);
165
+ const name = `wasm/${basename(id, ".wasm")}-${sha1(source)}.wasm`;
166
+ assets[id] = { name, id, source };
167
+ return `export default "UNWASM DUMMY EXPORT";`;
168
+ },
169
+ transform(_code, id) {
170
+ if (!id.endsWith(".wasm")) {
171
+ return;
172
+ }
173
+ const asset = assets[id];
174
+ if (!asset) {
175
+ return;
176
+ }
177
+ return {
178
+ code: getWasmBinding(asset, opts),
179
+ map: { mappings: "" },
180
+ syntheticNamedExports: true
181
+ };
182
+ },
183
+ renderChunk(code, chunk) {
184
+ if (!opts.esmImport) {
185
+ return;
186
+ }
187
+ if (!(chunk.moduleIds.some((id) => id.endsWith(".wasm")) || chunk.imports.some((id) => id.endsWith(".wasm"))) || !code.includes(UNWASM_EXTERNAL_PREFIX)) {
188
+ return;
189
+ }
190
+ const s = new MagicString(code);
191
+ const resolveImport = (id) => {
192
+ if (typeof id !== "string") {
193
+ return;
194
+ }
195
+ const asset = assets[id];
196
+ if (!asset) {
197
+ return;
198
+ }
199
+ const nestedLevel = chunk.fileName.split("/").length - 1;
200
+ const relativeId = (nestedLevel ? "../".repeat(nestedLevel) : "./") + asset.name;
201
+ return {
202
+ relativeId,
203
+ asset
204
+ };
205
+ };
206
+ const ReplaceRE = new RegExp(`${UNWASM_EXTERNAL_PREFIX}([^"']+)`, "g");
207
+ for (const match of code.matchAll(ReplaceRE)) {
208
+ const resolved = resolveImport(match[1]);
209
+ const index = match.index;
210
+ const len = match[0].length;
211
+ if (!resolved || !index) {
212
+ console.warn(
213
+ `Failed to resolve WASM import: ${JSON.stringify(match[1])}`
214
+ );
215
+ continue;
216
+ }
217
+ s.overwrite(index, index + len, resolved.relativeId);
218
+ }
219
+ if (s.hasChanged()) {
220
+ return {
221
+ code: s.toString(),
222
+ map: s.generateMap({ includeContent: true })
223
+ };
224
+ }
225
+ }
226
+ };
227
+ });
228
+ const rollup = unplugin.rollup;
229
+ const index = {
230
+ rollup
231
+ };
232
+
233
+ export { index as default };
Binary file
Binary file
package/package.json CHANGED
@@ -1,22 +1,26 @@
1
1
  {
2
2
  "name": "unwasm",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "WebAssembly tools for JavaScript",
5
5
  "repository": "unjs/unwasm",
6
6
  "license": "MIT",
7
7
  "sideEffects": false,
8
8
  "type": "module",
9
9
  "exports": {
10
+ "./examples/*": "./examples/*",
10
11
  "./plugin": {
11
- "types": "./dist/plugin.d.mts",
12
- "import": "./dist/plugin.mjs"
12
+ "types": "./dist/plugin/index.d.mts",
13
+ "import": "./dist/plugin/index.mjs"
13
14
  }
14
15
  },
15
16
  "files": [
16
- "dist"
17
+ "dist",
18
+ "*.d.ts",
19
+ "examples/*.wasm"
17
20
  ],
18
21
  "scripts": {
19
- "build": "unbuild",
22
+ "build": "unbuild && pnpm build:examples",
23
+ "build:examples": "node ./examples/build.mjs",
20
24
  "dev": "vitest dev",
21
25
  "lint": "eslint --cache --ext .ts,.js,.mjs,.cjs . && prettier -c src test",
22
26
  "lint:fix": "eslint --cache --ext .ts,.js,.mjs,.cjs . --fix && prettier -c src test -w",
@@ -35,10 +39,12 @@
35
39
  "@rollup/plugin-node-resolve": "^15.2.3",
36
40
  "@types/node": "^20.10.5",
37
41
  "@vitest/coverage-v8": "^1.1.0",
42
+ "assemblyscript": "^0.27.22",
38
43
  "changelogen": "^0.5.5",
39
44
  "eslint": "^8.56.0",
40
45
  "eslint-config-unjs": "^0.2.1",
41
46
  "jiti": "^1.21.0",
47
+ "miniflare": "^3.20231030.4",
42
48
  "prettier": "^3.1.1",
43
49
  "rollup": "^4.9.1",
44
50
  "typescript": "^5.3.3",
package/plugin.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./dist/plugin";
package/dist/plugin.mjs DELETED
@@ -1,129 +0,0 @@
1
- import { existsSync, promises } from 'node:fs';
2
- import { basename } from 'pathe';
3
- import MagicString from 'magic-string';
4
- import { createUnplugin } from 'unplugin';
5
- import { createHash } from 'node:crypto';
6
-
7
- function sha1(source) {
8
- return createHash("sha1").update(source).digest("hex").slice(0, 16);
9
- }
10
-
11
- const WASM_EXTERNAL_ID = "\0unwasm:external:";
12
- const unplugin = createUnplugin((opts) => {
13
- const assets = /* @__PURE__ */ Object.create(null);
14
- return {
15
- name: "unwasm",
16
- rollup: {
17
- async resolveId(id, importer) {
18
- if (id.startsWith(WASM_EXTERNAL_ID)) {
19
- return {
20
- id,
21
- external: true
22
- };
23
- }
24
- if (id.endsWith(".wasm")) {
25
- const r = await this.resolve(id, importer, { skipSelf: true });
26
- if (r?.id && r?.id !== id) {
27
- return {
28
- id: r.id.startsWith("file://") ? r.id.slice(7) : r.id,
29
- external: false,
30
- moduleSideEffects: false,
31
- syntheticNamedExports: false
32
- };
33
- }
34
- }
35
- },
36
- generateBundle() {
37
- if (opts.esmImport) {
38
- for (const asset of Object.values(assets)) {
39
- this.emitFile({
40
- type: "asset",
41
- source: asset.source,
42
- fileName: asset.name
43
- });
44
- }
45
- }
46
- }
47
- },
48
- async load(id) {
49
- if (!id.endsWith(".wasm") || !existsSync(id)) {
50
- return;
51
- }
52
- const source = await promises.readFile(id);
53
- const name = `wasm/${basename(id, ".wasm")}-${sha1(source)}.wasm`;
54
- assets[id] = { name, source };
55
- return `export default "WASM";`;
56
- },
57
- transform(_code, id) {
58
- if (!id.endsWith(".wasm")) {
59
- return;
60
- }
61
- const asset = assets[id];
62
- if (!asset) {
63
- return;
64
- }
65
- let _dataStr;
66
- if (opts.esmImport) {
67
- _dataStr = `await import("${WASM_EXTERNAL_ID}${id}").then(r => r?.default || r)`;
68
- } else {
69
- const base64Str = asset.source.toString("base64");
70
- _dataStr = `(()=>{const d=atob("${base64Str}");const s=d.length;const b=new Uint8Array(s);for(let i=0;i<s;i++)b[i]=d.charCodeAt(i);return b})()`;
71
- }
72
- let _str = `await WebAssembly.instantiate(${_dataStr}, { env: { "Math.random": () => Math.random, "Math.floor": () => Math.floor } }).then(r => r?.exports||r?.instance?.exports || r);`;
73
- if (opts.lazy) {
74
- _str = `(()=>{const e=async()=>{return ${_str}};let _p;const p=()=>{if(!_p)_p=e();return _p;};return {then:cb=>p().then(cb),catch:cb=>p().catch(cb)}})()`;
75
- }
76
- return {
77
- code: `export default ${_str};`,
78
- map: { mappings: "" },
79
- syntheticNamedExports: true
80
- };
81
- },
82
- renderChunk(code, chunk) {
83
- if (!chunk.moduleIds.some((id) => id.endsWith(".wasm")) || !code.includes(WASM_EXTERNAL_ID)) {
84
- return;
85
- }
86
- const s = new MagicString(code);
87
- const resolveImport = (id) => {
88
- if (typeof id !== "string") {
89
- return;
90
- }
91
- const asset = assets[id];
92
- if (!asset) {
93
- return;
94
- }
95
- const nestedLevel = chunk.fileName.split("/").length - 1;
96
- const relativeId = (nestedLevel ? "../".repeat(nestedLevel) : "./") + asset.name;
97
- return {
98
- relativeId,
99
- asset
100
- };
101
- };
102
- const ReplaceRE = new RegExp(`${WASM_EXTERNAL_ID}([^"']+)`, "g");
103
- for (const match of code.matchAll(ReplaceRE)) {
104
- const resolved = resolveImport(match[1]);
105
- const index = match.index;
106
- const len = match[0].length;
107
- if (!resolved || !index) {
108
- console.warn(
109
- `Failed to resolve WASM import: ${JSON.stringify(match[1])}`
110
- );
111
- continue;
112
- }
113
- s.overwrite(index, index + len, resolved.relativeId);
114
- }
115
- if (s.hasChanged()) {
116
- return {
117
- code: s.toString(),
118
- map: s.generateMap({ includeContent: true })
119
- };
120
- }
121
- }
122
- };
123
- });
124
- const rollup = unplugin.rollup;
125
- const plugin = {
126
- rollup
127
- };
128
-
129
- export { plugin as default };