ttsc 0.11.0 → 0.12.0

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 (79) hide show
  1. package/README.md +25 -6
  2. package/cmd/ttsc/api_compile.go +101 -106
  3. package/cmd/ttsc/api_compile_branches_test.go +81 -0
  4. package/cmd/ttsc/api_transform.go +49 -54
  5. package/cmd/ttsc/api_transform_branches_test.go +41 -0
  6. package/cmd/ttsc/build.go +108 -109
  7. package/cmd/ttsc/build_branches_test.go +130 -0
  8. package/cmd/ttsc/helpers_test.go +47 -0
  9. package/cmd/ttsc/main.go +104 -103
  10. package/cmd/ttsc/main_alias_branches_test.go +29 -0
  11. package/cmd/ttscserver/main.go +104 -96
  12. package/cmd/ttscserver/run_lsp_prefers_tsgo_flag_test.go +42 -0
  13. package/cmd/ttscserver/run_lsp_reports_server_error_test.go +23 -25
  14. package/cmd/ttscserver/run_lsp_uses_tsgo_environment_test.go +44 -0
  15. package/cmd/utility-host/main.go +42 -0
  16. package/driver/lsp.go +60 -0
  17. package/driver/plugins.go +122 -0
  18. package/driver/program.go +346 -356
  19. package/driver/rewrite.go +336 -339
  20. package/{driver → internal/lspserver}/lsp_envelope.go +70 -64
  21. package/{driver → internal/lspserver}/lsp_frame.go +62 -62
  22. package/{driver → internal/lspserver}/lsp_plugin.go +58 -58
  23. package/internal/lspserver/lsp_proxy.go +391 -0
  24. package/internal/lspserver/lsp_server.go +231 -0
  25. package/lib/TtscCompiler.d.ts +3 -3
  26. package/lib/TtscCompiler.js +20 -6
  27. package/lib/TtscCompiler.js.map +1 -1
  28. package/lib/compiler/internal/resolveTsgo.d.ts +1 -0
  29. package/lib/compiler/internal/resolveTsgo.js +15 -4
  30. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  31. package/lib/compiler/internal/runBuild.js +12 -4
  32. package/lib/compiler/internal/runBuild.js.map +1 -1
  33. package/lib/compiler/internal/sharedHostHelpers.d.ts +12 -33
  34. package/lib/compiler/internal/sharedHostHelpers.js +27 -70
  35. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  36. package/lib/compiler/internal/transformProjectInMemory.js +12 -5
  37. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  38. package/lib/index.d.ts +0 -1
  39. package/lib/index.js +0 -1
  40. package/lib/index.js.map +1 -1
  41. package/lib/launcher/internal/runTtsc.js +9 -4
  42. package/lib/launcher/internal/runTtsc.js.map +1 -1
  43. package/lib/launcher/internal/runTtscserver.d.ts +1 -0
  44. package/lib/launcher/internal/runTtscserver.js +38 -1
  45. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  46. package/lib/plugin/internal/buildSourcePlugin.d.ts +3 -0
  47. package/lib/plugin/internal/buildSourcePlugin.js +444 -41
  48. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  49. package/lib/plugin/internal/loadProjectPlugins.js +153 -34
  50. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  51. package/lib/structures/ITtscCompilerContext.d.ts +2 -1
  52. package/lib/structures/ITtscPlugin.d.ts +8 -13
  53. package/lib/structures/ITtscPluginFactoryContext.d.ts +4 -5
  54. package/lib/structures/ITtscProjectPluginConfig.d.ts +2 -2
  55. package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +4 -2
  56. package/package.json +8 -8
  57. package/shim/lsp/shim.go +11 -17
  58. package/src/TtscCompiler.ts +20 -7
  59. package/src/compiler/internal/resolveTsgo.ts +18 -5
  60. package/src/compiler/internal/runBuild.ts +17 -4
  61. package/src/compiler/internal/sharedHostHelpers.ts +34 -67
  62. package/src/compiler/internal/transformProjectInMemory.ts +18 -5
  63. package/src/index.ts +0 -1
  64. package/src/launcher/internal/runTtsc.ts +9 -4
  65. package/src/launcher/internal/runTtscserver.ts +43 -1
  66. package/src/plugin/internal/buildSourcePlugin.ts +475 -41
  67. package/src/plugin/internal/loadProjectPlugins.ts +199 -39
  68. package/src/structures/ITtscCompilerContext.ts +2 -1
  69. package/src/structures/ITtscPlugin.ts +9 -13
  70. package/src/structures/ITtscPluginFactoryContext.ts +4 -5
  71. package/src/structures/ITtscProjectPluginConfig.ts +2 -2
  72. package/src/structures/internal/ITtscLoadedNativePlugin.ts +4 -2
  73. package/utility/host.go +267 -1296
  74. package/driver/lsp_proxy.go +0 -394
  75. package/driver/lsp_server.go +0 -197
  76. package/lib/defineTtscPlugin.d.ts +0 -18
  77. package/lib/defineTtscPlugin.js +0 -7
  78. package/lib/defineTtscPlugin.js.map +0 -1
  79. package/src/defineTtscPlugin.ts +0 -26
@@ -1,394 +0,0 @@
1
- package driver
2
-
3
- import (
4
- "bytes"
5
- "context"
6
- "encoding/json"
7
- "errors"
8
- "fmt"
9
- "io"
10
- "sync"
11
- )
12
-
13
-
14
- // ErrCommandNotHandled is returned by PluginSource.ExecuteCommand for
15
- // commands ttsc does not own. Callers should fall through to upstream.
16
- var ErrCommandNotHandled = errors.New("lsp: command not handled by ttsc")
17
-
18
- const (
19
- methodPublishDiagnostics = "textDocument/publishDiagnostics"
20
- methodCodeAction = "textDocument/codeAction"
21
- methodExecuteCommand = "workspace/executeCommand"
22
- methodCancelRequest = "$/cancelRequest"
23
- )
24
-
25
- // ProxyOptions wires the byte-level proxy together. ttscserver creates
26
- // the upstream pipes around an embedded tsgo lsp.Server and hands the
27
- // proxy editor stdio plus those pipe ends.
28
- type ProxyOptions struct {
29
- EditorIn io.Reader
30
- EditorOut io.Writer
31
- UpstreamIn io.Writer // we write here; tsgo lsp.Server reads
32
- UpstreamOut io.Reader // tsgo lsp.Server writes here; we read
33
- Source PluginSource
34
- }
35
-
36
- // Proxy bridges the editor and an upstream tsgo lsp.Server, intercepting
37
- // the message types ttsc cares about (publishDiagnostics merge, code
38
- // action augmentation, executeCommand for ttsc-owned commands).
39
- type Proxy struct {
40
- editorIn io.Reader
41
- editorOut io.Writer
42
- upstreamIn io.Writer
43
- upstreamOut io.Reader
44
- source PluginSource
45
-
46
- writeMu sync.Mutex // serializes WriteFrame calls to editorOut
47
-
48
- pendingMu sync.Mutex
49
- pendingActions map[string]pendingCodeActionRequest
50
- }
51
-
52
- type pendingCodeActionRequest struct {
53
- uri string
54
- rng LSPRange
55
- ctx LSPCodeActionContext
56
- }
57
-
58
- // NewProxy returns a Proxy ready to Run. The PluginSource is required;
59
- // pass NullPluginSource{} for a no-contribution setup.
60
- func NewProxy(opts ProxyOptions) *Proxy {
61
- source := opts.Source
62
- if source == nil {
63
- source = NullPluginSource{}
64
- }
65
- return &Proxy{
66
- editorIn: opts.EditorIn,
67
- editorOut: opts.EditorOut,
68
- upstreamIn: opts.UpstreamIn,
69
- upstreamOut: opts.UpstreamOut,
70
- source: source,
71
- pendingActions: make(map[string]pendingCodeActionRequest),
72
- }
73
- }
74
-
75
- // Run drives both pump goroutines until they return. Pumps return when
76
- // their input stream closes (ErrFrameClosed), when context cancellation
77
- // has already been observed by the upstream/editor closers, or when a
78
- // pipe write fails. ErrFrameClosed and context.Canceled are folded into
79
- // a nil result so editor shutdown does not look like a crash.
80
- func (p *Proxy) Run(ctx context.Context) error {
81
- errCh := make(chan error, 2)
82
- go func() { errCh <- p.pumpEditorToUpstream(ctx) }()
83
- go func() { errCh <- p.pumpUpstreamToEditor(ctx) }()
84
-
85
- var first error
86
- for i := 0; i < 2; i++ {
87
- err := <-errCh
88
- if first == nil && err != nil && !errors.Is(err, ErrFrameClosed) {
89
- first = err
90
- }
91
- }
92
- return first
93
- }
94
-
95
- // pumpEditorToUpstream reads frames from the editor, decides whether to
96
- // forward verbatim or handle locally (executeCommand for ttsc commands,
97
- // codeAction request bookkeeping), and writes the chosen frame. When the
98
- // editor closes its end (ErrFrameClosed) the pump closes the upstream
99
- // writer so tsgo's server-side Read returns EOF and its Run loop drains;
100
- // without that nudge tsgo would wait forever for more input.
101
- func (p *Proxy) pumpEditorToUpstream(_ context.Context) error {
102
- fr := NewFrameReader(p.editorIn)
103
- for {
104
- _, body, err := fr.Read()
105
- if err != nil {
106
- if errors.Is(err, ErrFrameClosed) {
107
- p.closeUpstreamInput()
108
- }
109
- return err
110
- }
111
- env, parseErr := ParseEnvelope(body)
112
- if parseErr != nil {
113
- if forwardErr := WriteFrame(p.upstreamIn, body); forwardErr != nil {
114
- return forwardErr
115
- }
116
- continue
117
- }
118
- if p.handleEditorEnvelope(env, body) {
119
- continue
120
- }
121
- if err := WriteFrame(p.upstreamIn, body); err != nil {
122
- return err
123
- }
124
- }
125
- }
126
-
127
- // closeUpstreamInput closes the upstream writer (if it is also an
128
- // io.Closer) so the embedded server reads io.EOF and exits its readLoop.
129
- // We type-assert because the public ProxyOptions surface promises only
130
- // io.Writer; RunLSPServer always passes an *io.PipeWriter in practice.
131
- func (p *Proxy) closeUpstreamInput() {
132
- if closer, ok := p.upstreamIn.(io.Closer); ok {
133
- _ = closer.Close()
134
- }
135
- }
136
-
137
- // handleEditorEnvelope returns true if the envelope was fully processed
138
- // locally (responded to without forwarding upstream).
139
- func (p *Proxy) handleEditorEnvelope(env Envelope, body []byte) bool {
140
- switch env.Method {
141
- case methodExecuteCommand:
142
- if env.IsRequest() && p.tryExecuteCommand(env) {
143
- return true
144
- }
145
- case methodCodeAction:
146
- if env.IsRequest() {
147
- p.rememberCodeActionRequest(env)
148
- }
149
- case methodCancelRequest:
150
- // $/cancelRequest names an in-flight id the editor has given up on.
151
- // The proxy drops any pending codeAction entry for that id so the
152
- // map cannot grow unbounded across long editor sessions, then lets
153
- // the notification continue to upstream.
154
- p.forgetCancelledRequest(env)
155
- }
156
- return false
157
- }
158
-
159
- // forgetCancelledRequest removes any pending codeAction entry whose id
160
- // the editor cancelled. The notification still flows to upstream so
161
- // tsgo can respond with its own cancellation error. The id is keyed
162
- // through the shared normalizer so a cancel for `1.0` deletes an entry
163
- // stored under `1` (and vice versa for string-vs-numeric encodings).
164
- func (p *Proxy) forgetCancelledRequest(env Envelope) {
165
- var params struct {
166
- ID json.RawMessage `json:"id"`
167
- }
168
- if err := json.Unmarshal(env.Params, &params); err != nil {
169
- return
170
- }
171
- key := idKeyFromRaw(params.ID)
172
- if key == "" {
173
- return
174
- }
175
- p.pendingMu.Lock()
176
- defer p.pendingMu.Unlock()
177
- delete(p.pendingActions, key)
178
- }
179
-
180
- // rememberCodeActionRequest stores the request payload so the matching
181
- // response from upstream can be augmented with ttsc-owned code actions
182
- // for the same range.
183
- func (p *Proxy) rememberCodeActionRequest(env Envelope) {
184
- var params struct {
185
- TextDocument struct {
186
- URI string `json:"uri"`
187
- } `json:"textDocument"`
188
- Range LSPRange `json:"range"`
189
- Context LSPCodeActionContext `json:"context"`
190
- }
191
- if err := json.Unmarshal(env.Params, &params); err != nil {
192
- return
193
- }
194
- p.pendingMu.Lock()
195
- defer p.pendingMu.Unlock()
196
- p.pendingActions[env.IDKey()] = pendingCodeActionRequest{
197
- uri: params.TextDocument.URI,
198
- rng: params.Range,
199
- ctx: params.Context,
200
- }
201
- }
202
-
203
- // tryExecuteCommand handles workspace/executeCommand requests whose
204
- // command id is registered with the PluginSource. Returns true on a
205
- // successful local response; false when the command should fall through
206
- // to upstream tsgo.
207
- func (p *Proxy) tryExecuteCommand(env Envelope) bool {
208
- var params struct {
209
- Command string `json:"command"`
210
- Arguments []json.RawMessage `json:"arguments,omitempty"`
211
- }
212
- if err := json.Unmarshal(env.Params, &params); err != nil {
213
- return false
214
- }
215
- if !p.ownsCommand(params.Command) {
216
- return false
217
- }
218
- edit, err := p.source.ExecuteCommand(params.Command, params.Arguments)
219
- if errors.Is(err, ErrCommandNotHandled) {
220
- return false
221
- }
222
- if err != nil {
223
- p.writeError(env.ID, fmt.Sprintf("ttsc command %q failed: %v", params.Command, err))
224
- return true
225
- }
226
- // Cycle 1 returns the WorkspaceEdit inside the executeCommand response
227
- // instead of sending workspace/applyEdit as a server→client request.
228
- // ttsc owns both ends (its VSCode extension), so the extension applies
229
- // the edit on its side. Sticking to one direction avoids tracking our
230
- // own outgoing request ids in the proxy.
231
- if edit == nil {
232
- p.writeResult(env.ID, nil)
233
- return true
234
- }
235
- p.writeResult(env.ID, edit)
236
- return true
237
- }
238
-
239
- func (p *Proxy) ownsCommand(command string) bool {
240
- for _, id := range p.source.CommandIDs() {
241
- if id == command {
242
- return true
243
- }
244
- }
245
- return false
246
- }
247
-
248
- // pumpUpstreamToEditor reads frames from the embedded tsgo server,
249
- // augments publishDiagnostics and codeAction responses, and forwards
250
- // every other frame untouched. The loop terminates on Read error; ctx
251
- // propagation happens through pipe closure by RunLSPServer's deferred
252
- // cleanup goroutines.
253
- func (p *Proxy) pumpUpstreamToEditor(_ context.Context) error {
254
- fr := NewFrameReader(p.upstreamOut)
255
- for {
256
- _, body, err := fr.Read()
257
- if err != nil {
258
- return err
259
- }
260
- env, parseErr := ParseEnvelope(body)
261
- if parseErr != nil {
262
- if forwardErr := p.writeEditorFrame(body); forwardErr != nil {
263
- return forwardErr
264
- }
265
- continue
266
- }
267
- augmented := p.augmentUpstream(env, body)
268
- if err := p.writeEditorFrame(augmented); err != nil {
269
- return err
270
- }
271
- }
272
- }
273
-
274
- // augmentUpstream returns the (possibly rewritten) body to forward. For
275
- // publishDiagnostics it merges ttsc plugin diagnostics; for codeAction
276
- // responses tied to a remembered request it appends ttsc actions.
277
- //
278
- // Cancellation and response are handled in two independent goroutines
279
- // (pumpEditorToUpstream owns $/cancelRequest cleanup, pumpUpstreamToEditor
280
- // owns response augmentation). When the response wins the race against
281
- // a pending cancel, pendingActions[env.IDKey()] is still populated and
282
- // the response is augmented before forgetCancelledRequest runs — the
283
- // editor is expected to discard the late response per LSP $/cancelRequest
284
- // semantics. When the cancel wins, the pending entry is gone and
285
- // augmentation skips cleanly.
286
- func (p *Proxy) augmentUpstream(env Envelope, body []byte) []byte {
287
- if env.IsNotification() && env.Method == methodPublishDiagnostics {
288
- if merged, ok := p.mergePublishDiagnostics(env); ok {
289
- return merged
290
- }
291
- }
292
- if env.IsResponse() {
293
- p.pendingMu.Lock()
294
- pending, ok := p.pendingActions[env.IDKey()]
295
- if ok {
296
- delete(p.pendingActions, env.IDKey())
297
- }
298
- p.pendingMu.Unlock()
299
- if ok {
300
- if augmented, augOk := p.appendCodeActions(env, pending); augOk {
301
- return augmented
302
- }
303
- }
304
- }
305
- return body
306
- }
307
-
308
- // mergePublishDiagnostics decodes the publishDiagnostics params, asks
309
- // the PluginSource for the URI's contributions, appends them, and
310
- // re-encodes the envelope. Returns the rewritten body and true on
311
- // success; ok=false means "nothing to merge — forward the upstream body
312
- // verbatim." Re-encoding our own types is guaranteed by their
313
- // definitions in lsp_plugin.go, so the marshal errors are not checked.
314
- func (p *Proxy) mergePublishDiagnostics(env Envelope) ([]byte, bool) {
315
- var params struct {
316
- URI string `json:"uri"`
317
- Version *int `json:"version,omitempty"`
318
- Diagnostics []json.RawMessage `json:"diagnostics"`
319
- }
320
- if err := json.Unmarshal(env.Params, &params); err != nil {
321
- return nil, false
322
- }
323
- extras := p.source.Diagnostics(LSPDocumentVersion{URI: params.URI, Version: params.Version})
324
- if len(extras) == 0 {
325
- return nil, false
326
- }
327
- for _, extra := range extras {
328
- raw, _ := json.Marshal(extra)
329
- params.Diagnostics = append(params.Diagnostics, raw)
330
- }
331
- env.Params, _ = json.Marshal(params)
332
- body, _ := json.Marshal(env)
333
- return body, true
334
- }
335
-
336
- // appendCodeActions adds ttsc-owned code actions to a forwarded
337
- // codeAction response. The response body is either an array of actions
338
- // or null; we preserve that shape so editors that special-case null
339
- // keep working. The function refuses to splice into error responses
340
- // (JSON-RPC §5.1 forbids both `result` and `error` on the same frame)
341
- // and into non-array results (the LSP base protocol mandates an array
342
- // or null for codeAction; anything else means upstream returned a
343
- // shape ttsc cannot splice into, so the proxy forwards verbatim
344
- // rather than corrupting it).
345
- func (p *Proxy) appendCodeActions(env Envelope, pending pendingCodeActionRequest) ([]byte, bool) {
346
- if env.IsErrorResponse() {
347
- return nil, false
348
- }
349
- trimmed := bytes.TrimSpace(env.Result)
350
- if len(trimmed) > 0 && trimmed[0] != '[' && !bytes.Equal(trimmed, []byte("null")) {
351
- return nil, false
352
- }
353
- actions := p.source.CodeActions(pending.uri, pending.rng, pending.ctx)
354
- if len(actions) == 0 {
355
- return nil, false
356
- }
357
- var existing []json.RawMessage
358
- if len(trimmed) > 0 && !bytes.Equal(trimmed, []byte("null")) {
359
- if err := json.Unmarshal(trimmed, &existing); err != nil {
360
- return nil, false
361
- }
362
- }
363
- for _, action := range actions {
364
- raw, _ := json.Marshal(action)
365
- existing = append(existing, raw)
366
- }
367
- env.Result, _ = json.Marshal(existing)
368
- body, _ := json.Marshal(env)
369
- return body, true
370
- }
371
-
372
- func (p *Proxy) writeEditorFrame(body []byte) error {
373
- p.writeMu.Lock()
374
- defer p.writeMu.Unlock()
375
- return WriteFrame(p.editorOut, body)
376
- }
377
-
378
- func (p *Proxy) writeResult(id json.RawMessage, result any) error {
379
- rawResult, _ := json.Marshal(result)
380
- env := Envelope{JSONRPC: "2.0", ID: id, Result: rawResult}
381
- body, _ := json.Marshal(env)
382
- return p.writeEditorFrame(body)
383
- }
384
-
385
- func (p *Proxy) writeError(id json.RawMessage, message string) error {
386
- errPayload, _ := json.Marshal(struct {
387
- Code int `json:"code"`
388
- Message string `json:"message"`
389
- }{Code: -32603, Message: message})
390
- env := Envelope{JSONRPC: "2.0", ID: id, Error: errPayload}
391
- body, _ := json.Marshal(env)
392
- return p.writeEditorFrame(body)
393
- }
394
-
@@ -1,197 +0,0 @@
1
- package driver
2
-
3
- import (
4
- "context"
5
- "errors"
6
- "fmt"
7
- "io"
8
- "runtime/debug"
9
- "sync"
10
- "time"
11
-
12
- "github.com/microsoft/typescript-go/shim/bundled"
13
- shimlsp "github.com/microsoft/typescript-go/shim/lsp"
14
- "github.com/microsoft/typescript-go/shim/vfs/osvfs"
15
- )
16
-
17
- // ErrLSPUpstreamPanic wraps a panic recovered from inside the embedded
18
- // tsgo lsp.Server. Without recovery the host process would die when
19
- // tsgo crashed; with this wrapper the proxy can surface a typed error
20
- // to the editor instead.
21
- var ErrLSPUpstreamPanic = errors.New("ttscserver: embedded tsgo server panicked")
22
-
23
- // RecoverPanicAs runs fn and converts a panic into an
24
- // ErrLSPUpstreamPanic-wrapped error. defaultUpstreamRunner uses it and
25
- // downstream LSP-host embeddings can opt into the same recovery
26
- // contract; the recovered stack is attached for diagnostics.
27
- //
28
- // recover() per the Go spec catches panics but NOT runtime.Goexit, so
29
- // a Goexit raised from inside fn surfaces as a clean nil return here
30
- // (the upstream goroutine exits without an error). Hosting code that
31
- // must turn Goexit into a typed error should run fn in a separate
32
- // goroutine and join on a sentinel channel — outside this helper's
33
- // scope today.
34
- func RecoverPanicAs(fn func() error) (err error) {
35
- defer func() {
36
- if r := recover(); r != nil {
37
- err = fmt.Errorf("%w: %v\n%s", ErrLSPUpstreamPanic, r, debug.Stack())
38
- }
39
- }()
40
- return fn()
41
- }
42
-
43
- // LSPServerOptions wires ttscserver to its three channels of state:
44
- // editor stdio for the LSP transport, an optional ttsc PluginSource for
45
- // merging plugin diagnostics into the stream, and a working directory
46
- // the embedded tsgo server treats as the project root.
47
- type LSPServerOptions struct {
48
- // In is the editor-side reader; ttscserver reads JSON-RPC frames from
49
- // it and forwards or handles them.
50
- In io.Reader
51
- // Out is the editor-side writer; ttscserver writes both upstream
52
- // responses and locally-synthesized messages to it.
53
- Out io.Writer
54
- // Err is the upstream tsgo server's stderr sink. ttscserver does not
55
- // log to it directly.
56
- Err io.Writer
57
-
58
- // Cwd is the project root passed to tsgo's LSP. An empty string
59
- // panics inside tsgo, so RunLSPServer validates it up front.
60
- Cwd string
61
- // Source contributes ttsc plugin diagnostics / code actions /
62
- // executeCommand handling. Nil falls back to NullPluginSource{}.
63
- Source PluginSource
64
- // ProgressDelay matches tsgo's option for delaying the progress UI;
65
- // zero disables the delay.
66
- ProgressDelay time.Duration
67
- }
68
-
69
- // ErrLSPCwdRequired is returned when LSPServerOptions.Cwd is empty.
70
- // ttsc surfaces a clean error here instead of letting tsgo's panic
71
- // reach the editor.
72
- var ErrLSPCwdRequired = errors.New("ttscserver: cwd is required")
73
-
74
- // lspUpstreamRunner is the seam tests use to substitute the embedded
75
- // tsgo lsp.Server with a controllable fake. Production wires it to the
76
- // real shim-backed server.
77
- type lspUpstreamRunner func(ctx context.Context, in io.Reader, out io.Writer, opts LSPServerOptions) error
78
-
79
- // upstreamRunner is replaced in tests via WithUpstreamRunnerForTest.
80
- var upstreamRunner lspUpstreamRunner = defaultUpstreamRunner
81
-
82
- // WithUpstreamRunnerForTest substitutes the upstream runner used by
83
- // RunLSPServer. Returns a function the caller defers to restore the
84
- // production runner. The seam stays in the public API because tests
85
- // live in driver_test and otherwise have no way to bypass tsgo.
86
- func WithUpstreamRunnerForTest(runner lspUpstreamRunner) func() {
87
- prev := upstreamRunner
88
- upstreamRunner = runner
89
- return func() { upstreamRunner = prev }
90
- }
91
-
92
- func defaultUpstreamRunner(ctx context.Context, in io.Reader, out io.Writer, opts LSPServerOptions) error {
93
- return RecoverPanicAs(func() error {
94
- server := shimlsp.NewServer(&shimlsp.ServerOptions{
95
- In: shimlsp.ToReader(in),
96
- Out: shimlsp.ToWriter(out),
97
- Err: opts.Err,
98
- Cwd: opts.Cwd,
99
- FS: DefaultFS(),
100
- DefaultLibraryPath: bundled.LibPath(),
101
- TypingsLocation: osvfs.GetGlobalTypingsCacheLocation(),
102
- NpmInstall: DenyNpmInstall,
103
- ProgressDelay: opts.ProgressDelay,
104
- })
105
- return server.Run(ctx)
106
- })
107
- }
108
-
109
- // RunLSPServer starts the embedded tsgo lsp.Server and the byte-level
110
- // proxy, blocking until either side returns. The first non-graceful
111
- // error wins; ErrFrameClosed and context cancellation are treated as
112
- // clean shutdown so editor close sequences do not look like crashes.
113
- //
114
- // The lifecycle is:
115
- //
116
- // 1. Open two pipes around the embedded server (editor->server, server->editor).
117
- // 2. Spawn the upstream runner (real tsgo or a test fake) reading/writing those pipes.
118
- // 3. Run the proxy in parallel.
119
- // 4. A watchdog cascades context cancellation by closing every pipe so both
120
- // halves unblock; the goroutines' own defers close the rest.
121
- func RunLSPServer(ctx context.Context, opts LSPServerOptions) error {
122
- if opts.Cwd == "" {
123
- return ErrLSPCwdRequired
124
- }
125
- source := opts.Source
126
- if source == nil {
127
- source = NullPluginSource{}
128
- }
129
-
130
- upstreamInR, upstreamInW := io.Pipe()
131
- upstreamOutR, upstreamOutW := io.Pipe()
132
-
133
- proxy := NewProxy(ProxyOptions{
134
- EditorIn: opts.In,
135
- EditorOut: opts.Out,
136
- UpstreamIn: upstreamInW,
137
- UpstreamOut: upstreamOutR,
138
- Source: source,
139
- })
140
-
141
- serverCtx, cancel := context.WithCancel(ctx)
142
- defer cancel()
143
-
144
- // Watchdog: on cancel, close the writer ends of the upstream pipes so
145
- // both halves unblock with a clean io.EOF. Closing readers directly
146
- // would surface as io.ErrClosedPipe and make ttsc's error fold
147
- // ambiguous; closing writers preserves the ErrFrameClosed signal.
148
- go func() {
149
- <-serverCtx.Done()
150
- upstreamInW.Close()
151
- upstreamOutW.Close()
152
- }()
153
-
154
- var wg sync.WaitGroup
155
- var serverErr, proxyErr error
156
- wg.Add(2)
157
- go func() {
158
- defer wg.Done()
159
- defer cancel()
160
- defer upstreamOutW.Close()
161
- defer upstreamInR.Close()
162
- serverErr = upstreamRunner(serverCtx, upstreamInR, upstreamOutW, opts)
163
- }()
164
- go func() {
165
- defer wg.Done()
166
- defer cancel()
167
- defer upstreamInW.Close()
168
- defer upstreamOutR.Close()
169
- proxyErr = proxy.Run(serverCtx)
170
- }()
171
- wg.Wait()
172
-
173
- for _, err := range []error{serverErr, proxyErr} {
174
- if err == nil {
175
- continue
176
- }
177
- if errors.Is(err, context.Canceled) {
178
- continue
179
- }
180
- if errors.Is(err, ErrFrameClosed) {
181
- continue
182
- }
183
- if errors.Is(err, io.ErrClosedPipe) {
184
- continue
185
- }
186
- return err
187
- }
188
- return nil
189
- }
190
-
191
- // DenyNpmInstall is the NpmInstall callback ttscserver passes to tsgo's
192
- // LSP. ttsc has its own plugin / cache pipeline; we never want the LSP
193
- // host running npm under the user's editor. Exposed so other LSP host
194
- // embeddings can opt into the same behavior without copying the body.
195
- func DenyNpmInstall(_ string, args []string) ([]byte, error) {
196
- return nil, fmt.Errorf("ttscserver: npm install disabled in LSP host (args=%v)", args)
197
- }
@@ -1,18 +0,0 @@
1
- import type { ITtscPlugin } from "./structures/ITtscPlugin";
2
- /**
3
- * Identity helper for authoring `ITtscPlugin` descriptors with full literal
4
- * type preservation.
5
- *
6
- * Two call shapes:
7
- *
8
- * - Object form: returns the descriptor unchanged but pins its narrowed type so
9
- * editor tooling and `satisfies` clauses can see exact field values.
10
- * - Factory form: wraps a `(context) => descriptor` factory so the returned
11
- * function carries the literal descriptor type through to consumers without
12
- * forcing a manual `as const`.
13
- *
14
- * Pure pass-through at runtime — the function exists to teach TypeScript the
15
- * const-narrowing the user would otherwise have to spell out.
16
- */
17
- export declare function defineTtscPlugin<const T extends ITtscPlugin>(plugin: T): T;
18
- export declare function defineTtscPlugin<TContext, const T extends ITtscPlugin>(factory: (context: TContext) => T): (context: TContext) => T;
@@ -1,7 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.defineTtscPlugin = defineTtscPlugin;
4
- function defineTtscPlugin(value) {
5
- return value;
6
- }
7
- //# sourceMappingURL=defineTtscPlugin.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"defineTtscPlugin.js","sourceRoot":"","sources":["../src/defineTtscPlugin.ts"],"names":[],"mappings":";;;AAqBA,0BACE,KAAwD;IAExD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -1,26 +0,0 @@
1
- import type { ITtscPlugin } from "./structures/ITtscPlugin";
2
-
3
- /**
4
- * Identity helper for authoring `ITtscPlugin` descriptors with full literal
5
- * type preservation.
6
- *
7
- * Two call shapes:
8
- *
9
- * - Object form: returns the descriptor unchanged but pins its narrowed type so
10
- * editor tooling and `satisfies` clauses can see exact field values.
11
- * - Factory form: wraps a `(context) => descriptor` factory so the returned
12
- * function carries the literal descriptor type through to consumers without
13
- * forcing a manual `as const`.
14
- *
15
- * Pure pass-through at runtime — the function exists to teach TypeScript the
16
- * const-narrowing the user would otherwise have to spell out.
17
- */
18
- export function defineTtscPlugin<const T extends ITtscPlugin>(plugin: T): T;
19
- export function defineTtscPlugin<TContext, const T extends ITtscPlugin>(
20
- factory: (context: TContext) => T,
21
- ): (context: TContext) => T;
22
- export function defineTtscPlugin(
23
- value: ITtscPlugin | ((context: unknown) => ITtscPlugin),
24
- ): ITtscPlugin | ((context: unknown) => ITtscPlugin) {
25
- return value;
26
- }