Skip to content
Open
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: 2 additions & 0 deletions docs/src/interop/jco-std.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ used by the Hono adapter; assert and Buffer do not add further capabilities.

- `node:assert` and `node:assert/strict`, adapted from Node.js 24 for portable
execution without a host capability; and
- `node:util` and `node:util/types`, sharing assertion equality, console formatting,
scheduling and validation helpers, with portable parsing and MIME utilities; and
- `node:path`, `node:path/posix`, and `node:path/win32`, implemented with portable
path algorithms and a `wasi:cli/environment` provider for operations that need
the guest working directory; and
Expand Down
67 changes: 66 additions & 1 deletion docs/src/interop/nodejs-builtins.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,71 @@ is planned.
| `node:stream`, `node:stream/promises` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/stream` and `/stream/promises` | Classic streams, pipelines, operators, disposal, and Web adapters. No WIT capability. |
| `node:stream/consumers` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/stream/consumers` | Portable Node 24 collection helpers over async iterables and engine globals. Requires no WIT capability. |
| `node:stream/iter` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/stream/iter` | Experimental Node 24.20 iterable streams. Requires no WIT capability. Classic output adapters are explicitly unsupported. |
| `node:util`, `node:util/types` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/util` and `/util/types` | Portable Node 24 utilities; engine and process restrictions are described below. No WIT capability. |

### Utilities

`node:util` provides MIME parsing, argument and environment-file parsing, text
styling, string/array diffs, callback/promise conversion, inspection and formatting,
inheritance, deep equality, and type predicates. Default and named imports are
available; `node:util/types` shares the same predicate object as `util.types`.

```js
import { MIMEType, parseArgs, promisify, styleText } from 'node:util';
import { isUint8Array } from 'node:util/types';

const mime = new MIMEType('text/plain; charset=utf-8');
const { values } = parseArgs({
args: ['--verbose'],
options: { verbose: { type: 'boolean' } },
});
const increment = promisify((value, callback) => callback(null, value + 1));
const answer = await increment(41);
const heading = styleText('bold', mime.essence, { validateStream: false });
const bytes = isUint8Array(new Uint8Array([answer]));
```

The contract targets Node 24.20.0. Portable algorithms run in both QuickJS and
StarlingMonkey. The implementation shares deep equality with `node:assert`, the
formatting core with `node:console`, and scheduling and validation with the
existing stream and error helpers.

- `parseArgs` requires an explicit `args` array. It never reads host `process.argv`.
`parseEnv` returns parsed values without modifying an environment.
- `styleText` requires `{ validateStream: false }` for unconditional ANSI output,
or an explicit stream. Stream validation uses its `isTTY` flag; host color
environment variables and terminal capabilities are not consulted.
- `TextEncoder` and `TextDecoder` use the engine constructors. StarlingMonkey
provides them; QuickJS currently throws `ERR_JCO_UNSUPPORTED_NODE_API` on
construction.
- `promisify` preserves custom hooks, receivers and callback results. Passing a
declared async function without a custom hook throws a deprecated-API error.
The shim cannot identify an ordinary function that returns a promise without
calling it; use promise-returning functions directly. `callbackify` schedules
callbacks through the shared guest microtask queue, without a separate Node
`nextTick` phase.
- `inspect`, `format` and `formatWithOptions` support ordinary values, collections,
descriptors, custom hooks, circular references and inspection options. Native
engine details and Node's full pretty-print layout are not reproduced. Promises
display `<state unavailable>` and weak collections display `<items unknown>`.
`showProxy` and hidden promise/weak-collection state throw; `%o` inspects hidden
properties without unwrapping proxies. Inspection can trigger proxy traps.
- Buffer, typed-array, boxed-value and collection predicates use intrinsic brand
checks. Promise checks require the same realm. Arguments, generator, iterator,
module-namespace and function checks use observable tags and can be spoofed;
error checks have the same limitation when the engine lacks `Error.isError`.
`isCryptoKey` requires the engine's `CryptoKey` implementation.
- `aborted` requires the engine's `WeakRef` and `FinalizationRegistry` for an active
signal. It uses public abort listeners; an earlier listener calling
`stopImmediatePropagation()` can prevent notification.

Host process and native engine operations throw `ERR_JCO_UNSUPPORTED_NODE_API`:
`debug`/`debuglog`, `deprecate`, `getCallSites`, `getSystemErrorName`,
`getSystemErrorMessage`, `getSystemErrorMap`, `setTraceSigInt`,
`convertProcessSignalToExitCode`, `transferableAbortController`,
`transferableAbortSignal`, and the `isProxy`, `isExternal`, and `isKeyObject`
predicates. Deprecated `isArray`, `_extend`, `_errnoException`, and
`_exceptionWithHostPort` throw `ERR_JCO_UNSUPPORTED_DEPRECATED_NODE_API`.

### Classic streams

Expand Down Expand Up @@ -1609,7 +1674,7 @@ set of coordinated shims:
`node:crypto`, `node:dgram`, `node:http2`,
`node:perf_hooks`, `node:repl`, `node:stream`,
`node:stream/promises`, `node:stream/web`, `node:timers`,
`node:tls`, `node:util`, `node:util/types`, `node:v8`, `node:vm`, `node:wasi`,
`node:tls`, `node:v8`, `node:vm`, `node:wasi`,
`node:worker_threads`, and `node:zlib`.

#### Future composition
Expand Down
6 changes: 3 additions & 3 deletions packages/jco-std/LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,10 @@ prospectively choose to deem waived or otherwise exclude such Section(s) of
the License, but only in their entirety and only with respect to the Combined
Software.

--- Node.js stream adaptations (MIT License) ---
--- Node.js adaptations (MIT License) ---

The Node.js stream adaptations identified by upstream provenance comments
in src/wasi/0.2.x/node/24.x.x/stream/ and their compiled forms are covered
The Node.js adaptations identified by upstream provenance comments in
src/wasi/0.2.x/node/24.x.x/stream/ and util/, and their compiled forms, are covered
by the following notice:

Copyright Node.js contributors. All rights reserved.
Expand Down
10 changes: 10 additions & 0 deletions packages/jco-std/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,16 @@
"./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"
},
"./wasi/0.2.x/node/24.x.x/util": {
"types": "./dist/wasi/0.2.x/node/24.x.x/util/index.d.ts",
"browser": "./dist/wasi/0.2.x/node/24.x.x/util/index.js",
"default": "./dist/wasi/0.2.x/node/24.x.x/util/index.js"
},
"./wasi/0.2.x/node/24.x.x/util/types": {
"types": "./dist/wasi/0.2.x/node/24.x.x/util-types.d.ts",
"browser": "./dist/wasi/0.2.x/node/24.x.x/util-types.js",
"default": "./dist/wasi/0.2.x/node/24.x.x/util-types.js"
}
},
"scripts": {
Expand Down
98 changes: 25 additions & 73 deletions packages/jco-std/src/wasi/0.2.x/node/24.x.x/console/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
// Node.js is distributed under the MIT license. See https://github.com/nodejs/node.

import { unsupportedNodeApi } from "../errors/core.js";
import { inspect as inspectValue, type InspectOptions } from "../internal/inspect.js";

export type { InspectOptions };

import { inspect as inspectValue, type InspectOptions } from "../internal/inspect.js";
import { formatArgs as format } from "../util/format-core.js";
export type { InspectOptions } from "../internal/inspect.js";
const clocks = new WeakMap<object, () => number>();
const consoleMethods = [
"log",
Expand All @@ -32,10 +32,15 @@ const consoleMethods = [

export interface WritableStream {
write(value: string, callback?: (error?: Error | null) => void): unknown;

listenerCount?(event: string): number;

once?(event: string, listener: (error?: Error) => void): unknown;

removeListener?(event: string, listener: (error?: Error) => void): unknown;

isTTY?: boolean;

getColorDepth?(): number;
}

Expand All @@ -50,8 +55,11 @@ export interface ConsoleOptions {

export interface ConsoleProviders {
write(stream: "stdout" | "stderr", value: string): void;

isTerminal?(stream: "stdout" | "stderr"): boolean;

colorDepth?(stream: "stdout" | "stderr"): number;

now?: () => number;
}

Expand All @@ -71,76 +79,6 @@ function validateStream(value: unknown, name: string): asserts value is Writable
}
}

function json(value: unknown): string {
try {
return JSON.stringify(value) ?? "undefined";
} catch (error) {
if (error instanceof TypeError && /circular/i.test(error.message)) {
return "[Circular]";
}
throw error;
}
}

function formatNumber(value: unknown, integer: boolean): string {
if (typeof value === "bigint") {
return `${value}n`;
}
if (typeof value === "symbol") {
return "NaN";
}
const number = Number(value);
return String(integer ? Math.trunc(number) : number);
}

function format(args: unknown[], options: InspectOptions): string {
if (args.length === 0) {
return "";
}
if (typeof args[0] !== "string") {
return args.map((value) => inspectValue(value, options)).join(" ");
}

let index = 1;
const formatted = args[0].replace(/%[sdifjoOc%]/g, (token) => {
if (token === "%%") {
return "%";
}
if (token === "%c") {
if (index < args.length) {
index++;
}
return "";
}
if (index >= args.length) {
return token;
}
const value = args[index++];
switch (token) {
case "%s":
return typeof value === "object" && value !== null
? inspectValue(value, { ...options, colors: false, depth: 0 })
: String(value);
case "%d":
case "%f":
return formatNumber(value, false);
case "%i":
return formatNumber(typeof value === "string" ? Number.parseInt(value, 10) : value, true);
case "%j":
return json(value);
default:
return inspectValue(value, token === "%o" ? { ...options, depth: 4 } : options);
}
});
if (index === args.length) {
return formatted;
}
return `${formatted} ${args
.slice(index)
.map((value) => (typeof value === "string" ? value : inspectValue(value, options)))
.join(" ")}`;
}

function displayWidth(value: string): number {
return Array.from(value.replace(/\u001b\[[0-9;]*m/g, "")).length;
}
Expand All @@ -154,8 +92,10 @@ function renderTable(headings: string[], columns: string[][]): string {
}
}
const divider = widths.map((width) => "─".repeat(width + 2));

const row = (values: string[]) =>
`│ ${values.map((value, index) => value + " ".repeat(widths[index] - displayWidth(value))).join(" │ ")} │`;

const lines = [`┌${divider.join("┬")}┐`, row(headings), `├${divider.join("┼")}┤`];
for (let index = 0; index < rowCount; index++) {
lines.push(row(columns.map((column) => column[index] ?? "")));
Expand Down Expand Up @@ -338,15 +278,19 @@ class ConsoleImplementation {
log(...args: unknown[]): void {
this.#stdout(args);
}

info(...args: unknown[]): void {
this.#stdout(args);
}

debug(...args: unknown[]): void {
this.#stdout(args);
}

warn(...args: unknown[]): void {
this.#stderr(args);
}

error(...args: unknown[]): void {
this.#stderr(args);
}
Expand Down Expand Up @@ -446,8 +390,10 @@ class ConsoleImplementation {
this.log(data);
return;
}

const inspect = (value: unknown) =>
inspectValue(value, this.#inspection(this._stdout, { depth: 0, maxArrayLength: 3 }));

let indexHeading = "(index)";
if (data instanceof Map) {
const entries = Array.from(data.entries());
Expand Down Expand Up @@ -518,6 +464,7 @@ class ConsoleImplementation {
dirxml(...args: unknown[]): void {
this.log(...args);
}

groupCollapsed(...args: unknown[]): void {
this.group(...args);
}
Expand Down Expand Up @@ -555,8 +502,11 @@ Object.defineProperty(Console, "name", { value: "Console", configurable: true })

export interface ConsoleModule extends ConsoleImplementation {
Console: typeof Console;

profile(label?: string): void;

profileEnd(label?: string): void;

timeStamp(label?: string): void;
}

Expand All @@ -565,7 +515,9 @@ function hostStream(providers: ConsoleProviders, name: "stdout" | "stderr"): Wri
get isTTY() {
return providers.isTerminal?.(name) ?? false;
},

getColorDepth: providers.colorDepth ? () => providers.colorDepth!(name) : undefined,

write(value: string): void {
providers.write(name, value);
},
Expand Down
19 changes: 19 additions & 0 deletions packages/jco-std/src/wasi/0.2.x/node/24.x.x/util-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as predicates from "./util/types.js";
export * from "./util/types.js";

const types = { ...predicates };
Object.defineProperties(types, {
isCryptoKey: {
value: predicates.isCryptoKey,
enumerable: true,
writable: false,
configurable: false,
},
isKeyObject: {
value: predicates.isKeyObject,
enumerable: true,
writable: false,
configurable: false,
},
});
export default types;
46 changes: 46 additions & 0 deletions packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/aborted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Adapted from Node v24.20.0 lib/internal/abort_controller.js, aborted(), MIT,
// commit 71b8b174857e25106d39b61a9e6f30d927da8b01. Weak event resources use engine
// WeakRef/FinalizationRegistry; unsupported engines fail before installing listeners.
import { validateAbortSignal } from "../stream/shared.js";
import { invalidArgType, unsupportedNodeApi } from "../errors/core.js";

interface Subscription {
signal: WeakRef<AbortSignal>;

listener: () => void;
}

let registry: FinalizationRegistry<Subscription> | undefined;

export async function aborted(signal: AbortSignal, resource: object): Promise<void> {
validateAbortSignal(signal, "signal");
if (signal === undefined) {
throw invalidArgType("signal", "AbortSignal", signal);
}
if (resource === null || (typeof resource !== "object" && typeof resource !== "function")) {
throw invalidArgType("resource", "Object", resource);
}
if (signal.aborted) {
return;
}
if (typeof WeakRef !== "function" || typeof FinalizationRegistry !== "function") {
throw unsupportedNodeApi("util.aborted", "the engine must support weak resource lifetimes");
}
registry ??= new FinalizationRegistry(({ signal, listener }: Subscription): void => {
signal.deref()?.removeEventListener("abort", listener);
});
return new Promise<void>((resolve) => {
const token = {};
const weakResource = new WeakRef(resource);

const listener = (): void => {
registry!.unregister(token);
if (weakResource.deref()) {
resolve();
}
};

registry!.register(resource, { signal: new WeakRef(signal), listener }, token);
signal.addEventListener("abort", listener, { once: true });
});
}
Loading
Loading