Introduction

jco is a fully native tool for working with WebAssembly Components in JavaScript.

Features

  • Transpiling Wasm Component binaries into ECMAScript modules that can run in any JavaScript environment.
  • WASI Preview2 support in Node.js & browsers (experimental).
  • Component builds of Wasm Tools helpers, available for use as a library or CLI commands for use in native JS environments
  • Optimization helper for Components via Binaryen.
  • componentize command to easily create components written in JavaScript (wrapper of ComponentizeJS).

Note: This is an experimental project. No guarantees are provided for stability, security or support and breaking changes may be made without notice.

Contributing

To contribute to the codebase of the project, refer to the Contributor guide.

To contribute to the documentation, refer to the Contributor guide.

If you find a mistake, omission, ambiguity, or other problem, please let us know via GitHub issues.

Transpiling

Components can be transpiled in two separate modes:

When using the default direct ESM transpilation mode, the output file is a JavaScript module, which imports the component imports, and exports the component exports.

Instantiation mode allows dynamically providing the imports for the component instantiation, as well as for instantiating a component multiple times.

For the default output, you will likely want to ensure there is a package.json file with a { "type": "module" } set for Node.js ES module support (although this is not needed for browser module loading or JS build tooling).

Export Conventions

Components can represent both bundles of modules and individual modules. Compponents export the direct export interface as well as the canonical named interface for the implementation to represent both of these cases.

For example a component that imports an interface will be output as:

export { interface, interface as 'my:pkg/interface@version' }

The exact version allows for disambiguation when a component exports multiple interfaces with the same name but different versions.

If not needing this disambiguation feature, and since support for string exports in JS can be limited, this feature can be disabled with the --no-namespaced-exports flag to instead output only:

export { interface }

Import Conventions

When using the ESM integration default transpilation output bindings are output directly in the registry:name/interface form, but with versions removed.

For example an import to my:pkg/interface@1.2.3 will become an import to import { fn } from 'my:pkg/interface';.

Map Configuration

To customize the import specifiers used in JS, a --map configuration can be provided to the transpilation operation to convert the imports.

For example, jco transpile component.wasm --map my:pkg/interface@1.2.3=./myinterface.js will instead output import { fn } from './myinterface.js'.

Where the file myinterface.js would contain the function that is being imported from the interface:

export function fn () {
  // .. function implementation ..
}

Map configuration also supports # targets, which means that the interface can be read off of a nested JS object export.

For example with a JS file written:

export const interface = {
  fn () {
    // exported function to be imported from my:pkg/interface
  }
}

We can map the interface directly to this object instead of the entire module using the map configuration:

jco transpile component.wasm --map my:pkg/interface@1.2.3=./mypkg.js#interface

This way a single JS file can define multiple interfaces together.

Furthermore, wildcard mappings are also supported so that using (and quoting for bash compatibility):

jco transpile component.wasm --map 'my:pkg/*@1.2.3=./mypkg.js#*'

we can map all interfaces into a single JS file reading them off of exported objects for those interfaces.

WASI Shims

WASI is given special treatment and is automatically mapped to the @bytecodealliance/preview2-shim npm package, with interfaces imported off of the relevant subsystem.

Using the above rules, this is effectively provided by the default map configuration which is always automatically provided:

jco transpile component.wasm --map wasi:cli/*@0.2.0=@bytecodealliance/preview2-shim/cli#*

For all subsystems - cli, clocks, filesystem, http, io, random and sockets.

To disable this automatic WASI handling the --no-wasi-shim flag can be provided and WASI will be treated like any other import without special handling.

Interface Implementation Example

Here's an example of implementing a custom WIT interface in JavaScript:

example.wit

package test:pkg;
interface interface-types {
  type some-type = list<u32>;
  record some-record {
    some-field: some-type
  }
}
interface iface {
  use interface-types.{some-record};
  interface-fn: func(%record: some-record) -> result<string, string>;
}
world myworld {
  import iface;
  export test: func() -> string;
}

When transpiling, we can use the map rules as described in the previous section to implement all interfaces from a single JS file.

Given a component compiled for this world, we could transpile it, but given this is only an example, we can use the --stub feature of transpile to inspect the bindings:

jco transpile example.wit --stub -o output --map 'test:pkg/*=./imports.js#*'

The output/example.js file contains the generated bindgen:

import { iface } from './imports.js';
const { interfaceFn } = iface;

// ... bindings ...

function test () {
  // ...
}

export { test }

Therefore, we can implement this mapping of the world with the following JS file:

imports.js

export const iface = {
  interfaceFn (record) {
    return 'string';
  }
};

Note: Top-level results are turned into JS exceptions, all other results are treated as tagged objects { tag: 'ok' | 'err', val }.

WASI Proposals

Jco will always take PRs to support all open WASI proposals.

These PRs can be implemented by extending the default map configuration provided by Jco to support the new --map wasi:subsytem/*=shimpkg/subsystem#* for the WASI subsystem being implemented.

shimpkg in the above refers to a published npm package implementation to install per JS ecosystem conventions. This way, polyfill packages can be published to npm.

Upstreaming into the @bytecodealliance/preview2-shim package is also possible for WASI proposals that have progressed to Phase 1 in the WASI proposal stage process.

Instantiation

Instantiation output is enabled via jco transpile component.wasm --instantiation sync|async.

When using instantiation mode, the output is a JS module with a single instantiate() function.

For async instantiation, the instantiate function takes the following signature:

export async function instantiate(
  getCoreModule: (path: string) => Promise<WebAssembly.Module>,
  imports: {
    [importName: string]: any
  },
  instantiateCore?: (module: WebAssembly.Module, imports: Record<string, any>) => Promise<WebAssembly.Instance>
): Promise<{ [exportName: string]: any }>;

imports allows customizing the imports provided for instantiation.

instantiateCore defaults to WebAssembly.instantiate.

getCoreModule can typically be implemented as:

export async function getCoreModule(path: string) {
  return await WebAssembly.compile(await readFile(new URL(`./${path}`, import.meta.url)));
}

For synchronous instantiation, the instantiate function has the following signature:

export function instantiate(
  getCoreModule: (path: string) => WebAssembly.Module,
  imports: {
    [importName: string]: any
  },
  instantiateCore?: (module: WebAssembly.Module, imports: Record<string, any>) => WebAssembly.Instance
): Promise<{ [exportName: string]: any }>;

Where instead of promises, all functions are synchronous.

Jco Example Workflow

Given an existing Wasm Component, jco provides the tooling necessary to work with this Component fully natively in JS.

Jco also provides an experimental feature for generating components from JavaScript by wrapping ComponentizeJS in the jco componentize command.

To demonstrate a full end-to-end component, we can create a JavaScript component embedding Spidermnokey then run it in JavaScript.

Installing Jco

Either install Jco globally:

$ npm install -g @bytecodealliance/jco
$ jco --version
1.0.3

Or install it locally and use npx to run it:

$ npm install @bytecodealliance/jco
$ npx jco --version
1.0.3

Local usage can be preferable to ensure the project is reproducible and self-contained, but requires replacing all jco shell calls in the following example with either ./node_modules/.bin/jco or npx jco.

Installing ComponentizeJS

To use ComponentizeJS, it must be separately installed, globally or locally depending on whether Jco was installed globally or locally. Globally:

$ npm install -g @bytecodealliance/componentize-js

Or locally:

$ npm install @bytecodealliance/componentize-js

Now the jco componentize command will be ready to use.

Creating a Component with ComponentizeJS

This Cowsay component uses the following WIT file (WIT is the typing language used for defining Components):

cowsay.wit

package local:cowsay;
world cowsay {
  export cow: interface {
    enum cows {
      default,
      owl
    }
    say: func(text: string, cow: option<cows>) -> string;
  }
}

We can implement this with the following JS:

cowsay.js

export const cow = {
  say (text, cow = 'default') {
    switch (cow) {
      case 'default':
return `${text}
  \\   ^__^
    \\  (oo)\\______
      (__)\\      )\/\\
          ||----w |
          ||     ||
`;
      case 'owl':
return `${text}
   ___
  (o o)
 (  V  )
/--m-m-
`;
    }
  }
};

To turn this into a component run:

$ jco componentize cowsay.js --wit cowsay.wit -o cowsay.wasm

OK Successfully written cowsay.wasm with imports ().

Note: For debugging, it is useful to pass --enable-stdout to ComponentizeJS to get error messages and enable console.log.

Inspecting Component WIT

As a first step, we might like to look instead this binary black box of a Component and see what it actually does.

$ jco wit cowsay.wasm
package root:component;

world root {
  export cow: interface {
    enum cows {
      default,
      owl,
    }

    say: func(text: string, cow: option<cows>) -> string;
  }
}

Transpiling to JS

To execute the Component in a JS environment, use the jco transpile command to generate the JS for the Component:

$ jco transpile cowsay.wasm -o cowsay

Transpiled JS Component Files:

 - cowsay/cowsay.core.wasm     7.61 MiB
 - cowsay/cowsay.d.ts          0.07 KiB
 - cowsay/cowsay.js            2.62 KiB
 - cowsay/interfaces/cow.d.ts  0.21 KiB

Now the Component can be directly imported and used as an ES module:

test.js

import { cow } from './cowsay/cowsay.js';

console.log(cow.say('Hello Wasm Components!'));

For Node.js to allow us to run native ES modules, we must first create or edit the local package.json file to include a "type": "module" field:

package.json

{
  "type": "module"
}

The above JavaScript can now be executed in Node.js:

$ node test.js

 Hello Wasm Components!
  \   ^__^
    \  (oo)\______
      (__)\      )/\
          ||----w |
          ||     ||

Passing in the optional second parameter, we can change the cow:

test.js

import { cow } from './cowsay/cowsay.js';

console.log(cow.say('Hello Wasm Components!', 'owl'));
$ node test.js

 Hello Wasm Components!
   ___
  (o o)
 (  V  )
/--m-m-

It can also be executed in a browser via a module script:

<script type="module" src="test.js"></script>

There are a number of custom transpilation options available, detailed in the API section.

Contributor Guide

Contributing to the Codebase

Development is based on a standard npm install && npm run build && npm run test workflow.

Tests can be run without bundling via npm run build:dev && npm run test:dev.

Specific tests can be run adding the mocha --grep / -g flag, for example: npm run test:dev -- --grep exports_only.

Prerequisites

Required prerequisites for building jco include:

  • Latest stable Rust with the wasm32-wasi target
  • Node.js 18+ & npm (https://nodejs.org/en)

Rust Toolchain

The latest Rust stable toolchain can be installed using rustup.

Specifically:

rustup toolchain install stable
rustup target add wasm32-wasi

In case you do not have rustup installed on your system, please follow the installation instructions on the official Rust website based on your operating system

Project Structure

jco is effectively a monorepo consisting of the following projects:

  • crates/js-component-bindgen: Rust crate for creating JS component bindgen, published under https://crates.io/crates/js-component-bindgen.
  • crates/js-component-bindgen-component: Component wrapper crate for the component bindgen. This allows bindgen to be self-hosted in JS.
  • crates/wasm-tools-component: Component wrapper crate for wasm-tools, allowing jco to invoke various Wasm toolchain functionality and also make it available through the jco API.
  • src/api.js: The jco API which can be used as a library dependency on npm. Published as https://npmjs.org/package/@bytecodealliance/jco.
  • src/jco.js: The jco CLI. Published as https://npmjs.org/package/@bytecodealliance/jco.
  • packages/preview2-shim: The WASI Preview2 host implementations for Node.js & browsers. Published as https://www.npmjs.com/package/@bytecodealliance/preview2-shim.

Building

To build jco, run:

npm install
npm run build

Testing

There are three test suites in jco:

  • npm run test: Project-level transpilation, CLI & API tests.
  • npm run test --workspace packages/preview2-shim: preview2-shim unit tests.
  • test/browser.html: Bare-minimum browser validation test.
  • cargo test: Wasmtime preview2 conformance tests (not currently passing).

jco is a Bytecode Alliance project and follows the Bytecode Alliance's Code of Conduct and Organizational Code of Conduct.

Using this repository

You can run the website locally using the mdBook command line tool.

Prerequisites

To use this repository, you need mdBook installed on your workstation.

Running the website locally

After installing mdBook, on GitHub, click the Fork button in the upper-right area of the screen to create a copy of the jco repository in your account. This copy is called a fork.

Next, clone it locally by executing the command below.

git clone https://github.com/bytecodealliance/jco/
cd docs

To build and test the site locally, run:

mdbook serve --open

Submitting Changes

  • Follow the instructions above to make changes to your website locally.
  • When you are ready to submit those changes, go to your fork and create a new pull request to let us know about it.

Everyone is welcome to submit a pull request! Once your pull request is created, we'll try to get to reviewing it or responding to it in at most a few days. As the owner of the pull request, it is your responsibility to modify your pull request to address the feedback that has been provided to you by the reviewer.