ttsc 0.22.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 (46) hide show
  1. package/go.mod +1 -1
  2. package/internal/lspserver/lsp_launcher_reload_directory_fingerprint_parity_windows_test.go +94 -0
  3. package/internal/lspserver/lsp_project_input_glob_path_other.go +16 -0
  4. package/internal/lspserver/lsp_project_input_glob_path_windows.go +36 -0
  5. package/internal/lspserver/lsp_project_input_path_key_other.go +17 -0
  6. package/internal/lspserver/lsp_project_input_path_key_windows.go +228 -0
  7. package/internal/lspserver/lsp_project_input_path_key_windows_test.go +460 -0
  8. package/internal/lspserver/lsp_project_inputs.go +117 -49
  9. package/internal/lspserver/lsp_reload_directories_separate_glob_territory_test.go +181 -0
  10. package/lib/flags/schema.js +1 -1
  11. package/lib/flags/schema.js.map +1 -1
  12. package/lib/internal/projectInputPathIdentity.d.ts +13 -5
  13. package/lib/internal/projectInputPathIdentity.js +95 -41
  14. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  15. package/lib/internal/windowsDirectoryCaseSensitivity.d.ts +10 -0
  16. package/lib/internal/windowsDirectoryCaseSensitivity.js +34 -0
  17. package/lib/internal/windowsDirectoryCaseSensitivity.js.map +1 -0
  18. package/lib/launcher/internal/runTtsc.js +5 -0
  19. package/lib/launcher/internal/runTtsc.js.map +1 -1
  20. package/lib/launcher/internal/runTtscserver.js +59 -8
  21. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  22. package/lib/launcher/internal/runTtsx.js +61 -7
  23. package/lib/launcher/internal/runTtsx.js.map +1 -1
  24. package/lib/launcher/internal/runtimeHooks.d.ts +6 -5
  25. package/lib/launcher/internal/runtimeHooks.js +7 -15
  26. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  27. package/lib/launcher/internal/watchTopology.d.ts +82 -8
  28. package/lib/launcher/internal/watchTopology.js +535 -187
  29. package/lib/launcher/internal/watchTopology.js.map +1 -1
  30. package/lib/plugin/internal/descriptorProcessFailure.d.ts +32 -0
  31. package/lib/plugin/internal/descriptorProcessFailure.js +73 -0
  32. package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -0
  33. package/lib/plugin/internal/loadProjectPlugins.js +29 -8
  34. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  35. package/package.json +12 -8
  36. package/src/flags/schema.ts +2 -1
  37. package/src/internal/projectInputPathIdentity.ts +150 -46
  38. package/src/internal/windowsDirectoryCaseSensitivity.ts +33 -0
  39. package/src/launcher/internal/runTtsc.ts +7 -0
  40. package/src/launcher/internal/runTtscserver.ts +81 -11
  41. package/src/launcher/internal/runTtsx.ts +87 -10
  42. package/src/launcher/internal/runtimeHooks.ts +16 -16
  43. package/src/launcher/internal/watchTopology.ts +679 -208
  44. package/src/plugin/internal/descriptorProcessFailure.ts +100 -0
  45. package/src/plugin/internal/loadProjectPlugins.ts +41 -8
  46. package/tools/shim_audit/baseline.json +0 -9
@@ -0,0 +1,460 @@
1
+ //go:build windows
2
+
3
+ package lspserver
4
+
5
+ import (
6
+ "os"
7
+ "os/exec"
8
+ "path/filepath"
9
+ "reflect"
10
+ "slices"
11
+ "strings"
12
+ "testing"
13
+ )
14
+
15
+ // TestProjectInputPathKeyRespectsDirectoryCaseSemantics verifies the Go host
16
+ // keeps case-distinct Windows dependencies without splitting ordinary aliases.
17
+ //
18
+ // The original global case fold merged distinct files below an opted-in
19
+ // case-sensitive directory, while preserving every spelling instead split
20
+ // ordinary NTFS, UNC, and recreated-directory aliases. Identity must instead
21
+ // follow the case semantics of the directory that owns each path segment.
22
+ //
23
+ // 1. Enable case sensitivity on a disposable directory and create two real
24
+ // dependencies whose paths differ only by case.
25
+ // 2. Prove merged publication and owner matching retain both identities.
26
+ // 3. Prove glob matching and reload containment obey each owning directory.
27
+ // 4. Prove missing suffixes also retain case under an opted-in directory.
28
+ // 5. On an ordinary directory, prove existing and missing aliases converge.
29
+ // 6. Change a live directory's flag and normalize UNC volume aliases.
30
+ func TestProjectInputPathKeyRespectsDirectoryCaseSemantics(t *testing.T) {
31
+ sensitiveRoot := t.TempDir()
32
+ enableProjectInputCaseSensitivity(t, sensitiveRoot)
33
+
34
+ firstDirectory := filepath.Join(sensitiveRoot, "Project")
35
+ secondDirectory := filepath.Join(sensitiveRoot, "project")
36
+ if err := os.Mkdir(firstDirectory, 0o755); err != nil {
37
+ t.Fatal(err)
38
+ }
39
+ if err := os.Mkdir(secondDirectory, 0o755); err != nil {
40
+ t.Fatal(err)
41
+ }
42
+ enableProjectInputCaseSensitivity(t, firstDirectory)
43
+ enableProjectInputCaseSensitivity(t, secondDirectory)
44
+
45
+ firstInput := filepath.Join(firstDirectory, "Spec.md")
46
+ secondInput := filepath.Join(secondDirectory, "Spec.md")
47
+ if err := os.WriteFile(firstInput, []byte("first\n"), 0o644); err != nil {
48
+ t.Fatal(err)
49
+ }
50
+ if err := os.WriteFile(secondInput, []byte("second\n"), 0o644); err != nil {
51
+ t.Fatal(err)
52
+ }
53
+
54
+ first := NativeLSPPluginEntry{
55
+ Binary: "ttsc-case-sensitive-first",
56
+ Name: "@ttsc/case-sensitive-first",
57
+ ProjectInputs: true,
58
+ }
59
+ second := NativeLSPPluginEntry{
60
+ Binary: "ttsc-case-sensitive-second",
61
+ Name: "@ttsc/case-sensitive-second",
62
+ ProjectInputs: true,
63
+ }
64
+ source := &NativePluginSource{
65
+ plugins: []NativeLSPPluginEntry{first, second},
66
+ }
67
+ source.storeProjectInputs(first, 1, LSPProjectInputSnapshot{
68
+ Root: filepath.ToSlash(sensitiveRoot),
69
+ Files: []string{filepath.ToSlash(firstInput)},
70
+ })
71
+ source.storeProjectInputs(second, 1, LSPProjectInputSnapshot{
72
+ Root: filepath.ToSlash(sensitiveRoot),
73
+ Files: []string{filepath.ToSlash(secondInput)},
74
+ })
75
+
76
+ merged := source.ProjectInputs()
77
+ if !reflect.DeepEqual(
78
+ merged.Files,
79
+ []string{filepath.ToSlash(firstInput), filepath.ToSlash(secondInput)},
80
+ ) {
81
+ t.Fatalf("case-sensitive merged files = %#v", merged.Files)
82
+ }
83
+ assertProjectInputOwners(t, source, firstInput, []string{pluginKey(first)})
84
+ assertProjectInputOwners(t, source, secondInput, []string{pluginKey(second)})
85
+ if projectInputPathContains(firstDirectory, secondInput) {
86
+ t.Fatal("case-sensitive sibling was classified as a descendant")
87
+ }
88
+ reloadSource := &NativePluginSource{
89
+ plugins: []NativeLSPPluginEntry{first},
90
+ }
91
+ reloadSource.storeProjectInputs(first, 1, LSPProjectInputSnapshot{
92
+ Root: filepath.ToSlash(sensitiveRoot),
93
+ ReloadDirectories: []string{filepath.ToSlash(firstDirectory)},
94
+ })
95
+ if reloadSource.ProjectInputReloadMatchesURI(testFileURI(secondInput)) {
96
+ t.Fatal("case-sensitive sibling was classified as an immediate reload entry")
97
+ }
98
+
99
+ upperJSON := filepath.Join(firstDirectory, "Upper.JSON")
100
+ lowerJSON := filepath.Join(firstDirectory, "lower.json")
101
+ if err := os.WriteFile(upperJSON, []byte("{}\n"), 0o644); err != nil {
102
+ t.Fatal(err)
103
+ }
104
+ if err := os.WriteFile(lowerJSON, []byte("{}\n"), 0o644); err != nil {
105
+ t.Fatal(err)
106
+ }
107
+ sensitiveGlobSource := &NativePluginSource{
108
+ plugins: []NativeLSPPluginEntry{first},
109
+ }
110
+ sensitiveGlobSource.storeProjectInputs(first, 1, LSPProjectInputSnapshot{
111
+ Root: filepath.ToSlash(sensitiveRoot),
112
+ Globs: []string{filepath.ToSlash(filepath.Join(firstDirectory, "*.json"))},
113
+ })
114
+ assertProjectInputOwners(
115
+ t,
116
+ sensitiveGlobSource,
117
+ lowerJSON,
118
+ []string{pluginKey(first)},
119
+ )
120
+ assertProjectInputOwners(t, sensitiveGlobSource, upperJSON, nil)
121
+
122
+ insensitiveChild := filepath.Join(sensitiveRoot, "Insensitive")
123
+ if err := os.Mkdir(insensitiveChild, 0o755); err != nil {
124
+ t.Fatal(err)
125
+ }
126
+ disableProjectInputCaseSensitivity(t, insensitiveChild)
127
+ mixedJSON := filepath.Join(insensitiveChild, "Schema.JSON")
128
+ if err := os.WriteFile(mixedJSON, []byte("{}\n"), 0o644); err != nil {
129
+ t.Fatal(err)
130
+ }
131
+ mixedGlobSource := &NativePluginSource{
132
+ plugins: []NativeLSPPluginEntry{first},
133
+ }
134
+ mixedGlobSource.storeProjectInputs(first, 1, LSPProjectInputSnapshot{
135
+ Root: filepath.ToSlash(sensitiveRoot),
136
+ Globs: []string{
137
+ filepath.ToSlash(filepath.Join(insensitiveChild, "*.json")),
138
+ },
139
+ })
140
+ assertProjectInputOwners(
141
+ t,
142
+ mixedGlobSource,
143
+ mixedJSON,
144
+ []string{pluginKey(first)},
145
+ )
146
+
147
+ firstMissing := filepath.Join(firstDirectory, "Missing.json")
148
+ secondMissing := filepath.Join(firstDirectory, "missing.json")
149
+ if projectInputPathKey(firstMissing) == projectInputPathKey(secondMissing) {
150
+ t.Fatal("case-sensitive missing suffixes collapsed")
151
+ }
152
+
153
+ ordinaryRoot := t.TempDir()
154
+ if queryProjectInputDirectoryCaseSensitivity(ordinaryRoot) {
155
+ t.Skip("ordinary-volume negative twin requires a case-insensitive temp root")
156
+ }
157
+ ordinaryInput := filepath.Join(ordinaryRoot, "Alias.md")
158
+ ordinaryAlias := filepath.Join(ordinaryRoot, "aLIAS.MD")
159
+ if err := os.WriteFile(ordinaryInput, []byte("ordinary\n"), 0o644); err != nil {
160
+ t.Fatal(err)
161
+ }
162
+ if _, err := os.Stat(ordinaryAlias); err != nil {
163
+ t.Skipf("temp volume does not resolve case aliases: %v", err)
164
+ }
165
+ if projectInputPathKey(ordinaryInput) != projectInputPathKey(ordinaryAlias) {
166
+ t.Fatal("ordinary existing aliases split")
167
+ }
168
+ if projectInputPathKey(filepath.Join(ordinaryRoot, "Missing.json")) !=
169
+ projectInputPathKey(filepath.Join(ordinaryRoot, "missing.json")) {
170
+ t.Fatal("ordinary missing aliases split")
171
+ }
172
+ upperJSON = filepath.Join(ordinaryRoot, "Schema.JSON")
173
+ if err := os.WriteFile(upperJSON, []byte("{}\n"), 0o644); err != nil {
174
+ t.Fatal(err)
175
+ }
176
+ ordinaryGlobSource := &NativePluginSource{
177
+ plugins: []NativeLSPPluginEntry{first},
178
+ }
179
+ ordinaryGlobSource.storeProjectInputs(first, 1, LSPProjectInputSnapshot{
180
+ Root: filepath.ToSlash(ordinaryRoot),
181
+ Globs: []string{filepath.ToSlash(filepath.Join(ordinaryRoot, "*.json"))},
182
+ })
183
+ assertProjectInputOwners(
184
+ t,
185
+ ordinaryGlobSource,
186
+ upperJSON,
187
+ []string{pluginKey(first)},
188
+ )
189
+
190
+ ordinarySource := &NativePluginSource{
191
+ plugins: []NativeLSPPluginEntry{first, second},
192
+ }
193
+ ordinarySource.storeProjectInputs(first, 1, LSPProjectInputSnapshot{
194
+ Root: filepath.ToSlash(ordinaryRoot),
195
+ Files: []string{filepath.ToSlash(ordinaryInput)},
196
+ })
197
+ ordinarySource.storeProjectInputs(second, 1, LSPProjectInputSnapshot{
198
+ Root: filepath.ToSlash(ordinaryRoot),
199
+ Files: []string{filepath.ToSlash(ordinaryAlias)},
200
+ })
201
+ if got := ordinarySource.ProjectInputs().Files; len(got) != 1 {
202
+ t.Fatalf("ordinary aliases produced %d merged files: %#v", len(got), got)
203
+ }
204
+
205
+ mutableParent := t.TempDir()
206
+ mutableRoot := filepath.Join(mutableParent, "mutable")
207
+ if err := os.Mkdir(mutableRoot, 0o755); err != nil {
208
+ t.Fatal(err)
209
+ }
210
+ firstMissing = filepath.Join(mutableRoot, "Missing.json")
211
+ secondMissing = filepath.Join(mutableRoot, "missing.json")
212
+ if projectInputPathKey(firstMissing) != projectInputPathKey(secondMissing) {
213
+ t.Fatal("ordinary mutable directory began case-sensitive")
214
+ }
215
+ enableProjectInputCaseSensitivity(t, mutableRoot)
216
+ if projectInputPathKey(firstMissing) == projectInputPathKey(secondMissing) {
217
+ t.Fatal("case-sensitivity change was hidden by stale identity state")
218
+ }
219
+ if err := os.Remove(mutableRoot); err != nil {
220
+ t.Fatal(err)
221
+ }
222
+ if err := os.Mkdir(mutableRoot, 0o755); err != nil {
223
+ t.Fatal(err)
224
+ }
225
+ if projectInputPathKey(firstMissing) != projectInputPathKey(secondMissing) {
226
+ t.Fatal("recreated ordinary directory retained stale sensitivity")
227
+ }
228
+
229
+ upperUNC := windowsProjectInputKey(
230
+ `\\SERVER\Share\Folder\File.json`,
231
+ []string{"folder", "file.json"},
232
+ )
233
+ lowerUNC := windowsProjectInputKey(
234
+ `\\server\share\Folder\File.json`,
235
+ []string{"folder", "file.json"},
236
+ )
237
+ if upperUNC != lowerUNC || upperUNC != "//server/share/folder/file.json" {
238
+ t.Fatalf("UNC volume aliases = %q and %q", upperUNC, lowerUNC)
239
+ }
240
+
241
+ t.Run("extended UNC aliases", func(t *testing.T) {
242
+ volume := filepath.VolumeName(ordinaryRoot)
243
+ if len(volume) != 2 || volume[1] != ':' {
244
+ t.Skipf("temporary directory has no drive volume: %q", volume)
245
+ }
246
+ tail := strings.TrimPrefix(ordinaryRoot, volume)
247
+ unc := `\\localhost\` + strings.ToUpper(volume[:1]) + `$` + tail
248
+ if _, err := os.Stat(unc); err != nil {
249
+ t.Skipf("administrative UNC share is unavailable: %v", err)
250
+ }
251
+ extended := `\\?\UNC\` + strings.TrimPrefix(unc, `\\`)
252
+ if projectInputPathKey(realProjectInputPath(unc)) !=
253
+ projectInputPathKey(realProjectInputPath(extended)) {
254
+ t.Fatalf("standard and extended UNC aliases split: %q, %q", unc, extended)
255
+ }
256
+ })
257
+
258
+ t.Run("junction physical identity", func(t *testing.T) {
259
+ declarationRoot := t.TempDir()
260
+ physicalRoot := t.TempDir()
261
+ for _, directory := range []string{
262
+ filepath.Join(physicalRoot, "docs"),
263
+ filepath.Join(physicalRoot, "api"),
264
+ } {
265
+ if err := os.MkdirAll(directory, 0o755); err != nil {
266
+ t.Fatal(err)
267
+ }
268
+ }
269
+ exact := filepath.Join(physicalRoot, "docs", "spec.md")
270
+ globbed := filepath.Join(physicalRoot, "api", "schema.json")
271
+ for _, location := range []string{exact, globbed} {
272
+ if err := os.WriteFile(location, []byte("{}\n"), 0o644); err != nil {
273
+ t.Fatal(err)
274
+ }
275
+ }
276
+ junction := filepath.Join(declarationRoot, "alias")
277
+ createProjectInputJunction(t, junction, physicalRoot)
278
+ snapshot, err := normalizeLSPProjectInputSnapshot(
279
+ LSPProjectInputSnapshot{
280
+ Root: declarationRoot,
281
+ Files: []string{
282
+ filepath.Join(junction, "docs", "spec.md"),
283
+ },
284
+ Globs: []string{
285
+ filepath.Join(junction, "api", "**", "*.json"),
286
+ },
287
+ },
288
+ declarationRoot,
289
+ )
290
+ if err != nil {
291
+ t.Fatalf("normalize junction inputs: %v", err)
292
+ }
293
+ junctionSource := &NativePluginSource{
294
+ plugins: []NativeLSPPluginEntry{first},
295
+ }
296
+ junctionSource.storeProjectInputs(first, 1, snapshot)
297
+ if !junctionSource.ProjectInputReloadFingerprintsAreCurrent() {
298
+ t.Fatal("junction reload baseline began stale")
299
+ }
300
+ assertProjectInputOwners(
301
+ t,
302
+ junctionSource,
303
+ exact,
304
+ []string{pluginKey(first)},
305
+ )
306
+ assertProjectInputOwners(
307
+ t,
308
+ junctionSource,
309
+ globbed,
310
+ []string{pluginKey(first)},
311
+ )
312
+ })
313
+
314
+ t.Run("external junction selects reload", func(t *testing.T) {
315
+ reloadRoot := t.TempDir()
316
+ externalRoot := t.TempDir()
317
+ junction := filepath.Join(reloadRoot, "api")
318
+ snapshot, err := normalizeLSPProjectInputSnapshot(
319
+ LSPProjectInputSnapshot{
320
+ Root: reloadRoot,
321
+ Globs: []string{filepath.Join(junction, "**", "*.json")},
322
+ ReloadDirectories: []string{reloadRoot},
323
+ },
324
+ reloadRoot,
325
+ )
326
+ if err != nil {
327
+ t.Fatalf("normalize reload inputs: %v", err)
328
+ }
329
+ junctionSource := &NativePluginSource{
330
+ plugins: []NativeLSPPluginEntry{first},
331
+ }
332
+ junctionSource.storeProjectInputs(first, 1, snapshot)
333
+ createProjectInputJunction(t, junction, externalRoot)
334
+ created := fileChangeTypeCreated
335
+ if !junctionSource.ProjectInputReloadMatchesChange(
336
+ testFileURI(junction),
337
+ &created,
338
+ ) {
339
+ t.Fatal("external junction creation did not select plugin reload")
340
+ }
341
+ })
342
+
343
+ t.Run("junction reload directory retains entry identity", func(t *testing.T) {
344
+ parent := t.TempDir()
345
+ firstTarget := t.TempDir()
346
+ secondTarget := t.TempDir()
347
+ junction := filepath.Join(parent, "selection")
348
+ createProjectInputJunction(t, junction, firstTarget)
349
+ snapshot, err := normalizeLSPProjectInputSnapshot(
350
+ LSPProjectInputSnapshot{
351
+ Root: parent,
352
+ ReloadDirectories: []string{junction},
353
+ },
354
+ parent,
355
+ )
356
+ if err != nil {
357
+ t.Fatalf("normalize junction reload directory: %v", err)
358
+ }
359
+ if len(snapshot.ReloadDirectories) != 2 {
360
+ t.Fatalf(
361
+ "junction reload identities = %#v, want physical and entry spellings",
362
+ snapshot.ReloadDirectories,
363
+ )
364
+ }
365
+ junctionSource := &NativePluginSource{
366
+ plugins: []NativeLSPPluginEntry{first},
367
+ }
368
+ junctionSource.storeProjectInputs(first, 1, snapshot)
369
+
370
+ if err := os.Remove(junction); err != nil {
371
+ t.Fatal(err)
372
+ }
373
+ deleted := fileChangeTypeDeleted
374
+ if !junctionSource.ProjectInputReloadMatchesChange(
375
+ testFileURI(junction),
376
+ &deleted,
377
+ ) {
378
+ t.Fatal("junction reload directory deletion was not selected")
379
+ }
380
+
381
+ createProjectInputJunction(t, junction, secondTarget)
382
+ if junctionSource.ProjectInputReloadFingerprintsAreCurrent() {
383
+ t.Fatal("same-topology junction retarget kept startup fingerprint current")
384
+ }
385
+ changed := fileChangeTypeChanged
386
+ if !junctionSource.ProjectInputReloadMatchesChange(
387
+ testFileURI(junction),
388
+ &changed,
389
+ ) {
390
+ t.Fatal("junction reload directory retarget was not selected")
391
+ }
392
+ })
393
+ }
394
+
395
+ func enableProjectInputCaseSensitivity(t *testing.T, directory string) {
396
+ t.Helper()
397
+ command := exec.Command(
398
+ "fsutil.exe",
399
+ "file",
400
+ "setCaseSensitiveInfo",
401
+ directory,
402
+ "enable",
403
+ )
404
+ if output, err := command.CombinedOutput(); err != nil {
405
+ t.Skipf(
406
+ "per-directory case sensitivity is unavailable: %v\n%s",
407
+ err,
408
+ output,
409
+ )
410
+ }
411
+ }
412
+
413
+ func disableProjectInputCaseSensitivity(t *testing.T, directory string) {
414
+ t.Helper()
415
+ command := exec.Command(
416
+ "fsutil.exe",
417
+ "file",
418
+ "setCaseSensitiveInfo",
419
+ directory,
420
+ "disable",
421
+ )
422
+ if output, err := command.CombinedOutput(); err != nil {
423
+ t.Fatalf("failed to disable per-directory case sensitivity: %v\n%s", err, output)
424
+ }
425
+ }
426
+
427
+ func createProjectInputJunction(
428
+ t *testing.T,
429
+ junction string,
430
+ target string,
431
+ ) {
432
+ t.Helper()
433
+ command := exec.Command(
434
+ "cmd.exe",
435
+ "/d",
436
+ "/c",
437
+ "mklink",
438
+ "/J",
439
+ junction,
440
+ target,
441
+ )
442
+ if output, err := command.CombinedOutput(); err != nil {
443
+ t.Skipf("directory junction is unavailable: %v\n%s", err, output)
444
+ }
445
+ }
446
+
447
+ func assertProjectInputOwners(
448
+ t *testing.T,
449
+ source *NativePluginSource,
450
+ input string,
451
+ want []string,
452
+ ) {
453
+ t.Helper()
454
+ if got := source.ProjectInputOwnersForURI(testFileURI(input)); !slices.Equal(
455
+ got,
456
+ want,
457
+ ) {
458
+ t.Fatalf("%s owners = %#v, want %#v", input, got, want)
459
+ }
460
+ }