Convert JavaScript source code into WebAssembly components using QuickJS.
componentize-qjs takes a JavaScript source file and a
WIT definition,
and produces a standalone WebAssembly component that can run on any
component-model runtime (e.g. Wasmtime).
Under the hood it:
- Embeds the QuickJS engine (via rquickjs) as the JavaScript runtime.
- Uses wit-dylib to generate WIT bindings that bridge the component model and the JS engine.
- Snapshots the initialized JS state with Wizer so startup cost is paid at build time, not at runtime.
Building currently requires a nightly Rust toolchain because it relies on the wasi-libc being compiled with -fPIC. This requirement will be lifted with an upcoming version of Rust.
cargo +nightly install --path .The npm package is not yet published to the registry. To use it, build from source:
cd npm && npm install && npm run build1. Define a WIT interface (hello.wit):
package test:hello;
world hello {
export greet: func(name: string) -> string;
}2. Implement it in JavaScript (hello.js):
function greet(name) {
return `Hello, ${name}!`;
}3. Build the component:
componentize-qjs --wit hello.wit --js hello.js -o hello.wasm4. Run it:
wasmtime run --invoke 'greet("World")' hello.wasm
# Hello, World!componentize-qjs [OPTIONS] --wit <WIT> --js <JS>
| Flag | Short | Description |
|---|---|---|
--wit <PATH> |
-w |
Path to the WIT file or directory |
--js <PATH> |
-j |
Path to the JavaScript source file |
--output <PATH> |
-o |
Output path (default: output.wasm) |
--world <NAME> |
-n |
World name when the WIT defines multiple worlds |
--stub-wasi |
Replace all WASI imports with trap stubs | |
--minify |
-m |
Minify JS source before embedding |
| Feature | Effect |
|---|---|
optimize-size |
Uses -Oz instead of -O3 for smaller output |
Build with features:
cargo +nightly build --release --features optimize-sizeWIT imports are available on globalThis keyed by their fully-qualified name:
// imports.wit
package local:test;
interface math {
add: func(a: s32, b: s32) -> s32;
multiply: func(a: s32, b: s32) -> s32;
}
world imports {
import math;
export double-add: func(a: s32, b: s32) -> s32;
}// imports.js
function doubleAdd(a, b) {
const math = globalThis["local:test/math"];
const sum = math.add(a, b);
return math.multiply(sum, 2);
}| WIT Type | JS Type | Notes |
|---|---|---|
bool |
boolean |
|
u8, u16, u32 |
number |
|
s8, s16, s32 |
number |
|
u64, s64 |
number |
Precision limited to 2⁵³ (Number.MAX_SAFE_INTEGER) |
f32, f64 |
number |
|
char |
string |
Must be exactly one Unicode scalar value |
string |
string |
| WIT Type | JS Type | Example |
|---|---|---|
list<T> |
Array |
[1, 2, 3] |
list<u8> |
Uint8Array or Array |
new Uint8Array([1, 2, 3]) |
tuple<T, U, ...> |
Array |
[42, "hello"] |
option<T> |
T | null | undefined |
null for none |
result<T, E> |
{ tag: "ok"|"err", val?: T|E } |
{ tag: "ok", val: 42 } |
record { ... } |
object (camelCase keys) |
{ myField: 1 } |
variant |
{ tag: number, val?: T } |
{ tag: 0, val: "hi" } |
enum |
number |
Lookup tables provided on the interface |
flags |
number (bitmask) |
Bit constants provided on the interface |
own<R>, borrow<R> |
number (handle) |
Opaque resource handle |
Async exports are declared with the async keyword in WIT and implemented
as JavaScript async functions:
package example:greeting;
world greeter {
export greet: async func(name: string) -> string;
}async function greet(name) {
// You can use await here
await Promise.resolve();
return `Hello, ${name}!`;
}Streams transfer a sequence of values between components.
The wit global provides Stream and Future constructors for creating
stream/future pairs. The type is automatically determined from the WIT definition:
package example:streaming;
world streaming {
export produce: async func() -> stream<u8>;
}async function produce() {
// When only one stream type exists in the WIT, no argument needed
const { readable, writable } = wit.Stream();
writable.write(new Uint8Array([1, 2, 3]));
writable.drop();
return readable;
}When the WIT defines multiple stream types, use the type constant:
// wit.Stream.U8, wit.Stream.STRING, wit.Stream.U32, etc.
const { readable, writable } = wit.Stream(wit.Stream.U8);
const { readable, writable } = wit.Stream(wit.Stream.STRING);Available type constants (populated from WIT metadata):
| WIT type | Constant |
|---|---|
stream<u8> / future<u8> |
wit.Stream.U8 / wit.Future.U8 |
stream<u32> / future<u32> |
wit.Stream.U32 / wit.Future.U32 |
stream<string> / future<string> |
wit.Stream.STRING / wit.Future.STRING |
stream<f64> / future<f64> |
wit.Stream.F64 / wit.Future.F64 |
All constructors return { readable, writable }.
Complex element types are also supported. The type constant is generated recursively from the WIT type structure:
// stream<result<string, u32>>
wit.Stream(wit.Stream.RESULT_STRING_U32);
// stream<option<u32>>
wit.Stream(wit.Stream.OPTION_U32);
// stream<tuple<u32, string>>
wit.Stream(wit.Stream.TUPLE_U32_STRING);
// Named record types use their WIT name:
// record point { x: f64, y: f64 }
// stream<point>
wit.Stream(wit.Stream.POINT);Use wit.Stream.types or wit.Future.types to discover all available type
constants at runtime.
StreamReadable methods:
| Method | Returns | Description |
|---|---|---|
read(count?) |
Promise<T[]> (or Uint8Array for u8) |
Read up to count values |
cancelRead() |
result or undefined |
Cancel an in-progress read |
drop() |
void |
Release the stream handle |
StreamWritable methods:
| Method | Returns | Description |
|---|---|---|
write(data) |
Promise<number> |
Write values, returns count written |
writeAll(data) |
Promise<number> |
Write all values, retrying as needed |
cancelWrite() |
result or undefined |
Cancel an in-progress write |
drop() |
void |
Release the stream handle |
Futures transfer a single value. They work like streams but carry exactly one value:
package example:async-value;
world async-value {
export compute: async func() -> future<string>;
}async function compute() {
const { readable, writable } = wit.Future();
// Write the value (fire-and-forget; completes when reader reads)
writable.write("computed result");
return readable;
}Future type constants follow the same pattern: wit.Future.U32,
wit.Future.STRING, etc.
FutureReadable methods:
| Method | Returns | Description |
|---|---|---|
read() |
Promise<T> |
Read the single value |
cancelRead() |
result or undefined |
Cancel an in-progress read |
drop() |
void |
Release the future handle |
FutureWritable methods:
| Method | Returns | Description |
|---|---|---|
write(value) |
Promise<boolean> |
Write the value, returns success |
cancelWrite() |
result or undefined |
Cancel an in-progress write |
drop() |
void |
Release the future handle |
Stream and future handles support
Explicit Resource Management
via Symbol.dispose. In environments that support using:
{
using stream = wit.Stream();
// stream.writable and stream.readable are auto-dropped when leaving scope
}Otherwise, call .drop() explicitly to release handles.
## Node.js API
The npm package exposes both a CLI and a programmatic API. It is not yet
published to the registry — see [Installation](#installation) for building from
source.
### CLI
```bash
./npm/bin/componentize-qjs --wit hello.wit --js hello.js -o hello.wasm
import { componentize } from "componentize-qjs";
const { component } = await componentize({
witPath: "hello.wit",
jsSource: "function greet(name) { return `Hello, ${name}!`; }",
});
// component is a Buffer containing the WebAssembly component bytesThis project builds on ideas and code from:
- ComponentizeJS by Joel Dice
- lua-component-demo by Alex Crichton
Licensed under Apache-2.0.