ttsc 0.21.0 → 0.23.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 (119) hide show
  1. package/cmd/ttscserver/main.go +79 -3
  2. package/cmd/ttscserver/run_lsp_uses_native_plugin_manifest_source_test.go +96 -16
  3. package/driver/lsp.go +4 -0
  4. package/go.mod +1 -1
  5. package/internal/lspserver/lsp_document_diagnostics_omits_cached_project_when_current_result_has_none_test.go +48 -0
  6. package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go +98 -0
  7. package/internal/lspserver/lsp_external_watched_change_retains_program_test.go +53 -0
  8. package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +9 -0
  9. package/internal/lspserver/lsp_launcher_reload_directory_fingerprint_parity_windows_test.go +94 -0
  10. package/internal/lspserver/lsp_native_plugin_source.go +171 -55
  11. package/internal/lspserver/lsp_native_resident.go +110 -9
  12. package/internal/lspserver/lsp_plugin.go +6 -0
  13. package/internal/lspserver/lsp_project_context_transport_identity_test.go +43 -0
  14. package/internal/lspserver/lsp_project_diagnostics.go +235 -0
  15. package/internal/lspserver/lsp_project_diagnostics_capability_is_independent_test.go +67 -0
  16. package/internal/lspserver/lsp_project_diagnostics_program_input_overlap_refreshes_all_producers_test.go +78 -0
  17. package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go +65 -0
  18. package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go +90 -0
  19. package/internal/lspserver/lsp_project_diagnostics_refreshes_only_input_owners_test.go +62 -0
  20. package/internal/lspserver/lsp_project_diagnostics_resident_unsupported_falls_back_test.go +123 -0
  21. package/internal/lspserver/lsp_project_diagnostics_resume_preserves_generation_test.go +52 -0
  22. package/internal/lspserver/lsp_project_diagnostics_translates_relative_client_project_test.go +90 -0
  23. package/internal/lspserver/lsp_project_input_glob_path_other.go +16 -0
  24. package/internal/lspserver/lsp_project_input_glob_path_windows.go +36 -0
  25. package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
  26. package/internal/lspserver/lsp_project_input_path_key_other.go +17 -0
  27. package/internal/lspserver/lsp_project_input_path_key_windows.go +228 -0
  28. package/internal/lspserver/lsp_project_input_path_key_windows_test.go +460 -0
  29. package/internal/lspserver/lsp_project_input_registration.go +504 -0
  30. package/internal/lspserver/lsp_project_input_registration_rechecks_selection_baseline_test.go +71 -0
  31. package/internal/lspserver/lsp_project_input_registration_reconciles_rejected_pending_request_test.go +268 -0
  32. package/internal/lspserver/lsp_project_input_registration_shapes_patterns_test.go +94 -0
  33. package/internal/lspserver/lsp_project_input_watchers_anchor_missing_paths_test.go +48 -0
  34. package/internal/lspserver/lsp_project_inputs.go +1012 -0
  35. package/internal/lspserver/lsp_project_inputs_match_missing_exact_and_glob_test.go +129 -0
  36. package/internal/lspserver/lsp_project_inputs_reject_invalid_and_foreign_snapshots_test.go +110 -0
  37. package/internal/lspserver/lsp_proxy.go +591 -28
  38. package/internal/lspserver/lsp_reload_directories_compare_immediate_topology_test.go +188 -0
  39. package/internal/lspserver/lsp_reload_directories_separate_glob_territory_test.go +181 -0
  40. package/internal/lspserver/lsp_reload_file_fingerprint_preserves_raw_symlink_target_test.go +47 -0
  41. package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +1 -1
  42. package/internal/lspserver/lsp_resident_invalidation_broadcasts_program_input_overlap_test.go +60 -0
  43. package/internal/lspserver/lsp_resident_invalidation_targets_project_input_owners_test.go +51 -0
  44. package/internal/lspserver/lsp_same_transport_entries_are_deduplicated_test.go +66 -0
  45. package/lib/compiler/internal/project/readProjectConfig.d.ts +5 -3
  46. package/lib/compiler/internal/project/readProjectConfig.js +46 -14
  47. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  48. package/lib/compiler/internal/residentCheckProcess.d.ts +46 -0
  49. package/lib/compiler/internal/residentCheckProcess.js +227 -0
  50. package/lib/compiler/internal/residentCheckProcess.js.map +1 -0
  51. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -1
  52. package/lib/compiler/internal/resolveEmittedJavaScript.js +35 -18
  53. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  54. package/lib/compiler/internal/runBuild.d.ts +64 -1
  55. package/lib/compiler/internal/runBuild.js +484 -7
  56. package/lib/compiler/internal/runBuild.js.map +1 -1
  57. package/lib/compiler/internal/runSingleFileEmit.js +1 -0
  58. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  59. package/lib/flags/parser.js +13 -17
  60. package/lib/flags/parser.js.map +1 -1
  61. package/lib/flags/schema.d.ts +10 -3
  62. package/lib/flags/schema.js +99 -0
  63. package/lib/flags/schema.js.map +1 -1
  64. package/lib/internal/projectInputPathIdentity.d.ts +31 -0
  65. package/lib/internal/projectInputPathIdentity.js +276 -0
  66. package/lib/internal/projectInputPathIdentity.js.map +1 -0
  67. package/lib/internal/windowsDirectoryCaseSensitivity.d.ts +10 -0
  68. package/lib/internal/windowsDirectoryCaseSensitivity.js +34 -0
  69. package/lib/internal/windowsDirectoryCaseSensitivity.js.map +1 -0
  70. package/lib/launcher/internal/runTtsc.d.ts +17 -0
  71. package/lib/launcher/internal/runTtsc.js +126 -88
  72. package/lib/launcher/internal/runTtsc.js.map +1 -1
  73. package/lib/launcher/internal/runTtscserver.d.ts +13 -1
  74. package/lib/launcher/internal/runTtscserver.js +468 -47
  75. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  76. package/lib/launcher/internal/runTtsx.js +61 -7
  77. package/lib/launcher/internal/runTtsx.js.map +1 -1
  78. package/lib/launcher/internal/runtimeHooks.d.ts +6 -5
  79. package/lib/launcher/internal/runtimeHooks.js +7 -15
  80. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  81. package/lib/launcher/internal/singleFileOutput.d.ts +16 -0
  82. package/lib/launcher/internal/singleFileOutput.js +103 -0
  83. package/lib/launcher/internal/singleFileOutput.js.map +1 -0
  84. package/lib/launcher/internal/watchTopology.d.ts +288 -2
  85. package/lib/launcher/internal/watchTopology.js +1921 -54
  86. package/lib/launcher/internal/watchTopology.js.map +1 -1
  87. package/lib/plugin/internal/descriptorProcessFailure.d.ts +32 -0
  88. package/lib/plugin/internal/descriptorProcessFailure.js +73 -0
  89. package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -0
  90. package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
  91. package/lib/plugin/internal/loadProjectPlugins.js +34 -8
  92. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  93. package/lib/structures/ITtscPluginCapabilities.d.ts +31 -0
  94. package/lib/structures/internal/ITtscProjectInputSnapshot.d.ts +29 -0
  95. package/lib/structures/internal/ITtscProjectInputSnapshot.js +3 -0
  96. package/lib/structures/internal/ITtscProjectInputSnapshot.js.map +1 -0
  97. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -0
  98. package/package.json +12 -8
  99. package/src/compiler/internal/project/readProjectConfig.ts +62 -8
  100. package/src/compiler/internal/residentCheckProcess.ts +279 -0
  101. package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -21
  102. package/src/compiler/internal/runBuild.ts +728 -12
  103. package/src/compiler/internal/runSingleFileEmit.ts +1 -0
  104. package/src/flags/parser.ts +17 -17
  105. package/src/flags/schema.ts +112 -3
  106. package/src/internal/projectInputPathIdentity.ts +377 -0
  107. package/src/internal/windowsDirectoryCaseSensitivity.ts +33 -0
  108. package/src/launcher/internal/runTtsc.ts +148 -117
  109. package/src/launcher/internal/runTtscserver.ts +633 -56
  110. package/src/launcher/internal/runTtsx.ts +87 -10
  111. package/src/launcher/internal/runtimeHooks.ts +16 -16
  112. package/src/launcher/internal/singleFileOutput.ts +133 -0
  113. package/src/launcher/internal/watchTopology.ts +2494 -74
  114. package/src/plugin/internal/descriptorProcessFailure.ts +100 -0
  115. package/src/plugin/internal/loadProjectPlugins.ts +49 -8
  116. package/src/structures/ITtscPluginCapabilities.ts +34 -0
  117. package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
  118. package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
  119. package/tools/shim_audit/baseline.json +0 -9
@@ -1,8 +1,9 @@
1
1
  // Command ttscserver is the Go LSP host shipped by ttsc. It wraps the
2
2
  // project-selected TypeScript-Go LSP server process and proxies traffic
3
3
  // between the editor and that server. The JavaScript launcher resolves project
4
- // plugins first and passes an LSP manifest so this command can merge plugin
5
- // diagnostics, code actions, and ttsc-owned executeCommand handling.
4
+ // plugins first and passes a private LSP manifest file so this command can
5
+ // merge plugin diagnostics, code actions, and ttsc-owned executeCommand
6
+ // handling.
6
7
  //
7
8
  // The JavaScript launcher (`packages/ttsc/src/launcher/ttscserver.ts`)
8
9
  // resolves the native binary and forwards stdio so editors can spawn
@@ -29,6 +30,8 @@ import (
29
30
  "github.com/samchon/ttsc/packages/ttsc/internal/lspserver"
30
31
  )
31
32
 
33
+ const lspPluginManifestMaxBytes = 64 * 1024 * 1024
34
+
32
35
  // Build metadata; overwritten via -ldflags in release builds.
33
36
  var (
34
37
  version = "0.0.0-dev"
@@ -94,6 +97,12 @@ func runLSP(args []string) int {
94
97
  suppressExecuteCommandProviderFlag := fs.Bool("suppress-execute-command-provider", false, "do not advertise ttsc executeCommand ids during initialize")
95
98
  suppressExecuteCommandIDsFlag := fs.String("suppress-execute-command-ids", "", "comma-separated ttsc executeCommand ids to omit during initialize")
96
99
  executeCommandIDPrefixFlag := fs.String("execute-command-id-prefix", "", "prefix to apply to advertised executeCommand ids")
100
+ // The launcher passes this flag only when it actually resolved LSP-capable
101
+ // plugins, so a host too old to know it refuses to start instead of running
102
+ // silently without every plugin the project declared. Version skew between
103
+ // the launcher and a TTSCSERVER_BINARY override is therefore visible at the
104
+ // first request rather than diagnosed from missing diagnostics.
105
+ lspPluginsFileFlag := fs.String("lsp-plugins-file", "", "path to the private LSP plugin manifest written by the ttsc launcher")
97
106
  _ = fs.String("clientProcessId", "", "ignored VSCode language-client compatibility flag")
98
107
  if err := fs.Parse(args); err != nil {
99
108
  return 2
@@ -121,10 +130,15 @@ func runLSP(args []string) int {
121
130
  tsgoBinary = strings.TrimSpace(os.Getenv("TTSC_TSGO_BINARY"))
122
131
  }
123
132
 
133
+ manifestJSON, err := lspPluginManifestJSON(strings.TrimSpace(*lspPluginsFileFlag))
134
+ if err != nil {
135
+ fmt.Fprintf(stderr, "ttscserver: %v\n", err)
136
+ return 2
137
+ }
124
138
  source, err := lspserver.NewNativePluginSource(lspserver.NativePluginSourceOptions{
125
139
  Cwd: cwd,
126
140
  Err: stderr,
127
- ManifestJSON: os.Getenv("TTSC_LSP_PLUGINS_JSON"),
141
+ ManifestJSON: manifestJSON,
128
142
  Tsconfig: strings.TrimSpace(*tsconfigFlag),
129
143
  })
130
144
  if err != nil {
@@ -160,6 +174,63 @@ func runLSP(args []string) int {
160
174
  return 0
161
175
  }
162
176
 
177
+ // lspPluginManifestJSON reads the private plugin manifest prepared for this
178
+ // process and takes ownership of it.
179
+ //
180
+ // The manifest names every resolved project plugin and its launch context, so
181
+ // the copy this process was given to own is consumed exactly once: the file the
182
+ // launcher named with the flag is removed as soon as it has been read, which
183
+ // means a forcibly terminated launcher cannot leave the payload on disk. Both
184
+ // transport variables are cleared from this process either way, so no plugin
185
+ // sidecar spawned later inherits the payload or a path to it. The environment
186
+ // forms remain accepted because an editor pointed straight at a native binary
187
+ // has no launcher to pass the flag, and a manifest supplied that way is read
188
+ // without being removed, because it belongs to whoever wrote it.
189
+ func lspPluginManifestJSON(flagLocation string) (string, error) {
190
+ defer func() {
191
+ os.Unsetenv("TTSC_LSP_PLUGINS_FILE")
192
+ os.Unsetenv("TTSC_LSP_PLUGINS_JSON")
193
+ }()
194
+ if flagLocation != "" {
195
+ body, err := readLSPPluginManifestFile("--lsp-plugins-file", flagLocation)
196
+ if err != nil {
197
+ return "", err
198
+ }
199
+ // Only the flag names a file the launcher created for this process, so
200
+ // only that one is consumed here. Removing it is what keeps a forcibly
201
+ // terminated launcher from leaving the payload on disk. A path supplied
202
+ // out of band belongs to whoever wrote it and must survive the read, or
203
+ // the next session would start without the plugins it declared.
204
+ os.Remove(flagLocation)
205
+ return body, nil
206
+ }
207
+ location := strings.TrimSpace(os.Getenv("TTSC_LSP_PLUGINS_FILE"))
208
+ if location == "" {
209
+ return os.Getenv("TTSC_LSP_PLUGINS_JSON"), nil
210
+ }
211
+ return readLSPPluginManifestFile("TTSC_LSP_PLUGINS_FILE", location)
212
+ }
213
+
214
+ func readLSPPluginManifestFile(source, location string) (string, error) {
215
+ input, err := os.Open(location)
216
+ if err != nil {
217
+ return "", fmt.Errorf("read %s: %w", source, err)
218
+ }
219
+ defer input.Close()
220
+ body, err := io.ReadAll(io.LimitReader(input, lspPluginManifestMaxBytes+1))
221
+ if err != nil {
222
+ return "", fmt.Errorf("read %s: %w", source, err)
223
+ }
224
+ if len(body) > lspPluginManifestMaxBytes {
225
+ return "", fmt.Errorf(
226
+ "%s exceeds %d bytes",
227
+ source,
228
+ lspPluginManifestMaxBytes,
229
+ )
230
+ }
231
+ return string(body), nil
232
+ }
233
+
163
234
  func printVersion(w io.Writer) {
164
235
  fmt.Fprintf(
165
236
  w,
@@ -187,6 +258,11 @@ Options:
187
258
  --cwd <dir> Project root used as the tsgo server working directory.
188
259
  --tsconfig <path> Project config path used by ttsc plugin sidecars.
189
260
  --tsgo <path> Absolute tsgo binary path (defaults to TTSC_TSGO_BINARY).
261
+ --lsp-plugins-file <path>
262
+ Private plugin manifest written by the ttsc launcher; it is
263
+ consumed and deleted at startup. Editors that spawn this
264
+ binary directly supply their own manifest through
265
+ TTSC_LSP_PLUGINS_FILE instead, which is never deleted.
190
266
  --suppress-execute-command-provider
191
267
  Do not advertise ttsc executeCommand ids during initialize.
192
268
  --suppress-execute-command-ids <ids>
@@ -3,25 +3,39 @@ package main
3
3
  import (
4
4
  "bytes"
5
5
  "context"
6
+ "os"
7
+ "path/filepath"
8
+ "strings"
6
9
  "testing"
7
10
 
8
11
  "github.com/samchon/ttsc/packages/ttsc/internal/lspserver"
9
12
  )
10
13
 
11
14
  // TestRunLSPUsesNativePluginManifestSource verifies the command wires the
12
- // launcher-provided plugin manifest into the proxy source.
15
+ // launcher-provided plugin manifest into the proxy source and then owns it.
13
16
  //
14
17
  // A previous implementation always passed NullPluginSource, so editor sessions
15
- // could never receive ttsc plugin diagnostics or commands. This test pins the
16
- // command-level seam without starting tsgo: the JavaScript launcher owns plugin
17
- // discovery, and the Go command must turn its manifest into a NativePluginSource.
18
+ // could never receive ttsc plugin diagnostics or commands. The manifest also
19
+ // names every resolved plugin and its launch context, so leaving it readable
20
+ // for the process lifetime exposes it to any sidecar this host later spawns.
21
+ // This test pins both halves of that seam without starting tsgo.
18
22
  //
19
- // 1. Set TTSC_LSP_PLUGINS_JSON to an empty but valid manifest.
20
- // 2. Substitute runLSPServer and capture its LSPServerOptions.
21
- // 3. Run `ttscserver --stdio` against a temp cwd.
22
- // 4. Assert Source is the native sidecar-backed implementation.
23
+ // 1. Set the legacy TTSC_LSP_PLUGINS_JSON fallback to a valid manifest.
24
+ // 2. Substitute runLSPServer and capture its LSPServerOptions.
25
+ // 3. Run `ttscserver --stdio` and assert the native source is selected, and
26
+ // that the legacy payload no longer reaches a spawned sidecar.
27
+ // 4. Put a valid manifest in TTSC_LSP_PLUGINS_FILE while making the legacy
28
+ // environment payload invalid, and prove the bounded file transport wins,
29
+ // leaves neither variable behind, and survives the read because an editor
30
+ // that supplies it out of band must be able to launch twice.
31
+ // 5. Pass a manifest through --lsp-plugins-file while both environment forms
32
+ // are invalid, and prove the flag transport takes precedence and is
33
+ // consumed, because the launcher created that file for this process alone.
34
+ // 6. Point the flag at a missing manifest and prove the command fails instead
35
+ // of silently serving a project without its declared plugins.
23
36
  func TestRunLSPUsesNativePluginManifestSource(t *testing.T) {
24
37
  t.Setenv("TTSC_LSP_PLUGINS_JSON", `{"plugins":[],"lspPlugins":[]}`)
38
+ t.Setenv("TTSC_LSP_PLUGINS_FILE", "")
25
39
 
26
40
  prev := runLSPServer
27
41
  var captured lspserver.LSPServerOptions
@@ -31,15 +45,81 @@ func TestRunLSPUsesNativePluginManifestSource(t *testing.T) {
31
45
  }
32
46
  defer func() { runLSPServer = prev }()
33
47
 
34
- outBuf := &bytes.Buffer{}
35
- errBuf := &bytes.Buffer{}
36
- withIO(t, outBuf, errBuf, nil, func() {
37
- if code := runLSP([]string{"--stdio", "--cwd", t.TempDir(), "--tsconfig", "tsconfig.app.json"}); code != 0 {
38
- t.Fatalf("expected exit 0, got %d (stderr=%q)", code, errBuf.String())
48
+ invoke := func(extra ...string) (int, string) {
49
+ t.Helper()
50
+ outBuf := &bytes.Buffer{}
51
+ errBuf := &bytes.Buffer{}
52
+ code := 0
53
+ withIO(t, outBuf, errBuf, nil, func() {
54
+ code = runLSP(append(append([]string{}, extra...), []string{
55
+ "--stdio",
56
+ "--cwd",
57
+ t.TempDir(),
58
+ "--tsconfig",
59
+ "tsconfig.app.json",
60
+ }...))
61
+ })
62
+ return code, errBuf.String()
63
+ }
64
+ run := func(label string, extra ...string) {
65
+ t.Helper()
66
+ code, stderrText := invoke(extra...)
67
+ if code != 0 {
68
+ t.Fatalf("%s: expected exit 0, got %d (stderr=%q)", label, code, stderrText)
69
+ }
70
+ if _, ok := captured.Source.(*lspserver.NativePluginSource); !ok {
71
+ t.Fatalf("%s: expected NativePluginSource, got %T", label, captured.Source)
39
72
  }
40
- })
73
+ for _, name := range []string{
74
+ "TTSC_LSP_PLUGINS_FILE",
75
+ "TTSC_LSP_PLUGINS_JSON",
76
+ } {
77
+ if value := os.Getenv(name); value != "" {
78
+ t.Fatalf("%s: %s survived startup as %q", label, name, value)
79
+ }
80
+ }
81
+ }
82
+ writeManifest := func() string {
83
+ t.Helper()
84
+ location := filepath.Join(t.TempDir(), "plugins.json")
85
+ if err := os.WriteFile(
86
+ location,
87
+ []byte(`{"plugins":[],"lspPlugins":[]}`),
88
+ 0o600,
89
+ ); err != nil {
90
+ t.Fatal(err)
91
+ }
92
+ return location
93
+ }
94
+
95
+ run("legacy JSON")
41
96
 
42
- if _, ok := captured.Source.(*lspserver.NativePluginSource); !ok {
43
- t.Fatalf("expected NativePluginSource, got %T", captured.Source)
97
+ environmentManifest := writeManifest()
98
+ t.Setenv("TTSC_LSP_PLUGINS_JSON", "{invalid")
99
+ t.Setenv("TTSC_LSP_PLUGINS_FILE", environmentManifest)
100
+ run("manifest file")
101
+ if _, err := os.Stat(environmentManifest); err != nil {
102
+ t.Fatalf("out-of-band manifest was consumed by its reader: %v", err)
103
+ }
104
+
105
+ flagManifest := writeManifest()
106
+ t.Setenv("TTSC_LSP_PLUGINS_JSON", "{invalid")
107
+ t.Setenv("TTSC_LSP_PLUGINS_FILE", filepath.Join(t.TempDir(), "absent.json"))
108
+ run("manifest flag", "--lsp-plugins-file", flagManifest)
109
+ if _, err := os.Stat(flagManifest); !os.IsNotExist(err) {
110
+ t.Fatalf("flag manifest survived startup: %v", err)
111
+ }
112
+
113
+ t.Setenv("TTSC_LSP_PLUGINS_JSON", `{"plugins":[],"lspPlugins":[]}`)
114
+ t.Setenv("TTSC_LSP_PLUGINS_FILE", "")
115
+ code, stderrText := invoke(
116
+ "--lsp-plugins-file",
117
+ filepath.Join(t.TempDir(), "absent.json"),
118
+ )
119
+ if code == 0 {
120
+ t.Fatal("a missing manifest must not start the host")
121
+ }
122
+ if !strings.Contains(stderrText, "--lsp-plugins-file") {
123
+ t.Fatalf("expected the failing transport to be named, got %q", stderrText)
44
124
  }
45
125
  }
package/driver/lsp.go CHANGED
@@ -52,6 +52,10 @@ type LSPDocumentVersion = lspserver.LSPDocumentVersion
52
52
  // LSPProjectDiagnostics is the driver-level alias for a project publication.
53
53
  type LSPProjectDiagnostics = lspserver.LSPProjectDiagnostics
54
54
 
55
+ // LSPProjectInputSnapshot is the driver-level alias for the set of paths a
56
+ // producer declares its answers depend on.
57
+ type LSPProjectInputSnapshot = lspserver.LSPProjectInputSnapshot
58
+
55
59
  // LSPDiagnosticsResult separates document and project plugin diagnostics.
56
60
  type LSPDiagnosticsResult = lspserver.LSPDiagnosticsResult
57
61
 
package/go.mod CHANGED
@@ -41,6 +41,7 @@ require (
41
41
  github.com/microsoft/typescript-go/shim/vfs v0.0.0
42
42
  github.com/microsoft/typescript-go/shim/vfs/cachedvfs v0.0.0
43
43
  github.com/microsoft/typescript-go/shim/vfs/osvfs v0.0.0
44
+ golang.org/x/sys v0.43.0
44
45
  )
45
46
 
46
47
  require (
@@ -49,6 +50,5 @@ require (
49
50
  github.com/microsoft/typescript-go v0.0.0-20260429010842-56ab4af42157 // indirect
50
51
  github.com/zeebo/xxh3 v1.1.0 // indirect
51
52
  golang.org/x/sync v0.20.0 // indirect
52
- golang.org/x/sys v0.43.0 // indirect
53
53
  golang.org/x/text v0.36.0 // indirect
54
54
  )
@@ -0,0 +1,48 @@
1
+ package lspserver
2
+
3
+ import "testing"
4
+
5
+ // TestLSPDocumentDiagnosticsOmitsCachedProjectWhenCurrentResultHasNone
6
+ // verifies last-good cache state is not presented as a current document-cycle
7
+ // project result.
8
+ //
9
+ // A parse failure can make lsp-diagnostics omit project data. Returning the
10
+ // prior cache in that response lets the proxy complete a newer pending external
11
+ // refresh with stale evidence.
12
+ //
13
+ // 1. Seed one producer's last-good project publication.
14
+ // 2. Make the current document diagnostic invocation fail before publication.
15
+ // 3. Assert document diagnostics omit Project while the cache remains intact.
16
+ func TestLSPDocumentDiagnosticsOmitsCachedProjectWhenCurrentResultHasNone(
17
+ t *testing.T,
18
+ ) {
19
+ plugin := NativeLSPPluginEntry{
20
+ Binary: "ttsc-no-such-document-diagnostics-sidecar",
21
+ Name: "@ttsc/cached",
22
+ }
23
+ source := &NativePluginSource{plugins: []NativeLSPPluginEntry{plugin}}
24
+ source.storeProjectDiagnostics(
25
+ plugin,
26
+ 1,
27
+ &LSPProjectDiagnostics{
28
+ URI: "file:///project/tsconfig.json",
29
+ Diagnostics: []LSPDiagnostic{{
30
+ Code: "stale",
31
+ Message: "stale",
32
+ }},
33
+ },
34
+ )
35
+
36
+ got := source.Diagnostics(LSPDocumentVersion{
37
+ URI: "file:///project/src/main.ts",
38
+ })
39
+
40
+ if got.Project != nil {
41
+ t.Fatalf("current omitted project result reused cache: %#v", got.Project)
42
+ }
43
+ cached := source.projectDiagnosticsSnapshot()
44
+ if cached == nil || len(cached.Diagnostics) != 1 ||
45
+ cached.Diagnostics[0].Code != "stale" {
46
+ t.Fatalf("last-good cache was not retained: %#v", cached)
47
+ }
48
+ }
@@ -0,0 +1,98 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "errors"
7
+ "io"
8
+ "testing"
9
+ )
10
+
11
+ type executableProjectInputSource struct {
12
+ NullPluginSource
13
+ reloadURI string
14
+ }
15
+
16
+ func (s executableProjectInputSource) ProjectInputReloadMatchesChange(
17
+ uri string,
18
+ _ *int,
19
+ ) bool {
20
+ return uri == s.reloadURI
21
+ }
22
+
23
+ // TestLSPExecutableProjectInputChangeRequestsLauncherRestart verifies an input
24
+ // that can alter the descriptor's contributor set cannot be handled as an
25
+ // ordinary native resident-program refresh.
26
+ //
27
+ // The JavaScript launcher owns descriptor evaluation and native contributor
28
+ // compilation, so the already-running Go process cannot safely mutate this
29
+ // selection in place.
30
+ //
31
+ // 1. Send a watched change for the source's executable reload URI.
32
+ // 2. Assert the proxy announces the expected lifecycle transition, then
33
+ // returns its stable restart sentinel before forwarding.
34
+ // 3. Send an unrelated change and assert it follows the ordinary forwarded
35
+ // invalidation path without another lifecycle notification.
36
+ func TestLSPExecutableProjectInputChangeRequestsLauncherRestart(t *testing.T) {
37
+ reloadURI := "file:///project/lint.config.ts"
38
+ var editor bytes.Buffer
39
+ proxy := NewProxy(ProxyOptions{
40
+ EditorOut: &editor,
41
+ UpstreamIn: io.Discard,
42
+ Source: executableProjectInputSource{
43
+ reloadURI: reloadURI,
44
+ },
45
+ })
46
+ handled, err := proxy.handleEditorEnvelope(
47
+ watchedFilesEnvelope(
48
+ t,
49
+ `{"changes":[{"uri":"`+reloadURI+`","type":2}]}`,
50
+ ),
51
+ nil,
52
+ )
53
+ if handled {
54
+ t.Fatal("executable reload notification was marked locally handled")
55
+ }
56
+ if !errors.Is(err, ErrLSPPluginSelectionChanged) {
57
+ t.Fatalf("reload error = %v, want %v", err, ErrLSPPluginSelectionChanged)
58
+ }
59
+ _, body, err := NewFrameReader(&editor).Read()
60
+ if err != nil {
61
+ t.Fatalf("read plugin-selection notification: %v", err)
62
+ }
63
+ notification, err := ParseEnvelope(body)
64
+ if err != nil {
65
+ t.Fatalf("parse plugin-selection notification: %v", err)
66
+ }
67
+ if notification.Method != methodPluginSelectionChanged {
68
+ t.Fatalf(
69
+ "notification method = %q, want %q",
70
+ notification.Method,
71
+ methodPluginSelectionChanged,
72
+ )
73
+ }
74
+ var params map[string]string
75
+ if err := json.Unmarshal(notification.Params, &params); err != nil {
76
+ t.Fatalf("parse plugin-selection notification params: %v", err)
77
+ }
78
+ if params["reason"] != "projectInputChanged" {
79
+ t.Fatalf("notification params = %#v", params)
80
+ }
81
+
82
+ handled, err = proxy.handleEditorEnvelope(
83
+ watchedFilesEnvelope(
84
+ t,
85
+ `{"changes":[{"uri":"file:///project/src/main.ts","type":2}]}`,
86
+ ),
87
+ nil,
88
+ )
89
+ if err != nil {
90
+ t.Fatalf("ordinary watched change: %v", err)
91
+ }
92
+ if handled {
93
+ t.Fatal("ordinary watched change was swallowed instead of forwarded")
94
+ }
95
+ if editor.Len() != 0 {
96
+ t.Fatalf("ordinary change wrote an unexpected editor frame: %q", editor.String())
97
+ }
98
+ }
@@ -0,0 +1,53 @@
1
+ package lspserver
2
+
3
+ import "testing"
4
+
5
+ // TestExternalWatchedChangeRetainsProgram verifies external data events preserve
6
+ // the warm Program without hiding TypeScript root-set changes.
7
+ //
8
+ // Project-input globs may overlap source files even though Markdown and
9
+ // OpenAPI are the common case, but a JSON path may also be a resolveJsonModule
10
+ // source. JSON create/delete changes possible Program topology, while in-place
11
+ // JSON edits and data-only reference topology preserve the current root graph,
12
+ // except package metadata whose content participates in module resolution.
13
+ //
14
+ // 1. Retain changed Markdown and Swagger inputs.
15
+ // 2. Retain an in-place TypeScript edit for incremental Program update.
16
+ // 3. Reload package metadata, created/deleted modules, and project configs.
17
+ func TestExternalWatchedChangeRetainsProgram(t *testing.T) {
18
+ changed := fileChangeTypeChanged
19
+ created := fileChangeTypeCreated
20
+ deleted := fileChangeTypeDeleted
21
+ cases := []struct {
22
+ uri string
23
+ changeType *int
24
+ want bool
25
+ }{
26
+ {uri: "file:///project/docs/spec.md", changeType: &changed, want: true},
27
+ {uri: "file:///project/api/openapi.json", changeType: &changed, want: true},
28
+ {uri: "file:///project/package.json", changeType: &changed, want: false},
29
+ {uri: "file:///project/PACKAGE.JSON", changeType: &changed, want: false},
30
+ {uri: "file:///project/api/openapi.json", changeType: &created, want: false},
31
+ {uri: "file:///project/api/openapi.json", changeType: &deleted, want: false},
32
+ {uri: "file:///project/api/openapi.JSON", changeType: &created, want: false},
33
+ {uri: "file:///project/api/openapi.yaml", changeType: &deleted, want: true},
34
+ {uri: "file:///project/src/main.ts", changeType: &changed, want: true},
35
+ {uri: "file:///project/src/main.ts", changeType: &created, want: false},
36
+ {uri: "file:///project/src/main.ts", changeType: &deleted, want: false},
37
+ {uri: "file:///project/tsconfig.json", changeType: &changed, want: false},
38
+ {uri: "https://example.com/openapi.json", changeType: &changed, want: false},
39
+ {uri: "", changeType: &changed, want: false},
40
+ {uri: "file:///project/docs/spec.md", changeType: nil, want: false},
41
+ }
42
+ for _, tc := range cases {
43
+ if got := externalWatchedChangeRetainsProgram(tc.uri, tc.changeType); got != tc.want {
44
+ t.Fatalf(
45
+ "externalWatchedChangeRetainsProgram(%q, %v) = %v, want %v",
46
+ tc.uri,
47
+ tc.changeType,
48
+ got,
49
+ tc.want,
50
+ )
51
+ }
52
+ }
53
+ }
@@ -44,6 +44,15 @@ func TestLSPHintsRefreshScheduledByEditorEvents(t *testing.T) {
44
44
  `{"jsonrpc":"2.0","method":"textDocument/didSave","params":{"textDocument":{"uri":"file:///a.ts"}}}`,
45
45
  `{"jsonrpc":"2.0","method":"workspace/didChangeConfiguration","params":{"settings":{}}}`,
46
46
  `{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[{"uri":"file:///lint.config.ts","type":2}]}}`,
47
+ // JSON can be a resolveJsonModule source even when no ProjectRule declares
48
+ // it. Content and membership changes therefore invalidate the Program-wide
49
+ // hint corpus just like TypeScript changes do.
50
+ `{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[{"uri":"file:///src/data.json","type":1}]}}`,
51
+ `{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[{"uri":"file:///src/data.json","type":2}]}}`,
52
+ `{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[{"uri":"file:///src/data.json","type":3}]}}`,
53
+ // Package metadata participates in module resolution independently of
54
+ // ProjectRule ownership.
55
+ `{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[{"uri":"file:///package.json","type":2}]}}`,
47
56
  // Params the proxy cannot read take the conservative branch: it cannot tell
48
57
  // which inputs changed, so it assumes they all did — the same answer it
49
58
  // gives the resident daemon there.
@@ -0,0 +1,94 @@
1
+ //go:build windows
2
+
3
+ package lspserver
4
+
5
+ import (
6
+ "os"
7
+ "os/exec"
8
+ "path/filepath"
9
+ "strings"
10
+ "testing"
11
+ )
12
+
13
+ // TestLauncherReloadDirectoryFingerprintMatchesGo verifies that the JavaScript
14
+ // launcher and Go host serialize the same Windows physical directory identity.
15
+ //
16
+ // The launcher formerly retained a physical drive volume while Go lowercased
17
+ // it, and the two sides also disagreed about case folding for a missing suffix.
18
+ // A stable launcher snapshot was therefore rejected as stale during native
19
+ // watcher registration.
20
+ //
21
+ // 1. Ask the built JavaScript launcher to fingerprint one existing directory
22
+ // and one missing descendant under an ordinary case-insensitive parent.
23
+ // 2. Pass each launcher-produced digest through the real Go snapshot
24
+ // normalization path.
25
+ // 3. Prove the Go currentness validator accepts both unchanged directories.
26
+ func TestLauncherReloadDirectoryFingerprintMatchesGo(t *testing.T) {
27
+ root := t.TempDir()
28
+ existing := filepath.Join(root, "ExistingDirectory")
29
+ if err := os.Mkdir(existing, 0o755); err != nil {
30
+ t.Fatal(err)
31
+ }
32
+ for _, directory := range []string{
33
+ existing,
34
+ filepath.Join(root, "MissingDirectory"),
35
+ } {
36
+ digest := launcherReloadDirectoryDigest(t, root, directory)
37
+ snapshot, err := normalizeLSPProjectInputSnapshot(
38
+ LSPProjectInputSnapshot{
39
+ Root: root,
40
+ ReloadDirectories: []string{directory},
41
+ ReloadDirectoryDigests: map[string]string{
42
+ directory: digest,
43
+ },
44
+ },
45
+ root,
46
+ )
47
+ if err != nil {
48
+ t.Fatalf("normalize launcher fingerprint for %q: %v", directory, err)
49
+ }
50
+ if !projectInputReloadFingerprintsAreCurrent(snapshot) {
51
+ t.Fatalf("launcher fingerprint for %q began stale in Go", directory)
52
+ }
53
+ }
54
+ }
55
+
56
+ func launcherReloadDirectoryDigest(
57
+ t *testing.T,
58
+ root string,
59
+ directory string,
60
+ ) string {
61
+ t.Helper()
62
+ module, err := filepath.Abs(
63
+ filepath.Join("..", "..", "lib", "launcher", "internal", "runTtscserver.js"),
64
+ )
65
+ if err != nil {
66
+ t.Fatal(err)
67
+ }
68
+ script := `
69
+ import { pathToFileURL } from "node:url";
70
+ const launcher = await import(pathToFileURL(process.argv[1]).href);
71
+ const location = process.argv[2];
72
+ const snapshot = launcher.fingerprintInitialLSPProjectInputSnapshot({
73
+ files: [],
74
+ globs: [],
75
+ reloadDirectories: [location],
76
+ root: process.argv[3],
77
+ });
78
+ process.stdout.write(snapshot.reloadDirectoryDigests[location]);
79
+ `
80
+ command := exec.Command(
81
+ "node",
82
+ "--input-type=module",
83
+ "--eval",
84
+ script,
85
+ module,
86
+ directory,
87
+ root,
88
+ )
89
+ output, err := command.CombinedOutput()
90
+ if err != nil {
91
+ t.Fatalf("launcher fingerprint for %q: %v\n%s", directory, err, output)
92
+ }
93
+ return strings.TrimSpace(string(output))
94
+ }