diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index e321fb27b..c12a55c22 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -51,6 +51,7 @@ - [`node:timers`](./interop/nodejs-builtins/supported-modules/timers.md) - [`node:tty`](./interop/nodejs-builtins/supported-modules/tty.md) - [`node:url`](./interop/nodejs-builtins/supported-modules/url.md) + - [`node:util`](./interop/nodejs-builtins/supported-modules/util.md) - [Troubleshooting]() - [Common issues](./troubleshooting/common-issues.md) - [Contributor Guide]() diff --git a/docs/src/interop/jco-std.md b/docs/src/interop/jco-std.md index cf3a991ca..10a996b48 100644 --- a/docs/src/interop/jco-std.md +++ b/docs/src/interop/jco-std.md @@ -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 diff --git a/docs/src/interop/nodejs-builtins.md b/docs/src/interop/nodejs-builtins.md index 63d85a365..bdaf7f1a0 100644 --- a/docs/src/interop/nodejs-builtins.md +++ b/docs/src/interop/nodejs-builtins.md @@ -142,7 +142,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: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 diff --git a/docs/src/interop/nodejs-builtins/supported-modules/index.md b/docs/src/interop/nodejs-builtins/supported-modules/index.md index 6bb6120d8..45dea1c14 100644 --- a/docs/src/interop/nodejs-builtins/supported-modules/index.md +++ b/docs/src/interop/nodejs-builtins/supported-modules/index.md @@ -67,6 +67,7 @@ compatibility limits. Related submodules share their parent API page. See the | [`node:timers`](./timers.md), [`node:timers/promises`](./timers.md) | Node 24 timer handles and promise timers over engine task scheduling; see the API page for runtime limits. | | [`node:tty`](./tty.md) | Node 24.20 `isatty`, `ReadStream` and `WriteStream` over the host process's descriptors through an explicit host capability; denied by default. | | [`node:url`](./url.md) | Node 24 URL, URLSearchParams, URLPattern, domain and file conversions; relative file paths use optional WASI environment imports. | +| [`node:util`](./util.md), [`node:util/types`](./util.md) | Portable Node 24 utilities, sharing assertion equality and console formatting. No WIT capability; see the API page for engine and process restrictions. | [Globals](./globals.md) and [Errors](./errors.md) document runtime-wide Node.js APIs. They are not importable as `node:globals` or `node:errors`. diff --git a/docs/src/interop/nodejs-builtins/supported-modules/util.md b/docs/src/interop/nodejs-builtins/supported-modules/util.md new file mode 100644 index 000000000..38a901c22 --- /dev/null +++ b/docs/src/interop/nodejs-builtins/supported-modules/util.md @@ -0,0 +1,67 @@ +# `node:util` + +| Imports | Implementation | +| --- | --- | +| `node:util`, `node:util/types` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/util` and `/util/types` | + +`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 `` and weak collections display ``. + `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`. diff --git a/packages/jco-std/LICENSE b/packages/jco-std/LICENSE index cbeaf8bcb..742af984b 100644 --- a/packages/jco-std/LICENSE +++ b/packages/jco-std/LICENSE @@ -218,11 +218,13 @@ 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 and test runner adaptations (MIT License) --- +--- Node.js adaptations (MIT License) --- The Node.js adaptations identified by upstream provenance comments in -src/wasi/0.2.x/node/24.x.x/stream/ and src/wasi/0.2.x/node/24.x.x/test/, -and their compiled forms, are covered by the following notice: +src/wasi/0.2.x/node/24.x.x/stream/, src/wasi/0.2.x/node/24.x.x/test/, +and src/wasi/0.2.x/node/24.x.x/util/, and their compiled forms, are covered +by the following notice: + Copyright Node.js contributors. All rights reserved. diff --git a/packages/jco-std/package.json b/packages/jco-std/package.json index d425da5b2..cc889e769 100644 --- a/packages/jco-std/package.json +++ b/packages/jco-std/package.json @@ -493,6 +493,16 @@ "types": "./dist/wasi/0.2.x/node/24.x.x/url/idna.d.ts", "browser": "./dist/wasi/0.2.x/node/24.x.x/url/idna.js", "default": "./dist/wasi/0.2.x/node/24.x.x/url/idna.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": { diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/console/core.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/console/core.ts index 13534c220..bf039e87f 100644 --- a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/console/core.ts +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/console/core.ts @@ -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 number>(); const consoleMethods = [ "log", @@ -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; } @@ -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; } @@ -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; } @@ -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] ?? ""))); @@ -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); } @@ -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()); @@ -518,6 +464,7 @@ class ConsoleImplementation { dirxml(...args: unknown[]): void { this.log(...args); } + groupCollapsed(...args: unknown[]): void { this.group(...args); } @@ -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; } @@ -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); }, diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util-types.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util-types.ts new file mode 100644 index 000000000..9b220d0fe --- /dev/null +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util-types.ts @@ -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; diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/aborted.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/aborted.ts new file mode 100644 index 000000000..4dd88fc0c --- /dev/null +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/aborted.ts @@ -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; + + listener: () => void; +} + +let registry: FinalizationRegistry | undefined; + +export async function aborted(signal: AbortSignal, resource: object): Promise { + 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((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 }); + }); +} diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/callbacks.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/callbacks.ts new file mode 100644 index 000000000..3c860377e --- /dev/null +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/callbacks.ts @@ -0,0 +1,151 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Adapted from nodejs/node v24.20.0, commit +// 71b8b174857e25106d39b61a9e6f30d927da8b01, lib/util.js. +// Local changes: explicit TypeScript contracts, shared Jco errors, ECMAScript intrinsics. + +import { codedError, deprecatedNodeApi, invalidArgType, validateFunction } from "../errors/core.js"; +import { nextTick } from "../stream/scheduler.js"; + +type Callback = (error: unknown, value?: T) => void; + +type ResultCallback = (error: unknown, value: T) => void; + +type Callable = (...args: never[]) => unknown; + +export interface CustomPromisified { + [promisify.custom]: T; +} + +const promisifyCustom: unique symbol = Symbol.for("nodejs.util.promisify.custom"); + +export interface Promisify { + (original: CustomPromisified): T; + + ( + original: (...args: [...A, Callback]) => unknown, + ): (...args: A) => Promise; + + (original: Callable): Callable; + + custom: typeof promisifyCustom; +} + +function promisifyImpl(original: CustomPromisified): T; + +function promisifyImpl( + original: (...args: [...A, Callback]) => unknown, +): (...args: A) => Promise; + +function promisifyImpl(original: Callable): Callable; + +function promisifyImpl(original: unknown): Callable { + validateFunction(original, "original"); + const custom = Reflect.get(original, promisify.custom) as unknown; + if (custom) { + validateFunction(custom, "util.promisify.custom"); + return Object.defineProperty(custom, promisify.custom, { value: custom, configurable: true }); + } + // Calling a known async function is already a deprecated promisify call shape. + // Refuse before invoking it, rather than emitting a warning after its side effects. + if (Object.prototype.toString.call(original) === "[object AsyncFunction]") { + throw deprecatedNodeApi( + "util.promisify(async function)", + "call the promise-returning function directly", + ); + } + const invoke = original; + + function fn(this: unknown, ...args: unknown[]): Promise { + return new Promise((resolve, reject) => { + args.push((error: unknown, value: unknown): void => { + if (error) { + reject(error); + } else { + resolve(value); + } + }); + Reflect.apply(invoke, this, args); + }); + } + + Object.setPrototypeOf(fn, Object.getPrototypeOf(original)); + Object.defineProperty(fn, promisify.custom, { value: fn, configurable: true }); + return Object.defineProperties(fn, Object.getOwnPropertyDescriptors(original)); +} + +export const promisify: Promisify = Object.assign(promisifyImpl, { + custom: promisifyCustom, +} satisfies { custom: typeof promisifyCustom }); +Object.defineProperty(promisify, "name", { value: "promisify" }); + +export function callbackify( + original: (...args: A) => Promise, +): (...args: [...A, ResultCallback]) => void { + validateFunction(original, "original"); + + function callbackified(this: unknown, ...args: [...A, ResultCallback]): void { + const callback = args.pop(); + validateFunction(callback, "last argument"); + const cb = callback.bind(this); + const promise = Reflect.apply(original, this, args) as Promise; + promise.then( + (value: R): void => nextTick(cb, null, value), + (reason: unknown): void => + nextTick((): void => { + if (!reason) { + const error = codedError( + new Error("Promise was rejected with falsy value"), + "ERR_FALSY_VALUE_REJECTION", + ); + Object.assign(error, { reason }); + reason = error; + } + cb(reason); + }), + ); + } + + const descriptors = Object.getOwnPropertyDescriptors(original); + if (typeof descriptors.length?.value === "number") { + descriptors.length.value++; + } + if (typeof descriptors.name?.value === "string") { + descriptors.name.value += "Callbackified"; + } + Object.defineProperties(callbackified, descriptors); + return callbackified; +} + +export function inherits(ctor: { prototype: object }, superCtor: { prototype: object }): void { + if (ctor === undefined || ctor === null) { + throw invalidArgType("ctor", "Function", ctor); + } + if (superCtor === undefined || superCtor === null) { + throw invalidArgType("superCtor", "Function", superCtor); + } + if (superCtor.prototype === undefined) { + throw invalidArgType("superCtor.prototype", "Object", undefined); + } + Object.defineProperty(ctor, "super_", { value: superCtor, writable: true, configurable: true }); + Object.setPrototypeOf(ctor.prototype, superCtor.prototype); +} diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/diff.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/diff.ts new file mode 100644 index 000000000..f7a553ad4 --- /dev/null +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/diff.ts @@ -0,0 +1,154 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. +// Adapted from nodejs/node v24.20.0, commit +// 71b8b174857e25106d39b61a9e6f30d927da8b01, lib/internal/util/diff.js and lib/internal/assert/myers_diff.js. +// Local changes: explicit TypeScript contracts, shared Jco errors, ECMAScript intrinsics. +import { outOfRange, invalidArgType } from "../errors/core.js"; + +export type Difference = [operation: -1 | 0 | 1, value: string]; + +const kOperations = { + DELETE: -1, + NOP: 0, + INSERT: 1, +} as const; + +function areLinesEqual(actual: string, expected: string, checkCommaDisparity: boolean): boolean { + if (actual === expected) { + return true; + } + if (checkCommaDisparity) { + return actual + "," === expected || actual === expected + ","; + } + return false; +} + +function myersDiff( + actual: string | string[], + expected: string | string[], + checkCommaDisparity = false, +): Difference[] { + const actualLength = actual.length; + const expectedLength = expected.length; + const max = actualLength + expectedLength; + if (max > 2 ** 31 - 1) { + throw outOfRange("myersDiff input size", "< 2^31", max); + } + const v = new Int32Array(2 * max + 1); + const trace: Int32Array[] = []; + for (let diffLevel = 0; diffLevel <= max; diffLevel++) { + trace.push(new Int32Array(v)); // Clone the current state of `v` + for (let diagonalIndex = -diffLevel; diagonalIndex <= diffLevel; diagonalIndex += 2) { + const offset = diagonalIndex + max; + const previousOffset = v[offset - 1]; + const nextOffset = v[offset + 1]; + let x = + diagonalIndex === -diffLevel || (diagonalIndex !== diffLevel && previousOffset < nextOffset) + ? nextOffset + : previousOffset + 1; + let y = x - diagonalIndex; + while ( + x < actualLength && + y < expectedLength && + areLinesEqual(actual[x], expected[y], checkCommaDisparity) + ) { + x++; + y++; + } + v[offset] = x; + if (x >= actualLength && y >= expectedLength) { + return backtrack(trace, actual, expected, checkCommaDisparity); + } + } + } + throw new Error("Unreachable Myers diff state"); +} + +function backtrack( + trace: Int32Array[], + actual: string | string[], + expected: string | string[], + checkCommaDisparity: boolean, +): Difference[] { + const actualLength = actual.length; + const expectedLength = expected.length; + const max = actualLength + expectedLength; + let x = actualLength; + let y = expectedLength; + const result: Difference[] = []; + for (let diffLevel = trace.length - 1; diffLevel >= 0; diffLevel--) { + const v = trace[diffLevel]; + const diagonalIndex = x - y; + const offset = diagonalIndex + max; + let prevDiagonalIndex: number; + if ( + diagonalIndex === -diffLevel || + (diagonalIndex !== diffLevel && v[offset - 1] < v[offset + 1]) + ) { + prevDiagonalIndex = diagonalIndex + 1; + } else { + prevDiagonalIndex = diagonalIndex - 1; + } + const prevX = v[prevDiagonalIndex + max]; + const prevY = prevX - prevDiagonalIndex; + while (x > prevX && y > prevY) { + const actualItem = actual[x - 1]; + const value = checkCommaDisparity && !actualItem.endsWith(",") ? expected[y - 1] : actualItem; + result.push([kOperations.NOP, value]); + x--; + y--; + } + if (diffLevel > 0) { + if (x > prevX) { + result.push([kOperations.INSERT, actual[--x]]); + } else { + result.push([kOperations.DELETE, expected[--y]]); + } + } + } + return result; +} + +function validateInput(value: unknown, name: string): asserts value is string | string[] { + if (typeof value === "string") { + return; + } + if (!Array.isArray(value)) { + throw invalidArgType(name, "string", value); + } + for (let i = 0; i < value.length; i++) { + if (typeof value[i] !== "string") { + throw invalidArgType(`${name}[${i}]`, "string", value[i]); + } + } +} + +export function diff(actual: string | string[], expected: string | string[]): Difference[] { + if (actual === expected) { + return []; + } + validateInput(actual, "actual"); + validateInput(expected, "expected"); + if (actual.length + expected.length === 0) { + return []; + } + return myersDiff(actual, expected).reverse(); +} diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/format-core.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/format-core.ts new file mode 100644 index 000000000..2d936392f --- /dev/null +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/format-core.ts @@ -0,0 +1,79 @@ +// Console behavior in this module is adapted from Node.js v24.19.0's +// lib/internal/console/{constructor,global}.js and lib/internal/cli_table.js, +// commit cdc1b38d40cb567b7ad0b39c86addf830a0af0ae. +// Node.js is distributed under the MIT license. See https://github.com/nodejs/node. + +// Shared console formatting core, extracted without changing its output. + +import { inspect as inspectValue, type InspectOptions } from "../internal/inspect.js"; +export { inspect as inspectValue, type InspectOptions } from "../internal/inspect.js"; + +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); +} + +export function formatArgs(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(" ")}`; +} diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/index.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/index.ts new file mode 100644 index 000000000..6ba788282 --- /dev/null +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/index.ts @@ -0,0 +1,74 @@ +import { MIMEParams, MIMEType } from "./mime.js"; +import { callbackify, inherits, promisify } from "./callbacks.js"; +import { diff } from "./diff.js"; +import { parseArgs } from "./parse-args.js"; +import { parseEnv } from "./parse-env.js"; +import { inspect, format, formatWithOptions } from "./inspect.js"; +import { stripVTControlCharacters, styleText, toUSVString } from "./text.js"; +import { aborted } from "./aborted.js"; +import { isDeepStrictEqual } from "../assert/comparisons.js"; +import types from "../util-types.js"; +import { unsupportedNodeApi } from "../errors/core.js"; +import * as unsupported from "./unsupported.js"; + +export { + MIMEParams, + MIMEType, + callbackify, + inherits, + promisify, + diff, + parseArgs, + parseEnv, + inspect, + format, + formatWithOptions, + stripVTControlCharacters, + styleText, + toUSVString, + aborted, + isDeepStrictEqual, + types, +}; +export * from "./unsupported.js"; +export type { InspectOptions, InspectFunction } from "./inspect.js"; +export type { StyleTextOptions } from "./text.js"; +export type * from "./parse-args-types.js"; +export type { Difference } from "./diff.js"; + +// These fallbacks are constructor-only refusal points, with the public constructor +// types retained for callers. They never create an incomplete decoder or encoder. +export const TextDecoder: typeof globalThis.TextDecoder = + globalThis.TextDecoder ?? + (function TextDecoder(): never { + throw unsupportedNodeApi("util.TextDecoder", "the engine does not provide TextDecoder"); + } as unknown as typeof globalThis.TextDecoder); + +export const TextEncoder: typeof globalThis.TextEncoder = + globalThis.TextEncoder ?? + (function TextEncoder(): never { + throw unsupportedNodeApi("util.TextEncoder", "the engine does not provide TextEncoder"); + } as unknown as typeof globalThis.TextEncoder); + +export default { + ...unsupported, + MIMEParams, + MIMEType, + TextDecoder, + TextEncoder, + aborted, + callbackify, + diff, + format, + formatWithOptions, + inherits, + inspect, + isDeepStrictEqual, + parseArgs, + parseEnv, + promisify, + stripVTControlCharacters, + styleText, + toUSVString, + types, +}; diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/inspect.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/inspect.ts new file mode 100644 index 000000000..44f8aa548 --- /dev/null +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/inspect.ts @@ -0,0 +1,538 @@ +// Builds on jco-std's shared console formatter. Public option handling and ANSI +// metadata follow Node v24.20.0 lib/internal/util/inspect.js (MIT), commit +// 71b8b174857e25106d39b61a9e6f30d927da8b01. Native engine inspection is refused. +import { inspectValue, type InspectOptions } from "./format-core.js"; +import { + isBoxedPrimitive, + isBooleanObject, + isNumberObject, + isStringObject, + isBigIntObject, +} from "./types.js"; +import { invalidArgType, unsupportedNodeApi, validateObject } from "../errors/core.js"; +export type { InspectOptions } from "./format-core.js"; + +export interface InspectFunction { + (value: unknown, options?: InspectOptions): string; + + (value: unknown, showHidden?: boolean, depth?: number | null, colors?: boolean): string; + + custom: symbol; + defaultOptions: InspectOptions; + colors: Record; + styles: Record; +} + +const defaults: InspectOptions = Object.seal({ + showHidden: false, + depth: 2, + colors: false, + customInspect: true, + showProxy: false, + maxArrayLength: 100, + maxStringLength: 10000, + breakLength: 80, + compact: 3, + sorted: false, + getters: false, + numericSeparator: false, +}); + +function inspectImpl( + value: unknown, + options?: InspectOptions | boolean, + depth?: number | null, + colors?: boolean, +): string { + const opts = { + ...defaults, + ...(typeof options === "boolean" ? { showHidden: options } : options), + }; + if (arguments.length > 2) { + opts.depth = depth; + } + if (arguments.length > 3) { + opts.colors = colors; + } + if (opts.showProxy) { + throw unsupportedNodeApi("util.inspect showProxy", "the engine does not expose proxy targets"); + } + // Reuse the shared console renderer for simple cases; enhanced object traversal + // below handles descriptors, circular references, sorting and collection limits. + return render(value, opts, [], new Map(), 0); +} + +export const inspect: InspectFunction = Object.assign(inspectImpl, { + custom: Symbol.for("nodejs.util.inspect.custom"), + defaultOptions: defaults, + colors: Object.create(null) as Record, + styles: { + special: "cyan", + number: "yellow", + bigint: "yellow", + boolean: "yellow", + undefined: "grey", + null: "bold", + string: "green", + symbol: "green", + date: "magenta", + regexp: "red", + module: "underline", + }, +}); +Object.defineProperty(inspect, "name", { value: "inspect" }); +Object.defineProperty(inspect, "defaultOptions", { + enumerable: false, + configurable: false, + + get: (): InspectOptions => defaults, + + set: (options: InspectOptions): void => { + validateObject(options, "options"); + Object.assign(defaults, options); + }, +}); + +function stylize(text: string, type: string, opts: InspectOptions): string { + const codes = inspect.colors[inspect.styles[type]]; + return opts.colors && codes ? `\x1b[${codes[0]}m${text}\x1b[${codes[1]}m` : text; +} + +function quote(value: string): string { + const mark = !value.includes("'") + ? "'" + : !value.includes('"') + ? '"' + : !value.includes("`") && !value.includes("${") + ? "`" + : "'"; + const escaped = value.replace(/[\x00-\x1f\x7f-\x9f\\'"`]/g, (char) => { + if (char === mark || char === "\\") { + return `\\${char}`; + } + const names: Record = { + "\n": "\\n", + "\r": "\\r", + "\t": "\\t", + "\b": "\\b", + "\f": "\\f", + "\v": "\\v", + }; + if (char in names) { + return names[char]; + } + return char.charCodeAt(0) < 32 || char.charCodeAt(0) >= 127 + ? `\\x${char.charCodeAt(0).toString(16).padStart(2, "0")}` + : char; + }); + return mark + escaped + mark; +} + +// Adapted from Node v24.20.0 lib/internal/util/inspect.js, MIT, commit +// 71b8b174857e25106d39b61a9e6f30d927da8b01. Preserve decimal grouping without native bindings. +function numericString(value: number | bigint): string { + const raw = Object.is(value, -0) ? "-0" : String(value); + if (raw.includes("e")) { + return raw; + } + const [integer, fraction] = raw.split("."); + const grouped = integer.replace(/\B(?=(\d{3})+(?!\d))/g, "_"); + return ( + grouped + + (fraction === undefined ? "" : "." + fraction.replace(/(\d{3})(?=\d)/g, "$1_")) + + (typeof value === "bigint" ? "n" : "") + ); +} + +const builtInObjects = new Set( + Object.getOwnPropertyNames(globalThis).filter((name) => /^[A-Z][a-zA-Z0-9]+$/.test(name)), +); + +// Adapted from the same upstream hasBuiltInToString; proxy unwrapping is unavailable. +function hasBuiltInToString(value: object): boolean { + const hasString = typeof Reflect.get(value, "toString") === "function"; + const hasPrimitive = typeof Reflect.get(value, Symbol.toPrimitive) === "function"; + if (!hasString && !hasPrimitive) { + return true; + } + + const ownsConversion = (object: object): boolean => + (hasString && Object.hasOwn(object, "toString")) || + (hasPrimitive && Object.hasOwn(object, Symbol.toPrimitive)); + + if (ownsConversion(value)) { + return false; + } + let pointer: object | null = Object.getPrototypeOf(value); + while (pointer && !ownsConversion(pointer)) { + pointer = Object.getPrototypeOf(pointer); + } + const descriptor = pointer && Object.getOwnPropertyDescriptor(pointer, "constructor"); + return ( + !!descriptor && + typeof descriptor.value === "function" && + builtInObjects.has(descriptor.value.name) + ); +} + +function render( + value: unknown, + opts: InspectOptions, + ancestors: object[], + circular: Map, + level: number, + propertyWidth = 0, +): string { + if (typeof value === "string") { + const limit = opts.maxStringLength == null ? Infinity : Math.max(0, opts.maxStringLength); + const text = quote(value.slice(0, limit)); + const extra = + value.length > limit + ? `... ${value.length - limit} more character${value.length - limit === 1 ? "" : "s"}` + : ""; + return stylize(text, "string", opts) + extra; + } + if (value === null || (typeof value !== "object" && typeof value !== "function")) { + let text = inspectValue(value, { colors: false }); + if (opts.numericSeparator && (typeof value === "number" || typeof value === "bigint")) { + text = numericString(value); + } + return stylize(text, value === null ? "null" : typeof value, opts); + } + const object = value; + if (opts.customInspect !== false) { + const hook: unknown = Reflect.get(object, inspect.custom); + if ( + typeof hook === "function" && + hook !== inspect && + object !== Object.getPrototypeOf(object)?.constructor?.prototype + ) { + const result: unknown = Reflect.apply(hook, object, [ + opts.depth == null ? null : opts.depth - level, + { ...opts, stylize: (text: string, type: string): string => stylize(text, type, opts) }, + inspect, + ]); + if (result !== object) { + return typeof result === "string" + ? result + : render(result, opts, ancestors, circular, level); + } + } + } + if (ancestors.includes(object)) { + if (!circular.has(object)) { + circular.set(object, circular.size + 1); + } + return stylize(`[Circular *${circular.get(object)}]`, "special", opts); + } + if (opts.depth !== null && level > (opts.depth ?? 2)) { + return stylize( + `[${Array.isArray(value) ? "Array" : value.constructor?.name || "Object"}]`, + "special", + opts, + ); + } + if (value instanceof Date) { + return stylize(inspectValue(value, { colors: false }), "date", opts); + } + if (value instanceof RegExp) { + return stylize(String(value), "regexp", opts); + } + if (value instanceof Error) { + return inspectValue(value, opts); + } + if (value instanceof Promise || value instanceof WeakMap || value instanceof WeakSet) { + if (opts.showHidden) { + throw unsupportedNodeApi( + "util.inspect hidden engine state", + "promise and weak collection contents are not exposed", + ); + } + return `${value.constructor.name} { <${value instanceof Promise ? "state unavailable" : "items unknown"}> }`; + } + ancestors.push(object); + try { + const entries: string[] = []; + let open = "{", + close = "}", + prefix = ""; + const limit = opts.maxArrayLength == null ? Infinity : Math.max(0, opts.maxArrayLength); + + const child = (item: unknown, width = 0): string => + render(item, opts, ancestors, circular, level + 1, width); + + const boxed = isBoxedPrimitive(value); + const keys = opts.showHidden + ? Reflect.ownKeys(object) + : Reflect.ownKeys(object).filter((key) => + Object.prototype.propertyIsEnumerable.call(object, key), + ); + + const addProperty = (key: PropertyKey): void => { + const descriptor = Object.getOwnPropertyDescriptor(object, key)!; + const label = + typeof key === "symbol" + ? `[${stylize(String(key), "symbol", opts)}]` + : !descriptor.enumerable + ? `[${key}]` + : /^[A-Za-z_$][\w$]*$/.test(String(key)) + ? String(key) + : quote(String(key)); + let rendered: string; + if ("value" in descriptor) { + rendered = child(descriptor.value, label.length + 2); + } else if ( + descriptor.get && + (opts.getters === true || + (opts.getters === "get" && !descriptor.set) || + (opts.getters === "set" && descriptor.set)) + ) { + try { + rendered = `[Getter${descriptor.set ? "/Setter" : ""}: ${child(Reflect.apply(descriptor.get, object, []))}]`; + } catch (error) { + rendered = `[Getter: ]`; + } + } else { + rendered = stylize( + descriptor.get ? `[Getter${descriptor.set ? "/Setter" : ""}]` : "[Setter]", + "special", + opts, + ); + } + entries.push(`${label}: ${rendered}`); + }; + + if (boxed) { + const intrinsic = isBooleanObject(value) + ? Boolean.prototype.valueOf + : isNumberObject(value) + ? Number.prototype.valueOf + : isStringObject(value) + ? String.prototype.valueOf + : isBigIntObject(value) + ? BigInt.prototype.valueOf + : Symbol.prototype.valueOf; + const primitive: unknown = Reflect.apply(intrinsic, value, []); + prefix = `[${value.constructor.name}: ${render(primitive, { ...opts, colors: false }, [], new Map(), 0)}]`; + const own = keys.filter( + (key) => + !( + typeof primitive === "string" && + typeof key === "string" && + (/^(0|[1-9]\d*)$/.test(key) || key === "length") + ), + ); + if (!own.length) { + return stylize(prefix, typeof primitive, opts); + } + prefix += " "; + for (const key of own) { + addProperty(key); + } + } else if ( + Array.isArray(value) || + (ArrayBuffer.isView(value) && !(value instanceof DataView)) + ) { + const array = value as unknown as ArrayLike; + open = "["; + close = "]"; + if (!Array.isArray(value)) { + prefix = `${value.constructor.name}(${array.length}) `; + } + for (let i = 0; i < Math.min(array.length, limit); i++) { + if (!Object.hasOwn(value, i)) { + let count = 1; + while (i + count < Math.min(array.length, limit) && !Object.hasOwn(value, i + count)) { + count++; + } + entries.push( + stylize(`<${count} empty item${count === 1 ? "" : "s"}>`, "undefined", opts), + ); + i += count - 1; + } else { + entries.push(child(array[i])); + } + } + if (array.length > limit) { + entries.push( + `... ${array.length - limit} more item${array.length - limit === 1 ? "" : "s"}`, + ); + } + for (const key of keys) { + if (typeof key !== "string" || !/^(0|[1-9]\d*)$/.test(key)) { + addProperty(key); + } + } + } else if (value instanceof Map || value instanceof Set) { + prefix = `${value.constructor.name}(${value.size}) `; + let i = 0; + for (const entry of value) { + if (i++ >= limit) { + break; + } + entries.push( + value instanceof Map ? `${child(entry[0])} => ${child(entry[1])}` : child(entry), + ); + } + if (value.size > limit) { + entries.push(`... ${value.size - limit} more item${value.size - limit === 1 ? "" : "s"}`); + } + if (opts.sorted) { + entries.sort(typeof opts.sorted === "function" ? opts.sorted : undefined); + } + for (const key of keys) { + addProperty(key); + } + } else if ( + value instanceof ArrayBuffer || + (typeof SharedArrayBuffer === "function" && value instanceof SharedArrayBuffer) + ) { + prefix = `${value.constructor.name} `; + // Node formatArrayBuffer, adapted to Uint8Array instead of native Buffer.hexSlice. + try { + const bytes = new Uint8Array(value); + let hex = Array.from(bytes.subarray(0, limit), (byte) => + byte.toString(16).padStart(2, "0"), + ).join(" "); + if (bytes.length > limit) { + hex += ` ... ${bytes.length - limit} more byte${bytes.length - limit === 1 ? "" : "s"}`; + } + entries.push(`${stylize("[Uint8Contents]", "special", opts)}: <${hex}>`); + } catch { + entries.push(stylize("(detached)", "special", opts)); + } + entries.push(`${stylize("[byteLength]", "string", opts)}: ${child(value.byteLength)}`); + for (const key of keys) { + addProperty(key); + } + } else if (value instanceof DataView) { + prefix = "DataView "; + entries.push( + `${stylize("[byteLength]", "string", opts)}: ${child(value.byteLength)}`, + `${stylize("[byteOffset]", "string", opts)}: ${child(value.byteOffset)}`, + `${stylize("[buffer]", "string", opts)}: ${child(value.buffer, 10)}`, + ); + for (const key of keys) { + addProperty(key); + } + } else { + if (typeof value === "function") { + prefix = inspectValue(value, opts) + (keys.length ? " " : ""); + } else if (Object.getPrototypeOf(value) === null) { + prefix = "[Object: null prototype] "; + } else if (value.constructor?.name && value.constructor.name !== "Object") { + prefix = `${value.constructor.name} `; + } + if (opts.sorted) { + keys.sort( + typeof opts.sorted === "function" + ? (a, b) => (opts.sorted as (a: string, b: string) => number)(String(a), String(b)) + : (a, b) => String(a).localeCompare(String(b)), + ); + } + for (const key of keys) { + addProperty(key); + } + if (typeof value === "function" && !entries.length) { + return prefix; + } + } + const single = entries.length + ? `${prefix}${open} ${entries.join(", ")} ${close}` + : `${prefix}${open}${close}`; + const id = circular.get(object); + const reference = id ? ` ` : ""; + if ( + entries.length && + (opts.compact === false || + single.replace(/\x1b\[[0-9;]*m/g, "").length + level * 2 + propertyWidth > + (opts.breakLength ?? 80)) + ) { + const indent = " ".repeat(level + 1); + return `${reference}${prefix}${open}\n${indent}${entries.join(`,\n${indent}`)}\n${" ".repeat(level)}${close}`; + } + return reference + single; + } finally { + ancestors.pop(); + } +} + +export function format(...args: unknown[]): string { + return formatWithOptions({}, ...args); +} + +export function formatWithOptions(options: InspectOptions, ...args: unknown[]): string { + if (typeof options !== "object" || options === null) { + throw invalidArgType("inspectOptions", "Object", options); + } + if (!args.length) { + return ""; + } + if (typeof args[0] !== "string") { + return args.map((v) => (typeof v === "string" ? v : inspect(v, options))).join(" "); + } + if (args.length === 1) { + return args[0]; + } + let index = 1; + let text = args[0].replace(/%[sdifjoOc%]/g, (token) => { + if (token === "%%") { + return "%"; + } + if (index >= args.length) { + return token; + } + const value = args[index++]; + switch (token) { + case "%c": + return ""; + case "%s": + return typeof value === "object" && value !== null && hasBuiltInToString(value) + ? inspect(value, { ...options, colors: false, depth: 0, compact: 3 }) + : typeof value === "bigint" || typeof value === "number" + ? inspect(value, { colors: false, numericSeparator: options.numericSeparator }) + : String(value); + case "%d": + return typeof value === "bigint" + ? inspect(value, { colors: false, numericSeparator: options.numericSeparator }) + : typeof value === "symbol" + ? "NaN" + : inspect(Number(value), { colors: false, numericSeparator: options.numericSeparator }); + case "%i": + return typeof value === "bigint" + ? inspect(value, { colors: false, numericSeparator: options.numericSeparator }) + : typeof value === "symbol" + ? "NaN" + : inspect(parseInt(String(value)), { + colors: false, + numericSeparator: options.numericSeparator, + }); + case "%f": + return typeof value === "symbol" + ? "NaN" + : inspect(parseFloat(String(value)), { + colors: false, + numericSeparator: options.numericSeparator, + }); + case "%j": + try { + return JSON.stringify(value) ?? "undefined"; + } catch (error) { + if (error instanceof TypeError && /circular|cyclic/i.test(error.message)) { + return "[Circular]"; + } + throw error; + } + default: + return inspect( + value, + token === "%o" ? { ...options, showHidden: true, depth: 4 } : options, + ); + } + }); + while (index < args.length) { + const value = args[index++]; + text += ` ${typeof value === "string" ? value : inspect(value, options)}`; + } + return text; +} diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/mime.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/mime.ts new file mode 100644 index 000000000..12c52d2c2 --- /dev/null +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/mime.ts @@ -0,0 +1,379 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. +// Adapted from nodejs/node v24.20.0, commit +// 71b8b174857e25106d39b61a9e6f30d927da8b01, lib/internal/mime.js. +// Local changes: explicit TypeScript contracts, shared Jco errors, ECMAScript intrinsics. +import { codedError } from "../errors/core.js"; + +function mimeError(production: string, str: string, invalidIndex: number): TypeError { + return codedError( + new TypeError( + `The MIME syntax for a ${production} in "${str}" is invalid${invalidIndex !== -1 ? ` at ${invalidIndex}` : ""}`, + ), + "ERR_INVALID_MIME_SYNTAX", + ); +} + +const NOT_HTTP_TOKEN_CODE_POINT = /[^!#$%&'*+\-.^_`|~A-Za-z0-9]/g; +const NOT_HTTP_QUOTED_STRING_CODE_POINT = /[^\t\u0020-~\u0080-\u00FF]/g; +const END_BEGINNING_WHITESPACE = /[^\r\n\t ]|$/; +const START_ENDING_WHITESPACE = /[\r\n\t ]*$/; + +function toASCIILower(str: string): string { + // eslint-disable-next-line no-control-regex + if (!/[^\x00-\x7f]/.test(str)) { + return str.toLowerCase(); + } + let result = ""; + for (let i = 0; i < str.length; i++) { + const char = str[i]; + result += char >= "A" && char <= "Z" ? char.toLowerCase() : char; + } + return result; +} + +const SOLIDUS = "/"; +const SEMICOLON = ";"; + +function parseTypeAndSubtype(str: string): [string, string, number] { + // Skip only HTTP whitespace from start + let position = str.search(END_BEGINNING_WHITESPACE); + // read until '/' + const typeEnd = str.indexOf(SOLIDUS, position); + const trimmedType = typeEnd === -1 ? str.slice(position) : str.slice(position, typeEnd); + const invalidTypeIndex = trimmedType.search(NOT_HTTP_TOKEN_CODE_POINT); + if (trimmedType === "" || invalidTypeIndex !== -1 || typeEnd === -1) { + throw mimeError("type", str, invalidTypeIndex); + } + // skip type and '/' + position = typeEnd + 1; + const type = toASCIILower(trimmedType); + // read until ';' + const subtypeEnd = str.indexOf(SEMICOLON, position); + const rawSubtype = subtypeEnd === -1 ? str.slice(position) : str.slice(position, subtypeEnd); + position += rawSubtype.length; + if (subtypeEnd !== -1) { + // skip ';' + position += 1; + } + const trimmedSubtype = rawSubtype.slice(0, rawSubtype.search(START_ENDING_WHITESPACE)); + const invalidSubtypeIndex = trimmedSubtype.search(NOT_HTTP_TOKEN_CODE_POINT); + if (trimmedSubtype === "" || invalidSubtypeIndex !== -1) { + throw mimeError("subtype", str, invalidSubtypeIndex); + } + const subtype = toASCIILower(trimmedSubtype); + return [type, subtype, position]; +} + +const EQUALS_SEMICOLON_OR_END = /[;=]|$/; +const QUOTED_VALUE_PATTERN = /^(?:([\\]$)|[\\][\s\S]|[^"])*(?:(")|$)/u; + +function removeBackslashes(str: string): string { + let ret = ""; + // We stop at str.length - 1 because we want to look ahead one character. + let i; + for (i = 0; i < str.length - 1; i++) { + const c = str[i]; + if (c === "\\") { + i++; + ret += str[i]; + } else { + ret += c; + } + } + // We add the last character if we didn't skip to it. + if (i === str.length - 1) { + ret += str[i]; + } + return ret; +} + +function escapeQuoteOrSolidus(str: string): string { + let result = ""; + for (let i = 0; i < str.length; i++) { + const char = str[i]; + result += char === '"' || char === "\\" ? `\\${char}` : char; + } + return result; +} + +const encode = (value: string): string => { + if (value.length === 0) { + return '""'; + } + const encode = value.search(NOT_HTTP_TOKEN_CODE_POINT) !== -1; + if (!encode) { + return value; + } + const escaped = escapeQuoteOrSolidus(value); + return `"${escaped}"`; +}; + +let instantiateMimeParams: (str: string) => MIMEParams; + +export class MIMEParams { + declare [Symbol.iterator]: () => IterableIterator<[string, string]>; + declare toJSON: () => string; + #data = new Map(); + // We set the flag the MIMEParams instance as processed on initialization + // to defer the parsing of a potentially large string. + #processed = true; + #string = ""; + /** + * Used to instantiate a MIMEParams object within the MIMEType class and + * to allow it to be parsed lazily. + * @returns {MIMEParams} + */ + static { + instantiateMimeParams = (str: string): MIMEParams => { + const instance = new MIMEParams(); + instance.#string = str; + instance.#processed = false; + return instance; + }; + } + + /** + * @param {string} name + * @returns {void} + */ + delete(name: string): void { + this.#parse(); + this.#data.delete(toASCIILower(`${name}`)); + } + + get(name: string): string | null { + this.#parse(); + const data = this.#data; + name = toASCIILower(`${name}`); + if (data.has(name)) { + return data.get(name)!; + } + return null; + } + + has(name: string): boolean { + this.#parse(); + return this.#data.has(toASCIILower(`${name}`)); + } + + set(name: string, value: string): void { + this.#parse(); + const data = this.#data; + name = toASCIILower(`${name}`); + value = `${value}`; + const invalidNameIndex = name.search(NOT_HTTP_TOKEN_CODE_POINT); + if (name.length === 0 || invalidNameIndex !== -1) { + throw mimeError("parameter name", name, invalidNameIndex); + } + const invalidValueIndex = value.search(NOT_HTTP_QUOTED_STRING_CODE_POINT); + if (invalidValueIndex !== -1) { + throw mimeError("parameter value", value, invalidValueIndex); + } + data.set(name, value); + } + + *entries(): IterableIterator<[string, string]> { + this.#parse(); + yield* this.#data.entries(); + } + + *keys(): IterableIterator { + this.#parse(); + yield* this.#data.keys(); + } + + *values(): IterableIterator { + this.#parse(); + yield* this.#data.values(); + } + + toString(): string { + this.#parse(); + let ret = ""; + for (const { 0: key, 1: value } of this.#data) { + const encoded = encode(value); + // Ensure they are separated + if (ret.length) { + ret += ";"; + } + ret += `${key}=${encoded}`; + } + return ret; + } + + // Used to act as a friendly class to stringifying stuff + // not meant to be exposed to users, could inject invalid values + #parse(): void { + if (this.#processed) { + return; + } // already parsed + const paramsMap = this.#data; + let position = 0; + const str = this.#string; + const endOfSource = str.slice(position).search(START_ENDING_WHITESPACE) + position; + while (position < endOfSource) { + // Skip any whitespace before parameter + position += str.slice(position).search(END_BEGINNING_WHITESPACE); + // Read until ';' or '=' + const afterParameterName = str.slice(position).search(EQUALS_SEMICOLON_OR_END) + position; + const parameterString = toASCIILower(str.slice(position, afterParameterName)); + position = afterParameterName; + // If we found a terminating character + if (position < endOfSource) { + // Safe to use because we never do special actions for surrogate pairs + const char = str.charAt(position); + // Skip the terminating character + position += 1; + // Ignore parameters without values + if (char === ";") { + continue; + } + } + // If we are at end of the string, it cannot have a value + if (position >= endOfSource) { + break; + } + // Safe to use because we never do special actions for surrogate pairs + const char = str.charAt(position); + let parameterValue = null; + if (char === '"') { + // Handle quoted-string form of values + // skip '"' + position += 1; + // Find matching closing '"' or end of string + // use $1 to see if we terminated on unmatched '\' + // use $2 to see if we terminated on a matching '"' + // so we can skip the last char in either case + const insideMatch = QUOTED_VALUE_PATTERN.exec(str.slice(position))!; + position += insideMatch[0].length; + // Skip including last character if an unmatched '\' or '"' during + // unescape + const inside = + insideMatch[1] || insideMatch[2] ? insideMatch[0].slice(0, -1) : insideMatch[0]; + // Unescape '\' quoted characters + parameterValue = removeBackslashes(inside); + // If we did have an unmatched '\' add it back to the end + if (insideMatch[1]) { + parameterValue += "\\"; + } + } else { + // Handle the normal parameter value form + const valueEnd = str.indexOf(SEMICOLON, position); + const rawValue = valueEnd === -1 ? str.slice(position) : str.slice(position, valueEnd); + position += rawValue.length; + const trimmedValue = rawValue.slice(0, rawValue.search(START_ENDING_WHITESPACE)); + // Ignore parameters without values + if (trimmedValue === "") { + continue; + } + parameterValue = trimmedValue; + } + if ( + parameterString !== "" && + parameterString.search(NOT_HTTP_TOKEN_CODE_POINT) === -1 && + parameterValue.search(NOT_HTTP_QUOTED_STRING_CODE_POINT) === -1 && + paramsMap.has(parameterString) === false + ) { + paramsMap.set(parameterString, parameterValue); + } + position++; + } + this.#data = paramsMap; + this.#processed = true; + } +} + +const MIMEParamsStringify = MIMEParams.prototype.toString; +Object.defineProperty(MIMEParams.prototype, Symbol.iterator, { + configurable: true, + value: MIMEParams.prototype.entries, + writable: true, +}); +Object.defineProperty(MIMEParams.prototype, "toJSON", { + configurable: true, + value: MIMEParamsStringify, + writable: true, +}); + +export class MIMEType { + declare toJSON: () => string; + #type: string; + #subtype: string; + #parameters: MIMEParams; + + constructor(string: string) { + string = `${string}`; + const data = parseTypeAndSubtype(string); + this.#type = data[0]; + this.#subtype = data[1]; + this.#parameters = instantiateMimeParams(string.slice(data[2])); + } + + get type(): string { + return this.#type; + } + + set type(v: string) { + v = `${v}`; + const invalidTypeIndex = v.search(NOT_HTTP_TOKEN_CODE_POINT); + if (v.length === 0 || invalidTypeIndex !== -1) { + throw mimeError("type", v, invalidTypeIndex); + } + this.#type = toASCIILower(v); + } + + get subtype(): string { + return this.#subtype; + } + + set subtype(v: string) { + v = `${v}`; + const invalidSubtypeIndex = v.search(NOT_HTTP_TOKEN_CODE_POINT); + if (v.length === 0 || invalidSubtypeIndex !== -1) { + throw mimeError("subtype", v, invalidSubtypeIndex); + } + this.#subtype = toASCIILower(v); + } + + get essence(): string { + return `${this.#type}/${this.#subtype}`; + } + + get params(): MIMEParams { + return this.#parameters; + } + + toString(): string { + let ret = `${this.#type}/${this.#subtype}`; + const paramStr = MIMEParamsStringify.call(this.#parameters); + if (paramStr.length) { + ret += `;${paramStr}`; + } + return ret; + } +} + +Object.defineProperty(MIMEType.prototype, "toJSON", { + configurable: true, + value: MIMEType.prototype.toString, + writable: true, +}); diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/parse-args-types.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/parse-args-types.ts new file mode 100644 index 000000000..ad83eee6c --- /dev/null +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/parse-args-types.ts @@ -0,0 +1,67 @@ +export interface ParseArgsOption { + type: "string" | "boolean"; + multiple?: boolean; + short?: string; + default?: string | boolean | string[] | boolean[]; +} + +export interface ParseArgsConfig { + args?: string[]; + strict?: boolean; + allowPositionals?: boolean; + allowNegative?: boolean; + tokens?: boolean; + options?: Record; +} + +export interface OptionToken { + kind: "option"; + name: string; + rawName: string; + index: number; + value: string | undefined; + inlineValue: boolean | undefined; +} + +export type ParseArgsToken = + | OptionToken + | { kind: "positional"; index: number; value: string } + | { kind: "option-terminator"; index: number }; + +export type OptionValue = string | boolean | (string | boolean)[]; + +type Values = Record; + +type Options = Record; + +type IfStrict = T extends false ? No : Yes; + +type Scalar = IfStrict< + C["strict"], + O["type"] extends "string" ? string : O["type"] extends "boolean" ? boolean : string | boolean, + string | boolean +>; + +type ParsedValue = O["multiple"] extends true + ? Scalar[] + : Scalar; + +type KnownValues = { + -readonly [K in keyof O]?: ParsedValue; +} & { + -readonly [K in keyof O as O[K] extends { default: unknown } ? K : never]: ParsedValue; +}; + +export type ParsedResults = ParseArgsConfig extends T + ? { + values: Values; + positionals: string[]; + tokens?: ParseArgsToken[]; + } + : { + values: (T extends { options: infer O extends Options } + ? KnownValues + : Record) & + IfStrict>; + positionals: string[]; + } & (T extends { tokens: true } ? { tokens: ParseArgsToken[] } : Record); diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/parse-args-utils.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/parse-args-utils.ts new file mode 100644 index 000000000..5b04747c2 --- /dev/null +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/parse-args-utils.ts @@ -0,0 +1,236 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. +// Adapted from nodejs/node v24.20.0, commit +// 71b8b174857e25106d39b61a9e6f30d927da8b01, lib/internal/util/parse_args/{parse_args,utils}.js. +// Local changes: explicit TypeScript contracts, shared Jco errors, ECMAScript intrinsics. +import { validateObject } from "../errors/core.js"; +import type { ParseArgsOption, OptionValue } from "./parse-args-types.js"; + +type Options = Record; + +type Values = Record; + +// These are internal utilities to make the parsing logic easier to read, and +// add lots of detail for the curious. They are in a separate file to allow +// unit testing, although that is not essential (this could be rolled into +// main file and just tested implicitly via API). +// +// These routines are for internal use, not for export to client. +/** + * Return the named property, but only if it is an own property. + * @returns The own property value, or undefined. + */ +export function objectGetOwn( + obj: T, + prop: K, +): T[K] | undefined { + if (Object.hasOwn(obj, prop)) { + return obj[prop]; + } +} + +/** + * Return the named options property, but only if it is an own property. + * @returns The own property value, or undefined. + */ +export function optionsGetOwn( + options: Options, + longOption: string, + prop: K, +): ParseArgsOption[K] | undefined { + if (Object.hasOwn(options, longOption)) { + return objectGetOwn(options[longOption], prop); + } +} + +/** + * Determines if the argument may be used as an option value. + * @example + * ``` + * isOptionValue('V') // returns true + * isOptionValue('-v') // returns true (greedy) + * isOptionValue('--foo') // returns true (greedy) + * isOptionValue(undefined) // returns false + * ``` + * @returns {boolean} + */ +export function isOptionValue(value: string | undefined): boolean { + if (value == null) { + return false; + } + // Open Group Utility Conventions are that an option-argument + // is the argument after the option, and may start with a dash. + return true; // greedy! +} + +/** + * Detect whether there is possible confusion and user may have omitted + * the option argument, like `--port --verbose` when `port` of type:string. + * In strict mode we throw errors if value is option-like. + * @returns {boolean} + */ +export function isOptionLikeValue(value: string | undefined): boolean { + if (value == null) { + return false; + } + return value.length > 1 && value.charAt(0) === "-"; +} + +/** + * Determines if `arg` is just a short option. + * @example '-f' + * @returns {boolean} + */ +export function isLoneShortOption(arg: string): boolean { + return arg.length === 2 && arg.charAt(0) === "-" && arg.charAt(1) !== "-"; +} + +/** + * Determines if `arg` is a lone long option. + * @example + * ``` + * isLoneLongOption('a') // returns false + * isLoneLongOption('-a') // returns false + * isLoneLongOption('--foo') // returns true + * isLoneLongOption('--foo=bar') // returns false + * ``` + * @returns {boolean} + */ +export function isLoneLongOption(arg: string): boolean { + return arg.length > 2 && arg.startsWith("--") && !arg.includes("=", 3); +} + +/** + * Determines if `arg` is a long option and value in the same argument. + * @example + * ``` + * isLongOptionAndValue('--foo') // returns false + * isLongOptionAndValue('--foo=bar') // returns true + * ``` + * @returns {boolean} + */ +export function isLongOptionAndValue(arg: string): boolean { + return arg.length > 2 && arg.startsWith("--") && arg.includes("=", 3); +} + +/** + * Determines if `arg` is a short option group. + * + * See Guideline 5 of the [Open Group Utility Conventions](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). + * One or more options without option-arguments, followed by at most one + * option that takes an option-argument, should be accepted when grouped + * behind one '-' delimiter. + * @example + * ``` + * isShortOptionGroup('-a', {}) // returns false + * isShortOptionGroup('-ab', {}) // returns true + * // -fb is an option and a value, not a short option group + * isShortOptionGroup('-fb', { + * options: { f: { type: 'string' } } + * }) // returns false + * isShortOptionGroup('-bf', { + * options: { f: { type: 'string' } } + * }) // returns true + * // -bfb is an edge case, return true and caller sorts it out + * isShortOptionGroup('-bfb', { + * options: { f: { type: 'string' } } + * }) // returns true + * ``` + * @returns {boolean} + */ +export function isShortOptionGroup(arg: string, options: Options): boolean { + if (arg.length <= 2) { + return false; + } + if (arg.charAt(0) !== "-") { + return false; + } + if (arg.charAt(1) === "-") { + return false; + } + const firstShort = arg.charAt(1); + const longOption = findLongOptionForShort(firstShort, options); + return optionsGetOwn(options, longOption, "type") !== "string"; +} + +/** + * Determine if arg is a short string option followed by its value. + * @example + * ``` + * isShortOptionAndValue('-a', {}); // returns false + * isShortOptionAndValue('-ab', {}); // returns false + * isShortOptionAndValue('-fFILE', { + * options: { foo: { short: 'f', type: 'string' }} + * }) // returns true + * ``` + * @returns {boolean} + */ +export function isShortOptionAndValue(arg: string, options: Options): boolean { + validateObject(options, "options"); + if (arg.length <= 2) { + return false; + } + if (arg.charAt(0) !== "-") { + return false; + } + if (arg.charAt(1) === "-") { + return false; + } + const shortOption = arg.charAt(1); + const longOption = findLongOptionForShort(shortOption, options); + return optionsGetOwn(options, longOption, "type") === "string"; +} + +/** + * Find the long option associated with a short option. Looks for a configured + * `short` and returns the short option itself if a long option is not found. + * @example + * ``` + * findLongOptionForShort('a', {}) // returns 'a' + * findLongOptionForShort('b', { + * options: { bar: { short: 'b' } } + * }) // returns 'bar' + * ``` + * @returns {boolean} + */ +export function findLongOptionForShort(shortOption: string, options: Options): string { + validateObject(options, "options"); + const longOptionEntry = Object.entries(options).find( + ({ 1: optionConfig }) => objectGetOwn(optionConfig, "short") === shortOption, + ); + return longOptionEntry?.[0] ?? shortOption; +} + +/** + * Check if the given option includes a default value + * and that option has not been set by the input args. + * @param {string} longOption - long option name e.g. 'foo' + * @param {object} optionConfig - the option configuration properties + * @param {object} values - option values returned in `values` by parseArgs + * @returns {boolean} + */ +export function useDefaultValueOption( + longOption: string, + optionConfig: ParseArgsOption, + values: Values, +): boolean { + return objectGetOwn(optionConfig, "default") !== undefined && values[longOption] === undefined; +} diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/parse-args.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/parse-args.ts new file mode 100644 index 000000000..3d6388c3a --- /dev/null +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/parse-args.ts @@ -0,0 +1,444 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. +// Adapted from nodejs/node v24.20.0, commit +// 71b8b174857e25106d39b61a9e6f30d927da8b01, lib/internal/util/parse_args/{parse_args,utils}.js. +// Local changes: explicit TypeScript contracts, shared Jco errors, ECMAScript intrinsics. +import { + codedError, + invalidArgType, + invalidArgValue, + validateObject, + unsupportedNodeApi, +} from "../errors/core.js"; +import { + findLongOptionForShort, + isLoneLongOption, + isLoneShortOption, + isLongOptionAndValue, + isOptionValue, + isOptionLikeValue, + isShortOptionAndValue, + isShortOptionGroup, + useDefaultValueOption, + objectGetOwn, + optionsGetOwn, +} from "./parse-args-utils.js"; +import type { + ParseArgsOption, + ParseArgsConfig, + OptionToken, + ParseArgsToken, + OptionValue, + ParsedResults, +} from "./parse-args-types.js"; +export type * from "./parse-args-types.js"; + +type Options = Record; + +type Values = Record; + +interface Result { + values: Values; + positionals: string[]; + tokens?: ParseArgsToken[]; +} + +function validateString(value: unknown, name: string): asserts value is string { + if (typeof value !== "string") { + throw invalidArgType(name, "string", value); + } +} + +function validateBoolean(value: unknown, name: string): asserts value is boolean { + if (typeof value !== "boolean") { + throw invalidArgType(name, "boolean", value); + } +} + +function validateArray(value: unknown, name: string): asserts value is unknown[] { + if (!Array.isArray(value)) { + throw invalidArgType(name, "Array", value); + } +} + +function validateStringArray(value: unknown, name: string): asserts value is string[] { + validateArray(value, name); + value.forEach((v, i) => validateString(v, `${name}[${i}]`)); +} + +function validateBooleanArray(value: unknown, name: string): asserts value is boolean[] { + validateArray(value, name); + value.forEach((v, i) => validateBoolean(v, `${name}[${i}]`)); +} + +function validateUnion(value: unknown, name: string, allowed: string[]): void { + if (!allowed.includes(value as string)) { + throw invalidArgType(name, `('${allowed.join("| ")}')`, value); + } +} + +function invalidOption(message: string): TypeError { + return codedError(new TypeError(message), "ERR_PARSE_ARGS_INVALID_OPTION_VALUE"); +} + +function unknownOption(option: string, allowPositionals: boolean): TypeError { + const suffix = allowPositionals + ? `. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- ${JSON.stringify(option)}` + : ""; + return codedError( + new TypeError(`Unknown option '${option}'${suffix}`), + "ERR_PARSE_ARGS_UNKNOWN_OPTION", + ); +} + +/** + * In strict mode, throw for possible usage errors like --foo --bar + * @param {object} token - from tokens as available from parseArgs + */ +function checkOptionLikeValue(token: OptionToken): void { + if (!token.inlineValue && isOptionLikeValue(token.value)) { + // Only show short example if user used short option. + const example = token.rawName.startsWith("--") + ? `'${token.rawName}=-XYZ'` + : `'--${token.name}=-XYZ' or '${token.rawName}-XYZ'`; + const errorMessage = `Option '${token.rawName}' argument is ambiguous. +Did you forget to specify the option argument for '${token.rawName}'? +To specify an option argument starting with a dash use ${example}.`; + throw invalidOption(errorMessage); + } +} + +/** + * In strict mode, throw for usage errors. + * @param {object} config - from config passed to parseArgs + * @param {object} token - from tokens as available from parseArgs + */ +function checkOptionUsage( + config: { + options: Options; + allowNegative: boolean; + allowPositionals: boolean; + }, + token: OptionToken, +): void { + let tokenName = token.name; + if (!Object.hasOwn(config.options, tokenName)) { + // Check for negated boolean option. + if (config.allowNegative && tokenName.startsWith("no-")) { + tokenName = tokenName.slice(3); + if ( + !Object.hasOwn(config.options, tokenName) || + optionsGetOwn(config.options, tokenName, "type") !== "boolean" + ) { + throw unknownOption(token.rawName, config.allowPositionals); + } + } else { + throw unknownOption(token.rawName, config.allowPositionals); + } + } + const short = optionsGetOwn(config.options, tokenName, "short"); + const shortAndLong = `${short ? `-${short}, ` : ""}--${tokenName}`; + const type = optionsGetOwn(config.options, tokenName, "type"); + if (type === "string" && typeof token.value !== "string") { + throw invalidOption(`Option '${shortAndLong} ' argument missing`); + } + // (Idiomatic test for undefined||null, expecting undefined.) + if (type === "boolean" && token.value != null) { + throw invalidOption(`Option '${shortAndLong}' does not take an argument`); + } +} + +/** + * Store the option value in `values`. + * @param {object} token - from tokens as available from parseArgs + * @param {object} options - option configs, from parseArgs({ options }) + * @param {object} values - option values returned in `values` by parseArgs + * @param {boolean} allowNegative - allow negative optinons if true + */ +function storeOption( + token: OptionToken, + options: Options, + values: Values, + allowNegative: boolean, +): void { + let longOption = token.name; + let optionValue: string | boolean | undefined = token.value; + if (longOption === "__proto__") { + return; // No. Just no. + } + if (allowNegative && longOption.startsWith("no-") && optionValue === undefined) { + // Boolean option negation: --no-foo + longOption = longOption.slice(3); + token.name = longOption; + optionValue = false; + } + // We store based on the option value rather than option type, + // preserving the users intent for author to deal with. + const newValue = optionValue ?? true; + if (optionsGetOwn(options, longOption, "multiple")) { + // Always store value in array, including for boolean. + // values[longOption] starts out not present, + // first value is added as new array [newValue], + // subsequent values are pushed to existing array. + // (note: values has null prototype, so simpler usage) + if (values[longOption]) { + (values[longOption] as (string | boolean)[]).push(newValue); + } else { + values[longOption] = [newValue]; + } + } else { + values[longOption] = newValue; + } +} + +/** + * Store the default option value in `values`. + * @param {string} longOption - long option name e.g. 'foo' + * @param {string + * | boolean + * | string[] + * | boolean[]} optionValue - default value from option config + * @param {object} values - option values returned in `values` by parseArgs + */ +function storeDefaultOption(longOption: string, optionValue: OptionValue, values: Values): void { + if (longOption === "__proto__") { + return; // No. Just no. + } + values[longOption] = optionValue; +} + +/** + * Process args and turn into identified tokens: + * - option (along with value, if any) + * - positional + * - option-terminator + * @param {string[]} args - from parseArgs({ args }) or mainArgs + * @param {object} options - option configs, from parseArgs({ options }) + * @returns Parsed argument tokens. + */ +function argsToTokens(args: string[], options: Options): ParseArgsToken[] { + const tokens: ParseArgsToken[] = []; + let index = -1; + let groupCount = 0; + const remainingArgs = args.slice(); + while (remainingArgs.length > 0) { + const arg = remainingArgs.shift()!; + const nextArg = remainingArgs[0]; + if (groupCount > 0) { + groupCount--; + } else { + index++; + } + // Check if `arg` is an options terminator. + // Guideline 10 in https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html + if (arg === "--") { + // Everything after a bare '--' is considered a positional argument. + tokens.push({ kind: "option-terminator", index }); + tokens.push( + ...remainingArgs.map((arg): ParseArgsToken => { + return { kind: "positional", index: ++index, value: arg }; + }), + ); + break; // Finished processing args, leave while loop. + } + if (isLoneShortOption(arg)) { + // e.g. '-f' + const shortOption = arg.charAt(1); + const longOption = findLongOptionForShort(shortOption, options); + let value; + let inlineValue; + if (optionsGetOwn(options, longOption, "type") === "string" && isOptionValue(nextArg)) { + // e.g. '-f', 'bar' + value = remainingArgs.shift(); + inlineValue = false; + } + tokens.push({ kind: "option", name: longOption, rawName: arg, index, value, inlineValue }); + if (value != null) { + ++index; + } + continue; + } + if (isShortOptionGroup(arg, options)) { + // Expand -fXzy to -f -X -z -y + const expanded: string[] = []; + for (let index = 1; index < arg.length; index++) { + const shortOption = arg.charAt(index); + const longOption = findLongOptionForShort(shortOption, options); + if (optionsGetOwn(options, longOption, "type") !== "string" || index === arg.length - 1) { + // Boolean option, or last short in group. Well formed. + expanded.push(`-${shortOption}`); + } else { + // String option in middle. Yuck. + // Expand -abfFILE to -a -b -fFILE + expanded.push(`-${arg.slice(index)}`); + break; // finished short group + } + } + remainingArgs.unshift(...expanded); + groupCount = expanded.length; + continue; + } + if (isShortOptionAndValue(arg, options)) { + // e.g. -fFILE + const shortOption = arg.charAt(1); + const longOption = findLongOptionForShort(shortOption, options); + const value = arg.slice(2); + tokens.push({ + kind: "option", + name: longOption, + rawName: `-${shortOption}`, + index, + value, + inlineValue: true, + }); + continue; + } + if (isLoneLongOption(arg)) { + // e.g. '--foo' + const longOption = arg.slice(2); + let value; + let inlineValue; + if (optionsGetOwn(options, longOption, "type") === "string" && isOptionValue(nextArg)) { + // e.g. '--foo', 'bar' + value = remainingArgs.shift(); + inlineValue = false; + } + tokens.push({ kind: "option", name: longOption, rawName: arg, index, value, inlineValue }); + if (value != null) { + ++index; + } + continue; + } + if (isLongOptionAndValue(arg)) { + // e.g. --foo=bar + const equalIndex = arg.indexOf("="); + const longOption = arg.slice(2, equalIndex); + const value = arg.slice(equalIndex + 1); + tokens.push({ + kind: "option", + name: longOption, + rawName: `--${longOption}`, + index, + value, + inlineValue: true, + }); + continue; + } + tokens.push({ kind: "positional", index, value: arg }); + } + return tokens; +} + +export function parseArgs(config: T): ParsedResults; + +export function parseArgs(config?: ParseArgsConfig): Result; + +export function parseArgs(config: ParseArgsConfig = {}): Result { + const args = objectGetOwn(config, "args"); + if (args == null) { + throw unsupportedNodeApi("util.parseArgs without args", "pass guest arguments explicitly"); + } + const strict = objectGetOwn(config, "strict") ?? true; + const allowPositionals = objectGetOwn(config, "allowPositionals") ?? !strict; + const returnTokens = objectGetOwn(config, "tokens") ?? false; + const allowNegative = objectGetOwn(config, "allowNegative") ?? false; + const options = objectGetOwn(config, "options") ?? (Object.create(null) as Options); + // Bundle these up for passing to strict-mode checks. + const parseConfig = { args, strict, options, allowPositionals, allowNegative }; + // Validate input configuration. + validateArray(args, "args"); + validateBoolean(strict, "strict"); + validateBoolean(allowPositionals, "allowPositionals"); + validateBoolean(returnTokens, "tokens"); + validateBoolean(allowNegative, "allowNegative"); + validateObject(options, "options"); + Object.entries(options).forEach(({ 0: longOption, 1: optionConfig }) => { + validateObject(optionConfig, `options.${longOption}`); + // type is required + const optionType = objectGetOwn(optionConfig, "type"); + validateUnion(optionType, `options.${longOption}.type`, ["string", "boolean"]); + if (Object.hasOwn(optionConfig, "short")) { + const shortOption = optionConfig.short; + validateString(shortOption, `options.${longOption}.short`); + if (shortOption.length !== 1) { + throw invalidArgValue( + `options.${longOption}.short`, + shortOption, + "must be a single character", + ); + } + } + const multipleOption = objectGetOwn(optionConfig, "multiple"); + if (Object.hasOwn(optionConfig, "multiple")) { + validateBoolean(multipleOption, `options.${longOption}.multiple`); + } + const defaultValue = objectGetOwn(optionConfig, "default"); + if (defaultValue !== undefined) { + let validator: ((value: unknown, name: string) => void) | undefined; + switch (optionType) { + case "string": + validator = multipleOption ? validateStringArray : validateString; + break; + case "boolean": + validator = multipleOption ? validateBooleanArray : validateBoolean; + break; + } + validator!(defaultValue, `options.${longOption}.default`); + } + }); + // Phase 1: identify tokens + const tokens = argsToTokens(args, options); + // Phase 2: process tokens into parsed option values and positionals + const result: Result = { + values: Object.create(null) as Values, + positionals: [], + }; + if (returnTokens) { + result.tokens = tokens; + } + tokens.forEach((token) => { + if (token.kind === "option") { + if (strict) { + checkOptionUsage(parseConfig, token); + checkOptionLikeValue(token); + } + storeOption(token, options, result.values, parseConfig.allowNegative); + } else if (token.kind === "positional") { + if (!allowPositionals) { + throw codedError( + new TypeError( + `Unexpected argument '${token.value}'. This command does not take positional arguments`, + ), + "ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL", + ); + } + result.positionals.push(token.value); + } + }); + // Phase 3: fill in default values for missing args + Object.entries(options).forEach(({ 0: longOption, 1: optionConfig }) => { + const mustSetDefault = useDefaultValueOption(longOption, optionConfig, result.values); + if (mustSetDefault) { + storeDefaultOption(longOption, objectGetOwn(optionConfig, "default")!, result.values); + } + }); + return result; +} diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/parse-env.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/parse-env.ts new file mode 100644 index 000000000..392c33d03 --- /dev/null +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/parse-env.ts @@ -0,0 +1,99 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Adapted from nodejs/node v24.20.0, commit +// 71b8b174857e25106d39b61a9e6f30d927da8b01, src/node_dotenv.cc, Dotenv::ParseContent. +// Local changes: explicit TypeScript contracts, shared Jco errors, ECMAScript intrinsics. + +import { invalidArgType } from "../errors/core.js"; + +const trim = (value: string): string => value.replace(/^[ \t\n]+|[ \t\n]+$/g, ""); + +export function parseEnv(input: string): Record { + if (typeof input !== "string") { + throw invalidArgType("content", "string", input); + } + let content = trim(input.replaceAll("\r", "")); + const entries = new Map(); + while (content.length) { + if (content[0] === "\n" || content[0] === "#") { + const newline = content.indexOf("\n"); + content = newline === -1 ? "" : content.slice(newline + 1); + continue; + } + const equal = content.search(/[=\n]/); + if (equal === -1) { + break; + } + if (content[equal] === "\n") { + content = trim(content.slice(equal + 1)); + continue; + } + let key = trim(content.slice(0, equal)); + content = content.slice(equal + 1); + if (!content.length || content[0] === "\n") { + entries.set(key, ""); + continue; + } + content = trim(content); + if (!key) { + continue; + } + if (key.startsWith("export ")) { + key = trim(key.slice(7)); + } + if (!content.length) { + entries.set(key, ""); + break; + } + const quote = content[0]; + if (quote === '"' || quote === "'" || quote === "`") { + const closing = content.indexOf(quote, 1); + if (closing !== -1) { + let value = content.slice(1, closing); + if (quote === '"') { + value = value.replaceAll("\\n", "\n"); + } + entries.set(key, value); + const newline = content.indexOf("\n", closing + 1); + content = newline === -1 ? "" : content.slice(newline + 1); + continue; + } + const newline = content.indexOf("\n"); + entries.set(key, newline === -1 ? content : content.slice(0, newline)); + content = newline === -1 ? "" : content.slice(newline + 1); + } else { + const newline = content.indexOf("\n"); + let value = newline === -1 ? content : content.slice(0, newline); + const hash = value.indexOf("#"); + if (hash !== -1) { + value = value.slice(0, hash); + } + entries.set(key, trim(value)); + content = newline === -1 ? "" : content.slice(newline + 1); + } + content = trim(content); + } + // Node's native binding assigns into an ordinary object; a string assigned + // to its inherited __proto__ setter does not create an own property. + entries.delete("__proto__"); + return Object.fromEntries(entries); +} diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/text.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/text.ts new file mode 100644 index 000000000..9e358226b --- /dev/null +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/text.ts @@ -0,0 +1,246 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Adapted from nodejs/node v24.20.0, commit +// 71b8b174857e25106d39b61a9e6f30d927da8b01, lib/util.js and lib/internal/util/inspect.js. +// Local changes: explicit TypeScript contracts, shared Jco errors, ECMAScript intrinsics. + +import { + invalidArgType, + invalidArgValue, + unsupportedNodeApi, + validateObject, +} from "../errors/core.js"; +import { inspect } from "./inspect.js"; + +export const colors: Record = { + reset: [0, 0], + bold: [1, 22], + dim: [2, 22], // Alias: faint + italic: [3, 23], + underline: [4, 24], + blink: [5, 25], + // Swap foreground and background colors + inverse: [7, 27], // Alias: swapcolors, swapColors + hidden: [8, 28], // Alias: conceal + strikethrough: [9, 29], // Alias: strikeThrough, crossedout, crossedOut + doubleunderline: [21, 24], // Alias: doubleUnderline + black: [30, 39], + red: [31, 39], + green: [32, 39], + yellow: [33, 39], + blue: [34, 39], + magenta: [35, 39], + cyan: [36, 39], + white: [37, 39], + bgBlack: [40, 49], + bgRed: [41, 49], + bgGreen: [42, 49], + bgYellow: [43, 49], + bgBlue: [44, 49], + bgMagenta: [45, 49], + bgCyan: [46, 49], + bgWhite: [47, 49], + framed: [51, 54], + overlined: [53, 55], + gray: [90, 39], // Alias: grey, blackBright + redBright: [91, 39], + greenBright: [92, 39], + yellowBright: [93, 39], + blueBright: [94, 39], + magentaBright: [95, 39], + cyanBright: [96, 39], + whiteBright: [97, 39], + bgGray: [100, 49], // Alias: bgGrey, bgBlackBright + bgRedBright: [101, 49], + bgGreenBright: [102, 49], + bgYellowBright: [103, 49], + bgBlueBright: [104, 49], + bgMagentaBright: [105, 49], + bgCyanBright: [106, 49], + bgWhiteBright: [107, 49], +}; + +Object.setPrototypeOf(colors, null); + +const ansi = new RegExp( + "[\\u001B\\u009B][[\\]()#;?]*" + + "(?:(?:(?:(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]+)*" + + "|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]*)*)?" + + "(?:\\u0007|\\u001B\\u005C|\\u009C))" + + "|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?" + + "[\\dA-PR-TZcf-nq-uy=><~]))", + "g", +); +for (const [alias, canonical] of Object.entries({ + grey: "gray", + blackBright: "gray", + bgGrey: "bgGray", + bgBlackBright: "bgGray", + faint: "dim", + swapcolors: "inverse", + swapColors: "inverse", + conceal: "hidden", + strikeThrough: "strikethrough", + crossedout: "strikethrough", + crossedOut: "strikethrough", + doubleUnderline: "doubleunderline", +})) { + Object.defineProperty(colors, alias, { + enumerable: false, + configurable: true, + + get: (): [number, number] => colors[canonical], + + set: (value: [number, number]): void => { + colors[canonical] = value; + }, + }); +} +inspect.colors = colors; + +export function stripVTControlCharacters(str: string): string { + if (typeof str !== "string") { + throw invalidArgType("str", "string", str); + } + return str.replace(ansi, ""); +} + +export function toUSVString(input: unknown): string { + const text = `${input}`; + if (typeof text.toWellFormed === "function") { + return text.toWellFormed(); + } + let result = ""; + for (let i = 0; i < text.length; i++) { + const code = text.charCodeAt(i); + if (code >= 0xd800 && code <= 0xdbff) { + const next = text.charCodeAt(i + 1); + if (next >= 0xdc00 && next <= 0xdfff) { + result += text[i] + text[++i]; + continue; + } + result += "\ufffd"; + } else { + result += code >= 0xdc00 && code <= 0xdfff ? "\ufffd" : text[i]; + } + } + return result; +} + +export interface StyleTextOptions { + validateStream?: boolean; + stream?: + | { isTTY?: boolean; write?: (...args: never[]) => unknown; getColorDepth?: () => number } + | ReadableStream + | WritableStream; +} + +function replaceClose(text: string, close: string, open: string, keepClose: boolean): string { + let start = 0, + result = ""; + for (let index = text.indexOf(close); index !== -1; index = text.indexOf(close, start)) { + const after = index + close.length; + if (after >= text.length) { + break; + } + result += text.slice(start, index) + (keepClose ? close + open : open); + start = after; + } + return result + text.slice(start); +} + +export function styleText( + format: string | string[], + text: string, + options?: StyleTextOptions, +): string { + if (typeof text !== "string") { + throw invalidArgType("text", "string", text); + } + if (options !== undefined) { + validateObject(options, "options"); + } + const validateStream = options?.validateStream ?? true; + if (typeof validateStream !== "boolean") { + throw invalidArgType("options.validateStream", "boolean", validateStream); + } + let skip = false; + if (validateStream) { + const stream = options?.stream; + if (!stream) { + throw unsupportedNodeApi( + "util.styleText default stream", + "supply a stream or validateStream: false", + ); + } + if ( + typeof stream !== "object" || + (!("write" in stream) && !("getReader" in stream) && !("getWriter" in stream)) + ) { + throw invalidArgType("stream", ["ReadableStream", "WritableStream", "Stream"], stream); + } + skip = !("isTTY" in stream && stream.isTTY === true); + } + let openCodes = "", + closeCodes = "", + processed = text; + for (const key of Array.isArray(format) ? format : [format]) { + if (key === "none") { + continue; + } + let open: string, + close: string, + keep = false; + if (typeof key === "string" && key.startsWith("#")) { + if (!/^#(?:[a-f\d]{3}|[a-f\d]{6})$/i.test(key)) { + throw invalidArgValue("format", key, "must be a valid hex color (#RGB or #RRGGBB)"); + } + const hex = + key.length === 4 + ? key + .slice(1) + .split("") + .map((c) => c + c) + .join("") + : key.slice(1); + const rgb = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16)); + open = `\x1b[38;2;${rgb.join(";")}m`; + close = "\x1b[39m"; + } else { + const codes = colors[key]; + if (!codes) { + throw invalidArgValue( + "format", + key, + `must be one of: ${Object.getOwnPropertyNames(colors).join(", ")}`, + ); + } + open = `\x1b[${codes[0]}m`; + close = `\x1b[${codes[1]}m`; + keep = codes[0] === 1 || codes[0] === 2; + } + openCodes += open; + closeCodes = close + closeCodes; + processed = replaceClose(processed, close, open, keep); + } + return skip ? text : openCodes + processed + closeCodes; +} diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/types.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/types.ts new file mode 100644 index 000000000..4e5a3bc0d --- /dev/null +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/types.ts @@ -0,0 +1,251 @@ +// Node.js v24.20.0 lib/internal/util/types.js, commit +// 71b8b174857e25106d39b61a9e6f30d927da8b01 (MIT; see ../util/mime.ts). +// Adapted typed-array getters; native bindings use portable intrinsic brand checks. +// Tag-only checks for arguments, generators and iterators cannot resist tag spoofing. +import { unsupportedNodeApi } from "../errors/core.js"; + +type TypedArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float16Array + | Float32Array + | Float64Array + | BigInt64Array + | BigUint64Array; + +function brand( + value: unknown, + intrinsic: ((...args: never[]) => unknown) | undefined, + args: unknown[] = [], +): boolean { + if (!intrinsic || value === null || (typeof value !== "object" && typeof value !== "function")) { + return false; + } + try { + Reflect.apply(intrinsic, value, args); + return true; + } catch { + return false; + } +} + +function getter(prototype: object | undefined, key: PropertyKey): (() => unknown) | undefined { + return prototype && Object.getOwnPropertyDescriptor(prototype, key)?.get; +} + +const typedArrayTag = getter(Object.getPrototypeOf(Uint8Array.prototype), Symbol.toStringTag)!; + +function arrayTag(value: unknown): unknown { + return Reflect.apply(typedArrayTag, value, []); +} + +function tag(value: unknown, name: string): boolean { + return Object.prototype.toString.call(value) === `[object ${name}]`; +} + +export const isArrayBufferView: typeof ArrayBuffer.isView = ArrayBuffer.isView; + +export function isArrayBuffer(value: unknown): value is ArrayBuffer { + return brand(value, getter(ArrayBuffer.prototype, "byteLength")); +} + +export function isSharedArrayBuffer(value: unknown): value is SharedArrayBuffer { + return brand(value, getter(globalThis.SharedArrayBuffer?.prototype, "byteLength")); +} + +export function isAnyArrayBuffer(value: unknown): value is ArrayBuffer | SharedArrayBuffer { + return isArrayBuffer(value) || isSharedArrayBuffer(value); +} + +export function isDataView(value: unknown): value is DataView { + return ArrayBuffer.isView(value) && arrayTag(value) === undefined; +} + +export function isTypedArray(value: unknown): value is TypedArray { + return arrayTag(value) !== undefined; +} + +export function isInt8Array(value: unknown): value is Int8Array { + return arrayTag(value) === "Int8Array"; +} + +export function isUint8Array(value: unknown): value is Uint8Array { + return arrayTag(value) === "Uint8Array"; +} + +export function isUint8ClampedArray(value: unknown): value is Uint8ClampedArray { + return arrayTag(value) === "Uint8ClampedArray"; +} + +export function isInt16Array(value: unknown): value is Int16Array { + return arrayTag(value) === "Int16Array"; +} + +export function isUint16Array(value: unknown): value is Uint16Array { + return arrayTag(value) === "Uint16Array"; +} + +export function isInt32Array(value: unknown): value is Int32Array { + return arrayTag(value) === "Int32Array"; +} + +export function isUint32Array(value: unknown): value is Uint32Array { + return arrayTag(value) === "Uint32Array"; +} + +export function isFloat16Array(value: unknown): value is Float16Array { + return arrayTag(value) === "Float16Array"; +} + +export function isFloat32Array(value: unknown): value is Float32Array { + return arrayTag(value) === "Float32Array"; +} + +export function isFloat64Array(value: unknown): value is Float64Array { + return arrayTag(value) === "Float64Array"; +} + +export function isBigInt64Array(value: unknown): value is BigInt64Array { + return arrayTag(value) === "BigInt64Array"; +} + +export function isBigUint64Array(value: unknown): value is BigUint64Array { + return arrayTag(value) === "BigUint64Array"; +} + +export function isBooleanObject(value: unknown): value is object & { valueOf(): boolean } { + return brand(value, Boolean.prototype.valueOf); +} + +export function isNumberObject(value: unknown): value is object & { valueOf(): number } { + return brand(value, Number.prototype.valueOf); +} + +export function isStringObject(value: unknown): value is object & { valueOf(): string } { + return brand(value, String.prototype.valueOf); +} + +export function isSymbolObject(value: unknown): value is object & { valueOf(): symbol } { + return brand(value, Symbol.prototype.valueOf); +} + +export function isBigIntObject(value: unknown): value is object & { valueOf(): bigint } { + return brand(value, BigInt.prototype.valueOf); +} + +export function isBoxedPrimitive( + value: unknown, +): value is object & { valueOf(): boolean | number | string | symbol | bigint } { + return ( + isBooleanObject(value) || + isNumberObject(value) || + isStringObject(value) || + isSymbolObject(value) || + isBigIntObject(value) + ); +} + +export function isDate(value: unknown): value is Date { + return brand(value, Date.prototype.getTime); +} + +export function isRegExp(value: unknown): value is RegExp { + return brand(value, getter(RegExp.prototype, "source")); +} + +export function isMap(value: unknown): value is Map { + return brand(value, getter(Map.prototype, "size")); +} + +export function isSet(value: unknown): value is Set { + return brand(value, getter(Set.prototype, "size")); +} + +export function isWeakMap(value: unknown): value is WeakMap { + return brand(value, WeakMap.prototype.has, [{}]); +} + +export function isWeakSet(value: unknown): value is WeakSet { + return brand(value, WeakSet.prototype.has, [{}]); +} + +export function isPromise(value: unknown): value is Promise { + return value instanceof Promise; +} + +export function isNativeError(value: unknown): value is Error { + return typeof Error.isError === "function" ? Error.isError(value) : tag(value, "Error"); +} + +export function isAsyncFunction( + value: unknown, +): value is (...args: unknown[]) => Promise | AsyncGenerator { + return ( + typeof value === "function" && + (tag(value, "AsyncFunction") || tag(value, "AsyncGeneratorFunction")) + ); +} + +export function isGeneratorFunction( + value: unknown, +): value is (...args: unknown[]) => Generator | AsyncGenerator { + return ( + typeof value === "function" && + (tag(value, "GeneratorFunction") || tag(value, "AsyncGeneratorFunction")) + ); +} + +export function isGeneratorObject( + value: unknown, +): value is Generator | AsyncGenerator { + return tag(value, "Generator") || tag(value, "AsyncGenerator"); +} + +export function isArgumentsObject(value: unknown): value is IArguments { + return tag(value, "Arguments"); +} + +export function isMapIterator(value: unknown): value is IterableIterator { + return tag(value, "Map Iterator"); +} + +export function isSetIterator(value: unknown): value is IterableIterator { + return tag(value, "Set Iterator"); +} + +export function isModuleNamespaceObject(value: unknown): value is Record { + return ( + typeof value === "object" && + value !== null && + Object.getPrototypeOf(value) === null && + !Object.isExtensible(value) && + tag(value, "Module") + ); +} + +export function isCryptoKey(value: unknown): value is CryptoKey { + return brand(value, getter(globalThis.CryptoKey?.prototype, "type")); +} + +export function isKeyObject(_value: unknown): never { + throw unsupportedNodeApi( + "util.types.isKeyObject", + "native key objects are not available in components", + ); +} + +export function isExternal(_value: unknown): never { + throw unsupportedNodeApi( + "util.types.isExternal", + "native external pointers cannot be inspected in components", + ); +} + +export function isProxy(_value: unknown): never { + throw unsupportedNodeApi("util.types.isProxy", "the engine does not expose proxy targets"); +} diff --git a/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/unsupported.ts b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/unsupported.ts new file mode 100644 index 000000000..7cdc05724 --- /dev/null +++ b/packages/jco-std/src/wasi/0.2.x/node/24.x.x/util/unsupported.ts @@ -0,0 +1,91 @@ +import { deprecatedNodeApi, unsupportedNodeApi } from "../errors/core.js"; + +export function debuglog( + _section: string, + _callback?: (fn: (...args: unknown[]) => void) => void, +): never { + throw unsupportedNodeApi( + "util.debuglog", + "Node debug environment and process logging are not available", + ); +} + +export const debug: typeof debuglog = debuglog; + +export function deprecate unknown>( + _fn: T, + _message: string, + _code?: string, + _options?: { modifyPrototype?: boolean }, +): never { + throw unsupportedNodeApi("util.deprecate", "Node process warning policy is not available"); +} + +export function getCallSites( + _frameCount?: number | { sourceMap?: boolean }, + _options?: { sourceMap?: boolean }, +): never { + throw unsupportedNodeApi( + "util.getCallSites", + "native call sites and source maps are not exposed by component engines", + ); +} + +export function getSystemErrorName(_error: number): never { + throw unsupportedNodeApi("util.getSystemErrorName", "host libuv errno tables are not available"); +} + +export function getSystemErrorMessage(_error: number): never { + throw unsupportedNodeApi( + "util.getSystemErrorMessage", + "host libuv errno tables are not available", + ); +} + +export function getSystemErrorMap(): never { + throw unsupportedNodeApi("util.getSystemErrorMap", "host libuv errno tables are not available"); +} + +export function setTraceSigInt(_enable: boolean): never { + throw unsupportedNodeApi( + "util.setTraceSigInt", + "components cannot install host process signal handlers", + ); +} + +export function convertProcessSignalToExitCode(_signal: number | string): never { + throw unsupportedNodeApi( + "util.convertProcessSignalToExitCode", + "host process signals are not available", + ); +} + +export function transferableAbortController(): never { + throw unsupportedNodeApi( + "util.transferableAbortController", + "Node worker transfer hooks are not available", + ); +} + +export function transferableAbortSignal(_signal: AbortSignal): never { + throw unsupportedNodeApi( + "util.transferableAbortSignal", + "Node worker transfer hooks are not available", + ); +} + +export function _extend(_target: unknown, _source: unknown): never { + throw deprecatedNodeApi("util._extend", "Object.assign"); +} + +export function isArray(_value: unknown): never { + throw deprecatedNodeApi("util.isArray", "Array.isArray"); +} + +export function _errnoException(..._args: unknown[]): never { + throw deprecatedNodeApi("util._errnoException", "a public error API"); +} + +export function _exceptionWithHostPort(..._args: unknown[]): never { + throw deprecatedNodeApi("util._exceptionWithHostPort", "a public error API"); +} diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/helpers/util.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/helpers/util.ts new file mode 100644 index 000000000..18a6503da --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/helpers/util.ts @@ -0,0 +1,24 @@ +import native from "node:util"; +import util from "../../../../../../src/wasi/0.2.x/node/24.x.x/util/index.js"; +import { test as baseTest } from "vitest"; + +/** Differential fixtures target the exact source pin, independently of host CI majors. */ +export const test: ReturnType = baseTest.skipIf( + process.versions.node !== "24.20.0", +); +export { util, native }; + +export function capture(fn: () => unknown): unknown { + try { + return fn(); + } catch (error) { + if (!(error instanceof Error)) { + return { thrown: error }; + } + return { + name: error.name, + code: "code" in error ? error.code : undefined, + message: error.message, + }; + } +} diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/MIMEParams.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/MIMEParams.ts new file mode 100644 index 000000000..224482192 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/MIMEParams.ts @@ -0,0 +1,38 @@ +import { expect } from "vitest"; +import { util, native, test, capture } from "../helpers/util.js"; + +test("MIMEParams preserves case folding, serialization and live iterators", () => { + const report = (Ctor: typeof util.MIMEParams | typeof native.MIMEParams) => { + const p = new Ctor(); + p.set("X", "a;b"); + p.set("EMPTY", ""); + const iter = p.entries(); + const first = iter.next().value; + p.set("last", "v"); + p.delete("EMPTY"); + return [ + first, + [...iter], + p.get("x"), + p.has("X"), + p.get("missing"), + [...p.keys()], + [...p.values()], + String(p), + Reflect.apply(Reflect.get(p, "toJSON"), p, []), + ]; + }; + + expect(report(util.MIMEParams)).toEqual(report(native.MIMEParams)); + for (const [name, value] of [ + ["", "x"], + ["a b", "x"], + ["x", "\u0000"], + ]) { + expect(capture(() => new util.MIMEParams().set(name, value))).toEqual( + capture(() => new native.MIMEParams().set(name, value)), + ); + } + expect(util.MIMEParams.prototype[Symbol.iterator]).toBe(util.MIMEParams.prototype.entries); + expect(util.MIMEParams.prototype.toJSON).toBe(util.MIMEParams.prototype.toString); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/MIMEType.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/MIMEType.ts new file mode 100644 index 000000000..82152b31d --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/MIMEType.ts @@ -0,0 +1,38 @@ +import { expect } from "vitest"; +import { util, native, test, capture } from "../helpers/util.js"; + +test("MIMEType preserves parsing, setters, quoting and prototype identities", () => { + for (const input of [ + 'Text/HTML; Charset="utf-8"; foo="a;b";foo=no', + " application/json ", + 'text/plain;a="a\\b"', + "text/plain; a=; b=ok", + "bad", + "/plain", + "text/", + "text/pl ain", + ]) { + const report = (Ctor: typeof util.MIMEType | typeof native.MIMEType) => + capture(() => { + const mime = new Ctor(input); + return [ + mime.type, + mime.subtype, + mime.essence, + [...mime.params], + String(mime), + Reflect.apply(Reflect.get(mime, "toJSON"), mime, []), + ]; + }); + + expect(report(util.MIMEType)).toEqual(report(native.MIMEType)); + } + const mime = new util.MIMEType("text/plain"); + mime.type = "APPLICATION"; + mime.subtype = "JSON"; + expect(mime.essence).toBe("application/json"); + expect(Object.getOwnPropertyNames(util.MIMEType.prototype)).toEqual( + Object.getOwnPropertyNames(native.MIMEType.prototype), + ); + expect(util.MIMEType.prototype.toJSON).toBe(util.MIMEType.prototype.toString); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/TextDecoder.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/TextDecoder.ts new file mode 100644 index 000000000..877bfcdc2 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/TextDecoder.ts @@ -0,0 +1,10 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("TextDecoder reuses the engine constructor", () => { + expect(util.TextDecoder).toBe(globalThis.TextDecoder); + expect(new util.TextDecoder().decode(new Uint8Array([240, 159, 140, 141]))).toBe("🌍"); + expect(() => + new util.TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array([255])), + ).toThrow(TypeError); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/TextEncoder.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/TextEncoder.ts new file mode 100644 index 000000000..6761b4520 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/TextEncoder.ts @@ -0,0 +1,7 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("TextEncoder reuses the engine constructor", () => { + expect(util.TextEncoder).toBe(globalThis.TextEncoder); + expect(new util.TextEncoder().encode("🌍")).toEqual(new native.TextEncoder().encode("🌍")); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/_errnoException.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/_errnoException.ts new file mode 100644 index 000000000..2848ace07 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/_errnoException.ts @@ -0,0 +1,20 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("_errnoException refuses unavailable behavior before observing inputs", () => { + const trap = new Proxy( + {}, + { + get() { + throw Error("input touched"); + }, + + ownKeys() { + throw Error("input enumerated"); + }, + }, + ); + expect(() => Reflect.apply(util._errnoException, undefined, [trap])).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_DEPRECATED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/_exceptionWithHostPort.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/_exceptionWithHostPort.ts new file mode 100644 index 000000000..5b564d2aa --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/_exceptionWithHostPort.ts @@ -0,0 +1,20 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("_exceptionWithHostPort refuses unavailable behavior before observing inputs", () => { + const trap = new Proxy( + {}, + { + get() { + throw Error("input touched"); + }, + + ownKeys() { + throw Error("input enumerated"); + }, + }, + ); + expect(() => Reflect.apply(util._exceptionWithHostPort, undefined, [trap])).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_DEPRECATED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/_extend.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/_extend.ts new file mode 100644 index 000000000..ecc806616 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/_extend.ts @@ -0,0 +1,20 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("_extend refuses unavailable behavior before observing inputs", () => { + const trap = new Proxy( + {}, + { + get() { + throw Error("input touched"); + }, + + ownKeys() { + throw Error("input enumerated"); + }, + }, + ); + expect(() => Reflect.apply(util._extend, undefined, [trap, trap])).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_DEPRECATED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/aborted.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/aborted.ts new file mode 100644 index 000000000..a5abc1d4b --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/aborted.ts @@ -0,0 +1,14 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("aborted resolves on abort and validates resource before early resolution", async () => { + const controller = new AbortController(); + const resource = {}; + const promise = util.aborted(controller.signal, resource); + controller.abort(); + await promise; + await expect(util.aborted(controller.signal, null!)).rejects.toMatchObject({ + code: "ERR_INVALID_ARG_TYPE", + }); + await expect(util.aborted(controller.signal, {})).resolves.toBeUndefined(); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/callbackify.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/callbackify.ts new file mode 100644 index 000000000..c52eda75e --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/callbackify.ts @@ -0,0 +1,35 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("callbackify delivers results asynchronously and wraps falsy rejections", async () => { + for (const reason of [undefined, null, 0, ""]) { + const result = await new Promise((resolve) => { + util.callbackify(async () => { + throw reason; + })((error) => resolve(error)); + }); + expect(result).toMatchObject({ code: "ERR_FALSY_VALUE_REJECTION", reason }); + } + + const fn = async (value: number) => value + 1; + + const wrapped = util.callbackify(fn); + expect(wrapped.name).toBe("fnCallbackified"); + expect(wrapped.length).toBe(2); + const events: string[] = []; + await new Promise((resolve, reject) => { + wrapped(2, (error, value) => { + if (error) { + reject(error); + } + expect(value).toBe(3); + events.push("callback"); + resolve(); + }); + events.push("sync"); + }); + expect(events).toEqual(["sync", "callback"]); + expect(() => Reflect.apply(wrapped, undefined, [2, null])).toThrow( + expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/convertProcessSignalToExitCode.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/convertProcessSignalToExitCode.ts new file mode 100644 index 000000000..6407abe57 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/convertProcessSignalToExitCode.ts @@ -0,0 +1,20 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("convertProcessSignalToExitCode refuses unavailable behavior before observing inputs", () => { + const trap = new Proxy( + {}, + { + get() { + throw Error("input touched"); + }, + + ownKeys() { + throw Error("input enumerated"); + }, + }, + ); + expect(() => Reflect.apply(util.convertProcessSignalToExitCode, undefined, [trap])).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/debug.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/debug.ts new file mode 100644 index 000000000..7505245ac --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/debug.ts @@ -0,0 +1,8 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("debug refuses unavailable behavior before observing inputs", () => { + expect(() => Reflect.apply(util.debug, undefined, ["net"])).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/debuglog.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/debuglog.ts new file mode 100644 index 000000000..e78ea54be --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/debuglog.ts @@ -0,0 +1,13 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("debuglog refuses unavailable behavior before observing inputs", () => { + expect(() => + Reflect.apply(util.debuglog, undefined, [ + "net", + () => { + throw Error("callback touched"); + }, + ]), + ).toThrow(expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" })); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/deprecate.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/deprecate.ts new file mode 100644 index 000000000..a5d948a26 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/deprecate.ts @@ -0,0 +1,13 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("deprecate refuses unavailable behavior before observing inputs", () => { + expect(() => + Reflect.apply(util.deprecate, undefined, [ + () => { + throw Error("called"); + }, + "old", + ]), + ).toThrow(expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" })); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/diff.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/diff.ts new file mode 100644 index 000000000..d9653471d --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/diff.ts @@ -0,0 +1,23 @@ +import { expect } from "vitest"; +import { util, native, test, capture } from "../helpers/util.js"; + +test("diff preserves Node Myers tie-breaking and empty inputs", () => { + const cases: [string | string[], string | string[]][] = [ + ["abc", "adc"], + ["", "x"], + ["x", ""], + ["", ""], + [ + ["a", "b"], + ["b", "c"], + ], + [["a"], ["a"]], + ["🌍x", "🌍y"], + ]; + for (const [a, b] of cases) { + expect(util.diff(a, b)).toEqual(native.diff(a, b)); + } + expect(capture(() => Reflect.apply(util.diff, undefined, [1, "a"]))).toEqual( + capture(() => Reflect.apply(native.diff, undefined, [1, "a"])), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/format.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/format.ts new file mode 100644 index 000000000..677dd3679 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/format.ts @@ -0,0 +1,44 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("format follows substitutions and keeps unused percent sequences", () => { + const cases: unknown[][] = [ + [], + ["%%"], + ["%%", 1], + ["x %s %d %i %f", "a", 2, "2px", "3.5px"], + ["%s", 1n], + ["%j", { a: 1 }], + ["%o", [1]], + ["%O", { x: 1 }], + ["%cX", "css"], + [{ a: 1 }, "tail"], + ["%s", "x", "tail"], + ["%d", -0], + ]; + for (const args of cases) { + expect(util.format(...args)).toBe(native.format(...args)); + } +}); + +test("format applies numeric and custom string conversions", () => { + class Custom { + toString(): string { + return "custom"; + } + } + + const cases: unknown[][] = [ + ["%i", "0xff"], + ["%i", "-0"], + ["%f", "-0"], + ["%s", -0], + ["%s", new Custom()], + ["%s", { toString: () => "own" }], + ["%s", Object.create(null)], + ["%s", new Date(0)], + ]; + for (const args of cases) { + expect(util.format(...args)).toBe(native.format(...args)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/formatWithOptions.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/formatWithOptions.ts new file mode 100644 index 000000000..93d13eed7 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/formatWithOptions.ts @@ -0,0 +1,23 @@ +import { expect } from "vitest"; +import { util, native, test, capture } from "../helpers/util.js"; + +test("formatWithOptions passes inspection options through substitutions", () => { + for (const options of [{ colors: true }, { depth: 0 }, { sorted: true }]) { + expect(util.formatWithOptions(options, "%O", { z: 1, a: { b: 2 } })).toBe( + native.formatWithOptions(options, "%O", { z: 1, a: { b: 2 } }), + ); + } + expect(capture(() => Reflect.apply(util.formatWithOptions, undefined, [null, "x"]))).toEqual( + capture(() => Reflect.apply(native.formatWithOptions, undefined, [null, "x"])), + ); +}); + +test("formatWithOptions groups integers and fractions across numeric substitutions", () => { + for (const value of [12345678.12345, 12345678n, -0, 1e-9]) { + for (const placeholder of ["%s", "%d", "%i", "%f"]) { + expect(util.formatWithOptions({ numericSeparator: true }, placeholder, value)).toBe( + native.formatWithOptions({ numericSeparator: true }, placeholder, value), + ); + } + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/getCallSites.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/getCallSites.ts new file mode 100644 index 000000000..8b5c6b16c --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/getCallSites.ts @@ -0,0 +1,20 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("getCallSites refuses unavailable behavior before observing inputs", () => { + const trap = new Proxy( + {}, + { + get() { + throw Error("input touched"); + }, + + ownKeys() { + throw Error("input enumerated"); + }, + }, + ); + expect(() => Reflect.apply(util.getCallSites, undefined, [trap])).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/getSystemErrorMap.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/getSystemErrorMap.ts new file mode 100644 index 000000000..0b1ce8e74 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/getSystemErrorMap.ts @@ -0,0 +1,8 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("getSystemErrorMap refuses unavailable behavior before observing inputs", () => { + expect(() => Reflect.apply(util.getSystemErrorMap, undefined, [])).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/getSystemErrorMessage.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/getSystemErrorMessage.ts new file mode 100644 index 000000000..67517a1c8 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/getSystemErrorMessage.ts @@ -0,0 +1,20 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("getSystemErrorMessage refuses unavailable behavior before observing inputs", () => { + const trap = new Proxy( + {}, + { + get() { + throw Error("input touched"); + }, + + ownKeys() { + throw Error("input enumerated"); + }, + }, + ); + expect(() => Reflect.apply(util.getSystemErrorMessage, undefined, [trap])).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/getSystemErrorName.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/getSystemErrorName.ts new file mode 100644 index 000000000..7b332a46a --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/getSystemErrorName.ts @@ -0,0 +1,20 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("getSystemErrorName refuses unavailable behavior before observing inputs", () => { + const trap = new Proxy( + {}, + { + get() { + throw Error("input touched"); + }, + + ownKeys() { + throw Error("input enumerated"); + }, + }, + ); + expect(() => Reflect.apply(util.getSystemErrorName, undefined, [trap])).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/inherits.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/inherits.ts new file mode 100644 index 000000000..344e68814 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/inherits.ts @@ -0,0 +1,26 @@ +import { expect } from "vitest"; +import { util, native, test, capture } from "../helpers/util.js"; + +test("inherits preserves parent prototypes and static super_ descriptor", () => { + function Parent() {} + + function Child() {} + + util.inherits(Child, Parent); + expect(Object.getPrototypeOf(Child.prototype)).toBe(Parent.prototype); + expect(Object.getOwnPropertyDescriptor(Child, "super_")).toEqual({ + value: Parent, + writable: true, + enumerable: false, + configurable: true, + }); + for (const args of [ + [null, Parent], + [Child, null], + [Child, () => {}], + ]) { + expect(capture(() => Reflect.apply(util.inherits, undefined, args))).toEqual( + capture(() => Reflect.apply(native.inherits, undefined, args)), + ); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/inspect.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/inspect.ts new file mode 100644 index 000000000..58a05f1b2 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/inspect.ts @@ -0,0 +1,85 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("inspect supports descriptors, custom hooks, limits and circular references", () => { + const cycle: { self?: unknown } = {}; + cycle.self = cycle; + const custom = { + [util.inspect.custom]() { + return { custom: true }; + }, + }; + const values: unknown[] = [ + undefined, + null, + {}, + [], + [1, , 3], + { x: 1, s: "a" }, + cycle, + custom, + new Map([["a", 1]]), + new Set([1, 2]), + { + get safe() { + throw Error("not evaluated"); + }, + }, + Object(1), + Object("ab"), + ]; + for (const value of values) { + expect(util.inspect(value)).toBe(native.inspect(value)); + } + for (const options of [ + { sorted: true }, + { showHidden: true }, + { depth: 0 }, + { compact: false }, + { colors: true }, + { maxArrayLength: 1 }, + ]) { + expect(util.inspect({ z: 1, a: [1, 2] }, options)).toBe( + native.inspect({ z: 1, a: [1, 2] }, options), + ); + } + expect(() => util.inspect({}, { showProxy: true })).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" }), + ); +}); + +test("inspect.defaultOptions merges updates and affects subsequent calls", () => { + const before = { ...util.inspect.defaultOptions }; + try { + util.inspect.defaultOptions = { depth: 0 }; + expect(util.inspect({ a: { b: 1 } })).toBe("{ a: [Object] }"); + } finally { + util.inspect.defaultOptions = before; + } +}); + +test("inspect renders buffers, views and numeric separators", () => { + for (const value of [ + new ArrayBuffer(0), + new ArrayBuffer(2), + new DataView(new ArrayBuffer(2)), + Object(false), + Object(1n), + Object(Symbol("x")), + 12345.12345, + -0, + 1e30, + 1e-9, + 123456789n, + ]) { + for (const options of [ + {}, + { depth: 0 }, + { maxArrayLength: 1 }, + { colors: true }, + { numericSeparator: true }, + ]) { + expect(util.inspect(value, options)).toBe(native.inspect(value, options)); + } + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/isArray.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/isArray.ts new file mode 100644 index 000000000..9c09cc6ac --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/isArray.ts @@ -0,0 +1,20 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("isArray refuses unavailable behavior before observing inputs", () => { + const trap = new Proxy( + {}, + { + get() { + throw Error("input touched"); + }, + + ownKeys() { + throw Error("input enumerated"); + }, + }, + ); + expect(() => Reflect.apply(util.isArray, undefined, [trap])).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_DEPRECATED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/isDeepStrictEqual.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/isDeepStrictEqual.ts new file mode 100644 index 000000000..fe6283ebd --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/isDeepStrictEqual.ts @@ -0,0 +1,32 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("isDeepStrictEqual shares assertion comparison semantics", () => { + class A { + x = 1; + } + + class B { + x = 1; + } + + for (const [a, b] of [ + [new A(), new B()], + [new Map([[1, { x: 2 }]]), new Map([[1, { x: 2 }]])], + [NaN, NaN], + [-0, 0], + [{ x: [1] }, { x: [2] }], + ]) { + // The pinned runtime takes a boolean; installed Node types describe the later options object. + for (const skip of [false, true]) { + expect(util.isDeepStrictEqual(a, b, skip)).toBe( + Reflect.apply(native.isDeepStrictEqual, undefined, [a, b, skip]), + ); + } + } + const a: { self?: unknown } = {}; + a.self = a; + const b: { self?: unknown } = {}; + b.self = b; + expect(util.isDeepStrictEqual(a, b)).toBe(true); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/module.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/module.ts new file mode 100644 index 000000000..cdbd78bcf --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/module.ts @@ -0,0 +1,31 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +import * as namespace from "../../../../../../src/wasi/0.2.x/node/24.x.x/util/index.js"; +import * as typeNamespace from "../../../../../../src/wasi/0.2.x/node/24.x.x/util-types.js"; + +test("matches the complete module and predicate namespaces", () => { + expect(Object.keys(util).sort()).toEqual(Object.keys(native).sort()); + expect(Object.keys(namespace).sort()).toEqual([...Object.keys(native), "default"].sort()); + expect(Object.keys(util.types).sort()).toEqual(Object.keys(native.types).sort()); + expect(Object.keys(typeNamespace).sort()).toEqual( + [...Object.keys(native.types), "default"].sort(), + ); + expect(typeNamespace.default).toBe(util.types); + for (const key of Object.keys(util) as (keyof typeof util)[]) { + expect(namespace[key]).toBe(util[key]); + } + expect(util.debug).toBe(util.debuglog); + expect(util.inspect.custom).toBe(Symbol.for("nodejs.util.inspect.custom")); + expect(util.promisify.custom).toBe(Symbol.for("nodejs.util.promisify.custom")); + for (const key of Object.keys(util.types) as (keyof typeof util.types)[]) { + expect(typeNamespace[key]).toBe(util.types[key]); + const actual = Object.getOwnPropertyDescriptor(util.types, key)!; + const expected = Object.getOwnPropertyDescriptor(native.types, key)!; + expect([actual.enumerable, actual.writable, actual.configurable]).toEqual([ + expected.enumerable, + expected.writable, + expected.configurable, + ]); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/parseArgs.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/parseArgs.ts new file mode 100644 index 000000000..89476cb05 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/parseArgs.ts @@ -0,0 +1,40 @@ +import { expect } from "vitest"; +import { util, native, test, capture } from "../helpers/util.js"; + +test("parseArgs matches short groups, negation, defaults, positionals and tokens", () => { + const options = { + verbose: { type: "boolean", short: "v", multiple: true }, + file: { type: "string", short: "f" }, + color: { type: "boolean", default: true }, + } as const; + for (const args of [ + [], + ["-vvfinput", "rest"], + ["--no-color", "--file=x"], + ["--", "-x"], + ["--unknown"], + ["--file", "-v"], + ["--color=false"], + ]) { + const config = { args, options, allowPositionals: true, allowNegative: true, tokens: true }; + expect(capture(() => util.parseArgs(config))).toEqual(capture(() => native.parseArgs(config))); + } + expect( + util.parseArgs({ args: [], options: { name: { type: "string", default: "safe" } } }).values + .name, + ).toBe("safe"); + expect(Object.getPrototypeOf(util.parseArgs({ args: [] }).values)).toBeNull(); + expect(() => util.parseArgs()).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" }), + ); +}); + +test("parseArgs accepts unknown options and string values in non-strict mode", () => { + const config = { + args: ["--unknown=value", "--flag=text"], + strict: false, + options: { flag: { type: "boolean" } }, + } as const; + const input = { ...config, args: [...config.args] }; + expect(util.parseArgs(input)).toEqual(native.parseArgs(input)); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/parseEnv.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/parseEnv.ts new file mode 100644 index 000000000..85ffc9705 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/parseEnv.ts @@ -0,0 +1,20 @@ +import { expect } from "vitest"; +import { util, native, test, capture } from "../helpers/util.js"; + +test("parseEnv matches whitespace, quoting, duplicate keys and invalid lines", () => { + for (const source of [ + "", + "A=1\nA=2", + "export FOO = \"a\\nb\"\nQ='a#b'\nT=`multi\nline`", + "# comment\r\nA=hello # rest\ninvalid\nB=yes", + "__proto__=safe\nconstructor=ok", + " export X=\nY= ", + 'A="unclosed\nB=v', + "=value\nX=y", + ]) { + expect(util.parseEnv(source)).toEqual(native.parseEnv(source)); + } + expect(capture(() => Reflect.apply(util.parseEnv, undefined, [null]))).toEqual( + capture(() => Reflect.apply(native.parseEnv, undefined, [null])), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/promisify.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/promisify.ts new file mode 100644 index 000000000..5fbe1c690 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/promisify.ts @@ -0,0 +1,29 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("promisify preserves this, custom hooks and callback results", async () => { + const original = function ( + this: { base: number }, + value: number, + callback: (error: unknown, result?: number) => void, + ): void { + callback(null, this.base + value); + }; + + const wrapped = util.promisify(original); + expect(await wrapped.call({ base: 2 }, 3)).toBe(5); + expect(wrapped.name).toBe(original.name); + expect(wrapped.length).toBe(original.length); + + const custom = async () => 7; + + const fn = Object.assign(() => {}, { [util.promisify.custom]: custom }); + expect(util.promisify(fn)).toBe(custom); + expect(util.promisify(custom)).toBe(custom); + expect(() => util.promisify(async () => 1)).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_DEPRECATED_NODE_API" }), + ); + await expect( + util.promisify((cb: (error: unknown) => void) => cb(new Error("oops")))(), + ).rejects.toThrow("oops"); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/setTraceSigInt.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/setTraceSigInt.ts new file mode 100644 index 000000000..12a129e6a --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/setTraceSigInt.ts @@ -0,0 +1,20 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("setTraceSigInt refuses unavailable behavior before observing inputs", () => { + const trap = new Proxy( + {}, + { + get() { + throw Error("input touched"); + }, + + ownKeys() { + throw Error("input enumerated"); + }, + }, + ); + expect(() => Reflect.apply(util.setTraceSigInt, undefined, [trap])).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/stripVTControlCharacters.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/stripVTControlCharacters.ts new file mode 100644 index 000000000..6b3b0062a --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/stripVTControlCharacters.ts @@ -0,0 +1,16 @@ +import { expect } from "vitest"; +import { util, native, test, capture } from "../helpers/util.js"; + +test("stripVTControlCharacters handles ANSI and OSC sequences", () => { + for (const value of [ + "plain", + "\x1b[31mred\x1b[0m", + "\x1b]8;;https://example.com\x07label\x1b]8;;\x07", + "\x1b[2K🌍", + ]) { + expect(util.stripVTControlCharacters(value)).toBe(native.stripVTControlCharacters(value)); + } + expect(capture(() => Reflect.apply(util.stripVTControlCharacters, undefined, [1]))).toEqual( + capture(() => Reflect.apply(native.stripVTControlCharacters, undefined, [1])), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/styleText.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/styleText.ts new file mode 100644 index 000000000..6bb8c7463 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/styleText.ts @@ -0,0 +1,38 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("styleText preserves nested close codes, hex colors and explicit streams", () => { + for (const style of ["red", ["bold", "red"], "none", "#a0f", "#123456", "grey"]) { + for (const text of ["hello", "a\x1b[39mb", "a\x1b[39m"]) { + expect(util.styleText(style, text, { validateStream: false })).toBe( + Reflect.apply(native.styleText, undefined, [style, text, { validateStream: false }]), + ); + } + } + expect(util.styleText("red", "plain", { stream: { write() {}, isTTY: false } })).toBe("plain"); + expect(() => util.styleText("red", "text")).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" }), + ); + expect(() => util.styleText("#not-hex", "text", { validateStream: false })).toThrow( + expect.objectContaining({ code: "ERR_INVALID_ARG_VALUE" }), + ); +}); + +test("styleText accepts all Node color aliases", () => { + expect(Object.getOwnPropertyNames(util.inspect.colors).sort()).toEqual( + Object.getOwnPropertyNames(native.inspect.colors).sort(), + ); + for (const color of Object.getOwnPropertyNames(native.inspect.colors)) { + expect(util.styleText(color, "x", { validateStream: false })).toBe( + Reflect.apply(native.styleText, undefined, [color, "x", { validateStream: false }]), + ); + } +}); + +test("styleText rejects inherited object names as formats", () => { + for (const format of ["toString", "constructor", "__proto__"]) { + expect(() => util.styleText(format, "x", { validateStream: false })).toThrow( + expect.objectContaining({ code: "ERR_INVALID_ARG_VALUE" }), + ); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/toUSVString.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/toUSVString.ts new file mode 100644 index 000000000..31e80c6fa --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/toUSVString.ts @@ -0,0 +1,23 @@ +import { expect } from "vitest"; +import { util, native, test, capture } from "../helpers/util.js"; + +test("toUSVString replaces only lone surrogates and preserves coercion", () => { + for (const value of [ + "abc", + "🌍", + "\ud800x\udc00", + 42, + null, + undefined, + { + toString() { + return "x"; + }, + }, + Symbol("s"), + ]) { + expect(capture(() => util.toUSVString(value))).toEqual( + capture(() => Reflect.apply(native.toUSVString, undefined, [value])), + ); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/transferableAbortController.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/transferableAbortController.ts new file mode 100644 index 000000000..9aaa943fe --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/transferableAbortController.ts @@ -0,0 +1,8 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("transferableAbortController refuses unavailable behavior before observing inputs", () => { + expect(() => Reflect.apply(util.transferableAbortController, undefined, [])).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/transferableAbortSignal.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/transferableAbortSignal.ts new file mode 100644 index 000000000..d80161bcb --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/transferableAbortSignal.ts @@ -0,0 +1,20 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("transferableAbortSignal refuses unavailable behavior before observing inputs", () => { + const trap = new Proxy( + {}, + { + get() { + throw Error("input touched"); + }, + + ownKeys() { + throw Error("input enumerated"); + }, + }, + ); + expect(() => Reflect.apply(util.transferableAbortSignal, undefined, [trap])).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isAnyArrayBuffer.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isAnyArrayBuffer.ts new file mode 100644 index 000000000..1ca1909dd --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isAnyArrayBuffer.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isAnyArrayBuffer agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new ArrayBuffer(1), new SharedArrayBuffer(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isAnyArrayBuffer(value)).toBe(native.types.isAnyArrayBuffer(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isArgumentsObject.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isArgumentsObject.ts new file mode 100644 index 000000000..d5f44a954 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isArgumentsObject.ts @@ -0,0 +1,22 @@ +import { runInNewContext } from "node:vm"; +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isArgumentsObject agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [runInNewContext("(function () { return arguments; })(1, 2)")]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isArgumentsObject(value)).toBe(native.types.isArgumentsObject(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isArrayBuffer.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isArrayBuffer.ts new file mode 100644 index 000000000..13146e415 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isArrayBuffer.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isArrayBuffer agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new ArrayBuffer(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isArrayBuffer(value)).toBe(native.types.isArrayBuffer(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isArrayBufferView.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isArrayBufferView.ts new file mode 100644 index 000000000..a485ebb43 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isArrayBufferView.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isArrayBufferView agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new DataView(new ArrayBuffer(1)), new Uint8Array(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isArrayBufferView(value)).toBe(native.types.isArrayBufferView(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isAsyncFunction.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isAsyncFunction.ts new file mode 100644 index 000000000..e087078ea --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isAsyncFunction.ts @@ -0,0 +1,29 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isAsyncFunction agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [async function f() {}]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isAsyncFunction(value)).toBe(native.types.isAsyncFunction(value)); + } +}); + +test("isAsyncFunction recognizes async generators", () => { + const value = async function* (): AsyncGenerator { + yield 1; + }; + + expect(util.types.isAsyncFunction(value)).toBe(native.types.isAsyncFunction(value)); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isBigInt64Array.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isBigInt64Array.ts new file mode 100644 index 000000000..b19006cf5 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isBigInt64Array.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isBigInt64Array agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new BigInt64Array(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isBigInt64Array(value)).toBe(native.types.isBigInt64Array(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isBigIntObject.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isBigIntObject.ts new file mode 100644 index 000000000..7480d8aea --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isBigIntObject.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isBigIntObject agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [Object(1n)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isBigIntObject(value)).toBe(native.types.isBigIntObject(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isBigUint64Array.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isBigUint64Array.ts new file mode 100644 index 000000000..c6dc2def3 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isBigUint64Array.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isBigUint64Array agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new BigUint64Array(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isBigUint64Array(value)).toBe(native.types.isBigUint64Array(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isBooleanObject.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isBooleanObject.ts new file mode 100644 index 000000000..761974285 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isBooleanObject.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isBooleanObject agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [Object(true)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isBooleanObject(value)).toBe(native.types.isBooleanObject(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isBoxedPrimitive.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isBoxedPrimitive.ts new file mode 100644 index 000000000..f01244c9b --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isBoxedPrimitive.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isBoxedPrimitive agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [Object(1), Object("x"), Object(true), Object(1n), Object(Symbol())]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isBoxedPrimitive(value)).toBe(native.types.isBoxedPrimitive(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isCryptoKey.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isCryptoKey.ts new file mode 100644 index 000000000..998f95d6e --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isCryptoKey.ts @@ -0,0 +1,9 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isCryptoKey checks engine key slots", async () => { + const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 128 }, true, ["encrypt"]); + for (const value of [key, {}, null, { [Symbol.toStringTag]: "CryptoKey" }]) { + expect(util.types.isCryptoKey(value)).toBe(native.types.isCryptoKey(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isDataView.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isDataView.ts new file mode 100644 index 000000000..ee1778d94 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isDataView.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isDataView agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new DataView(new ArrayBuffer(1))]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isDataView(value)).toBe(native.types.isDataView(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isDate.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isDate.ts new file mode 100644 index 000000000..31f16fa6a --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isDate.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isDate agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Date(), new Date(NaN)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isDate(value)).toBe(native.types.isDate(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isExternal.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isExternal.ts new file mode 100644 index 000000000..9c89b4d7f --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isExternal.ts @@ -0,0 +1,20 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("types.isExternal refuses inaccessible native state without reflection", () => { + const value = new Proxy( + {}, + { + get() { + throw Error("getter"); + }, + + getPrototypeOf() { + throw Error("prototype"); + }, + }, + ); + expect(() => util.types.isExternal(value)).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isFloat16Array.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isFloat16Array.ts new file mode 100644 index 000000000..1aad1160a --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isFloat16Array.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isFloat16Array agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Float16Array(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isFloat16Array(value)).toBe(native.types.isFloat16Array(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isFloat32Array.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isFloat32Array.ts new file mode 100644 index 000000000..277f8cf57 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isFloat32Array.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isFloat32Array agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Float32Array(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isFloat32Array(value)).toBe(native.types.isFloat32Array(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isFloat64Array.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isFloat64Array.ts new file mode 100644 index 000000000..a9bfa0440 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isFloat64Array.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isFloat64Array agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Float64Array(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isFloat64Array(value)).toBe(native.types.isFloat64Array(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isGeneratorFunction.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isGeneratorFunction.ts new file mode 100644 index 000000000..74523b9a4 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isGeneratorFunction.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isGeneratorFunction agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [function* g() {}, async function* g() {}]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isGeneratorFunction(value)).toBe(native.types.isGeneratorFunction(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isGeneratorObject.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isGeneratorObject.ts new file mode 100644 index 000000000..ffab73b91 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isGeneratorObject.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isGeneratorObject agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [(function* g() {})(), (async function* g() {})()]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isGeneratorObject(value)).toBe(native.types.isGeneratorObject(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isInt16Array.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isInt16Array.ts new file mode 100644 index 000000000..bb34835cf --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isInt16Array.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isInt16Array agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Int16Array(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isInt16Array(value)).toBe(native.types.isInt16Array(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isInt32Array.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isInt32Array.ts new file mode 100644 index 000000000..9a438942e --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isInt32Array.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isInt32Array agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Int32Array(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isInt32Array(value)).toBe(native.types.isInt32Array(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isInt8Array.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isInt8Array.ts new file mode 100644 index 000000000..251521f9b --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isInt8Array.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isInt8Array agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Int8Array(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isInt8Array(value)).toBe(native.types.isInt8Array(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isKeyObject.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isKeyObject.ts new file mode 100644 index 000000000..6d901c8aa --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isKeyObject.ts @@ -0,0 +1,20 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("types.isKeyObject refuses inaccessible native state without reflection", () => { + const value = new Proxy( + {}, + { + get() { + throw Error("getter"); + }, + + getPrototypeOf() { + throw Error("prototype"); + }, + }, + ); + expect(() => util.types.isKeyObject(value)).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isMap.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isMap.ts new file mode 100644 index 000000000..57a27e32a --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isMap.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isMap agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Map()]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isMap(value)).toBe(native.types.isMap(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isMapIterator.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isMapIterator.ts new file mode 100644 index 000000000..4dd42cb62 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isMapIterator.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isMapIterator agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Map().entries(), new Map().keys()]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isMapIterator(value)).toBe(native.types.isMapIterator(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isModuleNamespaceObject.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isModuleNamespaceObject.ts new file mode 100644 index 000000000..4bb13ce8c --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isModuleNamespaceObject.ts @@ -0,0 +1,12 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isModuleNamespaceObject recognizes native namespace shape", async () => { + const url = "data:text/javascript,export const value = 1"; + const namespace: unknown = await import(/* @vite-ignore */ url); + for (const value of [namespace, null, {}, { [Symbol.toStringTag]: "Module" }]) { + expect(util.types.isModuleNamespaceObject(value)).toBe( + native.types.isModuleNamespaceObject(value), + ); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isNativeError.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isNativeError.ts new file mode 100644 index 000000000..dfb4a1cd6 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isNativeError.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isNativeError agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Error("x"), new TypeError("x")]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isNativeError(value)).toBe(native.types.isNativeError(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isNumberObject.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isNumberObject.ts new file mode 100644 index 000000000..8d9aea019 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isNumberObject.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isNumberObject agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [Object(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isNumberObject(value)).toBe(native.types.isNumberObject(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isPromise.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isPromise.ts new file mode 100644 index 000000000..8569d118d --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isPromise.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isPromise agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [Promise.resolve(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isPromise(value)).toBe(native.types.isPromise(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isProxy.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isProxy.ts new file mode 100644 index 000000000..cfbeecfd5 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isProxy.ts @@ -0,0 +1,20 @@ +import { expect } from "vitest"; +import { util, test } from "../helpers/util.js"; + +test("types.isProxy refuses inaccessible native state without reflection", () => { + const value = new Proxy( + {}, + { + get() { + throw Error("getter"); + }, + + getPrototypeOf() { + throw Error("prototype"); + }, + }, + ); + expect(() => util.types.isProxy(value)).toThrow( + expect.objectContaining({ code: "ERR_JCO_UNSUPPORTED_NODE_API" }), + ); +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isRegExp.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isRegExp.ts new file mode 100644 index 000000000..84dacaa6e --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isRegExp.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isRegExp agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [/x/g]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isRegExp(value)).toBe(native.types.isRegExp(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isSet.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isSet.ts new file mode 100644 index 000000000..711305708 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isSet.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isSet agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Set()]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isSet(value)).toBe(native.types.isSet(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isSetIterator.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isSetIterator.ts new file mode 100644 index 000000000..2973fd0ce --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isSetIterator.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isSetIterator agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Set().values(), new Set().entries()]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isSetIterator(value)).toBe(native.types.isSetIterator(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isSharedArrayBuffer.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isSharedArrayBuffer.ts new file mode 100644 index 000000000..2d89967b8 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isSharedArrayBuffer.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isSharedArrayBuffer agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new SharedArrayBuffer(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isSharedArrayBuffer(value)).toBe(native.types.isSharedArrayBuffer(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isStringObject.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isStringObject.ts new file mode 100644 index 000000000..d44321f31 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isStringObject.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isStringObject agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [Object("x")]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isStringObject(value)).toBe(native.types.isStringObject(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isSymbolObject.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isSymbolObject.ts new file mode 100644 index 000000000..91d001cc7 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isSymbolObject.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isSymbolObject agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [Object(Symbol("x"))]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isSymbolObject(value)).toBe(native.types.isSymbolObject(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isTypedArray.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isTypedArray.ts new file mode 100644 index 000000000..fd9292b54 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isTypedArray.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isTypedArray agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Uint8Array(1), new BigInt64Array(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isTypedArray(value)).toBe(native.types.isTypedArray(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isUint16Array.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isUint16Array.ts new file mode 100644 index 000000000..6336d37f4 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isUint16Array.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isUint16Array agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Uint16Array(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isUint16Array(value)).toBe(native.types.isUint16Array(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isUint32Array.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isUint32Array.ts new file mode 100644 index 000000000..a5e409069 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isUint32Array.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isUint32Array agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Uint32Array(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isUint32Array(value)).toBe(native.types.isUint32Array(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isUint8Array.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isUint8Array.ts new file mode 100644 index 000000000..fee778fd6 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isUint8Array.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isUint8Array agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Uint8Array(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isUint8Array(value)).toBe(native.types.isUint8Array(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isUint8ClampedArray.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isUint8ClampedArray.ts new file mode 100644 index 000000000..81b330023 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isUint8ClampedArray.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isUint8ClampedArray agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new Uint8ClampedArray(1)]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isUint8ClampedArray(value)).toBe(native.types.isUint8ClampedArray(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isWeakMap.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isWeakMap.ts new file mode 100644 index 000000000..49feb9085 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isWeakMap.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isWeakMap agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new WeakMap()]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isWeakMap(value)).toBe(native.types.isWeakMap(value)); + } +}); diff --git a/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isWeakSet.ts b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isWeakSet.ts new file mode 100644 index 000000000..c982cbed2 --- /dev/null +++ b/packages/jco-std/test/wasi/0.2.x/node/24.x.x/util/types-isWeakSet.ts @@ -0,0 +1,21 @@ +import { expect } from "vitest"; +import { util, native, test } from "../helpers/util.js"; + +test("types.isWeakSet agrees with Node for brands and primitive negatives", () => { + const positive: unknown[] = [new WeakSet()]; + for (const value of [ + ...positive, + null, + undefined, + 0, + false, + "x", + 1n, + Symbol(), + {}, + [], + () => {}, + ]) { + expect(util.types.isWeakSet(value)).toBe(native.types.isWeakSet(value)); + } +}); diff --git a/packages/jco/src/node-builtins/index.ts b/packages/jco/src/node-builtins/index.ts index 4b878719a..ac38d4658 100644 --- a/packages/jco/src/node-builtins/index.ts +++ b/packages/jco/src/node-builtins/index.ts @@ -14,6 +14,7 @@ import { createEventsBuiltin } from "./events.js"; import { createProcessBuiltin } from "./process.js"; import { createOsBuiltin } from "./os.js"; import { createTestBuiltin } from "./test.js"; +import { createUtilBuiltin } from "./util.js"; import { createSqliteBuiltin } from "./sqlite.js"; import { createReadlineBuiltin } from "./readline.js"; import { createReplBuiltin } from "./repl.js"; @@ -73,6 +74,7 @@ export function nodeBuiltinPlugin(worldMetadata: WorldMetadata, options: NodeBui createReadlineBuiltin, createReplBuiltin, createTestBuiltin, + createUtilBuiltin, createStringDecoderBuiltin, createTtyBuiltin, createStreamBuiltin, diff --git a/packages/jco/src/node-builtins/types.ts b/packages/jco/src/node-builtins/types.ts index d6ef54fec..48ce6ca7e 100644 --- a/packages/jco/src/node-builtins/types.ts +++ b/packages/jco/src/node-builtins/types.ts @@ -61,6 +61,9 @@ export interface NodeBuiltinOptions { /** Paths to the versioned test runner and reporters (overridable for tests). */ testModule?: string; testReportersModule?: string; + /** Paths to the versioned portable util and util/types modules. */ + utilModule?: string; + utilTypesModule?: string; /** Override the lazy node:process facade for integration tests. */ processModule?: string; /** Path to jco-std's versioned `node:string_decoder` module (overridable for tests) */ diff --git a/packages/jco/src/node-builtins/util.ts b/packages/jco/src/node-builtins/util.ts new file mode 100644 index 000000000..8840520e9 --- /dev/null +++ b/packages/jco/src/node-builtins/util.ts @@ -0,0 +1,12 @@ +import { builtin, starReexportAdapter, stdModule, type BuiltinContext, type BuiltinAdapter } from "./shared.js"; + +export function createUtilBuiltin({ options }: BuiltinContext): BuiltinAdapter { + return builtin(["node:util", "node:util/types"], (specifier) => + starReexportAdapter( + specifier === "node:util" + ? stdModule(options.utilModule, "util") + : stdModule(options.utilTypesModule, "util/types"), + "util", + ), + ); +} diff --git a/packages/jco/test/fixtures/componentize/node-util/source.js b/packages/jco/test/fixtures/componentize/node-util/source.js new file mode 100644 index 000000000..f2f51ea0a --- /dev/null +++ b/packages/jco/test/fixtures/componentize/node-util/source.js @@ -0,0 +1,132 @@ +import util, { + promisify, + callbackify, + MIMEType, + MIMEParams, + inspect, + format, + formatWithOptions, + parseArgs, + parseEnv, + diff, + styleText, + stripVTControlCharacters, + toUSVString, + isDeepStrictEqual, + inherits, + TextEncoder, + TextDecoder, +} from "node:util"; +import * as namespace from "node:util"; +import types, * as typeNamespace from "node:util/types"; + +const answer = await promisify((value, callback) => callback(null, value + 1))(41); +const callback = await new Promise((resolve) => + callbackify(async () => "callback")((error, value) => resolve([error, value])), +); + +export function run() { + const mime = new MIMEType('Text/HTML; charset="utf-8"; title="a;b"'); + mime.params.set("X", "y"); + const params = new MIMEParams(); + params.set("EMPTY", ""); + const cycle = {}; + cycle.self = cycle; + + function Parent() {} + + function Child() {} + + inherits(Child, Parent); + const refusals = {}; + for (const name of [ + "getCallSites", + "getSystemErrorName", + "getSystemErrorMessage", + "getSystemErrorMap", + "setTraceSigInt", + "convertProcessSignalToExitCode", + "debuglog", + "deprecate", + "transferableAbortController", + "transferableAbortSignal", + "_extend", + "isArray", + "_errnoException", + "_exceptionWithHostPort", + ]) { + try { + util[name](); + } catch (error) { + refusals[name] = error.code; + } + } + let encoded, decoded; + try { + encoded = [...new TextEncoder().encode("🌍")]; + } catch (error) { + encoded = error.code; + } + try { + decoded = new TextDecoder().decode(new Uint8Array([240, 159, 140, 141])); + } catch (error) { + decoded = error.code; + } + return JSON.stringify({ + identity: + namespace.default === util && + util.types === types && + typeNamespace.isUint8Array === types.isUint8Array && + util.MIMEType === MIMEType, + exports: Object.keys(util).sort(), + answer, + callback, + mime: [mime.essence, [...mime.params], String(mime), params.toJSON()], + args: parseArgs({ + args: ["-vv", "--name=guest", "--no-color", "tail"], + options: { + verbose: { type: "boolean", short: "v", multiple: true }, + name: { type: "string" }, + color: { type: "boolean", default: true }, + }, + allowNegative: true, + allowPositionals: true, + tokens: true, + }), + env: parseEnv('FOO=bar\nexport GREETING="hello\\nworld"'), + diff: diff("abc", "adc"), + formatted: format("%s %d %j", "ok", 3, { x: 1 }), + options: formatWithOptions({ sorted: true }, "%O", { z: 1, a: 2 }), + inspected: [ + inspect(cycle), + inspect({ + get x() { + throw Error("getter invoked"); + }, + }), + inspect({ + [inspect.custom]() { + return { custom: true }; + }, + }), + ], + styled: styleText(["bold", "red"], "hello", { validateStream: false }), + stripped: stripVTControlCharacters("\x1b[31mred\x1b[0m"), + unicode: toUSVString("\ud800x🌍"), + encoded, + decoded, + equal: isDeepStrictEqual(new Map([[1, { x: 2 }]]), new Map([[1, { x: 2 }]])), + inherited: Object.getPrototypeOf(Child.prototype) === Parent.prototype, + brands: [ + types.isUint8Array(new Uint8Array(1)), + types.isDataView(new DataView(new ArrayBuffer(1))), + types.isBoxedPrimitive(Object(false)), + types.isBooleanObject(false), + types.isMap(new Map()), + types.isSet(new Set()), + types.isPromise(Promise.resolve()), + types.isArrayBuffer(new ArrayBuffer(1)), + ], + refusals, + }); +} diff --git a/packages/jco/test/fixtures/componentize/node-util/source.wit b/packages/jco/test/fixtures/componentize/node-util/source.wit new file mode 100644 index 000000000..d4ef94476 --- /dev/null +++ b/packages/jco/test/fixtures/componentize/node-util/source.wit @@ -0,0 +1,5 @@ +package test:node-util; + +world test { + export run: func() -> string; +} diff --git a/packages/jco/test/node/util.js b/packages/jco/test/node/util.js new file mode 100644 index 000000000..acb87bb75 --- /dev/null +++ b/packages/jco/test/node/util.js @@ -0,0 +1,150 @@ +import assert from "node:assert/strict"; +import { writeFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { join } from "node:path"; +import { test, expect, vi } from "vitest"; +import { nodeBuiltinPlugin } from "../../src/node-builtins/index.js"; +import { bundleComponentSource } from "../../src/bundle.js"; +import { exec, getTmpDir, jcoPath, transpileComponent } from "../helpers.js"; +import { hasJspi } from "../common.js"; +import native from "node:util"; + +const fixture = new URL("../fixtures/componentize/node-util/", import.meta.url); + +const std = (path) => fileURLToPath(new URL(`../../../jco-std/dist/wasi/0.2.x/node/24.x.x/${path}`, import.meta.url)); + +const overrides = { utilModule: std("util/index.js"), utilTypesModule: std("util-types.js") }; + +test("util adapters resolve lazily without WIT capabilities", () => { + const onWitRequirement = vi.fn(); + const plugin = nodeBuiltinPlugin({ imports: [], exports: [] }, { ...overrides, onWitRequirement }); + for (const name of ["node:util", "node:util/types"]) { + expect(plugin.resolveId(name)).toBe(`\0jco-node-builtin:${name}`); + } + for (const name of ["util", "util/types", "node:util/unknown"]) { + expect(plugin.resolveId(name)).toBeNull(); + } + expect(onWitRequirement).not.toHaveBeenCalled(); +}); + +test.skipIf(!hasJspi).each(["quickjs", "starlingmonkey"])( + "runs util algorithms and namespaces in %s", + async (backend) => { + const dir = await getTmpDir(); + const entry = join(dir, "source.js"); + const componentPath = join(dir, "component.wasm"); + const source = await bundleComponentSource(fileURLToPath(new URL("source.js", fixture)), { + plugins: [nodeBuiltinPlugin({ imports: [], exports: [] }, overrides)], + }); + await writeFile(entry, source); + await exec( + jcoPath, + "componentize", + entry, + "--backend", + backend, + "-w", + fileURLToPath(new URL("source.wit", fixture)), + "-n", + "test", + "-o", + componentPath, + { closeStdin: true }, + ); + const { modulePath } = await transpileComponent({ componentPath, name: "util" }); + const component = await import(modulePath); + const report = JSON.parse(component.run()); + assert.equal(report.identity, true); + assert.deepEqual( + report.exports, + [ + "MIMEParams", + "MIMEType", + "TextDecoder", + "TextEncoder", + "_errnoException", + "_exceptionWithHostPort", + "_extend", + "aborted", + "callbackify", + "convertProcessSignalToExitCode", + "debug", + "debuglog", + "deprecate", + "diff", + "format", + "formatWithOptions", + "getCallSites", + "getSystemErrorMap", + "getSystemErrorMessage", + "getSystemErrorName", + "inherits", + "inspect", + "isArray", + "isDeepStrictEqual", + "parseArgs", + "parseEnv", + "promisify", + "setTraceSigInt", + "stripVTControlCharacters", + "styleText", + "toUSVString", + "transferableAbortController", + "transferableAbortSignal", + "types", + ].sort(), + ); + assert.equal(report.answer, 42); + assert.deepEqual(report.callback, [null, "callback"]); + assert.deepEqual(report.mime, [ + "text/html", + [ + ["charset", "utf-8"], + ["title", "a;b"], + ["x", "y"], + ], + 'text/html;charset=utf-8;title="a;b";x=y', + 'empty=""', + ]); + const config = { + args: ["-vv", "--name=guest", "--no-color", "tail"], + options: { + verbose: { type: "boolean", short: "v", multiple: true }, + name: { type: "string" }, + color: { type: "boolean", default: true }, + }, + allowNegative: true, + allowPositionals: true, + tokens: true, + }; + assert.deepEqual(report.args, JSON.parse(JSON.stringify(native.parseArgs(config)))); + assert.deepEqual(report.env, { FOO: "bar", GREETING: "hello\nworld" }); + assert.deepEqual(report.diff, [ + [0, "a"], + [1, "b"], + [-1, "d"], + [0, "c"], + ]); + assert.equal(report.formatted, 'ok 3 {"x":1}'); + assert.equal(report.options, "{ a: 2, z: 1 }"); + assert.deepEqual(report.inspected, [" { self: [Circular *1] }", "{ x: [Getter] }", "{ custom: true }"]); + assert.equal(report.styled, native.styleText(["bold", "red"], "hello", { validateStream: false })); + assert.equal(report.stripped, "red"); + assert.equal(report.unicode, "�x🌍"); + assert.deepEqual(report.encoded, backend === "quickjs" ? "ERR_JCO_UNSUPPORTED_NODE_API" : [240, 159, 140, 141]); + assert.equal(report.decoded, backend === "quickjs" ? "ERR_JCO_UNSUPPORTED_NODE_API" : "🌍"); + assert.equal(report.equal, true); + assert.equal(report.inherited, true); + assert.deepEqual(report.brands, [true, true, true, false, true, true, true, true]); + assert.equal(Object.keys(report.refusals).length, 14); + for (const [name, code] of Object.entries(report.refusals)) { + assert.equal( + code, + name.startsWith("_") || name === "isArray" + ? "ERR_JCO_UNSUPPORTED_DEPRECATED_NODE_API" + : "ERR_JCO_UNSUPPORTED_NODE_API", + ); + } + }, + 600_000, +);