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
@@ -0,0 +1,188 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "net/url"
5
+ "os"
6
+ "path/filepath"
7
+ "testing"
8
+ )
9
+
10
+ // TestLSPReloadDirectoriesCompareImmediateTopology verifies reload-directory
11
+ // notifications restart only when the declared non-recursive topology changes.
12
+ //
13
+ // A directory fingerprint represents resolution identity, not the contents of
14
+ // every child. Treating every descendant event as a restart turns ordinary
15
+ // edits into crashes, while ignoring directory identity or symlink targets
16
+ // leaves contributor selection stale.
17
+ //
18
+ // 1. Record a directory with one file and one nested directory.
19
+ // 2. Prove child-content and nested-descendant edits leave its digest stable.
20
+ // 3. Prove immediate creation and deletion change the digest.
21
+ // 4. Prove deleting the watched directory itself changes the digest.
22
+ // 5. Where supported, retarget a symlink without renaming it and prove the raw
23
+ // link target participates in the digest.
24
+ // 6. Replace an empty directory with the same topology and prove its identity
25
+ // event still restarts selection.
26
+ // 7. Nest one reload directory in another and prove the unchanged parent
27
+ // cannot hide an immediate topology change in the child.
28
+ // 8. Rediscover project inputs after topology drift and prove refresh retains
29
+ // the selection-time baseline until the server restarts.
30
+ func TestLSPReloadDirectoriesCompareImmediateTopology(t *testing.T) {
31
+ root := t.TempDir()
32
+ reloadDirectory := filepath.Join(root, "config-deps")
33
+ nested := filepath.Join(reloadDirectory, "nested")
34
+ if err := os.MkdirAll(nested, 0o755); err != nil {
35
+ t.Fatal(err)
36
+ }
37
+ selection := filepath.Join(reloadDirectory, "selection.cjs")
38
+ nestedSelection := filepath.Join(nested, "selection.cjs")
39
+ if err := os.WriteFile(selection, []byte("alpha"), 0o644); err != nil {
40
+ t.Fatal(err)
41
+ }
42
+ if err := os.WriteFile(nestedSelection, []byte("alpha"), 0o644); err != nil {
43
+ t.Fatal(err)
44
+ }
45
+ changed := fileChangeTypeChanged
46
+ created := fileChangeTypeCreated
47
+ deleted := fileChangeTypeDeleted
48
+ uri := func(location string) string {
49
+ normalized := filepath.ToSlash(location)
50
+ if filepath.VolumeName(location) != "" {
51
+ normalized = "/" + normalized
52
+ }
53
+ return (&url.URL{Scheme: "file", Path: normalized}).String()
54
+ }
55
+ snapshot := func(directory string) LSPProjectInputSnapshot {
56
+ normalized, err := normalizeLSPProjectInputSnapshot(
57
+ LSPProjectInputSnapshot{
58
+ Root: root,
59
+ ReloadDirectories: []string{directory},
60
+ },
61
+ root,
62
+ )
63
+ if err != nil {
64
+ t.Fatalf("normalize reload directory: %v", err)
65
+ }
66
+ return normalized
67
+ }
68
+ source := &NativePluginSource{projectInputs: snapshot(reloadDirectory)}
69
+
70
+ if err := os.WriteFile(selection, []byte("beta"), 0o644); err != nil {
71
+ t.Fatal(err)
72
+ }
73
+ if source.ProjectInputReloadMatchesChange(uri(selection), &changed) {
74
+ t.Fatal("ordinary child-content edit changed directory topology")
75
+ }
76
+ if err := os.WriteFile(nestedSelection, []byte("beta"), 0o644); err != nil {
77
+ t.Fatal(err)
78
+ }
79
+ if source.ProjectInputReloadMatchesChange(uri(nestedSelection), &changed) {
80
+ t.Fatal("nested descendant edit matched a non-recursive reload directory")
81
+ }
82
+
83
+ createdEntry := filepath.Join(reloadDirectory, "created.cjs")
84
+ if err := os.WriteFile(createdEntry, []byte("created"), 0o644); err != nil {
85
+ t.Fatal(err)
86
+ }
87
+ if !source.ProjectInputReloadMatchesChange(uri(createdEntry), &created) {
88
+ t.Fatal("immediate entry creation did not change directory topology")
89
+ }
90
+ source.projectInputs = snapshot(reloadDirectory)
91
+ if err := os.Remove(createdEntry); err != nil {
92
+ t.Fatal(err)
93
+ }
94
+ if !source.ProjectInputReloadMatchesChange(uri(createdEntry), &deleted) {
95
+ t.Fatal("immediate entry deletion did not change directory topology")
96
+ }
97
+
98
+ deletionRoot := filepath.Join(root, "deleted-deps")
99
+ if err := os.Mkdir(deletionRoot, 0o755); err != nil {
100
+ t.Fatal(err)
101
+ }
102
+ source.projectInputs = snapshot(deletionRoot)
103
+ if err := os.Remove(deletionRoot); err != nil {
104
+ t.Fatal(err)
105
+ }
106
+ if !source.ProjectInputReloadMatchesChange(uri(deletionRoot), &deleted) {
107
+ t.Fatal("reload-directory deletion did not change its topology state")
108
+ }
109
+
110
+ replacementRoot := filepath.Join(root, "replacement-deps")
111
+ if err := os.Mkdir(replacementRoot, 0o755); err != nil {
112
+ t.Fatal(err)
113
+ }
114
+ source.projectInputs = snapshot(replacementRoot)
115
+ if err := os.Remove(replacementRoot); err != nil {
116
+ t.Fatal(err)
117
+ }
118
+ if err := os.Mkdir(replacementRoot, 0o755); err != nil {
119
+ t.Fatal(err)
120
+ }
121
+ if !source.ProjectInputReloadMatchesChange(
122
+ uri(replacementRoot),
123
+ &created,
124
+ ) {
125
+ t.Fatal("same-topology directory replacement did not match its identity event")
126
+ }
127
+
128
+ nestedParent := filepath.Join(root, "nested-parent")
129
+ nestedChild := filepath.Join(nestedParent, "child")
130
+ if err := os.MkdirAll(nestedChild, 0o755); err != nil {
131
+ t.Fatal(err)
132
+ }
133
+ nestedSnapshot, err := normalizeLSPProjectInputSnapshot(
134
+ LSPProjectInputSnapshot{
135
+ Root: root,
136
+ ReloadDirectories: []string{nestedParent, nestedChild},
137
+ },
138
+ root,
139
+ )
140
+ if err != nil {
141
+ t.Fatalf("normalize nested reload directories: %v", err)
142
+ }
143
+ source.projectInputs = nestedSnapshot
144
+ nestedEntry := filepath.Join(nestedChild, "selection.cjs")
145
+ if err := os.WriteFile(nestedEntry, []byte("alpha"), 0o644); err != nil {
146
+ t.Fatal(err)
147
+ }
148
+ if !source.ProjectInputReloadMatchesChange(uri(nestedEntry), &created) {
149
+ t.Fatal("unchanged parent reload directory hid nested topology change")
150
+ }
151
+
152
+ refreshRoot := filepath.Join(root, "refresh-deps")
153
+ if err := os.Mkdir(refreshRoot, 0o755); err != nil {
154
+ t.Fatal(err)
155
+ }
156
+ baseline := snapshot(refreshRoot)
157
+ refreshEntry := filepath.Join(refreshRoot, "selection.cjs")
158
+ if err := os.WriteFile(refreshEntry, []byte("alpha"), 0o644); err != nil {
159
+ t.Fatal(err)
160
+ }
161
+ refreshed := snapshot(refreshRoot)
162
+ preserveProjectInputReloadFingerprints(baseline, &refreshed)
163
+ source.projectInputs = refreshed
164
+ if !source.ProjectInputReloadMatchesChange(uri(refreshEntry), &created) {
165
+ t.Fatal("project-input refresh absorbed selection-time topology drift")
166
+ }
167
+
168
+ firstTarget := filepath.Join(root, "first-target")
169
+ secondTarget := filepath.Join(root, "second-target")
170
+ for _, target := range []string{firstTarget, secondTarget} {
171
+ if err := os.Mkdir(target, 0o755); err != nil {
172
+ t.Fatal(err)
173
+ }
174
+ }
175
+ link := filepath.Join(reloadDirectory, "selection-link")
176
+ if err := os.Symlink(firstTarget, link); err == nil {
177
+ source.projectInputs = snapshot(reloadDirectory)
178
+ if err := os.Remove(link); err != nil {
179
+ t.Fatal(err)
180
+ }
181
+ if err := os.Symlink(secondTarget, link); err != nil {
182
+ t.Fatalf("retarget symlink: %v", err)
183
+ }
184
+ if !source.ProjectInputReloadMatchesChange(uri(link), &changed) {
185
+ t.Fatal("symlink retarget did not change directory topology")
186
+ }
187
+ }
188
+ }
@@ -0,0 +1,181 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "net/url"
5
+ "os"
6
+ "path/filepath"
7
+ "testing"
8
+ )
9
+
10
+ // TestLSPReloadDirectoriesSeparateGlobTerritory verifies the native LSP host
11
+ // applies the same data-versus-selection boundary as the CLI watcher.
12
+ //
13
+ // A reload directory records resolution topology, but a declared glob's
14
+ // literal root appearing strictly below it is an ordinary data-population
15
+ // transition. Restarting the JavaScript launcher for that event loses the
16
+ // resident process that should instead reload its Program.
17
+ //
18
+ // 1. Create a missing glob root directly inside a reload directory and prove
19
+ // it remains on the warm data lane.
20
+ // 2. Prove unrelated immediate entries and the directory itself remain cold.
21
+ // 3. Prove exact reload files are never exempt inside glob territory.
22
+ // 4. Prove a glob rooted on or above the reload directory exempts nothing.
23
+ func TestLSPReloadDirectoriesSeparateGlobTerritory(t *testing.T) {
24
+ uri := func(location string) string {
25
+ normalized := filepath.ToSlash(location)
26
+ if filepath.VolumeName(location) != "" {
27
+ normalized = "/" + normalized
28
+ }
29
+ return (&url.URL{Scheme: "file", Path: normalized}).String()
30
+ }
31
+ snapshot := func(
32
+ root string,
33
+ globs []string,
34
+ reloadFiles []string,
35
+ ) LSPProjectInputSnapshot {
36
+ normalized, err := normalizeLSPProjectInputSnapshot(
37
+ LSPProjectInputSnapshot{
38
+ Root: root,
39
+ Globs: globs,
40
+ ReloadFiles: reloadFiles,
41
+ ReloadDirectories: []string{root},
42
+ },
43
+ root,
44
+ )
45
+ if err != nil {
46
+ t.Fatalf("normalize project inputs: %v", err)
47
+ }
48
+ return normalized
49
+ }
50
+ created := fileChangeTypeCreated
51
+ changed := fileChangeTypeChanged
52
+
53
+ t.Run("literal root is data", func(t *testing.T) {
54
+ root := t.TempDir()
55
+ globRoot := filepath.Join(root, "api")
56
+ source := &NativePluginSource{
57
+ projectInputs: snapshot(
58
+ root,
59
+ []string{filepath.Join(globRoot, "**", "*.json")},
60
+ nil,
61
+ ),
62
+ }
63
+ if err := os.Mkdir(globRoot, 0o755); err != nil {
64
+ t.Fatal(err)
65
+ }
66
+ if source.ProjectInputReloadMatchesChange(uri(globRoot), &created) {
67
+ t.Fatal("declared glob root selected a cold launcher restart")
68
+ }
69
+
70
+ source.projectInputs = snapshot(
71
+ root,
72
+ []string{filepath.Join(globRoot, "**", "*.json")},
73
+ nil,
74
+ )
75
+ selectionEntry := filepath.Join(root, "new-package")
76
+ if err := os.Mkdir(selectionEntry, 0o755); err != nil {
77
+ t.Fatal(err)
78
+ }
79
+ if !source.ProjectInputReloadMatchesChange(uri(selectionEntry), &created) {
80
+ t.Fatal("unrelated immediate entry did not select a cold restart")
81
+ }
82
+ if !source.ProjectInputReloadMatchesChange(uri(root), &changed) {
83
+ t.Fatal("reload-directory identity event depended on its change type")
84
+ }
85
+ })
86
+
87
+ t.Run("exact file is never data", func(t *testing.T) {
88
+ root := t.TempDir()
89
+ globRoot := filepath.Join(root, "api")
90
+ reloadFile := filepath.Join(globRoot, "selection.json")
91
+ source := &NativePluginSource{
92
+ projectInputs: snapshot(
93
+ root,
94
+ []string{filepath.Join(globRoot, "**", "*.json")},
95
+ []string{reloadFile},
96
+ ),
97
+ }
98
+ if err := os.Mkdir(globRoot, 0o755); err != nil {
99
+ t.Fatal(err)
100
+ }
101
+ if err := os.WriteFile(reloadFile, []byte("{}"), 0o644); err != nil {
102
+ t.Fatal(err)
103
+ }
104
+ if !source.ProjectInputReloadMatchesChange(uri(reloadFile), &created) {
105
+ t.Fatal("exact reload file was exempted by surrounding glob territory")
106
+ }
107
+ })
108
+
109
+ t.Run("symlinked literal root is data", func(t *testing.T) {
110
+ root := t.TempDir()
111
+ globRoot := filepath.Join(root, "api")
112
+ physicalData := filepath.Join(root, "data")
113
+ if err := os.Mkdir(physicalData, 0o755); err != nil {
114
+ t.Fatal(err)
115
+ }
116
+ source := &NativePluginSource{
117
+ projectInputs: snapshot(
118
+ root,
119
+ []string{filepath.Join(globRoot, "**", "*.json")},
120
+ nil,
121
+ ),
122
+ }
123
+ if err := os.Symlink(physicalData, globRoot); err != nil {
124
+ t.Skipf("directory symlinks unavailable: %v", err)
125
+ }
126
+ if source.ProjectInputReloadMatchesChange(uri(globRoot), &created) {
127
+ t.Fatal("symlinked glob root lost its physical data identity")
128
+ }
129
+ })
130
+
131
+ t.Run("symlinked literal root outside reload directory is selection", func(t *testing.T) {
132
+ root := t.TempDir()
133
+ globRoot := filepath.Join(root, "api")
134
+ physicalData := t.TempDir()
135
+ source := &NativePluginSource{
136
+ projectInputs: snapshot(
137
+ root,
138
+ []string{filepath.Join(globRoot, "**", "*.json")},
139
+ nil,
140
+ ),
141
+ }
142
+ if err := os.Symlink(physicalData, globRoot); err != nil {
143
+ t.Skipf("directory symlinks unavailable: %v", err)
144
+ }
145
+ if !source.ProjectInputReloadMatchesChange(uri(globRoot), &created) {
146
+ t.Fatal("glob root outside reload directory exempted selection")
147
+ }
148
+ })
149
+
150
+ for _, test := range []struct {
151
+ name string
152
+ glob func(string) string
153
+ }{
154
+ {
155
+ name: "rooted on reload directory",
156
+ glob: func(root string) string {
157
+ return filepath.Join(root, "**", "*.json")
158
+ },
159
+ },
160
+ {
161
+ name: "rooted above reload directory",
162
+ glob: func(root string) string {
163
+ return filepath.Join(filepath.Dir(root), "**", "*.json")
164
+ },
165
+ },
166
+ } {
167
+ t.Run(test.name, func(t *testing.T) {
168
+ root := t.TempDir()
169
+ source := &NativePluginSource{
170
+ projectInputs: snapshot(root, []string{test.glob(root)}, nil),
171
+ }
172
+ entry := filepath.Join(root, "new-package")
173
+ if err := os.Mkdir(entry, 0o755); err != nil {
174
+ t.Fatal(err)
175
+ }
176
+ if !source.ProjectInputReloadMatchesChange(uri(entry), &created) {
177
+ t.Fatal("glob at or above reload directory exempted selection entry")
178
+ }
179
+ })
180
+ }
181
+ }
@@ -0,0 +1,47 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "bytes"
5
+ "crypto/sha256"
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ "testing"
10
+ )
11
+
12
+ // TestReloadFileFingerprintPreservesRawSymlinkTarget verifies the Go startup
13
+ // validator hashes exact-file symlinks with the launcher's byte protocol.
14
+ //
15
+ // Decoding a POSIX link target as UTF-8 replaces invalid bytes, making an
16
+ // unchanged launcher snapshot disagree with the native validator forever. The
17
+ // capability check keeps the vector portable to filesystems that cannot retain
18
+ // the raw name while requiring byte fidelity wherever they can.
19
+ //
20
+ // 1. Create a dangling symlink with a non-UTF-8 raw target where supported.
21
+ // 2. Read it back and continue only when the filesystem preserved the bytes.
22
+ // 3. Hash the protocol's symlink, raw target, and missing-content records.
23
+ // 4. Assert the production fingerprint is exactly that digest.
24
+ func TestReloadFileFingerprintPreservesRawSymlinkTarget(t *testing.T) {
25
+ target := string([]byte{0xff, 'x'})
26
+ link := filepath.Join(t.TempDir(), "reload-link")
27
+ if err := os.Symlink(target, link); err != nil {
28
+ return
29
+ }
30
+ retained, err := os.Readlink(link)
31
+ if err != nil {
32
+ t.Fatal(err)
33
+ }
34
+ if !bytes.Equal([]byte(retained), []byte(target)) {
35
+ return
36
+ }
37
+
38
+ digest := sha256.New()
39
+ digest.Write([]byte("symlink\x00"))
40
+ digest.Write([]byte(target))
41
+ digest.Write([]byte{0})
42
+ digest.Write([]byte("missing\x00"))
43
+ expected := fmt.Sprintf("%x", digest.Sum(nil))
44
+ if got := projectInputReloadFileDigest(link); got != expected {
45
+ t.Fatalf("reload symlink digest = %s, want raw-byte protocol %s", got, expected)
46
+ }
47
+ }
@@ -31,7 +31,7 @@ func TestResidentFallsBackWhenServeUnsupported(t *testing.T) {
31
31
  }
32
32
 
33
33
  source.residentMu.Lock()
34
- unsupported := source.serveUnsupported[plugin.Binary]
34
+ unsupported := source.serveUnsupported[pluginKey(plugin)]
35
35
  source.residentMu.Unlock()
36
36
  if !unsupported {
37
37
  t.Fatal("a first-spawn failure must mark lsp-serve unsupported so the source stops retrying")
@@ -0,0 +1,60 @@
1
+ package lspserver
2
+
3
+ import "testing"
4
+
5
+ // TestLSPResidentInvalidationBroadcastsProgramInputOverlap verifies an input's
6
+ // contributor ownership never hides its second role as a shared Program input.
7
+ //
8
+ // A TypeScript source or resolveJsonModule JSON file can be declared by one
9
+ // producer while belonging to every resident Program. All residents must see
10
+ // the content delta; the daemon itself skips an external path when its Program
11
+ // does not contain it.
12
+ //
13
+ // 1. Install two resident entries with distinct producer keys.
14
+ // 2. Attribute one shared-Program URI to the first producer only.
15
+ // 3. Assert both residents receive the changed and external deltas.
16
+ func TestLSPResidentInvalidationBroadcastsProgramInputOverlap(t *testing.T) {
17
+ for _, externalURI := range []string{
18
+ "file:///project/src/shared.ts",
19
+ "file:///project/src/shared.json",
20
+ } {
21
+ t.Run(externalURI, func(t *testing.T) {
22
+ first := NativeLSPPluginEntry{Binary: "first", Name: "@ttsc/first"}
23
+ second := NativeLSPPluginEntry{Binary: "second", Name: "@ttsc/second"}
24
+ firstResident := &residentSidecar{}
25
+ secondResident := &residentSidecar{}
26
+ source := &NativePluginSource{
27
+ plugins: []NativeLSPPluginEntry{first, second},
28
+ residents: map[string]*residentSidecar{
29
+ pluginKey(first): firstResident,
30
+ pluginKey(second): secondResident,
31
+ },
32
+ }
33
+
34
+ source.InvalidateResidentProgramsForOwnedWatchedChanges(
35
+ []string{externalURI},
36
+ []string{externalURI},
37
+ map[string][]string{
38
+ externalURI: {pluginKey(first)},
39
+ },
40
+ )
41
+
42
+ for label, resident := range map[string]*residentSidecar{
43
+ "owner": firstResident,
44
+ "non-owner": secondResident,
45
+ } {
46
+ if len(resident.changed) != 1 ||
47
+ resident.changed[0] != externalURI ||
48
+ len(resident.external) != 1 ||
49
+ resident.external[0] != externalURI {
50
+ t.Fatalf(
51
+ "%s resident deltas = changed %#v, external %#v",
52
+ label,
53
+ resident.changed,
54
+ resident.external,
55
+ )
56
+ }
57
+ }
58
+ })
59
+ }
60
+ }
@@ -0,0 +1,51 @@
1
+ package lspserver
2
+
3
+ import "testing"
4
+
5
+ // TestLSPResidentInvalidationTargetsProjectInputOwners verifies external data
6
+ // changes reach only resident binaries whose producer declared the input.
7
+ //
8
+ // 1. Install two resident entries with distinct producer keys.
9
+ // 2. Attribute one external URI to the first producer.
10
+ // 3. Assert only the first resident receives changed/external deltas.
11
+ func TestLSPResidentInvalidationTargetsProjectInputOwners(t *testing.T) {
12
+ first := NativeLSPPluginEntry{Binary: "first", Name: "@ttsc/first"}
13
+ second := NativeLSPPluginEntry{Binary: "second", Name: "@ttsc/second"}
14
+ firstResident := &residentSidecar{}
15
+ secondResident := &residentSidecar{}
16
+ source := &NativePluginSource{
17
+ plugins: []NativeLSPPluginEntry{first, second},
18
+ residents: map[string]*residentSidecar{
19
+ pluginKey(first): firstResident,
20
+ pluginKey(second): secondResident,
21
+ },
22
+ }
23
+ const externalURI = "file:///project/docs/spec.md"
24
+
25
+ source.InvalidateResidentProgramsForOwnedWatchedChanges(
26
+ []string{externalURI},
27
+ []string{externalURI},
28
+ map[string][]string{
29
+ externalURI: {pluginKey(first)},
30
+ },
31
+ )
32
+
33
+ if len(firstResident.changed) != 1 ||
34
+ firstResident.changed[0] != externalURI ||
35
+ len(firstResident.external) != 1 ||
36
+ firstResident.external[0] != externalURI {
37
+ t.Fatalf(
38
+ "owned resident deltas = changed %#v, external %#v",
39
+ firstResident.changed,
40
+ firstResident.external,
41
+ )
42
+ }
43
+ if len(secondResident.changed) != 0 ||
44
+ len(secondResident.external) != 0 {
45
+ t.Fatalf(
46
+ "unrelated resident received deltas = changed %#v, external %#v",
47
+ secondResident.changed,
48
+ secondResident.external,
49
+ )
50
+ }
51
+ }
@@ -0,0 +1,66 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+ )
7
+
8
+ // TestLSPSameTransportEntriesAreDeduplicated verifies logical manifest entries
9
+ // sharing one effective native launch identity produce one aggregate result.
10
+ //
11
+ // Native verbs receive the full plugin manifest and no selected-entry key. A
12
+ // composed aggregate and leaf can therefore have distinct names while invoking
13
+ // the same binary with the same argv; treating them as separate producers would
14
+ // duplicate diagnostics, inputs, completion hints, and command discovery.
15
+ func TestLSPSameTransportEntriesAreDeduplicated(t *testing.T) {
16
+ aggregate := NativeLSPPluginEntry{
17
+ Binary: "shared-sidecar",
18
+ Name: "@ttsc/aggregate",
19
+ ProjectDiagnostics: true,
20
+ ProjectInputs: true,
21
+ Stage: "check",
22
+ }
23
+ leaf := aggregate
24
+ leaf.Name = "@ttsc/leaf"
25
+
26
+ selected := selectPluginTransports(
27
+ []NativeLSPPluginEntry{aggregate, leaf},
28
+ nil,
29
+ )
30
+ if len(selected) != 1 || selected[0].Name != aggregate.Name {
31
+ t.Fatalf("same launch transport selection = %#v", selected)
32
+ }
33
+
34
+ root := t.TempDir()
35
+ snapshot := LSPProjectInputSnapshot{
36
+ Root: root,
37
+ Files: []string{filepath.Join(root, "spec.md")},
38
+ }
39
+ source := &NativePluginSource{
40
+ plugins: []NativeLSPPluginEntry{aggregate, leaf},
41
+ }
42
+ source.storeProjectInputs(aggregate, 1, snapshot)
43
+ source.storeProjectInputs(leaf, 1, snapshot)
44
+ owners := source.ProjectInputOwnersForURI(testFileURI(snapshot.Files[0]))
45
+ if len(owners) != 1 || owners[0] != pluginKey(aggregate) {
46
+ t.Fatalf("same transport input owners = %#v", owners)
47
+ }
48
+
49
+ publication := &LSPProjectDiagnostics{
50
+ URI: "file:///tsconfig.json",
51
+ Diagnostics: []LSPDiagnostic{
52
+ {Message: "one aggregate finding"},
53
+ },
54
+ }
55
+ source.storeProjectDiagnostics(aggregate, 1, publication)
56
+ source.storeProjectDiagnostics(leaf, 1, publication)
57
+ merged := source.projectDiagnosticsSnapshot()
58
+ if merged == nil || len(merged.Diagnostics) != 1 {
59
+ t.Fatalf("same transport project publication = %#v", merged)
60
+ }
61
+
62
+ result := source.ProjectDiagnosticsForOwners(nil)
63
+ if result.selected != 1 {
64
+ t.Fatalf("same transport selected %d project producers", result.selected)
65
+ }
66
+ }
@@ -5,8 +5,10 @@ import type { ITtscProjectLocatorOptions } from "../../../structures/internal/IT
5
5
  *
6
6
  * Follows `extends` chains (including arrays) and merges compiler options with
7
7
  * later configs taking precedence. Path-typed options (`baseUrl`,
8
- * `declarationDir`, `rootDir`) are resolved relative to the config that
9
- * declares them. The `outDir` is resolved to an absolute path. Plugins are
10
- * inherited from the nearest ancestor that declares them.
8
+ * `declarationDir`, `outFile`, `rootDir`, `tsBuildInfoFile`) are resolved
9
+ * relative to the config that declares them. `${configDir}` paths are resolved
10
+ * against the final consuming config after the extends chain is merged. The
11
+ * `outDir` is resolved to an absolute path. Plugins are inherited from the
12
+ * nearest ancestor that declares them.
11
13
  */
12
14
  export declare function readProjectConfig(opts?: ITtscProjectLocatorOptions): ITtscParsedProjectConfig;