ttsc 0.23.0 → 0.25.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 (149) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +66 -63
  3. package/cmd/ttscgraph/serve.go +264 -48
  4. package/cmd/ttscgraph/serve_answers_a_project_that_implements_its_own_ambient_declaration_test.go +69 -0
  5. package/cmd/ttscgraph/serve_early_error_encode_failure_is_observable_test.go +40 -0
  6. package/cmd/ttscgraph/serve_graph_snapshot_version_matches_typescript_client_test.go +32 -0
  7. package/cmd/ttscgraph/serve_phase_trace_accounts_for_load_failure_test.go +45 -0
  8. package/cmd/ttscgraph/serve_phase_trace_is_opt_in_and_payload_free_test.go +52 -0
  9. package/cmd/ttscgraph/serve_protocol_negotiates_incremental_shards_test.go +44 -0
  10. package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +2 -2
  11. package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +46 -20
  12. package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +9 -9
  13. package/cmd/ttscgraph/serve_shards.go +1390 -0
  14. package/cmd/ttscgraph/serve_shards_abandoned_generation_converges_on_retry_test.go +92 -0
  15. package/cmd/ttscgraph/serve_shards_delete_removed_source_test.go +57 -0
  16. package/cmd/ttscgraph/serve_shards_external_endpoints_test.go +53 -0
  17. package/cmd/ttscgraph/serve_shards_keep_source_distributed_dependencies_at_the_boundary_test.go +108 -0
  18. package/cmd/ttscgraph/serve_shards_match_full_dump_oracle_test.go +88 -0
  19. package/cmd/ttscgraph/serve_shards_merge_pending_and_new_edits_test.go +50 -0
  20. package/cmd/ttscgraph/serve_shards_prune_unreferenced_external_endpoints_test.go +65 -0
  21. package/cmd/ttscgraph/serve_shards_publish_created_source_test.go +92 -0
  22. package/cmd/ttscgraph/serve_shards_rebuild_declaration_edit_test.go +43 -0
  23. package/cmd/ttscgraph/serve_shards_refresh_cross_file_implementation_test.go +63 -0
  24. package/cmd/ttscgraph/serve_shards_refresh_dependent_diagnostics_test.go +80 -0
  25. package/cmd/ttscgraph/serve_shards_rename_moves_source_identity_test.go +95 -0
  26. package/cmd/ttscgraph/serve_shards_retry_preserves_committed_generation_test.go +48 -0
  27. package/cmd/ttscgraph/serve_shards_reuse_unaffected_sources_test.go +121 -0
  28. package/driver/declaration_shape.go +85 -0
  29. package/driver/program.go +39 -10
  30. package/go.sum +22 -22
  31. package/internal/graph/assigned_implementations_of_declaration_file_symbols_leave_no_edge_source_test.go +84 -0
  32. package/internal/graph/assigned_implementations_of_node_modules_symbols_leave_no_edge_source_test.go +85 -0
  33. package/internal/graph/build.go +62 -10
  34. package/internal/graph/build_files_reuses_committed_cross_file_endpoints_test.go +87 -0
  35. package/internal/graph/build_keeps_source_distributed_dependencies_at_the_external_boundary_test.go +74 -0
  36. package/internal/graph/dump.go +84 -43
  37. package/internal/graph/dump_path_mapper_caches_alias_resolution_test.go +29 -0
  38. package/internal/graph/dump_paths.go +134 -11
  39. package/internal/graph/dump_paths_canonicalize_windows_short_root_test.go +39 -0
  40. package/internal/graph/dump_rejects_provenance_only_path_errors_test.go +32 -0
  41. package/internal/graph/dump_serializes_the_full_graph_test.go +2 -2
  42. package/internal/graph/dump_symlink_project_publishes_matching_base_test.go +82 -0
  43. package/internal/graph/edges.go +73 -17
  44. package/internal/graph/exports.go +9 -2
  45. package/internal/graph/graph.go +47 -7
  46. package/internal/graph/graph_kind_contracts_match_their_producers_test.go +121 -0
  47. package/internal/graph/node_modifiers_emit_union_strings_test.go +1 -1
  48. package/internal/graph/provenance.go +11 -1
  49. package/internal/graph/resolve.go +1 -4
  50. package/internal/graph/source.go +23 -0
  51. package/internal/graph/wire_node_ids_share_alias_resolution_test.go +29 -0
  52. package/internal/lspserver/lsp_native_plugin_source.go +5 -6
  53. package/internal/lspserver/lsp_native_resident.go +17 -24
  54. package/lib/TtscService.d.ts +1 -6
  55. package/lib/TtscService.js +2 -2
  56. package/lib/TtscService.js.map +1 -1
  57. package/lib/compiler/internal/captureProcessOutput.d.ts +26 -0
  58. package/lib/compiler/internal/captureProcessOutput.js +92 -0
  59. package/lib/compiler/internal/captureProcessOutput.js.map +1 -0
  60. package/lib/compiler/internal/compileProjectInMemory.js +4 -7
  61. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  62. package/lib/compiler/internal/residentCheckProcess.d.ts +0 -2
  63. package/lib/compiler/internal/residentCheckProcess.js +1 -15
  64. package/lib/compiler/internal/residentCheckProcess.js.map +1 -1
  65. package/lib/compiler/internal/residentTransformProcess.d.ts +0 -6
  66. package/lib/compiler/internal/residentTransformProcess.js +0 -25
  67. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  68. package/lib/compiler/internal/spawnNative.d.ts +14 -6
  69. package/lib/compiler/internal/spawnNative.js +32 -11
  70. package/lib/compiler/internal/spawnNative.js.map +1 -1
  71. package/lib/compiler/internal/startResidentTransform.d.ts +1 -5
  72. package/lib/compiler/internal/startResidentTransform.js +1 -3
  73. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  74. package/lib/launcher/internal/prepareExecution.d.ts +2 -1
  75. package/lib/launcher/internal/prepareExecution.js +269 -23
  76. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  77. package/lib/launcher/internal/registerRuntimeHooks.js +6 -1
  78. package/lib/launcher/internal/registerRuntimeHooks.js.map +1 -1
  79. package/lib/launcher/internal/runTtscserver.js +4 -9
  80. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  81. package/lib/launcher/internal/runTtsx.js +2 -55
  82. package/lib/launcher/internal/runTtsx.js.map +1 -1
  83. package/lib/launcher/internal/runtimeHooks.d.ts +25 -2
  84. package/lib/launcher/internal/runtimeHooks.js +344 -69
  85. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  86. package/lib/plugin/internal/buildSourcePlugin.js +31 -3
  87. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  88. package/lib/plugin/internal/descriptorProcessFailure.d.ts +23 -19
  89. package/lib/plugin/internal/descriptorProcessFailure.js +40 -51
  90. package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -1
  91. package/lib/plugin/internal/loadProjectPlugins.d.ts +12 -0
  92. package/lib/plugin/internal/loadProjectPlugins.js +77 -28
  93. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  94. package/package.json +8 -8
  95. package/shim/ast/extra-shim.json +4 -4
  96. package/shim/ast/go.mod +14 -14
  97. package/shim/ast/go.sum +20 -20
  98. package/shim/astnav/go.mod +14 -14
  99. package/shim/astnav/go.sum +20 -20
  100. package/shim/bundled/extra-shim.json +1 -1
  101. package/shim/bundled/go.mod +5 -5
  102. package/shim/checker/extra-shim.json +31 -31
  103. package/shim/checker/go.mod +5 -5
  104. package/shim/compiler/extra-shim.json +4 -4
  105. package/shim/compiler/go.mod +14 -14
  106. package/shim/compiler/go.sum +22 -22
  107. package/shim/core/extra-shim.json +4 -4
  108. package/shim/core/go.mod +5 -5
  109. package/shim/diagnosticwriter/go.mod +5 -5
  110. package/shim/lsp/extra-shim.json +4 -4
  111. package/shim/lsp/go.mod +16 -16
  112. package/shim/lsp/go.sum +26 -26
  113. package/shim/parser/extra-shim.json +4 -4
  114. package/shim/parser/go.mod +5 -5
  115. package/shim/printer/go.mod +14 -14
  116. package/shim/printer/go.sum +22 -22
  117. package/shim/scanner/extra-shim.json +4 -4
  118. package/shim/scanner/go.mod +5 -5
  119. package/shim/transformers/go.mod +14 -14
  120. package/shim/transformers/go.sum +20 -20
  121. package/shim/tsoptions/extra-shim.json +4 -4
  122. package/shim/tsoptions/go.mod +5 -5
  123. package/shim/tspath/extra-shim.json +4 -4
  124. package/shim/tspath/go.mod +5 -5
  125. package/shim/vfs/cachedvfs/extra-shim.json +4 -4
  126. package/shim/vfs/cachedvfs/go.mod +5 -5
  127. package/shim/vfs/extra-shim.json +4 -4
  128. package/shim/vfs/go.mod +5 -5
  129. package/shim/vfs/osvfs/extra-shim.json +4 -4
  130. package/shim/vfs/osvfs/go.mod +5 -5
  131. package/src/TtscService.ts +8 -20
  132. package/src/compiler/internal/captureProcessOutput.ts +96 -0
  133. package/src/compiler/internal/compileProjectInMemory.ts +7 -7
  134. package/src/compiler/internal/residentCheckProcess.ts +1 -21
  135. package/src/compiler/internal/residentTransformProcess.ts +0 -42
  136. package/src/compiler/internal/spawnNative.ts +39 -18
  137. package/src/compiler/internal/startResidentTransform.ts +1 -12
  138. package/src/launcher/internal/prepareExecution.ts +297 -26
  139. package/src/launcher/internal/registerRuntimeHooks.ts +11 -2
  140. package/src/launcher/internal/runTtscserver.ts +4 -9
  141. package/src/launcher/internal/runTtsx.ts +2 -78
  142. package/src/launcher/internal/runtimeHooks.ts +385 -82
  143. package/src/plugin/internal/buildSourcePlugin.ts +36 -3
  144. package/src/plugin/internal/descriptorProcessFailure.ts +34 -65
  145. package/src/plugin/internal/loadProjectPlugins.ts +78 -39
  146. package/tools/gen_shims/go.mod +14 -14
  147. package/tools/gen_shims/go.sum +12 -12
  148. package/tools/shim_audit/go.mod +10 -10
  149. package/tools/shim_audit/go.sum +8 -8
@@ -2,6 +2,8 @@ package graph
2
2
 
3
3
  import (
4
4
  "fmt"
5
+ "path/filepath"
6
+ "runtime"
5
7
  "strings"
6
8
 
7
9
  shimtspath "github.com/microsoft/typescript-go/shim/tspath"
@@ -12,19 +14,26 @@ import (
12
14
  // reject both an unportable filesystem root and a non-injective projection
13
15
  // before any JSON is written.
14
16
  type dumpPathMapper struct {
17
+ rawProject string
15
18
  project string
16
19
  caseSensitive bool
20
+ canonicalize func(string) string
17
21
 
22
+ rawToWire map[string]string
18
23
  physicalToWire map[string]string
19
24
  wireToPhysical map[string]string
20
25
  mappingErr error
21
26
  }
22
27
 
23
28
  func newDumpPathMapper(project string) *dumpPathMapper {
24
- normalized := shimtspath.NormalizePath(shimtspath.NormalizeSlashes(project))
29
+ raw := shimtspath.NormalizePath(shimtspath.NormalizeSlashes(project))
30
+ normalized := canonicalDumpPath(raw)
25
31
  mapper := &dumpPathMapper{
32
+ rawProject: raw,
26
33
  project: normalized,
27
34
  caseSensitive: dumpPathRootIsCaseSensitive(normalized),
35
+ canonicalize: canonicalDumpPath,
36
+ rawToWire: map[string]string{},
28
37
  physicalToWire: map[string]string{},
29
38
  wireToPhysical: map[string]string{},
30
39
  }
@@ -34,6 +43,62 @@ func newDumpPathMapper(project string) *dumpPathMapper {
34
43
  return mapper
35
44
  }
36
45
 
46
+ // WireProject returns the canonical filesystem base that owns every relative
47
+ // wire path emitted by this package.
48
+ func WireProject(project string) (string, error) {
49
+ mapper := newDumpPathMapper(project)
50
+ return mapper.project, mapper.err()
51
+ }
52
+
53
+ // WirePath maps one compiler path into the portable schema-v6 vocabulary used
54
+ // by dumps and resident graph shards. Callers that project a complete graph
55
+ // keep one dumpPathMapper so collision detection spans every path; callers
56
+ // shaping a single source/config coordinate use this helper and receive the
57
+ // same filesystem-alias and cross-root behavior.
58
+ func WirePath(project, file string) (string, error) {
59
+ mapper := newDumpPathMapper(project)
60
+ wire := mapper.mapPath(file)
61
+ return wire, mapper.err()
62
+ }
63
+
64
+ // WireNodeID maps the filesystem-bearing portions of one internal node ID into
65
+ // the same portable vocabulary as NewDumpFacts. Resident stores use it when a
66
+ // wire-level external reference count must be reconciled with the immutable
67
+ // graph.Node cache, whose keys retain compiler-physical paths.
68
+ func WireNodeID(project, id string) (string, error) {
69
+ ids, err := WireNodeIDs(project, []string{id})
70
+ return ids[id], err
71
+ }
72
+
73
+ // WireNodeIDs maps a set of internal node IDs through one path mapper. Sharing
74
+ // the mapper preserves cross-ID collision detection and resolves each repeated
75
+ // filesystem alias only once per snapshot generation.
76
+ func WireNodeIDs(project string, ids []string) (map[string]string, error) {
77
+ mapper := newDumpPathMapper(project)
78
+ wireIDs := make(map[string]string, len(ids))
79
+ for _, id := range ids {
80
+ wire, err := wireNodeID(mapper, id)
81
+ if err != nil {
82
+ return nil, err
83
+ }
84
+ wireIDs[id] = wire
85
+ }
86
+ return wireIDs, mapper.err()
87
+ }
88
+
89
+ func wireNodeID(mapper *dumpPathMapper, id string) (string, error) {
90
+ parts, ok := parseNodeID(id)
91
+ if !ok {
92
+ return "", fmt.Errorf("ttscgraph: invalid internal graph node id %q", id)
93
+ }
94
+ name := parts.name
95
+ if parts.kind == NodeModule {
96
+ name = mapper.mapPath(name)
97
+ }
98
+ wire := nodeID(mapper.mapPath(parts.path), name, parts.kind)
99
+ return wire, nil
100
+ }
101
+
37
102
  // mapPath returns one portable, slash-normalized coordinate:
38
103
  //
39
104
  // - project files stay project-relative;
@@ -49,26 +114,30 @@ func (m *dumpPathMapper) mapPath(file string) string {
49
114
  if file == "" {
50
115
  return ""
51
116
  }
52
- normalized := shimtspath.NormalizeSlashes(file)
117
+ normalized := shimtspath.NormalizePath(shimtspath.NormalizeSlashes(file))
53
118
  if strings.HasPrefix(normalized, "bundled:///") {
54
119
  return m.claim(normalized, normalized)
55
120
  }
56
- normalized = shimtspath.NormalizePath(normalized)
57
121
  if m.project == "" || shimtspath.GetRootLength(m.project) == 0 {
58
122
  return normalized
59
123
  }
60
124
 
61
- physical := normalized
62
- if shimtspath.GetRootLength(physical) == 0 {
63
- physical = shimtspath.GetNormalizedAbsolutePath(physical, m.project)
125
+ rawPhysical := normalized
126
+ if shimtspath.GetRootLength(rawPhysical) == 0 {
127
+ rawPhysical = shimtspath.GetNormalizedAbsolutePath(rawPhysical, m.rawProject)
64
128
  }
129
+ rawKey := m.pathKey(rawPhysical)
130
+ if wire, ok := m.rawToWire[rawKey]; ok {
131
+ return wire
132
+ }
133
+ physical := m.canonicalize(rawPhysical)
65
134
  if !dumpPathRootsEqual(m.project, physical, m.caseSensitive) {
66
135
  m.fail(fmt.Errorf(
67
136
  "ttscgraph: source path %q cannot be represented relative to project %q because they are on different filesystem roots",
68
- normalized,
137
+ rawPhysical,
69
138
  m.project,
70
139
  ))
71
- return normalized
140
+ return rawPhysical
72
141
  }
73
142
  options := shimtspath.ComparePathsOptions{
74
143
  CurrentDirectory: m.project,
@@ -78,12 +147,21 @@ func (m *dumpPathMapper) mapPath(file string) string {
78
147
  if shimtspath.GetRootLength(wire) != 0 {
79
148
  m.fail(fmt.Errorf(
80
149
  "ttscgraph: source path %q cannot be represented relative to project %q because they are on different filesystem roots",
81
- normalized,
150
+ rawPhysical,
82
151
  m.project,
83
152
  ))
84
- return normalized
153
+ return rawPhysical
85
154
  }
86
- return m.claim(physical, wire)
155
+ wire = m.claim(physical, wire)
156
+ m.rawToWire[rawKey] = wire
157
+ return wire
158
+ }
159
+
160
+ func (m *dumpPathMapper) pathKey(path string) string {
161
+ if !m.caseSensitive {
162
+ return strings.ToLower(path)
163
+ }
164
+ return path
87
165
  }
88
166
 
89
167
  // claim records both directions of the projection. The reverse map is the
@@ -127,6 +205,51 @@ func (m *dumpPathMapper) fail(err error) {
127
205
 
128
206
  func (m *dumpPathMapper) err() error { return m.mappingErr }
129
207
 
208
+ // canonicalDumpPath collapses filesystem aliases for an existing path before
209
+ // it enters the wire-coordinate mapper. TypeScript reports physical source
210
+ // names, while a caller may select the same project through a symlink or a
211
+ // Windows 8.3 spelling; comparing those raw strings would leak a producer-local
212
+ // absolute path into an otherwise portable snapshot.
213
+ //
214
+ // Synthetic paths in mapper unit tests and missing paths retain their lexical
215
+ // spelling. Real graph inputs exist by construction, so the best-effort branch
216
+ // covers their physical identity without weakening the mapper's explicit
217
+ // cross-root and collision checks.
218
+ func canonicalDumpPath(location string) string {
219
+ normalized := shimtspath.NormalizePath(shimtspath.NormalizeSlashes(location))
220
+ if normalized == "" || shimtspath.GetRootLength(normalized) == 0 {
221
+ return normalized
222
+ }
223
+ if !dumpPathUsesHostFilesystem(normalized) {
224
+ return normalized
225
+ }
226
+ candidate := filepath.Clean(filepath.FromSlash(normalized))
227
+ suffix := []string{}
228
+ for {
229
+ physical, err := filepath.EvalSymlinks(candidate)
230
+ if err == nil {
231
+ for index := len(suffix) - 1; index >= 0; index-- {
232
+ physical = filepath.Join(physical, suffix[index])
233
+ }
234
+ return shimtspath.NormalizePath(shimtspath.NormalizeSlashes(physical))
235
+ }
236
+ parent := filepath.Dir(candidate)
237
+ if parent == candidate {
238
+ break
239
+ }
240
+ suffix = append(suffix, filepath.Base(candidate))
241
+ candidate = parent
242
+ }
243
+ return normalized
244
+ }
245
+
246
+ func dumpPathUsesHostFilesystem(path string) bool {
247
+ if runtime.GOOS == "windows" {
248
+ return strings.HasPrefix(path, "//") || (len(path) >= 2 && path[1] == ':')
249
+ }
250
+ return strings.HasPrefix(path, "/") && !strings.HasPrefix(path, "//")
251
+ }
252
+
130
253
  // Windows drive and UNC roots use case-insensitive path comparison. POSIX
131
254
  // roots remain case-sensitive. This decision follows the path's own grammar so
132
255
  // synthetic Windows/UNC fixtures behave the same on every CI host.
@@ -0,0 +1,39 @@
1
+ //go:build windows
2
+
3
+ package graph
4
+
5
+ import (
6
+ "path/filepath"
7
+ "testing"
8
+ )
9
+
10
+ // TestDumpPathMapperCanonicalizesWindowsShortRoot verifies that a project
11
+ // selected through an 8.3 path still owns checker sources reported through the
12
+ // expanded physical spelling.
13
+ //
14
+ // 1. Obtain the environment's temp spelling and its physical spelling.
15
+ // 2. Skip hosts where Windows exposes only one spelling.
16
+ // 3. Require a physical child source to retain a project-relative identity.
17
+ func TestDumpPathMapperCanonicalizesWindowsShortRoot(t *testing.T) {
18
+ project := t.TempDir()
19
+ physical, err := filepath.EvalSymlinks(project)
20
+ if err != nil {
21
+ t.Fatal(err)
22
+ }
23
+ if filepath.Clean(project) == filepath.Clean(physical) {
24
+ t.Skip("temporary directory has no alternate Windows path spelling")
25
+ }
26
+
27
+ mapper := newDumpPathMapper(project)
28
+ source := filepath.Join(physical, "src", "main.ts")
29
+ if got := mapper.mapPath(source); got != "src/main.ts" {
30
+ t.Fatalf("mapPath(%q) = %q, want project-relative identity", source, got)
31
+ }
32
+ missing := filepath.Join(project, "future", "main.ts")
33
+ if got := mapper.mapPath(missing); got != "future/main.ts" {
34
+ t.Fatalf("mapPath(%q) = %q, want stable missing-root identity", missing, got)
35
+ }
36
+ if err := mapper.err(); err != nil {
37
+ t.Fatal(err)
38
+ }
39
+ }
@@ -0,0 +1,32 @@
1
+ package graph
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ // TestNewDumpRejectsProvenanceOnlyPathErrors verifies paths introduced after
9
+ // fact projection still pass through the dump context's fail-closed boundary.
10
+ //
11
+ // Provenance inputs are mapped after graph facts. Without a final path-error
12
+ // check, a cross-drive config could survive into otherwise valid JSON.
13
+ //
14
+ // 1. Build an empty graph under one synthetic Windows drive.
15
+ // 2. Add a provenance-only config input from another drive.
16
+ // 3. Require NewDump to reject the unrepresentable coordinate.
17
+ func TestNewDumpRejectsProvenanceOnlyPathErrors(t *testing.T) {
18
+ _, err := NewDump(
19
+ &Graph{Nodes: map[string]*Node{}},
20
+ "C:/checkout/app",
21
+ "tsconfig.json",
22
+ nil,
23
+ nil,
24
+ DumpOrigin{Provenance: Provenance{Universe: Universe{Configs: []FileDigest{{
25
+ File: "D:/shared/tsconfig.json",
26
+ Digest: "config-digest",
27
+ }}}}},
28
+ )
29
+ if err == nil || !strings.Contains(err.Error(), "different filesystem roots") {
30
+ t.Fatalf("provenance-only cross-root error = %v, want rejection before serialization", err)
31
+ }
32
+ }
@@ -55,8 +55,8 @@ export function main(): void {
55
55
  if err := json.Unmarshal(data, &dump); err != nil {
56
56
  t.Fatalf("dump is not valid JSON: %v\n%s", err, data)
57
57
  }
58
- if dump.Project != root || dump.Tsconfig != "tsconfig.json" {
59
- t.Fatalf("project/tsconfig not echoed: %q / %q", dump.Project, dump.Tsconfig)
58
+ if dump.Project != canonicalDumpPath(root) || dump.Tsconfig != "tsconfig.json" {
59
+ t.Fatalf("project/tsconfig coordinates are not canonical: %q / %q", dump.Project, dump.Tsconfig)
60
60
  }
61
61
  if len(dump.Nodes) != len(g.Nodes) {
62
62
  t.Fatalf("dumped %d nodes, graph has %d", len(dump.Nodes), len(g.Nodes))
@@ -0,0 +1,82 @@
1
+ package graph
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "testing"
7
+ )
8
+
9
+ // TestDumpSymlinkProjectPublishesMatchingBase verifies relative wire paths and
10
+ // the published project coordinate share one canonical filesystem base.
11
+ //
12
+ // A raw symlink cwd combined with canonical source paths makes `../` siblings,
13
+ // missing literal roots and a symlinked relative tsconfig resolve incorrectly
14
+ // unless project publication and path mapping use the same physical root.
15
+ //
16
+ // 1. Select a real project directory through a symlink and add sibling inputs.
17
+ // 2. Map an existing sibling, a missing child and a relative config symlink.
18
+ // 3. Require stable coordinates and a canonical published project base.
19
+ func TestDumpSymlinkProjectPublishesMatchingBase(t *testing.T) {
20
+ root := t.TempDir()
21
+ realRoot := filepath.Join(root, "real")
22
+ project := filepath.Join(realRoot, "pkg")
23
+ configDir := filepath.Join(realRoot, "config")
24
+ if err := os.MkdirAll(project, 0o755); err != nil {
25
+ t.Fatal(err)
26
+ }
27
+ if err := os.MkdirAll(configDir, 0o755); err != nil {
28
+ t.Fatal(err)
29
+ }
30
+ shared := filepath.Join(realRoot, "shared.ts")
31
+ config := filepath.Join(configDir, "tsconfig.json")
32
+ if err := os.WriteFile(shared, []byte("export const shared = true;\n"), 0o644); err != nil {
33
+ t.Fatal(err)
34
+ }
35
+ if err := os.WriteFile(config, []byte("{}\n"), 0o644); err != nil {
36
+ t.Fatal(err)
37
+ }
38
+ alias := filepath.Join(root, "project-link")
39
+ if err := os.Symlink(project, alias); err != nil {
40
+ t.Skipf("host cannot create directory symlink: %v", err)
41
+ }
42
+ if err := os.Symlink(config, filepath.Join(project, "tsconfig.json")); err != nil {
43
+ t.Skipf("host cannot create file symlink: %v", err)
44
+ }
45
+
46
+ mapper := newDumpPathMapper(alias)
47
+ if wire := mapper.mapPath(shared); wire != "../shared.ts" {
48
+ t.Fatalf("sibling wire path = %q, want ../shared.ts", wire)
49
+ }
50
+ if wire := mapper.mapPath(filepath.Join(alias, "src", "new.ts")); wire != "src/new.ts" {
51
+ t.Fatalf("missing-root wire path = %q, want src/new.ts", wire)
52
+ }
53
+ if wire := mapper.mapPath("tsconfig.json"); wire != "../config/tsconfig.json" {
54
+ t.Fatalf("relative config wire path = %q, want ../config/tsconfig.json", wire)
55
+ }
56
+ dump, err := NewDump(&Graph{Nodes: map[string]*Node{}}, alias, "tsconfig.json", nil, nil, DumpOrigin{})
57
+ if err != nil {
58
+ t.Fatal(err)
59
+ }
60
+ physicalProject, err := filepath.EvalSymlinks(alias)
61
+ if err != nil {
62
+ t.Fatal(err)
63
+ }
64
+ if filepath.Clean(filepath.FromSlash(dump.Project)) != filepath.Clean(physicalProject) {
65
+ t.Fatalf("published project = %q, want %q", dump.Project, physicalProject)
66
+ }
67
+ resolvedSibling := filepath.Clean(filepath.Join(filepath.FromSlash(dump.Project), filepath.FromSlash("../shared.ts")))
68
+ resolvedSiblingInfo, err := os.Stat(resolvedSibling)
69
+ if err != nil {
70
+ t.Fatal(err)
71
+ }
72
+ sharedInfo, err := os.Stat(shared)
73
+ if err != nil {
74
+ t.Fatal(err)
75
+ }
76
+ if !os.SameFile(resolvedSiblingInfo, sharedInfo) {
77
+ t.Fatalf("published base resolves sibling to %q, want %q", resolvedSibling, shared)
78
+ }
79
+ if dump.Tsconfig != "../config/tsconfig.json" {
80
+ t.Fatalf("published tsconfig = %q, want ../config/tsconfig.json", dump.Tsconfig)
81
+ }
82
+ }
@@ -18,9 +18,15 @@ import (
18
18
  // it annotates, a literal value set on the type that admits it. Both need the
19
19
  // same checker and the same walk, so they are resolved here and written to the
20
20
  // node the build pass keyed.
21
- func (g *Graph) addEdges(prog *driver.Program) {
21
+ func (g *Graph) addEdges(prog *driver.Program, selected map[string]bool, partial bool) {
22
22
  checker := prog.Checker
23
23
  for _, file := range prog.SourceFiles() {
24
+ if !IsWorkspaceSourceFile(file) {
25
+ continue
26
+ }
27
+ if partial && !selected[file.FileName()] {
28
+ continue
29
+ }
24
30
  g.markExports(checker, file)
25
31
  g.collectHeritage(checker, file)
26
32
  g.collectCalls(checker, file)
@@ -54,7 +60,8 @@ func (g *Graph) addEdgeAt(from, to string, kind EdgeKind, origin string, pos, en
54
60
  return
55
61
  }
56
62
  g.seen[key] = struct{}{}
57
- g.Edges = append(g.Edges, &Edge{From: from, To: to, Kind: kind, Origin: origin, Pos: pos, End: end})
63
+ file := g.edgeEvidenceFiles[from]
64
+ g.Edges = append(g.Edges, &Edge{From: from, To: to, Kind: kind, Origin: origin, File: file, Pos: pos, End: end})
58
65
  }
59
66
 
60
67
  // collectHeritage adds a heritage edge for every base of every class and
@@ -320,7 +327,7 @@ func graphMemberNodeID(g *Graph, member *shimast.Node) string {
320
327
  return ""
321
328
  }
322
329
  id := nodeID(file.FileName(), name, kind)
323
- stored, ok := g.Nodes[id]
330
+ stored, ok := g.lookupNode(id)
324
331
  if !ok {
325
332
  return ""
326
333
  }
@@ -576,8 +583,10 @@ func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shi
576
583
  }
577
584
  case shimast.KindBinaryExpression:
578
585
  if target := g.assignedFunctionTarget(checker, from, child); target != "" {
579
- g.recordImplementation(target, child)
580
- g.callsWithin(checker, target, child.AsBinaryExpression().Right)
586
+ file := g.recordImplementation(target, child)
587
+ g.withEdgeEvidenceFile(target, file, func() {
588
+ g.callsWithin(checker, target, child.AsBinaryExpression().Right)
589
+ })
581
590
  }
582
591
  case shimast.KindJsxSelfClosingElement:
583
592
  // `<Component />` is a use of the component; an intrinsic tag (`<div />`)
@@ -595,6 +604,20 @@ func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shi
595
604
  })
596
605
  }
597
606
 
607
+ // assignedFunctionTarget returns the graph node an `x.y = function` assignment
608
+ // implements, or "" when the assignment implements no node this graph owns.
609
+ //
610
+ // A declaration outside the workspace is not such a node. `globalThis.assert =
611
+ // …` against a `declare global { function assert(…) }`, or a patched member of
612
+ // a dependency's `@types`, resolves to a symbol whose only declaration is in a
613
+ // declaration file — which has no body, holds no facts, and enters the graph as
614
+ // an external boundary leaf. Re-attributing the assigned body to it moved the
615
+ // implementation's calls and type references onto a node that never ran them,
616
+ // under offsets read against the wrong file: `recordImplementation` already
617
+ // refuses an external node, so the evidence file stayed unset and the spans
618
+ // pointed at neither the declaration nor the implementation. And the edge
619
+ // itself was unassemblable, so every request against such a project failed at
620
+ // shard partition.
598
621
  func (g *Graph) assignedFunctionTarget(checker *shimchecker.Checker, from string, node *shimast.Node) string {
599
622
  binary := node.AsBinaryExpression()
600
623
  if binary == nil ||
@@ -605,7 +628,7 @@ func (g *Graph) assignedFunctionTarget(checker *shimchecker.Checker, from string
605
628
  return ""
606
629
  }
607
630
  target := g.resolve(checker, binary.Left)
608
- if target == nil || target.Symbol == nil {
631
+ if target == nil || target.Symbol == nil || target.External {
609
632
  return ""
610
633
  }
611
634
  to := g.ensureTargetNode(target)
@@ -615,18 +638,45 @@ func (g *Graph) assignedFunctionTarget(checker *shimchecker.Checker, from string
615
638
  return to
616
639
  }
617
640
 
618
- func (g *Graph) recordImplementation(id string, assignment *shimast.Node) {
619
- node := g.Nodes[id]
620
- if node == nil || node.External || node.ImplementationFile != "" {
621
- return
641
+ func (g *Graph) recordImplementation(id string, assignment *shimast.Node) string {
642
+ node, ok := g.lookupNode(id)
643
+ if !ok || node.External {
644
+ return ""
622
645
  }
623
646
  file := shimast.GetSourceFileOfNode(assignment)
624
647
  if file == nil {
648
+ return ""
649
+ }
650
+ sources := g.ImplementationSources[id]
651
+ if sources == nil {
652
+ sources = map[string]bool{}
653
+ g.ImplementationSources[id] = sources
654
+ }
655
+ sources[file.FileName()] = true
656
+ current := g.Nodes[id]
657
+ if current != nil && current.ImplementationFile == "" {
658
+ current.ImplementationFile = file.FileName()
659
+ current.ImplementationPos = assignment.Pos()
660
+ current.ImplementationEnd = assignment.End()
661
+ }
662
+ return file.FileName()
663
+ }
664
+
665
+ func (g *Graph) withEdgeEvidenceFile(from, file string, visit func()) {
666
+ if file == "" {
667
+ visit()
625
668
  return
626
669
  }
627
- node.ImplementationFile = file.FileName()
628
- node.ImplementationPos = assignment.Pos()
629
- node.ImplementationEnd = assignment.End()
670
+ previous, existed := g.edgeEvidenceFiles[from]
671
+ g.edgeEvidenceFiles[from] = file
672
+ defer func() {
673
+ if existed {
674
+ g.edgeEvidenceFiles[from] = previous
675
+ } else {
676
+ delete(g.edgeEvidenceFiles, from)
677
+ }
678
+ }()
679
+ visit()
630
680
  }
631
681
 
632
682
  func isInvokedAccess(access *shimast.Node) bool {
@@ -745,7 +795,13 @@ func (g *Graph) typeRefsWithin(checker *shimchecker.Checker, from string, node *
745
795
  }
746
796
  case shimast.KindBinaryExpression:
747
797
  if target := g.assignedFunctionTarget(checker, from, child); target != "" {
748
- g.typeRefsWithin(checker, target, child.AsBinaryExpression().Right)
798
+ file := ""
799
+ if source := shimast.GetSourceFileOfNode(child); source != nil {
800
+ file = source.FileName()
801
+ }
802
+ g.withEdgeEvidenceFile(target, file, func() {
803
+ g.typeRefsWithin(checker, target, child.AsBinaryExpression().Right)
804
+ })
749
805
  }
750
806
  }
751
807
  g.typeRefsWithin(checker, from, child)
@@ -795,7 +851,7 @@ func (g *Graph) ensureTargetNode(target *Target) string {
795
851
  return ""
796
852
  }
797
853
  id := nodeID(target.File, name, NodeMethod)
798
- if _, exists := g.Nodes[id]; exists {
854
+ if _, exists := g.lookupNode(id); exists {
799
855
  return id
800
856
  }
801
857
  return ""
@@ -805,7 +861,7 @@ func (g *Graph) ensureTargetNode(target *Target) string {
805
861
  }
806
862
  name := qualifiedName(target.Symbol)
807
863
  id := nodeID(target.File, name, kind)
808
- if _, exists := g.Nodes[id]; exists {
864
+ if _, exists := g.lookupNode(id); exists {
809
865
  return id
810
866
  }
811
867
  if !target.External {
@@ -851,7 +907,7 @@ func (g *Graph) closureTargetID(target *Target) string {
851
907
  kind = NodeVariable
852
908
  }
853
909
  id := nodeID(target.File, name, kind)
854
- if _, exists := g.Nodes[id]; exists {
910
+ if _, exists := g.lookupNode(id); exists {
855
911
  return id
856
912
  }
857
913
  }
@@ -108,10 +108,17 @@ func (g *Graph) markExportedSymbol(symbol *shimast.Symbol) (string, bool) {
108
108
  if name == "" {
109
109
  return "", false
110
110
  }
111
- node, ok := g.Nodes[nodeID(file.FileName(), name, kind)]
111
+ node, ok := g.lookupNode(nodeID(file.FileName(), name, kind))
112
112
  if !ok {
113
113
  return "", false
114
114
  }
115
- node.Exported = true
115
+ g.ExportedTargets[node.ID] = true
116
+ // A complete build and an invalidation transaction that includes the
117
+ // declaration owner both hold a fresh node and may stamp it directly. Never
118
+ // mutate a base node: it belongs to the last committed generation until the
119
+ // caller has assembled and validated every replacement shard.
120
+ if current, exists := g.Nodes[node.ID]; exists {
121
+ current.Exported = true
122
+ }
116
123
  return node.ID, true
117
124
  }
@@ -171,9 +171,11 @@ const (
171
171
  )
172
172
 
173
173
  // Edge is a directed, checker-resolved relationship from one node to another,
174
- // both referenced by Node.ID. Pos and End bound the source expression in the
175
- // From node's file that produced the edge. They are evidence, not identity; a
176
- // duplicate relationship keeps the first source-order span.
174
+ // both referenced by Node.ID. File, Pos, and End bound the source expression
175
+ // that produced the edge. File is empty when the From node names it and is set
176
+ // only when an assigned implementation attributes an expression in another
177
+ // file to the declaration node it implements. They are evidence, not identity;
178
+ // a duplicate relationship keeps the first source-order span.
177
179
  type Edge struct {
178
180
  From string
179
181
  To string
@@ -186,6 +188,7 @@ type Edge struct {
186
188
  // "jsx", or "tagged"; for EdgeHeritage it is "extends" or "implements";
187
189
  // for EdgeMemberRelation it is "implements" or "overrides".
188
190
  Origin string
191
+ File string
189
192
  Pos int
190
193
  End int
191
194
  }
@@ -196,10 +199,10 @@ type Graph struct {
196
199
  Nodes map[string]*Node
197
200
  Edges []*Edge
198
201
  // Decorators holds the decorators written on the workspace's declarations,
199
- // captured syntactically so the JSON dump can emit `decorates` edges and a
200
- // consumer can interpret `@Controller`/`@Get` conventions without re-parsing
201
- // source. It is dump-only metadata, separate from Edges so the existing
202
- // checker-resolved relationships are untouched.
202
+ // captured syntactically so the JSON dump can attach raw facts to each target
203
+ // node and a consumer can interpret `@Controller`/`@Get` conventions without
204
+ // re-parsing source. It is dump-only metadata, separate from Edges so the
205
+ // existing checker-resolved relationships are untouched.
203
206
  Decorators []*Decorator
204
207
  // bodyNodes tracks whether a callable node's display span is the overload
205
208
  // implementation rather than an overload signature. It is build-only metadata
@@ -213,6 +216,43 @@ type Graph struct {
213
216
  // build: the edge pass visits the same node several times, and a resolution
214
217
  // cannot change while the program is fixed.
215
218
  resolved map[*shimast.Node]*Target
219
+
220
+ // edgeEvidenceFiles temporarily overrides a declaration node's source while
221
+ // an assigned implementation in another file is walked. Builds are
222
+ // single-threaded, and the helper that installs an override restores it
223
+ // before the traversal returns.
224
+ edgeEvidenceFiles map[string]string
225
+
226
+ // baseNodes is the immutable endpoint index of the preceding committed
227
+ // generation during a partial build. Nodes owned by selectedFiles are never
228
+ // read through it: a changed file may delete or re-key a declaration, so its
229
+ // old nodes must disappear before the new checker walk begins.
230
+ baseNodes map[string]*Node
231
+ selectedFiles map[string]bool
232
+
233
+ // ExportedTargets records every declaration reached through a selected
234
+ // module's checker export table. A partial caller uses it to add re-exported
235
+ // owner files to the same invalidation transaction before publishing.
236
+ ExportedTargets map[string]bool
237
+
238
+ // ImplementationSources records every source that assigns a function body
239
+ // to a modeled declaration, including candidates after the first assignment
240
+ // that supplied the node's display span. A partial caller uses the complete
241
+ // set to rebuild a declaration owner and all competing assignments together.
242
+ ImplementationSources map[string]map[string]bool
243
+ }
244
+
245
+ // lookupNode returns a node from this build, then from the preceding committed
246
+ // endpoint index when that node's file is not being replaced.
247
+ func (g *Graph) lookupNode(id string) (*Node, bool) {
248
+ if node, ok := g.Nodes[id]; ok {
249
+ return node, true
250
+ }
251
+ node, ok := g.baseNodes[id]
252
+ if !ok || g.selectedFiles[node.File] {
253
+ return nil, false
254
+ }
255
+ return node, true
216
256
  }
217
257
 
218
258
  // edgeKey identifies an edge by its two ends and the wire kind it will surface