ttsc 0.26.2 → 0.28.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 (170) hide show
  1. package/README.md +9 -0
  2. package/cmd/graphbench/main.go +26 -16
  3. package/cmd/graphdump/main.go +37 -12
  4. package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
  5. package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
  6. package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
  7. package/cmd/ttsc/api_transform.go +28 -3
  8. package/cmd/ttscgraph/auxiliary_identity_state_tracks_retargets_not_source_contents_test.go +85 -0
  9. package/cmd/ttscgraph/disk_realpath_other.go +16 -0
  10. package/cmd/ttscgraph/disk_realpath_windows.go +56 -0
  11. package/cmd/ttscgraph/dump.go +43 -11
  12. package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
  13. package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
  14. package/cmd/ttscgraph/serve.go +357 -60
  15. package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
  16. package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
  17. package/cmd/ttscgraph/serve_shards.go +31 -9
  18. package/driver/config_discovery.go +121 -0
  19. package/driver/emit_plugin.go +77 -11
  20. package/driver/graph.go +64 -11
  21. package/driver/input_observation.go +273 -0
  22. package/driver/input_observation_test.go +149 -0
  23. package/driver/plugins.go +506 -15
  24. package/driver/program.go +54 -1
  25. package/driver/resolution_candidates.go +13 -5
  26. package/driver/rewrite.go +45 -2
  27. package/driver/transform_dependencies.go +283 -0
  28. package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
  29. package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
  30. package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
  31. package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
  32. package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
  33. package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
  34. package/internal/graph/artifacts.go +230 -0
  35. package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
  36. package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
  37. package/internal/graph/build.go +35 -0
  38. package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
  39. package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
  40. package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
  41. package/internal/graph/doc_tags.go +416 -0
  42. package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
  43. package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
  44. package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
  45. package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
  46. package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
  47. package/internal/graph/dump.go +60 -13
  48. package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
  49. package/internal/graph/dump_paths.go +2 -2
  50. package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
  51. package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
  52. package/internal/graph/edges.go +1 -0
  53. package/internal/graph/graph.go +79 -4
  54. package/internal/graph/provenance.go +29 -2
  55. package/lib/TtscCompiler.d.ts +12 -11
  56. package/lib/TtscCompiler.js +44 -29
  57. package/lib/TtscCompiler.js.map +1 -1
  58. package/lib/compiler/internal/captureProcessOutput.js +2 -2
  59. package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
  60. package/lib/compiler/internal/compileProjectInMemory.js +4 -3
  61. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  62. package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +9 -0
  63. package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
  64. package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -1
  65. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -0
  66. package/lib/compiler/internal/resolveEmittedJavaScript.js +3 -0
  67. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  68. package/lib/compiler/internal/runBuild.d.ts +1 -1
  69. package/lib/compiler/internal/runBuild.js +80 -34
  70. package/lib/compiler/internal/runBuild.js.map +1 -1
  71. package/lib/compiler/internal/runSingleFileEmit.js +2 -2
  72. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  73. package/lib/compiler/internal/sharedHostHelpers.d.ts +28 -0
  74. package/lib/compiler/internal/sharedHostHelpers.js +39 -0
  75. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  76. package/lib/compiler/internal/startResidentTransform.js +12 -4
  77. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  78. package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -0
  79. package/lib/compiler/internal/transformProjectInMemory.js +239 -35
  80. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  81. package/lib/flags/schema.js +12 -5
  82. package/lib/flags/schema.js.map +1 -1
  83. package/lib/index.d.ts +1 -0
  84. package/lib/index.js +1 -0
  85. package/lib/index.js.map +1 -1
  86. package/lib/internal/createCanonicalTempDirectory.d.ts +17 -0
  87. package/lib/internal/createCanonicalTempDirectory.js +42 -0
  88. package/lib/internal/createCanonicalTempDirectory.js.map +1 -0
  89. package/lib/internal/projectInputPathIdentity.d.ts +3 -0
  90. package/lib/internal/projectInputPathIdentity.js +11 -5
  91. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  92. package/lib/internal/resolveNodeBinary.d.ts +14 -0
  93. package/lib/internal/resolveNodeBinary.js +150 -0
  94. package/lib/internal/resolveNodeBinary.js.map +1 -0
  95. package/lib/internal/resolveSafeCacheCleanupTargets.d.ts +15 -0
  96. package/lib/internal/resolveSafeCacheCleanupTargets.js +61 -0
  97. package/lib/internal/resolveSafeCacheCleanupTargets.js.map +1 -0
  98. package/lib/launcher/internal/prepareExecution.d.ts +3 -0
  99. package/lib/launcher/internal/prepareExecution.js +42 -14
  100. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  101. package/lib/launcher/internal/runTtsc.js +25 -13
  102. package/lib/launcher/internal/runTtsc.js.map +1 -1
  103. package/lib/launcher/internal/runTtscserver.js +18 -3
  104. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  105. package/lib/launcher/internal/runTtsx.js +2 -0
  106. package/lib/launcher/internal/runTtsx.js.map +1 -1
  107. package/lib/launcher/internal/runtimeHooks.d.ts +47 -3
  108. package/lib/launcher/internal/runtimeHooks.js +746 -64
  109. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  110. package/lib/plugin/internal/buildSourcePlugin.d.ts +62 -8
  111. package/lib/plugin/internal/buildSourcePlugin.js +754 -72
  112. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  113. package/lib/plugin/internal/capabilityResolutionCache.d.ts +76 -0
  114. package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
  115. package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
  116. package/lib/plugin/internal/loadProjectPlugins.d.ts +17 -5
  117. package/lib/plugin/internal/loadProjectPlugins.js +1339 -77
  118. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  119. package/lib/plugin/resolveCapabilityPlugins.d.ts +55 -0
  120. package/lib/plugin/resolveCapabilityPlugins.js +118 -0
  121. package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
  122. package/lib/register.d.ts +1 -0
  123. package/lib/register.js +46 -0
  124. package/lib/register.js.map +1 -0
  125. package/lib/structures/ITtscCompilerContext.d.ts +6 -3
  126. package/lib/structures/ITtscCompilerTransformation.d.ts +38 -0
  127. package/lib/structures/ITtscPlugin.d.ts +33 -0
  128. package/package.json +16 -9
  129. package/shim/ast/parent.go +16 -3
  130. package/shim/checker/enums_gen.go +11 -0
  131. package/shim/checker/go.mod +9 -0
  132. package/shim/checker/go.sum +22 -0
  133. package/shim/checker/shim.go +78 -0
  134. package/shim/compiler/emithook.go +10 -0
  135. package/shim/compiler/incremental.go +45 -8
  136. package/src/TtscCompiler.ts +55 -33
  137. package/src/compiler/internal/captureProcessOutput.ts +3 -2
  138. package/src/compiler/internal/compileProjectInMemory.ts +4 -3
  139. package/src/compiler/internal/project/createNativeProjectContextArgs.ts +19 -4
  140. package/src/compiler/internal/resolveEmittedJavaScript.ts +6 -0
  141. package/src/compiler/internal/runBuild.ts +102 -39
  142. package/src/compiler/internal/runSingleFileEmit.ts +2 -2
  143. package/src/compiler/internal/sharedHostHelpers.ts +44 -0
  144. package/src/compiler/internal/startResidentTransform.ts +12 -3
  145. package/src/compiler/internal/transformProjectInMemory.ts +410 -39
  146. package/src/flags/schema.ts +12 -5
  147. package/src/index.ts +1 -0
  148. package/src/internal/createCanonicalTempDirectory.ts +53 -0
  149. package/src/internal/projectInputPathIdentity.ts +17 -4
  150. package/src/internal/resolveNodeBinary.ts +173 -0
  151. package/src/internal/resolveSafeCacheCleanupTargets.ts +85 -0
  152. package/src/launcher/internal/prepareExecution.ts +50 -14
  153. package/src/launcher/internal/runTtsc.ts +24 -13
  154. package/src/launcher/internal/runTtscserver.ts +18 -3
  155. package/src/launcher/internal/runTtsx.ts +2 -0
  156. package/src/launcher/internal/runtimeHooks.ts +934 -68
  157. package/src/plugin/internal/buildSourcePlugin.ts +984 -79
  158. package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
  159. package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
  160. package/src/plugin/resolveCapabilityPlugins.ts +171 -0
  161. package/src/register.ts +47 -0
  162. package/src/structures/ITtscCompilerContext.ts +6 -3
  163. package/src/structures/ITtscCompilerTransformation.ts +46 -0
  164. package/src/structures/ITtscPlugin.ts +36 -0
  165. package/tools/shim_audit/baseline.json +0 -2
  166. package/utility/host.go +18 -3
  167. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +0 -5
  168. package/lib/internal/assertSafeExplicitCacheDirectory.js +0 -26
  169. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +0 -1
  170. package/src/internal/assertSafeExplicitCacheDirectory.ts +0 -29
@@ -57,27 +57,82 @@ const (
57
57
  )
58
58
 
59
59
  // fullSnapshotCapabilities is what a snapshot from a resident compiler session
60
- // proves. Both commands that own a real Program declare all three; the constant
61
- // exists so the envelope's claim and the dump's claim cannot drift apart.
60
+ // proves. Both commands that own a real Program declare every one of them; the
61
+ // constant exists so the envelope's claim and the dump's claim cannot drift
62
+ // apart.
62
63
  var fullSnapshotCapabilities = []string{
63
64
  graph.CapabilityUniverse,
64
65
  graph.CapabilitySourceDigests,
65
66
  graph.CapabilityDiskDigests,
66
67
  graph.CapabilityDiagnostics,
68
+ graph.CapabilityDocTags,
67
69
  }
68
70
 
69
- // serveCapabilities is what this server can prove, answered before a consumer
70
- // has a dump to inspect — an `unchanged` response carries no dump, and a client
71
- // negotiating on the first frame has not parsed one yet. It mirrors what every
72
- // dump this server publishes declares for itself.
71
+ // serveCapabilities is what this server can prove before a session exists —
72
+ // the frame a client reads when the project could not even be loaded. Once a
73
+ // session exists its own capabilities answer, because those depend on what a
74
+ // plugin published and this list cannot know it.
73
75
  var serveCapabilities = fullSnapshotCapabilities
74
76
 
77
+ // capabilities is what THIS session proves, which is the shared list plus the
78
+ // artifact claim when a plugin published one.
79
+ //
80
+ // The claim has to be conditional for the same reason it is on the dump command:
81
+ // a project that publishes no artifacts and a session that was never handed any
82
+ // are the same absent nodes, and only the claim separates them.
83
+ func (s *graphSession) capabilities() []string {
84
+ if len(s.artifacts) == 0 {
85
+ return fullSnapshotCapabilities
86
+ }
87
+ return append(
88
+ append([]string{}, fullSnapshotCapabilities...),
89
+ graph.CapabilityArtifactNodes,
90
+ )
91
+ }
92
+
93
+ // artifactProducer names the second producer behind this session's artifact
94
+ // nodes, and is nil when it has none.
95
+ // provenance is this session's published origin for one generation.
96
+ func (s *graphSession) provenance(texts map[string]string) graph.Provenance {
97
+ built := graph.NewProvenance(
98
+ serveProducer(),
99
+ s.capabilities(),
100
+ s.configDigests,
101
+ s.roots,
102
+ texts,
103
+ s.diskDigests,
104
+ )
105
+ built.ArtifactProducer = s.artifactProducer()
106
+ return built
107
+ }
108
+
109
+ func (s *graphSession) artifactProducer() *graph.Producer {
110
+ if len(s.artifacts) == 0 {
111
+ return nil
112
+ }
113
+ return &graph.Producer{Tool: "@ttsc/lint graph-nodes"}
114
+ }
115
+
75
116
  type serveRequest struct {
76
117
  ID int `json:"id"`
77
118
  // GraphSnapshotVersion opts into the incremental shard protocol. Omitted
78
- // requests retain the schema-v6 full-dump response for existing
79
- // @ttsc/graph clients.
119
+ // requests retain the full-dump response for existing @ttsc/graph clients.
80
120
  GraphSnapshotVersion int `json:"graphSnapshotVersion,omitempty"`
121
+ // Artifacts is the path to the set a plugin published for this request, which
122
+ // the client re-derives when the documents or lint configuration behind it
123
+ // moved. It rides every request rather than only the changed ones: the client
124
+ // is the only side that can see those inputs, and the server is the only side
125
+ // that knows what it last applied, so the honest exchange is the client
126
+ // stating what it currently has and the server comparing it with its own.
127
+ //
128
+ // Three states, which is why it is a pointer. Absent is a client with no
129
+ // opinion — one predating this field, or one driving a session through the
130
+ // startup flag — and the set already applied stands. Empty is a client
131
+ // stating it now has none, which withdraws that set: a project whose
132
+ // publisher was removed must stop being answered with its artifacts, and
133
+ // without a way to say so it would be answered with them until the editor
134
+ // restarted. A path is the set to apply.
135
+ Artifacts *string `json:"artifacts,omitempty"`
81
136
  }
82
137
 
83
138
  type serveResponse struct {
@@ -119,8 +174,30 @@ func errorResponse(id int, message string) serveResponse {
119
174
  }
120
175
 
121
176
  type graphSession struct {
122
- cwd string
123
- tsconfig string
177
+ cwd string
178
+ tsconfig string
179
+ // artifacts is the set a plugin published: a second producer's facts about
180
+ // documents this Program never read. Refreshing it is therefore not a refresh
181
+ // of the graph — editing a Markdown heading moves the artifact and not one
182
+ // compiler fact — so the two invalidations stay separate questions, and this
183
+ // one is answered by adoptArtifacts rather than by the build universe.
184
+ artifacts []graph.Artifact
185
+ // artifactsDigest is the content of the set currently applied. Content, and
186
+ // not the path or its modification time: the client overwrites one file per
187
+ // process and project, so the path never moves for a given session, and a
188
+ // republish triggered by a document whose headings did not actually change
189
+ // writes the same bytes and must therefore cost nothing.
190
+ artifactsDigest [sha256.Size]byte
191
+ // artifactsFile and artifactsStat are what the digest was taken from, kept so
192
+ // an unchanged file need not be read at all. The set is one entry per
193
+ // document section, model field, and operation, so it is bounded by the
194
+ // project's documentation rather than by anything small — and this comparison
195
+ // runs before every graph request, where reading and hashing that whole file
196
+ // to learn it did not move is the kind of cost a resident session pays
197
+ // forever.
198
+ artifactsFile string
199
+ artifactsStat artifactsFileState
200
+
124
201
  compiler *driver.Session
125
202
  configHashes map[string][sha256.Size]byte
126
203
  auxStates map[string]diskState
@@ -140,21 +217,134 @@ type graphSession struct {
140
217
  graphStore *serveGraphStore
141
218
  requestProtocol int
142
219
  requestProtocolSelected bool
143
- // pending remembers a generation whose state was captured but whose selected
144
- // projection failed. Until an input change lets the session rebuild, an
145
- // unchanged request retries the same full or partial invalidation instead of
146
- // falsely confirming the older client graph.
220
+ // pending remembers work the next request owes regardless of what the build
221
+ // universe says: a generation whose state was captured but whose selected
222
+ // projection failed, or an artifact set adopted after the graph carrying the
223
+ // previous one was built. Until it is discharged, an otherwise unchanged
224
+ // request retries the recorded invalidation instead of falsely confirming the
225
+ // older client graph.
147
226
  pending *graphChange
148
227
  }
149
228
 
150
229
  func newGraphSession(cwd, tsconfig string) (*graphSession, error) {
151
- session := &graphSession{cwd: cwd, tsconfig: tsconfig}
230
+ return newGraphSessionWithArtifacts(cwd, tsconfig, nil)
231
+ }
232
+
233
+ // newGraphSessionWithArtifacts is the constructor the serve command uses, and
234
+ // the plain one above is it with nothing published. The artifacts are read once
235
+ // and held for the session: they are a second producer's facts about documents
236
+ // this Program never read, so refreshing them is a different question from
237
+ // refreshing the graph, and only the first answer is wired today.
238
+ func newGraphSessionWithArtifacts(
239
+ cwd, tsconfig string,
240
+ artifacts []graph.Artifact,
241
+ ) (*graphSession, error) {
242
+ session := &graphSession{cwd: cwd, tsconfig: tsconfig, artifacts: artifacts}
152
243
  if err := session.reload(); err != nil {
153
244
  return nil, err
154
245
  }
155
246
  return session, nil
156
247
  }
157
248
 
249
+ // adoptArtifacts makes what the request states the set this session applies,
250
+ // and reports whether that replaced a set some already-built graph carries.
251
+ //
252
+ // A nil statement is a client with no opinion, and the set already applied
253
+ // stands: dropping it on the say-so of a client that never mentioned artifacts
254
+ // would delete them from the graph of every session driven through the startup
255
+ // flag alone.
256
+ //
257
+ // A read failure is returned rather than swallowed. The client wrote this file
258
+ // moments ago and named it in the same request, so a file that cannot be read
259
+ // is a broken exchange, not a project without artifacts — and answering with a
260
+ // silently artifact-free graph would look exactly like a correct answer.
261
+ func (s *graphSession) adoptArtifacts(named *string) (bool, error) {
262
+ if named == nil {
263
+ return false, nil
264
+ }
265
+ path := strings.TrimSpace(*named)
266
+ if path == "" {
267
+ return s.applyArtifacts(nil, withdrawnArtifactsDigest), nil
268
+ }
269
+ stat, err := os.Stat(path)
270
+ if err != nil {
271
+ return false, err
272
+ }
273
+ state := artifactsFileState{size: stat.Size(), modTime: stat.ModTime().UnixNano()}
274
+ if path == s.artifactsFile && state == s.artifactsStat {
275
+ return false, nil
276
+ }
277
+ // One read, hashed and decoded from the same bytes. Reading twice would let
278
+ // an overwrite land between them and leave this session recording an identity
279
+ // for a set it is not holding — after which a later request naming the file
280
+ // it recorded would be answered as unchanged.
281
+ contents, err := os.ReadFile(path)
282
+ if err != nil {
283
+ return false, err
284
+ }
285
+ next, err := graph.ParseArtifacts(contents)
286
+ if err != nil {
287
+ return false, err
288
+ }
289
+ s.artifactsFile = path
290
+ s.artifactsStat = state
291
+ return s.applyArtifacts(next, sha256.Sum256(contents)), nil
292
+ }
293
+
294
+ // artifactsFileState is what an unchanged published file looks like from the
295
+ // outside: size and modification time, never contents. It exists only to decide
296
+ // whether the file is worth reading, and the digest taken from those contents is
297
+ // still what decides whether the set is worth applying.
298
+ type artifactsFileState struct {
299
+ size int64
300
+ // modTime is nanoseconds since the epoch rather than a time.Time, so the
301
+ // struct compares with == and means it. A time.Time carries a location
302
+ // pointer and an optional monotonic reading, and comparing two of those with
303
+ // == is a well-known way to get a false inequality that reads as a changed
304
+ // file forever.
305
+ modTime int64
306
+ }
307
+
308
+ // applyArtifacts installs a set and reports whether it differs from the applied
309
+ // one. The first application of a session reports a change like any other and
310
+ // costs nothing: no graph has been projected yet, so the invalidation it
311
+ // records is discharged by the initial projection that was going to happen
312
+ // regardless.
313
+ func (s *graphSession) applyArtifacts(next []graph.Artifact, digest [sha256.Size]byte) bool {
314
+ if digest == s.artifactsDigest {
315
+ return false
316
+ }
317
+ s.artifacts = next
318
+ s.artifactsDigest = digest
319
+ return true
320
+ }
321
+
322
+ // withdrawnArtifactsDigest stands for "the client states it has none".
323
+ //
324
+ // A constant rather than the hash of an empty file, so that the withdrawn state
325
+ // and a file whose contents happen to hash to the same value cannot be confused
326
+ // — and domain-separated so it is not the digest of anything a producer writes.
327
+ var withdrawnArtifactsDigest = sha256.Sum256([]byte("ttscgraph:artifacts:none"))
328
+
329
+ // invalidateArtifacts records that the next projection must be a full one.
330
+ //
331
+ // Full, because replacing the published set is not a per-file delta: it moves
332
+ // the artifact nodes and every citation edge into them at once, and the
333
+ // incremental path is built to answer a different question — which sources
334
+ // changed. It happens to reach the same answer here, through the closure
335
+ // expansion that pulls a citing source back in, but that is a property of
336
+ // machinery aimed elsewhere and not a guarantee this depends on.
337
+ //
338
+ // Full, but not a reload: the Program is reused untouched, because no compiler
339
+ // input moved. That is the whole point of keeping documents out of the build
340
+ // universe — a Markdown edit costs one projection, never one typecheck.
341
+ func (s *graphSession) invalidateArtifacts() {
342
+ if s.pending != nil && s.pending.full {
343
+ return
344
+ }
345
+ s.pending = &graphChange{mode: serveModeRebuild, full: true}
346
+ }
347
+
158
348
  func (s *graphSession) Close() error {
159
349
  if s.compiler == nil {
160
350
  return nil
@@ -385,9 +575,11 @@ func (s *graphSession) captureState() error {
385
575
  return err
386
576
  }
387
577
  inputs := auxiliaryInputs(program, configs, s.cwd)
388
- inputs = append(inputs, missingRootInputs(configs, sourceHashes)...)
578
+ for _, path := range missingRootInputs(configs, sourceHashes) {
579
+ inputs = append(inputs, auxiliaryInput{path: path})
580
+ }
389
581
  s.configHashes = configHashes
390
- s.auxStates = captureDiskStates(compactSortedStrings(inputs))
582
+ s.auxStates = captureDiskStates(compactAuxiliaryInputs(inputs))
391
583
  s.sourceHashes = sourceHashes
392
584
  s.diskDigests = diskDigests
393
585
  s.rootFiles = projectRootFilesFromConfigs(configs, false)
@@ -439,6 +631,7 @@ func missingRootInputs(configs []*shimtsoptions.ParsedCommandLine, sourceHashes
439
631
  func (s *graphSession) buildDump() (graph.Dump, error) {
440
632
  program := s.compiler.Program()
441
633
  built := graph.Build(program)
634
+ graph.ApplyArtifacts(built, s.artifacts)
442
635
  // One texts map feeds both the spans and the manifest digests, so the bytes a
443
636
  // span points into are provably the bytes the manifest attests to.
444
637
  texts := graph.SourceTexts(program)
@@ -449,14 +642,7 @@ func (s *graphSession) buildDump() (graph.Dump, error) {
449
642
  graph.GitIgnoredFiles(s.cwd, built),
450
643
  texts,
451
644
  graph.DumpOrigin{
452
- Provenance: graph.NewProvenance(
453
- serveProducer(),
454
- fullSnapshotCapabilities,
455
- s.configDigests,
456
- s.roots,
457
- texts,
458
- s.diskDigests,
459
- ),
645
+ Provenance: s.provenance(texts),
460
646
  Diagnostics: graph.NewDiagnostics(program),
461
647
  },
462
648
  )
@@ -661,8 +847,19 @@ func changedSources(previous map[string][sha256.Size]byte) (map[string]string, b
661
847
  }
662
848
 
663
849
  type diskState struct {
664
- Hash [sha256.Size]byte
665
- Exists bool
850
+ Hash [sha256.Size]byte
851
+ Exists bool
852
+ Realpath string
853
+ IdentityOnly bool
854
+ }
855
+
856
+ // auxiliaryInput distinguishes speculative inputs whose contents select the
857
+ // build universe from the lexical spelling of the source the compiler already
858
+ // selected. The resident source hash owns the latter's contents; this input
859
+ // owns only whether the spelling still reaches the same physical file.
860
+ type auxiliaryInput struct {
861
+ path string
862
+ identityOnly bool
666
863
  }
667
864
 
668
865
  // captureDiskStates records the freshness state of speculative resolution
@@ -671,29 +868,35 @@ type diskState struct {
671
868
  // Windows), so any path that is neither a readable file nor a directory is
672
869
  // recorded as absent instead of failing the snapshot: the recorded state only
673
870
  // needs to flip when the candidate becomes resolvable.
674
- func captureDiskStates(paths []string) map[string]diskState {
675
- states := make(map[string]diskState, len(paths))
676
- for _, path := range paths {
677
- content, err := os.ReadFile(path)
871
+ func captureDiskStates(inputs []auxiliaryInput) map[string]diskState {
872
+ states := make(map[string]diskState, len(inputs))
873
+ for _, input := range inputs {
874
+ state := diskState{IdentityOnly: input.identityOnly}
875
+ content, err := os.ReadFile(input.path)
678
876
  if err != nil {
679
- if info, statErr := os.Stat(path); statErr == nil && info.IsDir() {
680
- states[path] = diskState{Exists: true}
681
- } else {
682
- states[path] = diskState{}
877
+ if info, statErr := os.Stat(input.path); statErr == nil && info.IsDir() {
878
+ state.Exists = true
879
+ state.Realpath = diskRealpath(input.path)
683
880
  }
881
+ states[input.path] = state
684
882
  continue
685
883
  }
686
- states[path] = diskState{Hash: sha256.Sum256(content), Exists: true}
884
+ state.Exists = true
885
+ state.Realpath = diskRealpath(input.path)
886
+ if !input.identityOnly {
887
+ state.Hash = sha256.Sum256(content)
888
+ }
889
+ states[input.path] = state
687
890
  }
688
891
  return states
689
892
  }
690
893
 
691
894
  func diskStatesChanged(previous map[string]diskState) bool {
692
- paths := make([]string, 0, len(previous))
693
- for path := range previous {
694
- paths = append(paths, path)
895
+ inputs := make([]auxiliaryInput, 0, len(previous))
896
+ for path, state := range previous {
897
+ inputs = append(inputs, auxiliaryInput{path: path, identityOnly: state.IdentityOnly})
695
898
  }
696
- current := captureDiskStates(paths)
899
+ current := captureDiskStates(inputs)
697
900
  for path, state := range previous {
698
901
  if current[path] != state {
699
902
  return true
@@ -702,16 +905,16 @@ func diskStatesChanged(previous map[string]diskState) bool {
702
905
  return false
703
906
  }
704
907
 
705
- func auxiliaryInputs(program *driver.Program, configs []*shimtsoptions.ParsedCommandLine, cwd string) []string {
706
- inputs := []string{
707
- filepath.Join(cwd, ".gitignore"),
708
- filepath.Join(cwd, ".git", "info", "exclude"),
709
- filepath.Join(cwd, "package.json"),
710
- filepath.Join(cwd, "package-lock.json"),
711
- filepath.Join(cwd, "pnpm-lock.yaml"),
712
- filepath.Join(cwd, "yarn.lock"),
713
- filepath.Join(cwd, "bun.lock"),
714
- filepath.Join(cwd, "bun.lockb"),
908
+ func auxiliaryInputs(program *driver.Program, configs []*shimtsoptions.ParsedCommandLine, cwd string) []auxiliaryInput {
909
+ inputs := []auxiliaryInput{
910
+ {path: filepath.Join(cwd, ".gitignore")},
911
+ {path: filepath.Join(cwd, ".git", "info", "exclude")},
912
+ {path: filepath.Join(cwd, "package.json")},
913
+ {path: filepath.Join(cwd, "package-lock.json")},
914
+ {path: filepath.Join(cwd, "pnpm-lock.yaml")},
915
+ {path: filepath.Join(cwd, "yarn.lock")},
916
+ {path: filepath.Join(cwd, "bun.lock")},
917
+ {path: filepath.Join(cwd, "bun.lockb")},
715
918
  }
716
919
  for _, source := range program.TSProgram.SourceFiles() {
717
920
  file := source.FileName()
@@ -719,12 +922,18 @@ func auxiliaryInputs(program *driver.Program, configs []*shimtsoptions.ParsedCom
719
922
  continue
720
923
  }
721
924
  directory := filepath.Dir(file)
722
- inputs = appendAncestorInputs(inputs, directory, cwd)
925
+ for _, path := range appendAncestorInputs(nil, directory, cwd) {
926
+ inputs = append(inputs, auxiliaryInput{path: path})
927
+ }
723
928
  for _, reference := range source.ReferencedFiles {
724
- inputs = append(inputs, driver.FileCandidates(filepath.Join(directory, filepath.FromSlash(reference.FileName)))...)
929
+ for _, path := range driver.FileCandidates(filepath.Join(directory, filepath.FromSlash(reference.FileName))) {
930
+ inputs = append(inputs, auxiliaryInput{path: path})
931
+ }
725
932
  }
726
933
  for _, reference := range source.TypeReferenceDirectives {
727
- inputs = append(inputs, driver.TypeReferenceCandidates(configs, directory, cwd, reference.FileName)...)
934
+ for _, path := range driver.TypeReferenceCandidates(configs, directory, cwd, reference.FileName) {
935
+ inputs = append(inputs, auxiliaryInput{path: path})
936
+ }
728
937
  }
729
938
  for _, specifier := range driver.SourceModuleSpecifiers(source) {
730
939
  context := driver.ModuleResolutionContext{
@@ -738,7 +947,7 @@ func auxiliaryInputs(program *driver.Program, configs []*shimtsoptions.ParsedCom
738
947
  }
739
948
  resolved := program.TSProgram.GetResolvedModuleFromModuleSpecifier(source, specifier)
740
949
  if resolved != nil && resolved.IsResolved() {
741
- inputs = append(inputs, driver.ModuleResolutionPredecessors(
950
+ predecessors := driver.ModuleResolutionPredecessors(
742
951
  configs,
743
952
  directory,
744
953
  cwd,
@@ -746,10 +955,18 @@ func auxiliaryInputs(program *driver.Program, configs []*shimtsoptions.ParsedCom
746
955
  resolved.ResolvedFileName,
747
956
  program.FS.UseCaseSensitiveFileNames(),
748
957
  context,
749
- )...)
958
+ )
959
+ for _, path := range predecessors {
960
+ inputs = append(inputs, auxiliaryInput{
961
+ path: path,
962
+ identityOnly: sameExistingAuxiliaryPath(path, resolved.ResolvedFileName),
963
+ })
964
+ }
750
965
  continue
751
966
  }
752
- inputs = append(inputs, driver.ModuleResolutionCandidates(configs, directory, cwd, specifier.Text(), context)...)
967
+ for _, path := range driver.ModuleResolutionCandidates(configs, directory, cwd, specifier.Text(), context) {
968
+ inputs = append(inputs, auxiliaryInput{path: path})
969
+ }
753
970
  }
754
971
  }
755
972
  // Config `types` entries request type packages without any source syntax, so
@@ -760,10 +977,46 @@ func auxiliaryInputs(program *driver.Program, configs []*shimtsoptions.ParsedCom
760
977
  continue
761
978
  }
762
979
  for _, name := range parsed.ParsedConfig.CompilerOptions.Types {
763
- inputs = append(inputs, driver.TypeReferenceCandidates(configs, parsed.GetCurrentDirectory(), cwd, name)...)
980
+ for _, path := range driver.TypeReferenceCandidates(configs, parsed.GetCurrentDirectory(), cwd, name) {
981
+ inputs = append(inputs, auxiliaryInput{path: path})
982
+ }
764
983
  }
765
984
  }
766
- return compactSortedStrings(inputs)
985
+ return compactAuxiliaryInputs(inputs)
986
+ }
987
+
988
+ func sameExistingAuxiliaryPath(left, right string) bool {
989
+ leftInfo, err := os.Stat(left)
990
+ if err != nil {
991
+ return false
992
+ }
993
+ rightInfo, err := os.Stat(right)
994
+ return err == nil && os.SameFile(leftInfo, rightInfo)
995
+ }
996
+
997
+ func compactAuxiliaryInputs(inputs []auxiliaryInput) []auxiliaryInput {
998
+ byPath := make(map[string]auxiliaryInput, len(inputs))
999
+ for _, input := range inputs {
1000
+ if strings.TrimSpace(input.path) == "" {
1001
+ continue
1002
+ }
1003
+ if previous, exists := byPath[input.path]; exists {
1004
+ // A path that also participates as a manifest or other content-bearing
1005
+ // input keeps the stronger content-sensitive contract.
1006
+ input.identityOnly = previous.identityOnly && input.identityOnly
1007
+ }
1008
+ byPath[input.path] = input
1009
+ }
1010
+ paths := make([]string, 0, len(byPath))
1011
+ for path := range byPath {
1012
+ paths = append(paths, path)
1013
+ }
1014
+ sort.Strings(paths)
1015
+ output := make([]auxiliaryInput, 0, len(paths))
1016
+ for _, path := range paths {
1017
+ output = append(output, byPath[path])
1018
+ }
1019
+ return output
767
1020
  }
768
1021
 
769
1022
  func appendAncestorInputs(inputs []string, directory, stop string) []string {
@@ -792,9 +1045,23 @@ func runServe(args []string) int {
792
1045
  fs.SetOutput(stderr)
793
1046
  cwdFlag := fs.String("cwd", "", "project root (defaults to process cwd)")
794
1047
  tsconfigFlag := fs.String("tsconfig", "tsconfig.json", "project tsconfig path")
1048
+ artifactsFlag := fs.String(
1049
+ "artifacts",
1050
+ "",
1051
+ "path to the artifacts a plugin published (JSON); absent or missing means none",
1052
+ )
795
1053
  if err := fs.Parse(args); err != nil {
796
1054
  return 2
797
1055
  }
1056
+ artifacts, artifactsErr := graph.LoadArtifacts(strings.TrimSpace(*artifactsFlag))
1057
+ if artifactsErr != nil {
1058
+ fmt.Fprintf(
1059
+ stderr,
1060
+ "ttscgraph: could not read the published artifacts: %v\n",
1061
+ artifactsErr,
1062
+ )
1063
+ return 1
1064
+ }
798
1065
 
799
1066
  cwd := strings.TrimSpace(*cwdFlag)
800
1067
  if cwd == "" {
@@ -811,10 +1078,22 @@ func runServe(args []string) int {
811
1078
  cwd = shimtspath.ResolvePath(cwd)
812
1079
  tsconfig := strings.TrimSpace(*tsconfigFlag)
813
1080
 
814
- return serveSnapshots(os.Stdin, stdout, cwd, tsconfig)
1081
+ return serveSnapshotsWithArtifacts(os.Stdin, stdout, cwd, tsconfig, artifacts)
815
1082
  }
816
1083
 
817
1084
  func serveSnapshots(input io.Reader, output io.Writer, cwd, tsconfig string) int {
1085
+ return serveSnapshotsWithArtifacts(input, output, cwd, tsconfig, nil)
1086
+ }
1087
+
1088
+ // serveSnapshotsWithArtifacts is the loop the serve command runs, and
1089
+ // serveSnapshots is it with nothing published — the shape every existing case
1090
+ // drives, unchanged.
1091
+ func serveSnapshotsWithArtifacts(
1092
+ input io.Reader,
1093
+ output io.Writer,
1094
+ cwd, tsconfig string,
1095
+ artifacts []graph.Artifact,
1096
+ ) int {
818
1097
  scanner := bufio.NewScanner(input)
819
1098
  scanner.Buffer(make([]byte, 64*1024), 1024*1024)
820
1099
  encoder := json.NewEncoder(output)
@@ -863,7 +1142,7 @@ func serveSnapshots(input io.Reader, output io.Writer, cwd, tsconfig string) int
863
1142
  var loadDuration time.Duration
864
1143
  if session == nil {
865
1144
  loadStarted := time.Now()
866
- created, err := newGraphSession(cwd, tsconfig)
1145
+ created, err := newGraphSessionWithArtifacts(cwd, tsconfig, artifacts)
867
1146
  loadDuration = time.Since(loadStarted)
868
1147
  if err != nil {
869
1148
  if err := encodeServeResponseWithTrace(
@@ -893,6 +1172,18 @@ func serveSnapshots(input io.Reader, output io.Writer, cwd, tsconfig string) int
893
1172
  }
894
1173
  session.requestProtocol = request.GraphSnapshotVersion
895
1174
  session.requestProtocolSelected = true
1175
+ if replaced, err := session.adoptArtifacts(request.Artifacts); err != nil {
1176
+ if err := encodeServeResponseWithTrace(encoder, errorResponse(
1177
+ request.ID,
1178
+ fmt.Sprintf("ttscgraph: could not read the published artifacts: %v", err),
1179
+ ), requestStarted, loadDuration, 0, 0); err != nil {
1180
+ fmt.Fprintf(stderr, "ttscgraph: write serve response: %v\n", err)
1181
+ return 1
1182
+ }
1183
+ continue
1184
+ } else if replaced {
1185
+ session.invalidateArtifacts()
1186
+ }
896
1187
  var dump *graph.Dump
897
1188
  var snapshot *serveGraphSnapshot
898
1189
  var mode string
@@ -906,6 +1197,12 @@ func serveSnapshots(input io.Reader, output io.Writer, cwd, tsconfig string) int
906
1197
  dump, mode, changed, err = session.Snapshot()
907
1198
  }
908
1199
  response := newServeResponse(request.ID)
1200
+ // The envelope answers for THIS session, not for the command in general. An
1201
+ // `unchanged` response carries no dump, so the envelope is the only place a
1202
+ // client learns what this server proves — and a session holding artifacts
1203
+ // publishes a dump that claims them. Leaving the shared list here made the
1204
+ // envelope and the dump disagree on the one frame that has no dump to check.
1205
+ response.Capabilities = session.capabilities()
909
1206
  response.Dump = dump
910
1207
  response.Snapshot = snapshot
911
1208
  response.Mode = mode