ttsc 0.26.2 → 0.27.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 (111) hide show
  1. package/README.md +9 -0
  2. package/cmd/ttsc/api_transform.go +17 -3
  3. package/cmd/ttscgraph/auxiliary_identity_state_tracks_retargets_not_source_contents_test.go +85 -0
  4. package/cmd/ttscgraph/disk_realpath_other.go +16 -0
  5. package/cmd/ttscgraph/disk_realpath_windows.go +56 -0
  6. package/cmd/ttscgraph/serve.go +104 -35
  7. package/driver/emit_plugin.go +20 -3
  8. package/driver/graph.go +64 -11
  9. package/driver/input_observation.go +273 -0
  10. package/driver/input_observation_test.go +149 -0
  11. package/driver/plugins.go +384 -4
  12. package/driver/program.go +54 -1
  13. package/driver/resolution_candidates.go +13 -5
  14. package/driver/rewrite.go +45 -2
  15. package/lib/TtscCompiler.d.ts +12 -11
  16. package/lib/TtscCompiler.js +44 -29
  17. package/lib/TtscCompiler.js.map +1 -1
  18. package/lib/compiler/internal/captureProcessOutput.js +2 -2
  19. package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
  20. package/lib/compiler/internal/compileProjectInMemory.js +4 -3
  21. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  22. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -0
  23. package/lib/compiler/internal/resolveEmittedJavaScript.js +3 -0
  24. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  25. package/lib/compiler/internal/runBuild.d.ts +1 -1
  26. package/lib/compiler/internal/runBuild.js +80 -34
  27. package/lib/compiler/internal/runBuild.js.map +1 -1
  28. package/lib/compiler/internal/runSingleFileEmit.js +2 -2
  29. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  30. package/lib/compiler/internal/sharedHostHelpers.d.ts +28 -0
  31. package/lib/compiler/internal/sharedHostHelpers.js +39 -0
  32. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  33. package/lib/compiler/internal/startResidentTransform.js +12 -4
  34. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  35. package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -0
  36. package/lib/compiler/internal/transformProjectInMemory.js +239 -35
  37. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  38. package/lib/flags/schema.js +12 -5
  39. package/lib/flags/schema.js.map +1 -1
  40. package/lib/internal/createCanonicalTempDirectory.d.ts +17 -0
  41. package/lib/internal/createCanonicalTempDirectory.js +42 -0
  42. package/lib/internal/createCanonicalTempDirectory.js.map +1 -0
  43. package/lib/internal/projectInputPathIdentity.d.ts +3 -0
  44. package/lib/internal/projectInputPathIdentity.js +11 -5
  45. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  46. package/lib/internal/resolveNodeBinary.d.ts +14 -0
  47. package/lib/internal/resolveNodeBinary.js +150 -0
  48. package/lib/internal/resolveNodeBinary.js.map +1 -0
  49. package/lib/internal/resolveSafeCacheCleanupTargets.d.ts +15 -0
  50. package/lib/internal/resolveSafeCacheCleanupTargets.js +61 -0
  51. package/lib/internal/resolveSafeCacheCleanupTargets.js.map +1 -0
  52. package/lib/launcher/internal/prepareExecution.d.ts +3 -0
  53. package/lib/launcher/internal/prepareExecution.js +42 -14
  54. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  55. package/lib/launcher/internal/runTtsc.js +25 -13
  56. package/lib/launcher/internal/runTtsc.js.map +1 -1
  57. package/lib/launcher/internal/runTtscserver.js +18 -3
  58. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  59. package/lib/launcher/internal/runTtsx.js +2 -0
  60. package/lib/launcher/internal/runTtsx.js.map +1 -1
  61. package/lib/launcher/internal/runtimeHooks.d.ts +37 -1
  62. package/lib/launcher/internal/runtimeHooks.js +653 -62
  63. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  64. package/lib/plugin/internal/buildSourcePlugin.d.ts +62 -8
  65. package/lib/plugin/internal/buildSourcePlugin.js +754 -72
  66. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  67. package/lib/plugin/internal/loadProjectPlugins.d.ts +17 -5
  68. package/lib/plugin/internal/loadProjectPlugins.js +1339 -77
  69. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  70. package/lib/register.d.ts +1 -0
  71. package/lib/register.js +46 -0
  72. package/lib/register.js.map +1 -0
  73. package/lib/structures/ITtscCompilerContext.d.ts +6 -3
  74. package/lib/structures/ITtscCompilerTransformation.d.ts +31 -0
  75. package/lib/structures/ITtscPlugin.d.ts +33 -0
  76. package/package.json +16 -9
  77. package/shim/checker/go.mod +9 -0
  78. package/shim/checker/go.sum +22 -0
  79. package/shim/checker/shim.go +75 -0
  80. package/shim/compiler/incremental.go +45 -8
  81. package/src/TtscCompiler.ts +55 -33
  82. package/src/compiler/internal/captureProcessOutput.ts +3 -2
  83. package/src/compiler/internal/compileProjectInMemory.ts +4 -3
  84. package/src/compiler/internal/resolveEmittedJavaScript.ts +6 -0
  85. package/src/compiler/internal/runBuild.ts +102 -39
  86. package/src/compiler/internal/runSingleFileEmit.ts +2 -2
  87. package/src/compiler/internal/sharedHostHelpers.ts +44 -0
  88. package/src/compiler/internal/startResidentTransform.ts +12 -3
  89. package/src/compiler/internal/transformProjectInMemory.ts +410 -39
  90. package/src/flags/schema.ts +12 -5
  91. package/src/internal/createCanonicalTempDirectory.ts +53 -0
  92. package/src/internal/projectInputPathIdentity.ts +17 -4
  93. package/src/internal/resolveNodeBinary.ts +173 -0
  94. package/src/internal/resolveSafeCacheCleanupTargets.ts +85 -0
  95. package/src/launcher/internal/prepareExecution.ts +50 -14
  96. package/src/launcher/internal/runTtsc.ts +24 -13
  97. package/src/launcher/internal/runTtscserver.ts +18 -3
  98. package/src/launcher/internal/runTtsx.ts +2 -0
  99. package/src/launcher/internal/runtimeHooks.ts +823 -66
  100. package/src/plugin/internal/buildSourcePlugin.ts +984 -79
  101. package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
  102. package/src/register.ts +47 -0
  103. package/src/structures/ITtscCompilerContext.ts +6 -3
  104. package/src/structures/ITtscCompilerTransformation.ts +39 -0
  105. package/src/structures/ITtscPlugin.ts +36 -0
  106. package/tools/shim_audit/baseline.json +0 -1
  107. package/utility/host.go +9 -3
  108. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +0 -5
  109. package/lib/internal/assertSafeExplicitCacheDirectory.js +0 -26
  110. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +0 -1
  111. 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,90 @@ 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
+ }
110
+
111
+ // ReportHostInputHash declares the exact file state consumed by a native
112
+ // plugin. hash is a lowercase SHA-256 digest for an observed file and nil for
113
+ // a missing candidate. Conflicting observations are retained as host inputs
114
+ // but omitted from PluginHostInputHashes, forcing persistent adapters to
115
+ // decline narrow reuse without failing the transform.
116
+ func (ctx PluginContext) ReportHostInputHash(file string, hash *string) {
117
+ if ctx.reportHostInputHash == nil || strings.TrimSpace(file) == "" {
118
+ return
119
+ }
120
+ if !filepath.IsAbs(file) {
121
+ file = filepath.Join(ctx.Cwd, file)
122
+ }
123
+ file = filepath.Clean(file)
124
+ if hash != nil && !isLowerSHA256(*hash) {
125
+ if ctx.reportHostInputHashUnknown != nil {
126
+ ctx.reportHostInputHashUnknown(file)
127
+ } else if ctx.reportHostInput != nil {
128
+ ctx.reportHostInput(file)
129
+ }
130
+ return
131
+ }
132
+ ctx.reportHostInputHash(file, hash)
133
+ }
134
+
135
+ // ReportHostInputRealpath declares the physical path resolved while a native
136
+ // plugin consumed file. realpath is absolute for an observed path and nil for
137
+ // a missing candidate. Conflicting observations remain host inputs but are
138
+ // omitted from PluginHostInputRealpaths so adapters cannot attach an earlier
139
+ // result to a retargeted symlink or junction.
140
+ func (ctx PluginContext) ReportHostInputRealpath(file string, realpath *string) {
141
+ if ctx.reportHostInputRealpath == nil || strings.TrimSpace(file) == "" {
142
+ return
143
+ }
144
+ if !filepath.IsAbs(file) {
145
+ file = filepath.Join(ctx.Cwd, file)
146
+ }
147
+ file = filepath.Clean(file)
148
+ if realpath != nil {
149
+ if strings.TrimSpace(*realpath) == "" || !filepath.IsAbs(*realpath) {
150
+ if ctx.reportHostInputRealpathUnknown != nil {
151
+ ctx.reportHostInputRealpathUnknown(file)
152
+ } else if ctx.reportHostInput != nil {
153
+ ctx.reportHostInput(file)
154
+ }
155
+ return
156
+ }
157
+ resolved := filepath.Clean(*realpath)
158
+ realpath = &resolved
159
+ }
160
+ ctx.reportHostInputRealpath(file, realpath)
161
+ }
162
+
163
+ func isLowerSHA256(value string) bool {
164
+ if len(value) != 64 {
165
+ return false
166
+ }
167
+ for _, char := range value {
168
+ if (char < '0' || char > '9') && (char < 'a' || char > 'f') {
169
+ return false
170
+ }
171
+ }
172
+ return true
173
+ }
174
+
175
+ // ReportHostInput declares an absolute file whose content or presence was
176
+ // consumed while the native plugin evaluated configuration. Native transform
177
+ // envelopes expose the generation-wide union so persistent hosts can invalidate
178
+ // without re-evaluating plugin config on the JavaScript side.
179
+ func (ctx PluginContext) ReportHostInput(file string) {
180
+ if ctx.reportHostInput == nil || strings.TrimSpace(file) == "" {
181
+ return
182
+ }
183
+ if !filepath.IsAbs(file) {
184
+ file = filepath.Join(ctx.Cwd, file)
185
+ }
186
+ ctx.reportHostInput(filepath.Clean(file))
63
187
  }
64
188
 
65
189
  // SourcePreamblePlugin can inject source text before TypeScript-Go parses the
@@ -92,9 +216,244 @@ type EmitTransformPlugin interface {
92
216
  type linkedPluginState struct {
93
217
  cwd string
94
218
  entries []PluginEntry
219
+ inputs *pluginHostInputScopes
95
220
  tsconfig string
96
221
  }
97
222
 
223
+ // pluginHostInputScopes keeps one observation set per plugin hook. A hash from
224
+ // one hook cannot prove another hook's same-file dependency when that hook only
225
+ // reported the path, while ReportHostInput followed by ReportHostInputHash in
226
+ // one hook remains one complete observation.
227
+ type pluginHostInputScopes struct {
228
+ mu sync.Mutex
229
+ scopes []*pluginHostInputs
230
+ }
231
+
232
+ type pluginHostInputs struct {
233
+ files map[string]struct{}
234
+ hashes map[string]pluginHostInputHash
235
+ realpaths map[string]pluginHostInputHash
236
+ mu sync.Mutex
237
+ }
238
+
239
+ type pluginHostInputHash struct {
240
+ hash *string
241
+ known bool
242
+ }
243
+
244
+ func newPluginHostInputScopes() *pluginHostInputScopes {
245
+ return &pluginHostInputScopes{}
246
+ }
247
+
248
+ func newPluginHostInputs() *pluginHostInputs {
249
+ return &pluginHostInputs{
250
+ files: map[string]struct{}{},
251
+ hashes: map[string]pluginHostInputHash{},
252
+ realpaths: map[string]pluginHostInputHash{},
253
+ }
254
+ }
255
+
256
+ func (inputs *pluginHostInputScopes) newScope() *pluginHostInputs {
257
+ scope := newPluginHostInputs()
258
+ if inputs == nil {
259
+ return scope
260
+ }
261
+ inputs.mu.Lock()
262
+ inputs.scopes = append(inputs.scopes, scope)
263
+ inputs.mu.Unlock()
264
+ return scope
265
+ }
266
+
267
+ func (inputs *pluginHostInputScopes) snapshot() []*pluginHostInputs {
268
+ if inputs == nil {
269
+ return nil
270
+ }
271
+ inputs.mu.Lock()
272
+ defer inputs.mu.Unlock()
273
+ return append([]*pluginHostInputs(nil), inputs.scopes...)
274
+ }
275
+
276
+ func (inputs *pluginHostInputs) addHash(file string, hash *string) {
277
+ if inputs == nil {
278
+ return
279
+ }
280
+ inputs.mu.Lock()
281
+ defer inputs.mu.Unlock()
282
+ inputs.files[file] = struct{}{}
283
+ previous, exists := inputs.hashes[file]
284
+ if !exists {
285
+ inputs.hashes[file] = pluginHostInputHash{hash: cloneStringPointer(hash), known: true}
286
+ return
287
+ }
288
+ if !previous.known || !sameStringPointer(previous.hash, hash) {
289
+ inputs.hashes[file] = pluginHostInputHash{known: false}
290
+ }
291
+ }
292
+
293
+ func (inputs *pluginHostInputs) invalidateHash(file string) {
294
+ if inputs == nil {
295
+ return
296
+ }
297
+ inputs.mu.Lock()
298
+ defer inputs.mu.Unlock()
299
+ inputs.files[file] = struct{}{}
300
+ inputs.hashes[file] = pluginHostInputHash{known: false}
301
+ }
302
+
303
+ func (inputs *pluginHostInputs) addRealpath(file string, realpath *string) {
304
+ if inputs == nil {
305
+ return
306
+ }
307
+ inputs.mu.Lock()
308
+ defer inputs.mu.Unlock()
309
+ inputs.files[file] = struct{}{}
310
+ previous, exists := inputs.realpaths[file]
311
+ if !exists {
312
+ inputs.realpaths[file] = pluginHostInputHash{hash: cloneStringPointer(realpath), known: true}
313
+ return
314
+ }
315
+ if !previous.known || !sameStringPointer(previous.hash, realpath) {
316
+ inputs.realpaths[file] = pluginHostInputHash{known: false}
317
+ }
318
+ }
319
+
320
+ func (inputs *pluginHostInputs) invalidateRealpath(file string) {
321
+ if inputs == nil {
322
+ return
323
+ }
324
+ inputs.mu.Lock()
325
+ defer inputs.mu.Unlock()
326
+ inputs.files[file] = struct{}{}
327
+ inputs.realpaths[file] = pluginHostInputHash{known: false}
328
+ }
329
+
330
+ func cloneStringPointer(value *string) *string {
331
+ if value == nil {
332
+ return nil
333
+ }
334
+ cloned := *value
335
+ return &cloned
336
+ }
337
+
338
+ func sameStringPointer(left, right *string) bool {
339
+ if left == nil || right == nil {
340
+ return left == nil && right == nil
341
+ }
342
+ return *left == *right
343
+ }
344
+
345
+ func (inputs *pluginHostInputs) add(file string) {
346
+ if inputs == nil {
347
+ return
348
+ }
349
+ inputs.mu.Lock()
350
+ inputs.files[file] = struct{}{}
351
+ inputs.mu.Unlock()
352
+ }
353
+
354
+ func (inputs *pluginHostInputs) snapshot() (map[string]struct{}, map[string]pluginHostInputHash, map[string]pluginHostInputHash) {
355
+ if inputs == nil {
356
+ return nil, nil, nil
357
+ }
358
+ inputs.mu.Lock()
359
+ defer inputs.mu.Unlock()
360
+ files := make(map[string]struct{}, len(inputs.files))
361
+ for file := range inputs.files {
362
+ files[file] = struct{}{}
363
+ }
364
+ hashes := make(map[string]pluginHostInputHash, len(inputs.hashes))
365
+ for file, observation := range inputs.hashes {
366
+ hashes[file] = pluginHostInputHash{
367
+ hash: cloneStringPointer(observation.hash),
368
+ known: observation.known,
369
+ }
370
+ }
371
+ realpaths := make(map[string]pluginHostInputHash, len(inputs.realpaths))
372
+ for file, observation := range inputs.realpaths {
373
+ realpaths[file] = pluginHostInputHash{
374
+ hash: cloneStringPointer(observation.hash),
375
+ known: observation.known,
376
+ }
377
+ }
378
+ return files, hashes, realpaths
379
+ }
380
+
381
+ func (inputs *pluginHostInputScopes) list() []string {
382
+ union := map[string]struct{}{}
383
+ for _, scope := range inputs.snapshot() {
384
+ files, _, _ := scope.snapshot()
385
+ for file := range files {
386
+ union[file] = struct{}{}
387
+ }
388
+ }
389
+ files := make([]string, 0, len(union))
390
+ for file := range union {
391
+ files = append(files, file)
392
+ }
393
+ sort.Strings(files)
394
+ return files
395
+ }
396
+
397
+ func (inputs *pluginHostInputScopes) hashList() map[string]*string {
398
+ combined := map[string]pluginHostInputHash{}
399
+ for _, scope := range inputs.snapshot() {
400
+ files, hashes, _ := scope.snapshot()
401
+ for file := range files {
402
+ observation, exists := hashes[file]
403
+ if !exists || !observation.known {
404
+ combined[file] = pluginHostInputHash{known: false}
405
+ continue
406
+ }
407
+ previous, exists := combined[file]
408
+ if !exists {
409
+ combined[file] = observation
410
+ } else if !previous.known || !sameStringPointer(previous.hash, observation.hash) {
411
+ combined[file] = pluginHostInputHash{known: false}
412
+ }
413
+ }
414
+ }
415
+ hashes := map[string]*string{}
416
+ for file, observation := range combined {
417
+ if observation.known {
418
+ hashes[file] = cloneStringPointer(observation.hash)
419
+ }
420
+ }
421
+ if len(hashes) == 0 {
422
+ return nil
423
+ }
424
+ return hashes
425
+ }
426
+
427
+ func (inputs *pluginHostInputScopes) realpathList() map[string]*string {
428
+ combined := map[string]pluginHostInputHash{}
429
+ for _, scope := range inputs.snapshot() {
430
+ files, _, realpaths := scope.snapshot()
431
+ for file := range files {
432
+ observation, exists := realpaths[file]
433
+ if !exists || !observation.known {
434
+ combined[file] = pluginHostInputHash{known: false}
435
+ continue
436
+ }
437
+ previous, exists := combined[file]
438
+ if !exists {
439
+ combined[file] = observation
440
+ } else if !previous.known || !sameStringPointer(previous.hash, observation.hash) {
441
+ combined[file] = pluginHostInputHash{known: false}
442
+ }
443
+ }
444
+ }
445
+ realpaths := map[string]*string{}
446
+ for file, observation := range combined {
447
+ if observation.known {
448
+ realpaths[file] = cloneStringPointer(observation.hash)
449
+ }
450
+ }
451
+ if len(realpaths) == 0 {
452
+ return nil
453
+ }
454
+ return realpaths
455
+ }
456
+
98
457
  var pluginRegistry []any
99
458
 
100
459
  // RegisterPlugin registers a driver-level plugin implementation. Linked Go
@@ -113,7 +472,7 @@ func RegisterPlugin(plugin any) {
113
472
  func loadLinkedPluginState(cwd, tsconfigPath string) (linkedPluginState, error) {
114
473
  input := strings.TrimSpace(os.Getenv(LinkedPluginsEnv))
115
474
  if input == "" {
116
- return linkedPluginState{cwd: cwd, tsconfig: tsconfigPath}, nil
475
+ return linkedPluginState{cwd: cwd, inputs: newPluginHostInputScopes(), tsconfig: tsconfigPath}, nil
117
476
  }
118
477
  var entries []PluginEntry
119
478
  if err := json.Unmarshal([]byte(input), &entries); err != nil {
@@ -122,6 +481,7 @@ func loadLinkedPluginState(cwd, tsconfigPath string) (linkedPluginState, error)
122
481
  return linkedPluginState{
123
482
  cwd: cwd,
124
483
  entries: entries,
484
+ inputs: newPluginHostInputScopes(),
125
485
  tsconfig: tsconfigPath,
126
486
  }, nil
127
487
  }
@@ -218,9 +578,29 @@ func registeredPlugin(index int) (any, bool) {
218
578
 
219
579
  // context builds the PluginContext the driver passes to each plugin hook.
220
580
  func (state linkedPluginState) context(entry PluginEntry) PluginContext {
581
+ inputs := state.inputs.newScope()
221
582
  return PluginContext{
222
- Cwd: state.cwd,
223
- Entry: entry,
224
- Tsconfig: state.tsconfig,
583
+ Cwd: state.cwd,
584
+ Entry: entry,
585
+ Tsconfig: state.tsconfig,
586
+ reportHostInput: inputs.add,
587
+ reportHostInputHash: inputs.addHash,
588
+ reportHostInputHashUnknown: inputs.invalidateHash,
589
+ reportHostInputRealpath: inputs.addRealpath,
590
+ reportHostInputRealpathUnknown: inputs.invalidateRealpath,
225
591
  }
226
592
  }
593
+
594
+ // hostInputs returns the exact native configuration inputs reported in this
595
+ // generation.
596
+ func (state linkedPluginState) hostInputs() []string {
597
+ return state.inputs.list()
598
+ }
599
+
600
+ func (state linkedPluginState) hostInputHashes() map[string]*string {
601
+ return state.inputs.hashList()
602
+ }
603
+
604
+ func (state linkedPluginState) hostInputRealpaths() map[string]*string {
605
+ return state.inputs.realpathList()
606
+ }
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 {
@@ -23,9 +23,10 @@ type ModuleResolutionContext struct {
23
23
 
24
24
  // SupersedingModuleCandidates returns, per source-file envelope key, the
25
25
  // resolution candidates strictly ahead of each module target the loaded
26
- // program selected. A missing member can therefore become a freshness input
27
- // without making the whole resolution search (including lower-priority paths)
28
- // an invalidation input.
26
+ // program selected. When the selected target is reported by physical path, it
27
+ // also retains that candidate's lexical spelling. A missing member or a link
28
+ // retarget can therefore become a freshness input without making the whole
29
+ // resolution search (including lower-priority paths) an invalidation input.
29
30
  //
30
31
  // Candidate enumeration is host-owned. It mirrors the resident graph session's
31
32
  // speculation for relative modules, paths, rootDirs, package imports/exports,
@@ -75,7 +76,10 @@ func SupersedingModuleCandidates(prog *Program, cwd string) map[string][]string
75
76
  }
76
77
 
77
78
  // ModuleResolutionPredecessors returns the paths in the host-owned candidate
78
- // search that precede resolvedFileName. An unresolved specifier keeps the full
79
+ // search that precede resolvedFileName. When the compiler reports the winner by
80
+ // physical realpath, the result also retains that selected candidate's lexical
81
+ // spelling: retargeting its symlink/junction can change resolution without
82
+ // changing the old or new target bytes. An unresolved specifier keeps the full
79
83
  // candidate list through ModuleResolutionCandidates; a resolved specifier must
80
84
  // not track a lower-priority path because creating it cannot alter resolution.
81
85
  func ModuleResolutionPredecessors(
@@ -101,7 +105,11 @@ func ModuleResolutionPredecessors(
101
105
  // spellings already agree pays nothing for it.
102
106
  for index, candidate := range candidates {
103
107
  if sameExistingPath(candidate, resolvedFileName) {
104
- return compactStringsInOrder(candidates[:index])
108
+ end := index
109
+ if !sameCandidatePath(candidate, resolvedFileName, caseSensitive) {
110
+ end++
111
+ }
112
+ return compactStringsInOrder(candidates[:end])
105
113
  }
106
114
  }
107
115
  // Never widen freshness to candidates whose precedence relative to the
package/driver/rewrite.go CHANGED
@@ -129,7 +129,7 @@ func (p *Program) EmitAllRaw(writeFile shimcompiler.WriteFile) (*shimcompiler.Em
129
129
  }
130
130
  return DefaultWriteFile(fileName, text)
131
131
  }
132
- result := p.TSProgram.Emit(context.Background(), shimcompiler.EmitOptions{
132
+ result := p.emitProgram(shimcompiler.EmitOptions{
133
133
  WriteFile: wf,
134
134
  })
135
135
  return result, p.convertProgramDiagnostics(result.Diagnostics), nil
@@ -197,13 +197,56 @@ func (p *Program) emit(rs *RewriteSet, target *ast.SourceFile, writeFile shimcom
197
197
  return DefaultWriteFile(fileName, patched)
198
198
  }
199
199
 
200
- result := p.TSProgram.Emit(context.Background(), shimcompiler.EmitOptions{
200
+ result := p.emitProgram(shimcompiler.EmitOptions{
201
201
  TargetSourceFile: target,
202
202
  WriteFile: wf,
203
203
  })
204
204
  return result, p.convertProgramDiagnostics(result.Diagnostics), nil
205
205
  }
206
206
 
207
+ // emitProgram runs one whole-program or single-file emit, taking tsgo's
208
+ // incremental lane when the resolved compiler options ask for build
209
+ // information.
210
+ //
211
+ // tsgo's own CLI branches the same way — `performIncrementalCompilation` vs
212
+ // `performCompilation`, on `CompilerOptions.IsIncremental()` — but it branches
213
+ // in `internal/execute`, which a host constructing its Program in-process never
214
+ // enters. ttsc always took the plain branch, so `incremental`, `composite`, and
215
+ // `tsBuildInfoFile` parsed, resolved, and then vanished: a plugin-carrying
216
+ // project emitted its JavaScript and no `.tsbuildinfo` at all (issue #1188).
217
+ // `driver/emit_containment.go` had already exempted `.tsbuildinfo` from the
218
+ // outDir guard for a write that could not happen.
219
+ //
220
+ // A single-file emit stays on the plain lane. Build information describes a
221
+ // whole program, and tsgo's incremental program returns early on a
222
+ // `TargetSourceFile` request without writing any, so routing it there would
223
+ // only add a snapshot computation nothing reads.
224
+ func (p *Program) emitProgram(options shimcompiler.EmitOptions) *shimcompiler.EmitResult {
225
+ ctx := context.Background()
226
+ if options.TargetSourceFile == nil && p.emitsBuildInfo() {
227
+ return shimcompiler.EmitFreshWithBuildInfo(ctx, p.TSProgram, options)
228
+ }
229
+ return p.TSProgram.Emit(ctx, options)
230
+ }
231
+
232
+ // emitsBuildInfo reports whether this program's resolved options ask tsgo to
233
+ // write build information.
234
+ //
235
+ // The question is delegated to `CompilerOptions.IsIncremental`, the same
236
+ // predicate tsgo's own CLI branches on, rather than restated as
237
+ // `Incremental || Composite` here: a second copy of the rule is a second thing
238
+ // to keep in step with upstream.
239
+ func (p *Program) emitsBuildInfo() bool {
240
+ if p == nil || p.TSProgram == nil {
241
+ return false
242
+ }
243
+ options := p.TSProgram.Options()
244
+ if options == nil {
245
+ return false
246
+ }
247
+ return options.IsIncremental()
248
+ }
249
+
207
250
  // DefaultWriteFile is the default disk writer used when EmitAll's caller does not
208
251
  // supply a custom WriteFile callback.
209
252
  func DefaultWriteFile(fileName, text string) error {
@@ -48,15 +48,16 @@ export declare class TtscCompiler {
48
48
  /**
49
49
  * Remove compiled cache artifacts for this compiler instance.
50
50
  *
51
- * Removes the resolved cache root (which holds both the plugin binaries and,
52
- * when ttsc-owned, the Go build cache), a ttsc-owned Go build cache that
53
- * lives outside that root (`TTSC_GO_CACHE_DIR`), and the two legacy
54
- * project-local caches. A user-provided `GOCACHE` is never removed. The cache
51
+ * Removes an explicit `cacheDir` wholesale. Otherwise removes the plugin
52
+ * binary subdirectory, a safely identified ttsc-owned Go build cache
53
+ * (including an external `TTSC_GO_CACHE_DIR`), and the two legacy
54
+ * project-local caches. A user-provided `GOCACHE` is never removed. Every
55
+ * resolved deletion target is validated before the first removal. The cache
55
56
  * location comes from this instance's `cacheDir` and environment
56
57
  * (`TTSC_CACHE_DIR` / `TTSC_GO_CACHE_DIR`), defaulting to
57
- * `<workspaceRoot>/node_modules/.cache/ttsc`. An explicit cache directory
58
- * that equals or contains the project, or names a filesystem root, is
59
- * rejected before any directory is removed.
58
+ * `<workspaceRoot>/node_modules/.cache/ttsc`. Any resolved cache target that
59
+ * equals or contains the project, or names a filesystem root, is rejected
60
+ * before any directory is removed.
60
61
  *
61
62
  * @returns Cache directories that were removed.
62
63
  */
@@ -107,10 +108,10 @@ export declare class TtscCompiler {
107
108
  * The effective environment for this instance's source-plugin builds and
108
109
  * clean targets: `context.env` merged over `process.env`, matching the
109
110
  * documented {@link ITtscCompilerContext.env} contract that child compiler,
110
- * native-plugin, and native-host processes already receive. Returned as a
111
- * fresh object so callers never mutate the shared `process.env`; when no
112
- * `context.env` was supplied this is a plain copy of `process.env`, so CLI /
113
- * default behavior is unchanged.
111
+ * native-plugin, native-host, and isolated descriptor processes already
112
+ * receive. Returned as a fresh object so callers never mutate the shared
113
+ * `process.env`; when no `context.env` was supplied this is a plain copy of
114
+ * `process.env`, so CLI / default behavior is unchanged.
114
115
  */
115
116
  private resolveEffectiveEnv;
116
117
  }