ttsc 0.13.0 → 0.14.0-dev.20260528.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +36 -3
  2. package/cmd/ttscserver/main.go +56 -14
  3. package/cmd/ttscserver/run_lsp_forwards_execute_command_advertisement_flags_test.go +55 -0
  4. package/cmd/ttscserver/run_lsp_uses_native_plugin_manifest_source_test.go +45 -0
  5. package/driver/lsp.go +21 -1
  6. package/internal/lspserver/lsp_frame.go +30 -10
  7. package/internal/lspserver/lsp_native_plugin_source.go +380 -0
  8. package/internal/lspserver/lsp_plugin.go +16 -11
  9. package/internal/lspserver/lsp_proxy.go +1153 -73
  10. package/internal/lspserver/lsp_server.go +17 -5
  11. package/lib/TtscCompiler.d.ts +5 -4
  12. package/lib/TtscCompiler.js +56 -4
  13. package/lib/TtscCompiler.js.map +1 -1
  14. package/lib/compiler/internal/runBuild.js +149 -38
  15. package/lib/compiler/internal/runBuild.js.map +1 -1
  16. package/lib/compiler/internal/transformProjectInMemory.js +9 -7
  17. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  18. package/lib/flags/schema.d.ts +42 -43
  19. package/lib/flags/schema.js +35 -17
  20. package/lib/flags/schema.js.map +1 -1
  21. package/lib/launcher/internal/getCompilerVersionText.js +6 -14
  22. package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
  23. package/lib/launcher/internal/prepareExecution.d.ts +1 -0
  24. package/lib/launcher/internal/prepareExecution.js +31 -16
  25. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  26. package/lib/launcher/internal/runTtscserver.d.ts +3 -1
  27. package/lib/launcher/internal/runTtscserver.js +88 -11
  28. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  29. package/lib/launcher/internal/runTtsx.js +39 -26
  30. package/lib/launcher/internal/runTtsx.js.map +1 -1
  31. package/lib/structures/ITtscCompilerContext.d.ts +1 -1
  32. package/lib/structures/ITtscCompilerResult.d.ts +11 -0
  33. package/lib/structures/ITtscCompilerTransformation.d.ts +14 -3
  34. package/lib/structures/ITtscPlugin.d.ts +24 -15
  35. package/lib/structures/ITtscPluginCapabilities.d.ts +36 -14
  36. package/lib/structures/ITtscPluginContributor.d.ts +4 -2
  37. package/lib/structures/ITtscPluginFactoryContext.d.ts +3 -3
  38. package/lib/structures/ITtscProjectPluginConfig.d.ts +3 -3
  39. package/lib/structures/TtscPluginStage.d.ts +4 -3
  40. package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +7 -4
  41. package/package.json +10 -10
  42. package/shim/ast/lint.go +7 -1
  43. package/shim/checker/shim.go +3 -0
  44. package/shim/core/shim.go +11 -0
  45. package/src/TtscCompiler.ts +67 -4
  46. package/src/compiler/internal/runBuild.ts +209 -33
  47. package/src/compiler/internal/transformProjectInMemory.ts +10 -5
  48. package/src/flags/schema.ts +62 -43
  49. package/src/launcher/internal/getCompilerVersionText.ts +4 -13
  50. package/src/launcher/internal/prepareExecution.ts +31 -16
  51. package/src/launcher/internal/runTtscserver.ts +105 -11
  52. package/src/launcher/internal/runTtsx.ts +41 -29
  53. package/src/structures/ITtscCompilerContext.ts +1 -1
  54. package/src/structures/ITtscCompilerResult.ts +12 -0
  55. package/src/structures/ITtscCompilerTransformation.ts +15 -3
  56. package/src/structures/ITtscPlugin.ts +24 -15
  57. package/src/structures/ITtscPluginCapabilities.ts +38 -14
  58. package/src/structures/ITtscPluginContributor.ts +4 -2
  59. package/src/structures/ITtscPluginFactoryContext.ts +3 -3
  60. package/src/structures/ITtscProjectPluginConfig.ts +3 -3
  61. package/src/structures/TtscPluginStage.ts +4 -3
  62. package/src/structures/internal/ITtscLoadedNativePlugin.ts +7 -4
@@ -0,0 +1,380 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "fmt"
8
+ "io"
9
+ "os"
10
+ "os/exec"
11
+ "strings"
12
+ "sync"
13
+ "time"
14
+ )
15
+
16
+ const nativePluginCommandTimeout = 30 * time.Second
17
+ const nativePluginCommandStdoutLimit = 4 * 1024 * 1024
18
+ const nativePluginCommandStderrLimit = 1024 * 1024
19
+
20
+ // NativePluginManifest is the JSON shape the JavaScript ttscserver launcher
21
+ // passes through TTSC_LSP_PLUGINS_JSON after running normal project plugin
22
+ // discovery and source-plugin builds.
23
+ type NativePluginManifest struct {
24
+ Plugins []NativePluginConfigEntry `json:"plugins"`
25
+ LSPPlugins []NativeLSPPluginEntry `json:"lspPlugins"`
26
+ }
27
+
28
+ // NativePluginConfigEntry mirrors the compact sidecar protocol used by
29
+ // --plugins-json. It intentionally excludes host-only fields such as binary.
30
+ type NativePluginConfigEntry struct {
31
+ Config map[string]any `json:"config"`
32
+ Name string `json:"name"`
33
+ Stage string `json:"stage"`
34
+ }
35
+
36
+ // NativeLSPPluginEntry names one built sidecar that opted into the LSP
37
+ // protocol through its JavaScript descriptor capabilities.
38
+ type NativeLSPPluginEntry struct {
39
+ Binary string `json:"binary"`
40
+ Name string `json:"name,omitempty"`
41
+ Stage string `json:"stage,omitempty"`
42
+ }
43
+
44
+ // NativePluginSourceOptions configures a sidecar-backed PluginSource.
45
+ type NativePluginSourceOptions struct {
46
+ Cwd string
47
+ Err io.Writer
48
+ ManifestJSON string
49
+ Tsconfig string
50
+ }
51
+
52
+ // NativePluginSource implements PluginSource by delegating to native sidecars
53
+ // that explicitly support ttsc's LSP subcommands.
54
+ type NativePluginSource struct {
55
+ cwd string
56
+ err io.Writer
57
+ plugins []NativeLSPPluginEntry
58
+ pluginsJSON string
59
+ tsconfig string
60
+
61
+ commandIDs []string
62
+ codeActionKinds []string
63
+ owners map[string]NativeLSPPluginEntry
64
+ logMu sync.Mutex
65
+ }
66
+
67
+ type limitedBuffer struct {
68
+ buf bytes.Buffer
69
+ limit int
70
+ truncated bool
71
+ }
72
+
73
+ func (b *limitedBuffer) Write(p []byte) (int, error) {
74
+ remaining := b.limit - b.Len()
75
+ if remaining <= 0 {
76
+ b.truncated = true
77
+ return len(p), nil
78
+ }
79
+ if len(p) > remaining {
80
+ _, _ = b.buf.Write(p[:remaining])
81
+ b.truncated = true
82
+ return len(p), nil
83
+ }
84
+ _, _ = b.buf.Write(p)
85
+ return len(p), nil
86
+ }
87
+
88
+ func (b *limitedBuffer) Len() int {
89
+ return b.buf.Len()
90
+ }
91
+
92
+ func (b *limitedBuffer) String() string {
93
+ return b.buf.String()
94
+ }
95
+
96
+ func (b *limitedBuffer) Bytes() []byte {
97
+ return b.buf.Bytes()
98
+ }
99
+
100
+ // NewNativePluginSource parses a launcher-produced manifest and discovers the
101
+ // command ids owned by every LSP-capable sidecar.
102
+ func NewNativePluginSource(opts NativePluginSourceOptions) (*NativePluginSource, error) {
103
+ var manifest NativePluginManifest
104
+ if strings.TrimSpace(opts.ManifestJSON) != "" {
105
+ if err := json.Unmarshal([]byte(opts.ManifestJSON), &manifest); err != nil {
106
+ return nil, fmt.Errorf("ttscserver: invalid TTSC_LSP_PLUGINS_JSON: %w", err)
107
+ }
108
+ }
109
+ pluginsJSON, err := json.Marshal(manifest.Plugins)
110
+ if err != nil {
111
+ return nil, fmt.Errorf("ttscserver: encode plugin manifest: %w", err)
112
+ }
113
+ source := &NativePluginSource{
114
+ cwd: opts.Cwd,
115
+ err: opts.Err,
116
+ plugins: manifest.LSPPlugins,
117
+ pluginsJSON: string(pluginsJSON),
118
+ tsconfig: opts.Tsconfig,
119
+ owners: map[string]NativeLSPPluginEntry{},
120
+ }
121
+ source.discoverCommandIDs()
122
+ return source, nil
123
+ }
124
+
125
+ // Diagnostics asks every LSP-capable sidecar for diagnostics matching doc.URI.
126
+ func (s *NativePluginSource) Diagnostics(doc LSPDocumentVersion) []LSPDiagnostic {
127
+ if s == nil || doc.URI == "" {
128
+ return nil
129
+ }
130
+ var out []LSPDiagnostic
131
+ for _, plugin := range s.plugins {
132
+ body, err := s.run(plugin, "lsp-diagnostics", "--uri="+doc.URI)
133
+ if err != nil {
134
+ s.log("%v", err)
135
+ continue
136
+ }
137
+ var diagnostics []LSPDiagnostic
138
+ if err := json.Unmarshal(body, &diagnostics); err != nil {
139
+ s.log("ttscserver: %s lsp-diagnostics returned invalid JSON: %v", pluginLabel(plugin), err)
140
+ continue
141
+ }
142
+ out = append(out, diagnostics...)
143
+ }
144
+ return out
145
+ }
146
+
147
+ // CodeActions asks every LSP-capable sidecar for actions matching the request.
148
+ func (s *NativePluginSource) CodeActions(uri string, rng LSPRange, ctx LSPCodeActionContext) []LSPCodeAction {
149
+ if s == nil || uri == "" {
150
+ return nil
151
+ }
152
+ rangeJSON, _ := json.Marshal(rng)
153
+ contextJSON, _ := json.Marshal(ctx)
154
+ var out []LSPCodeAction
155
+ for _, plugin := range s.plugins {
156
+ body, err := s.run(
157
+ plugin,
158
+ "lsp-code-actions",
159
+ "--uri="+uri,
160
+ "--range-json="+string(rangeJSON),
161
+ "--context-json="+string(contextJSON),
162
+ )
163
+ if err != nil {
164
+ s.log("%v", err)
165
+ continue
166
+ }
167
+ var actions []LSPCodeAction
168
+ if err := json.Unmarshal(body, &actions); err != nil {
169
+ s.log("ttscserver: %s lsp-code-actions returned invalid JSON: %v", pluginLabel(plugin), err)
170
+ continue
171
+ }
172
+ for _, action := range actions {
173
+ if hasDirectCodeActionEdit(action.Edit) {
174
+ s.log("ttscserver: %s returned direct LSP edit for action %q; command-backed actions are required", pluginLabel(plugin), action.Title)
175
+ continue
176
+ }
177
+ if action.Command == nil {
178
+ s.log("ttscserver: %s returned commandless LSP action %q; command-backed actions are required", pluginLabel(plugin), action.Title)
179
+ continue
180
+ }
181
+ if !s.pluginOwnsCommand(plugin, action.Command.Command) {
182
+ s.log("ttscserver: %s returned unowned LSP command %q", pluginLabel(plugin), action.Command.Command)
183
+ continue
184
+ }
185
+ out = append(out, action)
186
+ }
187
+ }
188
+ return out
189
+ }
190
+
191
+ // ExecuteCommand routes a ttsc-owned workspace command to the sidecar that
192
+ // advertised it through lsp-command-ids.
193
+ func (s *NativePluginSource) ExecuteCommand(command string, args []json.RawMessage) (*LSPWorkspaceEdit, error) {
194
+ if s == nil {
195
+ return nil, ErrCommandNotHandled
196
+ }
197
+ plugin, ok := s.owners[command]
198
+ if !ok {
199
+ return nil, ErrCommandNotHandled
200
+ }
201
+ argsJSON, _ := json.Marshal(args)
202
+ body, err := s.run(
203
+ plugin,
204
+ "lsp-execute-command",
205
+ "--command="+command,
206
+ "--arguments-json="+string(argsJSON),
207
+ )
208
+ if err != nil {
209
+ return nil, err
210
+ }
211
+ if bytes.Equal(bytes.TrimSpace(body), []byte("null")) {
212
+ return nil, nil
213
+ }
214
+ edit, err := decodeNativeLSPWorkspaceEdit(plugin, body)
215
+ if err != nil {
216
+ return nil, err
217
+ }
218
+ return edit, nil
219
+ }
220
+
221
+ func decodeNativeLSPWorkspaceEdit(plugin NativeLSPPluginEntry, body []byte) (*LSPWorkspaceEdit, error) {
222
+ var probe struct {
223
+ Changes json.RawMessage `json:"changes,omitempty"`
224
+ DocumentChanges json.RawMessage `json:"documentChanges,omitempty"`
225
+ }
226
+ if err := json.Unmarshal(body, &probe); err != nil {
227
+ return nil, fmt.Errorf("ttscserver: %s lsp-execute-command returned invalid JSON: %w", pluginLabel(plugin), err)
228
+ }
229
+ if probe.DocumentChanges != nil && !bytes.Equal(bytes.TrimSpace(probe.DocumentChanges), []byte("null")) {
230
+ return nil, fmt.Errorf("ttscserver: %s lsp-execute-command returned unsupported WorkspaceEdit.documentChanges; return changes or null", pluginLabel(plugin))
231
+ }
232
+ var edit LSPWorkspaceEdit
233
+ if err := json.Unmarshal(body, &edit); err != nil {
234
+ return nil, fmt.Errorf("ttscserver: %s lsp-execute-command returned invalid WorkspaceEdit: %w", pluginLabel(plugin), err)
235
+ }
236
+ return &edit, nil
237
+ }
238
+
239
+ // CommandIDs returns the command ids discovered at source construction time.
240
+ func (s *NativePluginSource) CommandIDs() []string {
241
+ if s == nil || len(s.commandIDs) == 0 {
242
+ return nil
243
+ }
244
+ out := make([]string, len(s.commandIDs))
245
+ copy(out, s.commandIDs)
246
+ return out
247
+ }
248
+
249
+ // CodeActionKinds returns the action kinds discovered from LSP-capable sidecars.
250
+ func (s *NativePluginSource) CodeActionKinds() []string {
251
+ if s == nil || len(s.codeActionKinds) == 0 {
252
+ return nil
253
+ }
254
+ out := make([]string, len(s.codeActionKinds))
255
+ copy(out, s.codeActionKinds)
256
+ return out
257
+ }
258
+
259
+ func (s *NativePluginSource) discoverCommandIDs() {
260
+ seen := map[string]struct{}{}
261
+ kindSeen := map[string]struct{}{}
262
+ for _, plugin := range s.plugins {
263
+ body, err := s.run(plugin, "lsp-command-ids")
264
+ if err != nil {
265
+ s.log("%v", err)
266
+ continue
267
+ }
268
+ var ids []string
269
+ if err := json.Unmarshal(body, &ids); err != nil {
270
+ s.log("ttscserver: %s lsp-command-ids returned invalid JSON: %v", pluginLabel(plugin), err)
271
+ continue
272
+ }
273
+ for _, id := range ids {
274
+ if id == "" {
275
+ continue
276
+ }
277
+ if _, ok := seen[id]; ok {
278
+ s.log("ttscserver: duplicate LSP command id %q from %s ignored", id, pluginLabel(plugin))
279
+ continue
280
+ }
281
+ seen[id] = struct{}{}
282
+ s.commandIDs = append(s.commandIDs, id)
283
+ s.owners[id] = plugin
284
+ }
285
+ kindBody, kindErr := s.run(plugin, "lsp-code-action-kinds")
286
+ if kindErr != nil {
287
+ s.log("%v", kindErr)
288
+ continue
289
+ }
290
+ var kinds []string
291
+ if err := json.Unmarshal(kindBody, &kinds); err != nil {
292
+ s.log("ttscserver: %s lsp-code-action-kinds returned invalid JSON: %v", pluginLabel(plugin), err)
293
+ continue
294
+ }
295
+ for _, kind := range kinds {
296
+ if kind == "" {
297
+ continue
298
+ }
299
+ if _, ok := kindSeen[kind]; ok {
300
+ continue
301
+ }
302
+ kindSeen[kind] = struct{}{}
303
+ s.codeActionKinds = append(s.codeActionKinds, kind)
304
+ }
305
+ }
306
+ }
307
+
308
+ func (s *NativePluginSource) pluginOwnsCommand(plugin NativeLSPPluginEntry, command string) bool {
309
+ if strings.TrimSpace(command) == "" {
310
+ return false
311
+ }
312
+ owner, ok := s.owners[command]
313
+ if !ok {
314
+ return false
315
+ }
316
+ return owner.Binary == plugin.Binary && owner.Name == plugin.Name && owner.Stage == plugin.Stage
317
+ }
318
+
319
+ func (s *NativePluginSource) run(plugin NativeLSPPluginEntry, command string, args ...string) ([]byte, error) {
320
+ if strings.TrimSpace(plugin.Binary) == "" {
321
+ return nil, fmt.Errorf("ttscserver: %s has no binary", pluginLabel(plugin))
322
+ }
323
+ ctx, cancel := context.WithTimeout(context.Background(), nativePluginCommandTimeout)
324
+ defer cancel()
325
+ allArgs := []string{
326
+ command,
327
+ "--cwd=" + s.cwd,
328
+ "--tsconfig=" + s.tsconfig,
329
+ "--plugins-json=" + s.pluginsJSON,
330
+ }
331
+ allArgs = append(allArgs, args...)
332
+ cmd := exec.CommandContext(ctx, plugin.Binary, allArgs...)
333
+ cmd.Dir = s.cwd
334
+ cmd.Env = os.Environ()
335
+ stdout := limitedBuffer{limit: nativePluginCommandStdoutLimit}
336
+ stderr := limitedBuffer{limit: nativePluginCommandStderrLimit}
337
+ cmd.Stdout = &stdout
338
+ cmd.Stderr = &stderr
339
+ err := cmd.Run()
340
+ if ctx.Err() != nil {
341
+ return nil, fmt.Errorf("ttscserver: %s %s timed out", pluginLabel(plugin), command)
342
+ }
343
+ if err != nil {
344
+ msg := strings.TrimSpace(stderr.String())
345
+ if msg == "" {
346
+ msg = err.Error()
347
+ } else if stderr.truncated || stderr.Len() >= nativePluginCommandStderrLimit {
348
+ msg += " (stderr truncated)"
349
+ }
350
+ return nil, fmt.Errorf("ttscserver: %s %s failed: %s", pluginLabel(plugin), command, msg)
351
+ }
352
+ if stdout.truncated {
353
+ return nil, fmt.Errorf("ttscserver: %s %s produced more than %d bytes on stdout", pluginLabel(plugin), command, nativePluginCommandStdoutLimit)
354
+ }
355
+ return bytes.TrimSpace(stdout.Bytes()), nil
356
+ }
357
+
358
+ func hasDirectCodeActionEdit(edit json.RawMessage) bool {
359
+ trimmed := bytes.TrimSpace(edit)
360
+ return len(trimmed) > 0 && !bytes.Equal(trimmed, []byte("null"))
361
+ }
362
+
363
+ func (s *NativePluginSource) log(format string, args ...any) {
364
+ if s == nil || s.err == nil {
365
+ return
366
+ }
367
+ s.logMu.Lock()
368
+ defer s.logMu.Unlock()
369
+ fmt.Fprintf(s.err, format+"\n", args...)
370
+ }
371
+
372
+ func pluginLabel(plugin NativeLSPPluginEntry) string {
373
+ if plugin.Name != "" {
374
+ return plugin.Name
375
+ }
376
+ if plugin.Binary != "" {
377
+ return plugin.Binary
378
+ }
379
+ return "plugin"
380
+ }
@@ -46,8 +46,10 @@ type LSPDiagnostic struct {
46
46
  }
47
47
 
48
48
  // LSPCodeAction is the minimal Code Action shape ttscserver returns from
49
- // the textDocument/codeAction handler. ttsc actions are always command-
50
- // driven (the server side does the heavy lifting) so Edit stays nil.
49
+ // the textDocument/codeAction handler. Sidecar-backed NativePluginSource
50
+ // accepts only command-driven actions and drops non-null direct edits; custom
51
+ // in-process PluginSource implementations may still return Edit when they own
52
+ // that policy.
51
53
  type LSPCodeAction struct {
52
54
  Title string `json:"title"`
53
55
  Kind string `json:"kind,omitempty"`
@@ -64,8 +66,9 @@ type LSPCommand struct {
64
66
  }
65
67
 
66
68
  // LSPCodeActionContext mirrors the context object the editor sends with
67
- // textDocument/codeAction. Only the diagnostics slice is consumed by
68
- // ttsc; the rest is opaque and forwarded by Proxy verbatim.
69
+ // textDocument/codeAction. The proxy inspects Only to decide local routing, and
70
+ // plugin sources may inspect Diagnostics, Only, and TriggerKind when filtering
71
+ // their own actions.
69
72
  type LSPCodeActionContext struct {
70
73
  Diagnostics []json.RawMessage `json:"diagnostics,omitempty"`
71
74
  Only []string `json:"only,omitempty"`
@@ -112,13 +115,15 @@ type PluginSource interface {
112
115
  Diagnostics(doc LSPDocumentVersion) []LSPDiagnostic
113
116
 
114
117
  // CodeActions contributes additional actions for the given range. The
115
- // proxy appends them to the upstream tsgo response.
118
+ // proxy appends them to upstream responses, or answers locally when
119
+ // the request is plugin-only / upstream advertised no provider.
116
120
  CodeActions(uri string, rng LSPRange, ctx LSPCodeActionContext) []LSPCodeAction
117
121
 
118
- // ExecuteCommand handles workspace/executeCommand requests whose
119
- // command id appears in CommandIDs. A nil edit
120
- // means the command ran but produced no workspace changes; a non-nil
121
- // error surfaces as an LSP error response.
122
+ // ExecuteCommand handles workspace/executeCommand requests whose command id
123
+ // appears in CommandIDs. A nil edit means the command ran but produced no
124
+ // workspace changes; a non-nil error surfaces as an LSP error response.
125
+ // Returning ErrCommandNotHandled for an advertised id is also treated as an
126
+ // error by the proxy because advertised commands are owned locally.
122
127
  ExecuteCommand(command string, args []json.RawMessage) (*LSPWorkspaceEdit, error)
123
128
 
124
129
  // CommandIDs lists the workspace command ids ttsc handles locally so
@@ -128,8 +133,8 @@ type PluginSource interface {
128
133
 
129
134
  // NullPluginSource is the zero-contribution PluginSource used when the
130
135
  // LSP server is hosted without any ttsc plugin pipeline (smoke tests,
131
- // docs build). Every method returns an empty result so the proxy still
132
- // exercises its merge paths even with no plugin activity.
136
+ // docs build). It returns no diagnostics/actions and no command ids so the
137
+ // proxy still exercises its merge paths even with no plugin activity.
133
138
  type NullPluginSource struct{}
134
139
 
135
140
  // Diagnostics returns no plugin diagnostics.