Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
## Features

- [Transpiling](./transpiling.md) Wasm Component binaries into [ECMAScript modules](https://nodejs.org/api/esm.html#modules-ecmascript-modules) that can run in any JavaScript environment.
- WASI Preview2 support in Node.js & browsers (experimental).
- WASI Preview2 support in Node.js & browsers.
- 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](https://github.com/bytecodealliance/ComponentizeJS)).
Expand Down
3 changes: 2 additions & 1 deletion docs/src/transpiling.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ For all subsystems - `cli`, `clocks`, `filesystem`, `http`, `io`, `random` and `

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.

Note that browser support for WASI is currently experimental.
Browser WASI support is subject to web-platform capability limitations; some interfaces require
application-provided adapters.

### Interface Implementation Example

Expand Down
2 changes: 1 addition & 1 deletion examples/components/fs-write-file/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pnpm install
At this point, since this project is *just* NodeJS, you could use the module from any NodeJS project or browser project where appropriate.

That said, we'll be focusing on building the JS code we've written so far into a WebAssembly binary, which can run *anywhere*
WebAssembly runtimes are supported, including in other languages, and the browser (experimental support).
WebAssembly runtimes are supported, including in other languages and the browser.

## Building the WebAssembly component

Expand Down
3 changes: 0 additions & 3 deletions examples/transpile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ This folder contains examples of how to use `@bytecodealliance/jco-transpile` di
converting a WebAssembly component into a Javascript ES module that can be run from JS
runtimes like [NodeJS][nodejs] and the browser.

> [!WARNING]
> Browser support is still experimental

Most (if not all) individual example projects are standard Javascript projects, and since we are focused on
transpiling existing components, they may contain a pre-built WebAssembly binary that is transpiled.

Expand Down
3 changes: 0 additions & 3 deletions packages/jco-std/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ an evolving architecture for interoperabl WebAssembly libraries, aplications and
WebAssembly components can be used from server side applications _and_ in the browser, and
`@bytecodealliance/jco-std` contains shared functionality and helpers for both environments.

> [!WARNING]
> Browser support is considered experimental, and not currently suitable for production applications.

[cm-book]: https://component-model.bytecodealliance.org/
[jco]: https://www.npmjs.com/package/@bytecodealliance/jco

Expand Down
5 changes: 1 addition & 4 deletions packages/jco-transpile/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
# `@bytecodealliance/jco-transpile`

This [`@bytecodealliance/jco`][jco] sub-project enables transpilation of [WebAssembly Components][cm-book] into ES modules
that can be run in Javascript environments like NodeJS and the browser (experimental).
that can be run in Javascript environments like NodeJS and the browser.

`@bytecodealliance/jco-transpile` is used primarily when only transpilation functionality of `jco` is needed,
and `jco` derives it's use of transpilation from this library.

> [!WARNING]
> Browser support is considered experimental, and not currently suitable for production applications.

[cm-book]: https://component-model.bytecodealliance.org/
[jco]: https://www.npmjs.com/package/@bytecodealliance/jco

Expand Down
2 changes: 1 addition & 1 deletion packages/jco/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Jco provides a fully native JS toolchain for working with [WebAssembly Component
Features include:

- "Transpiling" Wasm Component binaries into ES modules that can run in any JS environment.
- WASI Preview2 support in Node.js & browsers (experimental).
- WASI Preview2 support in Node.js & browsers.
- Component builds of [Wasm Tools](https://github.com/bytecodealliance/wasm-tools) helpers, available for use as a library or CLI commands for use in native JS environments, as well as optimization helper for Components via Binaryen.
- Run and serve commands like Wasmtime, as JS implementations of the Command and HTTP Proxy worlds.
- "Componentize" command to easily create components written in JavaScript (wrapper of [ComponentizeJS](https://github.com/bytecodealliance/ComponentizeJS)).
Expand Down
77 changes: 71 additions & 6 deletions packages/preview2-shim/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,74 @@ WASI Preview2 implementations for Node.js & browsers.

Node.js support is fully tested and conformant against the Wasmtime test suite.

Browser support is considered experimental, and not currently suitable for production applications.
Browser support is available with the platform limitations documented below.

The Node.js implementation owns its worker artifact. Direct package use and supported downstream
bundlers should resolve it through the public shim imports; applications do not need to import or
copy files from `dist/io`.

## Browser support matrix

Browser defaults are capability-safe: clocks and secure randomness use Web APIs, stdout and stderr
write to the console, stdin is closed, outbound HTTP uses `fetch`, filesystem preopens must be
configured explicitly, and raw sockets are unavailable unless an embedding supplies an adapter.

| WASI area | Browser status | Default capability |
| ----------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------- |
| CLI environment and arguments | Configurable per `WASIShim`; compatibility setters are global | Empty snapshots unless configured |
| CLI stdin | Adapter-backed | Closed stream |
| CLI stdout and stderr | Web API | Console-backed, preserving split UTF-8 writes until flush/newline |
| CLI terminals | Adapter-backed | No terminal resource |
| Clocks | Web API | `performance.now`, `Date.now`, and timer-backed pollables |
| Random | Web API | `crypto.getRandomValues`, including requests larger than 64 KiB |
| I/O streams and poll | Implemented browser resources | Non-blocking streams depend on their injected handlers |
| Filesystem | Adapter-backed; opt-in in-memory compatibility implementation | No persistent storage is selected implicitly |
| Outbound HTTP | Web API | Delegates to `fetch` |
| Incoming HTTP | Host adapter required | Browsers cannot listen for arbitrary inbound HTTP |
| TCP, UDP, and DNS | Host adapter required | Raw sockets are not exposed by standard browsers |
| `WASIShim` instantiation | Implemented | Interface namespaces can be overridden per instance |

An operation is not considered supported merely because its interface shape exists. Adapter-backed
rows require the embedding application to provide that capability; unavailable operations fail with
a WASI-domain error instead of logging or returning a placeholder resource.

Browser applications select storage explicitly. The bundled file-data adapter is ephemeral and must
be opted into:

```js
import { filesystem } from "@bytecodealliance/preview2-shim";
import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";

const shim = new WASIShim({
environment: { MODE: "browser" },
arguments: ["component"],
stdout: { write: (bytes) => terminal.write(bytes) },
browserFilesystem: {
adapter: new filesystem.InMemoryFilesystemAdapter(),
preopens: { "/data": { dir: {} } },
},
sandbox: { enableNetwork: false },
});
```

The browser shim does not request File System Access permissions or choose IndexedDB/OPFS on an
application's behalf. Applications that need another storage model implement the generated
`wasi:filesystem/types` and `wasi:filesystem/preopens` namespaces and inject them through the
`filesystem` option:

```js
const shim = new WASIShim({
filesystem: {
types: applicationFilesystemTypes,
preopens: applicationFilesystemPreopens,
},
});
```

This keeps permission prompts, handle acquisition, persistence, and synchronization policy in
application code. Raw TCP, UDP, and DNS are denied by default; outbound HTTP remains a separate
`fetch` capability.

# Features

## WASI Shim object for easy instantiation
Expand Down Expand Up @@ -57,9 +119,9 @@ const component = await instantiate(loader, new WASIShim().getImportObject());

## Sandboxing

By default, the preview2-shim provides full access to the host filesystem, environment variables,
and network - matching the default behavior of Node.js libraries. However, you can configure
sandboxing to restrict what guests can access.
On Node.js, the preview2-shim provides host filesystem, environment, and network access by default,
matching the usual behavior of Node.js libraries. Browser defaults expose no filesystem preopens or
raw sockets. Both platforms can configure which capabilities a guest receives.

### Using WASIShim for sandboxing

Expand All @@ -78,7 +140,7 @@ const sandboxedShim = new WASIShim({
},
});

// Limited filesystem access - map virtual paths to host paths
// Node.js only: map virtual paths to host paths
const limitedShim = new WASIShim({
sandbox: {
preopens: {
Expand All @@ -95,7 +157,10 @@ const component = await instantiate(loader, sandboxedShim.getImportObject());
### Notes on sandboxing

- By default (when no options are passed), the shim is providing full access to match typical
Node.js library behavior.
Node.js library behavior. In browsers, filesystem preopens remain empty until the application
explicitly injects filesystem namespaces or selects the ephemeral file-data adapter.
- `sandbox.preopens` maps guest paths to Node.js host paths. Browser applications use the
`filesystem` or `browserFilesystem` options shown above; host paths are rejected in browsers.
- Each `WASIShim` instance has its own isolated preopens, environment variables, and arguments.
Multiple instances with different configurations will not affect each other.
- The direct preopen functions (`_setPreopens`, `_clearPreopens`, etc.) modify global state and
Expand Down
134 changes: 98 additions & 36 deletions packages/preview2-shim/src/browser/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
environment as EnvironmentNamespace,
exit as ExitNamespace,
stderr as StderrNamespace,
stdin as StdinNamespace,
Expand Down Expand Up @@ -52,45 +53,61 @@ export function _setStdout(handler: OutputStreamHandler): void {
stdoutStream.handler = handler;
}

export interface BrowserCliConfig {
environment?: Record<string, string>;
arguments?: string[];
initialCwd?: string;
stdin?: InputStreamHandler;
stdout?: OutputStreamHandler;
stderr?: OutputStreamHandler;
}

const stdinStream = inputStreamCreate({
blockingRead(_len: bigint) {
// TODO
return new Uint8Array(0);
blockingRead() {
throw { tag: "closed" };
},
subscribe() {
// TODO
return pollableCreate();
},
[symbolDispose]() {
// TODO
},
[symbolDispose]() {},
});

const textDecoder = new TextDecoder();
function consoleStream(writeLine: (line: string) => void): OutputStreamHandler {
const decoder = new TextDecoder();
let pending = "";

const stdoutStream = outputStreamCreate({
write(contents: Uint8Array): void {
if (contents.at(-1) == 10) {
// console.log already appends a new line
contents = contents.subarray(0, -1);
const emitCompleteLines = () => {
const lines = pending.split("\n");
pending = lines.pop()!;
for (const line of lines) {
writeLine(line.endsWith("\r") ? line.slice(0, -1) : line);
}
console.log(textDecoder.decode(contents));
},
blockingFlush() {},
[symbolDispose]() {},
});
};

return {
write(contents: Uint8Array) {
pending += decoder.decode(contents, { stream: true });
emitCompleteLines();
},
flush() {
pending += decoder.decode();
if (pending) {
writeLine(pending);
}
pending = "";
},
blockingFlush() {
this.flush?.();
},
drop() {
this.flush?.();
},
};
}

const stderrStream = outputStreamCreate({
write(contents: Uint8Array): void {
if (contents.at(-1) == 10) {
// console.error already appends a new line
contents = contents.subarray(0, -1);
}
console.error(textDecoder.decode(contents));
},
blockingFlush() {},
[symbolDispose]() {},
});
const stdoutStream = outputStreamCreate(consoleStream((line) => console.log(line)));

const stderrStream = outputStreamCreate(consoleStream((line) => console.error(line)));

export const stdin: typeof StdinNamespace = {
getStdin() {
Expand All @@ -113,10 +130,6 @@ export const stderr: typeof StderrNamespace = {
class TerminalInput implements TerminalInputNamespace.TerminalInput {}
class TerminalOutput implements TerminalOutputNamespace.TerminalOutput {}

const terminalStdoutInstance = new TerminalOutput();
const terminalStderrInstance = new TerminalOutput();
const terminalStdinInstance = new TerminalInput();

export const terminalInput: typeof TerminalInputNamespace = {
TerminalInput,
};
Expand All @@ -127,18 +140,67 @@ export const terminalOutput: typeof TerminalOutputNamespace = {

export const terminalStderr: typeof TerminalStderrNamespace = {
getTerminalStderr() {
return terminalStderrInstance;
return undefined;
},
};

export const terminalStdin: typeof TerminalStdinNamespace = {
getTerminalStdin() {
return terminalStdinInstance;
return undefined;
},
};

export const terminalStdout: typeof TerminalStdoutNamespace = {
getTerminalStdout() {
return terminalStdoutInstance;
return undefined;
},
};

/** Create isolated browser CLI interfaces without changing compatibility globals. */
export function createCli(config: BrowserCliConfig = {}): {
environment: typeof EnvironmentNamespace;
exit: typeof ExitNamespace;
stdin: typeof StdinNamespace;
stdout: typeof StdoutNamespace;
stderr: typeof StderrNamespace;
terminalInput: typeof TerminalInputNamespace;
terminalOutput: typeof TerminalOutputNamespace;
terminalStdin: typeof TerminalStdinNamespace;
terminalStdout: typeof TerminalStdoutNamespace;
terminalStderr: typeof TerminalStderrNamespace;
} {
const stdinInstance = inputStreamCreate(
config.stdin ?? {
blockingRead() {
throw { tag: "closed" };
},
subscribe: () => pollableCreate(),
},
);
const stdoutInstance = outputStreamCreate(
config.stdout ?? consoleStream((line) => console.log(line)),
);
const stderrInstance = outputStreamCreate(
config.stderr ?? consoleStream((line) => console.error(line)),
);
const env = Object.entries(config.environment ?? {});
const args = [...(config.arguments ?? [])];
const cwd = config.initialCwd ?? "/";

return {
environment: {
getEnvironment: () => env.map(([key, value]) => [key, value] as [string, string]),
getArguments: () => [...args],
initialCwd: () => cwd,
},
exit,
stdin: { getStdin: () => stdinInstance },
stdout: { getStdout: () => stdoutInstance },
stderr: { getStderr: () => stderrInstance },
terminalInput,
terminalOutput,
terminalStdin,
terminalStdout,
terminalStderr,
};
}
Loading
Loading