From 5c3f9514a8b79a6bede0dad977424a755fe46385 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:46:40 +0100 Subject: [PATCH 1/5] feat(p2-shim): migrate to typescript --- packages/preview2-shim/.gitignore | 2 + packages/preview2-shim/lib/browser/cli.js | 135 -------- packages/preview2-shim/lib/browser/config.js | 9 - .../preview2-shim/lib/browser/environment.js | 29 -- packages/preview2-shim/lib/browser/index.js | 9 - packages/preview2-shim/lib/browser/io.js | 262 ---------------- packages/preview2-shim/lib/nodejs/cli.js | 109 ------- packages/preview2-shim/lib/nodejs/index.js | 9 - packages/preview2-shim/lib/nodejs/io.js | 1 - packages/preview2-shim/package.json | 24 +- packages/preview2-shim/src/browser/cli.ts | 144 +++++++++ .../clocks.js => src/browser/clocks.ts} | 25 +- packages/preview2-shim/src/browser/config.ts | 9 + .../preview2-shim/src/browser/environment.ts | 30 ++ .../browser/filesystem.ts} | 241 ++++++++------- .../browser/http.js => src/browser/http.ts} | 229 ++++++++------ packages/preview2-shim/src/browser/index.ts | 7 + packages/preview2-shim/src/browser/io.ts | 272 ++++++++++++++++ .../random.js => src/browser/random.ts} | 25 +- .../sockets.js => src/browser/sockets.ts} | 47 ++- .../common/instantiation.ts} | 63 ++-- .../{lib/io/calls.js => src/io/calls.ts} | 0 .../worker-http.js => src/io/worker-http.ts} | 23 +- .../io/worker-io.js => src/io/worker-io.ts} | 56 ++-- .../io/worker-socket-tcp.ts} | 127 ++++---- .../io/worker-socket-udp.ts} | 224 +++++++------- .../io/worker-sockets.ts} | 88 ++---- .../io/worker-thread.ts} | 81 +++-- packages/preview2-shim/src/nodejs/cli.ts | 115 +++++++ .../nodejs/clocks.js => src/nodejs/clocks.ts} | 20 +- .../nodejs/filesystem.ts} | 87 +++--- .../nodejs/http.js => src/nodejs/http.ts} | 89 ++++-- packages/preview2-shim/src/nodejs/index.ts | 7 + packages/preview2-shim/src/nodejs/io.ts | 1 + .../nodejs/random.js => src/nodejs/random.ts} | 22 +- .../sockets.js => src/nodejs/sockets.ts} | 91 +++--- .../synckit/index.js => src/synckit/index.ts} | 6 +- .../test/{browser.js => browser.ts} | 72 ++--- .../test/{common.js => common.ts} | 268 +++++++++------- .../iface-namespaces-at-runtime/example.ts | 4 +- .../{tsconfig.json => tsconfig.test.json} | 0 .../preview2-shim/test/{test.js => test.ts} | 290 ++++++++++-------- .../preview2-shim/test/{types.js => types.ts} | 2 +- packages/preview2-shim/test/vitest.ts | 4 +- packages/preview2-shim/tsconfig.build.json | 11 + packages/preview2-shim/tsconfig.json | 17 +- .../preview2-shim/types/instantiation.d.ts | 14 +- pnpm-lock.yaml | 3 + 48 files changed, 1830 insertions(+), 1573 deletions(-) create mode 100644 packages/preview2-shim/.gitignore delete mode 100644 packages/preview2-shim/lib/browser/cli.js delete mode 100644 packages/preview2-shim/lib/browser/config.js delete mode 100644 packages/preview2-shim/lib/browser/environment.js delete mode 100644 packages/preview2-shim/lib/browser/index.js delete mode 100644 packages/preview2-shim/lib/browser/io.js delete mode 100644 packages/preview2-shim/lib/nodejs/cli.js delete mode 100644 packages/preview2-shim/lib/nodejs/index.js delete mode 100644 packages/preview2-shim/lib/nodejs/io.js create mode 100644 packages/preview2-shim/src/browser/cli.ts rename packages/preview2-shim/{lib/browser/clocks.js => src/browser/clocks.ts} (57%) create mode 100644 packages/preview2-shim/src/browser/config.ts create mode 100644 packages/preview2-shim/src/browser/environment.ts rename packages/preview2-shim/{lib/browser/filesystem.js => src/browser/filesystem.ts} (61%) rename packages/preview2-shim/{lib/browser/http.js => src/browser/http.ts} (72%) create mode 100644 packages/preview2-shim/src/browser/index.ts create mode 100644 packages/preview2-shim/src/browser/io.ts rename packages/preview2-shim/{lib/browser/random.js => src/browser/random.ts} (61%) rename packages/preview2-shim/{lib/browser/sockets.js => src/browser/sockets.ts} (61%) rename packages/preview2-shim/{lib/common/instantiation.js => src/common/instantiation.ts} (87%) rename packages/preview2-shim/{lib/io/calls.js => src/io/calls.ts} (100%) rename packages/preview2-shim/{lib/io/worker-http.js => src/io/worker-http.ts} (91%) rename packages/preview2-shim/{lib/io/worker-io.js => src/io/worker-io.ts} (91%) rename packages/preview2-shim/{lib/io/worker-socket-tcp.js => src/io/worker-socket-tcp.ts} (71%) rename packages/preview2-shim/{lib/io/worker-socket-udp.js => src/io/worker-socket-udp.ts} (73%) rename packages/preview2-shim/{lib/io/worker-sockets.js => src/io/worker-sockets.ts} (80%) rename packages/preview2-shim/{lib/io/worker-thread.js => src/io/worker-thread.ts} (95%) create mode 100644 packages/preview2-shim/src/nodejs/cli.ts rename packages/preview2-shim/{lib/nodejs/clocks.js => src/nodejs/clocks.ts} (75%) rename packages/preview2-shim/{lib/nodejs/filesystem.js => src/nodejs/filesystem.ts} (91%) rename packages/preview2-shim/{lib/nodejs/http.js => src/nodejs/http.ts} (88%) create mode 100644 packages/preview2-shim/src/nodejs/index.ts create mode 100644 packages/preview2-shim/src/nodejs/io.ts rename packages/preview2-shim/{lib/nodejs/random.js => src/nodejs/random.ts} (61%) rename packages/preview2-shim/{lib/nodejs/sockets.js => src/nodejs/sockets.ts} (84%) rename packages/preview2-shim/{lib/synckit/index.js => src/synckit/index.ts} (96%) rename packages/preview2-shim/test/{browser.js => browser.ts} (95%) rename packages/preview2-shim/test/{common.js => common.ts} (52%) rename packages/preview2-shim/test/fixtures/types/iface-namespaces-at-runtime/{tsconfig.json => tsconfig.test.json} (100%) rename packages/preview2-shim/test/{test.js => test.ts} (78%) rename packages/preview2-shim/test/{types.js => types.ts} (91%) create mode 100644 packages/preview2-shim/tsconfig.build.json diff --git a/packages/preview2-shim/.gitignore b/packages/preview2-shim/.gitignore new file mode 100644 index 000000000..c37207708 --- /dev/null +++ b/packages/preview2-shim/.gitignore @@ -0,0 +1,2 @@ +# TypeScript build outputs +/lib/ diff --git a/packages/preview2-shim/lib/browser/cli.js b/packages/preview2-shim/lib/browser/cli.js deleted file mode 100644 index 31e2b4f88..000000000 --- a/packages/preview2-shim/lib/browser/cli.js +++ /dev/null @@ -1,135 +0,0 @@ -import { streams } from "./io.js"; -const { InputStream, OutputStream } = streams; - -export { _setEnv, _setArgs, environment } from "./environment.js"; -export { _setCwd } from "./config.js"; - -const symbolDispose = Symbol.dispose ?? Symbol.for("dispose"); - -class ComponentExit extends Error { - constructor(code) { - super(`Component exited ${code === 0 ? "successfully" : "with error"}`); - this.exitError = true; - this.code = code; - } -} - -export const exit = { - exit(status) { - throw new ComponentExit(status.tag === "err" ? 1 : 0); - }, - exitWithCode(code) { - throw new ComponentExit(code); - }, -}; - -/** - * @param {import('../common/io.js').InputStreamHandler} handler - */ -export function _setStdin(handler) { - stdinStream.handler = handler; -} -/** - * @param {import('../common/io.js').OutputStreamHandler} handler - */ -export function _setStderr(handler) { - stderrStream.handler = handler; -} -/** - * @param {import('../common/io.js').OutputStreamHandler} handler - */ -export function _setStdout(handler) { - stdoutStream.handler = handler; -} - -const stdinStream = new InputStream({ - blockingRead(_len) { - // TODO - }, - subscribe() { - // TODO - }, - [symbolDispose]() { - // TODO - }, -}); -let textDecoder = new TextDecoder(); -const stdoutStream = new OutputStream({ - write(contents) { - if (contents[contents.length - 1] == 10) { - // console.log already appends a new line - contents = contents.subarray(0, contents.length - 1); - } - console.log(textDecoder.decode(contents)); - }, - blockingFlush() {}, - [symbolDispose]() {}, -}); -const stderrStream = new OutputStream({ - write(contents) { - if (contents[contents.length - 1] == 10) { - // console.error already appends a new line - contents = contents.subarray(0, contents.length - 1); - } - console.error(textDecoder.decode(contents)); - }, - blockingFlush() {}, - [symbolDispose]() {}, -}); - -export const stdin = { - InputStream, - getStdin() { - return stdinStream; - }, -}; - -export const stdout = { - OutputStream, - getStdout() { - return stdoutStream; - }, -}; - -export const stderr = { - OutputStream, - getStderr() { - return stderrStream; - }, -}; - -class TerminalInput {} -class TerminalOutput {} - -const terminalStdoutInstance = new TerminalOutput(); -const terminalStderrInstance = new TerminalOutput(); -const terminalStdinInstance = new TerminalInput(); - -export const terminalInput = { - TerminalInput, -}; - -export const terminalOutput = { - TerminalOutput, -}; - -export const terminalStderr = { - TerminalOutput, - getTerminalStderr() { - return terminalStderrInstance; - }, -}; - -export const terminalStdin = { - TerminalInput, - getTerminalStdin() { - return terminalStdinInstance; - }, -}; - -export const terminalStdout = { - TerminalOutput, - getTerminalStdout() { - return terminalStdoutInstance; - }, -}; diff --git a/packages/preview2-shim/lib/browser/config.js b/packages/preview2-shim/lib/browser/config.js deleted file mode 100644 index a13438009..000000000 --- a/packages/preview2-shim/lib/browser/config.js +++ /dev/null @@ -1,9 +0,0 @@ -let _cwd = "/"; - -export function _setCwd(cwd) { - _cwd = cwd; -} - -export function _getCwd() { - return _cwd; -} diff --git a/packages/preview2-shim/lib/browser/environment.js b/packages/preview2-shim/lib/browser/environment.js deleted file mode 100644 index c1af6922b..000000000 --- a/packages/preview2-shim/lib/browser/environment.js +++ /dev/null @@ -1,29 +0,0 @@ -import { _setCwd as fsSetCwd } from "./config.js"; - -let _env = [], - _args = [], - _cwd = "/"; - -export function _setEnv(envObj) { - _env = Object.entries(envObj); -} - -export function _setArgs(args) { - _args = args; -} - -export function _setCwd(cwd) { - fsSetCwd((_cwd = cwd)); -} - -export const environment = { - getEnvironment() { - return _env; - }, - getArguments() { - return _args; - }, - initialCwd() { - return _cwd; - }, -}; diff --git a/packages/preview2-shim/lib/browser/index.js b/packages/preview2-shim/lib/browser/index.js deleted file mode 100644 index c6c5cd75a..000000000 --- a/packages/preview2-shim/lib/browser/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import * as clocks from "./clocks.js"; -import * as filesystem from "./filesystem.js"; -import * as http from "./http.js"; -import * as io from "./io.js"; -import * as random from "./random.js"; -import * as sockets from "./sockets.js"; -import * as cli from "./cli.js"; - -export { clocks, filesystem, http, io, random, sockets, cli }; diff --git a/packages/preview2-shim/lib/browser/io.js b/packages/preview2-shim/lib/browser/io.js deleted file mode 100644 index 8424efebd..000000000 --- a/packages/preview2-shim/lib/browser/io.js +++ /dev/null @@ -1,262 +0,0 @@ -let id = 0; - -const symbolDispose = Symbol.dispose || Symbol.for("dispose"); - -const IoError = class Error { - constructor(msg) { - this.msg = msg; - } - toDebugString() { - return this.msg; - } -}; - -/** - * @typedef {{ - * read?: (len: BigInt) => Uint8Array, - * blockingRead: (len: BigInt) => Uint8Array, - * skip?: (len: BigInt) => BigInt, - * blockingSkip?: (len: BigInt) => BigInt, - * subscribe: () => Pollable, - * drop?: () => void, - * }} InputStreamHandler - * - * @typedef {{ - * checkWrite?: () -> BigInt, - * write: (buf: Uint8Array) => BigInt, - * blockingWriteAndFlush?: (buf: Uint8Array) => void, - * flush?: () => void, - * blockingFlush: () => void, - * writeZeroes?: (len: BigInt) => void, - * blockingWriteZeroes?: (len: BigInt) => void, - * blockingWriteZeroesAndFlush?: (len: BigInt) => void, - * splice?: (src: InputStream, len: BigInt) => BigInt, - * blockingSplice?: (src: InputStream, len: BigInt) => BigInt, - * forward?: (src: InputStream) => void, - * subscribe?: () => Pollable, - * drop?: () => void, - * }} OutputStreamHandler - * - **/ - -class InputStream { - /** - * @param {InputStreamHandler} handler - */ - constructor(handler) { - if (!handler) { - console.trace("no handler"); - } - this.id = ++id; - this.handler = handler; - } - read(len) { - if (this.handler.read) { - return this.handler.read(len); - } - return this.handler.blockingRead.call(this, len); - } - blockingRead(len) { - return this.handler.blockingRead.call(this, len); - } - skip(len) { - if (this.handler.skip) { - return this.handler.skip.call(this, len); - } - if (this.handler.read) { - const bytes = this.handler.read.call(this, len); - return BigInt(bytes.byteLength); - } - return this.blockingSkip.call(this, len); - } - blockingSkip(len) { - if (this.handler.blockingSkip) { - return this.handler.blockingSkip.call(this, len); - } - const bytes = this.handler.blockingRead.call(this, len); - return BigInt(bytes.byteLength); - } - subscribe() { - if (this.handler.subscribe) { - return this.handler.subscribe(); - } - return new Pollable(); - } - [symbolDispose]() { - if (this.handler.drop) { - this.handler.drop.call(this); - } - } -} - -class OutputStream { - /** - * @param {OutputStreamHandler} handler - */ - constructor(handler) { - if (!handler) { - console.trace("no handler"); - } - this.id = ++id; - this.open = true; - this.handler = handler; - } - checkWrite(len) { - if (!this.open) { - return 0n; - } - if (this.handler.checkWrite) { - return this.handler.checkWrite.call(this, len); - } - return 1_000_000n; - } - write(buf) { - this.handler.write.call(this, buf); - } - blockingWriteAndFlush(buf) { - /// Perform a write of up to 4096 bytes, and then flush the stream. Block - /// until all of these operations are complete, or an error occurs. - /// - /// This is a convenience wrapper around the use of `check-write`, - /// `subscribe`, `write`, and `flush`, and is implemented with the - /// following pseudo-code: - /// - /// ```text - /// let pollable = this.subscribe(); - /// while !contents.is_empty() { - /// // Wait for the stream to become writable - /// poll-one(pollable); - /// let Ok(n) = this.check-write(); // eliding error handling - /// let len = min(n, contents.len()); - /// let (chunk, rest) = contents.split_at(len); - /// this.write(chunk ); // eliding error handling - /// contents = rest; - /// } - /// this.flush(); - /// // Wait for completion of `flush` - /// poll-one(pollable); - /// // Check for any errors that arose during `flush` - /// let _ = this.check-write(); // eliding error handling - /// ``` - this.handler.write.call(this, buf); - } - flush() { - if (this.handler.flush) { - this.handler.flush.call(this); - } - } - blockingFlush() { - this.open = true; - } - writeZeroes(len) { - this.write.call(this, new Uint8Array(Number(len))); - } - blockingWriteZeroes(len) { - this.blockingWrite.call(this, new Uint8Array(Number(len))); - } - blockingWriteZeroesAndFlush(len) { - this.blockingWriteAndFlush.call(this, new Uint8Array(Number(len))); - } - splice(src, len) { - const spliceLen = Math.min(len, this.checkWrite.call(this)); - const bytes = src.read(spliceLen); - this.write.call(this, bytes); - return bytes.byteLength; - } - blockingSplice(_src, _len) { - console.log(`[streams] Blocking splice ${this.id}`); - } - forward(_src) { - console.log(`[streams] Forward ${this.id}`); - } - subscribe() { - if (this.handler.subscribe) { - return this.handler.subscribe(); - } - return new Pollable(); - } - [symbolDispose]() {} -} - -export const error = { Error: IoError }; - -export const streams = { InputStream, OutputStream }; - -class Pollable { - #ready = false; - #promise = null; - - constructor(promise) { - if (!promise) { - this.#ready = true; - } else { - this.#promise = promise.then( - () => { - this.#ready = true; - }, - () => { - this.#ready = true; - }, - ); - } - } - - ready() { - return this.#ready; - } - - block() { - if (this.#ready) { - return Promise.resolve(); - } - return this.#promise; - } - - [symbolDispose]() { - this.#promise = null; - } -} - -function pollList(list) { - if (list.length === 0) { - throw new Error("poll list must not be empty"); - } - if (list.length > 0xffffffff) { - throw new Error("poll list length exceeds u32 index range"); - } - const ready = []; - for (let i = 0; i < list.length; i++) { - if (list[i].ready()) { - ready.push(i); - } - } - if (ready.length > 0) { - return new Uint32Array(ready); - } - // None ready synchronously. Wait for the first to resolve via Promise.race, - // then sweep for any others that became ready concurrently. - return Promise.race( - list.map((p, i) => - p.block().then(() => { - const result = [i]; - for (let j = 0; j < list.length; j++) { - if (j !== i && list[j].ready()) { - result.push(j); - } - } - return new Uint32Array(result); - }), - ), - ); -} - -function pollOne(poll) { - return poll.block(); -} - -export const poll = { - Pollable, - pollList, - pollOne, - poll: pollList, -}; diff --git a/packages/preview2-shim/lib/nodejs/cli.js b/packages/preview2-shim/lib/nodejs/cli.js deleted file mode 100644 index 3419c3ea8..000000000 --- a/packages/preview2-shim/lib/nodejs/cli.js +++ /dev/null @@ -1,109 +0,0 @@ -import process, { argv, env, cwd } from "node:process"; -import { ioCall, streams, inputStreamCreate, outputStreamCreate } from "../io/worker-io.js"; -import { INPUT_STREAM_CREATE, STDERR, STDIN, STDOUT } from "../io/calls.js"; -const { InputStream, OutputStream } = streams; - -export const _appendEnv = (env) => { - void (_env = [..._env.filter(([curKey]) => !(curKey in env)), ...Object.entries(env)]); -}; -export const _setEnv = (env) => void (_env = Object.entries(env)); -export const _setArgs = (args) => void (_args = args); -export const _setCwd = (cwd) => void (_cwd = cwd); -export const _setStdin = (stdin) => void (stdinStream = stdin); -export const _setStdout = (stdout) => void (stdoutStream = stdout); -export const _setStderr = (stderr) => void (stderrStream = stderr); -export const _setTerminalStdin = (terminalStdin) => void (terminalStdinInstance = terminalStdin); -export const _setTerminalStdout = (terminalStdout) => - void (terminalStdoutInstance = terminalStdout); -export const _setTerminalStderr = (terminalStderr) => - void (terminalStderrInstance = terminalStderr); - -let _env = Object.entries(env), - _args = argv.slice(1), - _cwd = cwd(); - -export const environment = { - getEnvironment() { - return _env; - }, - getArguments() { - return _args; - }, - initialCwd() { - return _cwd; - }, -}; - -export const exit = { - exit(status) { - process.exit(status.tag === "err" ? 1 : 0); - }, - exitWithCode(code) { - process.exit(code); - }, -}; - -// Stdin is created as a FILE descriptor -let stdinStream; -let stdoutStream = outputStreamCreate(STDOUT, 1); -let stderrStream = outputStreamCreate(STDERR, 2); - -export const stdin = { - InputStream, - getStdin() { - if (!stdinStream) { - stdinStream = inputStreamCreate(STDIN, ioCall(INPUT_STREAM_CREATE | STDIN, null, null)); - } - return stdinStream; - }, -}; - -export const stdout = { - OutputStream, - getStdout() { - return stdoutStream; - }, -}; - -export const stderr = { - OutputStream, - getStderr() { - return stderrStream; - }, -}; - -class TerminalInput {} -class TerminalOutput {} - -let terminalStdoutInstance = new TerminalOutput(); -let terminalStderrInstance = new TerminalOutput(); -let terminalStdinInstance = new TerminalInput(); - -export const terminalInput = { - TerminalInput, -}; - -export const terminalOutput = { - TerminalOutput, -}; - -export const terminalStderr = { - TerminalOutput, - getTerminalStderr() { - return terminalStderrInstance; - }, -}; - -export const terminalStdin = { - TerminalInput, - getTerminalStdin() { - return terminalStdinInstance; - }, -}; - -export const terminalStdout = { - TerminalOutput, - getTerminalStdout() { - return terminalStdoutInstance; - }, -}; diff --git a/packages/preview2-shim/lib/nodejs/index.js b/packages/preview2-shim/lib/nodejs/index.js deleted file mode 100644 index c6c5cd75a..000000000 --- a/packages/preview2-shim/lib/nodejs/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import * as clocks from "./clocks.js"; -import * as filesystem from "./filesystem.js"; -import * as http from "./http.js"; -import * as io from "./io.js"; -import * as random from "./random.js"; -import * as sockets from "./sockets.js"; -import * as cli from "./cli.js"; - -export { clocks, filesystem, http, io, random, sockets, cli }; diff --git a/packages/preview2-shim/lib/nodejs/io.js b/packages/preview2-shim/lib/nodejs/io.js deleted file mode 100644 index e4eef265f..000000000 --- a/packages/preview2-shim/lib/nodejs/io.js +++ /dev/null @@ -1 +0,0 @@ -export { error, streams, poll } from "../io/worker-io.js"; diff --git a/packages/preview2-shim/package.json b/packages/preview2-shim/package.json index c1b25c9c8..14e6f4235 100644 --- a/packages/preview2-shim/package.json +++ b/packages/preview2-shim/package.json @@ -30,35 +30,47 @@ "lib" ], "type": "module", - "types": "./types/index.d.ts", "exports": { ".": { - "types": "./types/index.d.ts", - "node": "./lib/nodejs/index.js", + "types": "./lib/browser/index.d.ts", + "node": { + "types": "./lib/nodejs/index.d.ts", + "default": "./lib/nodejs/index.js" + }, "default": "./lib/browser/index.js" }, "./*": { - "types": "./types/*.d.ts", - "node": "./lib/nodejs/*.js", + "types": "./lib/browser/*.d.ts", + "node": { + "types": "./lib/nodejs/*.d.ts", + "default": "./lib/nodejs/*.js" + }, "default": "./lib/browser/*.js" }, "./instantiation": { "types": "./types/instantiation.d.ts", "node": "./lib/common/instantiation.js", "default": "./lib/common/instantiation.js" + }, + "./interfaces/*": { + "types": "./types/interfaces/*.d.ts" } }, "scripts": { + "build": "tsc --project tsconfig.build.json", + "build:watch": "tsc --watch", "types:check": "tsc --noEmit", "fmt": "oxfmt", "fmt:check": "oxfmt --check", "lint": "oxlint", "lint:fix": "oxlint --fix", - "test": "vitest run -c test/vitest.ts" + "test": "vitest run -c test/vitest.ts", + "prepublishOnly": "pnpm run build" }, "devDependencies": { "@bytecodealliance/componentize-js": "0.21.0", "@bytecodealliance/jco": "1.20.0", + "@types/node": "^24.12.4", "mime": "^4.0.7", "puppeteer": "catalog:", "typescript": "catalog:", diff --git a/packages/preview2-shim/src/browser/cli.ts b/packages/preview2-shim/src/browser/cli.ts new file mode 100644 index 000000000..855132d51 --- /dev/null +++ b/packages/preview2-shim/src/browser/cli.ts @@ -0,0 +1,144 @@ +import type { + exit as ExitNamespace, + stderr as StderrNamespace, + stdin as StdinNamespace, + stdout as StdoutNamespace, + terminalInput as TerminalInputNamespace, + terminalOutput as TerminalOutputNamespace, + terminalStderr as TerminalStderrNamespace, + terminalStdin as TerminalStdinNamespace, + terminalStdout as TerminalStdoutNamespace, +} from "../../types/cli.js"; +import { + inputStreamCreate, + outputStreamCreate, + pollableCreate, + type InputStreamHandler, + type OutputStreamHandler, +} from "./io.js"; +export { _setEnv, _setArgs, environment } from "./environment.js"; +export { _setCwd } from "./config.js"; + +const symbolDispose = Symbol.dispose ?? Symbol.for("dispose"); +class ComponentExit extends Error { + exitError = true; + code: number; + + constructor(code: number) { + super(`Component exited ${code === 0 ? "successfully" : "with error"}`); + this.code = code; + } +} + +export const exit: typeof ExitNamespace = { + exit(status: ExitNamespace.Result): never { + throw new ComponentExit(status.tag === "err" ? 1 : 0); + }, + // @ts-expect-error - Available only wasi-cli v0.2.12 + exitWithCode(code: number): never { + throw new ComponentExit(code); + }, +}; + +export function _setStdin(handler: InputStreamHandler): void { + stdinStream.handler = handler; +} + +export function _setStderr(handler: OutputStreamHandler): void { + stderrStream.handler = handler; +} + +export function _setStdout(handler: OutputStreamHandler): void { + stdoutStream.handler = handler; +} + +const stdinStream = inputStreamCreate({ + blockingRead(_len: bigint) { + // TODO + return new Uint8Array(0); + }, + subscribe() { + // TODO + return pollableCreate(); + }, + [symbolDispose]() { + // TODO + }, +}); + +const textDecoder = new TextDecoder(); + +const stdoutStream = outputStreamCreate({ + write(contents: Uint8Array): void { + if (contents.at(-1) == 10) { + // console.log already appends a new line + contents = contents.subarray(0, -1); + } + console.log(textDecoder.decode(contents)); + }, + blockingFlush() {}, + [symbolDispose]() {}, +}); + +const stderrStream = outputStreamCreate({ + write(contents: Uint8Array): void { + if (contents.at(-1) == 10) { + // console.error already appends a new line + contents = contents.subarray(0, -1); + } + console.error(textDecoder.decode(contents)); + }, + blockingFlush() {}, + [symbolDispose]() {}, +}); + +export const stdin: typeof StdinNamespace = { + getStdin() { + return stdinStream; + }, +}; + +export const stdout: typeof StdoutNamespace = { + getStdout() { + return stdoutStream; + }, +}; + +export const stderr: typeof StderrNamespace = { + getStderr() { + return stderrStream; + }, +}; + +class TerminalInput implements TerminalInputNamespace.TerminalInput {} +class TerminalOutput implements TerminalOutputNamespace.TerminalOutput {} + +const terminalStdoutInstance = new TerminalOutput(); +const terminalStderrInstance = new TerminalOutput(); +const terminalStdinInstance = new TerminalInput(); + +export const terminalInput: typeof TerminalInputNamespace = { + TerminalInput, +}; + +export const terminalOutput: typeof TerminalOutputNamespace = { + TerminalOutput, +}; + +export const terminalStderr: typeof TerminalStderrNamespace = { + getTerminalStderr() { + return terminalStderrInstance; + }, +}; + +export const terminalStdin: typeof TerminalStdinNamespace = { + getTerminalStdin() { + return terminalStdinInstance; + }, +}; + +export const terminalStdout: typeof TerminalStdoutNamespace = { + getTerminalStdout() { + return terminalStdoutInstance; + }, +}; diff --git a/packages/preview2-shim/lib/browser/clocks.js b/packages/preview2-shim/src/browser/clocks.ts similarity index 57% rename from packages/preview2-shim/lib/browser/clocks.js rename to packages/preview2-shim/src/browser/clocks.ts index 535cef56a..9fb0a95b8 100644 --- a/packages/preview2-shim/lib/browser/clocks.js +++ b/packages/preview2-shim/src/browser/clocks.ts @@ -1,32 +1,35 @@ -import { poll } from "./io.js"; -const { Pollable } = poll; +import type { + monotonicClock as MonotonicClockNamespace, + wallClock as WallClockNamespace, +} from "../../types/clocks.js"; +import { pollableCreate } from "./io.js"; -export const monotonicClock = { - resolution() { +export const monotonicClock: typeof MonotonicClockNamespace = { + resolution(): bigint { // usually we dont get sub-millisecond accuracy in the browser // Note: is there a better way to determine this? - return 1e6; + return BigInt(1e6); }, - now() { + now(): bigint { // performance.now() is in milliseconds, but we want nanoseconds return BigInt(Math.floor(performance.now() * 1e6)); }, - subscribeInstant(instant) { + subscribeInstant(instant: bigint) { instant = BigInt(instant); const now = monotonicClock.now(); if (instant <= now) { - return new Pollable(new Promise((resolve) => setTimeout(resolve, 0))); + return pollableCreate(new Promise((resolve) => setTimeout(resolve, 0))); } return monotonicClock.subscribeDuration(instant - now); }, - subscribeDuration(duration) { + subscribeDuration(duration: bigint) { duration = BigInt(duration); const ms = duration <= 0n ? 0 : Number(duration / 1_000_000n); - return new Pollable(new Promise((resolve) => setTimeout(resolve, ms))); + return pollableCreate(new Promise((resolve) => setTimeout(resolve, ms))); }, }; -export const wallClock = { +export const wallClock: typeof WallClockNamespace = { now() { let now = Date.now(); // in milliseconds const seconds = BigInt(Math.floor(now / 1e3)); diff --git a/packages/preview2-shim/src/browser/config.ts b/packages/preview2-shim/src/browser/config.ts new file mode 100644 index 000000000..b67132b66 --- /dev/null +++ b/packages/preview2-shim/src/browser/config.ts @@ -0,0 +1,9 @@ +let _cwd = "/"; + +export function _setCwd(cwd: string): void { + _cwd = cwd; +} + +export function _getCwd(): string { + return _cwd; +} diff --git a/packages/preview2-shim/src/browser/environment.ts b/packages/preview2-shim/src/browser/environment.ts new file mode 100644 index 000000000..4fa710f5f --- /dev/null +++ b/packages/preview2-shim/src/browser/environment.ts @@ -0,0 +1,30 @@ +import type { environment as EnvironmentNamespace } from "../../types/cli.js"; +import { _setCwd as fsSetCwd } from "./config.js"; + +let _env: [string, string][] = []; +let _args: string[] = []; +let _cwd = "/"; + +export function _setEnv(envObj: Record): void { + _env = Object.entries(envObj); +} + +export function _setArgs(args: string[]): void { + _args = args; +} + +export function _setCwd(cwd: string): void { + fsSetCwd((_cwd = cwd)); +} + +export const environment: typeof EnvironmentNamespace = { + getEnvironment() { + return _env; + }, + getArguments() { + return _args; + }, + initialCwd() { + return _cwd; + }, +}; diff --git a/packages/preview2-shim/lib/browser/filesystem.js b/packages/preview2-shim/src/browser/filesystem.ts similarity index 61% rename from packages/preview2-shim/lib/browser/filesystem.js rename to packages/preview2-shim/src/browser/filesystem.ts index 5827b8955..e715e98a6 100644 --- a/packages/preview2-shim/lib/browser/filesystem.js +++ b/packages/preview2-shim/src/browser/filesystem.ts @@ -1,19 +1,27 @@ -import { streams } from "./io.js"; +import { types as TypesNamespace, preopens as PreopensNamespace } from "../../types/filesystem.js"; +import { Error as IoError } from "../../types/interfaces/wasi-io-error.js"; +import { + InputStream as IInputStream, + OutputStream as IOutputStream, +} from "../../types/interfaces/wasi-io-streams.js"; +import { inputStreamCreate, outputStreamCreate } from "./io.js"; import { environment } from "./environment.js"; - import { _setCwd, _getCwd } from "./config.js"; + export { _setCwd } from "./config.js"; -const { InputStream, OutputStream } = streams; +type Filesize = TypesNamespace.Filesize; +type OpenFlags = TypesNamespace.OpenFlags; +type PathFlags = TypesNamespace.PathFlags; -/** - * @typedef {Object} FileDataEntry - * @property {Record} [dir] - Directory contents (present for directories) - * @property {Uint8Array|string} [source] - File contents (present for files) - */ +export interface FileDataEntry { + // Directory contents (present for directories) + dir?: Record; + // File contents (present for files) + source?: Uint8Array | string; +} /** - * @typedef {FileDataEntry} FileData * Root file data structure representing a filesystem tree. * Each entry is either a directory (has `dir` property) or a file (has `source` property). * @example @@ -24,19 +32,20 @@ const { InputStream, OutputStream } = streams; * } * }; */ +export type FileData = FileDataEntry; -export function _setFileData(fileData) { +export function _setFileData(fileData: FileData): void { _fileData = fileData; - _rootPreopen[0] = new Descriptor(fileData); + _rootPreopen![0] = descriptorCreate(fileData); const cwd = environment.initialCwd(); _setCwd(cwd || "/"); } -export function _getFileData() { +export function _getFileData(): string { return JSON.stringify(_fileData); } -let _fileData = { dir: {} }; +let _fileData: FileData = { dir: {} }; const timeZero = { seconds: BigInt(0), @@ -44,8 +53,8 @@ const timeZero = { }; /** Coerce the given object to a safe integer */ -function coerceToSafeIntegerNumber(obj) { - let n; +function coerceToSafeIntegerNumber(obj: number | bigint): number { + let n: number; if (typeof obj === "number") { n = obj; } else if (typeof obj == "bigint") { @@ -59,17 +68,21 @@ function coerceToSafeIntegerNumber(obj) { return n; } -function getChildEntry(parentEntry, subpath, openFlags) { +function getChildEntry( + parentEntry: FileDataEntry, + subpath: string, + openFlags: OpenFlags, +): FileDataEntry { if (subpath === "." && _rootPreopen && descriptorGetEntry(_rootPreopen[0]) === parentEntry) { subpath = _getCwd(); if (subpath.startsWith("/") && subpath !== "/") { subpath = subpath.slice(1); } } - let entry = parentEntry; - let segmentIdx; + let entry: FileDataEntry | undefined = parentEntry; + let segmentIdx: number; do { - if (!entry || !entry.dir) { + if (!entry?.dir) { throw "not-directory"; } segmentIdx = subpath.indexOf("/"); @@ -93,53 +106,64 @@ function getChildEntry(parentEntry, subpath, openFlags) { return entry; } -function getSource(fileEntry) { +function getSource(fileEntry: FileDataEntry): Uint8Array { if (typeof fileEntry.source === "string") { fileEntry.source = new TextEncoder().encode(fileEntry.source); } - return fileEntry.source; + return fileEntry.source!; } -class DirectoryEntryStream { - constructor(entries) { - this.idx = 0; - this.entries = entries; +class DirectoryEntryStream implements TypesNamespace.DirectoryEntryStream { + idx = 0; + entries: [string, FileDataEntry][] = []; + + static _create(entries: [string, FileDataEntry][]) { + const stream = new DirectoryEntryStream(); + stream.entries = entries; + return stream; } + readDirectoryEntry() { if (this.idx === this.entries.length) { - return null; + return undefined; } const [name, entry] = this.entries[this.idx]; this.idx += 1; return { name, type: entry.dir ? "directory" : "regular-file", - }; + } as TypesNamespace.DirectoryEntry; } } -class Descriptor { - #stream; - #entry; +const descriptorEntryStreamCreate = DirectoryEntryStream._create; +// @ts-expect-error - Deleting static method +delete DirectoryEntryStream._create; + +class Descriptor implements TypesNamespace.Descriptor { + #stream: any; + #entry!: FileDataEntry; #mtime = 0; - _getEntry(descriptor) { + _getEntry(descriptor: Descriptor): FileDataEntry { return descriptor.#entry; } - constructor(entry, isStream) { + static _create(entry: FileDataEntry | any, isStream?: boolean) { + const descriptor = new Descriptor(); if (isStream) { - this.#stream = entry; + descriptor.#stream = entry; } else { - this.#entry = entry; + descriptor.#entry = entry; } + return descriptor; } - readViaStream(_offset) { + readViaStream(_offset: bigint) { const source = getSource(this.#entry); let offset = Number(_offset); - return new InputStream({ - blockingRead(len) { + return inputStreamCreate({ + blockingRead(len: bigint): Uint8Array { if (offset === source.byteLength) { throw { tag: "closed" }; } @@ -147,30 +171,31 @@ class Descriptor { offset += bytes.byteLength; return bytes; }, - }); + }) as IInputStream; } - writeViaStream(_offset) { + writeViaStream(_offset: bigint) { const entry = this.#entry; let offset = Number(_offset); - return new OutputStream({ - write(buf) { - const newSource = new Uint8Array(buf.byteLength + entry.source.byteLength); - newSource.set(entry.source, 0); + return outputStreamCreate({ + write(buf: Uint8Array): void { + const src = entry.source as Uint8Array; + const newSource = new Uint8Array(buf.byteLength + src.byteLength); + newSource.set(src, 0); newSource.set(buf, offset); offset += buf.byteLength; entry.source = newSource; - return buf.byteLength; }, - }); + }) as IOutputStream; } appendViaStream() { console.log(`[filesystem] APPEND STREAM`); + return {} as IOutputStream; } - advise(descriptor, offset, length, advice) { - console.log(`[filesystem] ADVISE`, descriptor, offset, length, advice); + advise(offset: Filesize, length: Filesize, advice: TypesNamespace.Advice) { + console.log(`[filesystem] ADVISE`, offset, length, advice); } syncData() { @@ -179,6 +204,7 @@ class Descriptor { getFlags() { console.log(`[filesystem] FLAGS FOR`); + return {} as TypesNamespace.DescriptorFlags; } getType() { @@ -194,34 +220,38 @@ class Descriptor { return "unknown"; } - setSize(size) { + setSize(size: bigint) { console.log(`[filesystem] SET SIZE`, size); } - setTimes(dataAccessTimestamp, dataModificationTimestamp) { + setTimes(dataAccessTimestamp: any, dataModificationTimestamp: any) { console.log(`[filesystem] SET TIMES`, dataAccessTimestamp, dataModificationTimestamp); } - read(length, offset) { + read(length: bigint, offset: bigint) { const source = getSource(this.#entry); const off = coerceToSafeIntegerNumber(offset); const len = coerceToSafeIntegerNumber(length); - return [source.slice(off, off + len), off + len >= source.byteLength]; + const result: [Uint8Array, boolean] = [ + source.slice(off, off + len), + off + len >= source.byteLength, + ]; + return result; } - write(buffer, offset) { - if (offset !== 0) { + write(buffer: Uint8Array, offset: Filesize) { + if (offset !== 0n) { throw "invalid-seek"; } this.#entry.source = buffer; - return buffer.byteLength; + return BigInt(buffer.byteLength); } readDirectory() { if (!this.#entry?.dir) { throw "bad-descriptor"; } - return new DirectoryEntryStream( + return descriptorEntryStreamCreate( Object.entries(this.#entry.dir).sort(([a], [b]) => (a > b ? 1 : -1)), ); } @@ -230,7 +260,7 @@ class Descriptor { console.log(`[filesystem] SYNC`); } - createDirectoryAt(path) { + createDirectoryAt(path: string) { const entry = getChildEntry(this.#entry, path, { create: true, directory: true, @@ -241,8 +271,8 @@ class Descriptor { } stat() { - let type = "unknown", - size = BigInt(0); + let type: TypesNamespace.DescriptorType = "unknown"; + let size = BigInt(0); if (this.#entry.source) { type = "regular-file"; const source = getSource(this.#entry); @@ -260,13 +290,13 @@ class Descriptor { }; } - statAt(_pathFlags, path) { + statAt(_pathFlags: PathFlags, path: string) { const entry = getChildEntry(this.#entry, path, { create: false, directory: false, }); - let type = "unknown", - size = BigInt(0); + let type: TypesNamespace.DescriptorType = "unknown"; + let size = BigInt(0); if (entry.source) { type = "regular-file"; const source = getSource(entry); @@ -292,13 +322,19 @@ class Descriptor { console.log(`[filesystem] LINK AT`); } - openAt(_pathFlags, path, openFlags, _descriptorFlags, _modes) { + openAt( + _pathFlags: PathFlags, + path: string, + openFlags: OpenFlags, + _flags: TypesNamespace.DescriptorFlags, + ) { const childEntry = getChildEntry(this.#entry, path, openFlags); - return new Descriptor(childEntry); + return descriptorCreate(childEntry); } - readlinkAt() { + readlinkAt(_path: string) { console.log(`[filesystem] READLINK AT`); + return ""; } removeDirectoryAt() { @@ -317,7 +353,7 @@ class Descriptor { console.log(`[filesystem] UNLINK FILE AT`); } - isSameObject(other) { + isSameObject(other: TypesNamespace.Descriptor) { return other === this; } @@ -327,19 +363,22 @@ class Descriptor { return { upper, lower: BigInt(0) }; } - metadataHashAt(_pathFlags, _path) { - let upper = BigInt(0); - upper += BigInt(this.#mtime); - return { upper, lower: BigInt(0) }; + metadataHashAt(_pathFlags: any, _path: string) { + return this.metadataHash(); } } + const descriptorGetEntry = Descriptor.prototype._getEntry; +// @ts-expect-error - Deleting prototype method delete Descriptor.prototype._getEntry; +const descriptorCreate = Descriptor._create; +// @ts-expect-error - Deleting static method +delete Descriptor._create; -let _preopens = [[new Descriptor(_fileData), "/"]], - _rootPreopen = _preopens[0]; +let _preopens: [Descriptor, string][] = [[descriptorCreate(_fileData), "/"]]; +let _rootPreopen: [Descriptor, string] | null = _preopens[0]; -export const preopens = { +export const preopens: typeof PreopensNamespace = { getDirectories() { return _preopens; }, @@ -347,9 +386,9 @@ export const preopens = { /** * Replace all preopens with the given set. - * @param {Record} preopensConfig - Map of virtual paths to file data entries + * @param preopensConfig - Map of virtual paths to file data entries */ -export function _setPreopens(preopensConfig) { +export function _setPreopens(preopensConfig: Record): void { _preopens = []; for (const [virtualPath, fileData] of Object.entries(preopensConfig)) { _addPreopen(virtualPath, fileData); @@ -358,11 +397,11 @@ export function _setPreopens(preopensConfig) { /** * Add a single preopen mapping. - * @param {string} virtualPath - The virtual path visible to the guest - * @param {FileData} fileData - The file data object representing the directory + * @param virtualPath - The virtual path visible to the guest + * @param fileData - The file data object representing the directory */ -export function _addPreopen(virtualPath, fileData) { - const descriptor = new Descriptor(fileData); +export function _addPreopen(virtualPath: string, fileData: FileData): void { + const descriptor = descriptorCreate(fileData); _preopens.push([descriptor, virtualPath]); if (virtualPath === "/") { _rootPreopen = [descriptor, virtualPath]; @@ -375,38 +414,47 @@ export function _addPreopen(virtualPath, fileData) { * This functionality exists mostly to maintain backwards compatibility. Prefer setting preopens * via `WASIShim` rather than making top level changes to preopens using these functions. */ -export function _clearPreopens() { +export function _clearPreopens(): void { _preopens = []; _rootPreopen = null; } /** * Get current preopens configuration. - * @returns {Array<[Descriptor, string]>} Array of [descriptor, virtualPath] pairs + * @returns Array of [descriptor, virtualPath] pairs */ -export function _getPreopens() { +export function _getPreopens(): [Descriptor, string][] { return [..._preopens]; } /** - * Create a preopen descriptor for file data. + * Create a preopen descriptor for a host path. * This is used internally to create isolated preopen instances. - * @param {FileData} fileData - The file data object representing the directory - * @returns {Descriptor} A preopen descriptor + * @param hostPreopen - The host filesystem path + * @returns A preopen descriptor */ -export function _createPreopenDescriptor(fileData) { - return new Descriptor(fileData); +export function _createPreopenDescriptor(hostPreopen: string) { + _fileData.dir = { + [hostPreopen]: {}, + }; + return descriptorCreate(_fileData); } -export const types = { +export const types: typeof TypesNamespace = { Descriptor, DirectoryEntryStream, - filesystemErrorCode(err) { - return convertFsError(err.payload); + filesystemErrorCode: (err: IoError) => { + let message: unknown; + if ("payload" in err) { + message = err.payload; + } else if ("message" in err) { + message = err.message; + } + return convertFsError(message); }, }; -function convertFsError(e) { +function convertFsError(e: any): TypesNamespace.ErrorCode { switch (e.code) { case "EACCES": return "access"; @@ -468,10 +516,6 @@ function convertFsError(e) { return "unsupported"; case "ENOTTY": return "no-tty"; - // windows gives this error for badly structured `//` reads - // this seems like a slightly better error than unknown given - // that it's a common footgun - case -4094: case "ENXIO": return "no-such-device"; case "EOVERFLOW": @@ -488,16 +532,7 @@ function convertFsError(e) { return "text-file-busy"; case "EXDEV": return "cross-device"; - case "UNKNOWN": - switch (e.errno) { - case -4094: - return "no-such-device"; - default: - throw e; - } default: throw e; } } - -export { types as filesystemTypes }; diff --git a/packages/preview2-shim/lib/browser/http.js b/packages/preview2-shim/src/browser/http.ts similarity index 72% rename from packages/preview2-shim/lib/browser/http.js rename to packages/preview2-shim/src/browser/http.ts index adecfcf42..90a82a34e 100644 --- a/packages/preview2-shim/lib/browser/http.js +++ b/packages/preview2-shim/src/browser/http.ts @@ -1,7 +1,13 @@ -import { streams, poll } from "./io.js"; +import type { + incomingHandler as IncomingHandlerNamespace, + outgoingHandler as OutgoingHandlerNamespace, + types as TypesNamespace, +} from "../../types/http.js"; +import type { Error as IoError } from "../../types/interfaces/wasi-io-error.js"; +import type { Pollable } from "../../types/interfaces/wasi-io-poll.js"; +import { inputStreamCreate, outputStreamCreate, pollableCreate } from "./io.js"; -const { InputStream, OutputStream } = streams; -const { Pollable } = poll; +type Result = TypesNamespace.Result; const symbolDispose = Symbol.dispose || Symbol.for("dispose"); const utf8Decoder = new TextDecoder(); @@ -12,25 +18,28 @@ const DEFAULT_HTTP_TIMEOUT_NS = 600_000_000_000n; const TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; const FIELD_VALUE_RE = /^[\t\x20-\x7E\x80-\xFF]*$/; -function validateHeaderName(name) { +function validateHeaderName(name: string): void { if (!TOKEN_RE.test(name)) { throw { tag: "invalid-syntax" }; } } -function validateHeaderValue(value) { +function validateHeaderValue(value: string | Uint8Array): void { const str = typeof value === "string" ? value : utf8Decoder.decode(value); if (!FIELD_VALUE_RE.test(str)) { throw { tag: "invalid-syntax" }; } } -class Fields { +type FieldName = TypesNamespace.FieldName; +type FieldValue = TypesNamespace.FieldValue; + +class Fields implements TypesNamespace.Fields { #immutable = false; - /** @type {[string, Uint8Array][]} */ #entries = []; - /** @type {Map} */ #table = new Map(); + #entries: [FieldName, FieldValue][] = []; + #table = new Map(); - static fromList(entries) { + static fromList(entries: [FieldName, FieldValue][]) { const fields = new Fields(); for (const [key, value] of entries) { fields.append(key, value); @@ -38,7 +47,7 @@ class Fields { return fields; } - get(name) { + get(name: FieldName) { const tableEntries = this.#table.get(name.toLowerCase()); if (!tableEntries) { return []; @@ -59,7 +68,7 @@ class Fields { * * The existing-branch splice/reuse is an allocation optimization — values are cleared, not retained. */ - set(name, values) { + set(name: FieldName, values: FieldValue[]) { if (this.#immutable) { throw { tag: "immutable" }; } @@ -78,19 +87,19 @@ class Fields { } else { this.#table.set(lowercased, []); } - const newTableEntries = this.#table.get(lowercased); + const newTableEntries = this.#table.get(lowercased)!; for (const value of values) { - const entry = [name, value]; + const entry: [FieldName, FieldValue] = [name, value]; this.#entries.push(entry); newTableEntries.push(entry); } } - has(name) { + has(name: string) { return this.#table.has(name.toLowerCase()); } - delete(name) { + delete(name: string) { if (this.#immutable) { throw { tag: "immutable" }; } @@ -102,7 +111,7 @@ class Fields { } } - append(name, value) { + append(name: string, value: Uint8Array) { if (this.#immutable) { throw { tag: "immutable" }; } @@ -112,7 +121,7 @@ class Fields { if (forbiddenHeaders.has(lowercased)) { throw { tag: "forbidden" }; } - const entry = [name, value]; + const entry: [string, Uint8Array] = [name, value]; this.#entries.push(entry); const tableEntries = this.#table.get(lowercased); if (tableEntries) { @@ -130,12 +139,12 @@ class Fields { return fieldsFromEntriesChecked(this.#entries); } - static _lock(fields) { + static _lock(fields: Fields): Fields { fields.#immutable = true; return fields; } - static _fromEntriesChecked(entries) { + static _fromEntriesChecked(entries: [string, Uint8Array][]): Fields { const fields = new Fields(); fields.#entries = entries; for (const entry of entries) { @@ -151,36 +160,44 @@ class Fields { } } const fieldsLock = Fields._lock; +// @ts-expect-error - Deleting static method delete Fields._lock; const fieldsFromEntriesChecked = Fields._fromEntriesChecked; +// @ts-expect-error - Deleting static method delete Fields._fromEntriesChecked; -class RequestOptions { +class RequestOptions implements TypesNamespace.RequestOptions { #connectTimeout = DEFAULT_HTTP_TIMEOUT_NS; #firstByteTimeout = DEFAULT_HTTP_TIMEOUT_NS; #betweenBytesTimeout = DEFAULT_HTTP_TIMEOUT_NS; + connectTimeout() { return this.#connectTimeout; } - setConnectTimeout(duration) { + + setConnectTimeout(duration: bigint) { if (duration < 0n) { throw new Error("duration must not be negative"); } this.#connectTimeout = duration; } + firstByteTimeout() { return this.#firstByteTimeout; } - setFirstByteTimeout(duration) { + + setFirstByteTimeout(duration: bigint) { if (duration < 0n) { throw new Error("duration must not be negative"); } this.#firstByteTimeout = duration; } + betweenBytesTimeout() { return this.#betweenBytesTimeout; } - setBetweenBytesTimeout(duration) { + + setBetweenBytesTimeout(duration: bigint) { if (duration < 0n) { throw new Error("duration must not be negative"); } @@ -188,9 +205,9 @@ class RequestOptions { } } -class OutgoingBody { - #outputStream = null; - #chunks = []; +class OutgoingBody implements TypesNamespace.OutgoingBody { + #outputStream: any = null; + #chunks: Uint8Array[] = []; #finished = false; write() { @@ -202,7 +219,7 @@ class OutgoingBody { return outputStream; } - static finish(body, trailers) { + static finish(body: OutgoingBody, trailers?: any) { if (trailers) { throw { tag: "internal-error", val: "trailers unsupported" }; } @@ -212,7 +229,7 @@ class OutgoingBody { body.#finished = true; } - static _bodyData(outgoingBody) { + static _bodyData(outgoingBody: OutgoingBody): Uint8Array | null { if (outgoingBody.#chunks.length === 0) { return null; } @@ -229,17 +246,16 @@ class OutgoingBody { return result; } - static _create() { + static _create(): OutgoingBody { const outgoingBody = new OutgoingBody(); const chunks = outgoingBody.#chunks; - outgoingBody.#outputStream = new OutputStream({ - write(buf) { + outgoingBody.#outputStream = outputStreamCreate({ + write(buf: Uint8Array): void { chunks.push(new Uint8Array(buf)); }, - flush() {}, blockingFlush() {}, - subscribe() { - return new Pollable(); + subscribe(): any { + return pollableCreate(); }, }); return outgoingBody; @@ -248,20 +264,25 @@ class OutgoingBody { [symbolDispose]() {} } const outgoingBodyCreate = OutgoingBody._create; +// @ts-expect-error - Deleting static method delete OutgoingBody._create; const outgoingBodyData = OutgoingBody._bodyData; +// @ts-expect-error - Deleting static method delete OutgoingBody._bodyData; -class OutgoingRequest { - /** @type {{ tag: string, val?: string }} */ #method = { tag: "get" }; - /** @type {{ tag: string, val?: string } | undefined} */ #scheme = undefined; - /** @type {string | undefined} */ #pathWithQuery = undefined; - /** @type {string | undefined} */ #authority = undefined; - /** @type {Fields} */ #headers; - /** @type {OutgoingBody} */ #body; +type Method = TypesNamespace.Method; +type Scheme = TypesNamespace.Scheme; + +class OutgoingRequest implements TypesNamespace.OutgoingRequest { + #method: Method = { tag: "get" }; + #scheme: Scheme | undefined = undefined; + #pathWithQuery: string | undefined = undefined; + #authority: string | undefined = undefined; + #headers: Fields; + #body: OutgoingBody; #bodyRequested = false; - constructor(headers) { + constructor(headers: Fields) { fieldsLock(headers); this.#headers = headers; this.#body = outgoingBodyCreate(); @@ -279,8 +300,8 @@ class OutgoingRequest { return this.#method; } - setMethod(method) { - if (method.tag === "other" && !method.val.match(/^[a-zA-Z-]+$/)) { + setMethod(method: Method) { + if (method.tag === "other" && method.val && !method.val.match(/^[a-zA-Z-]+$/)) { throw undefined; } this.#method = method; @@ -290,7 +311,7 @@ class OutgoingRequest { return this.#pathWithQuery; } - setPathWithQuery(pathWithQuery) { + setPathWithQuery(pathWithQuery: string | undefined) { if (pathWithQuery && !pathWithQuery.match(/^[a-zA-Z0-9.\-_~!$&'()*+,;=:@%?/]+$/)) { throw undefined; } @@ -301,8 +322,8 @@ class OutgoingRequest { return this.#scheme; } - setScheme(scheme) { - if (scheme?.tag === "other" && !scheme.val.match(/^[a-zA-Z]+$/)) { + setScheme(scheme: Scheme | undefined) { + if (scheme?.tag === "other" && scheme.val && !scheme.val.match(/^[a-zA-Z]+$/)) { throw undefined; } this.#scheme = scheme; @@ -312,7 +333,7 @@ class OutgoingRequest { return this.#authority; } - setAuthority(authority) { + setAuthority(authority: string | undefined) { if (authority) { const [host, port, ...extra] = authority.split(":"); const portNum = Number(port); @@ -333,9 +354,9 @@ class OutgoingRequest { [symbolDispose]() {} - static _handle(request, options) { + static _handle(request: OutgoingRequest, options?: RequestOptions): FutureIncomingResponse { const scheme = schemeString(request.#scheme); - const method = request.#method.val || request.#method.tag; + const method = "val" in request.#method ? request.#method.val : request.#method.tag; if (!request.#pathWithQuery) { throw { tag: "HTTP-request-URI-invalid" }; @@ -365,11 +386,12 @@ class OutgoingRequest { } } const outgoingRequestHandle = OutgoingRequest._handle; +// @ts-expect-error - Deleting static method delete OutgoingRequest._handle; -class IncomingBody { +class IncomingBody implements TypesNamespace.IncomingBody { #finished = false; - #stream = undefined; + #stream: any = undefined; stream() { if (!this.#stream) { @@ -380,7 +402,7 @@ class IncomingBody { return stream; } - static finish(incomingBody) { + static finish(incomingBody: IncomingBody) { if (incomingBody.#finished) { throw new Error("incoming body already finished"); } @@ -390,13 +412,13 @@ class IncomingBody { [symbolDispose]() {} - static _create(fetchResponse) { + static _create(fetchResponse: Response): IncomingBody { const incomingBody = new IncomingBody(); - let buffer = null; + let buffer: Uint8Array | null = null; let bufferOffset = 0; let done = false; - let reader = null; - let readPromise = null; + let reader: ReadableStreamDefaultReader | null = null; + let readPromise: Promise | null = null; function ensureReader() { if (!reader && fetchResponse.body) { @@ -430,8 +452,8 @@ class IncomingBody { ); } - incomingBody.#stream = new InputStream({ - read(len) { + incomingBody.#stream = inputStreamCreate({ + read(len: bigint) { if (done && (buffer === null || bufferOffset >= buffer.byteLength)) { throw { tag: "closed" }; } @@ -451,7 +473,7 @@ class IncomingBody { } throw { tag: "would-block" }; }, - blockingRead(len) { + blockingRead(len: bigint): any { if (done && (buffer === null || bufferOffset >= buffer.byteLength)) { throw { tag: "closed" }; } @@ -494,13 +516,13 @@ class IncomingBody { }, subscribe() { if (done || (buffer !== null && bufferOffset < buffer.byteLength)) { - return new Pollable(); + return pollableCreate(); } startRead(); if (readPromise) { - return new Pollable(readPromise); + return pollableCreate(readPromise); } - return new Pollable(); + return pollableCreate(); }, }); @@ -509,12 +531,13 @@ class IncomingBody { } } const incomingBodyCreate = IncomingBody._create; +// @ts-expect-error - Deleting static method delete IncomingBody._create; -class IncomingResponse { - /** @type {Fields} */ #headers = undefined; +class IncomingResponse implements TypesNamespace.IncomingResponse { + #headers: Fields = undefined as any; #status = 0; - /** @type {IncomingBody} */ #body; + #body: IncomingBody | undefined; status() { return this.#status; @@ -535,11 +558,11 @@ class IncomingResponse { [symbolDispose]() {} - static _create(fetchResponse) { + static _create(fetchResponse: Response): IncomingResponse { const res = new IncomingResponse(); res.#status = fetchResponse.status; - const headerEntries = []; + const headerEntries: [string, Uint8Array][] = []; const encoder = new TextEncoder(); fetchResponse.headers.forEach((value, key) => { headerEntries.push([key, encoder.encode(value)]); @@ -550,16 +573,21 @@ class IncomingResponse { } } const incomingResponseCreate = IncomingResponse._create; +// @ts-expect-error - Deleting static method delete IncomingResponse._create; -class FutureTrailers { +class FutureTrailers implements TypesNamespace.FutureTrailers { #requested = false; - subscribe() { - return new Pollable(); + + subscribe(): any { + return pollableCreate(); } - get() { + + get(): + | Result, void> + | undefined { if (this.#requested) { - return { tag: "err" }; + return { tag: "err", val: undefined }; } this.#requested = true; return { @@ -570,14 +598,16 @@ class FutureTrailers { }, }; } - static _create() { + + static _create(): FutureTrailers { return new FutureTrailers(); } } const futureTrailersCreate = FutureTrailers._create; +// @ts-expect-error - Deleting static method delete FutureTrailers._create; -function mapFetchError(err) { +function mapFetchError(err: Error) { if (err.name === "AbortError") { return { tag: "connection-timeout" }; } @@ -587,12 +617,12 @@ function mapFetchError(err) { return { tag: "internal-error", val: err.message }; } -class FutureIncomingResponse { - #result = undefined; - #promise = null; +class FutureIncomingResponse implements TypesNamespace.FutureIncomingResponse { + #result: any = undefined; + #promise: Promise | null = null; - subscribe() { - return new Pollable(this.#promise); + subscribe(): Pollable { + return pollableCreate(this.#promise!); } get() { @@ -608,22 +638,28 @@ class FutureIncomingResponse { this.#promise = null; } - static _create(url, method, headers, bodyData, timeoutMs) { + static _create( + url: string, + method: string, + headers: Headers, + bodyData: Uint8Array | null, + timeoutMs: number, + ): FutureIncomingResponse { const future = new FutureIncomingResponse(); const controller = new AbortController(); - let timer; + let timer: ReturnType | undefined; if (timeoutMs < Infinity) { timer = setTimeout(() => controller.abort(), timeoutMs); } - const init = { + const init: RequestInit = { method, headers, signal: controller.signal, }; if (bodyData && method !== "GET" && method !== "HEAD") { - init.body = bodyData; + init.body = bodyData as BodyInit; } future.#promise = fetch(url, init).then( @@ -657,9 +693,10 @@ class FutureIncomingResponse { } } const futureIncomingResponseCreate = FutureIncomingResponse._create; +// @ts-expect-error - Deleting static method delete FutureIncomingResponse._create; -function schemeString(scheme) { +function schemeString(scheme: Scheme | undefined): string { if (!scheme) { return "https:"; } @@ -669,38 +706,44 @@ function schemeString(scheme) { case "HTTPS": return "https:"; case "other": - return scheme.val.toLowerCase() + ":"; + return scheme.val!.toLowerCase() + ":"; } + return "https:"; } -function httpErrorCode(err) { - if (err.payload) { - return err.payload; +function httpErrorCode(err: IoError): TypesNamespace.ErrorCode | undefined { + if ("payload" in err) { + return err.payload as TypesNamespace.ErrorCode; } return { tag: "internal-error", - val: err.message, + val: "message" in err ? (err.message as string) : err.toDebugString(), }; } -export const outgoingHandler = { +export const outgoingHandler: typeof OutgoingHandlerNamespace = { + // @ts-expect-error Not matching signature in WIT handle: outgoingRequestHandle, }; -export const incomingHandler = { +export const incomingHandler: typeof IncomingHandlerNamespace = { + // Not implemented handle() {}, }; -export const types = { +export const types: typeof TypesNamespace = { Fields, FutureIncomingResponse, FutureTrailers, IncomingBody, + // @ts-expect-error Not implemented IncomingRequest: class IncomingRequest {}, IncomingResponse, OutgoingBody, OutgoingRequest, + // @ts-expect-error Not implemented OutgoingResponse: class OutgoingResponse {}, + // @ts-expect-error Not implemented ResponseOutparam: class ResponseOutparam {}, RequestOptions, httpErrorCode, diff --git a/packages/preview2-shim/src/browser/index.ts b/packages/preview2-shim/src/browser/index.ts new file mode 100644 index 000000000..4e49f6d32 --- /dev/null +++ b/packages/preview2-shim/src/browser/index.ts @@ -0,0 +1,7 @@ +export * as cli from "./cli.js"; +export * as clocks from "./clocks.js"; +export * as filesystem from "./filesystem.js"; +export * as http from "./http.js"; +export * as io from "./io.js"; +export * as random from "./random.js"; +export * as sockets from "./sockets.js"; diff --git a/packages/preview2-shim/src/browser/io.ts b/packages/preview2-shim/src/browser/io.ts new file mode 100644 index 000000000..419858fe9 --- /dev/null +++ b/packages/preview2-shim/src/browser/io.ts @@ -0,0 +1,272 @@ +import type { + error as ErrorNamespace, + poll as PollNamespace, + streams as StreamsNamespace, +} from "../../types/io.js"; + +let id = 0; + +const symbolDispose = Symbol.dispose || Symbol.for("dispose"); + +type IInputStream = StreamsNamespace.InputStream; +type IOutputStream = StreamsNamespace.OutputStream; + +/** + * Handler interface for creating custom input streams + */ +export type InputStreamHandler = Partial & + Required> & { + drop?: () => void; + }; + +/** + * Handler interface for creating custom output streams + */ +export type OutputStreamHandler = Partial & + Required> & { + drop?: () => void; + }; + +class IoError extends Error implements ErrorNamespace.Error { + toDebugString() { + return this.message; + } +} + +class InputStream implements IInputStream { + id!: number; + handler!: InputStreamHandler; + + static _create(handler: InputStreamHandler) { + const stream = new InputStream(); + if (!handler) { + console.trace("no handler"); + } + stream.id = ++id; + stream.handler = handler; + return stream; + } + + read(len: bigint) { + if (this.handler.read) { + return this.handler.read(len); + } + return this.handler.blockingRead.call(this, len); + } + + blockingRead(len: bigint) { + return this.handler.blockingRead.call(this, len); + } + + skip(len: bigint) { + if (this.handler.skip) { + return this.handler.skip.call(this, len); + } + if (this.handler.read) { + const bytes = this.handler.read.call(this, len); + return BigInt(bytes.byteLength); + } + return this.blockingSkip.call(this, len); + } + + blockingSkip(len: bigint) { + if (this.handler.blockingSkip) { + return this.handler.blockingSkip.call(this, len); + } + const bytes = this.handler.blockingRead.call(this, len); + return BigInt(bytes.byteLength); + } + + subscribe() { + if (this.handler.subscribe) { + return this.handler.subscribe(); + } + return new Pollable(); + } + + [symbolDispose]() { + if (this.handler.drop) { + this.handler.drop.call(this); + } + } +} + +export const inputStreamCreate = InputStream._create; +// @ts-expect-error - Deleting static method +delete InputStream._create; + +class OutputStream implements IOutputStream { + id!: number; + open!: boolean; + handler!: OutputStreamHandler; + + static _create(handler: OutputStreamHandler) { + const stream = new OutputStream(); + if (!handler) { + console.trace("no handler"); + } + stream.id = ++id; + stream.open = true; + stream.handler = handler; + return stream; + } + + checkWrite() { + if (!this.open) { + return 0n; + } + if (this.handler.checkWrite) { + return this.handler.checkWrite.call(this); + } + return 1_000_000n; + } + + write(buf: Uint8Array) { + this.handler.write.call(this, buf); + } + + blockingWriteAndFlush(buf: Uint8Array) { + if (this.handler.blockingWriteAndFlush) { + return this.handler.blockingWriteAndFlush.call(this, buf); + } + this.handler.write.call(this, buf); + } + + flush() { + if (this.handler.flush) { + this.handler.flush.call(this); + } + } + + blockingFlush() { + this.open = true; + if (this.handler.blockingFlush) { + this.handler.blockingFlush.call(this); + } + } + + writeZeroes(len: bigint) { + this.write.call(this, new Uint8Array(Number(len))); + } + + blockingWriteZeroesAndFlush(len: bigint) { + this.blockingWriteAndFlush.call(this, new Uint8Array(Number(len))); + } + + splice(src: InputStream, len: bigint) { + const spliceLen = Math.min(Number(len), Number(this.checkWrite.call(this))); + const bytes = src.read(BigInt(spliceLen)); + this.write.call(this, bytes); + return BigInt(bytes.byteLength); + } + + blockingSplice(_src: InputStream, _len: bigint) { + console.log(`[streams] Blocking splice ${this.id}`); + return 0n; + } + + subscribe() { + if (this.handler.subscribe) { + return this.handler.subscribe(); + } + return new Pollable(); + } + + [symbolDispose]() {} +} + +export const outputStreamCreate = OutputStream._create; +// @ts-expect-error - Deleting static method +delete OutputStream._create; + +export const error: typeof ErrorNamespace = { + Error: IoError, +}; + +export const streams: typeof StreamsNamespace = { InputStream, OutputStream }; + +class Pollable implements PollNamespace.Pollable { + #ready = false; + #promise: Promise | null = null; + + static _create(promise?: Promise) { + const pollable = new Pollable(); + if (!promise) { + pollable.#ready = true; + } else { + pollable.#promise = promise.then( + () => { + pollable.#ready = true; + }, + () => { + pollable.#ready = true; + }, + ); + } + return pollable; + } + + ready() { + return this.#ready; + } + + block() { + if (this.#ready) { + return Promise.resolve(); + } + return this.#promise || Promise.resolve(); + } + + [symbolDispose]() { + this.#promise = null; + } +} + +export const pollableCreate = Pollable._create; +// @ts-expect-error - Deleting static method +delete Pollable._create; + +function pollList(list: Pollable[]): Uint32Array | Promise { + if (list.length === 0) { + throw new Error("poll list must not be empty"); + } + if (list.length > 0xffffffff) { + throw new Error("poll list length exceeds u32 index range"); + } + const ready: number[] = []; + for (let i = 0; i < list.length; i++) { + if (list[i].ready()) { + ready.push(i); + } + } + if (ready.length > 0) { + return new Uint32Array(ready); + } + // None ready synchronously. Wait for the first to resolve via Promise.race, + // then sweep for any others that became ready concurrently. + return Promise.race( + list.map((p, i) => + p.block().then(() => { + const result = [i]; + for (let j = 0; j < list.length; j++) { + if (j !== i && list[j].ready()) { + result.push(j); + } + } + return new Uint32Array(result); + }), + ), + ); +} + +function pollOne(poll: Pollable): Promise { + return poll.block(); +} + +export const poll: typeof PollNamespace = { + Pollable, + pollList, + pollOne, + // @ts-expect-error Not matching signature from WIT + poll: pollList, +}; diff --git a/packages/preview2-shim/lib/browser/random.js b/packages/preview2-shim/src/browser/random.ts similarity index 61% rename from packages/preview2-shim/lib/browser/random.js rename to packages/preview2-shim/src/browser/random.ts index 503dc176e..a524267c8 100644 --- a/packages/preview2-shim/lib/browser/random.js +++ b/packages/preview2-shim/src/browser/random.ts @@ -1,9 +1,15 @@ +import type { + insecure as InsecureNamespace, + insecureSeed as InsecureSeedNamespace, + random as RandomNamespace, +} from "../../types/random.js"; + const MAX_BYTES = 65536; -let insecureRandomValue1, insecureRandomValue2; +let insecureRandomValue1: bigint | undefined, insecureRandomValue2: bigint | undefined; -export const insecure = { - getInsecureRandomBytes(len) { +export const insecure: typeof InsecureNamespace = { + getInsecureRandomBytes(len: bigint) { return random.getRandomBytes(len); }, getInsecureRandomU64() { @@ -11,11 +17,11 @@ export const insecure = { }, }; -let insecureSeedValue1, insecureSeedValue2; +let insecureSeedValue1: bigint | undefined, insecureSeedValue2: bigint | undefined; -export const insecureSeed = { +export const insecureSeed: typeof InsecureSeedNamespace = { insecureSeed() { - if (insecureSeedValue1 === undefined) { + if (insecureSeedValue1 === undefined || insecureSeedValue2 === undefined) { insecureSeedValue1 = random.getRandomU64(); insecureSeedValue2 = random.getRandomU64(); } @@ -23,8 +29,8 @@ export const insecureSeed = { }, }; -export const random = { - getRandomBytes(len) { +export const random: typeof RandomNamespace = { + getRandomBytes(len: bigint) { const bytes = new Uint8Array(Number(len)); if (len > MAX_BYTES) { @@ -46,8 +52,9 @@ export const random = { return crypto.getRandomValues(new BigUint64Array(1))[0]; }, + // @ts-expect-error Not defined in WIT insecureRandom() { - if (insecureRandomValue1 === undefined) { + if (insecureRandomValue1 === undefined || insecureRandomValue2 === undefined) { insecureRandomValue1 = random.getRandomU64(); insecureRandomValue2 = random.getRandomU64(); } diff --git a/packages/preview2-shim/lib/browser/sockets.js b/packages/preview2-shim/src/browser/sockets.ts similarity index 61% rename from packages/preview2-shim/lib/browser/sockets.js rename to packages/preview2-shim/src/browser/sockets.ts index 1c9554bcd..c0816ff7f 100644 --- a/packages/preview2-shim/lib/browser/sockets.js +++ b/packages/preview2-shim/src/browser/sockets.ts @@ -1,10 +1,21 @@ -export const instanceNetwork = { +// @ts-nocheck +import type { + instanceNetwork as InstanceNetworkNamespace, + ipNameLookup as IpNameLookupNamespace, + network as NetworkNamespace, + tcpCreateSocket as TcpCreateSocketNamespace, + tcp as TcpNamespace, + udpCreateSocket as UdpCreateSocketNamespace, + udp as UdpNamespace, +} from "../../types/sockets.js"; + +export const instanceNetwork: typeof InstanceNetworkNamespace = { instanceNetwork() { console.log(`[sockets] instance network`); }, }; -export const ipNameLookup = { +export const ipNameLookup: typeof IpNameLookupNamespace = { dropResolveAddressStream() {}, subscribe() {}, resolveAddresses() {}, @@ -13,15 +24,15 @@ export const ipNameLookup = { setNonBlocking() {}, }; -export const network = { +export const network: typeof NetworkNamespace = { dropNetwork() {}, }; -export const tcpCreateSocket = { +export const tcpCreateSocket: typeof TcpCreateSocketNamespace = { createTcpSocket() {}, }; -export const tcp = { +export const tcp: typeof TcpNamespace = { subscribe() {}, dropTcpSocket() {}, bind() {}, @@ -47,42 +58,26 @@ export const tcp = { shutdown() {}, }; -export const udp = { - subscribe() {}, +export const udpCreateSocket: typeof UdpCreateSocketNamespace = { + createUdpSocket() {}, +}; +export const udp: typeof UdpNamespace = { + subscribe() {}, dropUdpSocket() {}, - bind() {}, - connect() {}, - receive() {}, - send() {}, - localAddress() {}, - remoteAddress() {}, - addressFamily() {}, - unicastHopLimit() {}, - setUnicastHopLimit() {}, - receiveBufferSize() {}, - setReceiveBufferSize() {}, - sendBufferSize() {}, - setSendBufferSize() {}, - nonBlocking() {}, - setNonBlocking() {}, }; - -export const udpCreateSocket = { - createUdpSocket() {}, -}; diff --git a/packages/preview2-shim/lib/common/instantiation.js b/packages/preview2-shim/src/common/instantiation.ts similarity index 87% rename from packages/preview2-shim/lib/common/instantiation.js rename to packages/preview2-shim/src/common/instantiation.ts index a27a2dd2a..dc0068c4d 100644 --- a/packages/preview2-shim/lib/common/instantiation.js +++ b/packages/preview2-shim/src/common/instantiation.ts @@ -1,4 +1,10 @@ import * as wasi from "@bytecodealliance/preview2-shim"; +import { types, _createPreopenDescriptor } from "@bytecodealliance/preview2-shim/filesystem"; +import type { + WASIShimConfig, + GetImportObjectArgs, + WASIImportObject, +} from "../../types/instantiation.js"; /** * (EXPERIMENTAL) A class that holds WASI shims and can be used to configure @@ -80,30 +86,30 @@ import * as wasi from "@bytecodealliance/preview2-shim"; */ export class WASIShim { /** Object that confirms to the shim interface for `wasi:cli` */ - #cli; + #cli: any; /** Object that confirms to the shim interface for `wasi:filesystem` */ - #filesystem; + #filesystem: any; /** Object that confirms to the shim interface for `wasi:io` */ - #io; + #io: any; /** Object that confirms to the shim interface for `wasi:random` */ - #random; + #random: any; /** Object that confirms to the shim interface for `wasi:clocks` */ - #clocks; + #clocks: any; /** Object that confirms to the shim interface for `wasi:sockets` */ - #sockets; + #sockets: any; /** Object that confirms to the shim interface for `wasi:http` */ - #http; + #http: any; /** Isolated preopens for this instance */ - #preopens; + #preopens: any; /** Isolated environment for this instance */ - #environment; + #environment: any; /** * Create a new WASIShim instance. * - * @param {import('../types/instantiation.d.ts').WASIShimConfig} [config] - Configuration options + * @param config - Configuration options */ - constructor(config) { + constructor(config?: WASIShimConfig) { // Support both old 'shims' parameter name and new 'config' style const shims = config; @@ -148,14 +154,10 @@ export class WASIShim { * functions like `instantiate` that are exposed from a transpiled * WebAssembly component. * - * @param {GetImportObjectArgs} [opts] - options for import object generation - * @returns {WASIImportObject} - * - * @typedef {{ - * asVersion?: number, - * }} GetImportObjectArgs + * @param opts - options for import object generation + * @returns WASIImportObject */ - getImportObject(opts) { + getImportObject(opts?: GetImportObjectArgs) { const versionSuffix = opts?.asVersion ? `@${opts.asVersion}` : ""; const obj = {}; @@ -196,19 +198,18 @@ export class WASIShim { obj[`wasi:http/types${versionSuffix}`] = this.#http.types; obj[`wasi:http/outgoing-handler${versionSuffix}`] = this.#http.outgoingHandler; - return obj; + return obj as WASIImportObject; } } /** * Create an isolated preopens object with its own preopen entries. * - * @param {Record} preopensConfig - Map of virtual paths to host paths - * @returns {object} A preopens object with Descriptor and getDirectories() + * @param preopensConfig - Map of virtual paths to host paths + * @returns A preopens object with Descriptor and getDirectories() */ -function createIsolatedPreopens(preopensConfig) { - const { types, _createPreopenDescriptor } = wasi.filesystem; - const entries = []; +function createIsolatedPreopens(preopensConfig: Record) { + const entries: any[] = []; // Populate entries using the filesystem's descriptor creation if (_createPreopenDescriptor) { @@ -229,12 +230,16 @@ function createIsolatedPreopens(preopensConfig) { /** * Create an isolated CLI environment with its own env and args. * - * @param {Record} env - Environment variables - * @param {string[]} args - Command-line arguments - * @param {object} baseCli - The base CLI module to extend - * @returns {object} An isolated CLI environment object + * @param env - Environment variables + * @param args - Command-line arguments + * @param baseCli - The base CLI module to extend + * @returns An isolated CLI environment object */ -function createIsolatedEnvironment(env, args, baseCli) { +function createIsolatedEnvironment( + env: Record | undefined, + args: string[] | undefined, + baseCli: any, +) { const envEntries = env ? Object.entries(env) : null; const argsArray = args || null; diff --git a/packages/preview2-shim/lib/io/calls.js b/packages/preview2-shim/src/io/calls.ts similarity index 100% rename from packages/preview2-shim/lib/io/calls.js rename to packages/preview2-shim/src/io/calls.ts diff --git a/packages/preview2-shim/lib/io/worker-http.js b/packages/preview2-shim/src/io/worker-http.ts similarity index 91% rename from packages/preview2-shim/lib/io/worker-http.js rename to packages/preview2-shim/src/io/worker-http.ts index 78bc0ad2d..d96de44e4 100644 --- a/packages/preview2-shim/lib/io/worker-http.js +++ b/packages/preview2-shim/src/io/worker-http.ts @@ -1,5 +1,11 @@ import { createReadableStream, getStreamOrThrow } from "./worker-thread.js"; -import { createServer, request as httpRequest, Agent as HttpAgent } from "node:http"; +import { + createServer, + request as httpRequest, + Agent as HttpAgent, + ClientRequest, + IncomingMessage, +} from "node:http"; import { request as httpsRequest, Agent as HttpsAgent } from "node:https"; import { parentPort } from "node:worker_threads"; import { HTTP_SERVER_INCOMING_HANDLER } from "./calls.js"; @@ -45,7 +51,7 @@ export async function startHttpServer(id, { port, host }) { // create the streams and their ids const streamId = createReadableStream(req); const responseId = ++responseCnt; - parentPort.postMessage({ + parentPort?.postMessage({ type: HTTP_SERVER_INCOMING_HANDLER, id, payload: { @@ -54,7 +60,7 @@ export async function startHttpServer(id, { port, host }) { host: req.headers.host || host || "localhost", pathWithQuery: req.url, headers: Object.entries(req.headersDistinct).flatMap(([key, val]) => - val.map((val) => [key, val]), + val?.map((val) => [key, val]), ), streamId, }, @@ -62,6 +68,7 @@ export async function startHttpServer(id, { port, host }) { responses.set(responseId, res); }); await new Promise((resolve, reject) => { + // @ts-expect-error Resolve has different signature server.listen(port, host, resolve); server.on("error", reject); }); @@ -79,11 +86,11 @@ export async function createHttpRequest( betweenBytesTimeout, firstByteTimeout, ) { - let stream = null; + let stream: NodeJS.ReadableStream | null = null; if (bodyId) { try { ({ stream } = getStreamOrThrow(bodyId)); - } catch (e) { + } catch (e: any) { if (e.tag === "closed") { throw { tag: "internal-error", @@ -105,7 +112,7 @@ export async function createHttpRequest( } try { // Make a request - let req; + let req: ClientRequest; switch (scheme) { case "http:": req = httpRequest({ @@ -139,7 +146,7 @@ export async function createHttpRequest( } else { req.end(); } - const res = await new Promise((resolve, reject) => { + const res: IncomingMessage = await new Promise((resolve, reject) => { req.once("timeout", () => { reject({ tag: "connection-timeout", @@ -164,7 +171,7 @@ export async function createHttpRequest( headers: Array.from(Object.entries(res.headers)), bodyStreamId, }; - } catch (e) { + } catch (e: any) { if (e?.tag) { throw e; } diff --git a/packages/preview2-shim/lib/io/worker-io.js b/packages/preview2-shim/src/io/worker-io.ts similarity index 91% rename from packages/preview2-shim/lib/io/worker-io.js rename to packages/preview2-shim/src/io/worker-io.ts index 4b1a383cc..e3b441f41 100644 --- a/packages/preview2-shim/lib/io/worker-io.js +++ b/packages/preview2-shim/src/io/worker-io.ts @@ -35,7 +35,17 @@ import { } from "./calls.js"; import nodeProcess, { exit, stderr, stdout, env } from "node:process"; -const _rawDebug = nodeProcess._rawDebug || console.error.bind(console); +import type { streams as StreamsNamespace } from "../../types/io.js"; + +const symbolDispose = Symbol.dispose || Symbol.for("dispose"); + +type IInputStream = StreamsNamespace.InputStream; +type IOutputStream = StreamsNamespace.OutputStream; + +const _rawDebug = + "_rawDebug" in nodeProcess + ? (nodeProcess._rawDebug as (...args: unknown[]) => void) + : console.error.bind(console); const workerPath = fileURLToPath(new URL("./worker-thread.js", import.meta.url)); @@ -90,20 +100,16 @@ if (DEBUG) { }; } -const symbolDispose = Symbol.dispose || Symbol.for("dispose"); - -const finalizationRegistry = new FinalizationRegistry((dispose) => void dispose()); +const finalizationRegistry = new FinalizationRegistry((dispose: any) => void dispose()); const dummySymbol = Symbol(); -/** - * - * @param {any} resource - * @param {any} parentResource - * @param {number} id - * @param {(number) => void} disposeFn - */ -export function registerDispose(resource, parentResource, id, disposeFn) { +export function registerDispose( + resource: any, + parentResource: any, + id: number, + disposeFn: (id: number) => void, +) { // While strictly speaking all components should handle their disposal, // this acts as a last-resort to catch all missed drops through the JS GC. // Mainly for two cases - (1) components which are long lived, that get shut @@ -131,7 +137,8 @@ export function earlyDispose(finalizer) { const _Error = Error; const IoError = class Error extends _Error { - constructor(payload) { + payload: any; + constructor(payload: any) { super(payload); this.payload = payload; } @@ -143,7 +150,7 @@ const IoError = class Error extends _Error { function streamIoErrorCall(call, id, payload) { try { return ioCall(call, id, payload); - } catch (e) { + } catch (e: any) { if (e.tag === "closed") { throw e; } @@ -157,7 +164,7 @@ function streamIoErrorCall(call, id, payload) { } } -class InputStream { +class InputStream implements IInputStream { #id; #streamType; #finalizer; @@ -233,17 +240,19 @@ function httpInputStreamDispose(id) { } export const inputStreamCreate = InputStream._create; +// @ts-expect-error - Deleting static method delete InputStream._create; export const inputStreamId = InputStream._id; +// @ts-expect-error - Deleting static method delete InputStream._id; -class OutputStream { +class OutputStream implements IOutputStream { #id; #streamType; #finalizer; - checkWrite(len) { - return streamIoErrorCall(OUTPUT_STREAM_CHECK_WRITE | this.#streamType, this.#id, len); + checkWrite() { + return streamIoErrorCall(OUTPUT_STREAM_CHECK_WRITE | this.#streamType, this.#id, null); } write(buf) { if (this.#streamType <= STDERR) { @@ -263,10 +272,10 @@ class OutputStream { ); } flush() { - return streamIoErrorCall(OUTPUT_STREAM_FLUSH | this.#streamType, this.#id); + return streamIoErrorCall(OUTPUT_STREAM_FLUSH | this.#streamType, this.#id, undefined); } blockingFlush() { - return streamIoErrorCall(OUTPUT_STREAM_BLOCKING_FLUSH | this.#streamType, this.#id); + return streamIoErrorCall(OUTPUT_STREAM_BLOCKING_FLUSH | this.#streamType, this.#id, undefined); } writeZeroes(len) { return streamIoErrorCall(OUTPUT_STREAM_WRITE_ZEROES | this.#streamType, this.#id, len); @@ -291,7 +300,7 @@ class OutputStream { }); } subscribe() { - return pollableCreate(ioCall(OUTPUT_STREAM_SUBSCRIBE | this.#streamType, this.#id)); + return pollableCreate(ioCall(OUTPUT_STREAM_SUBSCRIBE | this.#streamType, this.#id), undefined); } static _id(outputStream) { @@ -355,9 +364,11 @@ function fileOutputStreamDispose(id) { } export const outputStreamCreate = OutputStream._create; +// @ts-expect-error - Deleting static method delete OutputStream._create; export const outputStreamId = OutputStream._id; +// @ts-expect-error - Deleting static method delete OutputStream._id; export const error = { Error: IoError }; @@ -415,6 +426,7 @@ Pollable[Symbol.for("cabiDispose")] = function pollableDispose(rep) { }; export const pollableCreate = Pollable._create; +// @ts-expect-error - Deleting static method delete Pollable._create; export const poll = { @@ -448,7 +460,7 @@ poll.poll[cabiLowerSymbol] = function ({ memory, realloc, resourceTables: [table }; export function createPoll(call, id, initPayload) { - return pollableCreate(ioCall(call, id, initPayload)); + return pollableCreate(ioCall(call, id, initPayload), undefined); } export function createPollLower(call, id, table) { diff --git a/packages/preview2-shim/lib/io/worker-socket-tcp.js b/packages/preview2-shim/src/io/worker-socket-tcp.ts similarity index 71% rename from packages/preview2-shim/lib/io/worker-socket-tcp.js rename to packages/preview2-shim/src/io/worker-socket-tcp.ts index a41874cd0..ca2279c65 100644 --- a/packages/preview2-shim/lib/io/worker-socket-tcp.js +++ b/packages/preview2-shim/src/io/worker-socket-tcp.ts @@ -5,11 +5,13 @@ import { createWritableStream, futureDispose, futureTakeValue, + PollState, pollStateReady, verifyPollsDroppedForDrop, } from "./worker-thread.js"; import process from "node:process"; -const { TCP, constants: TCPConstants } = process.binding("tcp_wrap"); +// TODO: tcp_wrap is no longer exposed in newer versions of Node.js +const { TCP, constants: TCPConstants } = (process as any).binding("tcp_wrap"); import { convertSocketError, convertSocketErrorCode, @@ -29,42 +31,32 @@ import { SOCKET_STATE_LISTEN, SOCKET_STATE_LISTENER, } from "./worker-sockets.js"; -import { Socket, Server } from "node:net"; +import { Server, Socket as TcpSocket } from "node:net"; +import { IpSocketAddress } from "../../types/interfaces/wasi-sockets-network.js"; const win = process.platform === "win32"; -/** - * @typedef {import("../../types/interfaces/wasi-sockets-network.js").IpSocketAddress} IpSocketAddress - * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").IpAddressFamily} IpAddressFamily - * @typedef {import("node:net").Socket} TcpSocket - * - * @typedef {{ - * tcpSocket: number, - * err: Error | null, - * pollState: PollState, - * }} PendingAccept - * - * @typedef {{ - * state: number, - * future: number | null, - * tcpSocket: TcpSocket | null, - * listenBacklogSize: number, - * handle: TCP, - * pendingAccepts: PendingAccept[], - * pollState: PollState, - * }} TcpSocketRecord - */ +interface PendingAccept { + tcpSocket: TcpSocket | null; + err: Error | null; + pollState: PollState | null; +} + +interface TcpSocketRecord { + state: number; + future: number | null; + tcpSocket: TcpSocket | null; + listenBacklogSize: number; + // @ts-expect-error + handle: TCP; + pendingAccepts: PendingAccept[]; + pollState: PollState | null; +} -/** - * @type {Map} - */ -export const tcpSockets = new Map(); +export const tcpSockets: Map = new Map(); let tcpSocketCnt = 0; -/** - * @param {IpAddressFamily} addressFamily - */ export function createTcpSocket() { const handle = new TCP(TCPConstants.SOCKET); tcpSockets.set(++tcpSocketCnt, { @@ -84,15 +76,15 @@ export function createTcpSocket() { return tcpSocketCnt; } -export function socketTcpFinish(id, fromState, toState) { - const socket = tcpSockets.get(id); +export function socketTcpFinish(id: number, fromState, toState) { + const socket = tcpSockets.get(id)!; if (socket.state !== fromState) { throw "not-in-progress"; } - if (!socket.pollState.ready) { + if (!socket.pollState?.ready) { throw "would-block"; } - const { tag, val } = futureTakeValue(socket.future).val; + const { tag, val } = futureTakeValue(socket.future)?.val ?? {}; futureDispose(socket.future, false); socket.future = null; if (tag === "err") { @@ -108,8 +100,8 @@ export function socketTcpFinish(id, fromState, toState) { } } -export function socketTcpBindStart(id, localAddress, family) { - const socket = tcpSockets.get(id); +export function socketTcpBindStart(id: number, localAddress, family) { + const socket = tcpSockets.get(id)!; if (socket.state !== SOCKET_STATE_INIT) { throw "invalid-state"; } @@ -147,8 +139,8 @@ export function socketTcpBindStart(id, localAddress, family) { ); } -export function socketTcpConnectStart(id, remoteAddress, family) { - const socket = tcpSockets.get(id); +export function socketTcpConnectStart(id: number, remoteAddress: IpSocketAddress, family) { + const socket = tcpSockets.get(id)!; if (socket.state !== SOCKET_STATE_INIT && socket.state !== SOCKET_STATE_BOUND) { throw "invalid-state"; } @@ -165,7 +157,8 @@ export function socketTcpConnectStart(id, remoteAddress, family) { socket.state = SOCKET_STATE_CONNECT; socket.future = createFuture( new Promise((resolve, reject) => { - const tcpSocket = (socket.tcpSocket = new Socket({ + const tcpSocket = (socket.tcpSocket = new TcpSocket({ + // @ts-expect-error Not present in type definition handle: socket.handle, pauseOnCreate: true, allowHalfOpen: true, @@ -182,7 +175,7 @@ export function socketTcpConnectStart(id, remoteAddress, family) { tcpSocket.once("error", handleErr); tcpSocket.connect({ port: remoteAddress.val.port, - host: serializeIpAddress(remoteAddress), + host: serializeIpAddress(remoteAddress) ?? undefined, lookup: noLookup, }); }), @@ -190,15 +183,15 @@ export function socketTcpConnectStart(id, remoteAddress, family) { ); } -export function socketTcpListenStart(id) { - const socket = tcpSockets.get(id); +export function socketTcpListenStart(id: number) { + const socket = tcpSockets.get(id)!; if (socket.state !== SOCKET_STATE_BOUND) { throw "invalid-state"; } const { handle } = socket; socket.state = SOCKET_STATE_LISTEN; socket.future = createFuture( - new Promise((resolve, reject) => { + new Promise((resolve, reject) => { const server = new Server({ pauseOnConnect: true, allowHalfOpen: true, @@ -236,8 +229,8 @@ export function socketTcpListenStart(id) { ); } -export function socketTcpAccept(id) { - const socket = tcpSockets.get(id); +export function socketTcpAccept(id: number) { + const socket = tcpSockets.get(id)!; if (socket.state !== SOCKET_STATE_LISTENER) { throw "invalid-state"; } @@ -245,11 +238,11 @@ export function socketTcpAccept(id) { throw "would-block"; } const accept = socket.pendingAccepts.shift(); - if (accept.err) { + if (!accept || accept.err) { socket.state = SOCKET_STATE_CLOSED; - throw convertSocketError(accept.err); + throw convertSocketError(accept?.err); } - if (socket.pendingAccepts.length === 0) { + if (socket.pollState && socket.pendingAccepts.length === 0) { socket.pollState.ready = false; } tcpSockets.set(++tcpSocketCnt, { @@ -257,19 +250,21 @@ export function socketTcpAccept(id) { future: null, tcpSocket: accept.tcpSocket, listenBacklogSize: 128, - handle: accept.tcpSocket._handle, + // @ts-expect-error + handle: accept.tcpSocket?._handle, pendingAccepts: [], pollState: accept.pollState, }); return [ tcpSocketCnt, - createReadableStream(accept.tcpSocket, accept.pollState), + // @ts-expect-error + createReadableStream(accept.tcpSocket, accept.pollState ?? undefined), createWritableStream(accept.tcpSocket), ]; } -export function socketTcpSetListenBacklogSize(id, backlogSize) { - const socket = tcpSockets.get(id); +export function socketTcpSetListenBacklogSize(id: number, backlogSize) { + const socket = tcpSockets.get(id)!; if (socket.state === SOCKET_STATE_LISTEN || socket.state === SOCKET_STATE_LISTENER) { throw "not-supported"; } @@ -283,9 +278,9 @@ export function socketTcpSetListenBacklogSize(id, backlogSize) { socket.listenBacklogSize = Number(backlogSize); } -export function socketTcpGetLocalAddress(id) { - const { handle } = tcpSockets.get(id); - const out = {}; +export function socketTcpGetLocalAddress(id: number) { + const { handle } = tcpSockets.get(id)!; + const out: any = {}; const code = handle.getsockname(out); if (code !== 0) { throw convertSocketErrorCode(-code); @@ -293,9 +288,9 @@ export function socketTcpGetLocalAddress(id) { return ipSocketAddress(out.family.toLowerCase(), out.address, out.port); } -export function socketTcpGetRemoteAddress(id) { - const { handle } = tcpSockets.get(id); - const out = {}; +export function socketTcpGetRemoteAddress(id: number) { + const { handle } = tcpSockets.get(id)!; + const out: any = {}; const code = handle.getpeername(out); if (code !== 0) { throw convertSocketErrorCode(-code); @@ -303,28 +298,28 @@ export function socketTcpGetRemoteAddress(id) { return ipSocketAddress(out.family.toLowerCase(), out.address, out.port); } -export function socketTcpShutdown(id, _shutdownType) { - const socket = tcpSockets.get(id); +export function socketTcpShutdown(id: number, _shutdownType) { + const socket = tcpSockets.get(id)!; if (socket.state !== SOCKET_STATE_CONNECTION) { throw "invalid-state"; } - if (win && socket.tcpSocket.destroySoon) { + if (win && socket.tcpSocket?.destroySoon) { socket.tcpSocket.destroySoon(); } else { - socket.tcpSocket.destroy(); + socket.tcpSocket?.destroy(); } } -export function socketTcpSetKeepAlive(id, { keepAlive, keepAliveIdleTime }) { - const { handle } = tcpSockets.get(id); +export function socketTcpSetKeepAlive(id: number, { keepAlive, keepAliveIdleTime }) { + const { handle } = tcpSockets.get(id)!; const code = handle.setKeepAlive(keepAlive, Number(keepAliveIdleTime / 1_000_000_000n)); if (code !== 0) { throw convertSocketErrorCode(-code); } } -export function socketTcpDispose(id) { - const socket = tcpSockets.get(id); +export function socketTcpDispose(id: number) { + const socket = tcpSockets.get(id)!; verifyPollsDroppedForDrop(socket.pollState, "tcp socket"); socket.handle.close(); tcpSockets.delete(id); diff --git a/packages/preview2-shim/lib/io/worker-socket-udp.js b/packages/preview2-shim/src/io/worker-socket-udp.ts similarity index 73% rename from packages/preview2-shim/lib/io/worker-socket-udp.js rename to packages/preview2-shim/src/io/worker-socket-udp.ts index 319eb4bd6..f64d374d6 100644 --- a/packages/preview2-shim/lib/io/worker-socket-udp.js +++ b/packages/preview2-shim/src/io/worker-socket-udp.ts @@ -1,8 +1,9 @@ -import { createSocket } from "node:dgram"; +import { createSocket, Socket } from "node:dgram"; import { createFuture, futureDispose, futureTakeValue, + PollState, pollStateReady, verifyPollsDroppedForDrop, } from "./worker-thread.js"; @@ -22,6 +23,8 @@ import { SOCKET_STATE_CONNECTION, SOCKET_STATE_INIT, } from "./worker-sockets.js"; +import { IpAddressFamily, IpSocketAddress } from "../../types/interfaces/wasi-sockets-network.js"; +import { OutgoingDatagram } from "../../types/interfaces/wasi-sockets-udp.js"; // Experimental support for batched UDP sends. Set this to true to enable. // This is not enabled by default because we need to figure out how to know @@ -29,55 +32,45 @@ import { // See the err path in "handler" in the "doSendBatch" of socketOutgoingDatagramStreamSend. const UDP_BATCH_SENDS = false; -/** - * @typedef {import("../../types/interfaces/wasi-sockets-network.js").IpSocketAddress} IpSocketAddress - * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").IpAddressFamily} IpAddressFamily - * - * - * @typedef {{ - * state: number, - * remoteAddress: string | null, - * remotePort: number | null, - * sendBufferSize: number | null, - * receiveBufferSize: number | null, - * unicastHopLimit: number, - * udpSocket: import('node:dgram').Socket, - * future: number | null, - * serializedLocalAddress: string | null, - * pollState: PollState, - * incomingDatagramStream: number | null, - * outgoingDatagramStream: number | null, - * }} UdpSocketRecord - * - * @typedef {{ - * active: bool, - * error: any | null, - * socket: UdpSocketRecord, - * pollState: PollState, - * queue?: Buffer[], - * cleanup: () => void | null, - * }} DatagramStreamRecord - * - */ +interface UdpSocketRecord { + state: number; + remoteAddress: string | null; + remotePort: number | null; + sendBufferSize: number | null; + receiveBufferSize: number | null; + unicastHopLimit: number; + udpSocket: Socket; + future: number | null; + serializedLocalAddress: string | null; + pollState: PollState; + incomingDatagramStream: DatagramStreamRecord | null; + outgoingDatagramStream: DatagramStreamRecord | null; +} + +interface DatagramStreamRecord { + id: number; + active: boolean; + error: any | null; + socket: UdpSocketRecord; + pollState: PollState; + queue?: Buffer[]; + cleanup: (() => void) | null; +} let udpSocketCnt = 0, datagramStreamCnt = 0; -/** - * @type {Map} - */ -export const udpSockets = new Map(); +export const udpSockets = new Map(); -/** - * @type {Map} - */ -export const datagramStreams = new Map(); +export const datagramStreams = new Map(); -/** - * @param {IpAddressFamily} addressFamily - * @returns {number} - */ -export function createUdpSocket({ family, unicastHopLimit }) { +export function createUdpSocket({ + family, + unicastHopLimit, +}: { + family: IpAddressFamily; + unicastHopLimit: number; +}): number { const udpSocket = createSocket({ type: family === "ipv6" ? "udp6" : "udp4", reuseAddr: false, @@ -96,7 +89,7 @@ export function createUdpSocket({ family, unicastHopLimit }) { serializedLocalAddress: null, pollState: { ready: true, - listener: null, + listener: () => null, polls: [], parentStream: null, }, @@ -106,19 +99,15 @@ export function createUdpSocket({ family, unicastHopLimit }) { return udpSocketCnt; } -/** - * @param {UdpSocketRecord} socket - * @returns {DatagramStreamRecord} - */ -function createIncomingDatagramStream(socket) { +function createIncomingDatagramStream(socket: UdpSocketRecord): DatagramStreamRecord { const id = ++datagramStreamCnt; - const pollState = { + const pollState: PollState = { ready: false, listener: null, polls: [], parentStream: null, }; - const datagramStream = { + const datagramStream: DatagramStreamRecord = { id, active: true, error: null, @@ -135,10 +124,10 @@ function createIncomingDatagramStream(socket) { } function onMessage(data, rinfo) { const family = rinfo.family.toLowerCase(); - datagramStream.queue.push({ + datagramStream.queue?.push({ data, remoteAddress: ipSocketAddress(family, rinfo.address, rinfo.port), - }); + } as any); if (!pollState.ready) { pollStateReady(pollState); } @@ -152,11 +141,7 @@ function createIncomingDatagramStream(socket) { return datagramStream; } -/** - * @param {UdpSocketRecord} socket - * @returns {DatagramStreamRecord} - */ -function createOutgoingDatagramStream(socket) { +function createOutgoingDatagramStream(socket: UdpSocketRecord): DatagramStreamRecord { const id = ++datagramStreamCnt; const datagramStream = { id, @@ -184,8 +169,8 @@ function createOutgoingDatagramStream(socket) { return datagramStream; } -export function socketUdpBindStart(id, localAddress, family) { - const socket = udpSockets.get(id); +export function socketUdpBindStart(id: number, localAddress, family) { + const socket = udpSockets.get(id)!; if (family !== localAddress.tag || isIPv4MappedAddress(localAddress)) { throw "invalid-argument"; @@ -199,7 +184,7 @@ export function socketUdpBindStart(id, localAddress, family) { socket.state = SOCKET_STATE_BIND; const { udpSocket } = socket; socket.future = createFuture( - new Promise((resolve, reject) => { + new Promise((resolve, reject) => { function bindOk() { resolve(); udpSocket.off("error", bindErr); @@ -210,21 +195,21 @@ export function socketUdpBindStart(id, localAddress, family) { } udpSocket.once("listening", bindOk); udpSocket.once("error", bindErr); - udpSocket.bind(localAddress.val.port, serializedLocalAddress); + udpSocket.bind(localAddress.val.port, serializedLocalAddress ?? undefined); }), socket.pollState, ); } -export function socketUdpBindFinish(id) { - const socket = udpSockets.get(id); +export function socketUdpBindFinish(id: number) { + const socket = udpSockets.get(id)!; if (socket.state !== SOCKET_STATE_BIND) { throw "not-in-progress"; } if (!socket.pollState.ready) { throw "would-block"; } - const { tag, val } = futureTakeValue(socket.future).val; + const { tag, val } = futureTakeValue(socket.future)?.val ?? {}; futureDispose(socket.future, false); socket.future = null; if (tag === "err") { @@ -237,7 +222,7 @@ export function socketUdpBindFinish(id) { if (socket.sendBufferSize) { socket.udpSocket.setRecvBufferSize(socket.sendBufferSize); } - if (socket.receieveBufferSize) { + if (socket.receiveBufferSize) { socket.udpSocket.setSendBufferSize(socket.receiveBufferSize); } socket.state = SOCKET_STATE_BOUND; @@ -245,12 +230,8 @@ export function socketUdpBindFinish(id) { } } -/** - * @param {number} id - * @returns {IpSocketAddress} - */ -export function socketUdpGetLocalAddress(id) { - const { udpSocket } = udpSockets.get(id); +export function socketUdpGetLocalAddress(id: number): IpSocketAddress { + const { udpSocket } = udpSockets.get(id)!; let address, family, port; try { ({ address, family, port } = udpSocket.address()); @@ -265,7 +246,7 @@ export function socketUdpGetLocalAddress(id) { * @returns {IpSocketAddress} */ export function socketUdpGetRemoteAddress(id) { - const { udpSocket } = udpSockets.get(id); + const { udpSocket } = udpSockets.get(id)!; let address, family, port; try { ({ address, family, port } = udpSocket.remoteAddress()); @@ -276,7 +257,7 @@ export function socketUdpGetRemoteAddress(id) { } export function socketUdpStream(id, remoteAddress) { - const socket = udpSockets.get(id); + const socket = udpSockets.get(id)!; const { udpSocket } = socket; if (socket.state !== SOCKET_STATE_BOUND && socket.state !== SOCKET_STATE_CONNECTION) { @@ -297,8 +278,8 @@ export function socketUdpStream(id, remoteAddress) { } if (socket.state === SOCKET_STATE_CONNECTION) { - socketDatagramStreamClear(socket.incomingDatagramStream); - socketDatagramStreamClear(socket.outgoingDatagramStream); + socketDatagramStreamClear(socket.incomingDatagramStream!); + socketDatagramStreamClear(socket.outgoingDatagramStream!); try { udpSocket.disconnect(); } catch (e) { @@ -314,8 +295,8 @@ export function socketUdpStream(id, remoteAddress) { function connectOk() { if (socket.state === SOCKET_STATE_INIT) { socket.udpSocket.setTTL(socket.unicastHopLimit); - socket.udpSocket.setRecvBufferSize(socket.sendBufferSize); - socket.udpSocket.setSendBufferSize(socket.receiveBufferSize); + socket.udpSocket.setRecvBufferSize(socket.sendBufferSize!); + socket.udpSocket.setSendBufferSize(socket.receiveBufferSize!); } udpSocket.off("error", connectErr); socket.state = SOCKET_STATE_CONNECTION; @@ -330,7 +311,7 @@ export function socketUdpStream(id, remoteAddress) { } udpSocket.once("connect", connectOk); udpSocket.once("error", connectErr); - udpSocket.connect(remoteAddress.val.port, serializedRemoteAddress); + udpSocket.connect(remoteAddress.val.port, serializedRemoteAddress ?? undefined); }); } else { socket.state = SOCKET_STATE_BOUND; @@ -343,34 +324,34 @@ export function socketUdpStream(id, remoteAddress) { } } -export function socketUdpSetReceiveBufferSize(id, bufferSize) { - const socket = udpSockets.get(id); - bufferSize = Number(bufferSize); +export function socketUdpSetReceiveBufferSize(id: number, bufferSize: bigint) { + const socket = udpSockets.get(id)!; + const buf = Number(bufferSize); if (socket.state !== SOCKET_STATE_INIT && socket.state !== SOCKET_STATE_BIND) { try { - socket.udpSocket.setRecvBufferSize(bufferSize); + socket.udpSocket.setRecvBufferSize(buf); } catch (err) { throw convertSocketError(err); } } - socket.receiveBufferSize = bufferSize; + socket.receiveBufferSize = buf; } -export function socketUdpSetSendBufferSize(id, bufferSize) { - const socket = udpSockets.get(id); - bufferSize = Number(bufferSize); +export function socketUdpSetSendBufferSize(id: number, bufferSize: bigint) { + const socket = udpSockets.get(id)!; + const buf = Number(bufferSize); if (socket.state !== SOCKET_STATE_INIT && socket.state !== SOCKET_STATE_BIND) { try { - socket.udpSocket.setSendBufferSize(bufferSize); + socket.udpSocket.setSendBufferSize(buf); } catch (err) { throw convertSocketError(err); } } - socket.sendBufferSize = bufferSize; + socket.sendBufferSize = buf; } -export function socketUdpSetUnicastHopLimit(id, hopLimit) { - const socket = udpSockets.get(id); +export function socketUdpSetUnicastHopLimit(id: number, hopLimit: number) { + const socket = udpSockets.get(id)!; if (socket.state !== SOCKET_STATE_INIT && socket.state !== SOCKET_STATE_BIND) { try { socket.udpSocket.setTTL(hopLimit); @@ -381,8 +362,8 @@ export function socketUdpSetUnicastHopLimit(id, hopLimit) { socket.unicastHopLimit = hopLimit; } -export async function socketUdpGetReceiveBufferSize(id) { - const socket = udpSockets.get(id); +export async function socketUdpGetReceiveBufferSize(id: number) { + const socket = udpSockets.get(id)!; if (socket.receiveBufferSize) { return BigInt(socket.receiveBufferSize); } @@ -393,12 +374,14 @@ export async function socketUdpGetReceiveBufferSize(id) { throw convertSocketError(err); } } else { - return BigInt((socket.receiveBufferSize = await getDefaultReceiveBufferSize())); + const receiveBufferSize = await getDefaultReceiveBufferSize(); + socket.receiveBufferSize = Number(receiveBufferSize); + return receiveBufferSize; } } -export async function socketUdpGetSendBufferSize(id) { - const socket = udpSockets.get(id); +export async function socketUdpGetSendBufferSize(id: number) { + const socket = udpSockets.get(id)!; if (socket.sendBufferSize) { return BigInt(socket.sendBufferSize); } @@ -409,17 +392,19 @@ export async function socketUdpGetSendBufferSize(id) { throw convertSocketError(err); } } else { - return BigInt((socket.sendBufferSize = await getDefaultSendBufferSize())); + const sendBufferSize = await getDefaultSendBufferSize(); + socket.sendBufferSize = Number(sendBufferSize); + return sendBufferSize; } } -export function socketUdpGetUnicastHopLimit(id) { - const { unicastHopLimit } = udpSockets.get(id); +export function socketUdpGetUnicastHopLimit(id: number) { + const { unicastHopLimit } = udpSockets.get(id)!; return unicastHopLimit; } -export function socketUdpDispose(id) { - const { udpSocket } = udpSockets.get(id); +export function socketUdpDispose(id: number) { + const { udpSocket } = udpSockets.get(id)!; return new Promise((resolve) => { udpSocket.close(() => { udpSockets.delete(id); @@ -428,30 +413,30 @@ export function socketUdpDispose(id) { }); } -export function socketIncomingDatagramStreamReceive(id, maxResults) { - const datagramStream = datagramStreams.get(id); +export function socketIncomingDatagramStreamReceive(id: number, maxResults: bigint) { + const datagramStream = datagramStreams.get(id)!; if (!datagramStream.active) { throw new Error("wasi-io trap: attempt to receive on inactive incoming datagram stream"); } - if (maxResults === 0n || datagramStream.queue.length === 0) { + if (maxResults === 0n || datagramStream.queue?.length === 0) { return []; } if (datagramStream.error) { throw convertSocketError(datagramStream.error); } - return datagramStream.queue.splice(0, Number(maxResults)); + return datagramStream.queue?.splice(0, Number(maxResults)); } -export async function socketOutgoingDatagramStreamSend(id, datagrams) { - const { active, socket } = datagramStreams.get(id); +export async function socketOutgoingDatagramStreamSend(id: number, datagrams: OutgoingDatagram[]) { + const { active, socket } = datagramStreams.get(id)!; if (!active) { throw new Error("wasi-io trap: writing to inactive outgoing datagram stream"); } const { udpSocket } = socket; - let sendQueue = [], - sendQueueAddress, - sendQueuePort; + let sendQueue: Uint8Array[] = []; + let sendQueueAddress: string | null = null; + let sendQueuePort: number | null = null; let datagramsSent = 0; for (const { data, remoteAddress } of datagrams) { const address = remoteAddress ? serializeIpAddress(remoteAddress) : socket.remoteAddress; @@ -507,9 +492,10 @@ export async function socketOutgoingDatagramStreamSend(id, datagrams) { if (!sendQueueAddress) { return void reject("invalid-argument"); } + // @ts-expect-error Proper function overload is not being recognized udpSocket.send(sendQueue, sendQueuePort, sendQueueAddress, handler); } - function handler(err, _sentBytes) { + function handler(err: any, _sentBytes: unknown) { if (err) { // TODO: update datagramsSent properly on error for multiple sends // to enable send batching. Perhaps a Node.js PR could @@ -528,7 +514,7 @@ export async function socketOutgoingDatagramStreamSend(id, datagrams) { } } -function checkSend(socket) { +function checkSend(socket: UdpSocketRecord) { try { return Math.floor( (socket.udpSocket.getSendBufferSize() - socket.udpSocket.getSendQueueSize()) / 1500, @@ -538,7 +524,7 @@ function checkSend(socket) { } } -function pollSend(socket) { +function pollSend(socket: UdpSocketRecord) { socket.pollState.ready = false; // The only way we have of dealing with getting a backpressure // ready signal in Node.js is to just poll on the queue reducing. @@ -555,8 +541,8 @@ function pollSend(socket) { }); } -export function socketOutgoingDatagramStreamCheckSend(id) { - const { active, socket } = datagramStreams.get(id); +export function socketOutgoingDatagramStreamCheckSend(id: number) { + const { active, socket } = datagramStreams.get(id)!; if (!active) { throw new Error("wasi-io trap: check send on inactive outgoing datagram stream"); } @@ -567,7 +553,7 @@ export function socketOutgoingDatagramStreamCheckSend(id) { return BigInt(remaining); } -function socketDatagramStreamClear(datagramStream) { +function socketDatagramStreamClear(datagramStream: DatagramStreamRecord) { datagramStream.active = false; if (datagramStream.cleanup) { datagramStream.cleanup(); @@ -576,11 +562,11 @@ function socketDatagramStreamClear(datagramStream) { } export function socketDatagramStreamDispose(id) { - const datagramStream = datagramStreams.get(id); + const datagramStream = datagramStreams.get(id)!; datagramStream.active = false; if (datagramStream.cleanup) { datagramStream.cleanup(); - datagramStream.cleanup = null; + datagramStream.cleanup = () => null; } verifyPollsDroppedForDrop(datagramStream.pollState, "datagram stream"); datagramStreams.delete(id); diff --git a/packages/preview2-shim/lib/io/worker-sockets.js b/packages/preview2-shim/src/io/worker-sockets.ts similarity index 80% rename from packages/preview2-shim/lib/io/worker-sockets.js rename to packages/preview2-shim/src/io/worker-sockets.ts index 08021dd68..1f23c28b1 100644 --- a/packages/preview2-shim/lib/io/worker-sockets.js +++ b/packages/preview2-shim/src/io/worker-sockets.ts @@ -1,6 +1,6 @@ import { isIP } from "node:net"; import { lookup } from "node:dns/promises"; -import { Socket } from "node:dgram"; +import { createSocket } from "node:dgram"; import { ALL, BADFAMILY, @@ -32,6 +32,12 @@ import { EPERM, EWOULDBLOCK, } from "node:constants"; +import { + IpAddressFamily, + IpSocketAddress, + Ipv4Address, + Ipv6Address, +} from "../../types/interfaces/wasi-sockets-network.js"; let stateCnt = 0; export const SOCKET_STATE_INIT = ++stateCnt; @@ -53,7 +59,7 @@ export function noLookup(ip, _opts, cb) { cb(null, ip); } -export function socketResolveAddress(name) { +export function socketResolveAddress(name: string) { const isIpNum = isIP(name[0] === "[" && name[name.length - 1] === "]" ? name.slice(1, -1) : name); if (isIpNum > 0) { return Promise.resolve([ @@ -64,7 +70,7 @@ export function socketResolveAddress(name) { ]); } // verify it is a valid domain name using the URL parser - let parsedUrl = null; + let parsedUrl: URL | null = null; try { parsedUrl = new URL(`https://${name}`); if ( @@ -86,7 +92,7 @@ export function socketResolveAddress(name) { return lookup(name, dnsLookupOptions).then( (addresses) => { - return addresses.map(({ address, family }) => { + return (Array.isArray(addresses) ? addresses : [addresses]).map(({ address, family }) => { return [ { tag: "ipv" + family, @@ -198,59 +204,36 @@ export function convertSocketErrorCode(code) { } } -/** - * @typedef {import("../../../types/interfaces/wasi-sockets-network.js").IpSocketAddress} IpSocketAddress - * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").IpAddressFamily} IpAddressFamily - * @typedef {import("../../../types/interfaces/wasi-sockets-tcp").TcpSocket} TcpSocket - * @typedef {import("../../../types/interfaces/wasi-sockets-udp").UdpSocket} UdpSocket - */ - -export function tupleToIPv6(arr) { +export function tupleToIPv6(arr: Ipv6Address) { if (arr.length !== 8) { return null; } return arr.map((segment) => segment.toString(16)).join(":"); } -export function tupleToIpv4(arr) { +export function tupleToIpv4(arr: Ipv4Address) { if (arr.length !== 4) { return null; } return arr.map((segment) => segment.toString(10)).join("."); } -/** - * @param {IpSocketAddress} ipSocketAddress - * @returns {boolean} - */ -export function isMulticastIpAddress(ipSocketAddress) { +export function isMulticastIpAddress(ipSocketAddress: IpSocketAddress): boolean { return ( (ipSocketAddress.tag === "ipv4" && ipSocketAddress.val.address[0] === 0xe0) || (ipSocketAddress.tag === "ipv6" && ipSocketAddress.val.address[0] === 0xff00) ); } -/** - * @param {IpSocketAddress} ipSocketAddress - * @returns {boolean} - */ -export function isIPv4MappedAddress(ipSocketAddress) { +export function isIPv4MappedAddress(ipSocketAddress: IpSocketAddress): boolean { return ipSocketAddress.tag === "ipv6" && ipSocketAddress.val.address[5] === 0xffff; } -/** - * @param {IpSocketAddress} ipSocketAddress - * @returns {boolean} - */ -export function isUnicastIpAddress(ipSocketAddress) { +export function isUnicastIpAddress(ipSocketAddress: IpSocketAddress): boolean { return !isMulticastIpAddress(ipSocketAddress) && !isBroadcastIpAddress(ipSocketAddress); } -/** - * @param {IpSocketAddress} isWildcardAddress - * @returns {boolean} - */ -export function isWildcardAddress(ipSocketAddress) { +export function isWildcardAddress(ipSocketAddress: IpSocketAddress): boolean { const { address } = ipSocketAddress.val; if (ipSocketAddress.tag === "ipv4") { return address[0] === 0 && address[1] === 0 && address[2] === 0 && address[3] === 0; @@ -268,11 +251,7 @@ export function isWildcardAddress(ipSocketAddress) { } } -/** - * @param {IpSocketAddress} isWildcardAddress - * @returns {boolean} - */ -export function isBroadcastIpAddress(ipSocketAddress) { +export function isBroadcastIpAddress(ipSocketAddress: IpSocketAddress): boolean { const { address } = ipSocketAddress.val; return ( ipSocketAddress.tag === "ipv4" && @@ -283,39 +262,31 @@ export function isBroadcastIpAddress(ipSocketAddress) { ); } -/** - * - * @param {IpSocketAddress} addr - * @param {boolean} includePort - * @returns {string} - */ -export function serializeIpAddress(addr) { +export function serializeIpAddress(addr: IpSocketAddress): string | null { if (addr.tag === "ipv4") { return tupleToIpv4(addr.val.address); } return tupleToIPv6(addr.val.address); } -export function ipv6ToTuple(ipv6) { +export function ipv6ToTuple(ipv6: string) { const [lhs, rhs = ""] = ipv6.includes("::") ? ipv6.split("::") : [ipv6]; const lhsParts = lhs === "" ? [] : lhs.split(":"); const rhsParts = rhs === "" ? [] : rhs.split(":"); return [...lhsParts, ...Array(8 - lhsParts.length - rhsParts.length).fill(0), ...rhsParts].map( (segment) => parseInt(segment, 16), - ); + ) as Ipv6Address; } -export function ipv4ToTuple(ipv4) { - return ipv4.split(".").map((segment) => parseInt(segment, 10)); +export function ipv4ToTuple(ipv4: string) { + return ipv4.split(".").map((segment) => parseInt(segment, 10)) as Ipv4Address; } -/** - * - * @param {string} addr - * @param {IpAddressFamily} family - * @returns {IpSocketAddress} - */ -export function ipSocketAddress(family, addr, port) { +export function ipSocketAddress( + family: IpAddressFamily, + addr: string, + port: number, +): IpSocketAddress { if (family === "ipv4") { return { tag: "ipv4", @@ -336,9 +307,10 @@ export function ipSocketAddress(family, addr, port) { }; } -let _recvBufferSize, _sendBufferSize; +let _recvBufferSize: bigint; +let _sendBufferSize: bigint; async function getDefaultBufferSizes() { - var s = new Socket({ type: "udp4" }); + var s = createSocket({ type: "udp4" }); s.bind(0); await new Promise((resolve, reject) => { s.once("error", reject); diff --git a/packages/preview2-shim/lib/io/worker-thread.js b/packages/preview2-shim/src/io/worker-thread.ts similarity index 95% rename from packages/preview2-shim/lib/io/worker-thread.js rename to packages/preview2-shim/src/io/worker-thread.ts index 96a0ebb36..6b5fb5a1f 100644 --- a/packages/preview2-shim/lib/io/worker-thread.js +++ b/packages/preview2-shim/src/io/worker-thread.ts @@ -1,4 +1,4 @@ -import { createReadStream, createWriteStream } from "node:fs"; +import { createReadStream, createWriteStream, PathLike } from "node:fs"; import { hrtime, stderr, stdout } from "node:process"; import { PassThrough } from "node:stream"; import { runAsWorker } from "../synckit/index.js"; @@ -151,7 +151,7 @@ import process from "node:process"; export function log(msg) { if (debug) { - process._rawDebug(msg); + (process as any)._rawDebug(msg); } } @@ -159,28 +159,26 @@ let pollCnt = 0, streamCnt = 0, futureCnt = 0; -/** - * @typedef {{ - * ready: bool, - * listener: () => void | null, - * polls: number[], - * parentStream: null | NodeJS.ReadableStream - * }} PollState - * - * @typedef {{ - * stream: NodeJS.ReadableStream | NodeJS.WritableStream, - * flushPromise: Promise | null, - * pollState - * }} Stream - * - * @typedef {{ - * future: { - * tag: 'ok' | 'err', - * val: any, - * }, - * pollState - * }} Future - */ +export interface PollState { + ready: boolean; + listener: (() => void) | null; + polls: number[]; + parentStream: null | NodeJS.ReadableStream; +} + +export interface Stream { + stream: NodeJS.ReadableStream | NodeJS.WritableStream; + flushPromise: Promise | null; + pollState; +} + +export interface Future { + future: { + tag: "ok" | "err"; + val: any; + }; + pollState; +} /** @type {Map} */ export const polls = new Map(); @@ -330,13 +328,14 @@ function handle(call, id, payload) { betweenBytesTimeout, firstByteTimeout, ), + undefined, ); } case OUTPUT_STREAM_CREATE | HTTP: { const stream = new PassThrough(); // content length is passed as payload - stream.contentLength = payload; - stream.bytesRemaining = payload; + stream["contentLength"] = payload; + stream["bytesRemaining"] = payload; return createWritableStream(stream); } case OUTPUT_STREAM_SUBSCRIBE | HTTP: @@ -380,7 +379,7 @@ function handle(call, id, payload) { let stream; try { ({ stream } = getStreamOrThrow(id)); - } catch (e) { + } catch (e: any) { if (e.tag === "closed") { throw { tag: "internal-error", val: "stream closed" }; } @@ -422,7 +421,7 @@ function handle(call, id, payload) { // Sockets name resolution case SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST: - return createFuture(socketResolveAddress(payload)); + return createFuture(socketResolveAddress(payload), undefined); case SOCKET_RESOLVE_ADDRESS_SUBSCRIBE_REQUEST: return createPoll(futures.get(id).pollState); case SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST: @@ -454,13 +453,13 @@ function handle(call, id, payload) { case SOCKET_TCP_LISTEN_FINISH: return socketTcpFinish(id, SOCKET_STATE_LISTEN, SOCKET_STATE_LISTENER); case SOCKET_TCP_IS_LISTENING: - return tcpSockets.get(id).state === SOCKET_STATE_LISTENER; + return tcpSockets.get(id)?.state === SOCKET_STATE_LISTENER; case SOCKET_GET_DEFAULT_SEND_BUFFER_SIZE: - return getDefaultSendBufferSize(id); + return getDefaultSendBufferSize(); case SOCKET_GET_DEFAULT_RECEIVE_BUFFER_SIZE: - return getDefaultReceiveBufferSize(id); + return getDefaultReceiveBufferSize(); case SOCKET_TCP_SET_LISTEN_BACKLOG_SIZE: - return socketTcpSetListenBacklogSize(id); + return socketTcpSetListenBacklogSize(id, undefined); case SOCKET_TCP_GET_LOCAL_ADDRESS: return socketTcpGetLocalAddress(id); case SOCKET_TCP_GET_REMOTE_ADDRESS: @@ -468,7 +467,7 @@ function handle(call, id, payload) { case SOCKET_TCP_SHUTDOWN: return socketTcpShutdown(id, payload); case SOCKET_TCP_SUBSCRIBE: - return createPoll(tcpSockets.get(id).pollState); + return createPoll(tcpSockets.get(id)?.pollState); case SOCKET_TCP_SET_KEEP_ALIVE: return socketTcpSetKeepAlive(id, payload); case SOCKET_TCP_DISPOSE: @@ -484,7 +483,7 @@ function handle(call, id, payload) { case SOCKET_UDP_STREAM: return socketUdpStream(id, payload); case SOCKET_UDP_SUBSCRIBE: - return createPoll(udpSockets.get(id).pollState); + return createPoll(udpSockets.get(id)?.pollState); case SOCKET_UDP_GET_LOCAL_ADDRESS: return socketUdpGetLocalAddress(id); case SOCKET_UDP_GET_REMOTE_ADDRESS: @@ -511,7 +510,7 @@ function handle(call, id, payload) { case SOCKET_OUTGOING_DATAGRAM_STREAM_SEND: return socketOutgoingDatagramStreamSend(id, payload); case SOCKET_DATAGRAM_STREAM_SUBSCRIBE: - return createPoll(datagramStreams.get(id).pollState); + return createPoll(datagramStreams.get(id)?.pollState); case SOCKET_DATAGRAM_STREAM_DISPOSE: return socketDatagramStreamDispose(id); @@ -572,7 +571,7 @@ function handle(call, id, payload) { // Filesystem case INPUT_STREAM_CREATE | FILE: { const { fd, offset } = payload; - const stream = createReadStream(null, { + const stream = createReadStream(null as unknown as PathLike, { fd, autoClose: false, highWaterMark: 64 * 1024, @@ -582,7 +581,7 @@ function handle(call, id, payload) { } case OUTPUT_STREAM_CREATE | FILE: { const { fd, offset } = payload; - const stream = createWriteStream(null, { + const stream = createWriteStream(null as unknown as PathLike, { fd, autoClose: false, emitClose: false, @@ -698,7 +697,7 @@ function handle(call, id, payload) { return; } stream.pollState.ready = false; - stream.flushPromise = new Promise((resolve, reject) => { + stream.flushPromise = new Promise((resolve, reject) => { if (stream.stream === stdout || stream.stream === stderr) { // Inside workers, NodeJS actually queues writes destined for // stdout/stderr in a port that is only flushed on exit of the worker. @@ -733,7 +732,7 @@ function handle(call, id, payload) { if (stream.flushPromise) { return stream.flushPromise; } - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { if (stream.stream === stdout || stream.stream === stderr) { // Inside workers, NodeJS actually queues writes destined for // stdout/stderr in a port that is only flushed on exit of the worker. @@ -848,8 +847,8 @@ function handle(call, id, payload) { if (payload.length === 0) { throw new Error("wasi-io trap: attempt to poll on empty list"); } - const doneList = []; - const pollList = payload.map((pollId) => polls.get(pollId)); + const doneList: number[] = []; + const pollList: [number, any] = payload.map((pollId) => polls.get(pollId)); for (const [idx, pollState] of pollList.entries()) { pollStateCheck(pollState); if (pollState.ready) { diff --git a/packages/preview2-shim/src/nodejs/cli.ts b/packages/preview2-shim/src/nodejs/cli.ts new file mode 100644 index 000000000..dfd48ff91 --- /dev/null +++ b/packages/preview2-shim/src/nodejs/cli.ts @@ -0,0 +1,115 @@ +import process, { argv, env, cwd } from "node:process"; +import { ioCall, inputStreamCreate, outputStreamCreate } from "../io/worker-io.js"; +import type { + exit as ExitNamespace, + stderr as StderrNamespace, + stdin as StdinNamespace, + stdout as StdoutNamespace, + terminalInput as TerminalInputNamespace, + terminalOutput as TerminalOutputNamespace, + terminalStderr as TerminalStderrNamespace, + terminalStdin as TerminalStdinNamespace, + terminalStdout as TerminalStdoutNamespace, +} from "../../types/cli.js"; +import { INPUT_STREAM_CREATE, STDERR, STDIN, STDOUT } from "../io/calls.js"; + +export const _appendEnv = (env: Record) => { + void (_env = [..._env.filter(([curKey]) => !(curKey in env)), ...Object.entries(env)]); +}; +export const _setEnv = (env: Record) => void (_env = Object.entries(env)); +export const _setArgs = (args: string[]) => void (_args = args); +export const _setCwd = (cwd: string) => void (_cwd = cwd); +export const _setStdin = (stdin: any) => void (stdinStream = stdin); +export const _setStdout = (stdout: any) => void (stdoutStream = stdout); +export const _setStderr = (stderr: any) => void (stderrStream = stderr); +export const _setTerminalStdin = (terminalStdin: TerminalInput) => + void (terminalStdinInstance = terminalStdin); +export const _setTerminalStdout = (terminalStdout: TerminalOutput) => + void (terminalStdoutInstance = terminalStdout); +export const _setTerminalStderr = (terminalStderr: TerminalOutput) => + void (terminalStderrInstance = terminalStderr); + +let _env = Object.entries(env), + _args = argv.slice(1), + _cwd = cwd(); + +export const environment = { + getEnvironment() { + return _env; + }, + getArguments() { + return _args; + }, + initialCwd() { + return _cwd; + }, +}; + +export const exit: typeof ExitNamespace = { + exit(status: { tag: string }) { + process.exit(status.tag === "err" ? 1 : 0); + }, + // @ts-expect-error - Available only wasi-cli v0.2.12 + exitWithCode(code: number) { + process.exit(code); + }, +}; + +// Stdin is created as a FILE descriptor +let stdinStream: any; +let stdoutStream = outputStreamCreate(STDOUT, 1); +let stderrStream = outputStreamCreate(STDERR, 2); + +export const stdin: typeof StdinNamespace = { + getStdin() { + if (!stdinStream) { + stdinStream = inputStreamCreate(STDIN, ioCall(INPUT_STREAM_CREATE | STDIN, null, null)); + } + return stdinStream; + }, +}; + +export const stdout: typeof StdoutNamespace = { + getStdout() { + return stdoutStream; + }, +}; + +export const stderr: typeof StderrNamespace = { + getStderr() { + return stderrStream; + }, +}; + +class TerminalInput {} +class TerminalOutput {} + +let terminalStdoutInstance = new TerminalOutput(); +let terminalStderrInstance = new TerminalOutput(); +let terminalStdinInstance = new TerminalInput(); + +export const terminalInput: typeof TerminalInputNamespace = { + TerminalInput, +}; + +export const terminalOutput: typeof TerminalOutputNamespace = { + TerminalOutput, +}; + +export const terminalStdin: typeof TerminalStdinNamespace = { + getTerminalStdin() { + return terminalStdinInstance; + }, +}; + +export const terminalStdout: typeof TerminalStdoutNamespace = { + getTerminalStdout() { + return terminalStdoutInstance; + }, +}; + +export const terminalStderr: typeof TerminalStderrNamespace = { + getTerminalStderr() { + return terminalStderrInstance; + }, +}; diff --git a/packages/preview2-shim/lib/nodejs/clocks.js b/packages/preview2-shim/src/nodejs/clocks.ts similarity index 75% rename from packages/preview2-shim/lib/nodejs/clocks.js rename to packages/preview2-shim/src/nodejs/clocks.ts index 8413d69b8..c72d7b08a 100644 --- a/packages/preview2-shim/lib/nodejs/clocks.js +++ b/packages/preview2-shim/src/nodejs/clocks.ts @@ -1,3 +1,7 @@ +import type { + monotonicClock as MonotonicClockNamespace, + wallClock as WallClockNamespace, +} from "../../types/clocks.js"; import { createPoll } from "../io/worker-io.js"; import { CLOCKS_INSTANT_SUBSCRIBE, CLOCKS_DURATION_SUBSCRIBE } from "../io/calls.js"; import { hrtime } from "node:process"; @@ -8,21 +12,21 @@ function resolution() { return 1n; } -export const monotonicClock = { +export const monotonicClock: typeof MonotonicClockNamespace = { resolution, now() { return hrtime.bigint(); }, - subscribeInstant(instant) { + subscribeInstant(instant: bigint) { return createPoll(CLOCKS_INSTANT_SUBSCRIBE, null, instant); }, - subscribeDuration(duration) { + subscribeDuration(duration: bigint | number) { duration = BigInt(duration); return createPoll(CLOCKS_DURATION_SUBSCRIBE, null, duration); }, }; -export const wallClock = { +export const wallClock: typeof WallClockNamespace = { resolution() { return { seconds: 0n, nanoseconds: 1e6 }; }, @@ -35,9 +39,9 @@ export const wallClock = { monotonicClock.resolution[symbolCabiLower] = () => resolution; monotonicClock.now[symbolCabiLower] = () => hrtime.bigint; -wallClock.resolution[symbolCabiLower] = ({ memory }) => { +wallClock.resolution[symbolCabiLower] = ({ memory }: any) => { let buf32 = new Int32Array(memory.buffer); - return function now(retptr) { + return function now(retptr: number) { if (memory.buffer !== buf32.buffer) { buf32 = new Int32Array(memory.buffer); } @@ -50,10 +54,10 @@ wallClock.resolution[symbolCabiLower] = ({ memory }) => { }; }; -wallClock.now[symbolCabiLower] = ({ memory }) => { +wallClock.now[symbolCabiLower] = ({ memory }: any) => { let buf32 = new Int32Array(memory.buffer); let buf64 = new BigInt64Array(memory.buffer); - return function now(retptr) { + return function now(retptr: number) { if (memory.buffer !== buf32.buffer) { buf32 = new Int32Array(memory.buffer); buf64 = new BigInt64Array(memory.buffer); diff --git a/packages/preview2-shim/lib/nodejs/filesystem.js b/packages/preview2-shim/src/nodejs/filesystem.ts similarity index 91% rename from packages/preview2-shim/lib/nodejs/filesystem.js rename to packages/preview2-shim/src/nodejs/filesystem.ts index a44029f99..24591d6ea 100644 --- a/packages/preview2-shim/lib/nodejs/filesystem.js +++ b/packages/preview2-shim/src/nodejs/filesystem.ts @@ -1,3 +1,9 @@ +import { types as TypesNamespace, preopens as PreopensNamespace } from "../../types/filesystem.js"; +import type { + Descriptor as IDescriptor, + DescriptorType, + DirectoryEntryStream as IDirectoryEntryStream, +} from "../../types/interfaces/wasi-filesystem-types.js"; import { earlyDispose, inputStreamCreate, @@ -5,9 +11,8 @@ import { outputStreamCreate, registerDispose, } from "../io/worker-io.js"; -import { INPUT_STREAM_CREATE, OUTPUT_STREAM_CREATE } from "../io/calls.js"; -import { FILE } from "../io/calls.js"; -import nodeFs, { +import { FILE, INPUT_STREAM_CREATE, OUTPUT_STREAM_CREATE } from "../io/calls.js"; +import { closeSync, constants, fdatasyncSync, @@ -17,6 +22,7 @@ import nodeFs, { futimesSync, linkSync, lstatSync, + lutimesSync, mkdirSync, opendirSync, openSync, @@ -32,8 +38,6 @@ import nodeFs, { } from "node:fs"; import { platform } from "node:process"; -const lutimesSync = nodeFs.lutimesSync; - const symbolDispose = Symbol.dispose || Symbol.for("dispose"); const isWindows = platform === "win32"; @@ -46,7 +50,7 @@ function nsToDateTime(ns) { return { seconds, nanoseconds }; } -function lookupType(obj) { +function lookupType(obj): DescriptorType { if (obj.isFile()) { return "regular-file"; } else if (obj.isSocket()) { @@ -72,12 +76,13 @@ function lookupType(obj) { /** * @implements {DescriptorProps} */ -class Descriptor { +class Descriptor implements IDescriptor { #hostPreopen; #fd; #finalizer; #mode; #fullPath; + private _id = 0; static _createPreopen(hostPreopen) { const descriptor = new Descriptor(); @@ -110,7 +115,7 @@ class Descriptor { } } - readViaStream(offset) { + readViaStream(offset): any { if (this.#hostPreopen) { throw "is-directory"; } @@ -123,7 +128,7 @@ class Descriptor { ); } - writeViaStream(offset) { + writeViaStream(offset): any { if (this.#hostPreopen) { throw "is-directory"; } @@ -133,7 +138,7 @@ class Descriptor { ); } - appendViaStream() { + appendViaStream(): any { return this.writeViaStream(this.stat().size); } @@ -149,7 +154,7 @@ class Descriptor { } try { fdatasyncSync(this.#fd); - } catch (e) { + } catch (e: any) { if (e.code === "EPERM") { return; } @@ -175,7 +180,7 @@ class Descriptor { } try { ftruncateSync(this.#fd, Number(size)); - } catch (e) { + } catch (e: any) { if (isWindows && e.code === "EPERM") { throw "access"; } @@ -210,21 +215,21 @@ class Descriptor { switch (newTimestamp.tag) { case "no-change": return timestampToMs(maybeNow); - case "now": - return Math.floor(Date.now() / 1e3); case "timestamp": return timestampToMs(newTimestamp.val); + default: + return Math.floor(Date.now() / 1e3); } } - read(length, offset) { + read(length, offset): [Uint8Array, boolean] { if (!this.#fullPath) { throw "bad-descriptor"; } const buf = new Uint8Array(Number(length)); const bytesRead = readSync(this.#fd, buf, 0, Number(length), Number(offset)); const out = new Uint8Array(buf.buffer, 0, bytesRead); - return [out, bytesRead === 0 ? "ended" : "open"]; + return [out, bytesRead === 0]; } write(buffer, offset) { @@ -234,7 +239,7 @@ class Descriptor { return BigInt(writeSync(this.#fd, buffer, 0, buffer.byteLength, Number(offset))); } - readDirectory() { + readDirectory(): IDirectoryEntryStream { if (!this.#fullPath) { throw "bad-descriptor"; } @@ -252,7 +257,7 @@ class Descriptor { } try { fsyncSync(this.#fd); - } catch (e) { + } catch (e: any) { if (e.code === "EPERM") { return; } @@ -261,7 +266,7 @@ class Descriptor { } createDirectoryAt(path) { - const fullPath = this.#getFullPath(path); + const fullPath = this.#getFullPath(path, undefined); try { mkdirSync(fullPath); } catch (e) { @@ -418,13 +423,13 @@ class Descriptor { } try { const fd = openSync(fullPath.endsWith("/") ? fullPath.slice(0, -1) : fullPath, fsOpenFlags); - const descriptor = descriptorCreate(fd, descriptorFlags, fullPath, preopenEntries); + const descriptor = descriptorCreate(fd, descriptorFlags, fullPath); if (fullPath.endsWith("/") && descriptor.getType() !== "directory") { descriptor[symbolDispose](); throw "not-directory"; } return descriptor; - } catch (e) { + } catch (e: any) { if (e.code === "ERR_INVALID_ARG_VALUE") { throw isWindows ? "no-entry" : "invalid"; } @@ -445,7 +450,7 @@ class Descriptor { const fullPath = this.#getFullPath(path, false); try { rmdirSync(fullPath); - } catch (e) { + } catch (e: any) { if (isWindows && e.code === "ENOENT") { throw "not-directory"; } @@ -458,7 +463,7 @@ class Descriptor { const newFullPath = newDescriptor.#getFullPath(newPath, false); try { renameSync(oldFullPath, newFullPath); - } catch (e) { + } catch (e: any) { if (isWindows && e.code === "EPERM") { throw "access"; } @@ -473,7 +478,7 @@ class Descriptor { } try { symlinkSync(target, fullPath); - } catch (e) { + } catch (e: any) { if (fullPath.endsWith("/") && e.code === "EEXIST") { let isDir = false; try { @@ -513,7 +518,7 @@ class Descriptor { : "not-directory"; } unlinkSync(fullPath); - } catch (e) { + } catch (e: any) { if (isWindows && e.code === "EPERM") { throw "access"; } @@ -563,7 +568,7 @@ class Descriptor { } // segment resolution - const segments = []; + const segments: string[] = []; let segmentIndex = -1; for (let i = 0; i < subpath.length; i++) { // busy reading a segment - only terminate on '/' @@ -614,11 +619,13 @@ class Descriptor { } } const descriptorCreatePreopen = Descriptor._createPreopen; +// @ts-expect-error - Deleting static method delete Descriptor._createPreopen; const descriptorCreate = Descriptor._create; +// @ts-expect-error - Deleting static method delete Descriptor._create; -class DirectoryEntryStream { +class DirectoryEntryStream implements IDirectoryEntryStream { #dir; #finalizer; readDirectoryEntry() { @@ -629,7 +636,7 @@ class DirectoryEntryStream { throw convertFsError(e); } if (entry === null) { - return null; + return undefined; } const name = entry.name; const type = lookupType(entry); @@ -637,7 +644,7 @@ class DirectoryEntryStream { } static _create(dir) { const dirStream = new DirectoryEntryStream(); - dirStream.#finalizer = registerDispose(dirStream, null, null, dir.closeSync.bind(dir)); + dirStream.#finalizer = registerDispose(dirStream, null, 0, dir.closeSync.bind(dir)); dirStream.#dir = dir; return dirStream; } @@ -649,12 +656,12 @@ class DirectoryEntryStream { } } const directoryEntryStreamCreate = DirectoryEntryStream._create; +// @ts-expect-error Deleting static method delete DirectoryEntryStream._create; -let preopenEntries = []; +let preopenEntries: Array<[Descriptor, string]> = []; -export const preopens = { - Descriptor, +export const preopens: typeof PreopensNamespace = { getDirectories() { return preopenEntries; }, @@ -662,10 +669,10 @@ export const preopens = { _addPreopen("/", isWindows ? "//" : "/"); -export const types = { +export const types: typeof TypesNamespace = { Descriptor, DirectoryEntryStream, - filesystemErrorCode(err) { + filesystemErrorCode(err: any) { return convertFsError(err.payload); }, }; @@ -687,7 +694,7 @@ export function _setPreopens(preopens) { * @param {string} hostPreopen - The host filesystem path */ export function _addPreopen(virtualPath, hostPreopen) { - const preopenEntry = [descriptorCreatePreopen(hostPreopen), virtualPath]; + const preopenEntry: [Descriptor, string] = [descriptorCreatePreopen(hostPreopen), virtualPath]; preopenEntries.push(preopenEntry); } @@ -714,10 +721,10 @@ export function _getPreopens() { /** * Create a preopen descriptor for a host path. * This is used internally to create isolated preopen instances. - * @param {string} hostPreopen - The host filesystem path - * @returns {Descriptor} A preopen descriptor + * @param hostPreopen - The host filesystem path + * @returns A preopen descriptor */ -export function _createPreopenDescriptor(hostPreopen) { +export function _createPreopenDescriptor(hostPreopen: string) { return descriptorCreatePreopen(hostPreopen); } @@ -818,10 +825,10 @@ function convertFsError(e) { function timestampToMs(timestamp) { let secondsInMillis = timestamp.seconds * 1000n; if (secondsInMillis > Number.MAX_SAFE_INTEGER) { - throw new Error(`cannot represent millisecond amount as Number [${{ nanosInMillis }}]`); + throw new Error(`cannot represent millisecond amount as Number [${{ secondsInMillis }}]`); } if (timestamp.nanoseconds > Number.MAX_SAFE_INTEGER) { - throw new Error(`cannot represent millisecond amount as Number [${{ nanosInMillis }}]`); + throw new Error(`cannot represent millisecond amount as Number [${{ secondsInMillis }}]`); } let remainderFractionalMillis = timestamp.nanoseconds / 1_000_000; const res = Number(secondsInMillis) + remainderFractionalMillis; diff --git a/packages/preview2-shim/lib/nodejs/http.js b/packages/preview2-shim/src/nodejs/http.ts similarity index 88% rename from packages/preview2-shim/lib/nodejs/http.js rename to packages/preview2-shim/src/nodejs/http.ts index 8227c0b9f..f2dfb10d1 100644 --- a/packages/preview2-shim/lib/nodejs/http.js +++ b/packages/preview2-shim/src/nodejs/http.ts @@ -1,3 +1,4 @@ +import type { types as TypesNamespace } from "../../types/http.js"; import { FUTURE_DISPOSE, FUTURE_SUBSCRIBE, @@ -25,14 +26,17 @@ import { import { HTTP } from "../io/calls.js"; import * as http from "node:http"; +import { InputStream, OutputStream, Pollable } from "../../types/interfaces/wasi-io-streams.js"; const { validateHeaderName = () => {}, validateHeaderValue = () => {} } = http; +type Result = TypesNamespace.Result; + const symbolDispose = Symbol.dispose || Symbol.for("dispose"); export const _forbiddenHeaders = new Set(["connection", "keep-alive", "host"]); -class IncomingBody { +class IncomingBody implements TypesNamespace.IncomingBody { #finished = false; - #stream = undefined; + #stream: InputStream | null = null; stream() { if (!this.#stream) { throw undefined; @@ -56,6 +60,7 @@ class IncomingBody { } } const incomingBodyCreate = IncomingBody._create; +// @ts-expect-error - Deleting static method delete IncomingBody._create; class IncomingRequest { @@ -96,16 +101,19 @@ class IncomingRequest { } } const incomingRequestCreate = IncomingRequest._create; +// @ts-expect-error - Deleting static method delete IncomingRequest._create; -class FutureTrailers { +class FutureTrailers implements TypesNamespace.FutureTrailers { #requested = false; subscribe() { return pollableCreate(0, this); } - get() { + get(): + | Result, void> + | undefined { if (this.#requested) { - return { tag: "err" }; + return { tag: "err", val: undefined }; } this.#requested = true; return { @@ -122,6 +130,7 @@ class FutureTrailers { } } const futureTrailersCreate = FutureTrailers._create; +// @ts-expect-error - Deleting static method delete FutureTrailers._create; class OutgoingResponse { @@ -166,6 +175,7 @@ class OutgoingResponse { } const outgoingResponseBody = OutgoingResponse._body; +// @ts-expect-error - Deleting static method delete OutgoingResponse._body; class ResponseOutparam { @@ -180,6 +190,7 @@ class ResponseOutparam { } } const responseOutparamCreate = ResponseOutparam._create; +// @ts-expect-error - Deleting static method delete ResponseOutparam._create; const defaultHttpTimeout = 600_000_000_000n; @@ -208,13 +219,16 @@ class RequestOptions { } } -class OutgoingRequest { - /** @type {Method} */ #method = { tag: "get" }; - /** @type {Scheme | undefined} */ #scheme = undefined; - /** @type {string | undefined} */ #pathWithQuery = undefined; - /** @type {string | undefined} */ #authority = undefined; - /** @type {Fields} */ #headers; - /** @type {OutgoingBody} */ #body; +type Method = TypesNamespace.Method; +type Scheme = TypesNamespace.Scheme; + +class OutgoingRequest implements TypesNamespace.OutgoingRequest { + #method: Method = { tag: "get" }; + #scheme: Scheme | undefined = undefined; + #pathWithQuery: string | undefined = undefined; + #authority: string | undefined = undefined; + #headers: Fields; + #body: OutgoingBody; #bodyRequested = false; constructor(headers) { fieldsLock(headers); @@ -290,7 +304,7 @@ class OutgoingRequest { const firstByteTimeout = options?.firstByteTimeout(); const scheme = schemeString(request.#scheme); // note: host header is automatically added by Node.js - const headers = []; + const headers: [string, string][] = []; const decoder = new TextDecoder(); for (const [key, value] of request.#headers.entries()) { headers.push([key, decoder.decode(value)]); @@ -313,11 +327,12 @@ class OutgoingRequest { } const outgoingRequestHandle = OutgoingRequest._handle; +// @ts-expect-error - Deleting static method delete OutgoingRequest._handle; -class OutgoingBody { - #outputStream = null; - #outputStreamId = null; +class OutgoingBody implements TypesNamespace.OutgoingBody { + #outputStream: OutputStream | null = null; + #outputStreamId: number | null = null; #contentLength = undefined; #finalizer; write() { @@ -329,11 +344,7 @@ class OutgoingBody { this.#outputStream = null; return outputStream; } - /** - * @param {OutgoingBody} body - * @param {Fields | undefined} trailers - */ - static finish(body, trailers) { + static finish(body: OutgoingBody, trailers: Fields | undefined) { if (trailers) { throw { tag: "internal-error", val: "trailers unsupported" }; } @@ -356,7 +367,7 @@ class OutgoingBody { outgoingBody.#finalizer = registerDispose( outgoingBody, null, - outgoingBody.#outputStreamId, + outgoingBody.#outputStreamId!, outgoingBodyDispose, ); return outgoingBody; @@ -374,15 +385,18 @@ function outgoingBodyDispose(id) { } const outgoingBodyOutputStreamId = OutgoingBody._outputStreamId; +// @ts-expect-error - Deleting static method delete OutgoingBody._outputStreamId; const outgoingBodyCreate = OutgoingBody._create; +// @ts-expect-error - Deleting static method delete OutgoingBody._create; -class IncomingResponse { - /** @type {Fields} */ #headers = undefined; +class IncomingResponse implements TypesNamespace.IncomingResponse { + #headers: Fields = undefined as any; #status = 0; - /** @type {number} */ #bodyStream; + #bodyStream: IncomingBody | undefined; + status() { return this.#status; } @@ -412,12 +426,13 @@ class IncomingResponse { } const incomingResponseCreate = IncomingResponse._create; +// @ts-expect-error - Deleting static method delete IncomingResponse._create; -class FutureIncomingResponse { +class FutureIncomingResponse implements TypesNamespace.FutureIncomingResponse { #id; #finalizer; - subscribe() { + subscribe(): Pollable { return pollableCreate(ioCall(FUTURE_SUBSCRIBE | HTTP, this.#id, null), this); } get() { @@ -475,12 +490,16 @@ function futureIncomingResponseDispose(id) { } const futureIncomingResponseCreate = FutureIncomingResponse._create; +// @ts-expect-error - Deleting static method delete FutureIncomingResponse._create; +type FieldName = TypesNamespace.FieldName; +type FieldValue = TypesNamespace.FieldValue; + class Fields { #immutable = false; - /** @type {[string, Uint8Array[]][]} */ #entries = []; - /** @type {Map} */ #table = new Map(); + #entries: [FieldName, FieldValue][] = []; + #table = new Map(); /** * @param {[string, Uint8Array[][]][]} entries @@ -534,16 +553,16 @@ class Fields { } let tableEntries = this.#table.get(lowercased); if (tableEntries) { - this.#entries = this.#entries.filter((entry) => !tableEntries.includes(entry)); + this.#entries = this.#entries.filter((entry) => !tableEntries?.includes(entry)); tableEntries.splice(0, tableEntries.length); } else { this.#table.set(lowercased, []); tableEntries = this.#table.get(lowercased); } for (const value of values) { - const entry = [name, value]; + const entry: [FieldName, FieldValue] = [name, value]; this.#entries.push(entry); - tableEntries.push(entry); + tableEntries?.push(entry); } } has(name) { @@ -578,7 +597,7 @@ class Fields { if (_forbiddenHeaders.has(lowercased)) { throw { tag: "forbidden" }; } - const entry = [name, value]; + const entry: [string, Uint8Array] = [name, value]; this.#entries.push(entry); const tableEntries = this.#table.get(lowercased); if (tableEntries) { @@ -614,8 +633,10 @@ class Fields { } } const fieldsLock = Fields._lock; +// @ts-expect-error - Deleting static method delete Fields._lock; const fieldsFromEntriesChecked = Fields._fromEntriesChecked; +// @ts-expect-error - Deleting static method delete Fields._fromEntriesChecked; export const outgoingHandler = { @@ -637,7 +658,7 @@ function httpErrorCode(err) { }; } -export const types = { +export const types: typeof TypesNamespace = { Fields, FutureIncomingResponse, FutureTrailers, diff --git a/packages/preview2-shim/src/nodejs/index.ts b/packages/preview2-shim/src/nodejs/index.ts new file mode 100644 index 000000000..4e49f6d32 --- /dev/null +++ b/packages/preview2-shim/src/nodejs/index.ts @@ -0,0 +1,7 @@ +export * as cli from "./cli.js"; +export * as clocks from "./clocks.js"; +export * as filesystem from "./filesystem.js"; +export * as http from "./http.js"; +export * as io from "./io.js"; +export * as random from "./random.js"; +export * as sockets from "./sockets.js"; diff --git a/packages/preview2-shim/src/nodejs/io.ts b/packages/preview2-shim/src/nodejs/io.ts new file mode 100644 index 000000000..a2cce6634 --- /dev/null +++ b/packages/preview2-shim/src/nodejs/io.ts @@ -0,0 +1 @@ +export { error, streams, poll, inputStreamCreate, outputStreamCreate } from "../io/worker-io.js"; diff --git a/packages/preview2-shim/lib/nodejs/random.js b/packages/preview2-shim/src/nodejs/random.ts similarity index 61% rename from packages/preview2-shim/lib/nodejs/random.js rename to packages/preview2-shim/src/nodejs/random.ts index 13200bed0..772ded57b 100644 --- a/packages/preview2-shim/lib/nodejs/random.js +++ b/packages/preview2-shim/src/nodejs/random.ts @@ -1,16 +1,21 @@ import { randomBytes, randomFillSync } from "node:crypto"; +import type { + insecure as InsecureNamespace, + insecureSeed as InsecureSeedNamespace, + random as RandomNamespace, +} from "../../types/random.js"; -export const insecure = { +export const insecure: typeof InsecureNamespace = { getInsecureRandomBytes: getRandomBytes, getInsecureRandomU64() { return new BigUint64Array(randomBytes(8).buffer)[0]; }, }; -let insecureSeedValue1, insecureSeedValue2; +let insecureSeedValue1: bigint, insecureSeedValue2: bigint; -export const insecureSeed = { - insecureSeed() { +export const insecureSeed: typeof InsecureSeedNamespace = { + insecureSeed(): [bigint, bigint] { if (insecureSeedValue1 === undefined) { insecureSeedValue1 = random.getRandomU64(); insecureSeedValue2 = random.getRandomU64(); @@ -19,21 +24,20 @@ export const insecureSeed = { }, }; -export const random = { +export const random: typeof RandomNamespace = { getRandomBytes, - getRandomU64() { return new BigUint64Array(randomBytes(8).buffer)[0]; }, }; -function getRandomBytes(len) { +function getRandomBytes(len: bigint | number): Uint8Array { return randomBytes(Number(len)); } -getRandomBytes[Symbol.for("cabiLower")] = ({ memory, realloc }) => { +getRandomBytes[Symbol.for("cabiLower")] = ({ memory, realloc }: any) => { let buf32 = new Uint32Array(memory.buffer); - return function randomBytes(len, retptr) { + return function randomBytesImpl(len: number, retptr: number) { len = Number(len); const ptr = realloc(0, 0, 1, len); randomFillSync(memory.buffer, ptr, len); diff --git a/packages/preview2-shim/lib/nodejs/sockets.js b/packages/preview2-shim/src/nodejs/sockets.ts similarity index 84% rename from packages/preview2-shim/lib/nodejs/sockets.js rename to packages/preview2-shim/src/nodejs/sockets.ts index dd045ce38..b8e4329a8 100644 --- a/packages/preview2-shim/lib/nodejs/sockets.js +++ b/packages/preview2-shim/src/nodejs/sockets.ts @@ -50,16 +50,21 @@ import { pollableCreate, registerDispose, } from "../io/worker-io.js"; +import type { + instanceNetwork as InstanceNetworkNamespace, + ipNameLookup as IpNameLookupNamespace, + network as NetworkNamespace, + tcpCreateSocket as TcpCreateSocketNamespace, + tcp as TcpNamespace, + udpCreateSocket as UdpCreateSocketNamespace, + udp as UdpNamespace, +} from "../../types/sockets.js"; +import { IpAddressFamily } from "../../types/interfaces/wasi-sockets-network.js"; const symbolDispose = Symbol.dispose || Symbol.for("dispose"); -/** - * @typedef {import("../../types/interfaces/wasi-sockets-network").IpSocketAddress} IpSocketAddress - * @typedef {import("../../types/interfaces/wasi-sockets-network").IpAddressFamily} IpAddressFamily - */ - // Network class privately stores capabilities -class Network { +class Network implements NetworkNamespace.Network { #allowDnsLookup = true; #allowTcp = true; #allowUdp = true; @@ -85,36 +90,44 @@ class Network { } export const _denyDnsLookup = Network._denyDnsLookup; +// @ts-expect-error - Deleting static method delete Network._denyDnsLookup; export const _denyTcp = Network._denyTcp; +// @ts-expect-error - Deleting static method delete Network._denyTcp; export const _denyUdp = Network._denyUdp; +// @ts-expect-error - Deleting static method delete Network._denyUdp; const mayDnsLookup = Network._mayDnsLookup; +// @ts-expect-error - Deleting static method delete Network._mayDnsLookup; const mayTcp = Network._mayTcp; +// @ts-expect-error - Deleting static method delete Network._mayTcp; const mayUdp = Network._mayUdp; +// @ts-expect-error - Deleting static method delete Network._mayUdp; const defaultNetwork = new Network(); -export const instanceNetwork = { +export const instanceNetwork: typeof InstanceNetworkNamespace = { instanceNetwork() { return defaultNetwork; }, }; -export const network = { Network }; +export const network: typeof NetworkNamespace = { + Network, +}; -class ResolveAddressStream { - #id; - #data; +class ResolveAddressStream implements IpNameLookupNamespace.ResolveAddressStream { + #id = 0; + #data: Array | null = null; #curItem = 0; #error = false; #finalizer; @@ -127,7 +140,7 @@ class ResolveAddressStream { if (this.#error) { throw this.#data; } - if (this.#curItem < this.#data.length) { + if (this.#data && this.#curItem < this.#data.length) { return this.#data[this.#curItem++]; } return undefined; @@ -135,8 +148,8 @@ class ResolveAddressStream { subscribe() { return pollableCreate(ioCall(SOCKET_RESOLVE_ADDRESS_SUBSCRIBE_REQUEST, this.#id, null), this); } - static _resolveAddresses(network, name) { - if (!mayDnsLookup(network)) { + static _resolveAddresses(network: NetworkNamespace.Network, name: string) { + if (!(network instanceof Network) || !mayDnsLookup(network)) { throw "permanent-resolver-failure"; } const res = new ResolveAddressStream(); @@ -151,20 +164,21 @@ class ResolveAddressStream { } } } -function resolveAddressStreamDispose(id) { +function resolveAddressStreamDispose(id: number) { ioCall(SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST, id, null); } const resolveAddresses = ResolveAddressStream._resolveAddresses; +// @ts-expect-error - Deleting static method delete ResolveAddressStream._resolveAddresses; -export const ipNameLookup = { +export const ipNameLookup: typeof IpNameLookupNamespace = { ResolveAddressStream, resolveAddresses, }; -class TcpSocket { - #id; +class TcpSocket implements TcpNamespace.TcpSocket { + #id = 0; #network; #family; #finalizer; @@ -190,12 +204,7 @@ class TcpSocket { sendBufferSize: undefined, receiveBufferSize: undefined, }; - /** - * @param {IpAddressFamily} addressFamily - * @param {number} id - * @returns {TcpSocket} - */ - static _create(addressFamily, id) { + static _create(addressFamily: IpAddressFamily, id: number) { const socket = new TcpSocket(); socket.#id = id; socket.#family = addressFamily; @@ -228,7 +237,7 @@ class TcpSocket { }); this.#network = network; } - finishConnect() { + finishConnect(): [TcpNamespace.InputStream, TcpNamespace.OutputStream] { const [inputStreamId, outputStreamId] = ioCall(SOCKET_TCP_CONNECT_FINISH, this.#id, null); return [ inputStreamCreate(SOCKET_TCP, inputStreamId), @@ -244,7 +253,7 @@ class TcpSocket { finishListen() { ioCall(SOCKET_TCP_LISTEN_FINISH, this.#id, null); } - accept() { + accept(): [TcpNamespace.TcpSocket, TcpNamespace.InputStream, TcpNamespace.OutputStream] { if (!mayTcp(this.#network)) { throw "access-denied"; } @@ -336,7 +345,7 @@ class TcpSocket { if (!this.#options.receiveBufferSize) { this.#options.receiveBufferSize = ioCall(SOCKET_GET_DEFAULT_RECEIVE_BUFFER_SIZE, null, null); } - return this.#options.receiveBufferSize; + return this.#options.receiveBufferSize as unknown as bigint; } setReceiveBufferSize(value) { if (value === 0n) { @@ -348,7 +357,7 @@ class TcpSocket { if (!this.#options.sendBufferSize) { this.#options.sendBufferSize = ioCall(SOCKET_GET_DEFAULT_SEND_BUFFER_SIZE, null, null); } - return this.#options.sendBufferSize; + return this.#options.sendBufferSize as unknown as bigint; } setSendBufferSize(value) { if (value === 0n) { @@ -375,9 +384,10 @@ function socketTcpDispose(id) { } const tcpSocketCreate = TcpSocket._create; +// @ts-expect-error - Deleting static method delete TcpSocket._create; -export const tcpCreateSocket = { +export const tcpCreateSocket: typeof TcpCreateSocketNamespace = { createTcpSocket(addressFamily) { if (addressFamily !== "ipv4" && addressFamily !== "ipv6") { throw "not-supported"; @@ -386,12 +396,12 @@ export const tcpCreateSocket = { }, }; -export const tcp = { +export const tcp: typeof TcpNamespace = { TcpSocket, }; -class UdpSocket { - #id; +class UdpSocket implements UdpNamespace.UdpSocket { + #id = 0; #network; #family; #finalizer; @@ -424,7 +434,9 @@ class UdpSocket { finishBind() { ioCall(SOCKET_UDP_BIND_FINISH, this.#id, null); } - stream(remoteAddress) { + stream( + remoteAddress, + ): [UdpNamespace.IncomingDatagramStream, UdpNamespace.OutgoingDatagramStream] { if (!mayUdp(this.#network)) { throw "access-denied"; } @@ -490,10 +502,11 @@ function socketUdpDispose(id) { } const createUdpSocket = UdpSocket._create; +// @ts-expect-error - Deleting static method delete UdpSocket._create; -class IncomingDatagramStream { - #id; +class IncomingDatagramStream implements UdpNamespace.IncomingDatagramStream { + #id = 0; #finalizer; static _create(id) { const stream = new IncomingDatagramStream(); @@ -520,9 +533,10 @@ function incomingDatagramStreamDispose(id) { } const incomingDatagramStreamCreate = IncomingDatagramStream._create; +// @ts-expect-error - Deleting static method delete IncomingDatagramStream._create; -class OutgoingDatagramStream { +class OutgoingDatagramStream implements UdpNamespace.OutgoingDatagramStream { #id = 0; #finalizer; static _create(id) { @@ -552,13 +566,14 @@ function outgoingDatagramStreamDispose(id) { } const outgoingDatagramStreamCreate = OutgoingDatagramStream._create; +// @ts-expect-error - Deleting static method delete OutgoingDatagramStream._create; -export const udpCreateSocket = { +export const udpCreateSocket: typeof UdpCreateSocketNamespace = { createUdpSocket, }; -export const udp = { +export const udp: typeof UdpNamespace = { UdpSocket, OutgoingDatagramStream, IncomingDatagramStream, diff --git a/packages/preview2-shim/lib/synckit/index.js b/packages/preview2-shim/src/synckit/index.ts similarity index 96% rename from packages/preview2-shim/lib/synckit/index.js rename to packages/preview2-shim/src/synckit/index.ts index 2b6c8437d..edce9e26f 100644 --- a/packages/preview2-shim/lib/synckit/index.js +++ b/packages/preview2-shim/src/synckit/index.ts @@ -73,7 +73,7 @@ export function createSyncFn(workerPath, debug, callbackHandler) { if (!["ok", "not-equal"].includes(status)) { throw new Error("Internal error: Atomics.wait() failed: " + status); } - const { cid: cid2, result, error, properties } = receiveMessageOnPort(mainPort).message; + const { cid: cid2, result, error, properties } = receiveMessageOnPort(mainPort)?.message ?? {}; if (cid !== cid2) { throw new Error(`Internal error: Expected id ${cid} but got id ${cid2}`); } @@ -97,7 +97,7 @@ export function runAsWorker(fn) { } const { workerPort, debug } = workerData; try { - parentPort.on("message", ({ sharedBuffer, cid, args }) => { + parentPort?.on("message", ({ sharedBuffer, cid, args }) => { (async () => { const sharedBufferView = new Int32Array(sharedBuffer); let msg; @@ -112,7 +112,7 @@ export function runAsWorker(fn) { })(); }); } catch (error) { - parentPort.on("message", ({ sharedBuffer, cid }) => { + parentPort?.on("message", ({ sharedBuffer, cid }) => { const sharedBufferView = new Int32Array(sharedBuffer); workerPort.postMessage({ cid, diff --git a/packages/preview2-shim/test/browser.js b/packages/preview2-shim/test/browser.ts similarity index 95% rename from packages/preview2-shim/test/browser.js rename to packages/preview2-shim/test/browser.ts index 0cb53db2c..ba0e51414 100644 --- a/packages/preview2-shim/test/browser.js +++ b/packages/preview2-shim/test/browser.ts @@ -2,11 +2,13 @@ import { writeFile, mkdir } from "node:fs/promises"; import { dirname } from "node:path"; import { suite, test, assert } from "vitest"; -import { componentize } from "@bytecodealliance/componentize-js"; +import { componentize, ComponentizeOptions } from "@bytecodealliance/componentize-js"; import { transpile } from "@bytecodealliance/jco"; import { getTmpDir, FIXTURES_WIT_DIR, startTestServer, runBasicHarnessPageTest } from "./common.js"; +type TranspileOutput = { files: { [filename: string]: Uint8Array } }; + suite("browser", () => { test("native-fetch", async () => { const outDir = await getTmpDir(); @@ -103,13 +105,12 @@ export const test = { } `, { - sourceName: "component", witPath: FIXTURES_WIT_DIR, worldName: "browser-http-fetch", - }, + } as ComponentizeOptions, ); - const { files } = await transpile(component, { + const { files }: TranspileOutput = await transpile(component, { async: true, name: "component", optimize: false, @@ -133,7 +134,7 @@ export const test = { url: `${baseURL}/index.html#transpiled:component.js`, }); - assert.ok(statusJSON.msg.includes("hello from test server")); + assert.ok(statusJSON.msg?.includes("hello from test server")); await cleanup(); }, 120_000); @@ -207,13 +208,12 @@ export const test = { } `, { - sourceName: "component", witPath: FIXTURES_WIT_DIR, worldName: "browser-http-fetch", - }, + } as ComponentizeOptions, ); - const { files } = await transpile(component, { + const { files }: TranspileOutput = await transpile(component, { async: true, name: "component", optimize: false, @@ -237,7 +237,7 @@ export const test = { url: `${baseURL}/index.html#transpiled:component.js`, }); - assert.ok(statusJSON.msg.includes("hello from test server")); + assert.ok(statusJSON.msg?.includes("hello from test server")); await cleanup(); }, 120_000); @@ -320,13 +320,12 @@ export const test = { } `, { - sourceName: "component", witPath: FIXTURES_WIT_DIR, worldName: "browser-http-poll-fetch", - }, + } as ComponentizeOptions, ); - const { files } = await transpile(component, { + const { files }: TranspileOutput = await transpile(component, { async: true, name: "component", optimize: false, @@ -351,7 +350,7 @@ export const test = { url: `${baseURL}/index.html#transpiled:component.js`, }); - assert.ok(statusJSON.msg.includes("hello from test server")); + assert.ok(statusJSON.msg?.includes("hello from test server")); await cleanup(); }, 120_000); @@ -424,13 +423,12 @@ export const test = { } `, { - sourceName: "component", witPath: FIXTURES_WIT_DIR, worldName: "browser-http-fetch", - }, + } as ComponentizeOptions, ); - const { files } = await transpile(component, { + const { files }: TranspileOutput = await transpile(component, { async: true, name: "component", optimize: false, @@ -454,7 +452,7 @@ export const test = { url: `${baseURL}/index.html#transpiled:component.js`, }); - assert.ok(statusJSON.msg.includes("hello from test server")); + assert.ok(statusJSON.msg?.includes("hello from test server")); await cleanup(); }, 120_000); @@ -495,13 +493,12 @@ export const test = { } `, { - sourceName: "component", witPath: FIXTURES_WIT_DIR, worldName: "browser-clocks-poll", - }, + } as ComponentizeOptions, ); - const { files } = await transpile(component, { + const { files }: TranspileOutput = await transpile(component, { async: true, name: "component", optimize: false, @@ -525,7 +522,7 @@ export const test = { url: `${baseURL}/index.html#transpiled:component.js`, }); - assert.ok(statusJSON.msg.includes("all passed")); + assert.ok(statusJSON.msg?.includes("all passed")); await cleanup(); }); @@ -574,13 +571,12 @@ export const test = { } `, { - sourceName: "component", witPath: FIXTURES_WIT_DIR, worldName: "browser-clocks-poll", - }, + } as ComponentizeOptions, ); - const { files } = await transpile(component, { + const { files }: TranspileOutput = await transpile(component, { async: true, name: "component", optimize: false, @@ -604,7 +600,7 @@ export const test = { url: `${baseURL}/index.html#transpiled:component.js`, }); - assert.ok(statusJSON.msg.includes("all passed")); + assert.ok(statusJSON.msg?.includes("all passed")); await cleanup(); }); @@ -639,13 +635,12 @@ export const test = { } `, { - sourceName: "component", witPath: FIXTURES_WIT_DIR, worldName: "browser-clocks-poll", - }, + } as ComponentizeOptions, ); - const { files } = await transpile(component, { + const { files }: TranspileOutput = await transpile(component, { async: true, name: "component", optimize: false, @@ -668,7 +663,7 @@ export const test = { url: `${baseURL}/index.html#transpiled:component.js`, }); - assert.ok(statusJSON.msg.includes("all passed")); + assert.ok(statusJSON.msg?.includes("all passed")); await cleanup(); }); @@ -792,13 +787,12 @@ export const test = { } `, { - sourceName: "component", witPath: FIXTURES_WIT_DIR, worldName: "browser-http-fetch", - }, + } as ComponentizeOptions, ); - const { files } = await transpile(component, { + const { files }: TranspileOutput = await transpile(component, { async: true, name: "component", optimize: false, @@ -822,7 +816,7 @@ export const test = { url: `${baseURL}/index.html#transpiled:component.js`, }); - assert.ok(statusJSON.msg.includes("all passed")); + assert.ok(statusJSON.msg?.includes("all passed")); await cleanup(); }, 120_000); @@ -990,13 +984,12 @@ export const test = { } `, { - sourceName: "component", witPath: FIXTURES_WIT_DIR, worldName: "browser-http-fetch", - }, + } as ComponentizeOptions, ); - const { files } = await transpile(component, { + const { files }: TranspileOutput = await transpile(component, { async: true, name: "component", optimize: false, @@ -1020,7 +1013,7 @@ export const test = { url: `${baseURL}/index.html#transpiled:component.js`, }); - assert.ok(statusJSON.msg.includes("all passed")); + assert.ok(statusJSON.msg?.includes("all passed")); await cleanup(); }, 120_000); @@ -1057,14 +1050,13 @@ export const test = { } `, { - sourceName: "component", witPath: FIXTURES_WIT_DIR, worldName: "browser-fs-write", - }, + } as ComponentizeOptions, ); // Transpile the component, write all output files to a temporary directory - const { files } = await transpile(component, { + const { files }: TranspileOutput = await transpile(component, { async: true, name: "component", optimize: false, diff --git a/packages/preview2-shim/test/common.js b/packages/preview2-shim/test/common.ts similarity index 52% rename from packages/preview2-shim/test/common.js rename to packages/preview2-shim/test/common.ts index b19165c62..6374e107d 100644 --- a/packages/preview2-shim/test/common.js +++ b/packages/preview2-shim/test/common.ts @@ -6,7 +6,7 @@ import { sep, normalize, resolve, extname } from "node:path"; import { createServer as createHTTPServer } from "node:http"; import ts from "typescript"; -import puppeteer from "puppeteer"; +import puppeteer, { Browser } from "puppeteer"; import mime from "mime"; import { vi, assert } from "vitest"; @@ -26,14 +26,14 @@ export const FIXTURES_WIT_DIR = fileURLToPath(new URL("./fixtures/wit", import.m * * The new directory is created using `fsPromises.mkdtemp()`. * - * @return {Promise} A Promise that resolves to the created temporary directory + * @return A Promise that resolves to the created temporary directory */ -export async function getTmpDir() { +export async function getTmpDir(): Promise { return await mkdtemp(normalize(tmpdir() + sep)); } /** Check if a path is an existing directory */ -export async function isExistingDir(p) { +export async function isExistingDir(p: string): Promise { if (!p || typeof p !== "string") { throw new Error(`invalid path [${p}]`); } @@ -42,17 +42,33 @@ export async function isExistingDir(p) { .catch(() => false); } +interface StartTestServerArgs { + transpiledOutputDir: string; + htmlDir?: string; + tmpDir?: string; + outDir?: string; + debug?: boolean; +} + +interface StartTestServerResult { + server: ReturnType; + port: number; + baseURL: string; + browser: Browser; + cleanup: () => Promise; +} + /** * Start a server that can be used for testing * - * @param {object} args - * @param {string} args.transpiledOutputDir - Directory that contains a transpiled component to be loaded into the browser - * @param {string} [args.htmlDir] - Directory that contains HTML that should be served by the server in all other cases (by deafult this uses the default harness index.html) - * @param {string} [args.tmpDir] - Directory in which to do work (e.g. a dir in /tmp) - * @param {string} [args.outDir] - Directory in which to place build outputs (normally inside scratch dir) - * @param {boolean} [args.debug] - Directory in which to place build outputs (normally inside scratch dir) + * @param args - Configuration options + * @param args.transpiledOutputDir - Directory that contains a transpiled component to be loaded into the browser + * @param args.htmlDir - Directory that contains HTML that should be served by the server in all other cases (by default this uses the default harness index.html) + * @param args.tmpDir - Directory in which to do work (e.g. a dir in /tmp) + * @param args.outDir - Directory in which to place build outputs (normally inside scratch dir) + * @param args.debug - Directory in which to place build outputs (normally inside scratch dir) */ -export async function startTestServer(args) { +export async function startTestServer(args: StartTestServerArgs): Promise { const debug = args?.debug; if (!args.transpiledOutputDir) { @@ -90,111 +106,117 @@ export async function startTestServer(args) { // ); // Create a local HTTP server that will serve the files in the directory - const { server } = await new Promise((resolve) => { - // Pre-compute the outDir as a URL - const outDirURL = pathToFileURL(transpiledOutputDir + "/"); - const p2ShimDirURL = pathToFileURL(P2_SHIM_CODE_DIR); - const htmlDirURL = pathToFileURL(htmlDir + "/"); - - const newServer = createHTTPServer(async (req, res) => { - // Handle CORS preflight for all endpoints - if (req.method === "OPTIONS") { - res.writeHead(204, { - "access-control-allow-origin": "*", - "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", - "access-control-allow-headers": req.headers["access-control-request-headers"] || "*", - "access-control-expose-headers": "*", - "access-control-max-age": "86400", - }); - res.end(); - return; - } - - if (req.url === "/api/test-echo") { - res.writeHead(200, { - "content-type": "application/json", - "x-test-header": "test-value", - "access-control-allow-origin": "*", - }); - res.end(JSON.stringify({ message: "hello from test server" })); - return; - } - - // Wasmtime-compatible echo endpoint: echoes method, URI, and body - if ( - req.url.startsWith("/echo/") || - ["/get", "/post", "/put"].includes(req.url.split("?")[0]) - ) { - const chunks = []; - for await (const chunk of req) { - chunks.push(chunk); + const { server } = await new Promise<{ server: ReturnType }>( + (resolve) => { + // Pre-compute the outDir as a URL + const outDirURL = pathToFileURL(transpiledOutputDir + "/"); + const p2ShimDirURL = pathToFileURL(P2_SHIM_CODE_DIR); + const htmlDirURL = pathToFileURL(htmlDir + "/"); + + const newServer = createHTTPServer(async (req, res) => { + // Handle CORS preflight for all endpoints + if (req.method === "OPTIONS") { + res.writeHead(204, { + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", + "access-control-allow-headers": req.headers["access-control-request-headers"] || "*", + "access-control-expose-headers": "*", + "access-control-max-age": "86400", + }); + res.end(); + return; } - const body = Buffer.concat(chunks); - res.writeHead(200, { - "content-type": "application/octet-stream", - "x-wasmtime-test-method": req.method, - "x-wasmtime-test-uri": req.url, - "access-control-allow-origin": "*", - "access-control-expose-headers": "x-wasmtime-test-method, x-wasmtime-test-uri", - }); - res.end(body); - return; - } - let fileURL; - try { - if (req.url.startsWith("/transpiled/")) { - // Strip prefix and load file from the transpiled output dir - const rest = req.url.slice("/transpiled/".length); - fileURL = new URL(`./${rest}`, outDirURL); - } else if (req.url.startsWith("/preview2-shim/")) { - const rest = req.url.slice("/preview2-shim/".length); - // Strip prefix and load file from the symlinked preview2-shim dir - fileURL = new URL(`./${rest}`, p2ShimDirURL); - } else if (req.url === "/") { - fileURL = new URL(`./index.html`, htmlDirURL); - } else { - // Read all other files from the HTML directory - fileURL = new URL(`.${req.url}`, htmlDirURL); + if (req.url === "/api/test-echo") { + res.writeHead(200, { + "content-type": "application/json", + "x-test-header": "test-value", + "access-control-allow-origin": "*", + }); + res.end(JSON.stringify({ message: "hello from test server" })); + return; } - if (debug) { - console.error("[server] serving file", fileURLToPath(fileURL)); + // Wasmtime-compatible echo endpoint: echoes method, URI, and body + if ( + req.url?.startsWith("/echo/") || + ["/get", "/post", "/put"].includes(req.url?.split("?")[0] || "") + ) { + const chunks: Uint8Array[] = []; + for await (const chunk of req) { + chunks.push(chunk); + } + const body = Buffer.concat(chunks); + res.writeHead(200, { + "content-type": "application/octet-stream", + "x-wasmtime-test-method": req.method, + "x-wasmtime-test-uri": req.url, + "access-control-allow-origin": "*", + "access-control-expose-headers": "x-wasmtime-test-method, x-wasmtime-test-uri", + }); + res.end(body); + return; } - const html = await readFile(fileURLToPath(fileURL)); - res.writeHead(200, { - "content-type": mime.getType(extname(req.url)), - }); - res.end(html); - } catch (e) { - if (e.code === "ENOENT") { - if (debug) { - console.error( - `[server] ERROR: no such file [${req.url}] (fileURL ? [${fileURL}]): ${e}`, - ); + let fileURL; + try { + if (req.url?.startsWith("/transpiled/")) { + // Strip prefix and load file from the transpiled output dir + const rest = req.url.slice("/transpiled/".length); + fileURL = new URL(`./${rest}`, outDirURL); + } else if (req.url?.startsWith("/preview2-shim/")) { + const rest = req.url.slice("/preview2-shim/".length); + // Strip prefix and load file from the symlinked preview2-shim dir + fileURL = new URL(`./${rest}`, p2ShimDirURL); + } else if (req.url === "/") { + fileURL = new URL(`./index.html`, htmlDirURL); + } else { + // Read all other files from the HTML directory + fileURL = new URL(`.${req.url}`, htmlDirURL); } - res.writeHead(404); - res.end(e.message); - } else { + if (debug) { - console.error( - `[server] ERROR: failed to serve URL [${req.url}] (fileURL ? [${fileURL}]): ${e}`, - ); + console.error("[server] serving file", fileURLToPath(fileURL)); + } + + const html = await readFile(fileURLToPath(fileURL)); + res.writeHead(200, { + "content-type": mime.getType(extname(req.url || "")) || "application/octet-stream", + }); + res.end(html); + } catch (e: any) { + if (e.code === "ENOENT") { + if (debug) { + console.error( + `[server] ERROR: no such file [${req.url}] (fileURL ? [${fileURL}]): ${e}`, + ); + } + res.writeHead(404); + res.end(e.message); + } else { + if (debug) { + console.error( + `[server] ERROR: failed to serve URL [${req.url}] (fileURL ? [${fileURL}]): ${e}`, + ); + } + res.writeHead(500); + res.end(e.message); } - res.writeHead(500); - res.end(e.message); } - } - }); + }); - newServer.listen(0, "localhost", () => { - resolve({ server: newServer }); - }); - }); + newServer.listen(0, "localhost", () => { + resolve({ server: newServer }); + }); + }, + ); - const { family, address, port } = server.address(); - const baseURL = `http://${family === "IPv6" ? "[" + address + "]" : address}:${port}`; + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("Unexpected server address format"); + } + const { family, address: addr, port } = address; + const baseURL = `http://${family === "IPv6" ? "[" + addr + "]" : addr}:${port}`; // Wait until the server is serving the page await vi.waitUntil( @@ -233,16 +255,29 @@ export async function startTestServer(args) { baseURL, browser, cleanup: async () => { - await new Promise((resolve) => server.close(resolve)); + await new Promise((resolve) => server.close(() => resolve())); }, }; } +interface RunBasicHarnessPageTestArgs { + browser: Browser; + url: string; + debug?: boolean; +} + +interface StatusJSON { + status: string; + msg?: string; +} + /** - * Run thet test for a basic harness, by loading a given URL + * Run the test for a basic harness, by loading a given URL * and evaluating the body to watch completion */ -export async function runBasicHarnessPageTest(args) { +export async function runBasicHarnessPageTest( + args: RunBasicHarnessPageTestArgs, +): Promise<{ statusJSON: StatusJSON }> { const { browser, url, debug } = args; const page = await browser.newPage(); if (debug) { @@ -250,14 +285,14 @@ export async function runBasicHarnessPageTest(args) { } let nav = await page.goto(url); - assert.ok(nav.ok()); + assert.ok(nav?.ok()); const body = await page.locator("body").waitHandle(); - let statusJSON = { status: "init" }; + let statusJSON: StatusJSON = { status: "init" }; // Continuously wait for the body tag to contain a JSON object that // conveys status of the test - let bodyHTML; + let bodyHTML: string | undefined; try { await vi.waitUntil( async () => { @@ -294,7 +329,18 @@ export async function runBasicHarnessPageTest(args) { return { statusJSON }; } -export function tsCodegen(args) { +interface TsCodegenArgs { + cwd?: string; + tsConfigPath: string; + configOverrides?: { + include?: string[]; + compilerOptions?: { + outDir?: string; + }; + }; +} + +export function tsCodegen(args: TsCodegenArgs) { if (!args) { throw new Error("missing ts codegen args"); } diff --git a/packages/preview2-shim/test/fixtures/types/iface-namespaces-at-runtime/example.ts b/packages/preview2-shim/test/fixtures/types/iface-namespaces-at-runtime/example.ts index 3f80f6c73..4e8d91af5 100644 --- a/packages/preview2-shim/test/fixtures/types/iface-namespaces-at-runtime/example.ts +++ b/packages/preview2-shim/test/fixtures/types/iface-namespaces-at-runtime/example.ts @@ -1,9 +1,9 @@ // eslint-disable no-unused-vars -// Ensures the `io` interface namespaces are usable as runtime values and not +// Ensures the `io` namespaces are usable as runtime values and not // just as types, so `tsc` (the `types:check` script) catches a regression to // type-only re-exports. -import { error, poll, streams } from "../../../../types/io.js"; +import { error, poll, streams } from "../../../../src/browser/io.js"; const _error: typeof error = error; const _poll: typeof poll = poll; diff --git a/packages/preview2-shim/test/fixtures/types/iface-namespaces-at-runtime/tsconfig.json b/packages/preview2-shim/test/fixtures/types/iface-namespaces-at-runtime/tsconfig.test.json similarity index 100% rename from packages/preview2-shim/test/fixtures/types/iface-namespaces-at-runtime/tsconfig.json rename to packages/preview2-shim/test/fixtures/types/iface-namespaces-at-runtime/tsconfig.test.json diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.ts similarity index 78% rename from packages/preview2-shim/test/test.js rename to packages/preview2-shim/test/test.ts index fb90e4d4e..eba46e632 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.ts @@ -3,12 +3,17 @@ import { throws } from "node:assert"; import { fileURLToPath } from "node:url"; import { suite, test, assert, beforeEach, afterEach } from "vitest"; +import { + IpAddress, + IpSocketAddress, + Ipv4Address, +} from "../types/interfaces/wasi-sockets-network.js"; const symbolDispose = Symbol.dispose || Symbol.for("dispose"); suite("Node.js Preview2", () => { test("Stdio", async () => { - const { cli } = await import("@bytecodealliance/preview2-shim"); + const { cli } = await import("../src/nodejs/index.js"); cli.stdout.getStdout().blockingWriteAndFlush(new TextEncoder().encode("test stdout")); cli.stderr.getStderr().blockingWriteAndFlush(new TextEncoder().encode("test stderr")); }); @@ -17,7 +22,7 @@ suite("Node.js Preview2", () => { test("Wall clock", async () => { const { clocks: { wallClock }, - } = await import("@bytecodealliance/preview2-shim"); + } = await import("../src/nodejs/index.js"); { const { seconds, nanoseconds } = wallClock.now(); @@ -35,7 +40,7 @@ suite("Node.js Preview2", () => { test("Monotonic clock now", async () => { const { clocks: { monotonicClock }, - } = await import("@bytecodealliance/preview2-shim"); + } = await import("../src/nodejs/index.js"); assert.strictEqual(typeof monotonicClock.resolution(), "bigint"); const curNow = monotonicClock.now(); @@ -46,7 +51,7 @@ suite("Node.js Preview2", () => { test("Monotonic clock immediately resolved polls", async () => { const { clocks: { monotonicClock }, - } = await import("@bytecodealliance/preview2-shim"); + } = await import("../src/nodejs/index.js"); const curNow = monotonicClock.now(); { const poll = monotonicClock.subscribeInstant(curNow - 10n); @@ -61,11 +66,11 @@ suite("Node.js Preview2", () => { test("Monotonic clock subscribe duration", async () => { const { clocks: { monotonicClock }, - } = await import("@bytecodealliance/preview2-shim"); + } = await import("../src/nodejs/index.js"); const curNow = monotonicClock.now(); - const poll = monotonicClock.subscribeDuration(10e6); + const poll = monotonicClock.subscribeDuration(BigInt(10e6)); poll.block(); // verify we are at the right time, and within 1ms of the original now @@ -79,7 +84,7 @@ suite("Node.js Preview2", () => { test("Monotonic clock subscribe instant", async () => { const { clocks: { monotonicClock }, - } = await import("@bytecodealliance/preview2-shim"); + } = await import("../src/nodejs/index.js"); const curNow = monotonicClock.now(); @@ -96,17 +101,17 @@ suite("Node.js Preview2", () => { }); test("FS read", async () => { - let toDispose = []; + let toDispose: any[] = []; await (async () => { - const { filesystem } = await import("@bytecodealliance/preview2-shim"); + const { filesystem } = await import("../src/nodejs/index.js"); const [[rootDescriptor]] = filesystem.preopens.getDirectories(); const childDescriptor = rootDescriptor.openAt( {}, fileURLToPath(import.meta.url).slice(1), {}, - {}, + { read: true }, ); - const stream = childDescriptor.readViaStream(0); + const stream = childDescriptor.readViaStream(0n); const poll = stream.subscribe(); poll.block(); let buf = stream.read(10000n); @@ -120,6 +125,7 @@ suite("Node.js Preview2", () => { })(); // Force the Poll to GC so the next dispose doesn't trap + // @ts-expect-error gc is defined when running with --expose-gc gc(); await new Promise((resolve) => setTimeout(resolve, 200)); @@ -129,7 +135,7 @@ suite("Node.js Preview2", () => { }); test("Fields.set on a fresh Fields", async () => { - const { http } = await import("@bytecodealliance/preview2-shim"); + const { http } = await import("../src/nodejs/index.js"); const { Fields } = http.types; const encoder = new TextEncoder(); @@ -144,7 +150,7 @@ suite("Node.js Preview2", () => { }); test("Fields.set replaces existing values", async () => { - const { http } = await import("@bytecodealliance/preview2-shim"); + const { http } = await import("../src/nodejs/index.js"); const { Fields } = http.types; const encoder = new TextEncoder(); @@ -161,7 +167,7 @@ suite("Node.js Preview2", () => { // `this.#table.set(lowercased, [])` for new keys. Calling set with an // empty values list exercises that branch without pushing any entry, // so this locks in the resulting state. - const { http } = await import("@bytecodealliance/preview2-shim"); + const { http } = await import("../src/nodejs/index.js"); const { Fields } = http.types; const fields = Fields.fromList([]); @@ -173,7 +179,7 @@ suite("Node.js Preview2", () => { }); test("Fields.set throws immutable when fields are locked", async () => { - const { http } = await import("@bytecodealliance/preview2-shim"); + const { http } = await import("../src/nodejs/index.js"); const { Fields, OutgoingRequest } = http.types; const encoder = new TextEncoder(); @@ -183,12 +189,12 @@ suite("Node.js Preview2", () => { throws( () => hdrs.set("content-type", [encoder.encode("text/plain")]), - (err) => err?.tag === "immutable", + (err: any) => err?.tag === "immutable", ); }); test("Fields.set throws invalid-syntax for an invalid name", async () => { - const { http } = await import("@bytecodealliance/preview2-shim"); + const { http } = await import("../src/nodejs/index.js"); const { Fields } = http.types; const encoder = new TextEncoder(); @@ -196,12 +202,12 @@ suite("Node.js Preview2", () => { throws( // names with spaces are rejected by node:http's validateHeaderName () => fields.set("bad header", [encoder.encode("ok value")]), - (err) => err?.tag === "invalid-syntax", + (err: any) => err?.tag === "invalid-syntax", ); }); test("Fields.set throws invalid-syntax for an invalid value", async () => { - const { http } = await import("@bytecodealliance/preview2-shim"); + const { http } = await import("../src/nodejs/index.js"); const { Fields } = http.types; const encoder = new TextEncoder(); @@ -209,12 +215,12 @@ suite("Node.js Preview2", () => { throws( // values containing CR/LF are rejected by node:http's validateHeaderValue () => fields.set("x-foo", [encoder.encode("bad\nvalue")]), - (err) => err?.tag === "invalid-syntax", + (err: any) => err?.tag === "invalid-syntax", ); }); test("Fields.set throws forbidden for restricted header names", async () => { - const { http } = await import("@bytecodealliance/preview2-shim"); + const { http } = await import("../src/nodejs/index.js"); const { Fields } = http.types; const encoder = new TextEncoder(); @@ -224,7 +230,7 @@ suite("Node.js Preview2", () => { const fields = Fields.fromList([]); throws( () => fields.set(name, [encoder.encode("x")]), - (err) => err?.tag === "forbidden", + (err: any) => err?.tag === "forbidden", `expected forbidden for ${name}`, ); } @@ -233,12 +239,12 @@ suite("Node.js Preview2", () => { test( "WASI HTTP", testWithGCWrap(async () => { - const { http } = await import("@bytecodealliance/preview2-shim"); + const { http } = await import("../src/nodejs/index.js"); const { handle } = http.outgoingHandler; const { OutgoingRequest, OutgoingBody, Fields } = http.types; const encoder = new TextEncoder(); const request = new OutgoingRequest( - new Fields([ + Fields.fromList([ ["User-agent", encoder.encode("WASI-HTTP/0.0.1")], ["Content-type", encoder.encode("application/json")], ]), @@ -248,24 +254,32 @@ suite("Node.js Preview2", () => { request.setScheme({ tag: "HTTPS" }); const outgoingBody = request.body(); - OutgoingBody.finish(outgoingBody); + OutgoingBody.finish(outgoingBody, undefined); - const futureIncomingResponse = handle(request); + const futureIncomingResponse = handle(request, undefined); futureIncomingResponse.subscribe().block(); - const incomingResponseResult = futureIncomingResponse.get().val; + const incomingResponseResult = futureIncomingResponse.get()?.val; + + if (!incomingResponseResult) { + throw new Error("Request failed: no response"); + } if (incomingResponseResult.tag !== "ok") { throw incomingResponseResult.val; } - const status = incomingResponseResult.val.status(); - const responseHeaders = incomingResponseResult.val.headers().entries(); + const incomingResponse = incomingResponseResult.val; + + const status = incomingResponse.status(); + const responseHeaders = incomingResponse.headers().entries(); const decoder = new TextDecoder(); - const headers = Object.fromEntries(responseHeaders.map(([k, v]) => [k, decoder.decode(v)])); + const headers = Object.fromEntries( + responseHeaders.map(([k, v]: [string, Uint8Array]) => [k, decoder.decode(v)]), + ); let responseBody; - const incomingBody = incomingResponseResult.val.consume(); + const incomingBody = incomingResponse.consume(); { const bodyStream = incomingBody.stream(); bodyStream.subscribe().block(); @@ -273,11 +287,11 @@ suite("Node.js Preview2", () => { while (buf.byteLength === 0) { try { buf = bodyStream.read(5000n); - } catch (e) { - if (e.tag === "closed") { + } catch (e: any) { + if (e?.tag === "closed") { break; } - throw e.val || e; + throw e?.val || e; } } responseBody = new TextDecoder().decode(buf); @@ -291,29 +305,29 @@ suite("Node.js Preview2", () => { suite("WASI Sockets (TCP)", async () => { test("sockets.instanceNetwork() should be a singleton", async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); + const { sockets } = await import("../src/nodejs/index.js"); const network1 = sockets.instanceNetwork.instanceNetwork(); const network2 = sockets.instanceNetwork.instanceNetwork(); assert.strictEqual(network1, network2); }); test("sockets.tcpCreateSocket() should throw not-supported", async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); + const { sockets } = await import("../src/nodejs/index.js"); const socket = sockets.tcpCreateSocket.createTcpSocket("ipv4"); assert.notEqual(socket, null); throws( () => { - sockets.tcpCreateSocket.createTcpSocket("abc"); + sockets.tcpCreateSocket.createTcpSocket("abc" as any); }, - (err) => err === "not-supported", + (err: any) => err === "not-supported", ); }); test("tcp.bind(): should bind to a valid ipv4 address", async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); + const { sockets } = await import("../src/nodejs/index.js"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket("ipv4"); - const localAddress = { + const localAddress: IpSocketAddress = { tag: "ipv4", val: { address: [0, 0, 0, 0], @@ -334,14 +348,16 @@ suite("Node.js Preview2", () => { }); test("tcp.bind(): should bind to a valid ipv6 address and port=0", async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); + const { sockets } = await import("../src/nodejs/index.js"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket("ipv6"); - const localAddress = { + const localAddress: IpSocketAddress = { tag: "ipv6", val: { address: [0, 0, 0, 0, 0, 0, 0, 0], port: 0, + flowInfo: 0, + scopeId: 0, }, }; tcpSocket.startBind(network, localAddress); @@ -365,16 +381,18 @@ suite("Node.js Preview2", () => { }); test("tcp.bind(): should throw invalid-argument when invalid address family", async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); + const { sockets } = await import("../src/nodejs/index.js"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket("ipv4"); - const localAddress = { + const localAddress: IpSocketAddress = { // invalid address family tag: "ipv6", val: { address: [0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0001], port: 0, + flowInfo: 0, + scopeId: 0, }, }; throws( @@ -386,11 +404,11 @@ suite("Node.js Preview2", () => { }); test("tcp.bind(): should throw invalid-state when already bound", async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); + const { sockets } = await import("../src/nodejs/index.js"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket("ipv4"); - const localAddress = { + const localAddress: IpSocketAddress = { tag: "ipv4", val: { address: [0, 0, 0, 0], @@ -409,10 +427,10 @@ suite("Node.js Preview2", () => { }); test("tcp.listen(): should listen to an ipv4 address", async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); + const { sockets } = await import("../src/nodejs/index.js"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket("ipv4"); - const localAddress = { + const localAddress: IpSocketAddress = { tag: "ipv4", val: { address: [0, 0, 0, 0], @@ -433,20 +451,21 @@ suite("Node.js Preview2", () => { { retry: env.CI ? 3 : 0 }, testWithGCWrap(async () => { const { lookup } = await import("node:dns"); - const { sockets } = await import("@bytecodealliance/preview2-shim"); + const { sockets } = await import("../src/nodejs/index.js"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket("ipv4"); const pollable = tcpSocket.subscribe(); - const googleIp = await new Promise((resolve, reject) => + const googleIp = await new Promise((resolve, reject) => lookup("google.com", (err, result) => (err ? reject(err) : resolve(result))), ); + const ipParts = googleIp.split(".").map(Number) as Ipv4Address; tcpSocket.startConnect(network, { tag: "ipv4", val: { - address: googleIp.split("."), + address: ipParts, port: 80, }, }); @@ -471,11 +490,11 @@ suite("Node.js Preview2", () => { while (buf.byteLength === 0) { try { buf = input.read(5000n); - } catch (e) { - if (e.tag === "closed") { + } catch (e: any) { + if (e?.tag === "closed") { break; } - throw e.val || e; + throw e?.val || e; } } const responseBody = new TextDecoder().decode(buf); @@ -490,20 +509,20 @@ suite("Node.js Preview2", () => { }); suite("WASI Sockets (UDP)", async () => { - test("sockets.udpCreateSocket() should be a singleton", async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); + test("sockets.udpCreateSocket() should create sockets", async () => { + const { sockets } = await import("../src/nodejs/index.js"); const socket1 = sockets.udpCreateSocket.createUdpSocket("ipv4"); - assert.notEqual(socket1.id, 1); + assert.notEqual(socket1, null); const socket2 = sockets.udpCreateSocket.createUdpSocket("ipv4"); - assert.notEqual(socket2.id, 1); + assert.notEqual(socket2, null); }); // TODO: figure out how to mock handle.on("message", ...) test("udp.bind(): should bind to a valid ipv4 address and port=0", async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); + const { sockets } = await import("../src/nodejs/index.js"); const network = sockets.instanceNetwork.instanceNetwork(); const socket = sockets.udpCreateSocket.createUdpSocket("ipv4"); - const localAddress = { + const localAddress: IpSocketAddress = { tag: "ipv4", val: { address: [0, 0, 0, 0], @@ -523,14 +542,25 @@ suite("Node.js Preview2", () => { }); test("udp.bind(): should bind to a valid ipv6 address and port=0", async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); + const { sockets } = await import("../src/nodejs/index.js"); const network = sockets.instanceNetwork.instanceNetwork(); const socket = sockets.udpCreateSocket.createUdpSocket("ipv6"); const localAddress = { - tag: "ipv6", + tag: "ipv6" as const, val: { - address: [0, 0, 0, 0, 0, 0, 0, 0], + address: [0, 0, 0, 0, 0, 0, 0, 0] as [ + number, + number, + number, + number, + number, + number, + number, + number, + ], port: 0, + flowInfo: 0, + scopeId: 0, }, }; socket.startBind(network, localAddress); @@ -545,17 +575,17 @@ suite("Node.js Preview2", () => { }); test("udp.stream(): should connect to a valid ipv4 address", async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); + const { sockets } = await import("../src/nodejs/index.js"); const network = sockets.instanceNetwork.instanceNetwork(); const socket = sockets.udpCreateSocket.createUdpSocket("ipv4"); - const localAddress = { + const localAddress: IpSocketAddress = { tag: "ipv4", val: { address: [0, 0, 0, 0], port: 0, }, }; - const remoteAddress = { + const remoteAddress: IpSocketAddress = { tag: "ipv4", val: { address: [192, 168, 0, 1], @@ -565,8 +595,9 @@ suite("Node.js Preview2", () => { socket.startBind(network, localAddress); socket.finishBind(); - socket.stream(remoteAddress); - + const [incomingDatagrams, outgoingDatagrams] = socket.stream(remoteAddress); + assert.ok(incomingDatagrams); + assert.ok(outgoingDatagrams); assert.strictEqual(socket.addressFamily(), "ipv4"); const boundAddress = socket.localAddress(); @@ -579,21 +610,24 @@ suite("Node.js Preview2", () => { test( "udp.stream(): should connect to a valid ipv6 address", testWithGCWrap(async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); + const { sockets } = await import("../src/nodejs/index.js"); const network = sockets.instanceNetwork.instanceNetwork(); const socket = sockets.udpCreateSocket.createUdpSocket("ipv6"); - const localAddress = { + const localAddress: IpSocketAddress = { tag: "ipv6", val: { address: [0, 0, 0, 0, 0, 0, 0, 0], port: 1337, + flowInfo: 0, + scopeId: 0, }, }; socket.startBind(network, localAddress); socket.finishBind(); - socket.stream(); - + const [incomingDatagrams, outgoingDatagrams] = socket.stream(undefined); + assert.ok(incomingDatagrams); + assert.ok(outgoingDatagrams); assert.strictEqual(socket.addressFamily(), "ipv6"); const boundAddress = socket.localAddress(); @@ -607,7 +641,7 @@ suite("Node.js Preview2", () => { test( "ipNameLookup.resolveAddresses(): should return valid IP addresses", testWithGCWrap(async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); + const { sockets } = await import("../src/nodejs/index.js"); const network = sockets.instanceNetwork.instanceNetwork(); const stream = sockets.ipNameLookup.resolveAddresses(network, "localhost"); @@ -615,20 +649,24 @@ suite("Node.js Preview2", () => { const poll = stream.subscribe(); poll.block(); - const addressGroup = stream.resolveNextAddress(); - - assert.ok(addressGroup != null, "should resolve to at least one address"); + const addresses = stream.resolveNextAddress(); + if (!addresses) { + throw new Error("should resolve to at least one address"); + } + const addressGroup: IpAddress[] = Array.isArray(addresses) ? addresses : [addresses]; assert.ok(addressGroup.length, "should be an address group"); + const firstAddress = addressGroup[0]; assert.ok( - addressGroup[0].tag === "ipv4" || addressGroup[0].tag === "ipv6", + firstAddress.tag === "ipv4" || firstAddress.tag === "ipv6", "should be an IP address variant", ); - assert.ok(Array.isArray(addressGroup[0].val), "address payload should be a tuple"); - if (addressGroup[0].tag === "ipv4") { - assert.strictEqual(addressGroup[0].val.length, 4); + if (firstAddress.tag === "ipv4") { + assert.ok(Array.isArray(firstAddress.val), "ipv4 address should be a tuple"); + assert.strictEqual(firstAddress.val.length, 4); } else { - assert.strictEqual(addressGroup[0].val.length, 8); + assert.ok(Array.isArray(firstAddress.val), "ipv6 address should be a tuple"); + assert.strictEqual(firstAddress.val.length, 8); } poll[symbolDispose](); @@ -642,7 +680,13 @@ suite("HTTPServer", () => { test( "HTTPServer: can retrieve randomized server address", testWithGCWrap(async () => { - const { HTTPServer } = await import("@bytecodealliance/preview2-shim/http"); + // HTTPServer is a Node.js-only feature + const httpModule = await import("../src/nodejs/http.js"); + const HTTPServer = (httpModule as any).HTTPServer; + if (!HTTPServer) { + console.log("HTTPServer not available in this environment, skipping test"); + return; + } const server = new HTTPServer({ handle() { throw new Error("never called"); @@ -660,8 +704,8 @@ suite("HTTPServer", () => { suite("Instantiation", () => { test("WASIShim export (random)", async () => { - const { random } = await import("@bytecodealliance/preview2-shim"); - const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); + const { random } = await import("../src/nodejs/index.js"); + const { WASIShim } = await import("../src/common/instantiation.js"); const shim = new WASIShim(); assert.ok(shim); assert.deepStrictEqual( @@ -679,8 +723,8 @@ suite("Instantiation", () => { }); test("WASIShim export override", async () => { - const { random } = await import("@bytecodealliance/preview2-shim"); - const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); + const { random } = await import("../src/nodejs/index.js"); + const { WASIShim } = await import("../src/common/instantiation.js"); const invalidWASIShim = { random: { random: { @@ -704,26 +748,26 @@ suite("Instantiation", () => { }); suite("Sandboxing", () => { - let originalEnv; - let originalArgs; + let originalEnv: [string, string][]; + let originalArgs: string[]; beforeEach(async () => { - const { cli } = await import("@bytecodealliance/preview2-shim"); + const { cli } = await import("../src/nodejs/index.js"); // Save original state - originalEnv = cli.environment.getEnvironment(); + originalEnv = cli.environment.getEnvironment() as [string, string][]; originalArgs = cli.environment.getArguments(); }); afterEach(async () => { - const { cli, filesystem } = await import("@bytecodealliance/preview2-shim"); + const { cli, filesystem } = await import("../src/nodejs/index.js"); // Restore default state - filesystem._setPreopens({ "/": "/" }); + (filesystem as any)._setPreopens({ "/": "/" }); cli._setEnv(Object.fromEntries(originalEnv)); cli._setArgs(originalArgs); }); test("_clearPreopens removes filesystem access", async () => { - const { filesystem } = await import("@bytecodealliance/preview2-shim"); + const { filesystem } = await import("../src/nodejs/index.js"); const initialPreopens = filesystem.preopens.getDirectories(); assert.ok(initialPreopens.length > 0, "Should have default preopens"); @@ -734,9 +778,9 @@ suite("Sandboxing", () => { }); test("_setPreopens replaces preopens", async () => { - const { filesystem } = await import("@bytecodealliance/preview2-shim"); + const { filesystem } = await import("../src/nodejs/index.js"); - filesystem._setPreopens({ + (filesystem as any)._setPreopens({ "/custom": "/tmp", }); @@ -746,18 +790,18 @@ suite("Sandboxing", () => { }); test("_getPreopens returns current preopens", async () => { - const { filesystem } = await import("@bytecodealliance/preview2-shim"); + const { filesystem } = await import("../src/nodejs/index.js"); const preopens = filesystem._getPreopens(); assert.ok(Array.isArray(preopens), "Should return an array"); // The returned array should be a copy - preopens.push(["fake", "/fake"]); + preopens.push([{} as any, "/fake"]); const preopensAfter = filesystem._getPreopens(); assert.notStrictEqual(preopens.length, preopensAfter.length, "Should return a copy"); }); test("WASIShim with empty preopens provides no filesystem access", async () => { - const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); + const { WASIShim } = await import("../src/common/instantiation.js"); const sandboxedShim = new WASIShim({ sandbox: { @@ -771,7 +815,7 @@ suite("Sandboxing", () => { }); test("WASIShim with custom env", async () => { - const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); + const { WASIShim } = await import("../src/common/instantiation.js"); const customShim = new WASIShim({ sandbox: { @@ -788,7 +832,7 @@ suite("Sandboxing", () => { }); test("WASIShim with custom args", async () => { - const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); + const { WASIShim } = await import("../src/common/instantiation.js"); const customShim = new WASIShim({ sandbox: { @@ -802,7 +846,7 @@ suite("Sandboxing", () => { }); test("WASIShim with enableNetwork=false denies network", async () => { - const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); + const { WASIShim } = await import("../src/common/instantiation.js"); const noNetworkShim = new WASIShim({ sandbox: { @@ -826,13 +870,16 @@ suite("Sandboxing", () => { // Bind should throw access-denied assert.throws(() => { - udpSocket.startBind(network, { tag: "ipv4", val: { address: [127, 0, 0, 1], port: 0 } }); + udpSocket.startBind(network, { + tag: "ipv4", + val: { address: [127, 0, 0, 1], port: 0 }, + }); }, /access-denied/); }); test("WASIShim with enableNetwork=true (default) allows network", async () => { - const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); - const { sockets } = await import("@bytecodealliance/preview2-shim"); + const { WASIShim } = await import("../src/common/instantiation.js"); + const { sockets } = await import("../src/nodejs/index.js"); const defaultShim = new WASIShim(); @@ -850,7 +897,7 @@ suite("Sandboxing", () => { }); test("Fully sandboxed WASIShim", async () => { - const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); + const { WASIShim } = await import("../src/common/instantiation.js"); const sandboxed = new WASIShim({ sandbox: { @@ -885,7 +932,10 @@ suite("Sandboxing", () => { const network = importObj["wasi:sockets/instance-network"].instanceNetwork(); assert.throws( () => { - tcpSocket.startBind(network, { tag: "ipv4", val: { address: [127, 0, 0, 1], port: 0 } }); + tcpSocket.startBind(network, { + tag: "ipv4", + val: { address: [127, 0, 0, 1], port: 0 }, + }); }, /access-denied/, "No network access", @@ -893,7 +943,7 @@ suite("Sandboxing", () => { }); test("Multiple WASIShim instances have isolated preopens", async () => { - const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); + const { WASIShim } = await import("../src/common/instantiation.js"); // Create two shims with different preopens const shim1 = new WASIShim({ @@ -923,7 +973,7 @@ suite("Sandboxing", () => { }); test("Multiple WASIShim instances have isolated env and args", async () => { - const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); + const { WASIShim } = await import("../src/common/instantiation.js"); const shim1 = new WASIShim({ sandbox: { @@ -955,12 +1005,12 @@ suite("Sandboxing", () => { test( "WASIShim isolated preopens can read files", testWithGCWrap(async () => { - const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); + const { WASIShim } = await import("../src/common/instantiation.js"); const { dirname } = await import("node:path"); const testFilePath = fileURLToPath(import.meta.url); const testDir = dirname(testFilePath); - const testFileName = "test.js"; + const testFileName = "test.ts"; // Create a shim with preopens pointing to the test directory const shim = new WASIShim({ @@ -981,7 +1031,7 @@ suite("Sandboxing", () => { // Open and read the test file const childDescriptor = rootDescriptor.openAt({}, testFileName, {}, { read: true }); - const stream = childDescriptor.readViaStream(0); + const stream = childDescriptor.readViaStream(0n); const poll = stream.subscribe(); poll.block(); let buf = stream.read(10000n); @@ -1003,7 +1053,7 @@ suite("Sandboxing", () => { test( "WASIShim isolated preopens don't access paths outside preopen", testWithGCWrap(async () => { - const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); + const { WASIShim } = await import("../src/common/instantiation.js"); const { dirname } = await import("node:path"); const testFilePath = fileURLToPath(import.meta.url); @@ -1032,39 +1082,39 @@ suite("Sandboxing", () => { }); suite("Browser shim guards", () => { test("pollList throws on empty list", async () => { - const { poll } = await import("../lib/browser/io.js"); + const { poll } = await import("../src/browser/io.js"); assert.throws(() => poll.poll([]), /empty/); }); test("pollList throws on list exceeding u32 range", async () => { - const { poll } = await import("../lib/browser/io.js"); - const fakeList = { length: 0x100000000 }; + const { poll } = await import("../src/browser/io.js"); + const fakeList = { length: 0x100000000 } as any; assert.throws(() => poll.poll(fakeList), /u32/); }); test("RequestOptions rejects negative connect timeout", async () => { - const { types } = await import("../lib/browser/http.js"); + const { types } = await import("../src/browser/http.js"); const opts = new types.RequestOptions(); assert.throws(() => opts.setConnectTimeout(-1n), /negative/); }); test("RequestOptions rejects negative first-byte timeout", async () => { - const { types } = await import("../lib/browser/http.js"); + const { types } = await import("../src/browser/http.js"); const opts = new types.RequestOptions(); assert.throws(() => opts.setFirstByteTimeout(-1n), /negative/); }); test("RequestOptions rejects negative between-bytes timeout", async () => { - const { types } = await import("../lib/browser/http.js"); + const { types } = await import("../src/browser/http.js"); const opts = new types.RequestOptions(); assert.throws(() => opts.setBetweenBytesTimeout(-1n), /negative/); }); }); -function testWithGCWrap(asyncTestFn) { +function testWithGCWrap(asyncTestFn: any) { return async () => { await asyncTestFn(); - // Force the JS GC to run finalizers + // @ts-expect-error Force the JS GC to run finalizers gc(); await new Promise((resolve) => setTimeout(resolve, 200)); }; diff --git a/packages/preview2-shim/test/types.js b/packages/preview2-shim/test/types.ts similarity index 91% rename from packages/preview2-shim/test/types.js rename to packages/preview2-shim/test/types.ts index 0a2307451..cd74cf39e 100644 --- a/packages/preview2-shim/test/types.js +++ b/packages/preview2-shim/test/types.ts @@ -7,7 +7,7 @@ import { tsCodegen, FIXTURES_TYPES_DIR } from "./common.js"; suite("preview2-shim types", () => { test("interface namespaces are exposed", async () => { tsCodegen({ - tsConfigPath: join(FIXTURES_TYPES_DIR, "iface-namespaces-at-runtime/tsconfig.json"), + tsConfigPath: join(FIXTURES_TYPES_DIR, "iface-namespaces-at-runtime/tsconfig.test.json"), }); }); }); diff --git a/packages/preview2-shim/test/vitest.ts b/packages/preview2-shim/test/vitest.ts index 88985e2a8..89936f0ea 100644 --- a/packages/preview2-shim/test/vitest.ts +++ b/packages/preview2-shim/test/vitest.ts @@ -10,8 +10,8 @@ export default defineConfig({ disableConsoleIntercept: true, printConsoleTrace: true, passWithNoTests: false, - include: ["test/*.js"], - exclude: ["test/common.js"], + include: ["test/*.ts"], + exclude: ["test/common.ts", "test/vitest.ts"], testTimeout: DEFAULT_TIMEOUT_MS, hookTimeout: DEFAULT_TIMEOUT_MS, teardownTimeout: DEFAULT_TIMEOUT_MS, diff --git a/packages/preview2-shim/tsconfig.build.json b/packages/preview2-shim/tsconfig.build.json new file mode 100644 index 000000000..5eec24ca9 --- /dev/null +++ b/packages/preview2-shim/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["test/**/*"], + "compilerOptions": { + "rootDir": "src/", + "declaration": true, + "declarationMap": false, + "sourceMap": false, + "noEmit": false + } +} diff --git a/packages/preview2-shim/tsconfig.json b/packages/preview2-shim/tsconfig.json index 7b447e9d7..0e15fcc4e 100644 --- a/packages/preview2-shim/tsconfig.json +++ b/packages/preview2-shim/tsconfig.json @@ -1,11 +1,20 @@ { + "include": ["src/**/*", "test/**/*.ts"], "compilerOptions": { + "outDir": "lib", + "target": "ES2022", + "lib": ["ESNext", "DOM"], + "types": ["node"], "module": "nodenext", "moduleResolution": "nodenext", - "noEmit": true, "strict": true, "noImplicitAny": false, - "strictNullChecks": true - }, - "include": ["./types", "example.ts"] + "strictNullChecks": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true + } } diff --git a/packages/preview2-shim/types/instantiation.d.ts b/packages/preview2-shim/types/instantiation.d.ts index f8b1863e3..eb527a631 100644 --- a/packages/preview2-shim/types/instantiation.d.ts +++ b/packages/preview2-shim/types/instantiation.d.ts @@ -56,13 +56,13 @@ type VersionedWASIImportObject = { type AppendVersion< Key extends string | number | symbol, Version extends string, -> = Version extends `${infer V}` - ? Key extends `${infer K}` - ? Key extends '' - ? `${K}` - : `${K}@${V}` - : never - : never; +> = Version extends '' + ? Key + : Version extends `${infer V}` + ? Key extends `${infer K}` + ? `${K}@${V}` + : never + : never; /** * Sandbox configuration options for WASIShim diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6c461caa..a0fa12f28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -386,6 +386,9 @@ importers: '@bytecodealliance/jco': specifier: 1.20.0 version: 1.20.0 + '@types/node': + specifier: ^24.12.4 + version: 24.12.4 mime: specifier: ^4.0.7 version: 4.1.0 From 8f5f4dd3cf51c99a8f99d5bbc49fc4693d735b60 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:53:59 +0100 Subject: [PATCH 2/5] chore: fix based on suggestions from pr --- crates/jco/tests/importmap.deno.json | 14 +-- .../components/ts-resource-export/README.md | 4 +- .../components/ts-resource-import/README.md | 4 +- packages/jco/test/browser.html | 14 +-- packages/preview2-shim/.gitignore | 2 +- packages/preview2-shim/package.json | 24 ++-- .../preview2-shim/src/browser/filesystem.ts | 25 +++-- packages/preview2-shim/src/browser/http.ts | 2 +- packages/preview2-shim/src/io/worker-http.ts | 11 +- packages/preview2-shim/src/nodejs/http.ts | 2 +- packages/preview2-shim/test/browser.ts | 20 ---- .../fixtures/browser/basic-harness/index.html | 14 +-- packages/preview2-shim/test/test.ts | 106 +++++++++--------- packages/preview2-shim/tsconfig.json | 2 +- .../lib/nodejs/sockets/address.js | 2 +- 15 files changed, 122 insertions(+), 124 deletions(-) diff --git a/crates/jco/tests/importmap.deno.json b/crates/jco/tests/importmap.deno.json index 4b5adfb43..5cc36660d 100644 --- a/crates/jco/tests/importmap.deno.json +++ b/crates/jco/tests/importmap.deno.json @@ -1,11 +1,11 @@ { "imports": { - "@bytecodealliance/preview2-shim/filesystem": "../../../node_modules/@bytecodealliance/preview2-shim/lib/nodejs/filesystem.js", - "@bytecodealliance/preview2-shim/sockets": "../../../node_modules/@bytecodealliance/preview2-shim/lib/nodejs/sockets.js", - "@bytecodealliance/preview2-shim/clocks": "../../../node_modules/@bytecodealliance/preview2-shim/lib/nodejs/clocks.js", - "@bytecodealliance/preview2-shim/io": "../../../node_modules/@bytecodealliance/preview2-shim/lib/nodejs/io.js", - "@bytecodealliance/preview2-shim/http": "../../../node_modules/@bytecodealliance/preview2-shim/lib/nodejs/http.js", - "@bytecodealliance/preview2-shim/cli": "../../../node_modules/@bytecodealliance/preview2-shim/lib/nodejs/cli.js", - "@bytecodealliance/preview2-shim/random": "../../../node_modules/@bytecodealliance/preview2-shim/lib/nodejs/random.js" + "@bytecodealliance/preview2-shim/filesystem": "../../../node_modules/@bytecodealliance/preview2-shim/dist/nodejs/filesystem.js", + "@bytecodealliance/preview2-shim/sockets": "../../../node_modules/@bytecodealliance/preview2-shim/dist/nodejs/sockets.js", + "@bytecodealliance/preview2-shim/clocks": "../../../node_modules/@bytecodealliance/preview2-shim/dist/nodejs/clocks.js", + "@bytecodealliance/preview2-shim/io": "../../../node_modules/@bytecodealliance/preview2-shim/dist/nodejs/io.js", + "@bytecodealliance/preview2-shim/http": "../../../node_modules/@bytecodealliance/preview2-shim/dist/nodejs/http.js", + "@bytecodealliance/preview2-shim/cli": "../../../node_modules/@bytecodealliance/preview2-shim/dist/nodejs/cli.js", + "@bytecodealliance/preview2-shim/random": "../../../node_modules/@bytecodealliance/preview2-shim/dist/nodejs/random.js" } } diff --git a/examples/components/ts-resource-export/README.md b/examples/components/ts-resource-export/README.md index 0567fe76f..2e313bd65 100644 --- a/examples/components/ts-resource-export/README.md +++ b/examples/components/ts-resource-export/README.md @@ -106,7 +106,7 @@ OK Successfully written dist/component.wasm. embedding.mts → dist/transpiled/embedding.js... (!) Circular dependency -../../../packages/preview2-shim/lib/browser/filesystem.js -> ../../../packages/preview2-shim/lib/browser/cli.js -> ../../../packages/preview2-shim/lib/browser/filesystem.js +../../../packages/preview2-shim/dist/browser/filesystem.js -> ../../../packages/preview2-shim/dist/browser/cli.js -> ../../../packages/preview2-shim/dist/browser/filesystem.js created dist/transpiled/embedding.js in 1.2s > ts-resource-import@0.1.0 run:embedding @@ -341,7 +341,7 @@ You should see output like the following: embedding.mts → dist/transpiled/embedding.js... (!) Circular dependency -../../../packages/preview2-shim/lib/browser/filesystem.js -> ../../../packages/preview2-shim/lib/browser/cli.js -> ../../../packages/preview2-shim/lib/browser/filesystem.js +../../../packages/preview2-shim/dist/browser/filesystem.js -> ../../../packages/preview2-shim/dist/browser/cli.js -> ../../../packages/preview2-shim/dist/browser/filesystem.js created dist/transpiled/embedding.js in 1.1s > ts-resource-import@0.1.0 run:embedding diff --git a/examples/components/ts-resource-import/README.md b/examples/components/ts-resource-import/README.md index c5f07d868..a2856dc90 100644 --- a/examples/components/ts-resource-import/README.md +++ b/examples/components/ts-resource-import/README.md @@ -106,7 +106,7 @@ OK Successfully written dist/component.wasm. embedding.mts → dist/transpiled/embedding.js... (!) Circular dependency -../../../packages/preview2-shim/lib/browser/filesystem.js -> ../../../packages/preview2-shim/lib/browser/cli.js -> ../../../packages/preview2-shim/lib/browser/filesystem.js +../../../packages/preview2-shim/dist/browser/filesystem.js -> ../../../packages/preview2-shim/dist/browser/cli.js -> ../../../packages/preview2-shim/dist/browser/filesystem.js created dist/transpiled/embedding.js in 1.2s > ts-resource-import@0.1.0 run:embedding @@ -341,7 +341,7 @@ You should see output like the following: embedding.mts → dist/transpiled/embedding.js... (!) Circular dependency -../../../packages/preview2-shim/lib/browser/filesystem.js -> ../../../packages/preview2-shim/lib/browser/cli.js -> ../../../packages/preview2-shim/lib/browser/filesystem.js +../../../packages/preview2-shim/dist/browser/filesystem.js -> ../../../packages/preview2-shim/dist/browser/cli.js -> ../../../packages/preview2-shim/dist/browser/filesystem.js created dist/transpiled/embedding.js in 1.1s > ts-resource-import@0.1.0 run:embedding diff --git a/packages/jco/test/browser.html b/packages/jco/test/browser.html index e00faaf03..d04f6973a 100644 --- a/packages/jco/test/browser.html +++ b/packages/jco/test/browser.html @@ -2,13 +2,13 @@ diff --git a/packages/preview2-shim/test/test.ts b/packages/preview2-shim/test/test.ts index eba46e632..3fc4d9f41 100644 --- a/packages/preview2-shim/test/test.ts +++ b/packages/preview2-shim/test/test.ts @@ -13,7 +13,7 @@ const symbolDispose = Symbol.dispose || Symbol.for("dispose"); suite("Node.js Preview2", () => { test("Stdio", async () => { - const { cli } = await import("../src/nodejs/index.js"); + const { cli } = await import("@bytecodealliance/preview2-shim"); cli.stdout.getStdout().blockingWriteAndFlush(new TextEncoder().encode("test stdout")); cli.stderr.getStderr().blockingWriteAndFlush(new TextEncoder().encode("test stderr")); }); @@ -22,7 +22,7 @@ suite("Node.js Preview2", () => { test("Wall clock", async () => { const { clocks: { wallClock }, - } = await import("../src/nodejs/index.js"); + } = await import("@bytecodealliance/preview2-shim"); { const { seconds, nanoseconds } = wallClock.now(); @@ -40,7 +40,7 @@ suite("Node.js Preview2", () => { test("Monotonic clock now", async () => { const { clocks: { monotonicClock }, - } = await import("../src/nodejs/index.js"); + } = await import("@bytecodealliance/preview2-shim"); assert.strictEqual(typeof monotonicClock.resolution(), "bigint"); const curNow = monotonicClock.now(); @@ -51,7 +51,7 @@ suite("Node.js Preview2", () => { test("Monotonic clock immediately resolved polls", async () => { const { clocks: { monotonicClock }, - } = await import("../src/nodejs/index.js"); + } = await import("@bytecodealliance/preview2-shim"); const curNow = monotonicClock.now(); { const poll = monotonicClock.subscribeInstant(curNow - 10n); @@ -66,7 +66,7 @@ suite("Node.js Preview2", () => { test("Monotonic clock subscribe duration", async () => { const { clocks: { monotonicClock }, - } = await import("../src/nodejs/index.js"); + } = await import("@bytecodealliance/preview2-shim"); const curNow = monotonicClock.now(); @@ -84,7 +84,7 @@ suite("Node.js Preview2", () => { test("Monotonic clock subscribe instant", async () => { const { clocks: { monotonicClock }, - } = await import("../src/nodejs/index.js"); + } = await import("@bytecodealliance/preview2-shim"); const curNow = monotonicClock.now(); @@ -93,9 +93,10 @@ suite("Node.js Preview2", () => { // verify we are at the right time, and within 1ms of the original now const nextNow = monotonicClock.now(); - assert.ok(nextNow - curNow >= 10e6); + const elapsed = nextNow - curNow; + assert.ok(elapsed >= 10e6); if (!env.CI) { - assert.ok(nextNow - curNow < 15e6); + assert.ok(elapsed < 15e6); } }); }); @@ -103,7 +104,7 @@ suite("Node.js Preview2", () => { test("FS read", async () => { let toDispose: any[] = []; await (async () => { - const { filesystem } = await import("../src/nodejs/index.js"); + const { filesystem } = await import("@bytecodealliance/preview2-shim"); const [[rootDescriptor]] = filesystem.preopens.getDirectories(); const childDescriptor = rootDescriptor.openAt( {}, @@ -135,7 +136,7 @@ suite("Node.js Preview2", () => { }); test("Fields.set on a fresh Fields", async () => { - const { http } = await import("../src/nodejs/index.js"); + const { http } = await import("@bytecodealliance/preview2-shim"); const { Fields } = http.types; const encoder = new TextEncoder(); @@ -150,7 +151,7 @@ suite("Node.js Preview2", () => { }); test("Fields.set replaces existing values", async () => { - const { http } = await import("../src/nodejs/index.js"); + const { http } = await import("@bytecodealliance/preview2-shim"); const { Fields } = http.types; const encoder = new TextEncoder(); @@ -167,7 +168,7 @@ suite("Node.js Preview2", () => { // `this.#table.set(lowercased, [])` for new keys. Calling set with an // empty values list exercises that branch without pushing any entry, // so this locks in the resulting state. - const { http } = await import("../src/nodejs/index.js"); + const { http } = await import("@bytecodealliance/preview2-shim"); const { Fields } = http.types; const fields = Fields.fromList([]); @@ -179,7 +180,7 @@ suite("Node.js Preview2", () => { }); test("Fields.set throws immutable when fields are locked", async () => { - const { http } = await import("../src/nodejs/index.js"); + const { http } = await import("@bytecodealliance/preview2-shim"); const { Fields, OutgoingRequest } = http.types; const encoder = new TextEncoder(); @@ -194,7 +195,7 @@ suite("Node.js Preview2", () => { }); test("Fields.set throws invalid-syntax for an invalid name", async () => { - const { http } = await import("../src/nodejs/index.js"); + const { http } = await import("@bytecodealliance/preview2-shim"); const { Fields } = http.types; const encoder = new TextEncoder(); @@ -207,7 +208,7 @@ suite("Node.js Preview2", () => { }); test("Fields.set throws invalid-syntax for an invalid value", async () => { - const { http } = await import("../src/nodejs/index.js"); + const { http } = await import("@bytecodealliance/preview2-shim"); const { Fields } = http.types; const encoder = new TextEncoder(); @@ -220,7 +221,7 @@ suite("Node.js Preview2", () => { }); test("Fields.set throws forbidden for restricted header names", async () => { - const { http } = await import("../src/nodejs/index.js"); + const { http } = await import("@bytecodealliance/preview2-shim"); const { Fields } = http.types; const encoder = new TextEncoder(); @@ -239,7 +240,7 @@ suite("Node.js Preview2", () => { test( "WASI HTTP", testWithGCWrap(async () => { - const { http } = await import("../src/nodejs/index.js"); + const { http } = await import("@bytecodealliance/preview2-shim"); const { handle } = http.outgoingHandler; const { OutgoingRequest, OutgoingBody, Fields } = http.types; const encoder = new TextEncoder(); @@ -305,14 +306,14 @@ suite("Node.js Preview2", () => { suite("WASI Sockets (TCP)", async () => { test("sockets.instanceNetwork() should be a singleton", async () => { - const { sockets } = await import("../src/nodejs/index.js"); + const { sockets } = await import("@bytecodealliance/preview2-shim"); const network1 = sockets.instanceNetwork.instanceNetwork(); const network2 = sockets.instanceNetwork.instanceNetwork(); assert.strictEqual(network1, network2); }); test("sockets.tcpCreateSocket() should throw not-supported", async () => { - const { sockets } = await import("../src/nodejs/index.js"); + const { sockets } = await import("@bytecodealliance/preview2-shim"); const socket = sockets.tcpCreateSocket.createTcpSocket("ipv4"); assert.notEqual(socket, null); @@ -324,7 +325,7 @@ suite("Node.js Preview2", () => { ); }); test("tcp.bind(): should bind to a valid ipv4 address", async () => { - const { sockets } = await import("../src/nodejs/index.js"); + const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket("ipv4"); const localAddress: IpSocketAddress = { @@ -348,7 +349,7 @@ suite("Node.js Preview2", () => { }); test("tcp.bind(): should bind to a valid ipv6 address and port=0", async () => { - const { sockets } = await import("../src/nodejs/index.js"); + const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket("ipv6"); const localAddress: IpSocketAddress = { @@ -381,7 +382,7 @@ suite("Node.js Preview2", () => { }); test("tcp.bind(): should throw invalid-argument when invalid address family", async () => { - const { sockets } = await import("../src/nodejs/index.js"); + const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket("ipv4"); @@ -404,7 +405,7 @@ suite("Node.js Preview2", () => { }); test("tcp.bind(): should throw invalid-state when already bound", async () => { - const { sockets } = await import("../src/nodejs/index.js"); + const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket("ipv4"); @@ -427,7 +428,7 @@ suite("Node.js Preview2", () => { }); test("tcp.listen(): should listen to an ipv4 address", async () => { - const { sockets } = await import("../src/nodejs/index.js"); + const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket("ipv4"); const localAddress: IpSocketAddress = { @@ -451,7 +452,7 @@ suite("Node.js Preview2", () => { { retry: env.CI ? 3 : 0 }, testWithGCWrap(async () => { const { lookup } = await import("node:dns"); - const { sockets } = await import("../src/nodejs/index.js"); + const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket("ipv4"); @@ -510,7 +511,7 @@ suite("Node.js Preview2", () => { suite("WASI Sockets (UDP)", async () => { test("sockets.udpCreateSocket() should create sockets", async () => { - const { sockets } = await import("../src/nodejs/index.js"); + const { sockets } = await import("@bytecodealliance/preview2-shim"); const socket1 = sockets.udpCreateSocket.createUdpSocket("ipv4"); assert.notEqual(socket1, null); const socket2 = sockets.udpCreateSocket.createUdpSocket("ipv4"); @@ -519,7 +520,7 @@ suite("Node.js Preview2", () => { // TODO: figure out how to mock handle.on("message", ...) test("udp.bind(): should bind to a valid ipv4 address and port=0", async () => { - const { sockets } = await import("../src/nodejs/index.js"); + const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const socket = sockets.udpCreateSocket.createUdpSocket("ipv4"); const localAddress: IpSocketAddress = { @@ -542,7 +543,7 @@ suite("Node.js Preview2", () => { }); test("udp.bind(): should bind to a valid ipv6 address and port=0", async () => { - const { sockets } = await import("../src/nodejs/index.js"); + const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const socket = sockets.udpCreateSocket.createUdpSocket("ipv6"); const localAddress = { @@ -575,7 +576,7 @@ suite("Node.js Preview2", () => { }); test("udp.stream(): should connect to a valid ipv4 address", async () => { - const { sockets } = await import("../src/nodejs/index.js"); + const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const socket = sockets.udpCreateSocket.createUdpSocket("ipv4"); const localAddress: IpSocketAddress = { @@ -610,7 +611,7 @@ suite("Node.js Preview2", () => { test( "udp.stream(): should connect to a valid ipv6 address", testWithGCWrap(async () => { - const { sockets } = await import("../src/nodejs/index.js"); + const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const socket = sockets.udpCreateSocket.createUdpSocket("ipv6"); const localAddress: IpSocketAddress = { @@ -641,7 +642,7 @@ suite("Node.js Preview2", () => { test( "ipNameLookup.resolveAddresses(): should return valid IP addresses", testWithGCWrap(async () => { - const { sockets } = await import("../src/nodejs/index.js"); + const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const stream = sockets.ipNameLookup.resolveAddresses(network, "localhost"); @@ -680,8 +681,7 @@ suite("HTTPServer", () => { test( "HTTPServer: can retrieve randomized server address", testWithGCWrap(async () => { - // HTTPServer is a Node.js-only feature - const httpModule = await import("../src/nodejs/http.js"); + const httpModule = await import("@bytecodealliance/preview2-shim/http"); const HTTPServer = (httpModule as any).HTTPServer; if (!HTTPServer) { console.log("HTTPServer not available in this environment, skipping test"); @@ -704,8 +704,8 @@ suite("HTTPServer", () => { suite("Instantiation", () => { test("WASIShim export (random)", async () => { - const { random } = await import("../src/nodejs/index.js"); - const { WASIShim } = await import("../src/common/instantiation.js"); + const { random } = await import("@bytecodealliance/preview2-shim"); + const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); const shim = new WASIShim(); assert.ok(shim); assert.deepStrictEqual( @@ -723,8 +723,8 @@ suite("Instantiation", () => { }); test("WASIShim export override", async () => { - const { random } = await import("../src/nodejs/index.js"); - const { WASIShim } = await import("../src/common/instantiation.js"); + const { random } = await import("@bytecodealliance/preview2-shim"); + const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); const invalidWASIShim = { random: { random: { @@ -752,14 +752,14 @@ suite("Sandboxing", () => { let originalArgs: string[]; beforeEach(async () => { - const { cli } = await import("../src/nodejs/index.js"); + const { cli } = await import("@bytecodealliance/preview2-shim"); // Save original state originalEnv = cli.environment.getEnvironment() as [string, string][]; originalArgs = cli.environment.getArguments(); }); afterEach(async () => { - const { cli, filesystem } = await import("../src/nodejs/index.js"); + const { cli, filesystem } = await import("@bytecodealliance/preview2-shim"); // Restore default state (filesystem as any)._setPreopens({ "/": "/" }); cli._setEnv(Object.fromEntries(originalEnv)); @@ -767,7 +767,7 @@ suite("Sandboxing", () => { }); test("_clearPreopens removes filesystem access", async () => { - const { filesystem } = await import("../src/nodejs/index.js"); + const { filesystem } = await import("@bytecodealliance/preview2-shim"); const initialPreopens = filesystem.preopens.getDirectories(); assert.ok(initialPreopens.length > 0, "Should have default preopens"); @@ -778,7 +778,7 @@ suite("Sandboxing", () => { }); test("_setPreopens replaces preopens", async () => { - const { filesystem } = await import("../src/nodejs/index.js"); + const { filesystem } = await import("@bytecodealliance/preview2-shim"); (filesystem as any)._setPreopens({ "/custom": "/tmp", @@ -790,7 +790,7 @@ suite("Sandboxing", () => { }); test("_getPreopens returns current preopens", async () => { - const { filesystem } = await import("../src/nodejs/index.js"); + const { filesystem } = await import("@bytecodealliance/preview2-shim"); const preopens = filesystem._getPreopens(); assert.ok(Array.isArray(preopens), "Should return an array"); @@ -801,7 +801,7 @@ suite("Sandboxing", () => { }); test("WASIShim with empty preopens provides no filesystem access", async () => { - const { WASIShim } = await import("../src/common/instantiation.js"); + const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); const sandboxedShim = new WASIShim({ sandbox: { @@ -815,7 +815,7 @@ suite("Sandboxing", () => { }); test("WASIShim with custom env", async () => { - const { WASIShim } = await import("../src/common/instantiation.js"); + const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); const customShim = new WASIShim({ sandbox: { @@ -832,7 +832,7 @@ suite("Sandboxing", () => { }); test("WASIShim with custom args", async () => { - const { WASIShim } = await import("../src/common/instantiation.js"); + const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); const customShim = new WASIShim({ sandbox: { @@ -846,7 +846,7 @@ suite("Sandboxing", () => { }); test("WASIShim with enableNetwork=false denies network", async () => { - const { WASIShim } = await import("../src/common/instantiation.js"); + const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); const noNetworkShim = new WASIShim({ sandbox: { @@ -878,8 +878,8 @@ suite("Sandboxing", () => { }); test("WASIShim with enableNetwork=true (default) allows network", async () => { - const { WASIShim } = await import("../src/common/instantiation.js"); - const { sockets } = await import("../src/nodejs/index.js"); + const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); + const { sockets } = await import("@bytecodealliance/preview2-shim"); const defaultShim = new WASIShim(); @@ -897,7 +897,7 @@ suite("Sandboxing", () => { }); test("Fully sandboxed WASIShim", async () => { - const { WASIShim } = await import("../src/common/instantiation.js"); + const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); const sandboxed = new WASIShim({ sandbox: { @@ -943,7 +943,7 @@ suite("Sandboxing", () => { }); test("Multiple WASIShim instances have isolated preopens", async () => { - const { WASIShim } = await import("../src/common/instantiation.js"); + const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); // Create two shims with different preopens const shim1 = new WASIShim({ @@ -973,7 +973,7 @@ suite("Sandboxing", () => { }); test("Multiple WASIShim instances have isolated env and args", async () => { - const { WASIShim } = await import("../src/common/instantiation.js"); + const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); const shim1 = new WASIShim({ sandbox: { @@ -1005,7 +1005,7 @@ suite("Sandboxing", () => { test( "WASIShim isolated preopens can read files", testWithGCWrap(async () => { - const { WASIShim } = await import("../src/common/instantiation.js"); + const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); const { dirname } = await import("node:path"); const testFilePath = fileURLToPath(import.meta.url); @@ -1053,7 +1053,7 @@ suite("Sandboxing", () => { test( "WASIShim isolated preopens don't access paths outside preopen", testWithGCWrap(async () => { - const { WASIShim } = await import("../src/common/instantiation.js"); + const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); const { dirname } = await import("node:path"); const testFilePath = fileURLToPath(import.meta.url); diff --git a/packages/preview2-shim/tsconfig.json b/packages/preview2-shim/tsconfig.json index 0e15fcc4e..d3e053346 100644 --- a/packages/preview2-shim/tsconfig.json +++ b/packages/preview2-shim/tsconfig.json @@ -1,7 +1,7 @@ { "include": ["src/**/*", "test/**/*.ts"], "compilerOptions": { - "outDir": "lib", + "outDir": "dist", "target": "ES2022", "lib": ["ESNext", "DOM"], "types": ["node"], diff --git a/packages/preview3-shim/lib/nodejs/sockets/address.js b/packages/preview3-shim/lib/nodejs/sockets/address.js index 48ca33b39..2282ebaea 100644 --- a/packages/preview3-shim/lib/nodejs/sockets/address.js +++ b/packages/preview3-shim/lib/nodejs/sockets/address.js @@ -1,4 +1,4 @@ -// Adapted from preview2-shim/lib/io/worker-sockets.js +// Adapted from preview2-shim/src/io/worker-sockets.ts import { networkInterfaces } from "node:os"; // TODO(tandr): switch to generated types From 0a823f0d6362a08d62e83b8c32a7e13f4b53dc18 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:58:56 +0100 Subject: [PATCH 3/5] chore(ci): ensure p2 shim build for tests --- .github/workflows/main.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 000f4ff25..40fcdd06a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,7 @@ name: test on: + workflow_dispatch: merge_group: push: branches: @@ -88,6 +89,15 @@ jobs: path: | packages/jco-transpile/vendor + - name: Build preview2-shim + run: pnpm --filter '@bytecodealliance/preview2-shim' run build + + - name: Upload preview2-shim build output + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: build-prewiew2-shim + path: packages/preview2-shim/dist + test-jco: runs-on: ${{ matrix.os }} needs: @@ -206,6 +216,12 @@ jobs: name: js-generated-tests path: packages/jco/test/output + - name: Restore preview2-shim build output + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: build-prewiew2-shim + path: packages/preview2-shim/dist + - name: Test LTS Node.js if: matrix.node != 'latest' working-directory: packages/jco @@ -270,6 +286,12 @@ jobs: name: js-generated-tests path: packages/jco/test/output + - name: Restore preview2-shim build output + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: build-prewiew2-shim + path: packages/preview2-shim/dist + - name: Generate tests run: | cargo xtask generate tests preview2 @@ -331,6 +353,12 @@ jobs: name: js-generated-tests path: packages/jco/test/output + - name: Restore preview2-shim build output + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: build-prewiew2-shim + path: packages/preview2-shim/dist + - name: Install node modules run: pnpm install From 69cf55ad38d170e820fd05f1834564862460a1be Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:22:05 +0100 Subject: [PATCH 4/5] chore(jco): revert p2-shim path change for deno --- crates/jco/tests/importmap.deno.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/jco/tests/importmap.deno.json b/crates/jco/tests/importmap.deno.json index 5cc36660d..4b5adfb43 100644 --- a/crates/jco/tests/importmap.deno.json +++ b/crates/jco/tests/importmap.deno.json @@ -1,11 +1,11 @@ { "imports": { - "@bytecodealliance/preview2-shim/filesystem": "../../../node_modules/@bytecodealliance/preview2-shim/dist/nodejs/filesystem.js", - "@bytecodealliance/preview2-shim/sockets": "../../../node_modules/@bytecodealliance/preview2-shim/dist/nodejs/sockets.js", - "@bytecodealliance/preview2-shim/clocks": "../../../node_modules/@bytecodealliance/preview2-shim/dist/nodejs/clocks.js", - "@bytecodealliance/preview2-shim/io": "../../../node_modules/@bytecodealliance/preview2-shim/dist/nodejs/io.js", - "@bytecodealliance/preview2-shim/http": "../../../node_modules/@bytecodealliance/preview2-shim/dist/nodejs/http.js", - "@bytecodealliance/preview2-shim/cli": "../../../node_modules/@bytecodealliance/preview2-shim/dist/nodejs/cli.js", - "@bytecodealliance/preview2-shim/random": "../../../node_modules/@bytecodealliance/preview2-shim/dist/nodejs/random.js" + "@bytecodealliance/preview2-shim/filesystem": "../../../node_modules/@bytecodealliance/preview2-shim/lib/nodejs/filesystem.js", + "@bytecodealliance/preview2-shim/sockets": "../../../node_modules/@bytecodealliance/preview2-shim/lib/nodejs/sockets.js", + "@bytecodealliance/preview2-shim/clocks": "../../../node_modules/@bytecodealliance/preview2-shim/lib/nodejs/clocks.js", + "@bytecodealliance/preview2-shim/io": "../../../node_modules/@bytecodealliance/preview2-shim/lib/nodejs/io.js", + "@bytecodealliance/preview2-shim/http": "../../../node_modules/@bytecodealliance/preview2-shim/lib/nodejs/http.js", + "@bytecodealliance/preview2-shim/cli": "../../../node_modules/@bytecodealliance/preview2-shim/lib/nodejs/cli.js", + "@bytecodealliance/preview2-shim/random": "../../../node_modules/@bytecodealliance/preview2-shim/lib/nodejs/random.js" } } From 13fdad6b71cde6960f7d8ec7cd5a6182f29aecd8 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Fri, 19 Jun 2026 12:55:23 +0900 Subject: [PATCH 5/5] chore(ci): remove workflow dispatch from main workflow --- .github/workflows/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 40fcdd06a..d31c3e65e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,7 +1,6 @@ name: test on: - workflow_dispatch: merge_group: push: branches: