@bytecodealliance/rolldown-plugin-jco
v0.1.0
Published
Rolldown and Rollup plugin for using the Jco WebAssembly component toolkit
Readme
@bytecodealliance/rolldown-plugin-jco
Easily import functionality provided by WebAssembly Components from JS/TS projects
that bundle with Rolldown/Rollup, inspired by unwasm.
This plugin transpiles each component with Jco (i.e. jco transpile) to an ES module,
then adds the generated bindings to the module graph, emitting core WebAssembly modules as
bundler-managed assets, along with functions you can use directly.
WebAssembly components are WebAssembly binaries that conform to the Component Model, which enables cross-language robust types, interface/contract driven development, async behavior, and much more.
WebAssembly components are distinct from WebAssembly modules generated by emscripten
or wasm-bindgen. You can read more about WebAssembly components in the
WebAssembly Component Book, maintained by the Bytecode Alliance.
Install
After installing your bundler of choice (rolldown/rollup), you can install the plugin via NPM
pnpm add -D @bytecodealliance/rolldown-plugin-jcoRolldown
To use the plugin from rolldown:
// rolldown.config.mjs
import { defineConfig } from "rolldown";
import jco from "@bytecodealliance/rolldown-plugin-jco";
export default defineConfig({
input: "src/main.js",
platform: "node",
plugins: [jco()],
});For Node.js WASI components, the plugin also emits self-contained worker artifacts owned by the Preview 2 and Preview 3 shims. No shim-internal worker entry or copy step is required in application configuration.
Rollup
The plugin uses the Rollup-compatible plugin API, so the same configuration works in Rollup:
// rollup.config.mjs
import jco from "@bytecodealliance/rolldown-plugin-jco";
export default {
input: "src/main.js",
plugins: [jco()],
};Importing and using WebAssembly Components
Given a WebAssembly Component calculator.wasm with the following WIT:
package jco:examples;
interface calculator {
/// Errors that may occur during the course of using `add()`
variant add-error {
overflow,
underflow,
other(string),
}
/// Addition of two signed integers
add: func(a: s32, b: s32) -> result<s32, add-error>;
}
world component {
export calculator;
}The default export is a way to instantiate the component, providing it all the imports it needs.
import instantiate from "./calculator.wasm";
const component = instantiate();
console.log(component.calculator.add(1, 2));For components without any imports (the calculator.wasm example case), you can use the
relevant interface from the component right away, because component model exports are
available as named imports:
import instantiate, { add } from "./adder.wasm";
console.log(add.add(1, 2));
// assert(instantiate().add === add);To group all imports, under one name, you can use a namespace import:
import * as component from "./adder.wasm";
console.log(component.add.add(1, 2));
// assert(component.default().add === component.add);Dynamic imports are also supported:
const component = await import("./adder.wasm");
// assert(component.default().add === component.add);
console.log(component.add.add(1, 2));?component is accepted as an explicit marker when it is useful to distinguish
component imports:
import component from "./calculator.wasm?component";Other Wasm query conventions such as ?url and ?module are left to other plugins (e.g. unwasm).
Typescript types
TypeScript projects can opt into a generic default-import declaration:
{
"compilerOptions": {
"types": ["@bytecodealliance/rolldown-plugin-jco/wasm"]
}
}[!WARN] Precise component TypeScript declarations are not yet supported, but will be in a future version
Custom instantiation
Components with imports can use Jco's custom instantiation API. Enable async or sync instantiation in the plugin configuration:
jco({
transpile: {
instantiation: "async",
},
});The default function's first argument loads a bundler-emitted core Wasm asset, the second supplies the component-model imports, and the optional third argument overrides core Wasm instantiation:
import instantiate, { run } from "./hosted.wasm";
const instance = await instantiate(async (url) => WebAssembly.compile(await (await fetch(url)).arrayBuffer()), {
"example:host/api": {
getValue() {
return 42;
},
},
});
console.log(instance.run === run); // true
console.log(run());In custom mode, named component exports are live ESM bindings. They are undefined until
instantiation succeeds and update automatically afterwards.
Avoid destructuring a namespace object before instantiation, because ordinary object destructuring captures the initial value rather than the live binding.
The default function also returns the complete component instance for convenient local destructuring:
const { run: localRun } = await instantiate(loadCoreModule, imports);A failed instantiation leaves every named binding unset and can be retried.
After a successful instantiation, another call throws because one imported module cannot represent multiple component instances.
Because lifecycle instantiation is the default export, a component-model export named instantiate
does not collide with it:
import instantiateComponent, { instantiate } from "./component.wasm";
await instantiateComponent(loadCoreModule, imports);
instantiate(); // the component-model exportIn eager mode (when transpile.instantiation is not set), components are instantiated as the module
loads, named exports are immediately available, and calling the default function simply returns the
component namespace.
Options
Here are the options that are accepted by this plugin:
jco({
include: ["src/**/*.wasm"],
exclude: ["src/core-wasm/**"],
transpile: {
minify: true,
map: {
"example:host/*": "./src/host.js#*",
},
},
name(id) {
return "my-component";
},
});FAQ
Some things to keep in mind, when using this plugin:
includeandexcludeuse the standard Rollup plugin filter syntax.transpileis forwarded to Jco. The plugin ownsnameandoutDirbecause generated files are managed inside the bundler graph.transpile.instantiationselects Jco's"async"or"sync"custom-instantiation mode.nameoptionally controls the name passed to Jco. By default the plugin combines the input basename with a stable path hash to avoid collisions.- Input files must be local WebAssembly Component files available at build time.
- Plain "core" Wasm modules are not valid inputs; exclude them or route them to a plugin that prioritizes core wasm such as unwasm.
- Components are deduplicated and may be inlined
- Only ESM module output is supported
Jco's normal WebAssembly System Interface ("WASI") mappings apply. Components that use WASI therefore retain the corresponding Preview 2 or Preview 3 shim imports in the generated module, and those runtime packages must be resolvable by the consuming build.
