@ttsc/wasm 0.13.0 → 0.14.0-dev.20260528

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.
Files changed (160) hide show
  1. package/README.md +61 -1
  2. package/dist/ttsc.wasm +0 -0
  3. package/host/api.go +5 -4
  4. package/host/doc.go +7 -5
  5. package/host/fountain.go +505 -0
  6. package/host/host.go +64 -10
  7. package/host/plugin.go +6 -5
  8. package/lib/src/MemFSError.d.ts +15 -0
  9. package/lib/src/MemFSError.js +47 -0
  10. package/lib/src/MemFSError.js.map +1 -0
  11. package/lib/src/bootTtsc.d.ts +21 -0
  12. package/lib/src/bootTtsc.js +180 -0
  13. package/lib/src/bootTtsc.js.map +1 -0
  14. package/lib/src/createMemFS.d.ts +11 -0
  15. package/lib/src/{MemFS.js → createMemFS.js} +26 -61
  16. package/lib/src/createMemFS.js.map +1 -0
  17. package/lib/src/index.d.ts +5 -6
  18. package/lib/src/index.js +19 -8
  19. package/lib/src/index.js.map +1 -1
  20. package/lib/src/parseResult.d.ts +9 -0
  21. package/lib/src/parseResult.js +21 -0
  22. package/lib/src/parseResult.js.map +1 -0
  23. package/lib/src/structures/IBootResult.d.ts +9 -0
  24. package/lib/src/structures/IBootResult.js +3 -0
  25. package/lib/src/structures/IBootResult.js.map +1 -0
  26. package/lib/src/structures/IBootTtscOptions.d.ts +21 -0
  27. package/lib/src/structures/IBootTtscOptions.js +3 -0
  28. package/lib/src/structures/IBootTtscOptions.js.map +1 -0
  29. package/lib/src/structures/IFileStats.d.ts +23 -0
  30. package/lib/src/structures/IFileStats.js +3 -0
  31. package/lib/src/structures/IFileStats.js.map +1 -0
  32. package/lib/src/structures/IMemFSHost.d.ts +21 -0
  33. package/lib/src/structures/IMemFSHost.js +3 -0
  34. package/lib/src/structures/IMemFSHost.js.map +1 -0
  35. package/lib/src/structures/ITtscApi.d.ts +86 -0
  36. package/lib/src/structures/ITtscApi.js +3 -0
  37. package/lib/src/structures/ITtscApi.js.map +1 -0
  38. package/lib/src/structures/ITtscBuildOpts.d.ts +7 -0
  39. package/lib/src/structures/ITtscBuildOpts.js +3 -0
  40. package/lib/src/structures/ITtscBuildOpts.js.map +1 -0
  41. package/lib/src/structures/ITtscCompileResult.d.ts +11 -0
  42. package/lib/src/structures/ITtscCompileResult.js +3 -0
  43. package/lib/src/structures/ITtscCompileResult.js.map +1 -0
  44. package/lib/src/structures/ITtscDiagnostic.d.ts +14 -0
  45. package/lib/src/structures/ITtscDiagnostic.js +3 -0
  46. package/lib/src/structures/ITtscDiagnostic.js.map +1 -0
  47. package/lib/src/structures/ITtscFileQuery.d.ts +6 -0
  48. package/lib/src/structures/ITtscFileQuery.js +3 -0
  49. package/lib/src/structures/ITtscFileQuery.js.map +1 -0
  50. package/lib/src/structures/ITtscFountainDiagnosticsResult.d.ts +5 -0
  51. package/lib/src/structures/ITtscFountainDiagnosticsResult.js +3 -0
  52. package/lib/src/structures/ITtscFountainDiagnosticsResult.js.map +1 -0
  53. package/lib/src/structures/ITtscNodeAtPositionResult.d.ts +6 -0
  54. package/lib/src/structures/ITtscNodeAtPositionResult.js +3 -0
  55. package/lib/src/structures/ITtscNodeAtPositionResult.js.map +1 -0
  56. package/lib/src/structures/ITtscNodeInfo.d.ts +13 -0
  57. package/lib/src/structures/ITtscNodeInfo.js +3 -0
  58. package/lib/src/structures/ITtscNodeInfo.js.map +1 -0
  59. package/lib/src/structures/ITtscPluginOpts.d.ts +13 -0
  60. package/lib/src/structures/ITtscPluginOpts.js +3 -0
  61. package/lib/src/structures/ITtscPluginOpts.js.map +1 -0
  62. package/lib/src/structures/ITtscPositionQuery.d.ts +9 -0
  63. package/lib/src/structures/ITtscPositionQuery.js +3 -0
  64. package/lib/src/structures/ITtscPositionQuery.js.map +1 -0
  65. package/lib/src/structures/ITtscReleaseSnapshotResult.d.ts +5 -0
  66. package/lib/src/structures/ITtscReleaseSnapshotResult.js +3 -0
  67. package/lib/src/structures/ITtscReleaseSnapshotResult.js.map +1 -0
  68. package/lib/src/structures/ITtscResult.d.ts +15 -0
  69. package/lib/src/structures/ITtscResult.js +3 -0
  70. package/lib/src/structures/ITtscResult.js.map +1 -0
  71. package/lib/src/structures/ITtscSnapshotHandle.d.ts +5 -0
  72. package/lib/src/structures/ITtscSnapshotHandle.js +3 -0
  73. package/lib/src/structures/ITtscSnapshotHandle.js.map +1 -0
  74. package/lib/src/structures/ITtscSnapshotResult.d.ts +5 -0
  75. package/lib/src/structures/ITtscSnapshotResult.js +3 -0
  76. package/lib/src/structures/ITtscSnapshotResult.js.map +1 -0
  77. package/lib/src/structures/ITtscSnapshotsResult.d.ts +4 -0
  78. package/lib/src/structures/ITtscSnapshotsResult.js +3 -0
  79. package/lib/src/structures/ITtscSnapshotsResult.js.map +1 -0
  80. package/lib/src/structures/ITtscSourceFileTextResult.d.ts +5 -0
  81. package/lib/src/structures/ITtscSourceFileTextResult.js +3 -0
  82. package/lib/src/structures/ITtscSourceFileTextResult.js.map +1 -0
  83. package/lib/src/structures/ITtscSourceFilesResult.d.ts +5 -0
  84. package/lib/src/structures/ITtscSourceFilesResult.js +3 -0
  85. package/lib/src/structures/ITtscSourceFilesResult.js.map +1 -0
  86. package/lib/src/structures/ITtscSymbolAtPositionResult.d.ts +6 -0
  87. package/lib/src/structures/ITtscSymbolAtPositionResult.js +3 -0
  88. package/lib/src/structures/ITtscSymbolAtPositionResult.js.map +1 -0
  89. package/lib/src/structures/ITtscSymbolDeclaration.d.ts +7 -0
  90. package/lib/src/structures/ITtscSymbolDeclaration.js +3 -0
  91. package/lib/src/structures/ITtscSymbolDeclaration.js.map +1 -0
  92. package/lib/src/structures/ITtscSymbolInfo.d.ts +14 -0
  93. package/lib/src/structures/ITtscSymbolInfo.js +3 -0
  94. package/lib/src/structures/ITtscSymbolInfo.js.map +1 -0
  95. package/lib/src/structures/ITtscTransformResult.d.ts +12 -0
  96. package/lib/src/structures/ITtscTransformResult.js +3 -0
  97. package/lib/src/structures/ITtscTransformResult.js.map +1 -0
  98. package/lib/src/structures/ITtscTypeAtPositionResult.d.ts +6 -0
  99. package/lib/src/structures/ITtscTypeAtPositionResult.js +3 -0
  100. package/lib/src/structures/ITtscTypeAtPositionResult.js.map +1 -0
  101. package/lib/src/structures/ITtscTypeInfo.d.ts +7 -0
  102. package/lib/src/structures/ITtscTypeInfo.js +3 -0
  103. package/lib/src/structures/ITtscTypeInfo.js.map +1 -0
  104. package/lib/src/structures/ITtscVersion.d.ts +9 -0
  105. package/lib/src/structures/ITtscVersion.js +3 -0
  106. package/lib/src/structures/ITtscVersion.js.map +1 -0
  107. package/lib/src/{MemFS.d.ts → structures/IWasmExecFS.d.ts} +7 -66
  108. package/lib/src/structures/IWasmExecFS.js +3 -0
  109. package/lib/src/structures/IWasmExecFS.js.map +1 -0
  110. package/lib/src/structures/index.d.ts +29 -0
  111. package/lib/src/structures/index.js +46 -0
  112. package/lib/src/structures/index.js.map +1 -0
  113. package/package.json +2 -2
  114. package/shim-vendor/shim/ast/lint.go +7 -1
  115. package/shim-vendor/shim/checker/shim.go +3 -0
  116. package/shim-vendor/shim/core/shim.go +11 -0
  117. package/src/MemFSError.ts +43 -0
  118. package/src/bootTtsc.ts +214 -0
  119. package/src/{MemFS.ts → createMemFS.ts} +13 -231
  120. package/src/index.ts +5 -17
  121. package/src/parseResult.ts +17 -0
  122. package/src/structures/IBootResult.ts +10 -0
  123. package/src/structures/IBootTtscOptions.ts +22 -0
  124. package/src/structures/IFileStats.ts +23 -0
  125. package/src/structures/IMemFSHost.ts +18 -0
  126. package/src/structures/ITtscApi.ts +101 -0
  127. package/src/structures/ITtscBuildOpts.ts +7 -0
  128. package/src/structures/ITtscCompileResult.ts +12 -0
  129. package/src/structures/ITtscDiagnostic.ts +14 -0
  130. package/src/structures/ITtscFileQuery.ts +7 -0
  131. package/src/structures/ITtscFountainDiagnosticsResult.ts +6 -0
  132. package/src/structures/ITtscNodeAtPositionResult.ts +7 -0
  133. package/src/structures/ITtscNodeInfo.ts +13 -0
  134. package/src/structures/ITtscPluginOpts.ts +13 -0
  135. package/src/structures/ITtscPositionQuery.ts +10 -0
  136. package/src/structures/ITtscReleaseSnapshotResult.ts +5 -0
  137. package/src/structures/ITtscResult.ts +15 -0
  138. package/src/structures/ITtscSnapshotHandle.ts +5 -0
  139. package/src/structures/ITtscSnapshotResult.ts +5 -0
  140. package/src/structures/ITtscSnapshotsResult.ts +4 -0
  141. package/src/structures/ITtscSourceFileTextResult.ts +5 -0
  142. package/src/structures/ITtscSourceFilesResult.ts +5 -0
  143. package/src/structures/ITtscSymbolAtPositionResult.ts +7 -0
  144. package/src/structures/ITtscSymbolDeclaration.ts +7 -0
  145. package/src/structures/ITtscSymbolInfo.ts +15 -0
  146. package/src/structures/ITtscTransformResult.ts +13 -0
  147. package/src/structures/ITtscTypeAtPositionResult.ts +7 -0
  148. package/src/structures/ITtscTypeInfo.ts +7 -0
  149. package/src/structures/ITtscVersion.ts +9 -0
  150. package/src/structures/IWasmExecFS.ts +150 -0
  151. package/src/structures/index.ts +29 -0
  152. package/lib/src/MemFS.js.map +0 -1
  153. package/lib/src/api.d.ts +0 -120
  154. package/lib/src/api.js +0 -23
  155. package/lib/src/api.js.map +0 -1
  156. package/lib/src/instantiate.d.ts +0 -30
  157. package/lib/src/instantiate.js +0 -88
  158. package/lib/src/instantiate.js.map +0 -1
  159. package/src/api.ts +0 -145
  160. package/src/instantiate.ts +0 -137
package/README.md CHANGED
@@ -116,7 +116,67 @@ const result = await api.build({ cwd: "/work" });
116
116
  console.log(result.result); // JSON: { diagnostics, output }
117
117
  ```
118
118
 
119
- Booting two wasms with the same `apiName` panics; pick a unique `apiName` per binary.
119
+ Booting two wasms with the same `apiName` overwrites the previous global
120
+ binding; pick a unique `apiName` per binary. `bootTtsc` also installs shared
121
+ `fs` and `process` globals in its Worker, so use separate Workers when binaries
122
+ need independent filesystems.
123
+
124
+ ## Fountain API (snapshot, AST, type checker)
125
+
126
+ For embedders that want `embed-typescript`-style raw access to the program — diagnostics, AST nodes, the type checker at a position — the same `globalThis[apiName]` object also exposes fountain verbs. They share the standard `{code, stdout, stderr, result}` envelope; `result` is JSON.
127
+
128
+ ```ts
129
+ import { bootTtsc, parseResult } from "@ttsc/wasm";
130
+ import type {
131
+ ITtscSnapshotResult,
132
+ ITtscTypeAtPositionResult,
133
+ } from "@ttsc/wasm";
134
+
135
+ const { api, host } = await bootTtsc({ wasmUrl, apiName });
136
+
137
+ host.writeFile("/work/tsconfig.json", "{}");
138
+ host.writeFile("/work/src/index.ts", "export const x: number = 1;");
139
+
140
+ const snap = parseResult<ITtscSnapshotResult>(
141
+ await api.snapshot({ cwd: "/work" }),
142
+ );
143
+ const handle = snap!.handle;
144
+
145
+ try {
146
+ const typeAt = parseResult<ITtscTypeAtPositionResult>(
147
+ await api.getTypeAtPosition({
148
+ handle,
149
+ path: "src/index.ts",
150
+ position: 18, // byte offset of `x`
151
+ }),
152
+ );
153
+ console.log(typeAt?.type?.text); // → "number"
154
+ } finally {
155
+ await api.releaseSnapshot({ handle });
156
+ }
157
+ ```
158
+
159
+ Verbs and payload types:
160
+
161
+ | Verb | Payload type |
162
+ | ---- | ------------ |
163
+ | `snapshot({ cwd, tsconfig? })` | `ITtscSnapshotResult` `{ handle }` |
164
+ | `releaseSnapshot({ handle })` | `ITtscReleaseSnapshotResult` `{ released }` |
165
+ | `snapshots()` | `ITtscSnapshotsResult` `{ handles }` |
166
+ | `getSourceFiles({ handle })` | `ITtscSourceFilesResult` `{ files }` |
167
+ | `getSourceFileText({ handle, path })` | `ITtscSourceFileTextResult` `{ text }` |
168
+ | `getDiagnostics({ handle, file? })` | `ITtscFountainDiagnosticsResult` `{ diagnostics }` |
169
+ | `getNodeAtPosition({ handle, path, position })` | `ITtscNodeAtPositionResult` `{ node }` |
170
+ | `getTypeAtPosition({ handle, path, position })` | `ITtscTypeAtPositionResult` `{ type }` |
171
+ | `getSymbolAtPosition({ handle, path, position })` | `ITtscSymbolAtPositionResult` `{ symbol }` |
172
+
173
+ `position` is a **byte offset** into the source text — the same coordinate
174
+ TypeScript-Go uses internally. JS callers that have a UTF-16 `(line,
175
+ character)` pair (e.g. from Monaco) must convert it before calling.
176
+
177
+ **Lifecycle:** JS owns the handle. The wasm keeps the program (parsed AST,
178
+ checker pool lease, every source file) alive until you call
179
+ `releaseSnapshot`. Leaking handles leaks memory in the wasm linear heap.
120
180
 
121
181
  ## Plugin contract
122
182
 
package/dist/ttsc.wasm CHANGED
Binary file
package/host/api.go CHANGED
@@ -16,10 +16,11 @@ import (
16
16
  "github.com/samchon/ttsc/packages/ttsc/driver"
17
17
  )
18
18
 
19
- // APIResult is the JSON shape every plugin-dispatch endpoint returns to the
20
- // JS host. `code` follows the native CLI exit-code contract (0 success, 2
21
- // usage error, 3 runtime error). `stdout` / `stderr` are raw strings the
22
- // caller can render in a console panel.
19
+ // APIResult is the stdout/stderr capture returned by runWithCapturedIO. The
20
+ // js/wasm binding wraps it in the same JS result envelope that build/check/
21
+ // transform use, adding `result` when an endpoint has a JSON payload. `code`
22
+ // follows the native CLI exit-code contract (0 success, 2 compiler/config/
23
+ // usage error, 3 runtime error).
23
24
  type APIResult struct {
24
25
  Code int `json:"code"`
25
26
  Stdout string `json:"stdout"`
package/host/doc.go CHANGED
@@ -19,11 +19,13 @@
19
19
  // }
20
20
  //
21
21
  // Expose binds `globalThis[name]` to an object that exposes ttsc's base
22
- // project commands (build, check, transform, version) plus a `plugin(name,
23
- // command, opts)` dispatcher that routes into a Plugin's CLI-shaped Run
24
- // callback. Plugins keep ttsc's existing argv-based contract the same
25
- // shape the native sidecars implement so the wasm and the native CLI can
26
- // share their Run* entry points byte-for-byte.
22
+ // project commands (build, check, transform, version) plus
23
+ // `plugin({ name, command, ...opts })`, which routes into a Plugin's
24
+ // CLI-shaped Run callback. Every async endpoint returns the JS result envelope;
25
+ // build/check/transform place JSON payloads in `result`, while plugin output
26
+ // is captured in stdout/stderr. Plugins keep ttsc's existing argv-based
27
+ // contract — the same shape the native sidecars implement — so the wasm and
28
+ // the native CLI can share their Run* entry points byte-for-byte.
27
29
  //
28
30
  // The package is browser-agnostic: every JS-facing helper sits behind
29
31
  // //go:build js, so a consumer can `go build ./...` natively without GOOS=js
@@ -0,0 +1,505 @@
1
+ //go:build js && wasm
2
+
3
+ // Fountain-style JS endpoints for the host package.
4
+ //
5
+ // These verbs let JS callers hold a TypeScript-Go Program in memory across
6
+ // multiple queries — diagnostics, AST lookup, type-checker queries — without
7
+ // re-loading and re-checking the project on every call. The shape mirrors the
8
+ // `embed-typescript` `fountain()` escape hatch over the legacy TS API, adapted
9
+ // to ttsc's TypeScript-Go driver.
10
+ //
11
+ // Lifecycle: JS owns the handle. `snapshot` returns an opaque string handle;
12
+ // callers MUST call `releaseSnapshot` to free the Program's checker pool lease
13
+ // and let Go GC reclaim the AST. Unreleased handles leak memory in the wasm
14
+ // linear heap.
15
+ //
16
+ // Result envelope: every verb returns the standard `{code, stdout, stderr,
17
+ // result}` shape used by build/check/transform. The structured payload is
18
+ // JSON-encoded into `result`; JS callers parse it with the same `parseResult`
19
+ // helper they use for the base endpoints.
20
+ package host
21
+
22
+ import (
23
+ "encoding/json"
24
+ "fmt"
25
+ "path/filepath"
26
+ "sync"
27
+ "sync/atomic"
28
+ "syscall/js"
29
+
30
+ "github.com/microsoft/typescript-go/shim/ast"
31
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
32
+
33
+ "github.com/samchon/ttsc/packages/ttsc/driver"
34
+ )
35
+
36
+ // snapshotEntry pairs a Program with the cwd it was loaded against so we can
37
+ // rewrite file paths to project-relative keys uniformly across all queries.
38
+ //
39
+ // `mu` serializes Checker-touching paths. driver.LoadProgram pins one
40
+ // Checker via forceSingleChecker, and TypeScript-Go's Checker is not
41
+ // documented thread-safe. On js/wasm cooperative scheduling normally
42
+ // prevents two goroutines from running concurrently, but any Go→JS bridge
43
+ // call (file read, Promise await) can yield mid-operation; without `mu`
44
+ // two fountain verbs invoked in the same frame could land their Checker
45
+ // reads on opposite sides of an internal mutation and corrupt state.
46
+ type snapshotEntry struct {
47
+ mu sync.Mutex
48
+ prog *driver.Program
49
+ cwd string
50
+ }
51
+
52
+ var (
53
+ snapshotsMu sync.RWMutex
54
+ snapshots = map[string]*snapshotEntry{}
55
+ nextHandle atomic.Uint64
56
+ )
57
+
58
+ // fountainAPIMap returns the verb → js.Func map appended to globalThis[apiName]
59
+ // during Expose. Kept in a helper so host.go's API map stays scannable.
60
+ func fountainAPIMap() map[string]any {
61
+ return map[string]any{
62
+ "snapshot": js.FuncOf(jsSnapshot),
63
+ "releaseSnapshot": js.FuncOf(jsReleaseSnapshot),
64
+ "snapshots": js.FuncOf(jsListSnapshots),
65
+ "getSourceFiles": js.FuncOf(jsGetSourceFiles),
66
+ "getSourceFileText": js.FuncOf(jsGetSourceFileText),
67
+ "getDiagnostics": js.FuncOf(jsGetDiagnostics),
68
+ "getNodeAtPosition": js.FuncOf(jsGetNodeAtPosition),
69
+ "getTypeAtPosition": js.FuncOf(jsGetTypeAtPosition),
70
+ "getSymbolAtPosition": js.FuncOf(jsGetSymbolAtPosition),
71
+ }
72
+ }
73
+
74
+ // SnapshotResult is the response shape for `snapshot()`.
75
+ type SnapshotResult struct {
76
+ Handle string `json:"handle"`
77
+ }
78
+
79
+ // ReleaseSnapshotResult is the response shape for `releaseSnapshot()`.
80
+ type ReleaseSnapshotResult struct {
81
+ Released bool `json:"released"`
82
+ }
83
+
84
+ // ListSnapshotsResult is the response shape for `snapshots()`.
85
+ type ListSnapshotsResult struct {
86
+ Handles []string `json:"handles"`
87
+ }
88
+
89
+ // GetSourceFilesResult is the response shape for `getSourceFiles()`.
90
+ type GetSourceFilesResult struct {
91
+ Files []string `json:"files"`
92
+ }
93
+
94
+ // GetSourceFileTextResult is the response shape for `getSourceFileText()`.
95
+ type GetSourceFileTextResult struct {
96
+ Text string `json:"text"`
97
+ }
98
+
99
+ // GetDiagnosticsResult is the response shape for `getDiagnostics()`.
100
+ type GetDiagnosticsResult struct {
101
+ Diagnostics []CompileDiagnostic `json:"diagnostics"`
102
+ }
103
+
104
+ // NodeInfo is the serialized AST node returned by `getNodeAtPosition`.
105
+ type NodeInfo struct {
106
+ Kind int `json:"kind"`
107
+ KindName string `json:"kindName"`
108
+ Pos int `json:"pos"`
109
+ End int `json:"end"`
110
+ Text string `json:"text,omitempty"`
111
+ }
112
+
113
+ // GetNodeAtPositionResult is the response shape for `getNodeAtPosition()`.
114
+ type GetNodeAtPositionResult struct {
115
+ Node *NodeInfo `json:"node"`
116
+ }
117
+
118
+ // TypeInfo is the serialized type returned by `getTypeAtPosition`.
119
+ type TypeInfo struct {
120
+ Text string `json:"text"`
121
+ Flags int `json:"flags"`
122
+ }
123
+
124
+ // GetTypeAtPositionResult is the response shape for `getTypeAtPosition()`.
125
+ type GetTypeAtPositionResult struct {
126
+ Type *TypeInfo `json:"type"`
127
+ }
128
+
129
+ // SymbolDeclaration is the serialized declaration site returned by
130
+ // `getSymbolAtPosition`.
131
+ type SymbolDeclaration struct {
132
+ File *string `json:"file"`
133
+ Pos int `json:"pos"`
134
+ End int `json:"end"`
135
+ }
136
+
137
+ // SymbolInfo is the serialized symbol returned by `getSymbolAtPosition`.
138
+ type SymbolInfo struct {
139
+ Name string `json:"name"`
140
+ Text string `json:"text,omitempty"`
141
+ Flags int `json:"flags"`
142
+ Declarations []SymbolDeclaration `json:"declarations,omitempty"`
143
+ DeclarationCount int `json:"declarationCount,omitempty"`
144
+ }
145
+
146
+ // GetSymbolAtPositionResult is the response shape for `getSymbolAtPosition()`.
147
+ type GetSymbolAtPositionResult struct {
148
+ Symbol *SymbolInfo `json:"symbol"`
149
+ }
150
+
151
+ // jsSnapshot({cwd, tsconfig?}) → Promise<ITtscResult>.
152
+ //
153
+ // Loads the project once and retains the Program for follow-up queries. The
154
+ // returned handle is opaque; treat it as a string.
155
+ func jsSnapshot(this js.Value, args []js.Value) any {
156
+ opts := optionsArg(args)
157
+ return makePromise(func() any {
158
+ cwd := stringProp(opts, "cwd")
159
+ tsconfig := stringProp(opts, "tsconfig")
160
+ if cwd == "" {
161
+ return errorResponse(2, "host.snapshot: \"cwd\" is required")
162
+ }
163
+ if tsconfig == "" {
164
+ tsconfig = "tsconfig.json"
165
+ }
166
+ prog, diags, err := driver.LoadProgram(cwd, tsconfig, driver.LoadProgramOptions{
167
+ ForceNoEmit: true,
168
+ })
169
+ if err != nil {
170
+ return errorResponse(2, err.Error())
171
+ }
172
+ if prog == nil {
173
+ msg := "host.snapshot: project load failed"
174
+ if len(diags) > 0 {
175
+ msg = diags[0].Message
176
+ }
177
+ return errorResponse(2, msg)
178
+ }
179
+ handle := fmt.Sprintf("snap-%d", nextHandle.Add(1))
180
+ snapshotsMu.Lock()
181
+ snapshots[handle] = &snapshotEntry{prog: prog, cwd: cwd}
182
+ snapshotsMu.Unlock()
183
+ return fountainOK(SnapshotResult{Handle: handle})
184
+ })
185
+ }
186
+
187
+ // jsReleaseSnapshot({handle}) → Promise<ITtscResult>.
188
+ //
189
+ // `released=false` indicates the handle was not present (already released or
190
+ // never created). The endpoint never errors on unknown handles so callers can
191
+ // release idempotently.
192
+ //
193
+ // Holds the write lock for the full delete+Close so any in-flight read
194
+ // (withSnapshot's RLock) finishes before the Program is closed. This is the
195
+ // pair of the TOCTOU guarantee documented on withSnapshot.
196
+ func jsReleaseSnapshot(this js.Value, args []js.Value) any {
197
+ opts := optionsArg(args)
198
+ return makePromise(func() any {
199
+ handle := stringProp(opts, "handle")
200
+ if handle == "" {
201
+ return errorResponse(2, "host.releaseSnapshot: \"handle\" is required")
202
+ }
203
+ snapshotsMu.Lock()
204
+ defer snapshotsMu.Unlock()
205
+ entry, ok := snapshots[handle]
206
+ if !ok {
207
+ return fountainOK(ReleaseSnapshotResult{Released: false})
208
+ }
209
+ delete(snapshots, handle)
210
+ if entry.prog != nil {
211
+ // Recover from any panic inside Close so the rest of the wasm
212
+ // instance survives; the entry has already been removed from
213
+ // the table so the handle is effectively released either way.
214
+ func() {
215
+ defer func() { _ = recover() }()
216
+ _ = entry.prog.Close()
217
+ }()
218
+ }
219
+ return fountainOK(ReleaseSnapshotResult{Released: true})
220
+ })
221
+ }
222
+
223
+ // jsListSnapshots() → Promise<ITtscResult>. Debug aid — lets a JS embedder
224
+ // verify it has released what it thinks it has.
225
+ func jsListSnapshots(this js.Value, args []js.Value) any {
226
+ return makePromise(func() any {
227
+ snapshotsMu.RLock()
228
+ handles := make([]string, 0, len(snapshots))
229
+ for h := range snapshots {
230
+ handles = append(handles, h)
231
+ }
232
+ snapshotsMu.RUnlock()
233
+ return fountainOK(ListSnapshotsResult{Handles: handles})
234
+ })
235
+ }
236
+
237
+ // jsGetSourceFiles({handle}) → Promise<ITtscResult>. Lists the non-
238
+ // declaration source files in the program, keyed by project-relative path
239
+ // (same convention as build/transform output).
240
+ func jsGetSourceFiles(this js.Value, args []js.Value) any {
241
+ return withSnapshot(args, func(entry *snapshotEntry, _ js.Value) any {
242
+ files := entry.prog.SourceFiles()
243
+ out := make([]string, 0, len(files))
244
+ for _, f := range files {
245
+ out = append(out, snapshotFileKey(entry.cwd, f.FileName()))
246
+ }
247
+ return fountainOK(GetSourceFilesResult{Files: out})
248
+ })
249
+ }
250
+
251
+ // jsGetSourceFileText({handle, path}) → Promise<ITtscResult>. Returns the
252
+ // current source text the Program is holding for the file. After transform
253
+ // plugins have run, this reflects the post-transform text.
254
+ func jsGetSourceFileText(this js.Value, args []js.Value) any {
255
+ return withSnapshot(args, func(entry *snapshotEntry, opts js.Value) any {
256
+ path := stringProp(opts, "path")
257
+ if path == "" {
258
+ return errorResponse(2, "host.getSourceFileText: \"path\" is required")
259
+ }
260
+ file := resolveSnapshotFile(entry, path)
261
+ if file == nil {
262
+ return errorResponse(2, fmt.Sprintf("host.getSourceFileText: file not found: %q", path))
263
+ }
264
+ return fountainOK(GetSourceFileTextResult{Text: file.Text()})
265
+ })
266
+ }
267
+
268
+ // jsGetDiagnostics({handle, file?}) → Promise<ITtscResult>. Returns the same
269
+ // diagnostic shape as build/check/transform. When `file` is set, results are
270
+ // filtered to that project-relative path.
271
+ func jsGetDiagnostics(this js.Value, args []js.Value) any {
272
+ return withSnapshot(args, func(entry *snapshotEntry, opts js.Value) any {
273
+ filter := stringProp(opts, "file")
274
+ diags := entry.prog.Diagnostics()
275
+ out := make([]CompileDiagnostic, 0, len(diags))
276
+ for _, d := range diags {
277
+ api := toAPIDiagnostic(d)
278
+ if filter != "" {
279
+ if api.File == nil {
280
+ continue
281
+ }
282
+ rel := snapshotFileKey(entry.cwd, *api.File)
283
+ if rel != filter && *api.File != filter {
284
+ continue
285
+ }
286
+ }
287
+ out = append(out, api)
288
+ }
289
+ return fountainOK(GetDiagnosticsResult{Diagnostics: out})
290
+ })
291
+ }
292
+
293
+ // jsGetNodeAtPosition({handle, path, position}) → Promise<ITtscResult>.
294
+ // `position` is a byte offset into the source text (the same coordinate the
295
+ // AST uses internally). JS callers that have a UTF-16 line/character pair
296
+ // should resolve it to a byte offset before calling (the @ttsc/playground
297
+ // helper does this).
298
+ func jsGetNodeAtPosition(this js.Value, args []js.Value) any {
299
+ return withSnapshotPosition(args, func(_ *snapshotEntry, file *ast.SourceFile, pos int) any {
300
+ node := ast.GetNodeAtPosition(file, pos, false)
301
+ return fountainOK(GetNodeAtPositionResult{Node: nodeInfoOf(node)})
302
+ })
303
+ }
304
+
305
+ // jsGetTypeAtPosition({handle, path, position}) → Promise<ITtscResult>.
306
+ // Resolves the node at position via GetNodeAtPosition and asks the Program's
307
+ // pinned single checker for its type. Returns `{type: null}` when there is no
308
+ // node at that position or no checker is available.
309
+ func jsGetTypeAtPosition(this js.Value, args []js.Value) any {
310
+ return withSnapshotPosition(args, func(entry *snapshotEntry, file *ast.SourceFile, pos int) any {
311
+ node := ast.GetNodeAtPosition(file, pos, false)
312
+ if node == nil || entry.prog.Checker == nil {
313
+ return fountainOK(GetTypeAtPositionResult{Type: nil})
314
+ }
315
+ t := entry.prog.Checker.GetTypeAtLocation(node)
316
+ if t == nil {
317
+ return fountainOK(GetTypeAtPositionResult{Type: nil})
318
+ }
319
+ return fountainOK(GetTypeAtPositionResult{
320
+ Type: &TypeInfo{
321
+ Text: entry.prog.Checker.TypeToString(t),
322
+ Flags: int(t.Flags()),
323
+ },
324
+ })
325
+ })
326
+ }
327
+
328
+ // jsGetSymbolAtPosition({handle, path, position}) → Promise<ITtscResult>.
329
+ // Returns `{symbol: null}` when the node at position has no associated
330
+ // symbol (e.g. punctuation, whitespace).
331
+ func jsGetSymbolAtPosition(this js.Value, args []js.Value) any {
332
+ return withSnapshotPosition(args, func(entry *snapshotEntry, file *ast.SourceFile, pos int) any {
333
+ node := ast.GetNodeAtPosition(file, pos, false)
334
+ if node == nil || entry.prog.Checker == nil {
335
+ return fountainOK(GetSymbolAtPositionResult{Symbol: nil})
336
+ }
337
+ sym := entry.prog.Checker.GetSymbolAtLocation(node)
338
+ if sym == nil {
339
+ return fountainOK(GetSymbolAtPositionResult{Symbol: nil})
340
+ }
341
+ return fountainOK(GetSymbolAtPositionResult{Symbol: symbolInfoOf(entry, sym)})
342
+ })
343
+ }
344
+
345
+ // withSnapshot is the shared envelope for every read-only fountain verb. It
346
+ // parses the JS arg, resolves the handle, holds the snapshot table's read
347
+ // lock + the per-entry mutex, and runs `fn`. Two layered locks:
348
+ //
349
+ // - snapshotsMu (RLock): pairs with jsReleaseSnapshot's write lock so
350
+ // the entry's prog isn't Close()d under the caller. Closes the prior
351
+ // TOCTOU between "found the entry" and "used entry.prog".
352
+ // - entry.mu: serializes Checker-touching paths. TS-Go's Checker is
353
+ // single-instance (forceSingleChecker) and not documented
354
+ // thread-safe; two fountain verbs invoked in the same frame could
355
+ // otherwise interleave Checker state mutations.
356
+ func withSnapshot(args []js.Value, fn func(*snapshotEntry, js.Value) any) any {
357
+ opts := optionsArg(args)
358
+ return makePromise(func() any {
359
+ handle := stringProp(opts, "handle")
360
+ if handle == "" {
361
+ return errorResponse(2, "host: \"handle\" is required")
362
+ }
363
+ snapshotsMu.RLock()
364
+ defer snapshotsMu.RUnlock()
365
+ entry := snapshots[handle]
366
+ if entry == nil {
367
+ return errorResponse(2, fmt.Sprintf("host: snapshot %q not found (already released or never created)", handle))
368
+ }
369
+ entry.mu.Lock()
370
+ defer entry.mu.Unlock()
371
+ return fn(entry, opts)
372
+ })
373
+ }
374
+
375
+ // withSnapshotPosition is the shared envelope for the 3 position-bound
376
+ // fountain verbs. Same read-lock lifecycle as withSnapshot, plus parses
377
+ // {path, position} and bounds-checks position against the file length so an
378
+ // out-of-range offset reaches `ast.GetNodeAtPosition` only as a clean
379
+ // error response instead of an internal panic.
380
+ func withSnapshotPosition(args []js.Value, fn func(*snapshotEntry, *ast.SourceFile, int) any) any {
381
+ return withSnapshot(args, func(entry *snapshotEntry, opts js.Value) any {
382
+ path := stringProp(opts, "path")
383
+ if path == "" {
384
+ return errorResponse(2, "host: \"path\" is required")
385
+ }
386
+ posVal := opts.Get("position")
387
+ if posVal.Type() != js.TypeNumber {
388
+ return errorResponse(2, "host: \"position\" must be a number (byte offset)")
389
+ }
390
+ pos := posVal.Int()
391
+ if pos < 0 {
392
+ return errorResponse(2, "host: \"position\" must be non-negative")
393
+ }
394
+ file := resolveSnapshotFile(entry, path)
395
+ if file == nil {
396
+ return errorResponse(2, fmt.Sprintf("host: file %q not found in snapshot", path))
397
+ }
398
+ if pos > len(file.Text()) {
399
+ return errorResponse(2, fmt.Sprintf("host: \"position\" %d exceeds file length %d", pos, len(file.Text())))
400
+ }
401
+ return fn(entry, file, pos)
402
+ })
403
+ }
404
+
405
+ // resolveSnapshotFile finds a SourceFile by path, accepting either an
406
+ // absolute path or a path relative to the snapshot's cwd.
407
+ func resolveSnapshotFile(entry *snapshotEntry, path string) *ast.SourceFile {
408
+ if file := entry.prog.SourceFile(path); file != nil {
409
+ return file
410
+ }
411
+ if !filepath.IsAbs(path) {
412
+ abs := filepath.Join(entry.cwd, path)
413
+ if file := entry.prog.SourceFile(abs); file != nil {
414
+ return file
415
+ }
416
+ }
417
+ return nil
418
+ }
419
+
420
+ // snapshotFileKey mirrors apiOutputKey but is exported for use by all
421
+ // fountain endpoints that return file paths.
422
+ func snapshotFileKey(cwd, fileName string) string {
423
+ return apiOutputKey(cwd, fileName)
424
+ }
425
+
426
+ // fountainOK wraps a JSON-able payload in the standard `{code, stdout,
427
+ // stderr, result}` envelope. The payload is JSON-encoded into `result`; JS
428
+ // callers `parseResult<T>` it the same way they do for build/check/transform.
429
+ func fountainOK(payload any) any {
430
+ data, err := json.Marshal(payload)
431
+ if err != nil {
432
+ return errorResponse(3, fmt.Sprintf("host: fountain result marshal failed: %v", err))
433
+ }
434
+ return js.ValueOf(map[string]any{
435
+ "code": 0,
436
+ "stdout": "",
437
+ "stderr": "",
438
+ "result": string(data),
439
+ })
440
+ }
441
+
442
+ // nodeInfoOf converts a *ast.Node into the JSON-serializable NodeInfo.
443
+ func nodeInfoOf(node *ast.Node) *NodeInfo {
444
+ if node == nil {
445
+ return nil
446
+ }
447
+ info := &NodeInfo{
448
+ Kind: int(node.Kind),
449
+ KindName: astKindName(node.Kind),
450
+ Pos: node.Pos(),
451
+ End: node.End(),
452
+ }
453
+ if text := shimscanner.GetTextOfNode(node); text != "" {
454
+ info.Text = text
455
+ }
456
+ return info
457
+ }
458
+
459
+ // symbolInfoOf converts a *ast.Symbol into the JSON-serializable SymbolInfo,
460
+ // including up to a few declaration sites so callers can implement "go to
461
+ // definition" without follow-up snapshot queries.
462
+ func symbolInfoOf(entry *snapshotEntry, sym *ast.Symbol) *SymbolInfo {
463
+ if sym == nil {
464
+ return nil
465
+ }
466
+ info := &SymbolInfo{
467
+ Name: sym.Name,
468
+ Flags: int(sym.Flags),
469
+ }
470
+ if entry.prog.Checker != nil {
471
+ info.Text = entry.prog.Checker.SymbolToString(sym)
472
+ }
473
+ decls := sym.Declarations
474
+ if len(decls) > 0 {
475
+ // Cap at 16 to keep merged-namespace symbols (e.g. global lib types)
476
+ // from ballooning the response.
477
+ const maxDecls = 16
478
+ n := len(decls)
479
+ capped := n
480
+ if n > maxDecls {
481
+ capped = maxDecls
482
+ }
483
+ out := make([]SymbolDeclaration, 0, capped)
484
+ for _, d := range decls[:capped] {
485
+ if d == nil {
486
+ continue
487
+ }
488
+ item := SymbolDeclaration{Pos: d.Pos(), End: d.End()}
489
+ if file := ast.GetSourceFileOfNode(d); file != nil {
490
+ key := snapshotFileKey(entry.cwd, file.FileName())
491
+ item.File = &key
492
+ }
493
+ out = append(out, item)
494
+ }
495
+ info.Declarations = out
496
+ info.DeclarationCount = n
497
+ }
498
+ return info
499
+ }
500
+
501
+ // astKindName renders an ast.Kind to its string representation. ast.Kind has
502
+ // a Stringer impl in tsgo (`%v` produces "FunctionDeclaration" etc.).
503
+ func astKindName(k ast.Kind) string {
504
+ return fmt.Sprintf("%v", k)
505
+ }