@tsrs/wasm 0.0.0-bootstrap.1 → 0.16.0-ts7.1.0-dev.20260929

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core.d.ts ADDED
@@ -0,0 +1,105 @@
1
+ /** Request flag: reply with the diagnostics as JSON (`DiagnosticResponse[]`) instead of printing them. */
2
+ export declare const REQUEST_JSON_DIAGNOSTICS: 1;
3
+ /** Request flag: the host file system is case-insensitive. */
4
+ export declare const REQUEST_CASE_INSENSITIVE: 2;
5
+ /** Request flag: stdout is a terminal (the default for `--pretty`). */
6
+ export declare const REQUEST_TTY: 4;
7
+ /** Exit status of a crashed run (a panic, or a trap such as a stack overflow or running out of memory); tsc's own are 0-4. */
8
+ export declare const EXIT_CRASHED: 5;
9
+
10
+ /** A compiled `WebAssembly.Module`, typed structurally so that these declarations need neither lib dom nor @types/node. */
11
+ export type WasmModule = object;
12
+
13
+ /** The module's `WasmMemory`. */
14
+ export interface WasmMemory {
15
+ readonly buffer: ArrayBuffer;
16
+ }
17
+
18
+ export declare class WasiExit extends Error {
19
+ code: number;
20
+ constructor(code: number);
21
+ }
22
+
23
+ /** Thrown by a HostFileSystem method for a missing path. */
24
+ export declare class NotFoundError extends Error {}
25
+
26
+ export interface FileStat {
27
+ kind: "f" | "d" | "o";
28
+ size: number | bigint;
29
+ mtimeNs: bigint;
30
+ }
31
+
32
+ /** The file system the module reads and writes through. Paths are absolute, with `/` separators. */
33
+ export interface HostFileSystem {
34
+ readFile(path: string): Uint8Array;
35
+ stat(path: string): FileStat;
36
+ /** [kind, name] pairs; kind "l" is a symbolic link (followed by a later stat). */
37
+ readDir(path: string): Array<["f" | "d" | "l" | "o", string]>;
38
+ realpath(path: string): string;
39
+ writeFile(path: string, data: Uint8Array, append: boolean): void;
40
+ remove(path: string): void;
41
+ chtimes(path: string, atimeNs: bigint, mtimeNs: bigint): void;
42
+ }
43
+
44
+ export interface Request {
45
+ cwd: string;
46
+ args: string[];
47
+ flags: number;
48
+ }
49
+
50
+ export interface RunIO {
51
+ env?: Record<string, string | undefined>;
52
+ stdout?(chunk: Uint8Array): void;
53
+ stderr?(chunk: Uint8Array): void;
54
+ beforeRun?(memory: WasmMemory): void;
55
+ afterRun?(memory: WasmMemory): void;
56
+ }
57
+
58
+ export interface RunResult {
59
+ exitCode: number;
60
+ reply: Uint8Array;
61
+ /** Linear memory at the end of the run (it never shrinks, so this is the peak). */
62
+ memoryBytes: number;
63
+ }
64
+
65
+ /** Runs one tsc invocation in a fresh instance of `module`. A trap gives exitCode `EXIT_CRASHED` and a message on stderr. */
66
+ export declare function runTsc(module: WasmModule, request: Request, host: HostFileSystem, io?: RunIO): RunResult;
67
+
68
+ /** `runTsc` with an asynchronous instantiation (browsers forbid a synchronous one of a large module on the page). */
69
+ export declare function runTscAsync(module: WasmModule, request: Request, host: HostFileSystem, io?: RunIO): Promise<RunResult>;
70
+
71
+ export declare function makeRequest(args: string[], options: { cwd: string; json?: boolean; caseInsensitive?: boolean; tty?: boolean }): Request;
72
+
73
+ export interface MemoryFileSystem extends HostFileSystem {
74
+ /** Files the compiler wrote: path -> text (UTF-8, BOM kept). */
75
+ written: Map<string, string>;
76
+ }
77
+
78
+ /** An in-memory file system over absolute path -> contents; directories are implied by the paths below them. */
79
+ export declare function memoryFileSystem(files?: Record<string, string | Uint8Array>, options?: { caseInsensitive?: boolean }): MemoryFileSystem;
80
+
81
+ export interface DiagnosticPosition {
82
+ line: number;
83
+ character: number;
84
+ }
85
+
86
+ /** The TypeScript API's diagnostic shape; positions are UTF-16 offsets. */
87
+ export interface Diagnostic {
88
+ fileName?: string;
89
+ pos: number;
90
+ end: number;
91
+ startPosition?: DiagnosticPosition;
92
+ endPosition?: DiagnosticPosition;
93
+ sourceLines?: Array<{ line: number; text: string }>;
94
+ code: number;
95
+ /** 0 warning, 1 error, 2 suggestion, 3 message */
96
+ category: 0 | 1 | 2 | 3;
97
+ source?: string;
98
+ text: string;
99
+ reportsUnnecessary?: boolean;
100
+ reportsDeprecated?: boolean;
101
+ messageChain?: Diagnostic[];
102
+ relatedInformation?: Diagnostic[];
103
+ }
104
+
105
+ export declare function parseReply(reply: Uint8Array): Diagnostic[];
package/core.js ADDED
@@ -0,0 +1,356 @@
1
+ // Runtime-agnostic host for tsrs.wasm (crates/tsrs_wasm): a small WASI preview1 shim (clocks, random, stdout/stderr,
2
+ // environ, proc_exit; no file system), the host file-system bridge (`tsrs_host.fs` / `tsrs_host.fs_take`), and
3
+ // `runTsc`. Node (node.js) and browsers (browser.js) build on it. No dependencies.
4
+
5
+ export const REQUEST_JSON_DIAGNOSTICS = 1;
6
+ export const REQUEST_CASE_INSENSITIVE = 2;
7
+ export const REQUEST_TTY = 4;
8
+
9
+ /**
10
+ * The exit status of a run that crashed: a Rust panic (the module exits with it, as native tsrs does for a panicked
11
+ * driver thread) or a trap (`runTsc` maps it). tsc's own statuses are 0-4.
12
+ */
13
+ export const EXIT_CRASHED = 5;
14
+
15
+ const OP_READ = 0;
16
+ const OP_STAT = 1;
17
+ const OP_READ_DIR = 2;
18
+ const OP_REALPATH = 3;
19
+ const OP_WRITE = 4;
20
+ const OP_APPEND = 5;
21
+ const OP_REMOVE = 6;
22
+ const OP_CHTIMES = 7;
23
+
24
+ const ESUCCESS = 0;
25
+ const EBADF = 8;
26
+ const EINVAL = 28;
27
+ const ENOSYS = 52;
28
+
29
+ /** Thrown by the shim's `proc_exit`; `runTsc` turns it into the exit code. */
30
+ export class WasiExit extends Error {
31
+ constructor(code) {
32
+ super(`exit ${code}`);
33
+ this.code = code;
34
+ }
35
+ }
36
+
37
+ /** Raised by a HostFileSystem method for a missing path (maps to "not found" in the module). */
38
+ export class NotFoundError extends Error {}
39
+
40
+ const encoder = new TextEncoder();
41
+ const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
42
+
43
+ function nowNs(realtime) {
44
+ const ms = realtime ? performance.timeOrigin + performance.now() : performance.now();
45
+ return BigInt(Math.round(ms * 1e6));
46
+ }
47
+
48
+ // The imports for one run, and `start(instance)`, which runs the request in that instance.
49
+ function prepare(module, request, host, io) {
50
+ let instance;
51
+ let memory;
52
+ const view = () => new DataView(memory.buffer);
53
+ const bytes = () => new Uint8Array(memory.buffer);
54
+ const env = Object.entries(io.env ?? {}).map(([k, v]) => encoder.encode(`${k}=${v}\0`));
55
+ const stdout = io.stdout ?? (() => {});
56
+ const stderr = io.stderr ?? (() => {});
57
+ const tty = (request.flags & REQUEST_TTY) !== 0;
58
+ let staged = new Uint8Array(0);
59
+
60
+ const wasi = {
61
+ args_sizes_get(argc, size) {
62
+ view().setUint32(argc, 0, true);
63
+ view().setUint32(size, 0, true);
64
+ return ESUCCESS;
65
+ },
66
+ args_get() {
67
+ return ESUCCESS;
68
+ },
69
+ environ_sizes_get(count, size) {
70
+ view().setUint32(count, env.length, true);
71
+ view().setUint32(size, env.reduce((n, e) => n + e.length, 0), true);
72
+ return ESUCCESS;
73
+ },
74
+ environ_get(ptrs, buf) {
75
+ let p = buf;
76
+ env.forEach((e, i) => {
77
+ view().setUint32(ptrs + i * 4, p, true);
78
+ bytes().set(e, p);
79
+ p += e.length;
80
+ });
81
+ return ESUCCESS;
82
+ },
83
+ clock_time_get(id, _precision, out) {
84
+ view().setBigUint64(out, nowNs(id === 0), true);
85
+ return ESUCCESS;
86
+ },
87
+ clock_res_get(_id, out) {
88
+ view().setBigUint64(out, 1000n, true);
89
+ return ESUCCESS;
90
+ },
91
+ random_get(ptr, len) {
92
+ for (let off = 0; off < len; off += 65536) {
93
+ crypto.getRandomValues(new Uint8Array(memory.buffer, ptr + off, Math.min(65536, len - off)));
94
+ }
95
+ return ESUCCESS;
96
+ },
97
+ fd_write(fd, iovs, count, written) {
98
+ if (fd !== 1 && fd !== 2) {
99
+ return EBADF;
100
+ }
101
+ let total = 0;
102
+ for (let i = 0; i < count; i++) {
103
+ const ptr = view().getUint32(iovs + i * 8, true);
104
+ const len = view().getUint32(iovs + i * 8 + 4, true);
105
+ const chunk = bytes().slice(ptr, ptr + len);
106
+ (fd === 1 ? stdout : stderr)(chunk);
107
+ total += len;
108
+ }
109
+ view().setUint32(written, total, true);
110
+ return ESUCCESS;
111
+ },
112
+ fd_fdstat_get(fd, out) {
113
+ if (fd > 2) {
114
+ return EBADF;
115
+ }
116
+ // filetype: 2 = character device (a tty), 0 = unknown.
117
+ const v = view();
118
+ for (let i = 0; i < 24; i++) {
119
+ v.setUint8(out + i, 0);
120
+ }
121
+ v.setUint8(out, fd === 1 && tty ? 2 : 0);
122
+ return ESUCCESS;
123
+ },
124
+ fd_prestat_get() {
125
+ return EBADF;
126
+ },
127
+ fd_prestat_dir_name() {
128
+ return EBADF;
129
+ },
130
+ fd_close() {
131
+ return EBADF;
132
+ },
133
+ proc_exit(code) {
134
+ throw new WasiExit(code);
135
+ },
136
+ sched_yield() {
137
+ return ESUCCESS;
138
+ },
139
+ poll_oneoff() {
140
+ return ESUCCESS;
141
+ },
142
+ };
143
+
144
+ const text = (ptr, len) => decoder.decode(bytes().subarray(ptr, ptr + len));
145
+ const splitNul = (ptr, len) => {
146
+ const b = bytes().subarray(ptr, ptr + len);
147
+ const i = b.indexOf(0);
148
+ return [decoder.decode(b.subarray(0, i < 0 ? len : i)), b.slice(i < 0 ? len : i + 1)];
149
+ };
150
+ const stage = (data) => {
151
+ staged = typeof data === "string" ? encoder.encode(data) : data;
152
+ return staged.length;
153
+ };
154
+ const fsCall = (op, ptr, len) => {
155
+ try {
156
+ switch (op) {
157
+ case OP_READ:
158
+ return stage(host.readFile(text(ptr, len)));
159
+ case OP_STAT: {
160
+ const s = host.stat(text(ptr, len));
161
+ return stage(`${s.kind} ${s.size} ${s.mtimeNs}`);
162
+ }
163
+ case OP_READ_DIR:
164
+ return stage(host.readDir(text(ptr, len)).map(([kind, name]) => `${kind}${name}\0`).join(""));
165
+ case OP_REALPATH:
166
+ return stage(host.realpath(text(ptr, len)));
167
+ case OP_WRITE:
168
+ case OP_APPEND: {
169
+ const [path, data] = splitNul(ptr, len);
170
+ host.writeFile(path, data, op === OP_APPEND);
171
+ return stage("");
172
+ }
173
+ case OP_REMOVE:
174
+ host.remove(text(ptr, len));
175
+ return stage("");
176
+ case OP_CHTIMES: {
177
+ const [path, rest] = splitNul(ptr, len);
178
+ const [atime, mtime] = decoder.decode(rest).split("\0").map(BigInt);
179
+ host.chtimes(path, atime, mtime);
180
+ return stage("");
181
+ }
182
+ default:
183
+ return -2 - stage(`unknown op ${op}`);
184
+ }
185
+ } catch (e) {
186
+ if (e instanceof NotFoundError) {
187
+ return -1;
188
+ }
189
+ return -2 - stage(String(e?.message ?? e));
190
+ }
191
+ };
192
+
193
+ const imports = { tsrs_host: { fs: fsCall, fs_take: (ptr) => bytes().set(staged, ptr) }, wasi_snapshot_preview1: {} };
194
+ for (const imp of WebAssembly.Module.imports(module)) {
195
+ if (imp.module === "wasi_snapshot_preview1") {
196
+ imports.wasi_snapshot_preview1[imp.name] = wasi[imp.name] ?? (() => ENOSYS);
197
+ }
198
+ }
199
+ const start = (inst) => {
200
+ instance = inst;
201
+ memory = instance.exports.memory;
202
+ return finish();
203
+ };
204
+ return { imports, start };
205
+
206
+ function finish() {
207
+ let exitCode;
208
+ let reply = new Uint8Array(0);
209
+ try {
210
+ instance.exports._initialize?.();
211
+ const fields = [request.cwd, String(request.flags ?? 0), ...request.args];
212
+ const req = encoder.encode(fields.join("\0"));
213
+ const ptr = instance.exports.tsrs_input(req.length);
214
+ bytes().set(req, ptr);
215
+ io.beforeRun?.(memory);
216
+ exitCode = instance.exports.tsrs_run();
217
+ const out = instance.exports.tsrs_output();
218
+ reply = bytes().slice(out, out + instance.exports.tsrs_output_len());
219
+ } catch (e) {
220
+ if (e instanceof WasiExit) {
221
+ exitCode = e.code;
222
+ } else if (isTrap(e)) {
223
+ stderr(encoder.encode(`error: tsrs.wasm trapped (${e.name}: ${e.message}); a stack overflow on deeply nested code or running out of memory are the usual causes\n`));
224
+ exitCode = EXIT_CRASHED;
225
+ } else {
226
+ throw e;
227
+ }
228
+ }
229
+ io.afterRun?.(memory);
230
+ return { exitCode, reply, memoryBytes: memory.buffer.byteLength };
231
+ }
232
+ }
233
+
234
+ // A trap ends the run without proc_exit: a shadow-stack overflow (the stack is first in memory, so it faults as an
235
+ // out-of-bounds access), an allocation failure (Rust aborts with `unreachable`), or the engine's own stack running
236
+ // out (V8 throws a RangeError there).
237
+ function isTrap(e) {
238
+ return e instanceof WebAssembly.RuntimeError || (e instanceof RangeError && /call stack/i.test(e.message));
239
+ }
240
+
241
+ /**
242
+ * Runs one tsc invocation in a fresh instance of `module` (a compiled WebAssembly.Module).
243
+ * request: { cwd, args, flags }; host: a HostFileSystem; io: { env, stdout(bytes), stderr(bytes), beforeRun(memory),
244
+ * afterRun(memory) } (the run hooks let a harness inspect linear memory, e.g. the shadow-stack census).
245
+ * Returns { exitCode, reply (Uint8Array), memoryBytes }. A trap returns exitCode EXIT_CRASHED with a message on stderr.
246
+ */
247
+ export function runTsc(module, request, host, io = {}) {
248
+ const run = prepare(module, request, host, io);
249
+ return run.start(new WebAssembly.Instance(module, run.imports));
250
+ }
251
+
252
+ /** `runTsc` with an asynchronous instantiation (browsers forbid a synchronous one of a large module on the page). */
253
+ export async function runTscAsync(module, request, host, io = {}) {
254
+ const run = prepare(module, request, host, io);
255
+ return run.start(await WebAssembly.instantiate(module, run.imports));
256
+ }
257
+
258
+ /** The tsc argument list plus the request flags, as `runTsc` takes them. */
259
+ export function makeRequest(args, { cwd, json = false, caseInsensitive = false, tty = false }) {
260
+ let flags = 0;
261
+ if (json) flags |= REQUEST_JSON_DIAGNOSTICS;
262
+ if (caseInsensitive) flags |= REQUEST_CASE_INSENSITIVE;
263
+ if (tty) flags |= REQUEST_TTY;
264
+ return { cwd, args, flags };
265
+ }
266
+
267
+ function normalize(path) {
268
+ const parts = [];
269
+ for (const part of path.replace(/\\/g, "/").split("/")) {
270
+ if (part === "" || part === ".") continue;
271
+ if (part === "..") parts.pop();
272
+ else parts.push(part);
273
+ }
274
+ return "/" + parts.join("/");
275
+ }
276
+
277
+ /**
278
+ * An in-memory HostFileSystem over a map of absolute path -> contents (string or Uint8Array). Directories are implied
279
+ * by the paths below them. `written` collects every file the compiler writes (decoded as UTF-8, BOM kept). With
280
+ * `caseInsensitive`, lookups ignore case (as on macOS's default file system) and names keep the case they were
281
+ * written with.
282
+ */
283
+ export function memoryFileSystem(files = {}, { caseInsensitive = false } = {}) {
284
+ const fold = caseInsensitive ? (p) => p.toLowerCase() : (p) => p;
285
+ const map = new Map(); // folded path -> { path, data }
286
+ const put = (p, data) => map.set(fold(p), { path: p, data });
287
+ for (const [path, data] of Object.entries(files)) {
288
+ put(normalize(path), typeof data === "string" ? encoder.encode(data) : data);
289
+ }
290
+ const written = new Map();
291
+ const isDir = (folded) => {
292
+ if (folded === "/") return true;
293
+ const prefix = folded + "/";
294
+ for (const key of map.keys()) {
295
+ if (key.startsWith(prefix)) return true;
296
+ }
297
+ return false;
298
+ };
299
+ return {
300
+ written,
301
+ files: map,
302
+ readFile(path) {
303
+ const entry = map.get(fold(normalize(path)));
304
+ if (entry === undefined) throw new NotFoundError(path);
305
+ return entry.data;
306
+ },
307
+ stat(path) {
308
+ const p = fold(normalize(path));
309
+ const entry = map.get(p);
310
+ if (entry !== undefined) return { kind: "f", size: entry.data.length, mtimeNs: 0n };
311
+ if (isDir(p)) return { kind: "d", size: 0, mtimeNs: 0n };
312
+ throw new NotFoundError(path);
313
+ },
314
+ readDir(path) {
315
+ const p = fold(normalize(path));
316
+ if (!isDir(p)) throw new NotFoundError(path);
317
+ const prefix = p === "/" ? "/" : p + "/";
318
+ const entries = new Map();
319
+ for (const [key, entry] of map) {
320
+ if (!key.startsWith(prefix)) continue;
321
+ const rest = entry.path.slice(prefix.length);
322
+ const slash = rest.indexOf("/");
323
+ if (slash < 0) entries.set(rest, "f");
324
+ else entries.set(rest.slice(0, slash), "d");
325
+ }
326
+ return [...entries].map(([name, kind]) => [kind, name]);
327
+ },
328
+ realpath(path) {
329
+ return normalize(path);
330
+ },
331
+ writeFile(path, data, append) {
332
+ const p = normalize(path);
333
+ const prev = append ? map.get(fold(p))?.data ?? new Uint8Array(0) : new Uint8Array(0);
334
+ const next = new Uint8Array(prev.length + data.length);
335
+ next.set(prev);
336
+ next.set(data, prev.length);
337
+ put(map.get(fold(p))?.path ?? p, next);
338
+ written.set(map.get(fold(p)).path, decoder.decode(next));
339
+ },
340
+ remove(path) {
341
+ const p = fold(normalize(path));
342
+ for (const [key, entry] of [...map]) {
343
+ if (key === p || key.startsWith(p + "/")) {
344
+ map.delete(key);
345
+ written.delete(entry.path);
346
+ }
347
+ }
348
+ },
349
+ chtimes() {},
350
+ };
351
+ }
352
+
353
+ /** Parses the JSON reply of a REQUEST_JSON_DIAGNOSTICS run. */
354
+ export function parseReply(reply) {
355
+ return reply.length === 0 ? [] : JSON.parse(decoder.decode(reply));
356
+ }
package/index.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ import type { Diagnostic, WasmModule } from "./core.js";
2
+
3
+ export type { Diagnostic, HostFileSystem, MemoryFileSystem, WasmModule } from "./core.js";
4
+
5
+ export interface TscOptions {
6
+ /** The current directory (Node: process.cwd() on the real file system; "/" with `files`). */
7
+ cwd?: string;
8
+ /** Run over these in-memory files (absolute path -> contents) instead of the real file system. */
9
+ files?: Record<string, string | Uint8Array>;
10
+ /** Environment variables the compiler sees (none by default). */
11
+ env?: Record<string, string | undefined>;
12
+ /** "json": return the diagnostics as `diagnostics` instead of printing them (not with --build). */
13
+ diagnostics?: "text" | "json";
14
+ /** stdout is a terminal (the default for --pretty). */
15
+ tty?: boolean;
16
+ /** Node: "inherit" writes to this process's fd 1 / fd 2; collected into the result otherwise. */
17
+ stdout?: "inherit" | "collect";
18
+ stderr?: "inherit" | "collect";
19
+ caseInsensitive?: boolean;
20
+ /** Node: the worker's stack in MB (default 256). */
21
+ stackSizeMb?: number;
22
+ /** Browser: an already compiled module, or where to fetch tsrs.wasm from. */
23
+ module?: WasmModule;
24
+ url?: string | { readonly href: string };
25
+ }
26
+
27
+ export interface TscResult {
28
+ exitCode: number;
29
+ stdout: string;
30
+ stderr: string;
31
+ diagnostics?: Diagnostic[];
32
+ /** With `files`: what the compiler wrote, path -> text. */
33
+ files?: Record<string, string>;
34
+ /** Linear memory at the end of the run, in bytes (the peak; it never shrinks). */
35
+ memoryBytes: number;
36
+ }
37
+
38
+ /** Runs tsc with these arguments. Each call uses a fresh module instance. */
39
+ export declare function tsc(args: string[], options?: TscOptions): Promise<TscResult>;
40
+
41
+ /** Node: the compiled module. Browser: loads and compiles it (from `url`, default ./tsrs.wasm next to browser.js). */
42
+ export declare function loadModule(url?: string | { readonly href: string }): WasmModule | Promise<WasmModule>;
package/node-fs.js ADDED
@@ -0,0 +1,117 @@
1
+ // The real file system as a HostFileSystem for core.js, matching native tsrs's osvfs: write errors are Rust's
2
+ // `io::Error` texts ("<strerror> (os error N)"), realpath is the kernel's on Linux and a symlink walk elsewhere,
3
+ // writes create missing parent directories, remove is recursive and ignores a missing path.
4
+
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { NotFoundError } from "./core.js";
8
+
9
+ const STRERROR = {
10
+ EACCES: "Permission denied",
11
+ ENOENT: "No such file or directory",
12
+ ENOTDIR: "Not a directory",
13
+ EISDIR: "Is a directory",
14
+ EEXIST: "File exists",
15
+ ENOSPC: "No space left on device",
16
+ EROFS: "Read-only file system",
17
+ ELOOP: "Too many levels of symbolic links",
18
+ ENAMETOOLONG: "File name too long",
19
+ EPERM: "Operation not permitted",
20
+ ENOTEMPTY: "Directory not empty",
21
+ EBUSY: "Resource busy",
22
+ };
23
+
24
+ /** Rust's `io::Error` display for a Node error. */
25
+ export function osErrorText(e) {
26
+ if (typeof e?.errno === "number" && STRERROR[e.code]) {
27
+ return `${STRERROR[e.code]} (os error ${-e.errno})`;
28
+ }
29
+ return String(e?.message ?? e);
30
+ }
31
+
32
+ function rethrow(e) {
33
+ throw new Error(osErrorText(e));
34
+ }
35
+
36
+ const realpath = process.platform === "linux" ? fs.realpathSync.native : fs.realpathSync;
37
+
38
+ function write(p, data, append) {
39
+ if (append) fs.appendFileSync(p, data);
40
+ else fs.writeFileSync(p, data);
41
+ }
42
+
43
+ /** Whether the file system holding Node's executable is case-insensitive (osvfs probes its own executable). */
44
+ export function isCaseInsensitive() {
45
+ const exe = process.execPath;
46
+ const swapped = [...exe].map((c) => (c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase())).join("");
47
+ return swapped !== exe && fs.existsSync(swapped);
48
+ }
49
+
50
+ export function nodeFileSystem() {
51
+ return {
52
+ readFile(p) {
53
+ try {
54
+ return fs.readFileSync(p);
55
+ } catch (e) {
56
+ if (e.code === "ENOENT") throw new NotFoundError(p);
57
+ rethrow(e);
58
+ }
59
+ },
60
+ stat(p) {
61
+ let s;
62
+ try {
63
+ s = fs.statSync(p, { bigint: true, throwIfNoEntry: false });
64
+ } catch (e) {
65
+ rethrow(e);
66
+ }
67
+ if (!s) throw new NotFoundError(p);
68
+ return { kind: s.isDirectory() ? "d" : s.isFile() ? "f" : "o", size: s.size, mtimeNs: s.mtimeNs };
69
+ },
70
+ readDir(p) {
71
+ let entries;
72
+ try {
73
+ entries = fs.readdirSync(p, { withFileTypes: true });
74
+ } catch (e) {
75
+ if (e.code === "ENOENT") throw new NotFoundError(p);
76
+ rethrow(e);
77
+ }
78
+ return entries.map((d) => [d.isDirectory() ? "d" : d.isFile() ? "f" : d.isSymbolicLink() ? "l" : "o", d.name]);
79
+ },
80
+ realpath(p) {
81
+ return realpath(p);
82
+ },
83
+ writeFile(p, data, append) {
84
+ try {
85
+ write(p, data, append);
86
+ return;
87
+ } catch {
88
+ // As osvfs: create the directory, then write again; errors come from those two steps.
89
+ }
90
+ try {
91
+ fs.mkdirSync(path.dirname(p), { recursive: true });
92
+ write(p, data, append);
93
+ } catch (e) {
94
+ rethrow(e);
95
+ }
96
+ },
97
+ remove(p) {
98
+ let s;
99
+ try {
100
+ s = fs.lstatSync(p, { throwIfNoEntry: false });
101
+ if (!s) return;
102
+ // unlink for a symlink: rmSync throws EISDIR on a symlink to a directory (Node 24).
103
+ if (s.isDirectory()) fs.rmSync(p, { recursive: true, force: true });
104
+ else fs.unlinkSync(p);
105
+ } catch (e) {
106
+ rethrow(e);
107
+ }
108
+ },
109
+ chtimes(p, atimeNs, mtimeNs) {
110
+ try {
111
+ fs.utimesSync(p, Number(atimeNs) / 1e9, Number(mtimeNs) / 1e9);
112
+ } catch (e) {
113
+ rethrow(e);
114
+ }
115
+ },
116
+ };
117
+ }
package/node-run.js ADDED
@@ -0,0 +1,55 @@
1
+ // One tsc run on the current thread: the request, the file system (real or in-memory) and the output streams.
2
+ import fs from "node:fs";
3
+ import { makeRequest, memoryFileSystem, parseReply, runTsc } from "./core.js";
4
+ import { isCaseInsensitive, nodeFileSystem } from "./node-fs.js";
5
+
6
+ const SLEEP = new Int32Array(new SharedArrayBuffer(4));
7
+
8
+ /** Writes all of `chunk` to `fd`, waiting out EAGAIN (a non-blocking pipe) and dropping the rest on EPIPE. */
9
+ export function writeAll(fd, chunk) {
10
+ let off = 0;
11
+ while (off < chunk.length) {
12
+ try {
13
+ off += fs.writeSync(fd, chunk, off, chunk.length - off);
14
+ } catch (e) {
15
+ if (e.code === "EAGAIN") {
16
+ Atomics.wait(SLEEP, 0, 0, 1);
17
+ continue;
18
+ }
19
+ if (e.code === "EPIPE") return;
20
+ throw e;
21
+ }
22
+ }
23
+ }
24
+
25
+ function sink(mode, fd, chunks) {
26
+ if (mode === "inherit") return (chunk) => writeAll(fd, chunk);
27
+ return (chunk) => chunks.push(chunk);
28
+ }
29
+
30
+ /** hooks: { beforeRun(memory), afterRun(memory) } (see core.js runTsc). */
31
+ export function run(module, options, hooks = {}) {
32
+ const caseInsensitive = options.caseInsensitive ?? (options.files ? false : isCaseInsensitive());
33
+ const memory = options.files ? memoryFileSystem(options.files, { caseInsensitive }) : null;
34
+ const host = memory ?? nodeFileSystem();
35
+ const cwd = options.cwd ?? (memory ? "/" : process.cwd());
36
+ const json = options.diagnostics === "json";
37
+ const out = [];
38
+ const err = [];
39
+ const request = makeRequest(options.args, { cwd, json, caseInsensitive, tty: !!options.tty });
40
+ const result = runTsc(module, request, host, {
41
+ env: options.env ?? {},
42
+ stdout: sink(options.stdout, 1, out),
43
+ stderr: sink(options.stderr, 2, err),
44
+ ...hooks,
45
+ });
46
+ const text = (chunks) => Buffer.concat(chunks).toString("utf8");
47
+ return {
48
+ exitCode: result.exitCode,
49
+ stdout: text(out),
50
+ stderr: text(err),
51
+ diagnostics: json ? parseReply(result.reply) : undefined,
52
+ files: memory ? Object.fromEntries(memory.written) : undefined,
53
+ memoryBytes: result.memoryBytes,
54
+ };
55
+ }
package/node-worker.js ADDED
@@ -0,0 +1,5 @@
1
+ // Runs one tsc request in a worker thread (node.js posts the compiled module and the options).
2
+ import { parentPort, workerData } from "node:worker_threads";
3
+ import { run } from "./node-run.js";
4
+
5
+ parentPort.postMessage(run(workerData.module, workerData.options));