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
package/driver/plugins.go CHANGED
@@ -5,7 +5,9 @@ import (
5
5
  "fmt"
6
6
  "os"
7
7
  "path/filepath"
8
+ "sort"
8
9
  "strings"
10
+ "sync"
9
11
  )
10
12
 
11
13
  // LinkedPluginsEnv is the environment variable ttsc sets to pass the JSON
@@ -27,6 +29,44 @@ const LinkedPluginsEnv = "TTSC_LINKED_PLUGINS_JSON"
27
29
  // still receive it.
28
30
  const PluginConfigDirEnv = "TTSC_PLUGIN_CONFIG_DIR"
29
31
 
32
+ // TsgoArgsEnv is the environment variable through which the ttsc launcher
33
+ // passes the tsgo CLI flags it did not consume itself (`--strict`,
34
+ // `--declaration`, the single-file lane's output containment, …) to a native
35
+ // sidecar. The value is a JSON array of argv tokens; an empty or absent value
36
+ // means nothing was forwarded.
37
+ //
38
+ // It rides the environment for the same reason PluginConfigDirEnv does, only
39
+ // with sharper consequences. The payload used to travel as a `--tsgo-args`
40
+ // flag, which #113 appended to a plugin protocol third-party hosts had already
41
+ // frozen. A Go `flag.FlagSet` created with `flag.ContinueOnError` treats an
42
+ // undeclared flag as fatal, so every sidecar built before #113 answered
43
+ // `flag provided but not defined: -tsgo-args` and exited 2 — including on
44
+ // `ttsc <file.ts>`, where the launcher forwards its own output-containment
45
+ // flags and the user passed nothing at all (issue #1188). An unknown
46
+ // environment variable is inert to every host; an unknown flag is fatal to all
47
+ // of them. ttsc's own hosts still accept `--tsgo-args` so an older launcher
48
+ // paired with a newer host keeps working.
49
+ const TsgoArgsEnv = "TTSC_TSGO_ARGS"
50
+
51
+ // TsgoArgsFromEnv decodes the forwarded tsgo argv the launcher published in
52
+ // TsgoArgsEnv. An absent or whitespace-only value yields a nil slice and no
53
+ // error, so a host can call this unconditionally.
54
+ //
55
+ // Hosts that also declare a `--tsgo-args` flag should prefer the explicit flag
56
+ // value and fall back to this; LoadProgram already does that for every
57
+ // driver-based host.
58
+ func TsgoArgsFromEnv() ([]string, error) {
59
+ raw := strings.TrimSpace(os.Getenv(TsgoArgsEnv))
60
+ if raw == "" {
61
+ return nil, nil
62
+ }
63
+ var args []string
64
+ if err := json.Unmarshal([]byte(raw), &args); err != nil {
65
+ return nil, fmt.Errorf("ttsc driver: invalid %s: %w", TsgoArgsEnv, err)
66
+ }
67
+ return args, nil
68
+ }
69
+
30
70
  // PluginConfigBaseDir returns the directory where a plugin anchors its
31
71
  // config-file discovery walk and resolves relative "configFile" paths.
32
72
  // The explicit PluginConfigDirEnv channel wins when set; otherwise the
@@ -60,6 +100,182 @@ type PluginContext struct {
60
100
  Cwd string
61
101
  Entry PluginEntry
62
102
  Tsconfig string
103
+
104
+ reportHostInput func(string)
105
+ reportHostInputHash func(string, *string)
106
+ reportHostInputHashUnknown func(string)
107
+ reportHostInputRealpath func(string, *string)
108
+ reportHostInputRealpathUnknown func(string)
109
+ reportFileDependency func(string, string)
110
+ reportFileDependencyRejected func(string)
111
+ reportFileComplete func(string)
112
+ reportEveryFileComplete func()
113
+ }
114
+
115
+ // ReportHostInputHash declares the exact file state consumed by a native
116
+ // plugin. hash is a lowercase SHA-256 digest for an observed file and nil for
117
+ // a missing candidate. Conflicting observations are retained as host inputs
118
+ // but omitted from PluginHostInputHashes, forcing persistent adapters to
119
+ // decline narrow reuse without failing the transform.
120
+ func (ctx PluginContext) ReportHostInputHash(file string, hash *string) {
121
+ if ctx.reportHostInputHash == nil || strings.TrimSpace(file) == "" {
122
+ return
123
+ }
124
+ if !filepath.IsAbs(file) {
125
+ file = filepath.Join(ctx.Cwd, file)
126
+ }
127
+ file = filepath.Clean(file)
128
+ if hash != nil && !isLowerSHA256(*hash) {
129
+ if ctx.reportHostInputHashUnknown != nil {
130
+ ctx.reportHostInputHashUnknown(file)
131
+ } else if ctx.reportHostInput != nil {
132
+ ctx.reportHostInput(file)
133
+ }
134
+ return
135
+ }
136
+ ctx.reportHostInputHash(file, hash)
137
+ }
138
+
139
+ // ReportHostInputRealpath declares the physical path resolved while a native
140
+ // plugin consumed file. realpath is absolute for an observed path and nil for
141
+ // a missing candidate. Conflicting observations remain host inputs but are
142
+ // omitted from PluginHostInputRealpaths so adapters cannot attach an earlier
143
+ // result to a retargeted symlink or junction.
144
+ func (ctx PluginContext) ReportHostInputRealpath(file string, realpath *string) {
145
+ if ctx.reportHostInputRealpath == nil || strings.TrimSpace(file) == "" {
146
+ return
147
+ }
148
+ if !filepath.IsAbs(file) {
149
+ file = filepath.Join(ctx.Cwd, file)
150
+ }
151
+ file = filepath.Clean(file)
152
+ if realpath != nil {
153
+ if strings.TrimSpace(*realpath) == "" || !filepath.IsAbs(*realpath) {
154
+ if ctx.reportHostInputRealpathUnknown != nil {
155
+ ctx.reportHostInputRealpathUnknown(file)
156
+ } else if ctx.reportHostInput != nil {
157
+ ctx.reportHostInput(file)
158
+ }
159
+ return
160
+ }
161
+ resolved := filepath.Clean(*realpath)
162
+ realpath = &resolved
163
+ }
164
+ ctx.reportHostInputRealpath(file, realpath)
165
+ }
166
+
167
+ func isLowerSHA256(value string) bool {
168
+ if len(value) != 64 {
169
+ return false
170
+ }
171
+ for _, char := range value {
172
+ if (char < '0' || char > '9') && (char < 'a' || char > 'f') {
173
+ return false
174
+ }
175
+ }
176
+ return true
177
+ }
178
+
179
+ // ReportHostInput declares an absolute file whose content or presence was
180
+ // consumed while the native plugin evaluated configuration. Native transform
181
+ // envelopes expose the generation-wide union so persistent hosts can invalidate
182
+ // without re-evaluating plugin config on the JavaScript side.
183
+ func (ctx PluginContext) ReportHostInput(file string) {
184
+ if ctx.reportHostInput == nil || strings.TrimSpace(file) == "" {
185
+ return
186
+ }
187
+ if !filepath.IsAbs(file) {
188
+ file = filepath.Join(ctx.Cwd, file)
189
+ }
190
+ ctx.reportHostInput(filepath.Clean(file))
191
+ }
192
+
193
+ // ReportFileDependency declares one file whose content influenced this
194
+ // plugin's contribution to the transformed file file. Both are resolved
195
+ // against the plugin's cwd and keyed like every other envelope section, so a
196
+ // plugin reports the program's own file names and the host joins the sections
197
+ // by key.
198
+ //
199
+ // Reporting alone only widens what a consumer invalidates on; it is
200
+ // ReportFileDependenciesComplete that turns the reported set into the file's
201
+ // whole input set.
202
+ //
203
+ // A dependency this cannot resolve to a key is not silently forgotten: it
204
+ // withdraws this plugin's completeness claim for that file, because a complete
205
+ // list missing a member is exactly what serves stale output. The plugin's other
206
+ // files keep theirs.
207
+ func (ctx PluginContext) ReportFileDependency(file string, dependency string) {
208
+ if ctx.reportFileDependency == nil {
209
+ return
210
+ }
211
+ target, ok := ctx.transformKey(file)
212
+ if !ok {
213
+ return
214
+ }
215
+ input, ok := ctx.transformKey(dependency)
216
+ if !ok {
217
+ if ctx.reportFileDependencyRejected != nil {
218
+ ctx.reportFileDependencyRejected(target)
219
+ }
220
+ return
221
+ }
222
+ ctx.reportFileDependency(target, input)
223
+ }
224
+
225
+ // ReportFileDependenciesComplete declares that everything this plugin's
226
+ // contribution to file consumed, beyond file's own text and the universal
227
+ // compiler-option chain, was reported through ReportFileDependency — possibly
228
+ // nothing at all.
229
+ //
230
+ // The declaration is per (plugin, file), the way the protocol's completeness
231
+ // contract defines it: the host lists a file in dependenciesComplete only when
232
+ // every plugin that can contribute to it declared it, because a consumer cannot
233
+ // attribute one plugin's entries back to it.
234
+ func (ctx PluginContext) ReportFileDependenciesComplete(file string) {
235
+ if ctx.reportFileComplete == nil {
236
+ return
237
+ }
238
+ target, ok := ctx.transformKey(file)
239
+ if !ok {
240
+ return
241
+ }
242
+ ctx.reportFileComplete(target)
243
+ }
244
+
245
+ // ReportDependenciesComplete makes the same declaration for every file in the
246
+ // program at once: this plugin's contribution to any transformed file is a
247
+ // function of that file's own text, whatever it reported for that file, its
248
+ // reported host inputs, and the compiler options.
249
+ //
250
+ // That is the honest claim of a syntactic transform — one that decides from the
251
+ // file in front of it and its own configuration rather than from the type
252
+ // system — and it is the only form available to a hook that never sees the
253
+ // program, such as SourcePreamble.
254
+ func (ctx PluginContext) ReportDependenciesComplete() {
255
+ if ctx.reportEveryFileComplete == nil {
256
+ return
257
+ }
258
+ ctx.reportEveryFileComplete()
259
+ }
260
+
261
+ // transformKey resolves one plugin-reported path to the key convention every
262
+ // envelope section uses. A plugin reports the spellings its host handed it
263
+ // (absolute program file names, or paths relative to the plugin's cwd), and
264
+ // the compiler's own file names are slash-normalized on every platform, so
265
+ // both sides must pass through TransformOutputKey to meet.
266
+ //
267
+ // Separators and the project prefix reconcile, but the suffix keeps the case
268
+ // the caller wrote: report the program's own spelling of a file rather than a
269
+ // re-cased one, or the key will not match on a case-insensitive filesystem and
270
+ // the declaration is dropped.
271
+ func (ctx PluginContext) transformKey(file string) (string, bool) {
272
+ if strings.TrimSpace(file) == "" {
273
+ return "", false
274
+ }
275
+ if !filepath.IsAbs(file) {
276
+ file = filepath.Join(ctx.Cwd, file)
277
+ }
278
+ return TransformOutputKey(ctx.Cwd, filepath.Clean(file)), true
63
279
  }
64
280
 
65
281
  // SourcePreamblePlugin can inject source text before TypeScript-Go parses the
@@ -90,9 +306,245 @@ type EmitTransformPlugin interface {
90
306
  }
91
307
 
92
308
  type linkedPluginState struct {
93
- cwd string
94
- entries []PluginEntry
95
- tsconfig string
309
+ cwd string
310
+ declarations *pluginFileDeclarations
311
+ entries []PluginEntry
312
+ inputs *pluginHostInputScopes
313
+ tsconfig string
314
+ }
315
+
316
+ // pluginHostInputScopes keeps one observation set per plugin hook. A hash from
317
+ // one hook cannot prove another hook's same-file dependency when that hook only
318
+ // reported the path, while ReportHostInput followed by ReportHostInputHash in
319
+ // one hook remains one complete observation.
320
+ type pluginHostInputScopes struct {
321
+ mu sync.Mutex
322
+ scopes []*pluginHostInputs
323
+ }
324
+
325
+ type pluginHostInputs struct {
326
+ files map[string]struct{}
327
+ hashes map[string]pluginHostInputHash
328
+ realpaths map[string]pluginHostInputHash
329
+ mu sync.Mutex
330
+ }
331
+
332
+ type pluginHostInputHash struct {
333
+ hash *string
334
+ known bool
335
+ }
336
+
337
+ func newPluginHostInputScopes() *pluginHostInputScopes {
338
+ return &pluginHostInputScopes{}
339
+ }
340
+
341
+ func newPluginHostInputs() *pluginHostInputs {
342
+ return &pluginHostInputs{
343
+ files: map[string]struct{}{},
344
+ hashes: map[string]pluginHostInputHash{},
345
+ realpaths: map[string]pluginHostInputHash{},
346
+ }
347
+ }
348
+
349
+ func (inputs *pluginHostInputScopes) newScope() *pluginHostInputs {
350
+ scope := newPluginHostInputs()
351
+ if inputs == nil {
352
+ return scope
353
+ }
354
+ inputs.mu.Lock()
355
+ inputs.scopes = append(inputs.scopes, scope)
356
+ inputs.mu.Unlock()
357
+ return scope
358
+ }
359
+
360
+ func (inputs *pluginHostInputScopes) snapshot() []*pluginHostInputs {
361
+ if inputs == nil {
362
+ return nil
363
+ }
364
+ inputs.mu.Lock()
365
+ defer inputs.mu.Unlock()
366
+ return append([]*pluginHostInputs(nil), inputs.scopes...)
367
+ }
368
+
369
+ func (inputs *pluginHostInputs) addHash(file string, hash *string) {
370
+ if inputs == nil {
371
+ return
372
+ }
373
+ inputs.mu.Lock()
374
+ defer inputs.mu.Unlock()
375
+ inputs.files[file] = struct{}{}
376
+ previous, exists := inputs.hashes[file]
377
+ if !exists {
378
+ inputs.hashes[file] = pluginHostInputHash{hash: cloneStringPointer(hash), known: true}
379
+ return
380
+ }
381
+ if !previous.known || !sameStringPointer(previous.hash, hash) {
382
+ inputs.hashes[file] = pluginHostInputHash{known: false}
383
+ }
384
+ }
385
+
386
+ func (inputs *pluginHostInputs) invalidateHash(file string) {
387
+ if inputs == nil {
388
+ return
389
+ }
390
+ inputs.mu.Lock()
391
+ defer inputs.mu.Unlock()
392
+ inputs.files[file] = struct{}{}
393
+ inputs.hashes[file] = pluginHostInputHash{known: false}
394
+ }
395
+
396
+ func (inputs *pluginHostInputs) addRealpath(file string, realpath *string) {
397
+ if inputs == nil {
398
+ return
399
+ }
400
+ inputs.mu.Lock()
401
+ defer inputs.mu.Unlock()
402
+ inputs.files[file] = struct{}{}
403
+ previous, exists := inputs.realpaths[file]
404
+ if !exists {
405
+ inputs.realpaths[file] = pluginHostInputHash{hash: cloneStringPointer(realpath), known: true}
406
+ return
407
+ }
408
+ if !previous.known || !sameStringPointer(previous.hash, realpath) {
409
+ inputs.realpaths[file] = pluginHostInputHash{known: false}
410
+ }
411
+ }
412
+
413
+ func (inputs *pluginHostInputs) invalidateRealpath(file string) {
414
+ if inputs == nil {
415
+ return
416
+ }
417
+ inputs.mu.Lock()
418
+ defer inputs.mu.Unlock()
419
+ inputs.files[file] = struct{}{}
420
+ inputs.realpaths[file] = pluginHostInputHash{known: false}
421
+ }
422
+
423
+ func cloneStringPointer(value *string) *string {
424
+ if value == nil {
425
+ return nil
426
+ }
427
+ cloned := *value
428
+ return &cloned
429
+ }
430
+
431
+ func sameStringPointer(left, right *string) bool {
432
+ if left == nil || right == nil {
433
+ return left == nil && right == nil
434
+ }
435
+ return *left == *right
436
+ }
437
+
438
+ func (inputs *pluginHostInputs) add(file string) {
439
+ if inputs == nil {
440
+ return
441
+ }
442
+ inputs.mu.Lock()
443
+ inputs.files[file] = struct{}{}
444
+ inputs.mu.Unlock()
445
+ }
446
+
447
+ func (inputs *pluginHostInputs) snapshot() (map[string]struct{}, map[string]pluginHostInputHash, map[string]pluginHostInputHash) {
448
+ if inputs == nil {
449
+ return nil, nil, nil
450
+ }
451
+ inputs.mu.Lock()
452
+ defer inputs.mu.Unlock()
453
+ files := make(map[string]struct{}, len(inputs.files))
454
+ for file := range inputs.files {
455
+ files[file] = struct{}{}
456
+ }
457
+ hashes := make(map[string]pluginHostInputHash, len(inputs.hashes))
458
+ for file, observation := range inputs.hashes {
459
+ hashes[file] = pluginHostInputHash{
460
+ hash: cloneStringPointer(observation.hash),
461
+ known: observation.known,
462
+ }
463
+ }
464
+ realpaths := make(map[string]pluginHostInputHash, len(inputs.realpaths))
465
+ for file, observation := range inputs.realpaths {
466
+ realpaths[file] = pluginHostInputHash{
467
+ hash: cloneStringPointer(observation.hash),
468
+ known: observation.known,
469
+ }
470
+ }
471
+ return files, hashes, realpaths
472
+ }
473
+
474
+ func (inputs *pluginHostInputScopes) list() []string {
475
+ union := map[string]struct{}{}
476
+ for _, scope := range inputs.snapshot() {
477
+ files, _, _ := scope.snapshot()
478
+ for file := range files {
479
+ union[file] = struct{}{}
480
+ }
481
+ }
482
+ files := make([]string, 0, len(union))
483
+ for file := range union {
484
+ files = append(files, file)
485
+ }
486
+ sort.Strings(files)
487
+ return files
488
+ }
489
+
490
+ func (inputs *pluginHostInputScopes) hashList() map[string]*string {
491
+ combined := map[string]pluginHostInputHash{}
492
+ for _, scope := range inputs.snapshot() {
493
+ files, hashes, _ := scope.snapshot()
494
+ for file := range files {
495
+ observation, exists := hashes[file]
496
+ if !exists || !observation.known {
497
+ combined[file] = pluginHostInputHash{known: false}
498
+ continue
499
+ }
500
+ previous, exists := combined[file]
501
+ if !exists {
502
+ combined[file] = observation
503
+ } else if !previous.known || !sameStringPointer(previous.hash, observation.hash) {
504
+ combined[file] = pluginHostInputHash{known: false}
505
+ }
506
+ }
507
+ }
508
+ hashes := map[string]*string{}
509
+ for file, observation := range combined {
510
+ if observation.known {
511
+ hashes[file] = cloneStringPointer(observation.hash)
512
+ }
513
+ }
514
+ if len(hashes) == 0 {
515
+ return nil
516
+ }
517
+ return hashes
518
+ }
519
+
520
+ func (inputs *pluginHostInputScopes) realpathList() map[string]*string {
521
+ combined := map[string]pluginHostInputHash{}
522
+ for _, scope := range inputs.snapshot() {
523
+ files, _, realpaths := scope.snapshot()
524
+ for file := range files {
525
+ observation, exists := realpaths[file]
526
+ if !exists || !observation.known {
527
+ combined[file] = pluginHostInputHash{known: false}
528
+ continue
529
+ }
530
+ previous, exists := combined[file]
531
+ if !exists {
532
+ combined[file] = observation
533
+ } else if !previous.known || !sameStringPointer(previous.hash, observation.hash) {
534
+ combined[file] = pluginHostInputHash{known: false}
535
+ }
536
+ }
537
+ }
538
+ realpaths := map[string]*string{}
539
+ for file, observation := range combined {
540
+ if observation.known {
541
+ realpaths[file] = cloneStringPointer(observation.hash)
542
+ }
543
+ }
544
+ if len(realpaths) == 0 {
545
+ return nil
546
+ }
547
+ return realpaths
96
548
  }
97
549
 
98
550
  var pluginRegistry []any
@@ -113,16 +565,23 @@ func RegisterPlugin(plugin any) {
113
565
  func loadLinkedPluginState(cwd, tsconfigPath string) (linkedPluginState, error) {
114
566
  input := strings.TrimSpace(os.Getenv(LinkedPluginsEnv))
115
567
  if input == "" {
116
- return linkedPluginState{cwd: cwd, tsconfig: tsconfigPath}, nil
568
+ return linkedPluginState{
569
+ cwd: cwd,
570
+ declarations: newPluginFileDeclarations(),
571
+ inputs: newPluginHostInputScopes(),
572
+ tsconfig: tsconfigPath,
573
+ }, nil
117
574
  }
118
575
  var entries []PluginEntry
119
576
  if err := json.Unmarshal([]byte(input), &entries); err != nil {
120
577
  return linkedPluginState{}, fmt.Errorf("ttsc driver: invalid %s: %w", LinkedPluginsEnv, err)
121
578
  }
122
579
  return linkedPluginState{
123
- cwd: cwd,
124
- entries: entries,
125
- tsconfig: tsconfigPath,
580
+ cwd: cwd,
581
+ declarations: newPluginFileDeclarations(),
582
+ entries: entries,
583
+ inputs: newPluginHostInputScopes(),
584
+ tsconfig: tsconfigPath,
126
585
  }, nil
127
586
  }
128
587
 
@@ -140,7 +599,7 @@ func (state linkedPluginState) sourcePreamble() (string, error) {
140
599
  if !ok {
141
600
  continue
142
601
  }
143
- text, err := preamble.SourcePreamble(state.context(entry))
602
+ text, err := preamble.SourcePreamble(state.contextAt(index, entry))
144
603
  if err != nil {
145
604
  return "", err
146
605
  }
@@ -161,7 +620,7 @@ func (state linkedPluginState) apply(prog *Program) error {
161
620
  if !ok {
162
621
  continue
163
622
  }
164
- if err := transform.ApplyProgram(prog, state.context(entry)); err != nil {
623
+ if err := transform.ApplyProgram(prog, state.contextAt(index, entry)); err != nil {
165
624
  return err
166
625
  }
167
626
  }
@@ -195,7 +654,7 @@ func (state linkedPluginState) emitTransforms() ([]PluginTransform, error) {
195
654
  if !ok {
196
655
  continue
197
656
  }
198
- transform, err := emitter.EmitTransform(state.context(entry))
657
+ transform, err := emitter.EmitTransform(state.contextAt(index, entry))
199
658
  if err != nil {
200
659
  return nil, err
201
660
  }
@@ -216,11 +675,43 @@ func registeredPlugin(index int) (any, bool) {
216
675
  return pluginRegistry[index], true
217
676
  }
218
677
 
219
- // context builds the PluginContext the driver passes to each plugin hook.
220
- func (state linkedPluginState) context(entry PluginEntry) PluginContext {
678
+ // contextAt builds the PluginContext the driver passes to one plugin hook.
679
+ //
680
+ // Host-input observations are scoped per hook, because a hash one hook reported
681
+ // cannot prove another hook's dependency on the same path. Dependency
682
+ // declarations are scoped per plugin instead: completeness is a claim about
683
+ // everything one plugin contributes to a file, so the claims of its preamble
684
+ // and program hooks are the same plugin's claim and must not be attributed
685
+ // separately.
686
+ func (state linkedPluginState) contextAt(index int, entry PluginEntry) PluginContext {
687
+ inputs := state.inputs.newScope()
688
+ declaration := state.declarations.forPlugin(index)
221
689
  return PluginContext{
222
- Cwd: state.cwd,
223
- Entry: entry,
224
- Tsconfig: state.tsconfig,
690
+ Cwd: state.cwd,
691
+ Entry: entry,
692
+ Tsconfig: state.tsconfig,
693
+ reportHostInput: inputs.add,
694
+ reportHostInputHash: inputs.addHash,
695
+ reportHostInputHashUnknown: inputs.invalidateHash,
696
+ reportHostInputRealpath: inputs.addRealpath,
697
+ reportHostInputRealpathUnknown: inputs.invalidateRealpath,
698
+ reportFileDependency: declaration.addDependency,
699
+ reportFileDependencyRejected: declaration.rejectDependency,
700
+ reportFileComplete: declaration.addComplete,
701
+ reportEveryFileComplete: declaration.completeEveryFile,
225
702
  }
226
703
  }
704
+
705
+ // hostInputs returns the exact native configuration inputs reported in this
706
+ // generation.
707
+ func (state linkedPluginState) hostInputs() []string {
708
+ return state.inputs.list()
709
+ }
710
+
711
+ func (state linkedPluginState) hostInputHashes() map[string]*string {
712
+ return state.inputs.hashList()
713
+ }
714
+
715
+ func (state linkedPluginState) hostInputRealpaths() map[string]*string {
716
+ return state.inputs.realpathList()
717
+ }
package/driver/program.go CHANGED
@@ -197,6 +197,7 @@ type Program struct {
197
197
  checkerRelease func()
198
198
  Host shimcompiler.CompilerHost
199
199
  FS vfs.FS
200
+ inputObserver *inputObservationFS
200
201
  SourcePreamble string
201
202
  plugins linkedPluginState
202
203
  pluginsApplied bool
@@ -263,6 +264,23 @@ func ParseTSConfig(fs vfs.FS, cwd, tsconfigPath string, host shimcompiler.Compil
263
264
  return parsed, nil, nil
264
265
  }
265
266
 
267
+ // resolveTsgoArgs picks the forwarded tsgo argv for this load: the caller's
268
+ // explicit LoadProgramOptions.TsgoArgs when it has one, otherwise whatever the
269
+ // launcher published in TsgoArgsEnv.
270
+ //
271
+ // The explicit value wins so a host that declares its own `--tsgo-args` flag
272
+ // (cmd/ttsc, the utility host, @ttsc/lint) keeps deciding for itself, and an
273
+ // embedder that deliberately passes an empty argv is not overridden by an
274
+ // environment variable an ancestor ttsc process happened to set. The fallback
275
+ // is what carries the payload into a third-party sidecar whose flag set does
276
+ // not declare `--tsgo-args` at all — see TsgoArgsEnv and issue #1188.
277
+ func resolveTsgoArgs(explicit []string) ([]string, error) {
278
+ if len(explicit) > 0 {
279
+ return explicit, nil
280
+ }
281
+ return TsgoArgsFromEnv()
282
+ }
283
+
266
284
  // parseTsgoArgs runs forwarded tsgo CLI flags through TypeScript-Go's own
267
285
  // command-line parser, yielding a CompilerOptions overlay ParseTSConfig merges
268
286
  // over the tsconfig. This is how a plugin build — which constructs its Program
@@ -357,6 +375,8 @@ func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (*Program
357
375
  if fs == nil {
358
376
  fs = DefaultFS()
359
377
  }
378
+ inputObserver := newInputObservationFS(fs)
379
+ fs = inputObserver
360
380
  if options.SourcePreamble != "" {
361
381
  fs = sourcePreambleFS{
362
382
  FS: fs,
@@ -365,7 +385,11 @@ func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (*Program
365
385
  }
366
386
  host := DefaultHost(cwd, fs)
367
387
 
368
- cliOptions, cliDiags, err := parseTsgoArgs(options.TsgoArgs, host)
388
+ tsgoArgs, err := resolveTsgoArgs(options.TsgoArgs)
389
+ if err != nil {
390
+ return nil, nil, err
391
+ }
392
+ cliOptions, cliDiags, err := parseTsgoArgs(tsgoArgs, host)
369
393
  if err != nil {
370
394
  return nil, nil, err
371
395
  }
@@ -401,6 +425,7 @@ func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (*Program
401
425
  checkerRelease: done,
402
426
  Host: host,
403
427
  FS: fs,
428
+ inputObserver: inputObserver,
404
429
  SourcePreamble: options.SourcePreamble,
405
430
  }
406
431
  prog.plugins = pluginState
@@ -563,6 +588,34 @@ func (p *Program) HasLinkedProgramPlugins() bool {
563
588
  return p != nil && p.plugins.hasProgramPlugins()
564
589
  }
565
590
 
591
+ // PluginHostInputs returns the generation-wide native configuration files
592
+ // reported by linked plugins while this Program was loaded or transformed.
593
+ func (p *Program) PluginHostInputs() []string {
594
+ if p == nil {
595
+ return nil
596
+ }
597
+ return p.plugins.hostInputs()
598
+ }
599
+
600
+ // PluginHostInputHashes returns evaluation-time fingerprints for the subset of
601
+ // native host inputs whose exact state plugins reported without conflict.
602
+ func (p *Program) PluginHostInputHashes() map[string]*string {
603
+ if p == nil {
604
+ return nil
605
+ }
606
+ return p.plugins.hostInputHashes()
607
+ }
608
+
609
+ // PluginHostInputRealpaths returns evaluation-time physical identities for the
610
+ // subset of native host inputs whose symlink or junction target was observed
611
+ // without conflict.
612
+ func (p *Program) PluginHostInputRealpaths() map[string]*string {
613
+ if p == nil {
614
+ return nil
615
+ }
616
+ return p.plugins.hostInputRealpaths()
617
+ }
618
+
566
619
  // Diagnostics returns project diagnostics that must block compilation or
567
620
  // runtime execution before any JavaScript is emitted or evaluated.
568
621
  func (p *Program) Diagnostics() []Diagnostic {