Skip to content

Commit

Permalink
feat!: support custom init
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Dec 28, 2023
1 parent d03a1cb commit 6861425
Show file tree
Hide file tree
Showing 15 changed files with 276 additions and 116 deletions.
67 changes: 56 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![Codecov][codecov-src]][codecov-href]

# 🇼 unwasm
# unwasm

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

## Goal

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.
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.

## Roadmap

The development will be split into multiple stages.

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

## Install
## Bindings API

Install package from [npm](https://www.npmjs.com/package/unwasm):
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.

WebAssembly modules that don't require any imports, can be imported simply like you import any other esm module:

**Using static import:**

```js
import { func } from "lib/module.wasm";
```

**Using dynamic import:**

```js
const { func } = await import("lib/module.wasm").then((mod) => mod.default);
```

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:

**Using static import with imports object:**

```js
import { func, $init } from "lib/module.wasm";

await $init({ env: {} });
```

**Using dynamic import with imports object:**

```js
const { func } = await import("lib/module.wasm").then((mod) => mod.$init(env));
```

> [!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.
> [!NOTE]
> Named exports with `$` prefix are reserved for unwasm. In case your module uses them, you can access from `$exports` property.
## Usage

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.

### Install

First, install the [`unwasm` npm package](https://www.npmjs.com/package/unwasm):

```sh
# npm
npm install unwasm
npm install --dev unwasm

# yarn
yarn add unwasm
yarn add -D unwasm

# pnpm
pnpm install unwasm
pnpm i -D unwasm

# bun
bun install unwasm
bun i -D unwasm
```

## Using build plugin
### Builder Plugins

###### Rollup

```js
// rollup.config.js
import unwasmPlugin from "unwasm/plugin";

export default {
Expand All @@ -59,7 +103,7 @@ export default {
};
```

### Options
### Plugin Options

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

## License

Expand Down
2 changes: 1 addition & 1 deletion build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import { defineBuildConfig } from "unbuild";

export default defineBuildConfig({
declaration: true,
entries: ["src/plugin"],
entries: ["src/plugin/index"],
externals: ["unwasm", "rollup"],
});
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
"type": "module",
"exports": {
"./plugin": {
"types": "./dist/plugin.d.mts",
"import": "./dist/plugin.mjs"
"types": "./dist/plugin/index.d.mts",
"import": "./dist/plugin/index.mjs"
}
},
"files": [
"dist"
"dist",
"*.d.ts"
],
"scripts": {
"build": "unbuild",
Expand Down Expand Up @@ -48,4 +49,4 @@
"vitest": "^1.1.0"
},
"packageManager": "pnpm@8.12.1"
}
}
1 change: 1 addition & 0 deletions plugin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./dist/plugin";
5 changes: 0 additions & 5 deletions src/_utils.ts

This file was deleted.

6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* eslint-disable unicorn/no-empty-file */ // <-- WTF!

// No runtime utils yet!
// https://github.com/unjs/unwasm/issues/4

// See plugin/ for real stuff!
93 changes: 11 additions & 82 deletions src/plugin.ts → src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,17 @@ import { basename } from "pathe";
import MagicString from "magic-string";
import type { RenderedChunk, Plugin as RollupPlugin } from "rollup";
import { createUnplugin } from "unplugin";
import { sha1 } from "./_utils";

const UNWASM_EXTERNAL_PREFIX = "\0unwasm:external:";
const UMWASM_HELPERS_ID = "\0unwasm:helpers";

export interface UnwasmPluginOptions {
/**
* Direct import the wasm file instead of bundling, required in Cloudflare Workers
*
* @default false
*/
esmImport?: boolean;

/**
* Import `.wasm` files using a lazily evaluated promise for compatibility with runtimes without top-level await support
*
* @default false
*/
lazy?: boolean;
}
import {
sha1,
UMWASM_HELPERS_ID,
UNWASM_EXTERNAL_PREFIX,
UnwasmPluginOptions,
WasmAsset,
} from "./shared";
import { getPluginUtils, getWasmBinding } from "./runtime";

const unplugin = createUnplugin<UnwasmPluginOptions>((opts) => {
type WasmAsset = {
name: string;
source: Buffer;
};

const assets: Record<string, WasmAsset> = Object.create(null);

return {
name: "unwasm",
rollup: {
Expand Down Expand Up @@ -78,9 +60,9 @@ const unplugin = createUnplugin<UnwasmPluginOptions>((opts) => {
}
const source = await fs.readFile(id);
const name = `wasm/${basename(id, ".wasm")}-${sha1(source)}.wasm`;
assets[id] = <WasmAsset>{ name, source };
assets[id] = <WasmAsset>{ name, id, source };
// TODO: Can we parse wasm to extract exports and avoid syntheticNamedExports?
return `export default "WASM";`; // dummy
return `export default "UNWASM DUMMY EXPORT";`;
},
transform(_code, id) {
if (!id.endsWith(".wasm")) {
Expand All @@ -91,46 +73,8 @@ const unplugin = createUnplugin<UnwasmPluginOptions>((opts) => {
return;
}

const envCode: string = opts.esmImport
? `
async function _instantiate(imports) {
const _mod = await import("${UNWASM_EXTERNAL_PREFIX}${id}").then(r => r.default || r);
return WebAssembly.instantiate(_mod, imports)
}
`
: `
import { base64ToUint8Array } from "${UMWASM_HELPERS_ID}";
function _instantiate(imports) {
const _mod = base64ToUint8Array("${asset.source.toString("base64")}")
return WebAssembly.instantiate(_mod, imports)
}
`;

const code = `${envCode}
const _defaultImports = Object.create(null);
// TODO: For testing only
Object.assign(_defaultImports, { env: { "seed": () => () => Date.now() * Math.random() } })
const instancePromises = new WeakMap();
function instantiate(imports = _defaultImports) {
let p = instancePromises.get(imports);
if (!p) {
p = _instantiate(imports);
instancePromises.set(imports, p);
}
return p;
}
const _instance = instantiate();
const _exports = _instance.then(r => r?.instance?.exports || r?.exports || r);
export default ${opts.lazy ? "" : "await "} _exports;
`;

return {
code,
code: getWasmBinding(asset, opts),
map: { mappings: "" },
syntheticNamedExports: true,
};
Expand All @@ -147,7 +91,6 @@ export default ${opts.lazy ? "" : "await "} _exports;
) ||
!code.includes(UNWASM_EXTERNAL_PREFIX)
) {
console.log(chunk);
return;
}
const s = new MagicString(code);
Expand Down Expand Up @@ -190,20 +133,6 @@ export default ${opts.lazy ? "" : "await "} _exports;
};
});

export function getPluginUtils() {
return `
export function base64ToUint8Array(str) {
const data = atob(str);
const size = data.length;
const bytes = new Uint8Array(size);
for (let i = 0; i < size; i++) {
bytes[i] = data.charCodeAt(i);
}
return bytes;
}
`;
}

const rollup = unplugin.rollup as (opts: UnwasmPluginOptions) => RollupPlugin;

export default {
Expand Down

0 comments on commit 6861425

Please sign in to comment.