@ttsc/wasm 0.19.3 → 0.20.1
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/dist/go.mod
CHANGED
|
@@ -8,6 +8,7 @@ go 1.26
|
|
|
8
8
|
// public proxy cannot resolve the in-tree shim modules.
|
|
9
9
|
replace (
|
|
10
10
|
github.com/microsoft/typescript-go/shim/ast => ./shim-vendor/shim/ast
|
|
11
|
+
github.com/microsoft/typescript-go/shim/astnav => ./shim-vendor/shim/astnav
|
|
11
12
|
github.com/microsoft/typescript-go/shim/bundled => ./shim-vendor/shim/bundled
|
|
12
13
|
github.com/microsoft/typescript-go/shim/checker => ./shim-vendor/shim/checker
|
|
13
14
|
github.com/microsoft/typescript-go/shim/compiler => ./shim-vendor/shim/compiler
|
|
@@ -26,6 +27,7 @@ replace (
|
|
|
26
27
|
|
|
27
28
|
require (
|
|
28
29
|
github.com/microsoft/typescript-go/shim/ast v0.0.0
|
|
30
|
+
github.com/microsoft/typescript-go/shim/astnav v0.0.0
|
|
29
31
|
github.com/microsoft/typescript-go/shim/compiler v0.0.0
|
|
30
32
|
github.com/microsoft/typescript-go/shim/scanner v0.0.0
|
|
31
33
|
github.com/samchon/ttsc/packages/ttsc v0.0.0
|
package/dist/ttsc.wasm
CHANGED
|
Binary file
|
package/go.mod
CHANGED
|
@@ -8,6 +8,7 @@ go 1.26
|
|
|
8
8
|
// public proxy cannot resolve the in-tree shim modules.
|
|
9
9
|
replace (
|
|
10
10
|
github.com/microsoft/typescript-go/shim/ast => ./shim-vendor/shim/ast
|
|
11
|
+
github.com/microsoft/typescript-go/shim/astnav => ./shim-vendor/shim/astnav
|
|
11
12
|
github.com/microsoft/typescript-go/shim/bundled => ./shim-vendor/shim/bundled
|
|
12
13
|
github.com/microsoft/typescript-go/shim/checker => ./shim-vendor/shim/checker
|
|
13
14
|
github.com/microsoft/typescript-go/shim/compiler => ./shim-vendor/shim/compiler
|
|
@@ -26,6 +27,7 @@ replace (
|
|
|
26
27
|
|
|
27
28
|
require (
|
|
28
29
|
github.com/microsoft/typescript-go/shim/ast v0.0.0
|
|
30
|
+
github.com/microsoft/typescript-go/shim/astnav v0.0.0
|
|
29
31
|
github.com/microsoft/typescript-go/shim/compiler v0.0.0
|
|
30
32
|
github.com/microsoft/typescript-go/shim/scanner v0.0.0
|
|
31
33
|
github.com/samchon/ttsc/packages/ttsc v0.0.0
|
package/host/fountain.go
CHANGED
|
@@ -28,6 +28,7 @@ import (
|
|
|
28
28
|
"syscall/js"
|
|
29
29
|
|
|
30
30
|
"github.com/microsoft/typescript-go/shim/ast"
|
|
31
|
+
"github.com/microsoft/typescript-go/shim/astnav"
|
|
31
32
|
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
32
33
|
|
|
33
34
|
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
@@ -291,28 +292,31 @@ func jsGetDiagnostics(this js.Value, args []js.Value) any {
|
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
// jsGetNodeAtPosition({handle, path, position}) → Promise<ITtscResult>.
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
// helper does this).
|
|
295
|
+
// Returns the token touching the byte offset, including punctuation.
|
|
296
|
+
// Whitespace and comments return `{node: null}`. JS callers with a UTF-16
|
|
297
|
+
// line/character pair must resolve it to a byte offset first.
|
|
298
298
|
func jsGetNodeAtPosition(this js.Value, args []js.Value) any {
|
|
299
299
|
return withSnapshotPosition(args, func(_ *snapshotEntry, file *ast.SourceFile, pos int) any {
|
|
300
|
-
node :=
|
|
300
|
+
node := tokenAtPosition(file, pos)
|
|
301
301
|
return fountainOK(GetNodeAtPositionResult{Node: nodeInfoOf(node)})
|
|
302
302
|
})
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
// jsGetTypeAtPosition({handle, path, position}) → Promise<ITtscResult>.
|
|
306
|
-
// Resolves the
|
|
307
|
-
//
|
|
308
|
-
//
|
|
306
|
+
// Resolves the touching token, then asks the Program's pinned single checker
|
|
307
|
+
// for the enclosing semantic AST node. Returns `{type: null}` when no token
|
|
308
|
+
// has type semantics, including whitespace, comments, and punctuation.
|
|
309
309
|
func jsGetTypeAtPosition(this js.Value, args []js.Value) any {
|
|
310
310
|
return withSnapshotPosition(args, func(entry *snapshotEntry, file *ast.SourceFile, pos int) any {
|
|
311
|
-
node :=
|
|
312
|
-
if node
|
|
311
|
+
node := tokenAtPosition(file, pos)
|
|
312
|
+
if !hasTypeAtLocation(node) || entry.prog.Checker == nil {
|
|
313
|
+
return fountainOK(GetTypeAtPositionResult{Type: nil})
|
|
314
|
+
}
|
|
315
|
+
semanticNode := ast.GetNodeAtPosition(file, pos, false)
|
|
316
|
+
if semanticNode == nil {
|
|
313
317
|
return fountainOK(GetTypeAtPositionResult{Type: nil})
|
|
314
318
|
}
|
|
315
|
-
t := entry.prog.Checker.GetTypeAtLocation(
|
|
319
|
+
t := entry.prog.Checker.GetTypeAtLocation(semanticNode)
|
|
316
320
|
if t == nil {
|
|
317
321
|
return fountainOK(GetTypeAtPositionResult{Type: nil})
|
|
318
322
|
}
|
|
@@ -326,11 +330,11 @@ func jsGetTypeAtPosition(this js.Value, args []js.Value) any {
|
|
|
326
330
|
}
|
|
327
331
|
|
|
328
332
|
// jsGetSymbolAtPosition({handle, path, position}) → Promise<ITtscResult>.
|
|
329
|
-
// Returns `{symbol: null}` when the
|
|
330
|
-
//
|
|
333
|
+
// Returns `{symbol: null}` when the touching token has no associated symbol,
|
|
334
|
+
// including punctuation, whitespace, and comments.
|
|
331
335
|
func jsGetSymbolAtPosition(this js.Value, args []js.Value) any {
|
|
332
336
|
return withSnapshotPosition(args, func(entry *snapshotEntry, file *ast.SourceFile, pos int) any {
|
|
333
|
-
node :=
|
|
337
|
+
node := tokenAtPosition(file, pos)
|
|
334
338
|
if node == nil || entry.prog.Checker == nil {
|
|
335
339
|
return fountainOK(GetSymbolAtPositionResult{Symbol: nil})
|
|
336
340
|
}
|
|
@@ -374,9 +378,9 @@ func withSnapshot(args []js.Value, fn func(*snapshotEntry, js.Value) any) any {
|
|
|
374
378
|
|
|
375
379
|
// withSnapshotPosition is the shared envelope for the 3 position-bound
|
|
376
380
|
// fountain verbs. Same read-lock lifecycle as withSnapshot, plus parses
|
|
377
|
-
// {path, position} and bounds-checks position against the file length
|
|
378
|
-
//
|
|
379
|
-
//
|
|
381
|
+
// {path, position} and bounds-checks position against the file length. A
|
|
382
|
+
// position must address an existing byte, so the offset at end-of-file is
|
|
383
|
+
// rejected before it can reach AST navigation.
|
|
380
384
|
func withSnapshotPosition(args []js.Value, fn func(*snapshotEntry, *ast.SourceFile, int) any) any {
|
|
381
385
|
return withSnapshot(args, func(entry *snapshotEntry, opts js.Value) any {
|
|
382
386
|
path := stringProp(opts, "path")
|
|
@@ -395,13 +399,38 @@ func withSnapshotPosition(args []js.Value, fn func(*snapshotEntry, *ast.SourceFi
|
|
|
395
399
|
if file == nil {
|
|
396
400
|
return errorResponse(2, fmt.Sprintf("host: file %q not found in snapshot", path))
|
|
397
401
|
}
|
|
398
|
-
if pos
|
|
399
|
-
return errorResponse(2, fmt.Sprintf("host: \"position\" %d
|
|
402
|
+
if pos >= len(file.Text()) {
|
|
403
|
+
return errorResponse(2, fmt.Sprintf("host: \"position\" %d is outside file length %d", pos, len(file.Text())))
|
|
400
404
|
}
|
|
401
405
|
return fn(entry, file, pos)
|
|
402
406
|
})
|
|
403
407
|
}
|
|
404
408
|
|
|
409
|
+
// tokenAtPosition resolves the exact token at pos. GetTouchingToken returns a
|
|
410
|
+
// containing AST node when a gap has no token, so preserve the fountain
|
|
411
|
+
// contract by filtering that fallback out explicitly.
|
|
412
|
+
func tokenAtPosition(file *ast.SourceFile, pos int) *ast.Node {
|
|
413
|
+
node := astnav.GetTouchingToken(file, pos)
|
|
414
|
+
if node == nil || !ast.IsTokenKind(node.Kind) {
|
|
415
|
+
return nil
|
|
416
|
+
}
|
|
417
|
+
return node
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// hasTypeAtLocation mirrors the public checker entrypoint's semantic cases.
|
|
421
|
+
// It deliberately excludes punctuation tokens, whose checker fallback is the
|
|
422
|
+
// error type even though a cursor query has no meaningful type answer.
|
|
423
|
+
func hasTypeAtLocation(node *ast.Node) bool {
|
|
424
|
+
return node != nil && (ast.IsPartOfTypeNode(node) ||
|
|
425
|
+
ast.IsExpressionNode(node) ||
|
|
426
|
+
ast.IsTypeDeclaration(node) ||
|
|
427
|
+
ast.IsTypeDeclarationName(node) ||
|
|
428
|
+
ast.IsBindingElement(node) ||
|
|
429
|
+
ast.IsDeclaration(node) ||
|
|
430
|
+
ast.IsDeclarationNameOrImportPropertyName(node) ||
|
|
431
|
+
ast.IsBindingPattern(node))
|
|
432
|
+
}
|
|
433
|
+
|
|
405
434
|
// resolveSnapshotFile finds a SourceFile by path, accepting either an
|
|
406
435
|
// absolute path or a path relative to the snapshot's cwd.
|
|
407
436
|
func resolveSnapshotFile(entry *snapshotEntry, path string) *ast.SourceFile {
|
package/lib/src/createMemFS.d.ts
CHANGED
|
@@ -7,5 +7,11 @@ import type { IMemFSHost } from "./structures/IMemFSHost";
|
|
|
7
7
|
* `globalThis.fs` before loading `wasm_exec.js`) and convenience helpers
|
|
8
8
|
* (`writeFile`, `readFile`, `mkdirp`, …) for seeding source files and reading
|
|
9
9
|
* compiler output without touching the real filesystem.
|
|
10
|
+
*
|
|
11
|
+
* Every successful mutation leaves a valid tree: `/` stays a directory, each
|
|
12
|
+
* proper ancestor of a node exists and is a directory, and a file has no
|
|
13
|
+
* descendants. An operation that cannot satisfy that throws (`writeFile`,
|
|
14
|
+
* `mkdirp`) or reports a POSIX error through its callback, having changed no
|
|
15
|
+
* node, byte, or descriptor.
|
|
10
16
|
*/
|
|
11
17
|
export declare function createMemFS(): IMemFSHost;
|
package/lib/src/createMemFS.js
CHANGED
|
@@ -55,6 +55,12 @@ function normalize(p) {
|
|
|
55
55
|
* `globalThis.fs` before loading `wasm_exec.js`) and convenience helpers
|
|
56
56
|
* (`writeFile`, `readFile`, `mkdirp`, …) for seeding source files and reading
|
|
57
57
|
* compiler output without touching the real filesystem.
|
|
58
|
+
*
|
|
59
|
+
* Every successful mutation leaves a valid tree: `/` stays a directory, each
|
|
60
|
+
* proper ancestor of a node exists and is a directory, and a file has no
|
|
61
|
+
* descendants. An operation that cannot satisfy that throws (`writeFile`,
|
|
62
|
+
* `mkdirp`) or reports a POSIX error through its callback, having changed no
|
|
63
|
+
* node, byte, or descriptor.
|
|
58
64
|
*/
|
|
59
65
|
function createMemFS() {
|
|
60
66
|
const nodes = new Map();
|
|
@@ -64,8 +70,22 @@ function createMemFS() {
|
|
|
64
70
|
const fdTable = new Map();
|
|
65
71
|
let nextFd = 100;
|
|
66
72
|
// Reserve 1/2 for stdout/stderr writeSync routing.
|
|
67
|
-
fdTable.set(1, {
|
|
68
|
-
|
|
73
|
+
fdTable.set(1, {
|
|
74
|
+
path: "/dev/stdout",
|
|
75
|
+
position: 0,
|
|
76
|
+
readable: false,
|
|
77
|
+
writable: true,
|
|
78
|
+
append: true,
|
|
79
|
+
isStdout: true,
|
|
80
|
+
});
|
|
81
|
+
fdTable.set(2, {
|
|
82
|
+
path: "/dev/stderr",
|
|
83
|
+
position: 0,
|
|
84
|
+
readable: false,
|
|
85
|
+
writable: true,
|
|
86
|
+
append: true,
|
|
87
|
+
isStderr: true,
|
|
88
|
+
});
|
|
69
89
|
const pipes = new Map();
|
|
70
90
|
/**
|
|
71
91
|
* Drain buffered pipe data into `buffer[offset..offset+length]`. Returns
|
|
@@ -99,21 +119,60 @@ function createMemFS() {
|
|
|
99
119
|
reader.callback(null, n);
|
|
100
120
|
}
|
|
101
121
|
}
|
|
102
|
-
/**
|
|
103
|
-
function
|
|
104
|
-
const
|
|
105
|
-
segments.pop();
|
|
122
|
+
/** Root-to-leaf path of every segment of `norm` (`/a/b` → `["/a", "/a/b"]`). */
|
|
123
|
+
function pathChain(norm) {
|
|
124
|
+
const chain = [];
|
|
106
125
|
let cursor = "";
|
|
107
|
-
for (const seg of
|
|
126
|
+
for (const seg of norm.split("/").filter(Boolean)) {
|
|
108
127
|
cursor += "/" + seg;
|
|
109
|
-
|
|
110
|
-
nodes.set(cursor, {
|
|
111
|
-
kind: "dir",
|
|
112
|
-
data: new Uint8Array(),
|
|
113
|
-
mtimeMs: Date.now(),
|
|
114
|
-
});
|
|
115
|
-
}
|
|
128
|
+
chain.push(cursor);
|
|
116
129
|
}
|
|
130
|
+
return chain;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Validate that every path in root-to-leaf `chain` is or can become a
|
|
134
|
+
* directory, and report the ones that do not exist yet.
|
|
135
|
+
*
|
|
136
|
+
* A segment that exists as a file makes everything below it impossible: a
|
|
137
|
+
* file has no descendants, so creating one there would leave a node map that
|
|
138
|
+
* is not a tree. Validation is deliberately separate from creation so every
|
|
139
|
+
* caller can reject before touching anything, and so a rejected operation
|
|
140
|
+
* never leaves half a directory chain behind.
|
|
141
|
+
*/
|
|
142
|
+
function missingDirs(chain, syscall) {
|
|
143
|
+
const missing = [];
|
|
144
|
+
for (const path of chain) {
|
|
145
|
+
const existing = nodes.get(path);
|
|
146
|
+
if (!existing)
|
|
147
|
+
missing.push(path);
|
|
148
|
+
else if (existing.kind !== "dir")
|
|
149
|
+
throw new MemFSError_1.MemFSError("ENOTDIR", syscall, path);
|
|
150
|
+
}
|
|
151
|
+
return missing;
|
|
152
|
+
}
|
|
153
|
+
/** Materialize the directories `missingDirs` reported. */
|
|
154
|
+
function createDirs(paths) {
|
|
155
|
+
for (const path of paths)
|
|
156
|
+
nodes.set(path, {
|
|
157
|
+
kind: "dir",
|
|
158
|
+
data: new Uint8Array(),
|
|
159
|
+
mtimeMs: Date.now(),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Validate that normalized path `norm` may hold a regular file, returning the
|
|
164
|
+
* node already there when one exists.
|
|
165
|
+
*
|
|
166
|
+
* A directory is never silently replaced by a file — including the root,
|
|
167
|
+
* which is a directory node like any other. POSIX answers `EISDIR`, and
|
|
168
|
+
* overwriting the node here would strand every descendant in the map behind a
|
|
169
|
+
* path `readdir` can no longer walk.
|
|
170
|
+
*/
|
|
171
|
+
function assertFileTarget(norm, syscall) {
|
|
172
|
+
const existing = nodes.get(norm);
|
|
173
|
+
if (existing && existing.kind !== "file")
|
|
174
|
+
throw new MemFSError_1.MemFSError("EISDIR", syscall, norm);
|
|
175
|
+
return existing;
|
|
117
176
|
}
|
|
118
177
|
/** Absolute parent directory of a normalized path (`/` for a top-level path). */
|
|
119
178
|
function parentDir(norm) {
|
|
@@ -138,6 +197,42 @@ function createMemFS() {
|
|
|
138
197
|
next.set(data.subarray(0, Math.min(length, data.byteLength)));
|
|
139
198
|
return next;
|
|
140
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Write `view` through descriptor `entry` and return the bytes stored.
|
|
202
|
+
*
|
|
203
|
+
* Three offsets are possible and only one of them is right per call: an
|
|
204
|
+
* `O_APPEND` descriptor always writes at end-of-file, an explicit `position`
|
|
205
|
+
* writes exactly there without disturbing the cursor (POSIX `pwrite`), and
|
|
206
|
+
* `position: null` writes at the cursor and advances it. A write that starts
|
|
207
|
+
* past end-of-file zero-fills the gap rather than silently relocating.
|
|
208
|
+
*
|
|
209
|
+
* A zero-byte write changes nothing at all, so a cursor sitting past
|
|
210
|
+
* end-of-file cannot extend the file by writing nothing into it.
|
|
211
|
+
*/
|
|
212
|
+
function writeThroughDescriptor(entry, view, position, syscall) {
|
|
213
|
+
if (!entry.writable)
|
|
214
|
+
throw new MemFSError_1.MemFSError("EBADF", syscall, entry.path);
|
|
215
|
+
const node = entry.node;
|
|
216
|
+
if (!node)
|
|
217
|
+
throw new MemFSError_1.MemFSError("ENOENT", syscall, entry.path);
|
|
218
|
+
if (node.kind !== "file")
|
|
219
|
+
throw new MemFSError_1.MemFSError("EISDIR", syscall, entry.path);
|
|
220
|
+
const start = entry.append
|
|
221
|
+
? node.data.byteLength
|
|
222
|
+
: (position ?? entry.position);
|
|
223
|
+
if (!Number.isInteger(start) || start < 0)
|
|
224
|
+
throw new MemFSError_1.MemFSError("EINVAL", syscall, entry.path);
|
|
225
|
+
if (view.byteLength === 0)
|
|
226
|
+
return 0;
|
|
227
|
+
const end = start + view.byteLength;
|
|
228
|
+
if (end > node.data.byteLength)
|
|
229
|
+
node.data = resizeFileData(node.data, end);
|
|
230
|
+
node.data.set(view, start);
|
|
231
|
+
node.mtimeMs = Date.now();
|
|
232
|
+
if (position === null || entry.append)
|
|
233
|
+
entry.position = end;
|
|
234
|
+
return view.byteLength;
|
|
235
|
+
}
|
|
141
236
|
/**
|
|
142
237
|
* Move the entire subtree rooted at `src` to `dest`, overwriting any existing
|
|
143
238
|
* `dest` node. Every descendant key is re-parented so no old-prefix node is
|
|
@@ -170,28 +265,26 @@ function createMemFS() {
|
|
|
170
265
|
}
|
|
171
266
|
}
|
|
172
267
|
function mkdirp(p) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
cursor += "/" + seg;
|
|
177
|
-
const existing = nodes.get(cursor);
|
|
178
|
-
if (!existing) {
|
|
179
|
-
nodes.set(cursor, {
|
|
180
|
-
kind: "dir",
|
|
181
|
-
data: new Uint8Array(),
|
|
182
|
-
mtimeMs: Date.now(),
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
else if (existing.kind !== "dir") {
|
|
186
|
-
throw new MemFSError_1.MemFSError("ENOTDIR", "mkdir", cursor);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
268
|
+
// Validate the whole chain before creating any of it: a rejected mkdirp
|
|
269
|
+
// must not leave the prefix it had already walked past behind.
|
|
270
|
+
createDirs(missingDirs(pathChain(normalize(p)), "mkdir"));
|
|
189
271
|
}
|
|
190
272
|
function writeFile(p, data) {
|
|
191
273
|
const norm = normalize(p);
|
|
192
|
-
|
|
274
|
+
// Resolve left to right the way POSIX does: an impossible ancestor is
|
|
275
|
+
// reported before the target, and both are checked before any mutation.
|
|
276
|
+
const missing = missingDirs(pathChain(norm).slice(0, -1), "open");
|
|
277
|
+
const existing = assertFileTarget(norm, "open");
|
|
193
278
|
const bytes = typeof data === "string" ? encoder.encode(data) : new Uint8Array(data);
|
|
194
|
-
|
|
279
|
+
createDirs(missing);
|
|
280
|
+
// Overwriting keeps the same node so descriptors already pointing at this
|
|
281
|
+
// file observe the replacement instead of a detached predecessor.
|
|
282
|
+
if (existing) {
|
|
283
|
+
existing.data = bytes;
|
|
284
|
+
existing.mtimeMs = Date.now();
|
|
285
|
+
}
|
|
286
|
+
else
|
|
287
|
+
nodes.set(norm, { kind: "file", data: bytes, mtimeMs: Date.now() });
|
|
195
288
|
}
|
|
196
289
|
function readFile(p) {
|
|
197
290
|
const node = nodes.get(normalize(p));
|
|
@@ -286,36 +379,20 @@ function createMemFS() {
|
|
|
286
379
|
console.error("[wasm]", line);
|
|
287
380
|
return buf.length;
|
|
288
381
|
}
|
|
289
|
-
// Open-file fds (>= 100):
|
|
290
|
-
//
|
|
382
|
+
// Open-file fds (>= 100): write at the descriptor cursor, the way Node's
|
|
383
|
+
// own `writeSync` without a position does. Browser-hosted tools use this
|
|
384
|
+
// path for ordinary virtual files and explicit outputs.
|
|
385
|
+
//
|
|
386
|
+
// An unknown or already-closed fd throws instead of being diverted into
|
|
387
|
+
// the captured stderr: reporting the byte count of a write nobody
|
|
388
|
+
// performed made the caller continue on a false success and quietly
|
|
389
|
+
// contaminated a diagnostic channel consumers read.
|
|
291
390
|
const entry = fdTable.get(fd);
|
|
292
|
-
if (entry)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const incoming = buf.subarray(0);
|
|
298
|
-
const existing = node.data;
|
|
299
|
-
const next = new Uint8Array(existing.byteLength + incoming.byteLength);
|
|
300
|
-
next.set(existing, 0);
|
|
301
|
-
next.set(incoming, existing.byteLength);
|
|
302
|
-
node.data = next;
|
|
303
|
-
node.mtimeMs = Date.now();
|
|
304
|
-
entry.position = next.byteLength;
|
|
305
|
-
return incoming.byteLength;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
// Unknown fd. Fall back to the stderr buffer so the bytes aren't lost
|
|
309
|
-
// entirely (and surface as a console.error so the regression is
|
|
310
|
-
// visible to whoever's looking).
|
|
311
|
-
stderr.buffer += decoder.decode(buf);
|
|
312
|
-
// eslint-disable-next-line no-console
|
|
313
|
-
console.error("[wasm] writeSync to unknown fd " +
|
|
314
|
-
fd +
|
|
315
|
-
" (" +
|
|
316
|
-
buf.byteLength +
|
|
317
|
-
" bytes); routed to stderr buffer");
|
|
318
|
-
return buf.length;
|
|
391
|
+
if (!entry)
|
|
392
|
+
throw new MemFSError_1.MemFSError("EBADF", "write");
|
|
393
|
+
// subarray(0) is a zero-copy view over the full incoming buffer; the
|
|
394
|
+
// bytes are copied into the node before writeSync returns.
|
|
395
|
+
return writeThroughDescriptor(entry, buf.subarray(0), null, "write");
|
|
319
396
|
},
|
|
320
397
|
write(fd, buf, offset, length, position, callback) {
|
|
321
398
|
try {
|
|
@@ -337,44 +414,88 @@ function createMemFS() {
|
|
|
337
414
|
callback(null, length);
|
|
338
415
|
return;
|
|
339
416
|
}
|
|
340
|
-
|
|
341
|
-
|
|
417
|
+
const view = buf.subarray(offset, offset + length);
|
|
418
|
+
// The stdout/stderr capture buffers are streams, not files: they have
|
|
419
|
+
// no seekable offset, so an explicit position is meaningless on them.
|
|
420
|
+
if (fd === 1 || fd === 2) {
|
|
421
|
+
if (position !== null && position !== 0)
|
|
422
|
+
throw new MemFSError_1.MemFSError("ESPIPE", "write");
|
|
423
|
+
callback(null, this.writeSync(fd, view));
|
|
342
424
|
return;
|
|
343
425
|
}
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
426
|
+
const entry = fdTable.get(fd);
|
|
427
|
+
if (!entry)
|
|
428
|
+
throw new MemFSError_1.MemFSError("EBADF", "write");
|
|
429
|
+
callback(null, writeThroughDescriptor(entry, view, position, "write"));
|
|
347
430
|
}
|
|
348
431
|
catch (err) {
|
|
349
432
|
callback(err, 0);
|
|
350
433
|
}
|
|
351
434
|
},
|
|
435
|
+
// Every rejection happens before the first mutation and before a
|
|
436
|
+
// descriptor is minted, so a refused open leaves neither a half-created
|
|
437
|
+
// node nor a leaked fd behind.
|
|
352
438
|
open(p, flags, _mode, callback) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
439
|
+
try {
|
|
440
|
+
const norm = normalize(p);
|
|
441
|
+
const creating = (flags & (this.constants.O_CREAT ?? 0)) !== 0;
|
|
442
|
+
const exclusive = (flags & (this.constants.O_EXCL ?? 0)) !== 0;
|
|
443
|
+
const truncating = (flags & (this.constants.O_TRUNC ?? 0)) !== 0;
|
|
444
|
+
const appending = (flags & (this.constants.O_APPEND ?? 0)) !== 0;
|
|
445
|
+
const directoryOnly = (flags & (this.constants.O_DIRECTORY ?? 0)) !== 0;
|
|
446
|
+
// The access mode is the low bits of `flags`: absent both means
|
|
447
|
+
// read-only, and `O_RDWR` alongside `O_WRONLY` still grants both.
|
|
448
|
+
const writeOnly = (flags & (this.constants.O_WRONLY ?? 0)) !== 0;
|
|
449
|
+
const readWrite = (flags & (this.constants.O_RDWR ?? 0)) !== 0;
|
|
450
|
+
const writable = writeOnly || readWrite;
|
|
451
|
+
const readable = !writeOnly || readWrite;
|
|
452
|
+
let node = nodes.get(norm);
|
|
453
|
+
if (!node) {
|
|
454
|
+
if (!creating)
|
|
455
|
+
throw new MemFSError_1.MemFSError("ENOENT", "open", norm);
|
|
456
|
+
// `open` can only ever create a regular file, so a caller demanding a
|
|
457
|
+
// directory cannot be satisfied by creating one.
|
|
458
|
+
if (directoryOnly)
|
|
459
|
+
throw new MemFSError_1.MemFSError("ENOTDIR", "open", norm);
|
|
460
|
+
// Creating the missing ancestor chain is the documented job of
|
|
461
|
+
// `writeFile` and `mkdirp`; the low-level `open` never promised it.
|
|
462
|
+
const missing = missingDirs(pathChain(norm).slice(0, -1), "open");
|
|
463
|
+
if (missing.length > 0)
|
|
464
|
+
throw new MemFSError_1.MemFSError("ENOENT", "open", missing[0]);
|
|
465
|
+
node = { kind: "file", data: new Uint8Array(), mtimeMs: Date.now() };
|
|
466
|
+
nodes.set(norm, node);
|
|
360
467
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
468
|
+
else {
|
|
469
|
+
if (creating && exclusive)
|
|
470
|
+
throw new MemFSError_1.MemFSError("EEXIST", "open", norm);
|
|
471
|
+
if (node.kind === "dir") {
|
|
472
|
+
// A directory opens read-only — Go stats the fd and lists the path
|
|
473
|
+
// that way. Writing to it or truncating it would turn it into a
|
|
474
|
+
// file and orphan every descendant.
|
|
475
|
+
if (writable || truncating)
|
|
476
|
+
throw new MemFSError_1.MemFSError("EISDIR", "open", norm);
|
|
477
|
+
}
|
|
478
|
+
else if (directoryOnly)
|
|
479
|
+
throw new MemFSError_1.MemFSError("ENOTDIR", "open", norm);
|
|
480
|
+
}
|
|
481
|
+
if (truncating) {
|
|
482
|
+
node.data = new Uint8Array();
|
|
483
|
+
node.mtimeMs = Date.now();
|
|
484
|
+
}
|
|
485
|
+
const fd = nextFd++;
|
|
486
|
+
fdTable.set(fd, {
|
|
487
|
+
path: norm,
|
|
488
|
+
node,
|
|
489
|
+
position: 0,
|
|
490
|
+
readable,
|
|
491
|
+
writable,
|
|
492
|
+
append: appending,
|
|
366
493
|
});
|
|
494
|
+
callback(null, fd);
|
|
367
495
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if ((flags & (this.constants.O_TRUNC ?? 0)) !== 0) {
|
|
371
|
-
nodes.set(norm, {
|
|
372
|
-
kind: "file",
|
|
373
|
-
data: new Uint8Array(),
|
|
374
|
-
mtimeMs: Date.now(),
|
|
375
|
-
});
|
|
496
|
+
catch (err) {
|
|
497
|
+
callback(err, -1);
|
|
376
498
|
}
|
|
377
|
-
callback(null, fd);
|
|
378
499
|
},
|
|
379
500
|
close(fd, callback) {
|
|
380
501
|
const pipeState = pipes.get(fd);
|
|
@@ -421,17 +542,30 @@ function createMemFS() {
|
|
|
421
542
|
callback(new MemFSError_1.MemFSError("EBADF", "read"), 0);
|
|
422
543
|
return;
|
|
423
544
|
}
|
|
424
|
-
|
|
545
|
+
// A write-only descriptor is not a readable one. POSIX answers EBADF for
|
|
546
|
+
// an operation the descriptor's access mode never granted.
|
|
547
|
+
if (!entry.readable) {
|
|
548
|
+
callback(new MemFSError_1.MemFSError("EBADF", "read", entry.path), 0);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const node = entry.node;
|
|
425
552
|
if (!node || node.kind !== "file") {
|
|
426
553
|
callback(new MemFSError_1.MemFSError("ENOENT", "read", entry.path), 0);
|
|
427
554
|
return;
|
|
428
555
|
}
|
|
429
556
|
const start = position ?? entry.position;
|
|
557
|
+
if (!Number.isInteger(start) || start < 0) {
|
|
558
|
+
callback(new MemFSError_1.MemFSError("EINVAL", "read", entry.path), 0);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
430
561
|
const end = Math.min(start + length, node.data.byteLength);
|
|
431
562
|
const slice = node.data.subarray(start, end);
|
|
432
563
|
buffer.set(slice, offset);
|
|
564
|
+
// The cursor advances by the bytes actually read. Assigning `end` would
|
|
565
|
+
// rewind a cursor already past end-of-file (after `ftruncate`, say) back
|
|
566
|
+
// onto live bytes and make the next sequential write overwrite them.
|
|
433
567
|
if (position === null)
|
|
434
|
-
entry.position =
|
|
568
|
+
entry.position = start + slice.byteLength;
|
|
435
569
|
callback(null, slice.byteLength);
|
|
436
570
|
},
|
|
437
571
|
readdir(p, callback) {
|
|
@@ -444,7 +578,19 @@ function createMemFS() {
|
|
|
444
578
|
},
|
|
445
579
|
mkdir(p, _perm, callback) {
|
|
446
580
|
try {
|
|
447
|
-
|
|
581
|
+
const norm = normalize(p);
|
|
582
|
+
if (nodes.has(norm))
|
|
583
|
+
throw new MemFSError_1.MemFSError("EEXIST", "mkdir", norm);
|
|
584
|
+
const parent = nodes.get(parentDir(norm));
|
|
585
|
+
if (!parent)
|
|
586
|
+
throw new MemFSError_1.MemFSError("ENOENT", "mkdir", parentDir(norm));
|
|
587
|
+
if (parent.kind !== "dir")
|
|
588
|
+
throw new MemFSError_1.MemFSError("ENOTDIR", "mkdir", parentDir(norm));
|
|
589
|
+
nodes.set(norm, {
|
|
590
|
+
kind: "dir",
|
|
591
|
+
data: new Uint8Array(),
|
|
592
|
+
mtimeMs: Date.now(),
|
|
593
|
+
});
|
|
448
594
|
callback(null);
|
|
449
595
|
}
|
|
450
596
|
catch (err) {
|
|
@@ -463,9 +609,9 @@ function createMemFS() {
|
|
|
463
609
|
this.stat(p, callback);
|
|
464
610
|
},
|
|
465
611
|
fstat(fd, callback) {
|
|
466
|
-
// fstat against a pipe end returns a synthetic file-stat
|
|
467
|
-
//
|
|
468
|
-
//
|
|
612
|
+
// fstat against a pipe end returns a synthetic file-stat so a direct
|
|
613
|
+
// JavaScript caller that wraps the fd in a file-like abstraction can
|
|
614
|
+
// populate its stat fields. A pipe end has no node in the tree.
|
|
469
615
|
if (pipes.has(fd)) {
|
|
470
616
|
callback(null, makeStats({
|
|
471
617
|
kind: "file",
|
|
@@ -480,7 +626,9 @@ function createMemFS() {
|
|
|
480
626
|
return;
|
|
481
627
|
}
|
|
482
628
|
try {
|
|
483
|
-
|
|
629
|
+
if (!entry.node)
|
|
630
|
+
throw new MemFSError_1.MemFSError("ENOENT", "fstat", entry.path);
|
|
631
|
+
callback(null, makeStats(entry.node));
|
|
484
632
|
}
|
|
485
633
|
catch (err) {
|
|
486
634
|
callback(err, undefined);
|
|
@@ -644,7 +792,11 @@ function createMemFS() {
|
|
|
644
792
|
callback(new MemFSError_1.MemFSError("EINVAL", "ftruncate"));
|
|
645
793
|
return;
|
|
646
794
|
}
|
|
647
|
-
|
|
795
|
+
if (!entry.writable) {
|
|
796
|
+
callback(new MemFSError_1.MemFSError("EBADF", "ftruncate", entry.path));
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const node = entry.node;
|
|
648
800
|
if (!node || node.kind !== "file") {
|
|
649
801
|
callback(new MemFSError_1.MemFSError("EINVAL", "ftruncate", entry.path));
|
|
650
802
|
return;
|