@ttsc/wasm 0.19.3 → 0.20.0
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/README.md +4 -2
- package/build/build-wasm.cjs +131 -126
- package/dist/.ttsc-wasm-build.json +319 -0
- package/dist/go.mod +2 -0
- package/dist/ttsc.wasm +0 -0
- package/go.mod +2 -0
- package/host/fountain.go +48 -19
- package/lib/src/createMemFS.d.ts +6 -0
- package/lib/src/createMemFS.js +247 -95
- package/lib/src/createMemFS.js.map +1 -1
- package/lib/src/structures/ITtscApi.d.ts +8 -5
- package/lib/src/structures/ITtscNodeAtPositionResult.d.ts +1 -1
- package/lib/src/structures/ITtscNodeInfo.d.ts +1 -1
- package/lib/src/structures/ITtscPositionQuery.d.ts +4 -2
- package/lib/src/structures/ITtscSymbolAtPositionResult.d.ts +1 -1
- package/lib/src/structures/ITtscTypeAtPositionResult.d.ts +1 -1
- package/lib/src/structures/IWasmExecFS.d.ts +14 -4
- package/package.json +1 -1
- package/shim-vendor/shim/ast/shim.go +48 -3
- package/shim-vendor/shim/astnav/go.mod +14 -0
- package/shim-vendor/shim/astnav/go.sum +20 -0
- package/shim-vendor/shim/astnav/shim.go +15 -0
- package/shim-vendor/shim/checker/shim.go +100 -0
- package/shim-vendor/shim/core/shim.go +47 -0
- package/shim-vendor/shim/diagnosticwriter/lint.go +50 -0
- package/src/createMemFS.ts +264 -103
- package/src/structures/ITtscApi.ts +8 -5
- package/src/structures/ITtscNodeAtPositionResult.ts +1 -1
- package/src/structures/ITtscNodeInfo.ts +1 -1
- package/src/structures/ITtscPositionQuery.ts +4 -2
- package/src/structures/ITtscSymbolAtPositionResult.ts +1 -1
- package/src/structures/ITtscTypeAtPositionResult.ts +1 -1
- package/src/structures/IWasmExecFS.ts +14 -4
package/src/createMemFS.ts
CHANGED
|
@@ -64,6 +64,12 @@ function normalize(p: string): string {
|
|
|
64
64
|
* `globalThis.fs` before loading `wasm_exec.js`) and convenience helpers
|
|
65
65
|
* (`writeFile`, `readFile`, `mkdirp`, …) for seeding source files and reading
|
|
66
66
|
* compiler output without touching the real filesystem.
|
|
67
|
+
*
|
|
68
|
+
* Every successful mutation leaves a valid tree: `/` stays a directory, each
|
|
69
|
+
* proper ancestor of a node exists and is a directory, and a file has no
|
|
70
|
+
* descendants. An operation that cannot satisfy that throws (`writeFile`,
|
|
71
|
+
* `mkdirp`) or reports a POSIX error through its callback, having changed no
|
|
72
|
+
* node, byte, or descriptor.
|
|
67
73
|
*/
|
|
68
74
|
export function createMemFS(): IMemFSHost {
|
|
69
75
|
const nodes = new Map<string, INode>();
|
|
@@ -72,19 +78,53 @@ export function createMemFS(): IMemFSHost {
|
|
|
72
78
|
const stdout = { buffer: "" };
|
|
73
79
|
const stderr = { buffer: "" };
|
|
74
80
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
/**
|
|
82
|
+
* One open descriptor.
|
|
83
|
+
*
|
|
84
|
+
* `node` retains the object opened even after its pathname is unlinked or
|
|
85
|
+
* replaced. `readable`, `writable`, and `append` are the access mode and
|
|
86
|
+
* `O_APPEND` flag captured at `open`; without them no descriptor mutation can
|
|
87
|
+
* distinguish an allowed operation from a forbidden one. `position` is the
|
|
88
|
+
* cursor a `position: null` read or write uses and advances.
|
|
89
|
+
*/
|
|
90
|
+
interface IDescriptor {
|
|
91
|
+
path: string;
|
|
92
|
+
node?: INode;
|
|
93
|
+
position: number;
|
|
94
|
+
readable: boolean;
|
|
95
|
+
writable: boolean;
|
|
96
|
+
append: boolean;
|
|
97
|
+
isStdout?: boolean;
|
|
98
|
+
isStderr?: boolean;
|
|
99
|
+
}
|
|
100
|
+
const fdTable = new Map<number, IDescriptor>();
|
|
79
101
|
let nextFd = 100;
|
|
80
102
|
// Reserve 1/2 for stdout/stderr writeSync routing.
|
|
81
|
-
fdTable.set(1, {
|
|
82
|
-
|
|
103
|
+
fdTable.set(1, {
|
|
104
|
+
path: "/dev/stdout",
|
|
105
|
+
position: 0,
|
|
106
|
+
readable: false,
|
|
107
|
+
writable: true,
|
|
108
|
+
append: true,
|
|
109
|
+
isStdout: true,
|
|
110
|
+
});
|
|
111
|
+
fdTable.set(2, {
|
|
112
|
+
path: "/dev/stderr",
|
|
113
|
+
position: 0,
|
|
114
|
+
readable: false,
|
|
115
|
+
writable: true,
|
|
116
|
+
append: true,
|
|
117
|
+
isStderr: true,
|
|
118
|
+
});
|
|
83
119
|
|
|
84
120
|
// Pipe state. fs.pipe2 mints a pair of fds backed by a shared queue;
|
|
85
121
|
// writes append, reads consume. The state is keyed by fd so a single Map
|
|
86
122
|
// lookup in read/write/close can detect "this is a pipe end" without
|
|
87
123
|
// changing the existing fdTable entries.
|
|
124
|
+
//
|
|
125
|
+
// These fds only ever come from a direct JavaScript `fs.pipe2` call. Go's
|
|
126
|
+
// wasm `os.Pipe` returns ENOSYS without crossing the `globalThis.fs` bridge,
|
|
127
|
+
// so the Go runtime never reaches this state — see IWasmExecFS.pipe2.
|
|
88
128
|
interface IPipeState {
|
|
89
129
|
buffers: Uint8Array[];
|
|
90
130
|
pendingReaders: Array<{
|
|
@@ -143,21 +183,62 @@ export function createMemFS(): IMemFSHost {
|
|
|
143
183
|
}
|
|
144
184
|
}
|
|
145
185
|
|
|
146
|
-
/**
|
|
147
|
-
function
|
|
148
|
-
const
|
|
149
|
-
segments.pop();
|
|
186
|
+
/** Root-to-leaf path of every segment of `norm` (`/a/b` → `["/a", "/a/b"]`). */
|
|
187
|
+
function pathChain(norm: string): string[] {
|
|
188
|
+
const chain: string[] = [];
|
|
150
189
|
let cursor = "";
|
|
151
|
-
for (const seg of
|
|
190
|
+
for (const seg of norm.split("/").filter(Boolean)) {
|
|
152
191
|
cursor += "/" + seg;
|
|
153
|
-
|
|
154
|
-
nodes.set(cursor, {
|
|
155
|
-
kind: "dir",
|
|
156
|
-
data: new Uint8Array(),
|
|
157
|
-
mtimeMs: Date.now(),
|
|
158
|
-
});
|
|
159
|
-
}
|
|
192
|
+
chain.push(cursor);
|
|
160
193
|
}
|
|
194
|
+
return chain;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Validate that every path in root-to-leaf `chain` is or can become a
|
|
199
|
+
* directory, and report the ones that do not exist yet.
|
|
200
|
+
*
|
|
201
|
+
* A segment that exists as a file makes everything below it impossible: a
|
|
202
|
+
* file has no descendants, so creating one there would leave a node map that
|
|
203
|
+
* is not a tree. Validation is deliberately separate from creation so every
|
|
204
|
+
* caller can reject before touching anything, and so a rejected operation
|
|
205
|
+
* never leaves half a directory chain behind.
|
|
206
|
+
*/
|
|
207
|
+
function missingDirs(chain: string[], syscall: string): string[] {
|
|
208
|
+
const missing: string[] = [];
|
|
209
|
+
for (const path of chain) {
|
|
210
|
+
const existing = nodes.get(path);
|
|
211
|
+
if (!existing) missing.push(path);
|
|
212
|
+
else if (existing.kind !== "dir")
|
|
213
|
+
throw new MemFSError("ENOTDIR", syscall, path);
|
|
214
|
+
}
|
|
215
|
+
return missing;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Materialize the directories `missingDirs` reported. */
|
|
219
|
+
function createDirs(paths: string[]): void {
|
|
220
|
+
for (const path of paths)
|
|
221
|
+
nodes.set(path, {
|
|
222
|
+
kind: "dir",
|
|
223
|
+
data: new Uint8Array(),
|
|
224
|
+
mtimeMs: Date.now(),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Validate that normalized path `norm` may hold a regular file, returning the
|
|
230
|
+
* node already there when one exists.
|
|
231
|
+
*
|
|
232
|
+
* A directory is never silently replaced by a file — including the root,
|
|
233
|
+
* which is a directory node like any other. POSIX answers `EISDIR`, and
|
|
234
|
+
* overwriting the node here would strand every descendant in the map behind a
|
|
235
|
+
* path `readdir` can no longer walk.
|
|
236
|
+
*/
|
|
237
|
+
function assertFileTarget(norm: string, syscall: string): INode | undefined {
|
|
238
|
+
const existing = nodes.get(norm);
|
|
239
|
+
if (existing && existing.kind !== "file")
|
|
240
|
+
throw new MemFSError("EISDIR", syscall, norm);
|
|
241
|
+
return existing;
|
|
161
242
|
}
|
|
162
243
|
|
|
163
244
|
/** Absolute parent directory of a normalized path (`/` for a top-level path). */
|
|
@@ -185,6 +266,43 @@ export function createMemFS(): IMemFSHost {
|
|
|
185
266
|
return next;
|
|
186
267
|
}
|
|
187
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Write `view` through descriptor `entry` and return the bytes stored.
|
|
271
|
+
*
|
|
272
|
+
* Three offsets are possible and only one of them is right per call: an
|
|
273
|
+
* `O_APPEND` descriptor always writes at end-of-file, an explicit `position`
|
|
274
|
+
* writes exactly there without disturbing the cursor (POSIX `pwrite`), and
|
|
275
|
+
* `position: null` writes at the cursor and advances it. A write that starts
|
|
276
|
+
* past end-of-file zero-fills the gap rather than silently relocating.
|
|
277
|
+
*
|
|
278
|
+
* A zero-byte write changes nothing at all, so a cursor sitting past
|
|
279
|
+
* end-of-file cannot extend the file by writing nothing into it.
|
|
280
|
+
*/
|
|
281
|
+
function writeThroughDescriptor(
|
|
282
|
+
entry: IDescriptor,
|
|
283
|
+
view: Uint8Array,
|
|
284
|
+
position: number | null,
|
|
285
|
+
syscall: string,
|
|
286
|
+
): number {
|
|
287
|
+
if (!entry.writable) throw new MemFSError("EBADF", syscall, entry.path);
|
|
288
|
+
const node = entry.node;
|
|
289
|
+
if (!node) throw new MemFSError("ENOENT", syscall, entry.path);
|
|
290
|
+
if (node.kind !== "file")
|
|
291
|
+
throw new MemFSError("EISDIR", syscall, entry.path);
|
|
292
|
+
const start = entry.append
|
|
293
|
+
? node.data.byteLength
|
|
294
|
+
: (position ?? entry.position);
|
|
295
|
+
if (!Number.isInteger(start) || start < 0)
|
|
296
|
+
throw new MemFSError("EINVAL", syscall, entry.path);
|
|
297
|
+
if (view.byteLength === 0) return 0;
|
|
298
|
+
const end = start + view.byteLength;
|
|
299
|
+
if (end > node.data.byteLength) node.data = resizeFileData(node.data, end);
|
|
300
|
+
node.data.set(view, start);
|
|
301
|
+
node.mtimeMs = Date.now();
|
|
302
|
+
if (position === null || entry.append) entry.position = end;
|
|
303
|
+
return view.byteLength;
|
|
304
|
+
}
|
|
305
|
+
|
|
188
306
|
/**
|
|
189
307
|
* Move the entire subtree rooted at `src` to `dest`, overwriting any existing
|
|
190
308
|
* `dest` node. Every descendant key is re-parented so no old-prefix node is
|
|
@@ -214,29 +332,26 @@ export function createMemFS(): IMemFSHost {
|
|
|
214
332
|
}
|
|
215
333
|
|
|
216
334
|
function mkdirp(p: string): void {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
cursor += "/" + seg;
|
|
221
|
-
const existing = nodes.get(cursor);
|
|
222
|
-
if (!existing) {
|
|
223
|
-
nodes.set(cursor, {
|
|
224
|
-
kind: "dir",
|
|
225
|
-
data: new Uint8Array(),
|
|
226
|
-
mtimeMs: Date.now(),
|
|
227
|
-
});
|
|
228
|
-
} else if (existing.kind !== "dir") {
|
|
229
|
-
throw new MemFSError("ENOTDIR", "mkdir", cursor);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
335
|
+
// Validate the whole chain before creating any of it: a rejected mkdirp
|
|
336
|
+
// must not leave the prefix it had already walked past behind.
|
|
337
|
+
createDirs(missingDirs(pathChain(normalize(p)), "mkdir"));
|
|
232
338
|
}
|
|
233
339
|
|
|
234
340
|
function writeFile(p: string, data: string | Uint8Array): void {
|
|
235
341
|
const norm = normalize(p);
|
|
236
|
-
|
|
342
|
+
// Resolve left to right the way POSIX does: an impossible ancestor is
|
|
343
|
+
// reported before the target, and both are checked before any mutation.
|
|
344
|
+
const missing = missingDirs(pathChain(norm).slice(0, -1), "open");
|
|
345
|
+
const existing = assertFileTarget(norm, "open");
|
|
237
346
|
const bytes =
|
|
238
347
|
typeof data === "string" ? encoder.encode(data) : new Uint8Array(data);
|
|
239
|
-
|
|
348
|
+
createDirs(missing);
|
|
349
|
+
// Overwriting keeps the same node so descriptors already pointing at this
|
|
350
|
+
// file observe the replacement instead of a detached predecessor.
|
|
351
|
+
if (existing) {
|
|
352
|
+
existing.data = bytes;
|
|
353
|
+
existing.mtimeMs = Date.now();
|
|
354
|
+
} else nodes.set(norm, { kind: "file", data: bytes, mtimeMs: Date.now() });
|
|
240
355
|
}
|
|
241
356
|
|
|
242
357
|
function readFile(p: string): Uint8Array | null {
|
|
@@ -334,40 +449,19 @@ export function createMemFS(): IMemFSHost {
|
|
|
334
449
|
console.error("[wasm]", line);
|
|
335
450
|
return buf.length;
|
|
336
451
|
}
|
|
337
|
-
// Open-file fds (>= 100):
|
|
338
|
-
//
|
|
452
|
+
// Open-file fds (>= 100): write at the descriptor cursor, the way Node's
|
|
453
|
+
// own `writeSync` without a position does. Browser-hosted tools use this
|
|
454
|
+
// path for ordinary virtual files and explicit outputs.
|
|
455
|
+
//
|
|
456
|
+
// An unknown or already-closed fd throws instead of being diverted into
|
|
457
|
+
// the captured stderr: reporting the byte count of a write nobody
|
|
458
|
+
// performed made the caller continue on a false success and quietly
|
|
459
|
+
// contaminated a diagnostic channel consumers read.
|
|
339
460
|
const entry = fdTable.get(fd);
|
|
340
|
-
if (entry)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
// bytes are copied into `next` before writeSync returns.
|
|
345
|
-
const incoming = buf.subarray(0);
|
|
346
|
-
const existing = node.data;
|
|
347
|
-
const next = new Uint8Array(
|
|
348
|
-
existing.byteLength + incoming.byteLength,
|
|
349
|
-
);
|
|
350
|
-
next.set(existing, 0);
|
|
351
|
-
next.set(incoming, existing.byteLength);
|
|
352
|
-
node.data = next;
|
|
353
|
-
node.mtimeMs = Date.now();
|
|
354
|
-
entry.position = next.byteLength;
|
|
355
|
-
return incoming.byteLength;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
// Unknown fd. Fall back to the stderr buffer so the bytes aren't lost
|
|
359
|
-
// entirely (and surface as a console.error so the regression is
|
|
360
|
-
// visible to whoever's looking).
|
|
361
|
-
stderr.buffer += decoder.decode(buf);
|
|
362
|
-
// eslint-disable-next-line no-console
|
|
363
|
-
console.error(
|
|
364
|
-
"[wasm] writeSync to unknown fd " +
|
|
365
|
-
fd +
|
|
366
|
-
" (" +
|
|
367
|
-
buf.byteLength +
|
|
368
|
-
" bytes); routed to stderr buffer",
|
|
369
|
-
);
|
|
370
|
-
return buf.length;
|
|
461
|
+
if (!entry) throw new MemFSError("EBADF", "write");
|
|
462
|
+
// subarray(0) is a zero-copy view over the full incoming buffer; the
|
|
463
|
+
// bytes are copied into the node before writeSync returns.
|
|
464
|
+
return writeThroughDescriptor(entry, buf.subarray(0), null, "write");
|
|
371
465
|
},
|
|
372
466
|
|
|
373
467
|
write(fd, buf, offset, length, position, callback) {
|
|
@@ -390,44 +484,83 @@ export function createMemFS(): IMemFSHost {
|
|
|
390
484
|
callback(null, length);
|
|
391
485
|
return;
|
|
392
486
|
}
|
|
393
|
-
|
|
394
|
-
|
|
487
|
+
const view = buf.subarray(offset, offset + length);
|
|
488
|
+
// The stdout/stderr capture buffers are streams, not files: they have
|
|
489
|
+
// no seekable offset, so an explicit position is meaningless on them.
|
|
490
|
+
if (fd === 1 || fd === 2) {
|
|
491
|
+
if (position !== null && position !== 0)
|
|
492
|
+
throw new MemFSError("ESPIPE", "write");
|
|
493
|
+
callback(null, this.writeSync(fd, view));
|
|
395
494
|
return;
|
|
396
495
|
}
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
callback(null,
|
|
496
|
+
const entry = fdTable.get(fd);
|
|
497
|
+
if (!entry) throw new MemFSError("EBADF", "write");
|
|
498
|
+
callback(null, writeThroughDescriptor(entry, view, position, "write"));
|
|
400
499
|
} catch (err) {
|
|
401
500
|
callback(err as NodeJS.ErrnoException, 0);
|
|
402
501
|
}
|
|
403
502
|
},
|
|
404
503
|
|
|
504
|
+
// Every rejection happens before the first mutation and before a
|
|
505
|
+
// descriptor is minted, so a refused open leaves neither a half-created
|
|
506
|
+
// node nor a leaked fd behind.
|
|
405
507
|
open(p, flags, _mode, callback) {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
508
|
+
try {
|
|
509
|
+
const norm = normalize(p);
|
|
510
|
+
const creating = (flags & (this.constants.O_CREAT ?? 0)) !== 0;
|
|
511
|
+
const exclusive = (flags & (this.constants.O_EXCL ?? 0)) !== 0;
|
|
512
|
+
const truncating = (flags & (this.constants.O_TRUNC ?? 0)) !== 0;
|
|
513
|
+
const appending = (flags & (this.constants.O_APPEND ?? 0)) !== 0;
|
|
514
|
+
const directoryOnly = (flags & (this.constants.O_DIRECTORY ?? 0)) !== 0;
|
|
515
|
+
// The access mode is the low bits of `flags`: absent both means
|
|
516
|
+
// read-only, and `O_RDWR` alongside `O_WRONLY` still grants both.
|
|
517
|
+
const writeOnly = (flags & (this.constants.O_WRONLY ?? 0)) !== 0;
|
|
518
|
+
const readWrite = (flags & (this.constants.O_RDWR ?? 0)) !== 0;
|
|
519
|
+
const writable = writeOnly || readWrite;
|
|
520
|
+
const readable = !writeOnly || readWrite;
|
|
521
|
+
|
|
522
|
+
let node = nodes.get(norm);
|
|
523
|
+
if (!node) {
|
|
524
|
+
if (!creating) throw new MemFSError("ENOENT", "open", norm);
|
|
525
|
+
// `open` can only ever create a regular file, so a caller demanding a
|
|
526
|
+
// directory cannot be satisfied by creating one.
|
|
527
|
+
if (directoryOnly) throw new MemFSError("ENOTDIR", "open", norm);
|
|
528
|
+
// Creating the missing ancestor chain is the documented job of
|
|
529
|
+
// `writeFile` and `mkdirp`; the low-level `open` never promised it.
|
|
530
|
+
const missing = missingDirs(pathChain(norm).slice(0, -1), "open");
|
|
531
|
+
if (missing.length > 0)
|
|
532
|
+
throw new MemFSError("ENOENT", "open", missing[0]!);
|
|
533
|
+
node = { kind: "file", data: new Uint8Array(), mtimeMs: Date.now() };
|
|
534
|
+
nodes.set(norm, node);
|
|
535
|
+
} else {
|
|
536
|
+
if (creating && exclusive)
|
|
537
|
+
throw new MemFSError("EEXIST", "open", norm);
|
|
538
|
+
if (node.kind === "dir") {
|
|
539
|
+
// A directory opens read-only — Go stats the fd and lists the path
|
|
540
|
+
// that way. Writing to it or truncating it would turn it into a
|
|
541
|
+
// file and orphan every descendant.
|
|
542
|
+
if (writable || truncating)
|
|
543
|
+
throw new MemFSError("EISDIR", "open", norm);
|
|
544
|
+
} else if (directoryOnly)
|
|
545
|
+
throw new MemFSError("ENOTDIR", "open", norm);
|
|
413
546
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
data: new Uint8Array(),
|
|
427
|
-
mtimeMs: Date.now(),
|
|
547
|
+
if (truncating) {
|
|
548
|
+
node.data = new Uint8Array();
|
|
549
|
+
node.mtimeMs = Date.now();
|
|
550
|
+
}
|
|
551
|
+
const fd = nextFd++;
|
|
552
|
+
fdTable.set(fd, {
|
|
553
|
+
path: norm,
|
|
554
|
+
node,
|
|
555
|
+
position: 0,
|
|
556
|
+
readable,
|
|
557
|
+
writable,
|
|
558
|
+
append: appending,
|
|
428
559
|
});
|
|
560
|
+
callback(null, fd);
|
|
561
|
+
} catch (err) {
|
|
562
|
+
callback(err as NodeJS.ErrnoException, -1);
|
|
429
563
|
}
|
|
430
|
-
callback(null, fd);
|
|
431
564
|
},
|
|
432
565
|
|
|
433
566
|
close(fd, callback) {
|
|
@@ -475,16 +608,29 @@ export function createMemFS(): IMemFSHost {
|
|
|
475
608
|
callback(new MemFSError("EBADF", "read"), 0);
|
|
476
609
|
return;
|
|
477
610
|
}
|
|
478
|
-
|
|
611
|
+
// A write-only descriptor is not a readable one. POSIX answers EBADF for
|
|
612
|
+
// an operation the descriptor's access mode never granted.
|
|
613
|
+
if (!entry.readable) {
|
|
614
|
+
callback(new MemFSError("EBADF", "read", entry.path), 0);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const node = entry.node;
|
|
479
618
|
if (!node || node.kind !== "file") {
|
|
480
619
|
callback(new MemFSError("ENOENT", "read", entry.path), 0);
|
|
481
620
|
return;
|
|
482
621
|
}
|
|
483
622
|
const start = position ?? entry.position;
|
|
623
|
+
if (!Number.isInteger(start) || start < 0) {
|
|
624
|
+
callback(new MemFSError("EINVAL", "read", entry.path), 0);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
484
627
|
const end = Math.min(start + length, node.data.byteLength);
|
|
485
628
|
const slice = node.data.subarray(start, end);
|
|
486
629
|
buffer.set(slice, offset);
|
|
487
|
-
|
|
630
|
+
// The cursor advances by the bytes actually read. Assigning `end` would
|
|
631
|
+
// rewind a cursor already past end-of-file (after `ftruncate`, say) back
|
|
632
|
+
// onto live bytes and make the next sequential write overwrite them.
|
|
633
|
+
if (position === null) entry.position = start + slice.byteLength;
|
|
488
634
|
callback(null, slice.byteLength);
|
|
489
635
|
},
|
|
490
636
|
|
|
@@ -498,7 +644,17 @@ export function createMemFS(): IMemFSHost {
|
|
|
498
644
|
|
|
499
645
|
mkdir(p, _perm, callback) {
|
|
500
646
|
try {
|
|
501
|
-
|
|
647
|
+
const norm = normalize(p);
|
|
648
|
+
if (nodes.has(norm)) throw new MemFSError("EEXIST", "mkdir", norm);
|
|
649
|
+
const parent = nodes.get(parentDir(norm));
|
|
650
|
+
if (!parent) throw new MemFSError("ENOENT", "mkdir", parentDir(norm));
|
|
651
|
+
if (parent.kind !== "dir")
|
|
652
|
+
throw new MemFSError("ENOTDIR", "mkdir", parentDir(norm));
|
|
653
|
+
nodes.set(norm, {
|
|
654
|
+
kind: "dir",
|
|
655
|
+
data: new Uint8Array(),
|
|
656
|
+
mtimeMs: Date.now(),
|
|
657
|
+
});
|
|
502
658
|
callback(null);
|
|
503
659
|
} catch (err) {
|
|
504
660
|
callback(err as NodeJS.ErrnoException);
|
|
@@ -521,9 +677,9 @@ export function createMemFS(): IMemFSHost {
|
|
|
521
677
|
},
|
|
522
678
|
|
|
523
679
|
fstat(fd, callback) {
|
|
524
|
-
// fstat against a pipe end returns a synthetic file-stat
|
|
525
|
-
//
|
|
526
|
-
//
|
|
680
|
+
// fstat against a pipe end returns a synthetic file-stat so a direct
|
|
681
|
+
// JavaScript caller that wraps the fd in a file-like abstraction can
|
|
682
|
+
// populate its stat fields. A pipe end has no node in the tree.
|
|
527
683
|
if (pipes.has(fd)) {
|
|
528
684
|
callback(
|
|
529
685
|
null,
|
|
@@ -544,7 +700,8 @@ export function createMemFS(): IMemFSHost {
|
|
|
544
700
|
return;
|
|
545
701
|
}
|
|
546
702
|
try {
|
|
547
|
-
|
|
703
|
+
if (!entry.node) throw new MemFSError("ENOENT", "fstat", entry.path);
|
|
704
|
+
callback(null, makeStats(entry.node));
|
|
548
705
|
} catch (err) {
|
|
549
706
|
callback(
|
|
550
707
|
err as NodeJS.ErrnoException,
|
|
@@ -713,7 +870,11 @@ export function createMemFS(): IMemFSHost {
|
|
|
713
870
|
callback(new MemFSError("EINVAL", "ftruncate"));
|
|
714
871
|
return;
|
|
715
872
|
}
|
|
716
|
-
|
|
873
|
+
if (!entry.writable) {
|
|
874
|
+
callback(new MemFSError("EBADF", "ftruncate", entry.path));
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const node = entry.node;
|
|
717
878
|
if (!node || node.kind !== "file") {
|
|
718
879
|
callback(new MemFSError("EINVAL", "ftruncate", entry.path));
|
|
719
880
|
return;
|
|
@@ -83,18 +83,21 @@ export interface ITtscApi {
|
|
|
83
83
|
opts: ITtscSnapshotHandle & { file?: string },
|
|
84
84
|
): Promise<ITtscResult>;
|
|
85
85
|
|
|
86
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Return the syntax token touching `position` (byte offset), including
|
|
88
|
+
* punctuation. Returns `{node: null}` for whitespace and comments.
|
|
89
|
+
*/
|
|
87
90
|
getNodeAtPosition(opts: ITtscPositionQuery): Promise<ITtscResult>;
|
|
88
91
|
|
|
89
92
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
+
* Resolve the token at `position` and return the enclosing TypeScript-Go
|
|
94
|
+
* semantic node's printed type string + flags. Returns `{type: null}` when
|
|
95
|
+
* the position has no token or no type-bearing token.
|
|
93
96
|
*/
|
|
94
97
|
getTypeAtPosition(opts: ITtscPositionQuery): Promise<ITtscResult>;
|
|
95
98
|
|
|
96
99
|
/**
|
|
97
|
-
* Resolve the symbol the
|
|
100
|
+
* Resolve the symbol the token at `position` refers to, including up to 16
|
|
98
101
|
* declaration sites. Returns `{symbol: null}` when no symbol is bound.
|
|
99
102
|
*/
|
|
100
103
|
getSymbolAtPosition(opts: ITtscPositionQuery): Promise<ITtscResult>;
|
|
@@ -2,6 +2,6 @@ import type { ITtscNodeInfo } from "./ITtscNodeInfo";
|
|
|
2
2
|
|
|
3
3
|
/** Payload inside `ITtscResult.result` for `getNodeAtPosition`. */
|
|
4
4
|
export interface ITtscNodeAtPositionResult {
|
|
5
|
-
/** `null` when no
|
|
5
|
+
/** `null` when no syntax token touches the position. */
|
|
6
6
|
node: ITtscNodeInfo | null;
|
|
7
7
|
}
|
|
@@ -6,8 +6,10 @@ import type { ITtscFileQuery } from "./ITtscFileQuery";
|
|
|
6
6
|
*/
|
|
7
7
|
export interface ITtscPositionQuery extends ITtscFileQuery {
|
|
8
8
|
/**
|
|
9
|
-
* Byte offset into the file's source text.
|
|
10
|
-
*
|
|
9
|
+
* Byte offset into the file's source text. It must satisfy `0 <= position <
|
|
10
|
+
* sourceText's UTF-8 byte length`; the offset immediately after the final
|
|
11
|
+
* byte is out of range. JS callers with a UTF-16 line/character pair must
|
|
12
|
+
* resolve it to a byte offset first.
|
|
11
13
|
*/
|
|
12
14
|
position: number;
|
|
13
15
|
}
|
|
@@ -2,6 +2,6 @@ import type { ITtscSymbolInfo } from "./ITtscSymbolInfo";
|
|
|
2
2
|
|
|
3
3
|
/** Payload inside `ITtscResult.result` for `getSymbolAtPosition`. */
|
|
4
4
|
export interface ITtscSymbolAtPositionResult {
|
|
5
|
-
/** `null` when
|
|
5
|
+
/** `null` when no touching token has an associated symbol. */
|
|
6
6
|
symbol: ITtscSymbolInfo | null;
|
|
7
7
|
}
|
|
@@ -2,6 +2,6 @@ import type { ITtscTypeInfo } from "./ITtscTypeInfo";
|
|
|
2
2
|
|
|
3
3
|
/** Payload inside `ITtscResult.result` for `getTypeAtPosition`. */
|
|
4
4
|
export interface ITtscTypeAtPositionResult {
|
|
5
|
-
/** `null` when
|
|
5
|
+
/** `null` when no touching token has a type. */
|
|
6
6
|
type: ITtscTypeInfo | null;
|
|
7
7
|
}
|
|
@@ -135,10 +135,20 @@ export interface IWasmExecFS {
|
|
|
135
135
|
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
136
136
|
): void;
|
|
137
137
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
138
|
+
* JavaScript-only pipe emulation. Returns two fds: a read end and a write
|
|
139
|
+
* end.
|
|
140
|
+
*
|
|
141
|
+
* Installing this does **not** make Go's wasm `os.Pipe()` work. Under the
|
|
142
|
+
* pinned Go toolchain neither `GOOS=js` nor `GOOS=wasip1` has pipes:
|
|
143
|
+
* `os.Pipe` returns an `ENOSYS` syscall error and `syscall.Pipe` returns
|
|
144
|
+
* `ENOSYS`, and neither one crosses the `globalThis.fs` bridge, so no
|
|
145
|
+
* JavaScript method is ever consulted. A custom Go/wasm host cannot reach
|
|
146
|
+
* this shim.
|
|
147
|
+
*
|
|
148
|
+
* The host captures wasm output without pipes: process-level writes to fd 1
|
|
149
|
+
* and fd 2 land in `stdout.buffer` and `stderr.buffer` through `writeSync`,
|
|
150
|
+
* and a plugin's output is collected by the Go-side `host.InvokePlugin` into
|
|
151
|
+
* invocation-owned in-process buffers.
|
|
142
152
|
*/
|
|
143
153
|
pipe2(
|
|
144
154
|
flags: number,
|