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
5 changes: 4 additions & 1 deletion docs/src/interop/jco-std.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,10 @@ used by the Hono adapter; assert and Buffer do not add further capabilities.
with no additional WIT capability;
- `node:repl`, ported from Node 24.20 over that readline port for global-scope
evaluation, keyword commands, completion and top-level `await`, with no
additional WIT capability and acorn bundled only when the REPL is imported; and
additional WIT capability and acorn bundled only when the REPL is imported;
- `node:tty`, ported from Node 24.20 over the explicit `jco:node/tty@0.1.0`
capability, which addresses the host process's terminals by descriptor and is
denied by default; and
- `node:stream/consumers`, implemented as portable iterable collection over the
engine's Blob, typed-array, and text-codec globals; and
- the experimental Node 24.20 `node:stream/iter` API, including portable sources,
Expand Down
56 changes: 55 additions & 1 deletion docs/src/interop/nodejs-builtins.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ is planned.
| `node:process` | Jco typed facade and explicit Node passthrough | Host process state and operations over `jco:node/process@0.1.0`; lazy default properties, named functions and objects. See Process restrictions below. |
| `node:sqlite` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/sqlite` | SQLite over an explicit typed host capability; denied by default. Synchronous SQL callbacks are unsupported -- see below. |
| `node:os` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/os` | Machine and user information over an explicit host capability; denied by default. Static POSIX constants resolve without a provider -- see below. |
| `node:tty` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/tty` | Node 24.20 `isatty`, `ReadStream` and `WriteStream` over the host process's descriptors through an explicit host capability; denied by default -- see below. |
| `node:buffer` | unenv's portable Buffer core with a Jco public adapter | Covers the commonly used modern Buffer operations. Jco controls deprecated and runtime-dependent exports. |
| `node:events` | unenv's EventEmitter with a Jco layer from `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/events` | Covers the complete Node 24 module surface, including the `on()` async iterator and `EventEmitterAsyncResource`. Requires no WIT capability. |
| `node:querystring` | unenv's Node-derived querystring implementation | Covers the complete Node 24 module surface and shares the audited Buffer core used by `node:buffer`. |
Expand Down Expand Up @@ -713,6 +714,59 @@ The interface supports `arch`, `availableParallelism`, `cpus`, `endianness`,
`type`, `uptime`, `userInfo`, and `version`, with Node's argument validation and
`ERR_SYSTEM_ERROR` reconstruction for failing calls.

### Terminals and host capabilities

A component has no terminal of its own, and WASI 0.2 can only say whether its
standard streams are terminals. `node:tty` therefore resolves against
`jco:node/tty@0.1.0`, which addresses the embedding process's descriptors as Node
does: `isatty(fd)`, a handle per descriptor and direction, raw mode, the window
size, blocking reads, writes, and the environment used for color detection. When
bundled source imports `node:tty`, Jco adds the import to the selected world and
installs `tty.wit` and the shared `types.wit` under `deps/jco-node-0.1.0`.

It is **denied by default**: every operation, including `isatty()` on an in-range
descriptor, throws `ERR_JCO_TTY_ADAPTER_REQUIRED` until the application maps a
provider. jco-std ships one for Node:

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

With it, `new tty.WriteStream(1)` is the embedding process's standard output when
that is a terminal, and fails with Node's own `ERR_TTY_INIT_FAILED` (with `errno`,
`syscall` and `info`) when it is not. `process.stdin` and `process.stdout` remain
unsupported on the process facade, so applications construct the streams from
descriptors explicitly. Because `readline` and the REPL default `terminal` to
`output.isTTY`, these streams are what makes an interactive session work:

```js
import { ReadStream, WriteStream } from "node:tty";
import repl from "node:repl";

export function start() {
const input = new ReadStream(0);
const output = new WriteStream(1);
repl.start({ prompt: "app> ", input, output, useGlobal: true });
input.resume();
}
```

The port follows [Node v24.20.0's `lib/tty.js`](https://github.com/nodejs/node/blob/v24.20.0/lib/tty.js)
and `lib/internal/tty.js`. The pinned unenv tty module answers `isatty() === false`
and writes through `console.log`; it is not used.

#### Boundaries

| Surface | Behavior |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Prototype chain | `WriteStream → Duplex → Readable → Stream → EventEmitter` with an internal terminal stream standing in for `net.Socket`; `instanceof net.Socket` is false without `wasi:sockets`. |
| Stream sides | A `ReadStream` is not writable and a `WriteStream` is not readable; Node's sockets open the descriptor read-write. |
| Reading | Blocks the component. A flowing `ReadStream` pulls one chunk per read and emits `'data'` synchronously between pulls; `pause()` stops after the current chunk. Nothing else runs while the terminal is idle. |
| `'resize'` | Emitted only by `_refreshSize()`; a component receives no `SIGWINCH`. |
| `getColorDepth()` / `hasColors()` | Exact port. The environment defaults to the provider's; the Windows branch answers the 16-color floor because the build-number probe needs `node:os`. |
| Standard descriptors on the Node host | Kept open when a stream is destroyed, as Node keeps its own stdio; descriptors above 2 are closed with the stream. Raw mode is restored when the last read handle goes. |

### Async hooks and synchronous scopes

`AsyncLocalStorage` works within a synchronous scope: `run`, `getStore`, `exit`, `enterWith`,
Expand Down Expand Up @@ -1543,7 +1597,7 @@ the module or upstream project.
| Modules | Why they are not enabled yet |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `node:timers/promises` | A component-aware timer/event-loop integration is needed for delays, cancellation, and abort signals. |
| `node:trace_events`, `node:tty` | The fallbacks preserve useful shapes, but tracing and terminal detection are synthetic or no-op without runtime integration. |
| `node:trace_events` | The fallbacks preserve useful shapes, but tracing is synthetic or no-op without runtime integration. |
| `node:url` | There is substantial Node-derived code, but its eager `node:path` dependency adds a WASI environment requirement even for global-only URL use, and its namespace combines modern and legacy APIs that need separate policy. |

### Host-backed or broad subsystems
Expand Down
44 changes: 44 additions & 0 deletions packages/jco-std/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ build NodeJS programs as components.
| `wasi/0.2.x/node/24.x.x/stream/consumers` | Portable `node:stream/consumers`, Node 24 |
| `wasi/0.2.x/node/24.x.x/stream/iter` | Experimental iterable streams from Node 24.20 |
| `wasi/0.2.x/node/24.x.x/repl` | `node:repl` over the readline port; global-scope evaluation only |
| `wasi/0.2.x/node/24.x.x/tty` | `node:tty` guest adapter, Node 24 over an explicit host capability |
| `wasi/0.2.x/node/24.x.x/tty/host` | Deny-by-default host for `jco:node/tty` |
| `wasi/0.2.x/node/24.x.x/tty/host/node` | Opt-in host over the runtime's real `node:tty` and its descriptors |
| `wasi/0.2.x/node/24.x.x/child-process/host` | Deny-by-default host for `jco:node/child-process` |
| `wasi/0.2.x/node/24.x.x/child-process/host/node` | Opt-in host over the runtime's real `node:child_process` |
| `wasi/0.2.x/node/24.x.x/cluster/host` | Deny-by-default host for `jco:node/cluster` |
Expand Down Expand Up @@ -176,6 +179,10 @@ Jco can bundle the following Node.js APIs into JavaScript WebAssembly components
`@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/repl` over the readline port.
Evaluation is global-scope only (`useGlobal: true`); the module needs no WIT
capability and is the only jco-std module that bundles `acorn`;
- `node:tty`, implemented by
`@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/tty` and the
application-provided `jco:node/tty@0.1.0` capability, giving readline and the
REPL real terminal streams;
- `node:module`, implemented by
`@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/module`. Classification,
source maps and `require.resolve` are exact; everything that loads throws,
Expand Down Expand Up @@ -331,6 +338,43 @@ between SpiderMonkey and QuickJS), rewriting top-level `await`, and locating the
expression to tab-complete. A component that does not import `node:repl` does
not carry acorn.

### Terminals

The versioned tty module ports Node 24.20.0's `node:tty`: `isatty()`, `ReadStream`
with `setRawMode()`, and `WriteStream` with `columns`/`rows`, `getWindowSize()`,
`getColorDepth()`, `hasColors()`, the cursor helpers and `'resize'`. A component
has no terminal of its own, so the module is host-backed: the streams address the
embedding process's descriptors through `jco:node/tty@0.1.0`, which is denied by
default and mapped explicitly at transpile time.

```js
import { ReadStream, WriteStream, isatty } from "node:tty";
import { createInterface } from "node:readline";

export function ask() {
if (!isatty(0) || !isatty(1)) {
throw new Error("an interactive terminal is required");
}
const rl = createInterface({ input: new ReadStream(0), output: new WriteStream(1) });
rl.question("Name? ", (name) => {
rl.write(`Hello ${name}, your terminal is ${rl.output.columns} columns wide\n`);
rl.close();
});
}
```

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

The streams are `stream.Duplex` instances rather than `net.Socket`s, a `ReadStream`
is not writable and a `WriteStream` is not readable. Reading blocks the component:
a flowing `ReadStream` pulls one chunk at a time and emits `'data'` synchronously
between pulls, so terminal input is read inside the export that asked for it.
`'resize'` is emitted only by `_refreshSize()`, since a component receives no
`SIGWINCH`. Color detection reads the provider's environment when none is passed.

### Errors globals

Node's [Errors API](https://nodejs.org/docs/latest-v24.x/api/errors.html) is not an
Expand Down
14 changes: 14 additions & 0 deletions packages/jco-std/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,20 @@
"types": "./dist/wasi/0.2.x/node/24.x.x/repl.d.ts",
"browser": "./dist/wasi/0.2.x/node/24.x.x/repl.js",
"default": "./dist/wasi/0.2.x/node/24.x.x/repl.js"
},
"./wasi/0.2.x/node/24.x.x/tty": {
"types": "./dist/wasi/0.2.x/node/24.x.x/tty.d.ts",
"browser": "./dist/wasi/0.2.x/node/24.x.x/tty.js",
"default": "./dist/wasi/0.2.x/node/24.x.x/tty.js"
},
"./wasi/0.2.x/node/24.x.x/tty/host": {
"types": "./dist/wasi/0.2.x/node/24.x.x/tty-host.d.ts",
"browser": "./dist/wasi/0.2.x/node/24.x.x/tty-host.js",
"default": "./dist/wasi/0.2.x/node/24.x.x/tty-host.js"
},
"./wasi/0.2.x/node/24.x.x/tty/host/node": {
"types": "./dist/wasi/0.2.x/node/24.x.x/tty-host-node.d.ts",
"node": "./dist/wasi/0.2.x/node/24.x.x/tty-host-node.js"
}
},
"scripts": {
Expand Down
3 changes: 3 additions & 0 deletions packages/jco-std/src/wasi/0.2.x/node/24.x.x/errors/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ export type ErrorCode =
| "ERR_ILLEGAL_CONSTRUCTOR"
| "ERR_INVALID_ARG_TYPE"
| "ERR_INVALID_ARG_VALUE"
| "ERR_INVALID_FD"
| "ERR_INVALID_RETURN_VALUE"
| "ERR_INVALID_THIS"
| "ERR_JCO_UNSUPPORTED_DEPRECATED_NODE_API"
| "ERR_JCO_UNSUPPORTED_NODE_API"
| "ERR_MISSING_ARGS"
| "ERR_OUT_OF_RANGE"
| "ERR_TTY_INIT_FAILED"
// Jco-specific codes. Every `ERR_JCO_*` code a shim raises is declared here so the set is
// auditable in one place; per-builtin modules import these rather than restating literals.
| "ERR_JCO_CHILD_PROCESS_ADAPTER_REQUIRED"
Expand All @@ -32,6 +34,7 @@ export type ErrorCode =
| "ERR_JCO_HTTP_ADAPTER_REQUIRED"
| "ERR_JCO_INSPECTOR_ADAPTER_REQUIRED"
| "ERR_JCO_OS_ADAPTER_REQUIRED"
| "ERR_JCO_TTY_ADAPTER_REQUIRED"
| "ERR_JCO_HTTP_IMPLEMENTATION"
| "ERR_JCO_INSPECTOR_HOST"
| "ERR_JCO_INSPECTOR_UNAVAILABLE"
Expand Down
Loading
Loading