@ttsc/wasm 0.13.1 → 0.14.0-dev.20260528.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 +61 -1
- package/dist/ttsc.wasm +0 -0
- package/host/api.go +5 -4
- package/host/doc.go +7 -5
- package/host/fountain.go +505 -0
- package/host/host.go +64 -10
- package/host/plugin.go +6 -5
- package/lib/src/MemFSError.d.ts +15 -0
- package/lib/src/MemFSError.js +47 -0
- package/lib/src/MemFSError.js.map +1 -0
- package/lib/src/bootTtsc.d.ts +21 -0
- package/lib/src/bootTtsc.js +180 -0
- package/lib/src/bootTtsc.js.map +1 -0
- package/lib/src/createMemFS.d.ts +11 -0
- package/lib/src/{MemFS.js → createMemFS.js} +26 -61
- package/lib/src/createMemFS.js.map +1 -0
- package/lib/src/index.d.ts +5 -6
- package/lib/src/index.js +19 -8
- package/lib/src/index.js.map +1 -1
- package/lib/src/parseResult.d.ts +9 -0
- package/lib/src/parseResult.js +21 -0
- package/lib/src/parseResult.js.map +1 -0
- package/lib/src/structures/IBootResult.d.ts +9 -0
- package/lib/src/structures/IBootResult.js +3 -0
- package/lib/src/structures/IBootResult.js.map +1 -0
- package/lib/src/structures/IBootTtscOptions.d.ts +21 -0
- package/lib/src/structures/IBootTtscOptions.js +3 -0
- package/lib/src/structures/IBootTtscOptions.js.map +1 -0
- package/lib/src/structures/IFileStats.d.ts +23 -0
- package/lib/src/structures/IFileStats.js +3 -0
- package/lib/src/structures/IFileStats.js.map +1 -0
- package/lib/src/structures/IMemFSHost.d.ts +21 -0
- package/lib/src/structures/IMemFSHost.js +3 -0
- package/lib/src/structures/IMemFSHost.js.map +1 -0
- package/lib/src/structures/ITtscApi.d.ts +86 -0
- package/lib/src/structures/ITtscApi.js +3 -0
- package/lib/src/structures/ITtscApi.js.map +1 -0
- package/lib/src/structures/ITtscBuildOpts.d.ts +7 -0
- package/lib/src/structures/ITtscBuildOpts.js +3 -0
- package/lib/src/structures/ITtscBuildOpts.js.map +1 -0
- package/lib/src/structures/ITtscCompileResult.d.ts +11 -0
- package/lib/src/structures/ITtscCompileResult.js +3 -0
- package/lib/src/structures/ITtscCompileResult.js.map +1 -0
- package/lib/src/structures/ITtscDiagnostic.d.ts +14 -0
- package/lib/src/structures/ITtscDiagnostic.js +3 -0
- package/lib/src/structures/ITtscDiagnostic.js.map +1 -0
- package/lib/src/structures/ITtscFileQuery.d.ts +6 -0
- package/lib/src/structures/ITtscFileQuery.js +3 -0
- package/lib/src/structures/ITtscFileQuery.js.map +1 -0
- package/lib/src/structures/ITtscFountainDiagnosticsResult.d.ts +5 -0
- package/lib/src/structures/ITtscFountainDiagnosticsResult.js +3 -0
- package/lib/src/structures/ITtscFountainDiagnosticsResult.js.map +1 -0
- package/lib/src/structures/ITtscNodeAtPositionResult.d.ts +6 -0
- package/lib/src/structures/ITtscNodeAtPositionResult.js +3 -0
- package/lib/src/structures/ITtscNodeAtPositionResult.js.map +1 -0
- package/lib/src/structures/ITtscNodeInfo.d.ts +13 -0
- package/lib/src/structures/ITtscNodeInfo.js +3 -0
- package/lib/src/structures/ITtscNodeInfo.js.map +1 -0
- package/lib/src/structures/ITtscPluginOpts.d.ts +13 -0
- package/lib/src/structures/ITtscPluginOpts.js +3 -0
- package/lib/src/structures/ITtscPluginOpts.js.map +1 -0
- package/lib/src/structures/ITtscPositionQuery.d.ts +9 -0
- package/lib/src/structures/ITtscPositionQuery.js +3 -0
- package/lib/src/structures/ITtscPositionQuery.js.map +1 -0
- package/lib/src/structures/ITtscReleaseSnapshotResult.d.ts +5 -0
- package/lib/src/structures/ITtscReleaseSnapshotResult.js +3 -0
- package/lib/src/structures/ITtscReleaseSnapshotResult.js.map +1 -0
- package/lib/src/structures/ITtscResult.d.ts +15 -0
- package/lib/src/structures/ITtscResult.js +3 -0
- package/lib/src/structures/ITtscResult.js.map +1 -0
- package/lib/src/structures/ITtscSnapshotHandle.d.ts +5 -0
- package/lib/src/structures/ITtscSnapshotHandle.js +3 -0
- package/lib/src/structures/ITtscSnapshotHandle.js.map +1 -0
- package/lib/src/structures/ITtscSnapshotResult.d.ts +5 -0
- package/lib/src/structures/ITtscSnapshotResult.js +3 -0
- package/lib/src/structures/ITtscSnapshotResult.js.map +1 -0
- package/lib/src/structures/ITtscSnapshotsResult.d.ts +4 -0
- package/lib/src/structures/ITtscSnapshotsResult.js +3 -0
- package/lib/src/structures/ITtscSnapshotsResult.js.map +1 -0
- package/lib/src/structures/ITtscSourceFileTextResult.d.ts +5 -0
- package/lib/src/structures/ITtscSourceFileTextResult.js +3 -0
- package/lib/src/structures/ITtscSourceFileTextResult.js.map +1 -0
- package/lib/src/structures/ITtscSourceFilesResult.d.ts +5 -0
- package/lib/src/structures/ITtscSourceFilesResult.js +3 -0
- package/lib/src/structures/ITtscSourceFilesResult.js.map +1 -0
- package/lib/src/structures/ITtscSymbolAtPositionResult.d.ts +6 -0
- package/lib/src/structures/ITtscSymbolAtPositionResult.js +3 -0
- package/lib/src/structures/ITtscSymbolAtPositionResult.js.map +1 -0
- package/lib/src/structures/ITtscSymbolDeclaration.d.ts +7 -0
- package/lib/src/structures/ITtscSymbolDeclaration.js +3 -0
- package/lib/src/structures/ITtscSymbolDeclaration.js.map +1 -0
- package/lib/src/structures/ITtscSymbolInfo.d.ts +14 -0
- package/lib/src/structures/ITtscSymbolInfo.js +3 -0
- package/lib/src/structures/ITtscSymbolInfo.js.map +1 -0
- package/lib/src/structures/ITtscTransformResult.d.ts +12 -0
- package/lib/src/structures/ITtscTransformResult.js +3 -0
- package/lib/src/structures/ITtscTransformResult.js.map +1 -0
- package/lib/src/structures/ITtscTypeAtPositionResult.d.ts +6 -0
- package/lib/src/structures/ITtscTypeAtPositionResult.js +3 -0
- package/lib/src/structures/ITtscTypeAtPositionResult.js.map +1 -0
- package/lib/src/structures/ITtscTypeInfo.d.ts +7 -0
- package/lib/src/structures/ITtscTypeInfo.js +3 -0
- package/lib/src/structures/ITtscTypeInfo.js.map +1 -0
- package/lib/src/structures/ITtscVersion.d.ts +9 -0
- package/lib/src/structures/ITtscVersion.js +3 -0
- package/lib/src/structures/ITtscVersion.js.map +1 -0
- package/lib/src/{MemFS.d.ts → structures/IWasmExecFS.d.ts} +7 -66
- package/lib/src/structures/IWasmExecFS.js +3 -0
- package/lib/src/structures/IWasmExecFS.js.map +1 -0
- package/lib/src/structures/index.d.ts +29 -0
- package/lib/src/structures/index.js +46 -0
- package/lib/src/structures/index.js.map +1 -0
- package/package.json +2 -2
- package/shim-vendor/shim/ast/lint.go +1 -1
- package/shim-vendor/shim/checker/shim.go +3 -0
- package/shim-vendor/shim/core/shim.go +11 -0
- package/src/MemFSError.ts +43 -0
- package/src/bootTtsc.ts +214 -0
- package/src/{MemFS.ts → createMemFS.ts} +13 -231
- package/src/index.ts +5 -17
- package/src/parseResult.ts +17 -0
- package/src/structures/IBootResult.ts +10 -0
- package/src/structures/IBootTtscOptions.ts +22 -0
- package/src/structures/IFileStats.ts +23 -0
- package/src/structures/IMemFSHost.ts +18 -0
- package/src/structures/ITtscApi.ts +101 -0
- package/src/structures/ITtscBuildOpts.ts +7 -0
- package/src/structures/ITtscCompileResult.ts +12 -0
- package/src/structures/ITtscDiagnostic.ts +14 -0
- package/src/structures/ITtscFileQuery.ts +7 -0
- package/src/structures/ITtscFountainDiagnosticsResult.ts +6 -0
- package/src/structures/ITtscNodeAtPositionResult.ts +7 -0
- package/src/structures/ITtscNodeInfo.ts +13 -0
- package/src/structures/ITtscPluginOpts.ts +13 -0
- package/src/structures/ITtscPositionQuery.ts +10 -0
- package/src/structures/ITtscReleaseSnapshotResult.ts +5 -0
- package/src/structures/ITtscResult.ts +15 -0
- package/src/structures/ITtscSnapshotHandle.ts +5 -0
- package/src/structures/ITtscSnapshotResult.ts +5 -0
- package/src/structures/ITtscSnapshotsResult.ts +4 -0
- package/src/structures/ITtscSourceFileTextResult.ts +5 -0
- package/src/structures/ITtscSourceFilesResult.ts +5 -0
- package/src/structures/ITtscSymbolAtPositionResult.ts +7 -0
- package/src/structures/ITtscSymbolDeclaration.ts +7 -0
- package/src/structures/ITtscSymbolInfo.ts +15 -0
- package/src/structures/ITtscTransformResult.ts +13 -0
- package/src/structures/ITtscTypeAtPositionResult.ts +7 -0
- package/src/structures/ITtscTypeInfo.ts +7 -0
- package/src/structures/ITtscVersion.ts +9 -0
- package/src/structures/IWasmExecFS.ts +150 -0
- package/src/structures/index.ts +29 -0
- package/lib/src/MemFS.js.map +0 -1
- package/lib/src/api.d.ts +0 -120
- package/lib/src/api.js +0 -23
- package/lib/src/api.js.map +0 -1
- package/lib/src/instantiate.d.ts +0 -30
- package/lib/src/instantiate.js +0 -88
- package/lib/src/instantiate.js.map +0 -1
- package/src/api.ts +0 -145
- package/src/instantiate.ts +0 -137
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem error with a POSIX error code and numeric `errno`.
|
|
3
|
+
*
|
|
4
|
+
* Matches the shape of `NodeJS.ErrnoException` so Go's `os` package interprets
|
|
5
|
+
* it as a proper `os.PathError` with a numeric error code. Thrown by the
|
|
6
|
+
* MemFS implementation when a callback would otherwise pass `null` for an
|
|
7
|
+
* impossible filesystem operation (missing path, type mismatch, …).
|
|
8
|
+
*/
|
|
9
|
+
export class MemFSError extends Error {
|
|
10
|
+
public code: string;
|
|
11
|
+
public errno: number;
|
|
12
|
+
public path?: string;
|
|
13
|
+
public syscall?: string;
|
|
14
|
+
constructor(code: string, syscall: string, path?: string) {
|
|
15
|
+
super(`${code}: ${syscall} ${path ?? ""}`.trim());
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.errno = errnoForCode(code);
|
|
18
|
+
this.path = path;
|
|
19
|
+
this.syscall = syscall;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Map a POSIX error name to its Linux numeric errno (negative by convention). */
|
|
24
|
+
function errnoForCode(code: string): number {
|
|
25
|
+
switch (code) {
|
|
26
|
+
case "ENOENT":
|
|
27
|
+
return -2;
|
|
28
|
+
case "EBADF":
|
|
29
|
+
return -9;
|
|
30
|
+
case "EEXIST":
|
|
31
|
+
return -17;
|
|
32
|
+
case "ENOTDIR":
|
|
33
|
+
return -20;
|
|
34
|
+
case "EISDIR":
|
|
35
|
+
return -21;
|
|
36
|
+
case "EINVAL":
|
|
37
|
+
return -22;
|
|
38
|
+
case "ESPIPE":
|
|
39
|
+
return -29;
|
|
40
|
+
default:
|
|
41
|
+
return -1;
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/bootTtsc.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Boots a host-built wasm and returns the JS-side handle.
|
|
2
|
+
//
|
|
3
|
+
// Order of operations matters: wasm_exec.js installs default no-op fs/process
|
|
4
|
+
// shims if `globalThis.fs` is missing at load time, so we must install our
|
|
5
|
+
// MemFS BEFORE importing wasm_exec.js.
|
|
6
|
+
//
|
|
7
|
+
// The boot helper is parameterized by `apiName` so any wasm built with
|
|
8
|
+
// `host.Expose(...)` can be loaded the same way. The base wasm uses "ttsc";
|
|
9
|
+
// downstream consumers pick their own (e.g. "ttscPlayground", "ttscTypia").
|
|
10
|
+
|
|
11
|
+
import { createMemFS } from "./createMemFS";
|
|
12
|
+
import type { IBootResult } from "./structures/IBootResult";
|
|
13
|
+
import type { IBootTtscOptions } from "./structures/IBootTtscOptions";
|
|
14
|
+
import type { ITtscApi } from "./structures/ITtscApi";
|
|
15
|
+
|
|
16
|
+
declare const importScripts: (...urls: string[]) => void;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Per-(apiName, wasmUrl) single-flight cache for boots. Keying on apiName
|
|
20
|
+
* alone would let a second call with the same apiName but a different
|
|
21
|
+
* wasmUrl silently return the cached IBootResult of the first wasm — the
|
|
22
|
+
* caller would think they booted a fresh binary while the cached one
|
|
23
|
+
* stayed in place. The composite key lets HMR / cache-busting query
|
|
24
|
+
* strings get a fresh boot while still single-flighting genuine concurrent
|
|
25
|
+
* duplicate calls.
|
|
26
|
+
*/
|
|
27
|
+
const bootsInFlight = new Map<string, Promise<IBootResult>>();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Per-apiName serialization chain. Two concurrent boots with the same
|
|
31
|
+
* apiName but different wasmUrls each install their own `globalThis[apiName
|
|
32
|
+
* + "Ready"]` resolver — they would race and the second would overwrite
|
|
33
|
+
* the first, stranding the first boot's await. The chain serializes them
|
|
34
|
+
* so one boot's Go-side `Ready.Invoke()` always lands on the resolver
|
|
35
|
+
* that boot installed.
|
|
36
|
+
*/
|
|
37
|
+
const bootChainByApiName = new Map<string, Promise<unknown>>();
|
|
38
|
+
|
|
39
|
+
function bootKey(apiName: string, wasmUrl: string): string {
|
|
40
|
+
return `${apiName}|${resolveWasmUrl(wasmUrl)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve `wasmUrl` against the current document base before keying so
|
|
45
|
+
* that `./playground.wasm`, `/compiler/playground.wasm`, and the fully
|
|
46
|
+
* qualified absolute href all collapse to the same cache entry instead
|
|
47
|
+
* of spawning duplicate boots. Falls back to the raw string when no
|
|
48
|
+
* base is available (Node-side tests, non-DOM workers).
|
|
49
|
+
*/
|
|
50
|
+
function resolveWasmUrl(wasmUrl: string): string {
|
|
51
|
+
try {
|
|
52
|
+
const base =
|
|
53
|
+
typeof location !== "undefined" ? location.href : "http://local/";
|
|
54
|
+
return new URL(wasmUrl, base).href;
|
|
55
|
+
} catch {
|
|
56
|
+
return wasmUrl;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Boot a host-built wasm. Re-entrant only if you reuse the same `host`.
|
|
62
|
+
*
|
|
63
|
+
* Concurrent calls with the same `(apiName, wasmUrl)` pair share the same
|
|
64
|
+
* in-flight boot. Calls with the same `apiName` but different `wasmUrl`
|
|
65
|
+
* are serialized via `bootChainByApiName` so they don't race on the
|
|
66
|
+
* shared `globalThis[apiName+"Ready"]` resolver slot. On rejection the
|
|
67
|
+
* cache entries are cleared so the next call retries from scratch.
|
|
68
|
+
*
|
|
69
|
+
* **Single-Worker caveat.** Even with the chain, a second boot loaded
|
|
70
|
+
* into the SAME Worker after a first boot completes will overlay its
|
|
71
|
+
* Go runtime on top of the first — `importScripts(wasmExecUrl)` rebinds
|
|
72
|
+
* `globalThis.Go`, and the first wasm's keepalive goroutine keeps
|
|
73
|
+
* running through the new runtime's js-bridge tables. The serialization
|
|
74
|
+
* is sufficient for the typical use case (one boot per Worker over the
|
|
75
|
+
* page's lifetime) but DOES NOT make a Worker safe to host two wasm
|
|
76
|
+
* instances at once. Create a fresh Worker per concurrent wasm.
|
|
77
|
+
*/
|
|
78
|
+
export function bootTtsc(options: IBootTtscOptions): Promise<IBootResult> {
|
|
79
|
+
const apiName = options.apiName ?? "ttsc";
|
|
80
|
+
const key = bootKey(apiName, options.wasmUrl);
|
|
81
|
+
const inflight = bootsInFlight.get(key);
|
|
82
|
+
if (inflight) return inflight;
|
|
83
|
+
const prior = bootChainByApiName.get(apiName) ?? Promise.resolve();
|
|
84
|
+
const promise = prior
|
|
85
|
+
.catch(() => {
|
|
86
|
+
/* don't propagate a prior boot's rejection — let this one run */
|
|
87
|
+
})
|
|
88
|
+
.then(() => bootTtscOnce(options, apiName))
|
|
89
|
+
.catch((err) => {
|
|
90
|
+
bootsInFlight.delete(key);
|
|
91
|
+
throw err;
|
|
92
|
+
});
|
|
93
|
+
bootsInFlight.set(key, promise);
|
|
94
|
+
// Track the chain head for this apiName so the next boot waits on it.
|
|
95
|
+
// Swallow on the chain head specifically: we already throw to the
|
|
96
|
+
// immediate caller; surfacing it through the chain would reject every
|
|
97
|
+
// subsequent boot just because this one failed.
|
|
98
|
+
bootChainByApiName.set(
|
|
99
|
+
apiName,
|
|
100
|
+
promise.catch(() => {}),
|
|
101
|
+
);
|
|
102
|
+
return promise;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function bootTtscOnce(
|
|
106
|
+
options: IBootTtscOptions,
|
|
107
|
+
apiName: string,
|
|
108
|
+
): Promise<IBootResult> {
|
|
109
|
+
const wasmUrl = options.wasmUrl;
|
|
110
|
+
const wasmExecUrl = options.wasmExecUrl ?? defaultWasmExecUrl(wasmUrl);
|
|
111
|
+
|
|
112
|
+
const host = options.host ?? createMemFS();
|
|
113
|
+
const globalAny = globalThis as Record<string, unknown>;
|
|
114
|
+
// Only install fs / process if they aren't already in place. A caller
|
|
115
|
+
// booting a second wasm over the same MemFS reuses the same shims.
|
|
116
|
+
if (!globalAny.fs) globalAny.fs = host.fs;
|
|
117
|
+
if (!globalAny.process) globalAny.process = createProcessShim();
|
|
118
|
+
|
|
119
|
+
// wasm_exec.js installs `globalThis.Go`. It also reads globalThis.fs at
|
|
120
|
+
// module-eval time, so this import must follow the assignment above.
|
|
121
|
+
importScripts(wasmExecUrl);
|
|
122
|
+
|
|
123
|
+
// Race the Ready resolver against a Failed signal so a wasm-side fault
|
|
124
|
+
// (e.g. `host.Expose` refusing a duplicate call) surfaces here instead of
|
|
125
|
+
// hanging on `await ready` forever. `go.run` is fire-and-forget so its
|
|
126
|
+
// own rejection cannot reach this promise without an explicit channel.
|
|
127
|
+
const ready = new Promise<void>((resolve, reject) => {
|
|
128
|
+
globalAny[apiName + "Ready"] = () => {
|
|
129
|
+
delete globalAny[apiName + "Failed"];
|
|
130
|
+
resolve();
|
|
131
|
+
};
|
|
132
|
+
globalAny[apiName + "Failed"] = (err: unknown) => {
|
|
133
|
+
delete globalAny[apiName + "Ready"];
|
|
134
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const goCtor = (globalAny as { Go?: new () => IGoInstance }).Go;
|
|
139
|
+
if (typeof goCtor !== "function") {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`bootTtsc: globalThis.Go was not installed by ${wasmExecUrl} — the file may not have loaded (CSP block, wrong content type, 404), or it is not the wasm_exec.js shipped with the Go toolchain.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const go = new goCtor();
|
|
145
|
+
|
|
146
|
+
const response = await fetch(wasmUrl);
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
throw new Error(`bootTtsc: failed to fetch ${wasmUrl}: ${response.status}`);
|
|
149
|
+
}
|
|
150
|
+
const wasm = await WebAssembly.instantiateStreaming(
|
|
151
|
+
response,
|
|
152
|
+
go.importObject,
|
|
153
|
+
);
|
|
154
|
+
// go.run never resolves until the wasm exits; we don't await it.
|
|
155
|
+
void go.run(wasm.instance);
|
|
156
|
+
await ready;
|
|
157
|
+
|
|
158
|
+
const api = (globalAny as Record<string, ITtscApi | undefined>)[apiName];
|
|
159
|
+
if (!api)
|
|
160
|
+
throw new Error(
|
|
161
|
+
`bootTtsc: ${apiName} global was not set — was the wasm built with host.Expose(${JSON.stringify(apiName)}, ...)?`,
|
|
162
|
+
);
|
|
163
|
+
return { api, host };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Derive the `wasm_exec.js` URL from the wasm URL by replacing the filename.
|
|
168
|
+
*
|
|
169
|
+
* If `wasmUrl` has no directory component, returns `"wasm_exec.js"` (same
|
|
170
|
+
* directory as the caller's base URL).
|
|
171
|
+
*/
|
|
172
|
+
function defaultWasmExecUrl(wasmUrl: string): string {
|
|
173
|
+
const slash = wasmUrl.lastIndexOf("/");
|
|
174
|
+
if (slash < 0) return "wasm_exec.js";
|
|
175
|
+
return wasmUrl.slice(0, slash + 1) + "wasm_exec.js";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Minimal shape of the `Go` constructor that `wasm_exec.js` exports on
|
|
180
|
+
* `globalThis`. Only the members we actually use are typed here.
|
|
181
|
+
*/
|
|
182
|
+
interface IGoInstance {
|
|
183
|
+
importObject: WebAssembly.Imports;
|
|
184
|
+
run(instance: WebAssembly.Instance): Promise<void>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Minimal `process` shim required by `wasm_exec.js` in non-Node environments.
|
|
189
|
+
*
|
|
190
|
+
* Go's js/wasm bridge reads `process.pid`, `process.ppid`, and calls
|
|
191
|
+
* `process.cwd()`. `getuid`/`getgid` and friends return `-1` (root-less).
|
|
192
|
+
* `umask` and `getgroups` are never exercised by the compiler but are
|
|
193
|
+
* included for completeness so unexpected calls surface as clear errors.
|
|
194
|
+
*/
|
|
195
|
+
function createProcessShim(): Record<string, unknown> {
|
|
196
|
+
return {
|
|
197
|
+
getuid: () => -1,
|
|
198
|
+
getgid: () => -1,
|
|
199
|
+
geteuid: () => -1,
|
|
200
|
+
getegid: () => -1,
|
|
201
|
+
getgroups: () => {
|
|
202
|
+
throw new Error("not implemented");
|
|
203
|
+
},
|
|
204
|
+
pid: -1,
|
|
205
|
+
ppid: -1,
|
|
206
|
+
umask: () => {
|
|
207
|
+
throw new Error("not implemented");
|
|
208
|
+
},
|
|
209
|
+
cwd: () => "/",
|
|
210
|
+
chdir: () => {
|
|
211
|
+
/* no-op; the workspace lives inside the MemFS */
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -11,8 +11,14 @@
|
|
|
11
11
|
// Errors carry a `code` (e.g. ENOENT, EBADF) so Go's os package sees them as
|
|
12
12
|
// proper os.PathError values.
|
|
13
13
|
//
|
|
14
|
-
// Files are stored as Uint8Array.
|
|
15
|
-
//
|
|
14
|
+
// Files are stored as Uint8Array. String input passed to writeFile is encoded,
|
|
15
|
+
// byte input is copied, and readFile returns a copy so callers cannot mutate
|
|
16
|
+
// stored filesystem state by keeping a reference.
|
|
17
|
+
|
|
18
|
+
import { MemFSError } from "./MemFSError";
|
|
19
|
+
import type { IFileStats } from "./structures/IFileStats";
|
|
20
|
+
import type { IMemFSHost } from "./structures/IMemFSHost";
|
|
21
|
+
import type { IWasmExecFS } from "./structures/IWasmExecFS";
|
|
16
22
|
|
|
17
23
|
/** Mode bits exposed by stat. We only differentiate file vs directory. */
|
|
18
24
|
const S_IFDIR = 0o040000;
|
|
@@ -20,177 +26,6 @@ const S_IFREG = 0o100000;
|
|
|
20
26
|
const DEFAULT_FILE_MODE = S_IFREG | 0o644;
|
|
21
27
|
const DEFAULT_DIR_MODE = S_IFDIR | 0o755;
|
|
22
28
|
|
|
23
|
-
/**
|
|
24
|
-
* Subset of the Node.js `fs` module that `wasm_exec.js` calls into.
|
|
25
|
-
*
|
|
26
|
-
* Go's js/wasm runtime routes all `syscall/js` filesystem operations through
|
|
27
|
-
* `globalThis.fs`. In a browser there is no real `fs`, so a MemFS
|
|
28
|
-
* implementation fulfils this interface. Only the operations that
|
|
29
|
-
* typescript-go's compiler exercises are required; the rest are no-ops or
|
|
30
|
-
* return `EPERM`/`EINVAL`.
|
|
31
|
-
*/
|
|
32
|
-
export interface IWasmExecFS {
|
|
33
|
-
constants: Record<string, number>;
|
|
34
|
-
writeSync(fd: number, buf: Uint8Array): number;
|
|
35
|
-
write(
|
|
36
|
-
fd: number,
|
|
37
|
-
buf: Uint8Array,
|
|
38
|
-
offset: number,
|
|
39
|
-
length: number,
|
|
40
|
-
position: number | null,
|
|
41
|
-
callback: (err: NodeJS.ErrnoException | null, n: number) => void,
|
|
42
|
-
): void;
|
|
43
|
-
open(
|
|
44
|
-
path: string,
|
|
45
|
-
flags: number,
|
|
46
|
-
mode: number,
|
|
47
|
-
callback: (err: NodeJS.ErrnoException | null, fd: number) => void,
|
|
48
|
-
): void;
|
|
49
|
-
close(
|
|
50
|
-
fd: number,
|
|
51
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
52
|
-
): void;
|
|
53
|
-
read(
|
|
54
|
-
fd: number,
|
|
55
|
-
buffer: Uint8Array,
|
|
56
|
-
offset: number,
|
|
57
|
-
length: number,
|
|
58
|
-
position: number | null,
|
|
59
|
-
callback: (err: NodeJS.ErrnoException | null, n: number) => void,
|
|
60
|
-
): void;
|
|
61
|
-
readdir(
|
|
62
|
-
path: string,
|
|
63
|
-
callback: (err: NodeJS.ErrnoException | null, entries: string[]) => void,
|
|
64
|
-
): void;
|
|
65
|
-
mkdir(
|
|
66
|
-
path: string,
|
|
67
|
-
perm: number,
|
|
68
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
69
|
-
): void;
|
|
70
|
-
stat(
|
|
71
|
-
path: string,
|
|
72
|
-
callback: (err: NodeJS.ErrnoException | null, stats: IFileStats) => void,
|
|
73
|
-
): void;
|
|
74
|
-
lstat(
|
|
75
|
-
path: string,
|
|
76
|
-
callback: (err: NodeJS.ErrnoException | null, stats: IFileStats) => void,
|
|
77
|
-
): void;
|
|
78
|
-
fstat(
|
|
79
|
-
fd: number,
|
|
80
|
-
callback: (err: NodeJS.ErrnoException | null, stats: IFileStats) => void,
|
|
81
|
-
): void;
|
|
82
|
-
fsync(
|
|
83
|
-
fd: number,
|
|
84
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
85
|
-
): void;
|
|
86
|
-
unlink(
|
|
87
|
-
path: string,
|
|
88
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
89
|
-
): void;
|
|
90
|
-
rename(
|
|
91
|
-
from: string,
|
|
92
|
-
to: string,
|
|
93
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
94
|
-
): void;
|
|
95
|
-
rmdir(
|
|
96
|
-
path: string,
|
|
97
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
98
|
-
): void;
|
|
99
|
-
chmod(
|
|
100
|
-
path: string,
|
|
101
|
-
mode: number,
|
|
102
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
103
|
-
): void;
|
|
104
|
-
fchmod(
|
|
105
|
-
fd: number,
|
|
106
|
-
mode: number,
|
|
107
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
108
|
-
): void;
|
|
109
|
-
chown(
|
|
110
|
-
path: string,
|
|
111
|
-
uid: number,
|
|
112
|
-
gid: number,
|
|
113
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
114
|
-
): void;
|
|
115
|
-
fchown(
|
|
116
|
-
fd: number,
|
|
117
|
-
uid: number,
|
|
118
|
-
gid: number,
|
|
119
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
120
|
-
): void;
|
|
121
|
-
lchown(
|
|
122
|
-
path: string,
|
|
123
|
-
uid: number,
|
|
124
|
-
gid: number,
|
|
125
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
126
|
-
): void;
|
|
127
|
-
utimes(
|
|
128
|
-
path: string,
|
|
129
|
-
atime: number,
|
|
130
|
-
mtime: number,
|
|
131
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
132
|
-
): void;
|
|
133
|
-
link(
|
|
134
|
-
path: string,
|
|
135
|
-
link: string,
|
|
136
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
137
|
-
): void;
|
|
138
|
-
symlink(
|
|
139
|
-
path: string,
|
|
140
|
-
link: string,
|
|
141
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
142
|
-
): void;
|
|
143
|
-
readlink(
|
|
144
|
-
path: string,
|
|
145
|
-
callback: (err: NodeJS.ErrnoException | null, link: string) => void,
|
|
146
|
-
): void;
|
|
147
|
-
truncate(
|
|
148
|
-
path: string,
|
|
149
|
-
length: number,
|
|
150
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
151
|
-
): void;
|
|
152
|
-
ftruncate(
|
|
153
|
-
fd: number,
|
|
154
|
-
length: number,
|
|
155
|
-
callback: (err: NodeJS.ErrnoException | null) => void,
|
|
156
|
-
): void;
|
|
157
|
-
// pipe2 is what Go's wasm `os.Pipe()` calls. Returns two fds: a read end
|
|
158
|
-
// and a write end. Used by the playground's host.runWithCapturedIO to
|
|
159
|
-
// capture plugin stdout/stderr; without it the plugin's typia transform
|
|
160
|
-
// output (multi-hundred-KB JSON) silently vanishes and Execute fails.
|
|
161
|
-
pipe2(
|
|
162
|
-
flags: number,
|
|
163
|
-
callback: (
|
|
164
|
-
err: NodeJS.ErrnoException | null,
|
|
165
|
-
fds: [number, number],
|
|
166
|
-
) => void,
|
|
167
|
-
): void;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Stat-like object returned by `stat`, `lstat`, and `fstat`.
|
|
172
|
-
*
|
|
173
|
-
* Mirrors the subset of `fs.Stats` that `wasm_exec.js` reads. Fields not
|
|
174
|
-
* relevant to Go's `os.FileInfo` (e.g. ownership) are zeroed.
|
|
175
|
-
*/
|
|
176
|
-
export interface IFileStats {
|
|
177
|
-
isDirectory(): boolean;
|
|
178
|
-
isFile(): boolean;
|
|
179
|
-
size: number;
|
|
180
|
-
mode: number;
|
|
181
|
-
mtimeMs: number;
|
|
182
|
-
atimeMs: number;
|
|
183
|
-
ctimeMs: number;
|
|
184
|
-
dev: number;
|
|
185
|
-
ino: number;
|
|
186
|
-
nlink: number;
|
|
187
|
-
uid: number;
|
|
188
|
-
gid: number;
|
|
189
|
-
rdev: number;
|
|
190
|
-
blksize: number;
|
|
191
|
-
blocks: number;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
29
|
/** Internal filesystem tree node. Directories carry an empty `data` buffer. */
|
|
195
30
|
interface INode {
|
|
196
31
|
kind: "file" | "dir";
|
|
@@ -198,61 +33,6 @@ interface INode {
|
|
|
198
33
|
mtimeMs: number;
|
|
199
34
|
}
|
|
200
35
|
|
|
201
|
-
/**
|
|
202
|
-
* Filesystem error with a POSIX error code and numeric `errno`.
|
|
203
|
-
*
|
|
204
|
-
* Matches the shape of `NodeJS.ErrnoException` so Go's os package interprets it
|
|
205
|
-
* as a proper `os.PathError` with a numeric error code.
|
|
206
|
-
*/
|
|
207
|
-
export class MemFSError extends Error {
|
|
208
|
-
public code: string;
|
|
209
|
-
public errno: number;
|
|
210
|
-
public path?: string;
|
|
211
|
-
public syscall?: string;
|
|
212
|
-
constructor(code: string, syscall: string, path?: string) {
|
|
213
|
-
super(`${code}: ${syscall} ${path ?? ""}`.trim());
|
|
214
|
-
this.code = code;
|
|
215
|
-
this.errno = errnoForCode(code);
|
|
216
|
-
this.path = path;
|
|
217
|
-
this.syscall = syscall;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** Map a POSIX error name to its Linux numeric errno (negative by convention). */
|
|
222
|
-
function errnoForCode(code: string): number {
|
|
223
|
-
switch (code) {
|
|
224
|
-
case "ENOENT":
|
|
225
|
-
return -2;
|
|
226
|
-
case "EBADF":
|
|
227
|
-
return -9;
|
|
228
|
-
case "EEXIST":
|
|
229
|
-
return -17;
|
|
230
|
-
case "ENOTDIR":
|
|
231
|
-
return -20;
|
|
232
|
-
case "EISDIR":
|
|
233
|
-
return -21;
|
|
234
|
-
default:
|
|
235
|
-
return -1;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Handle returned by `createMemFS`. Provides the `fs` shim to install on
|
|
241
|
-
* `globalThis` plus convenience methods for seeding the virtual filesystem
|
|
242
|
-
* before booting the wasm.
|
|
243
|
-
*/
|
|
244
|
-
export interface IMemFSHost {
|
|
245
|
-
fs: IWasmExecFS;
|
|
246
|
-
writeFile(path: string, data: string | Uint8Array): void;
|
|
247
|
-
readFile(path: string): Uint8Array | null;
|
|
248
|
-
readFileText(path: string): string | null;
|
|
249
|
-
exists(path: string): boolean;
|
|
250
|
-
mkdirp(path: string): void;
|
|
251
|
-
stdout: { buffer: string };
|
|
252
|
-
stderr: { buffer: string };
|
|
253
|
-
resetStdio(): void;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
36
|
const encoder = new TextEncoder();
|
|
257
37
|
const decoder = new TextDecoder();
|
|
258
38
|
|
|
@@ -402,14 +182,15 @@ export function createMemFS(): IMemFSHost {
|
|
|
402
182
|
function writeFile(p: string, data: string | Uint8Array): void {
|
|
403
183
|
const norm = normalize(p);
|
|
404
184
|
ensureParentDirs(norm);
|
|
405
|
-
const bytes =
|
|
185
|
+
const bytes =
|
|
186
|
+
typeof data === "string" ? encoder.encode(data) : new Uint8Array(data);
|
|
406
187
|
nodes.set(norm, { kind: "file", data: bytes, mtimeMs: Date.now() });
|
|
407
188
|
}
|
|
408
189
|
|
|
409
190
|
function readFile(p: string): Uint8Array | null {
|
|
410
191
|
const node = nodes.get(normalize(p));
|
|
411
192
|
if (!node || node.kind !== "file") return null;
|
|
412
|
-
return node.data;
|
|
193
|
+
return new Uint8Array(node.data);
|
|
413
194
|
}
|
|
414
195
|
|
|
415
196
|
function readFileText(p: string): string | null {
|
|
@@ -509,7 +290,8 @@ export function createMemFS(): IMemFSHost {
|
|
|
509
290
|
if (entry) {
|
|
510
291
|
const node = nodes.get(entry.path);
|
|
511
292
|
if (node && node.kind === "file") {
|
|
512
|
-
// subarray(0) is a zero-copy view over the full incoming buffer
|
|
293
|
+
// subarray(0) is a zero-copy view over the full incoming buffer; the
|
|
294
|
+
// bytes are copied into `next` before writeSync returns.
|
|
513
295
|
const incoming = buf.subarray(0);
|
|
514
296
|
const existing = node.data;
|
|
515
297
|
const next = new Uint8Array(
|
package/src/index.ts
CHANGED
|
@@ -5,20 +5,8 @@
|
|
|
5
5
|
// (`host/`) is shipped alongside on disk but loaded via `go build`, not
|
|
6
6
|
// imported from JS.
|
|
7
7
|
|
|
8
|
-
export
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
export
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
export type {
|
|
15
|
-
ITtscApi,
|
|
16
|
-
ITtscVersion,
|
|
17
|
-
ITtscBuildOpts,
|
|
18
|
-
ITtscPluginOpts,
|
|
19
|
-
ITtscResult,
|
|
20
|
-
ITtscCompileResult,
|
|
21
|
-
ITtscTransformResult,
|
|
22
|
-
ITtscDiagnostic,
|
|
23
|
-
} from "./api";
|
|
24
|
-
export { parseResult } from "./api";
|
|
8
|
+
export * from "./bootTtsc";
|
|
9
|
+
export * from "./createMemFS";
|
|
10
|
+
export * from "./MemFSError";
|
|
11
|
+
export * from "./parseResult";
|
|
12
|
+
export * from "./structures/index";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ITtscResult } from "./structures/ITtscResult";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse the `result` field of an ITtscResult into the structured payload.
|
|
5
|
+
*
|
|
6
|
+
* The wasm returns JSON as a string because `js.ValueOf` does not handle
|
|
7
|
+
* large nested maps efficiently. Callers JSON.parse exactly once at the
|
|
8
|
+
* boundary.
|
|
9
|
+
*/
|
|
10
|
+
export function parseResult<T>(result: ITtscResult): T | null {
|
|
11
|
+
if (!result.result) return null;
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(result.result) as T;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IMemFSHost } from "./IMemFSHost";
|
|
2
|
+
import type { ITtscApi } from "./ITtscApi";
|
|
3
|
+
|
|
4
|
+
/** Handle returned by `bootTtsc` once the wasm is ready. */
|
|
5
|
+
export interface IBootResult {
|
|
6
|
+
/** The typed API proxy bound by the wasm to `globalThis[apiName]`. */
|
|
7
|
+
api: ITtscApi;
|
|
8
|
+
/** The MemFS instance shared with the wasm's virtual filesystem. */
|
|
9
|
+
host: IMemFSHost;
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IMemFSHost } from "./IMemFSHost";
|
|
2
|
+
|
|
3
|
+
/** Options for `bootTtsc`. All fields except `wasmUrl` have sensible defaults. */
|
|
4
|
+
export interface IBootTtscOptions {
|
|
5
|
+
/** URL of the .wasm to fetch. */
|
|
6
|
+
wasmUrl: string;
|
|
7
|
+
/** URL of wasm_exec.js. Defaults to the same directory as wasmUrl. */
|
|
8
|
+
wasmExecUrl?: string;
|
|
9
|
+
/**
|
|
10
|
+
* GlobalThis property name the wasm binds. Must match the value the wasm
|
|
11
|
+
* was built with (the `apiName` passed to `host.Expose`). Defaults to
|
|
12
|
+
* `"ttsc"`.
|
|
13
|
+
*/
|
|
14
|
+
apiName?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Optional pre-existing MemFS host. When omitted, a fresh one is created
|
|
17
|
+
* and stored on the returned BootResult. Pass an existing host when you
|
|
18
|
+
* want to boot multiple wasms over the same filesystem (e.g. base ttsc + a
|
|
19
|
+
* typia wasm) so they share project sources.
|
|
20
|
+
*/
|
|
21
|
+
host?: IMemFSHost;
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stat-like object returned by `stat`, `lstat`, and `fstat`.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the subset of `fs.Stats` that `wasm_exec.js` reads. Fields not
|
|
5
|
+
* relevant to Go's `os.FileInfo` (e.g. ownership) are zeroed.
|
|
6
|
+
*/
|
|
7
|
+
export interface IFileStats {
|
|
8
|
+
isDirectory(): boolean;
|
|
9
|
+
isFile(): boolean;
|
|
10
|
+
size: number;
|
|
11
|
+
mode: number;
|
|
12
|
+
mtimeMs: number;
|
|
13
|
+
atimeMs: number;
|
|
14
|
+
ctimeMs: number;
|
|
15
|
+
dev: number;
|
|
16
|
+
ino: number;
|
|
17
|
+
nlink: number;
|
|
18
|
+
uid: number;
|
|
19
|
+
gid: number;
|
|
20
|
+
rdev: number;
|
|
21
|
+
blksize: number;
|
|
22
|
+
blocks: number;
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IWasmExecFS } from "./IWasmExecFS";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handle returned by `createMemFS`. Provides the `fs` shim to install on
|
|
5
|
+
* `globalThis` plus convenience methods for seeding the virtual filesystem
|
|
6
|
+
* before booting the wasm.
|
|
7
|
+
*/
|
|
8
|
+
export interface IMemFSHost {
|
|
9
|
+
fs: IWasmExecFS;
|
|
10
|
+
writeFile(path: string, data: string | Uint8Array): void;
|
|
11
|
+
readFile(path: string): Uint8Array | null;
|
|
12
|
+
readFileText(path: string): string | null;
|
|
13
|
+
exists(path: string): boolean;
|
|
14
|
+
mkdirp(path: string): void;
|
|
15
|
+
stdout: { buffer: string };
|
|
16
|
+
stderr: { buffer: string };
|
|
17
|
+
resetStdio(): void;
|
|
18
|
+
}
|