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
@@ -1,7 +1,6 @@
1
1
  package driver
2
2
 
3
3
  import (
4
- "context"
5
4
  "errors"
6
5
  "strings"
7
6
  "sync"
@@ -97,13 +96,30 @@ func (p *Program) EmitLinkedTransforms(writeFile shimcompiler.WriteFile) ([]Diag
97
96
  // because the emit context's update hook only records the original, it does not
98
97
  // copy `DeclarationBase.Symbol`.
99
98
  //
100
- // tsgo's emit resolver then walks the transformed tree in
101
- // MarkLinkedReferencesRecursively and, when it resolves an identifier whose
102
- // scope chain passes through one of those rebuilt containers, calls
103
- // getSymbolOfDeclaration(container) — which reads container.Symbol() and nil-
104
- // panics on a rebuilt class/interface/enum. Restoring the symbol from the
105
- // original (the symbol object is shared and node-independent for lookup) lets
106
- // the resolver mark references the same way it would on the parse tree.
99
+ // #201 added this against a real panic in the walk that
100
+ // MarkLinkedReferencesRecursively used to run over the transformed tree. That
101
+ // walk no longer happens here — the builtin chain is built from the parse tree
102
+ // below — so the original trigger is gone, and no probe reproduces a panic with
103
+ // this call stubbed out.
104
+ //
105
+ // Keep it anyway, because the remaining exposure is unproven rather than
106
+ // absent. tsgo's own defense against a plugin-built node is ast.IsParseTreeNode,
107
+ // which most EmitResolver reference methods test to bail out early — and a
108
+ // REBUILT container does not trip it. The emit context stamps
109
+ // NodeFlagsSynthesized when its factory creates a node, but ast.updateNode then
110
+ // ASSIGNS `updated.Flags = original.Flags` and clears the marker again, so a
111
+ // container carrying no symbol still answers "parse tree node" and is let
112
+ // through to checker name resolution. There resolveName reaches
113
+ // getSymbolOfDeclaration(container), an unguarded dereference for a class,
114
+ // class expression, or interface (enum and module are nil-checked upstream).
115
+ // What the probes could not settle is whether the builtin transformers ever
116
+ // actually hand a rebuilt container to one of those methods.
117
+ //
118
+ // Restoring the symbol from the original (the symbol object is shared and
119
+ // node-independent for lookup) makes that path resolve the way it would on the
120
+ // parse tree, at the cost of one walk per file. EmitContext.ParseNode is
121
+ // unaffected either way: it walks MostOriginal before testing the predicate, so
122
+ // it always lands on the genuine parse node.
107
123
  func restoreOriginalDeclarationSymbols(ec *shimprinter.EmitContext, node *shimast.Node) {
108
124
  if node == nil {
109
125
  return
@@ -181,7 +197,39 @@ func (p *Program) EmitWithPluginTransformers(transforms []PluginTransform, write
181
197
  }
182
198
  shimast.SetParentInChildrenUnset(out.AsNode())
183
199
  restoreOriginalDeclarationSymbols(ec, out.AsNode())
184
- for _, tr := range shimcompiler.GetScriptTransformers(ec, host, out) {
200
+ // Build the chain from the PARSE tree and transform the plugin's tree.
201
+ // Upstream passes one file to both roles only because it has no plugin
202
+ // pass between them: emitJSFile hands getScriptTransformers the file it
203
+ // parsed, then reassigns it to the transform result. Here they differ.
204
+ //
205
+ // getScriptTransformers reads its file for three things: in-JS-file, JSX
206
+ // language variant, and emitResolver.MarkLinkedReferencesRecursively. The
207
+ // first two survive UpdateSourceFile, so marking is the whole difference,
208
+ // and marking is a per-node checker resolution walk under the single
209
+ // checker mutex.
210
+ //
211
+ // Marking the plugin's tree does not merely spend that walk on nodes the
212
+ // binder never saw, it emits broken JavaScript. Elision reads the marks
213
+ // back for the file's parse-tree imports, so a reference the plugin
214
+ // REBUILT (a fresh identifier linked with SetOriginal, which is what any
215
+ // partial rewrite produces) leaves its import unmarked. The module
216
+ // transform still aliases that reference and elision still drops the
217
+ // binding the alias names: `dep_1.foo` with no `const dep_1 =
218
+ // require(...)`, a ReferenceError at load. The emit_plugin_rebuilt_*
219
+ // tests pin each import binding shape, plus the ES module lane where the
220
+ // whole import declaration disappears instead of just its binding.
221
+ //
222
+ // Nothing is lost by skipping the plugin's nodes. UpdateSourceFile
223
+ // rebuilds a SourceFile through copyFrom, which carries over neither
224
+ // Locals nor Symbol, and an ordinary import binds into Locals, so no
225
+ // resolveName branch reaches an import from a synthetic identifier. An
226
+ // import the plugin synthesized needs no mark at all: it has no parse
227
+ // original, and elision preserves it unconditionally.
228
+ //
229
+ // Marks accumulate on the checker and are never cleared, so marking one
230
+ // fixed tree per file also stops a second emit on the same Program from
231
+ // inheriting the first pass's plugin-tree marks.
232
+ for _, tr := range shimcompiler.GetScriptTransformers(ec, host, sf) {
185
233
  out = tr.TransformSourceFile(out)
186
234
  }
187
235
  // Print through the source-map-aware helper so a `sourceMap` /
@@ -222,12 +270,30 @@ func (p *Program) EmitWithPluginTransformers(transforms []PluginTransform, write
222
270
  }
223
271
  }
224
272
  }
225
- if !options.GetEmitDeclarations() {
273
+ // The declaration pass below doubles as this lane's build-information pass,
274
+ // so it also runs for a JavaScript-only `incremental` / `composite` project
275
+ // that has no declarations to write at all.
276
+ if !options.GetEmitDeclarations() && !p.emitsBuildInfo() {
226
277
  return nil, nil
227
278
  }
228
279
 
280
+ // What the build information this pass writes does and does not claim.
281
+ //
282
+ // The JavaScript above was hand-assembled and written outside tsgo's
283
+ // emitter, so tsgo's snapshot never saw it happen and records the JS emit of
284
+ // every file as still pending. That error is one-directional and safe: a
285
+ // consumer reading it can only decide to emit again, never to skip a file
286
+ // ttsc actually transformed. Making the record exact would mean running
287
+ // tsgo's own JavaScript emit a second time and discarding its output, paying
288
+ // a full emit to describe work already done. Everything else in the file —
289
+ // the compiler version, the resolved options, per-file versions and
290
+ // signatures, and the declaration state this pass does produce — is accurate.
291
+ //
292
+ // ttsc itself never reads build information back (see
293
+ // `shimcompiler.EmitFreshWithBuildInfo`), so this asymmetry costs a ttsc
294
+ // rebuild nothing.
229
295
  var wfMu sync.Mutex
230
- result := p.TSProgram.Emit(context.Background(), shimcompiler.EmitOptions{
296
+ result := p.emitProgram(shimcompiler.EmitOptions{
231
297
  EmitOnly: shimcompiler.EmitOnlyDts,
232
298
  WriteFile: func(fileName string, text string, data *shimcompiler.WriteFileData) error {
233
299
  wfMu.Lock()
package/driver/graph.go CHANGED
@@ -23,9 +23,10 @@ const bundledScheme = "bundled:///"
23
23
  //
24
24
  // - Edges maps each file to its direct resolved references — imports,
25
25
  // re-exports, `/// <reference>` targets, type reference directives, and
26
- // ambient-module declaration files, type-only edges included. Direct
27
- // edges are the minimal sufficient statistic; consumers that need a flat
28
- // per-file list compute the reachability closure themselves.
26
+ // ambient-module declaration files, type-only edges included. A leaf file
27
+ // has an empty list so the node and its compiler-time input proof remain
28
+ // explicit. Direct edges are the minimal sufficient statistic; consumers
29
+ // that need a flat per-file list compute the reachability closure themselves.
29
30
  // - Globals lists the files that contribute to the global scope (ambient
30
31
  // declaration files, script files, global augmentations, `typeRoots`
31
32
  // entries). A change to any of them can affect every file.
@@ -39,10 +40,12 @@ const bundledScheme = "bundled:///"
39
40
  // map: project-relative slash paths, falling back to slash-normalized
40
41
  // absolute paths outside the project root (see TransformOutputKey).
41
42
  type TransformGraph struct {
42
- Edges map[string][]string `json:"edges"`
43
- Globals []string `json:"globals"`
44
- Configs []string `json:"configs"`
45
- Candidates map[string][]string `json:"candidates,omitempty"`
43
+ Edges map[string][]string `json:"edges"`
44
+ Globals []string `json:"globals"`
45
+ Configs []string `json:"configs"`
46
+ Candidates map[string][]string `json:"candidates,omitempty"`
47
+ InputHashes map[string]*string `json:"inputHashes,omitempty"`
48
+ InputRealpaths map[string]*string `json:"inputRealpaths,omitempty"`
46
49
  }
47
50
 
48
51
  // NewTransformGraph computes the reference graph of a loaded program, keyed
@@ -69,16 +72,66 @@ func NewTransformGraph(prog *Program, cwd string) *TransformGraph {
69
72
  if shimcompiler.FileAffectsGlobalScope(file) {
70
73
  graph.Globals = append(graph.Globals, key)
71
74
  }
72
- targets := referenceTargets(prog, cwd, file)
73
- if len(targets) != 0 {
74
- graph.Edges[key] = targets
75
- }
75
+ // Keep leaf modules as empty adjacency-list entries. Besides making the
76
+ // graph's node universe explicit, this lets attachInputProof bind every
77
+ // source file to the bytes TypeScript-Go actually read. Omitting a leaf
78
+ // would let an A-B-A edit during compilation pair B's output with identical
79
+ // pre/post project snapshots for A.
80
+ graph.Edges[key] = referenceTargets(prog, cwd, file)
76
81
  }
77
82
  sort.Strings(graph.Globals)
78
83
  graph.Configs = configChain(prog, cwd)
84
+ graph.attachInputProof(prog, cwd)
79
85
  return graph
80
86
  }
81
87
 
88
+ // attachInputProof pairs every graph path with the state the compiler
89
+ // filesystem actually returned while constructing the resident Program. A
90
+ // missing entry means proof was incomplete or contradictory; persistent hosts
91
+ // then keep the fresh result but decline cross-build reuse.
92
+ func (graph *TransformGraph) attachInputProof(prog *Program, cwd string) {
93
+ if prog == nil || prog.inputObserver == nil {
94
+ return
95
+ }
96
+ inputs := map[string]struct{}{}
97
+ for source, targets := range graph.Edges {
98
+ inputs[source] = struct{}{}
99
+ for _, target := range targets {
100
+ inputs[target] = struct{}{}
101
+ }
102
+ }
103
+ for _, input := range graph.Globals {
104
+ inputs[input] = struct{}{}
105
+ }
106
+ for _, input := range graph.Configs {
107
+ inputs[input] = struct{}{}
108
+ }
109
+ for source, candidates := range graph.Candidates {
110
+ inputs[source] = struct{}{}
111
+ for _, candidate := range candidates {
112
+ inputs[candidate] = struct{}{}
113
+ }
114
+ }
115
+ hashes := map[string]*string{}
116
+ realpaths := map[string]*string{}
117
+ for input := range inputs {
118
+ file := filepath.FromSlash(input)
119
+ if !filepath.IsAbs(file) {
120
+ file = filepath.Join(cwd, file)
121
+ }
122
+ hash, realpath, ok := prog.inputObserver.proof(file)
123
+ if !ok {
124
+ continue
125
+ }
126
+ hashes[input] = hash
127
+ realpaths[input] = realpath
128
+ }
129
+ if len(hashes) != 0 {
130
+ graph.InputHashes = hashes
131
+ graph.InputRealpaths = realpaths
132
+ }
133
+ }
134
+
82
135
  // referenceTargets resolves one file's direct reference set to sorted envelope
83
136
  // keys, dropping bundled library files and the file itself.
84
137
  func referenceTargets(prog *Program, cwd string, file *ast.SourceFile) []string {
@@ -0,0 +1,273 @@
1
+ package driver
2
+
3
+ import (
4
+ "crypto/sha256"
5
+ "encoding/hex"
6
+ "path/filepath"
7
+ "sync"
8
+
9
+ shimtspath "github.com/microsoft/typescript-go/shim/tspath"
10
+ "github.com/microsoft/typescript-go/shim/vfs"
11
+ )
12
+
13
+ var observedDirectoryDigest = func() string {
14
+ digest := sha256.Sum256([]byte("ttsc:host-input:directory\x00"))
15
+ return hex.EncodeToString(digest[:])
16
+ }()
17
+
18
+ type observedInputKind uint8
19
+
20
+ const (
21
+ observedInputMissing observedInputKind = iota
22
+ observedInputFile
23
+ observedInputDirectory
24
+ )
25
+
26
+ type observedInput struct {
27
+ contentHash *string
28
+ kind observedInputKind
29
+ realpath *string
30
+ unstable bool
31
+ }
32
+
33
+ // inputObservationFS records the exact disk state returned through the
34
+ // compiler filesystem. A later transform envelope can therefore prove which
35
+ // bytes and resolution-candidate states produced its resident Program instead
36
+ // of attaching post-compile disk hashes to an earlier result.
37
+ type inputObservationFS struct {
38
+ vfs.FS
39
+ caseSensitive bool
40
+ mu sync.Mutex
41
+ observations map[string]observedInput
42
+ }
43
+
44
+ func newInputObservationFS(inner vfs.FS) *inputObservationFS {
45
+ return &inputObservationFS{
46
+ FS: inner,
47
+ caseSensitive: inner.UseCaseSensitiveFileNames(),
48
+ observations: map[string]observedInput{},
49
+ }
50
+ }
51
+
52
+ func (fs *inputObservationFS) FileExists(path string) bool {
53
+ exists := fs.FS.FileExists(path)
54
+ if exists {
55
+ // Existence participates in resolution, but only ReadFile returns bytes
56
+ // that can influence the resident Program. Do not duplicate every
57
+ // resolver probe with an eager file read.
58
+ fs.observe(path, observedInput{
59
+ kind: observedInputFile,
60
+ realpath: fs.currentRealpath(path),
61
+ })
62
+ } else {
63
+ fs.observe(path, observedInput{kind: observedInputMissing})
64
+ }
65
+ return exists
66
+ }
67
+
68
+ func (fs *inputObservationFS) ReadFile(path string) (string, bool) {
69
+ contents, ok := fs.FS.ReadFile(path)
70
+ if ok {
71
+ digest := sha256.Sum256([]byte(contents))
72
+ hash := hex.EncodeToString(digest[:])
73
+ fs.observe(path, observedInput{
74
+ contentHash: &hash,
75
+ kind: observedInputFile,
76
+ realpath: fs.currentRealpath(path),
77
+ })
78
+ } else {
79
+ fs.observe(path, observedInput{kind: observedInputMissing})
80
+ }
81
+ return contents, ok
82
+ }
83
+
84
+ func (fs *inputObservationFS) DirectoryExists(path string) bool {
85
+ exists := fs.FS.DirectoryExists(path)
86
+ if exists {
87
+ fs.observe(path, observedInput{
88
+ kind: observedInputDirectory,
89
+ realpath: fs.currentRealpath(path),
90
+ })
91
+ } else {
92
+ fs.observe(path, observedInput{kind: observedInputMissing})
93
+ }
94
+ return exists
95
+ }
96
+
97
+ func (fs *inputObservationFS) GetAccessibleEntries(path string) vfs.Entries {
98
+ // DirectoryExists records directory identity on the ordinary compiler path.
99
+ // An empty listing alone cannot distinguish a missing directory from an
100
+ // existing empty one without issuing an extra stat for every enumeration.
101
+ return fs.FS.GetAccessibleEntries(path)
102
+ }
103
+
104
+ func (fs *inputObservationFS) Stat(path string) vfs.FileInfo {
105
+ info := fs.FS.Stat(path)
106
+ if info == nil {
107
+ fs.observe(path, observedInput{kind: observedInputMissing})
108
+ } else if info.IsDir() {
109
+ fs.observe(path, observedInput{
110
+ kind: observedInputDirectory,
111
+ realpath: fs.currentRealpath(path),
112
+ })
113
+ } else {
114
+ fs.observe(path, observedInput{
115
+ kind: observedInputFile,
116
+ realpath: fs.currentRealpath(path),
117
+ })
118
+ }
119
+ return info
120
+ }
121
+
122
+ func (fs *inputObservationFS) Realpath(path string) string {
123
+ realpath := fs.FS.Realpath(path)
124
+ if realpath != "" {
125
+ resolved := filepath.Clean(realpath)
126
+ fs.observeRealpath(path, &resolved)
127
+ }
128
+ return realpath
129
+ }
130
+
131
+ func (fs *inputObservationFS) currentRealpath(path string) *string {
132
+ realpath := fs.FS.Realpath(path)
133
+ if realpath == "" {
134
+ return nil
135
+ }
136
+ resolved := filepath.Clean(realpath)
137
+ return &resolved
138
+ }
139
+
140
+ func (fs *inputObservationFS) observationKey(path string) string {
141
+ if !filepath.IsAbs(path) {
142
+ return ""
143
+ }
144
+ return shimtspath.GetCanonicalFileName(
145
+ shimtspath.NormalizePath(path),
146
+ fs.caseSensitive,
147
+ )
148
+ }
149
+
150
+ func (fs *inputObservationFS) observe(path string, next observedInput) {
151
+ key := fs.observationKey(path)
152
+ if key == "" {
153
+ return
154
+ }
155
+ fs.mu.Lock()
156
+ defer fs.mu.Unlock()
157
+ keys := []string{key}
158
+ // A compiler read can arrive through an 8.3, case-variant, or already-real
159
+ // spelling while resolution recorded the selected lexical alias. Index the
160
+ // returned bytes by the final physical path too, so proof can join the two
161
+ // observations without another disk read.
162
+ if next.contentHash != nil && next.realpath != nil {
163
+ physicalKey := fs.observationKey(*next.realpath)
164
+ if physicalKey != "" && physicalKey != key {
165
+ keys = append(keys, physicalKey)
166
+ }
167
+ }
168
+ for _, observationKey := range keys {
169
+ fs.mergeObservation(observationKey, next)
170
+ }
171
+ }
172
+
173
+ // mergeObservation merges one observation key while the caller holds fs.mu.
174
+ func (fs *inputObservationFS) mergeObservation(key string, next observedInput) {
175
+ previous, found := fs.observations[key]
176
+ if !found {
177
+ fs.observations[key] = next
178
+ return
179
+ }
180
+ if previous.unstable || next.unstable || previous.kind != next.kind || !sameOptionalString(previous.realpath, next.realpath) {
181
+ previous.unstable = true
182
+ previous.contentHash = nil
183
+ previous.realpath = nil
184
+ fs.observations[key] = previous
185
+ return
186
+ }
187
+ if previous.contentHash != nil && next.contentHash != nil && *previous.contentHash != *next.contentHash {
188
+ previous.unstable = true
189
+ previous.contentHash = nil
190
+ previous.realpath = nil
191
+ fs.observations[key] = previous
192
+ return
193
+ }
194
+ if previous.contentHash == nil && next.contentHash != nil {
195
+ previous.contentHash = next.contentHash
196
+ }
197
+ fs.observations[key] = previous
198
+ }
199
+
200
+ func (fs *inputObservationFS) observeRealpath(path string, realpath *string) {
201
+ key := fs.observationKey(path)
202
+ if key == "" {
203
+ return
204
+ }
205
+ fs.mu.Lock()
206
+ defer fs.mu.Unlock()
207
+ previous, found := fs.observations[key]
208
+ if !found {
209
+ // A realpath by itself cannot prove whether the path was a file or
210
+ // directory. The ordinary resolver probes record that state separately.
211
+ return
212
+ }
213
+ if previous.unstable || !sameOptionalString(previous.realpath, realpath) {
214
+ previous.unstable = true
215
+ previous.contentHash = nil
216
+ previous.realpath = nil
217
+ fs.observations[key] = previous
218
+ }
219
+ }
220
+
221
+ func sameOptionalString(left, right *string) bool {
222
+ if left == nil || right == nil {
223
+ return left == nil && right == nil
224
+ }
225
+ return *left == *right
226
+ }
227
+
228
+ // proof returns a stable compiler-time state. A nil hash/realpath with ok=true
229
+ // is an explicit JSON null for a path observed missing; ok=false means the
230
+ // compiler never produced a complete, internally consistent observation.
231
+ func (fs *inputObservationFS) proof(path string) (hash, realpath *string, ok bool) {
232
+ key := fs.observationKey(path)
233
+ if key == "" {
234
+ return nil, nil, false
235
+ }
236
+ fs.mu.Lock()
237
+ observation, found := fs.observations[key]
238
+ // TypeScript-Go can probe a lexical symlink candidate and then read the
239
+ // selected source by its physical filename. Reuse that exact observed read
240
+ // for the alias instead of issuing an eager duplicate read from FileExists.
241
+ if found && !observation.unstable && observation.kind == observedInputFile && observation.contentHash == nil && observation.realpath != nil {
242
+ targetKey := fs.observationKey(*observation.realpath)
243
+ target, targetFound := fs.observations[targetKey]
244
+ if targetFound && !target.unstable && target.kind == observedInputFile && target.contentHash != nil && sameOptionalString(target.realpath, observation.realpath) {
245
+ targetHash := *target.contentHash
246
+ observation.contentHash = &targetHash
247
+ }
248
+ }
249
+ fs.mu.Unlock()
250
+ if !found || observation.unstable {
251
+ return nil, nil, false
252
+ }
253
+ switch observation.kind {
254
+ case observedInputMissing:
255
+ return nil, nil, true
256
+ case observedInputDirectory:
257
+ hash := observedDirectoryDigest
258
+ if observation.realpath == nil {
259
+ return nil, nil, false
260
+ }
261
+ realpath := *observation.realpath
262
+ return &hash, &realpath, true
263
+ case observedInputFile:
264
+ if observation.contentHash == nil || observation.realpath == nil {
265
+ return nil, nil, false
266
+ }
267
+ hash := *observation.contentHash
268
+ realpath := *observation.realpath
269
+ return &hash, &realpath, true
270
+ default:
271
+ return nil, nil, false
272
+ }
273
+ }
@@ -0,0 +1,149 @@
1
+ package driver
2
+
3
+ import (
4
+ "crypto/sha256"
5
+ "encoding/hex"
6
+ "os"
7
+ "os/exec"
8
+ "path/filepath"
9
+ "runtime"
10
+ "testing"
11
+ )
12
+
13
+ func TestInputObservationFSRejectsRestoredContent(t *testing.T) {
14
+ root := t.TempDir()
15
+ file := filepath.Join(root, "external.d.ts")
16
+ if err := os.WriteFile(file, []byte("A"), 0o644); err != nil {
17
+ t.Fatal(err)
18
+ }
19
+ observed := newInputObservationFS(DefaultFS())
20
+ if contents, ok := observed.ReadFile(file); !ok || contents != "A" {
21
+ t.Fatalf("first read = %q, %v", contents, ok)
22
+ }
23
+ if err := os.WriteFile(file, []byte("B"), 0o644); err != nil {
24
+ t.Fatal(err)
25
+ }
26
+ if contents, ok := observed.ReadFile(file); !ok || contents != "B" {
27
+ t.Fatalf("second read = %q, %v", contents, ok)
28
+ }
29
+ if err := os.WriteFile(file, []byte("A"), 0o644); err != nil {
30
+ t.Fatal(err)
31
+ }
32
+ if _, _, ok := observed.proof(file); ok {
33
+ t.Fatal("A-B-A observation unexpectedly produced reusable proof")
34
+ }
35
+ }
36
+
37
+ func TestInputObservationFSProvesReadBytesAndMissingCandidates(t *testing.T) {
38
+ root := t.TempDir()
39
+ file := filepath.Join(root, "selected.ts")
40
+ missing := filepath.Join(root, "superseding.ts")
41
+ contents := "export const selected = true;\n"
42
+ if err := os.WriteFile(file, []byte(contents), 0o644); err != nil {
43
+ t.Fatal(err)
44
+ }
45
+ observed := newInputObservationFS(DefaultFS())
46
+ if !observed.FileExists(file) {
47
+ t.Fatal("selected file was not found")
48
+ }
49
+ if _, ok := observed.ReadFile(file); !ok {
50
+ t.Fatal("selected file was not read")
51
+ }
52
+ if observed.FileExists(missing) {
53
+ t.Fatal("missing candidate unexpectedly exists")
54
+ }
55
+
56
+ digest := sha256.Sum256([]byte(contents))
57
+ wantHash := hex.EncodeToString(digest[:])
58
+ hash, realpath, ok := observed.proof(file)
59
+ if !ok || hash == nil || *hash != wantHash {
60
+ t.Fatalf("selected proof hash = %v, %v", hash, ok)
61
+ }
62
+ wantRealpath, err := filepath.EvalSymlinks(file)
63
+ if err != nil {
64
+ t.Fatal(err)
65
+ }
66
+ if realpath == nil || filepath.Clean(*realpath) != filepath.Clean(wantRealpath) {
67
+ t.Fatalf("selected proof realpath = %v, want %q", realpath, wantRealpath)
68
+ }
69
+ hash, realpath, ok = observed.proof(missing)
70
+ if !ok || hash != nil || realpath != nil {
71
+ t.Fatalf("missing proof = %v, %v, %v", hash, realpath, ok)
72
+ }
73
+ }
74
+
75
+ func TestInputObservationFSHashesCompilerDecodedText(t *testing.T) {
76
+ root := t.TempDir()
77
+ file := filepath.Join(root, "bom.ts")
78
+ text := "export const value = true;\n"
79
+ raw := append([]byte{0xef, 0xbb, 0xbf}, []byte(text)...)
80
+ if err := os.WriteFile(file, raw, 0o644); err != nil {
81
+ t.Fatal(err)
82
+ }
83
+ observed := newInputObservationFS(DefaultFS())
84
+ contents, ok := observed.ReadFile(file)
85
+ if !ok || contents != text {
86
+ t.Fatalf("compiler read = %q, %v; want decoded text", contents, ok)
87
+ }
88
+ digest := sha256.Sum256([]byte(text))
89
+ hash, _, ok := observed.proof(file)
90
+ if !ok || hash == nil || *hash != hex.EncodeToString(digest[:]) {
91
+ t.Fatalf("decoded proof hash = %v, %v", hash, ok)
92
+ }
93
+ }
94
+
95
+ func TestInputObservationFSJoinsSelectedAliasProbeToPhysicalRead(t *testing.T) {
96
+ root := t.TempDir()
97
+ target := filepath.Join(root, "target")
98
+ if err := os.Mkdir(target, 0o755); err != nil {
99
+ t.Fatal(err)
100
+ }
101
+ physical := filepath.Join(target, "value.js")
102
+ contents := "export const value = true;\n"
103
+ if err := os.WriteFile(physical, []byte(contents), 0o644); err != nil {
104
+ t.Fatal(err)
105
+ }
106
+ alias := filepath.Join(root, "alias")
107
+ if runtime.GOOS == "windows" {
108
+ command := exec.Command(
109
+ "node",
110
+ "-e",
111
+ `require("node:fs").symlinkSync(process.argv[1], process.argv[2], "junction")`,
112
+ target,
113
+ alias,
114
+ )
115
+ if output, err := command.CombinedOutput(); err != nil {
116
+ t.Skipf("directory junction unavailable on this host: %v: %s", err, output)
117
+ }
118
+ } else if err := os.Symlink(target, alias); err != nil {
119
+ t.Skipf("directory symlink unavailable on this host: %v", err)
120
+ }
121
+ lexical := filepath.Join(alias, "value.js")
122
+ observed := newInputObservationFS(DefaultFS())
123
+ if !observed.FileExists(lexical) {
124
+ t.Fatal("selected lexical alias was not found")
125
+ }
126
+ if _, ok := observed.ReadFile(physical); !ok {
127
+ t.Fatal("selected physical source was not read")
128
+ }
129
+
130
+ digest := sha256.Sum256([]byte(contents))
131
+ hash, realpath, ok := observed.proof(lexical)
132
+ if !ok || hash == nil || *hash != hex.EncodeToString(digest[:]) {
133
+ t.Fatalf("alias proof hash = %v, %v", hash, ok)
134
+ }
135
+ if realpath == nil {
136
+ t.Fatalf("alias proof realpath = nil, want %q", physical)
137
+ }
138
+ actualInfo, err := os.Stat(*realpath)
139
+ if err != nil {
140
+ t.Fatal(err)
141
+ }
142
+ expectedInfo, err := os.Stat(physical)
143
+ if err != nil {
144
+ t.Fatal(err)
145
+ }
146
+ if !os.SameFile(actualInfo, expectedInfo) {
147
+ t.Fatalf("alias proof realpath = %q, want %q", *realpath, physical)
148
+ }
149
+ }