@ttsc/wasm 0.13.1 → 0.14.0-dev.20250530.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 +60 -15
- 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 +179 -0
- package/lib/src/bootTtsc.js.map +1 -0
- package/lib/src/createMemFS.d.ts +11 -0
- package/lib/src/{MemFS.js → createMemFS.js} +28 -63
- 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 +8 -0
- package/lib/src/parseResult.js +20 -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/{instantiate.d.ts → structures/IBootTtscOptions.d.ts} +2 -12
- 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 +12 -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 +212 -0
- package/src/{MemFS.ts → createMemFS.ts} +12 -231
- package/src/index.ts +5 -17
- package/src/parseResult.ts +16 -0
- package/src/structures/IBootResult.ts +10 -0
- package/src/structures/IBootTtscOptions.ts +21 -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 +13 -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.js +0 -88
- package/lib/src/instantiate.js.map +0 -1
- package/src/api.ts +0 -145
- package/src/instantiate.ts +0 -137
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ In-browser ttsc playground scaffolding. Compose ttsc + typescript-go with your o
|
|
|
6
6
|
|
|
7
7
|
## What you get
|
|
8
8
|
|
|
9
|
-
- A base `ttsc.wasm` (under `dist/`) that exposes vanilla `build` / `check` / `transform
|
|
9
|
+
- A base `ttsc.wasm` (under `dist/`) that exposes vanilla `build` / `check` / `transform`, no plugins linked. Useful as a sanity test or a no-plugin baseline.
|
|
10
10
|
- A Go helper package (`host/`) plugin authors import from their own `main_wasm.go` to bind a wasm to `globalThis[yourApiName]`.
|
|
11
11
|
- A JS runtime (`bootTtsc`, `createMemFS`, typed `ITtscApi` surface) that loads any host-built wasm into a Web Worker.
|
|
12
12
|
|
|
@@ -39,11 +39,7 @@ func main() {
|
|
|
39
39
|
}
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
The native sibling `main.go` is recommended when you want `go run ./cmd/your-wasm`
|
|
43
|
-
to smoke-test the same `host.Plugin` dispatchers without the browser MemFS
|
|
44
|
-
bridge. See `packages/wasm/cmd/ttsc-wasm/main.go` and
|
|
45
|
-
`website/compiler/cmd/playground/main.go` in the repo for reference layouts.
|
|
46
|
-
A minimal custom plugin dispatcher looks like this:
|
|
42
|
+
The native sibling `main.go` is recommended when you want `go run ./cmd/your-wasm` to smoke-test the same `host.Plugin` dispatchers without the browser MemFS bridge. See `packages/wasm/cmd/ttsc-wasm/main.go` and `website/compiler/cmd/playground/main.go` in the repo for reference layouts. A minimal custom plugin dispatcher looks like this:
|
|
47
43
|
|
|
48
44
|
```go
|
|
49
45
|
//go:build !js
|
|
@@ -81,11 +77,7 @@ GOOS=js GOARCH=wasm go build -trimpath -ldflags "-s -w" \
|
|
|
81
77
|
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" public/wasm_exec.js
|
|
82
78
|
```
|
|
83
79
|
|
|
84
|
-
Keep `wasm_exec.js` from the same Go toolchain that built your wasm. The
|
|
85
|
-
prebuilt `dist/ttsc.wasm` and `dist/wasm_exec.js` pair already match each
|
|
86
|
-
other; custom binaries should copy the loader from their own toolchain. Older
|
|
87
|
-
Go installs may keep the loader under `$(go env GOROOT)/misc/wasm/wasm_exec.js`
|
|
88
|
-
instead of `lib/wasm`.
|
|
80
|
+
Keep `wasm_exec.js` from the same Go toolchain that built your wasm. The prebuilt `dist/ttsc.wasm` and `dist/wasm_exec.js` pair already match each other; custom binaries should copy the loader from their own toolchain. Older Go installs may keep the loader under `$(go env GOROOT)/misc/wasm/wasm_exec.js` instead of `lib/wasm`.
|
|
89
81
|
|
|
90
82
|
### Stamping version metadata at link time
|
|
91
83
|
|
|
@@ -99,7 +91,7 @@ GOOS=js GOARCH=wasm go build -trimpath \
|
|
|
99
91
|
|
|
100
92
|
The `version`, `commit`, and `date` variables in `host/host.go` are all overridable. `globalThis[apiName].version()` reads from them.
|
|
101
93
|
|
|
102
|
-
4. Boot it from JS (a Web Worker is **required
|
|
94
|
+
4. Boot it from JS (a Web Worker is **required**: Go's `syscall/js` blocks the runtime while the wasm is alive; running on the main thread freezes the page). Use a classic Worker or a bundler target that still exposes `importScripts`; `bootTtsc` imports `wasm_exec.js` before starting Go.
|
|
103
95
|
|
|
104
96
|
```ts
|
|
105
97
|
import { bootTtsc } from "@ttsc/wasm";
|
|
@@ -116,7 +108,60 @@ const result = await api.build({ cwd: "/work" });
|
|
|
116
108
|
console.log(result.result); // JSON: { diagnostics, output }
|
|
117
109
|
```
|
|
118
110
|
|
|
119
|
-
Booting two wasms with the same `apiName`
|
|
111
|
+
Booting two wasms with the same `apiName` overwrites the previous global binding; pick a unique `apiName` per binary. `bootTtsc` also installs shared `fs` and `process` globals in its Worker, so use separate Workers when binaries need independent filesystems.
|
|
112
|
+
|
|
113
|
+
## Fountain API (snapshot, AST, type checker)
|
|
114
|
+
|
|
115
|
+
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.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { bootTtsc, parseResult } from "@ttsc/wasm";
|
|
119
|
+
import type {
|
|
120
|
+
ITtscSnapshotResult,
|
|
121
|
+
ITtscTypeAtPositionResult,
|
|
122
|
+
} from "@ttsc/wasm";
|
|
123
|
+
|
|
124
|
+
const { api, host } = await bootTtsc({ wasmUrl, apiName });
|
|
125
|
+
|
|
126
|
+
host.writeFile("/work/tsconfig.json", "{}");
|
|
127
|
+
host.writeFile("/work/src/index.ts", "export const x: number = 1;");
|
|
128
|
+
|
|
129
|
+
const snap = parseResult<ITtscSnapshotResult>(
|
|
130
|
+
await api.snapshot({ cwd: "/work" }),
|
|
131
|
+
);
|
|
132
|
+
const handle = snap!.handle;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const typeAt = parseResult<ITtscTypeAtPositionResult>(
|
|
136
|
+
await api.getTypeAtPosition({
|
|
137
|
+
handle,
|
|
138
|
+
path: "src/index.ts",
|
|
139
|
+
position: 18, // byte offset of `x`
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
console.log(typeAt?.type?.text); // → "number"
|
|
143
|
+
} finally {
|
|
144
|
+
await api.releaseSnapshot({ handle });
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Verbs and payload types:
|
|
149
|
+
|
|
150
|
+
| Verb | Payload type |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `snapshot({ cwd, tsconfig? })` | `ITtscSnapshotResult` `{ handle }` |
|
|
153
|
+
| `releaseSnapshot({ handle })` | `ITtscReleaseSnapshotResult` `{ released }` |
|
|
154
|
+
| `snapshots()` | `ITtscSnapshotsResult` `{ handles }` |
|
|
155
|
+
| `getSourceFiles({ handle })` | `ITtscSourceFilesResult` `{ files }` |
|
|
156
|
+
| `getSourceFileText({ handle, path })` | `ITtscSourceFileTextResult` `{ text }` |
|
|
157
|
+
| `getDiagnostics({ handle, file? })` | `ITtscFountainDiagnosticsResult` `{ diagnostics }` |
|
|
158
|
+
| `getNodeAtPosition({ handle, path, position })` | `ITtscNodeAtPositionResult` `{ node }` |
|
|
159
|
+
| `getTypeAtPosition({ handle, path, position })` | `ITtscTypeAtPositionResult` `{ type }` |
|
|
160
|
+
| `getSymbolAtPosition({ handle, path, position })` | `ITtscSymbolAtPositionResult` `{ symbol }` |
|
|
161
|
+
|
|
162
|
+
`position` is a **byte offset** into the source text. The same coordinate TypeScript-Go uses internally. JS callers that have a UTF-16 `(line, character)` pair (e.g. From Monaco) must convert it before calling.
|
|
163
|
+
|
|
164
|
+
**Lifecycle:** JS owns the handle. The wasm keeps the program (parsed AST, checker pool lease, every source file) alive until you call `releaseSnapshot`. Leaking handles leaks memory in the wasm linear heap.
|
|
120
165
|
|
|
121
166
|
## Plugin contract
|
|
122
167
|
|
|
@@ -129,7 +174,7 @@ type Plugin interface {
|
|
|
129
174
|
}
|
|
130
175
|
```
|
|
131
176
|
|
|
132
|
-
The host installs `globalThis[apiName].plugin({ name, command, ...opts })` that translates the JS options object into a CLI-shaped argv and calls your plugin's `Run`. Your `Run` body can forward to the same function the native sidecar's `main.go` calls
|
|
177
|
+
The host installs `globalThis[apiName].plugin({ name, command, ...opts })` that translates the JS options object into a CLI-shaped argv and calls your plugin's `Run`. Your `Run` body can forward to the same function the native sidecar's `main.go` calls, for example `utility.RunBuild(args)` for plugins backed by `packages/ttsc/utility`.
|
|
133
178
|
|
|
134
179
|
## Published-tarball Go module layout
|
|
135
180
|
|
|
@@ -137,7 +182,7 @@ The published `@ttsc/wasm` tarball ships:
|
|
|
137
182
|
|
|
138
183
|
- A rewritten root `go.mod` whose `replace` directives point at `./shim-vendor/shim/*` (vendored at pack time from `packages/ttsc/shim/`).
|
|
139
184
|
- The full `host/`, `cmd/`, `build/` Go source so consumers can `go build -tags '...'` their own wasm against your host helper.
|
|
140
|
-
- `dist/ttsc.wasm` + `dist/wasm_exec.js
|
|
185
|
+
- `dist/ttsc.wasm` + `dist/wasm_exec.js`: the no-plugin sanity binary and the Go runtime loader.
|
|
141
186
|
|
|
142
187
|
The tarball intentionally drops the `replace github.com/samchon/ttsc/packages/ttsc => ../ttsc` directive that the in-repo `go.mod` carries: consumers of the published module who want to rebuild the wasm must supply their own `replace` (or vendor `packages/ttsc` themselves). The published `dist/ttsc.wasm` is plug-and-play for runtime use; the Go module is for **plugin authors extending the host**, not for vanilla consumers.
|
|
143
188
|
|
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
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
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
|
|
23
|
-
// command, opts)
|
|
24
|
-
// callback.
|
|
25
|
-
//
|
|
26
|
-
//
|
|
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
|
package/host/fountain.go
ADDED
|
@@ -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
|
+
}
|