@ttsc/wasm 0.14.0-dev.20260529.2 → 0.14.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/host/fountain.go CHANGED
@@ -20,17 +20,17 @@
20
20
  package host
21
21
 
22
22
  import (
23
- "encoding/json"
24
- "fmt"
25
- "path/filepath"
26
- "sync"
27
- "sync/atomic"
28
- "syscall/js"
23
+ "encoding/json"
24
+ "fmt"
25
+ "path/filepath"
26
+ "sync"
27
+ "sync/atomic"
28
+ "syscall/js"
29
29
 
30
- "github.com/microsoft/typescript-go/shim/ast"
31
- shimscanner "github.com/microsoft/typescript-go/shim/scanner"
30
+ "github.com/microsoft/typescript-go/shim/ast"
31
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
32
32
 
33
- "github.com/samchon/ttsc/packages/ttsc/driver"
33
+ "github.com/samchon/ttsc/packages/ttsc/driver"
34
34
  )
35
35
 
36
36
  // snapshotEntry pairs a Program with the cwd it was loaded against so we can
@@ -44,108 +44,108 @@ import (
44
44
  // two fountain verbs invoked in the same frame could land their Checker
45
45
  // reads on opposite sides of an internal mutation and corrupt state.
46
46
  type snapshotEntry struct {
47
- mu sync.Mutex
48
- prog *driver.Program
49
- cwd string
47
+ mu sync.Mutex
48
+ prog *driver.Program
49
+ cwd string
50
50
  }
51
51
 
52
52
  var (
53
- snapshotsMu sync.RWMutex
54
- snapshots = map[string]*snapshotEntry{}
55
- nextHandle atomic.Uint64
53
+ snapshotsMu sync.RWMutex
54
+ snapshots = map[string]*snapshotEntry{}
55
+ nextHandle atomic.Uint64
56
56
  )
57
57
 
58
58
  // fountainAPIMap returns the verb → js.Func map appended to globalThis[apiName]
59
59
  // during Expose. Kept in a helper so host.go's API map stays scannable.
60
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
- }
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
72
  }
73
73
 
74
74
  // SnapshotResult is the response shape for `snapshot()`.
75
75
  type SnapshotResult struct {
76
- Handle string `json:"handle"`
76
+ Handle string `json:"handle"`
77
77
  }
78
78
 
79
79
  // ReleaseSnapshotResult is the response shape for `releaseSnapshot()`.
80
80
  type ReleaseSnapshotResult struct {
81
- Released bool `json:"released"`
81
+ Released bool `json:"released"`
82
82
  }
83
83
 
84
84
  // ListSnapshotsResult is the response shape for `snapshots()`.
85
85
  type ListSnapshotsResult struct {
86
- Handles []string `json:"handles"`
86
+ Handles []string `json:"handles"`
87
87
  }
88
88
 
89
89
  // GetSourceFilesResult is the response shape for `getSourceFiles()`.
90
90
  type GetSourceFilesResult struct {
91
- Files []string `json:"files"`
91
+ Files []string `json:"files"`
92
92
  }
93
93
 
94
94
  // GetSourceFileTextResult is the response shape for `getSourceFileText()`.
95
95
  type GetSourceFileTextResult struct {
96
- Text string `json:"text"`
96
+ Text string `json:"text"`
97
97
  }
98
98
 
99
99
  // GetDiagnosticsResult is the response shape for `getDiagnostics()`.
100
100
  type GetDiagnosticsResult struct {
101
- Diagnostics []CompileDiagnostic `json:"diagnostics"`
101
+ Diagnostics []CompileDiagnostic `json:"diagnostics"`
102
102
  }
103
103
 
104
104
  // NodeInfo is the serialized AST node returned by `getNodeAtPosition`.
105
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"`
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
111
  }
112
112
 
113
113
  // GetNodeAtPositionResult is the response shape for `getNodeAtPosition()`.
114
114
  type GetNodeAtPositionResult struct {
115
- Node *NodeInfo `json:"node"`
115
+ Node *NodeInfo `json:"node"`
116
116
  }
117
117
 
118
118
  // TypeInfo is the serialized type returned by `getTypeAtPosition`.
119
119
  type TypeInfo struct {
120
- Text string `json:"text"`
121
- Flags int `json:"flags"`
120
+ Text string `json:"text"`
121
+ Flags int `json:"flags"`
122
122
  }
123
123
 
124
124
  // GetTypeAtPositionResult is the response shape for `getTypeAtPosition()`.
125
125
  type GetTypeAtPositionResult struct {
126
- Type *TypeInfo `json:"type"`
126
+ Type *TypeInfo `json:"type"`
127
127
  }
128
128
 
129
129
  // SymbolDeclaration is the serialized declaration site returned by
130
130
  // `getSymbolAtPosition`.
131
131
  type SymbolDeclaration struct {
132
- File *string `json:"file"`
133
- Pos int `json:"pos"`
134
- End int `json:"end"`
132
+ File *string `json:"file"`
133
+ Pos int `json:"pos"`
134
+ End int `json:"end"`
135
135
  }
136
136
 
137
137
  // SymbolInfo is the serialized symbol returned by `getSymbolAtPosition`.
138
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"`
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
144
  }
145
145
 
146
146
  // GetSymbolAtPositionResult is the response shape for `getSymbolAtPosition()`.
147
147
  type GetSymbolAtPositionResult struct {
148
- Symbol *SymbolInfo `json:"symbol"`
148
+ Symbol *SymbolInfo `json:"symbol"`
149
149
  }
150
150
 
151
151
  // jsSnapshot({cwd, tsconfig?}) → Promise<ITtscResult>.
@@ -153,35 +153,35 @@ type GetSymbolAtPositionResult struct {
153
153
  // Loads the project once and retains the Program for follow-up queries. The
154
154
  // returned handle is opaque; treat it as a string.
155
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
- })
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
185
  }
186
186
 
187
187
  // jsReleaseSnapshot({handle}) → Promise<ITtscResult>.
@@ -194,100 +194,100 @@ func jsSnapshot(this js.Value, args []js.Value) any {
194
194
  // (withSnapshot's RLock) finishes before the Program is closed. This is the
195
195
  // pair of the TOCTOU guarantee documented on withSnapshot.
196
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
- })
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
221
  }
222
222
 
223
223
  // jsListSnapshots() → Promise<ITtscResult>. Debug aid — lets a JS embedder
224
224
  // verify it has released what it thinks it has.
225
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
- })
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
235
  }
236
236
 
237
237
  // jsGetSourceFiles({handle}) → Promise<ITtscResult>. Lists the non-
238
238
  // declaration source files in the program, keyed by project-relative path
239
239
  // (same convention as build/transform output).
240
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
- })
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
249
  }
250
250
 
251
251
  // jsGetSourceFileText({handle, path}) → Promise<ITtscResult>. Returns the
252
252
  // current source text the Program is holding for the file. After transform
253
253
  // plugins have run, this reflects the post-transform text.
254
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
- })
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
266
  }
267
267
 
268
268
  // jsGetDiagnostics({handle, file?}) → Promise<ITtscResult>. Returns the same
269
269
  // diagnostic shape as build/check/transform. When `file` is set, results are
270
270
  // filtered to that project-relative path.
271
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
- })
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
291
  }
292
292
 
293
293
  // jsGetNodeAtPosition({handle, path, position}) → Promise<ITtscResult>.
@@ -296,10 +296,10 @@ func jsGetDiagnostics(this js.Value, args []js.Value) any {
296
296
  // should resolve it to a byte offset before calling (the @ttsc/playground
297
297
  // helper does this).
298
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
- })
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
303
  }
304
304
 
305
305
  // jsGetTypeAtPosition({handle, path, position}) → Promise<ITtscResult>.
@@ -307,39 +307,39 @@ func jsGetNodeAtPosition(this js.Value, args []js.Value) any {
307
307
  // pinned single checker for its type. Returns `{type: null}` when there is no
308
308
  // node at that position or no checker is available.
309
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
- })
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
326
  }
327
327
 
328
328
  // jsGetSymbolAtPosition({handle, path, position}) → Promise<ITtscResult>.
329
329
  // Returns `{symbol: null}` when the node at position has no associated
330
330
  // symbol (e.g. punctuation, whitespace).
331
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
- })
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
343
  }
344
344
 
345
345
  // withSnapshot is the shared envelope for every read-only fountain verb. It
@@ -354,22 +354,22 @@ func jsGetSymbolAtPosition(this js.Value, args []js.Value) any {
354
354
  // thread-safe; two fountain verbs invoked in the same frame could
355
355
  // otherwise interleave Checker state mutations.
356
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
- })
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
373
  }
374
374
 
375
375
  // withSnapshotPosition is the shared envelope for the 3 position-bound
@@ -378,128 +378,128 @@ func withSnapshot(args []js.Value, fn func(*snapshotEntry, js.Value) any) any {
378
378
  // out-of-range offset reaches `ast.GetNodeAtPosition` only as a clean
379
379
  // error response instead of an internal panic.
380
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
- })
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
403
  }
404
404
 
405
405
  // resolveSnapshotFile finds a SourceFile by path, accepting either an
406
406
  // absolute path or a path relative to the snapshot's cwd.
407
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
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
418
  }
419
419
 
420
420
  // snapshotFileKey mirrors apiOutputKey but is exported for use by all
421
421
  // fountain endpoints that return file paths.
422
422
  func snapshotFileKey(cwd, fileName string) string {
423
- return apiOutputKey(cwd, fileName)
423
+ return apiOutputKey(cwd, fileName)
424
424
  }
425
425
 
426
426
  // fountainOK wraps a JSON-able payload in the standard `{code, stdout,
427
427
  // stderr, result}` envelope. The payload is JSON-encoded into `result`; JS
428
428
  // callers `parseResult<T>` it the same way they do for build/check/transform.
429
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
- })
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
440
  }
441
441
 
442
442
  // nodeInfoOf converts a *ast.Node into the JSON-serializable NodeInfo.
443
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
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
457
  }
458
458
 
459
459
  // symbolInfoOf converts a *ast.Symbol into the JSON-serializable SymbolInfo,
460
460
  // including up to a few declaration sites so callers can implement "go to
461
461
  // definition" without follow-up snapshot queries.
462
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
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
499
  }
500
500
 
501
501
  // astKindName renders an ast.Kind to its string representation. ast.Kind has
502
502
  // a Stringer impl in tsgo (`%v` produces "FunctionDeclaration" etc.).
503
503
  func astKindName(k ast.Kind) string {
504
- return fmt.Sprintf("%v", k)
504
+ return fmt.Sprintf("%v", k)
505
505
  }