Skip to content
Merged
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
33 changes: 33 additions & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,39 @@
- [JavaScript ecosystem interoperability]()
- [`jco-std`](./interop/jco-std.md)
- [Node.js built-in compatibility](./interop/nodejs-builtins.md)
- [Supported modules](./interop/nodejs-builtins/supported-modules/index.md)
- [`node:assert`](./interop/nodejs-builtins/supported-modules/assert.md)
- [`node:async_hooks`](./interop/nodejs-builtins/supported-modules/async-hooks.md)
- [`node:buffer`](./interop/nodejs-builtins/supported-modules/buffer.md)
- [`node:child_process`](./interop/nodejs-builtins/supported-modules/child-process.md)
- [`node:cluster`](./interop/nodejs-builtins/supported-modules/cluster.md)
- [`node:console`](./interop/nodejs-builtins/supported-modules/console.md)
- [`node:diagnostics_channel`](./interop/nodejs-builtins/supported-modules/diagnostics-channel.md)
- [`node:dns`](./interop/nodejs-builtins/supported-modules/dns.md)
- [`node:domain`](./interop/nodejs-builtins/supported-modules/domain.md)
- [Errors](./interop/nodejs-builtins/supported-modules/errors.md)
- [`node:events`](./interop/nodejs-builtins/supported-modules/events.md)
- [`node:ffi` (Node.js v26+)](./interop/nodejs-builtins/supported-modules/ffi.md)
- [`node:fs`](./interop/nodejs-builtins/supported-modules/fs.md)
- [Globals](./interop/nodejs-builtins/supported-modules/globals.md)
- [`node:http`](./interop/nodejs-builtins/supported-modules/http.md)
- [`node:http2`](./interop/nodejs-builtins/supported-modules/http2.md)
- [`node:https`](./interop/nodejs-builtins/supported-modules/https.md)
- [`node:inspector`](./interop/nodejs-builtins/supported-modules/inspector.md)
- [`node:module`](./interop/nodejs-builtins/supported-modules/module.md)
- [`node:net`](./interop/nodejs-builtins/supported-modules/net.md)
- [`node:os`](./interop/nodejs-builtins/supported-modules/os.md)
- [`node:path`](./interop/nodejs-builtins/supported-modules/path.md)
- [`node:perf_hooks`](./interop/nodejs-builtins/supported-modules/perf-hooks.md)
- [`node:process`](./interop/nodejs-builtins/supported-modules/process.md)
- [`node:querystring`](./interop/nodejs-builtins/supported-modules/querystring.md)
- [`node:readline`](./interop/nodejs-builtins/supported-modules/readline.md)
- [`node:repl`](./interop/nodejs-builtins/supported-modules/repl.md)
- [`node:sqlite`](./interop/nodejs-builtins/supported-modules/sqlite.md)
- [`node:stream`](./interop/nodejs-builtins/supported-modules/stream.md)
- [`node:string_decoder`](./interop/nodejs-builtins/supported-modules/string-decoder.md)
- [`node:timers`](./interop/nodejs-builtins/supported-modules/timers.md)
- [`node:tty`](./interop/nodejs-builtins/supported-modules/tty.md)
- [Troubleshooting]()
- [Common issues](./troubleshooting/common-issues.md)
- [Contributor Guide]()
Expand Down
1,601 changes: 5 additions & 1,596 deletions docs/src/interop/nodejs-builtins.md

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions docs/src/interop/nodejs-builtins/supported-modules/assert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# `node:assert`

| Imports | Implementation |
| --- | --- |
| `node:assert`, `node:assert/strict` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/assert` |

Jco keeps its own assert implementation because the assertion namespace is a
coherent system: comparison semantics, callable/default/strict identities,
`AssertionError`, and error matching must work together (and can change across
versions).

The implementation covers Node 24's public module surface and comparison of
cycles, Maps, Sets, typed arrays, errors, symbols, and other built-in families.
The deprecated `CallTracker` API throws immediately. The deprecated multi-argument
form of `assert.fail()` also throws immediately, while its current zero- and
one-argument forms remain available.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# `node:async_hooks`

| Imports | Implementation |
| --- | --- |
| `node:async_hooks` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/async-hooks` |

`AsyncLocalStorage` works within a synchronous scope: `run`, `getStore`, `exit`, `enterWith`,
nesting, `snapshot` and `bind` all behave as Node does, and `AsyncResource` binds to the context it
was constructed in.

What it cannot do is carry a store across an asynchronous boundary. `await` resolves through the
engine's internal `PerformPromiseThen`, which JavaScript cannot intercept -- patching
`Promise.prototype.then` does not see it -- and StarlingMonkey exposes no TC39 `AsyncContext` to
carry the value instead.

Rather than return an empty store after an `await`, Jco refuses at the call site: any callback
given to `run`, `exit`, `withScope` or a snapshot that returns a promise throws
`ERR_JCO_UNSUPPORTED_NODE_API`, naming the reason. A failure at the call site is easier to act on
than a store that silently disappears somewhere else.

`createHook`, `executionAsyncId`, `triggerAsyncId` and `executionAsyncResource` describe the async
resource graph and always throw: nothing tracks that graph in a component.
31 changes: 31 additions & 0 deletions docs/src/interop/nodejs-builtins/supported-modules/buffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# `node:buffer`

| Imports | Implementation |
| --- | --- |
| `node:buffer` | unenv's portable Buffer core with a Jco public adapter |

The Buffer core comes from `unenv`'s wrapper around the MIT-licensed Feross
[`buffer`][feross-buffer] implementation. Jco adds the Node-facing module shape,
one shared `globalThis.Buffer`, and policy for exports that cannot be faithfully
provided in the guest.

Supported behavior includes common text and binary encodings, allocation and
filling, concatenation, comparison, integer and floating-point IO, searching,
slicing, copying, swapping, and JSON conversion. `atob()` and `btoa()` use runtime
globals when available and portable Buffer fallbacks otherwise.

## Behavioral limits

There are intentional limits:

- `Buffer()` and `new Buffer()` are deprecated in Node and throw
`ERR_JCO_UNSUPPORTED_DEPRECATED_NODE_API`; use `Buffer.from()`,
`Buffer.alloc()`, or `Buffer.allocUnsafe()` instead.
- `SlowBuffer` is deprecated and throws the same error.
- `isAscii`, `isUtf8`, `resolveObjectURL`, and `transcode` currently throw
`ERR_JCO_UNSUPPORTED_NODE_API`.
- `Blob` and `File` use engine globals when those globals exist; otherwise their
fallback constructors throw an unsupported-API error.
- The current portable core does not support the `base64url` encoding.

[feross-buffer]: https://github.com/feross/buffer
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# `node:child_process`

| Imports | Implementation |
| --- | --- |
| `node:child_process` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/child-process` |

A WebAssembly guest cannot spawn a process itself. When bundled source imports
`node:child_process`, Jco ensures that the selected world declares the dedicated
interface:

```wit
world app {
import jco:node/child-process@0.1.0;
// component imports and exports...
}
```

If the selected world does not already contain the import, Jco edits its `.wit`
file in place, adds a comment identifying the generated line, installs the
interface definition under `deps/jco-node-0.1.0`, and prints a CLI warning naming
the changed files. This makes the capability change visible in the application's
source control. `--world` is honored when a package defines multiple worlds, and
repeated componentization does not add duplicate imports or dependencies.

The interface definition also ships in `jco-std` under `wit/node-0.1.0`.
Declaring or generating the import does not grant host access: Jco's default
transpilation map uses a provider that throws
`ERR_JCO_CHILD_PROCESS_ADAPTER_REQUIRED`. An application must make the security
decision explicitly, for example by mapping the Node host provider:

```console
jco transpile component.wasm \
--map 'jco:node/child-process@0.1.0=@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/child-process/host/node'
```

That produces the call path guest `node:child_process` → WIT capability → host
adapter → Node `node:child_process`.

The current interface supports `spawnSync`, `execFileSync`, and `execSync`,
including buffered input/output, encoding, cwd, environment, shell, stdio,
timeout, signal, identity, and Windows options. `spawn`, callback-based `exec`
and `execFile`, `ChildProcess`, and `fork`/IPC are present but throw
`ERR_JCO_UNSUPPORTED_NODE_API`. A synchronous WIT function cannot faithfully
carry Node callbacks, lifecycle events, or interactive streams; those APIs stay
explicitly unavailable until the capability grows an asynchronous resource and
stream model.
52 changes: 52 additions & 0 deletions docs/src/interop/nodejs-builtins/supported-modules/cluster.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# `node:cluster`

| Imports | Implementation |
| --- | --- |
| `node:cluster` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/cluster` |

A guest has no process model, so `node:cluster` follows the same pattern as
`node:child_process`. When bundled source imports it, Jco ensures the selected world
declares the interface, editing the `.wit` file in place, installing the definition under
`deps/jco-node-0.1.0`, and printing a CLI warning naming the changed files:

```wit
world app {
import jco:node/cluster@0.1.0;
// component imports and exports...
}
```

Declaring or generating the import does not grant host access. Jco's default
transpilation map uses a provider that throws `ERR_JCO_CLUSTER_ADAPTER_REQUIRED`, so an
application must make the security decision explicitly, for example by mapping the Node
host provider:

```console
jco transpile component.wasm \
--map 'jco:node/cluster@0.1.0=@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/cluster/host/node'
```

That produces the call path guest `node:cluster` → WIT capability → host adapter → Node
`node:cluster`. Because a transpiled component is itself a Node process, `cluster.fork()`
re-executes the entry, so a forked worker runs the component again and observes itself as
a worker.

Two differences from Node are unavoidable:

- **Event timing.** Node delivers cluster events on its event loop. A guest cannot be
called back across the host boundary, so events are queued by the host and emitted
when the guest next touches the module; `cluster.pump()` drains them on demand.
- **Messages cross as JSON.** WIT has no dynamic value type, so values JSON cannot
represent -- functions, symbols, cycles, `BigInt` -- are rejected rather than
silently altered.

These throw `ERR_JCO_UNSUPPORTED_NODE_API` rather than failing quietly:

| API | Why |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `worker.process` | A `ChildProcess` handle cannot cross the component boundary. |
| `listening` event, handle sharing | Cluster distributes Node `net` handles; guest servers are `wasi:sockets`, so nothing hooks them. `SCHED_RR` is accepted but does not distribute guest connections. |
| `setupPrimary({ exec, execArgv, stdio, uid, gid, inspectPort, serialization })` | These configure the host runner executing the component, not a guest file. |

`cluster.isMaster` and `cluster.setupMaster()` are deprecated in Node, so they throw
`ERR_JCO_UNSUPPORTED_DEPRECATED_NODE_API` and point at `isPrimary`/`setupPrimary`.
28 changes: 28 additions & 0 deletions docs/src/interop/nodejs-builtins/supported-modules/console.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# `node:console`

| Imports | Implementation |
| --- | --- |
| `node:console` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/console` |

Writing to a console is a host capability, not a portable one: a component has no
stdout of its own. `node:console` therefore resolves against
`jco:node/console@0.1.0`, which an application must provide.

It is **denied by default**. Transpiling maps the capability to jco-std's deny
host unless told otherwise, and every call -- `write`, `isTerminal`, `colorDepth`
-- throws `ERR_JCO_CONSOLE_ADAPTER_REQUIRED`. That is deliberate: a component that
silently discarded its output would be harder to diagnose than one that says the
capability is missing.

To grant it, map the interface to a provider. jco-std ships one for Node, which
writes through to the real `process.stdout`/`process.stderr` and reports their TTY
status and color depth:

```console
jco transpile component.wasm \
--map 'jco:node/console@0.1.0=@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/console/host/node'
```

Formatting is done in the guest -- `Console`, the `log`/`warn`/`error` family,
`group` indentation, `count`, `time`, `table` and `dir` all run guest-side, and only
the finished string crosses the boundary.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# `node:diagnostics_channel`

| Imports | Implementation |
| --- | --- |
| `node:diagnostics_channel` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/diagnostics-channel` |

Publish/subscribe for instrumentation, entirely in-process, so it needs no WIT capability. Channels
are interned by name: a publisher and a subscriber that never share a reference still meet on the
same object.

`TracingChannel` is implemented in full -- `traceSync`, `tracePromise` and `traceCallback`, with
the `start`/`end`/`asyncStart`/`asyncEnd`/`error` sub-channels emitted in Node's order.

`Channel.bindStore` accepts anything offering `run(value, fn)`, which includes jco-std's
`AsyncLocalStorage`. Stores are therefore scoped synchronously: a bound store is visible while
subscribers run and does not follow an `await`. See [`node:async_hooks`](./async-hooks.md) for why.
31 changes: 31 additions & 0 deletions docs/src/interop/nodejs-builtins/supported-modules/dns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# `node:dns`

| Imports | Implementation |
| --- | --- |
| `node:dns`, `node:dns/promises` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/dns` |

`node:dns` and `node:dns/promises` share one guest implementation and the
`jco:node/dns@0.1.0` capability. Jco adds that import and its `dns.wit`
dependency when bundled source uses either specifier. The default provider throws
`ERR_JCO_DNS_ADAPTER_REQUIRED`; applications opt into Node name resolution with:

```console
jco transpile component.wasm \
--map 'jco:node/dns@0.1.0=@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/dns/host/node'
```

The WIT interface represents each DNS operation as a named, typed function; it
does not tunnel requests through a serialized dispatcher. The Node provider calls
the real asynchronous `node:dns/promises` operations directly. When an application
supplies a DNS host map, Jco automatically enables JSPI for every function in
`jco:node/dns@0.1.0`. The Preview 2 WIT calls therefore remain synchronous from the
guest's perspective without blocking Node's event loop or creating a worker for
each query. Because any component export may transitively call DNS, mapped
components expose promise-returning exports that JavaScript hosts must await.
Callback APIs retain callback delivery in the guest, and the promises subpath
shares server and default-result-order state with the main module.

`Resolver.cancel()` throws `ERR_JCO_UNSUPPORTED_NODE_API`. The synchronous WIT
boundary does not expose an outstanding c-ares request that a later guest call
could cancel. The provider boundary otherwise remains Node-independent, leaving
room for a future browser implementation.
17 changes: 17 additions & 0 deletions docs/src/interop/nodejs-builtins/supported-modules/domain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# `node:domain`

| Imports | Implementation |
| --- | --- |
| `node:domain` | _(refused)_ |

`node:domain` is Stability 0 -- deprecated in its entirety -- and Jco implements none of it. Its
purpose is routing errors across asynchronous boundaries, which a component cannot do in any case
(see [`node:async_hooks`](./async-hooks.md)).

It still resolves rather than failing as an unknown import, so the error names the reason and a way
forward instead of reading `Could not resolve 'node:domain'`. Importing is fine; every use --
`create()`, `createDomain()`, `new Domain()`, and reading `active` or `_stack` -- throws
`ERR_JCO_UNSUPPORTED_DEPRECATED_NODE_API`, pointing at `AsyncLocalStorage` for carrying context.

`active` and `_stack` are reachable on the default import only. An ES module binding cannot throw
on read, so `import { active } from "node:domain"` fails at build time instead.
21 changes: 21 additions & 0 deletions docs/src/interop/nodejs-builtins/supported-modules/errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Errors

The Node [Errors API](https://nodejs.org/docs/latest-v24.x/api/errors.html) is
cross-cutting behavior rather than a `node:errors` module. Standard error
constructors are globals, while individual Node APIs create coded and system
errors. Jco therefore does not resolve `node:errors`; Node 24 rejects that
specifier as well.

Bundled code can use `Error`, `AggregateError`, `DOMException`, `EvalError`,
`RangeError`, `ReferenceError`, `SuppressedError`, `SyntaxError`, `TypeError`, and
`URIError` without an import. Rolldown injects
`@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/errors` only for constructors
actually referenced by the source graph. A graph that uses none of them contains
none of the adapter after bundling.

The adapter preserves the guest engine's constructor identities, supplies
portable fallbacks for missing newer constructors and V8 Error extensions, and
provides the common coded/system-error core used by other jco-std Node shims. No
WIT capability is required. Error classes, codes, and documented system fields
are compatibility targets; exact stack frames and source positions remain
engine-specific.
Loading
Loading