ttsc 0.26.1 → 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 (119) hide show
  1. package/README.md +10 -1
  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/diagnostics_preamble.go +284 -0
  8. package/driver/emit_plugin.go +71 -19
  9. package/driver/graph.go +64 -11
  10. package/driver/input_observation.go +273 -0
  11. package/driver/input_observation_test.go +149 -0
  12. package/driver/plugins.go +384 -4
  13. package/driver/program.go +81 -15
  14. package/driver/resolution_candidates.go +13 -5
  15. package/driver/rewrite.go +47 -4
  16. package/internal/graph/diagnostics.go +4 -3
  17. package/internal/graph/provenance.go +10 -1
  18. package/lib/TtscCompiler.d.ts +12 -11
  19. package/lib/TtscCompiler.js +44 -29
  20. package/lib/TtscCompiler.js.map +1 -1
  21. package/lib/compiler/internal/captureProcessOutput.js +2 -2
  22. package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
  23. package/lib/compiler/internal/compileProjectInMemory.js +11 -3
  24. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  25. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -0
  26. package/lib/compiler/internal/resolveEmittedJavaScript.js +3 -0
  27. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  28. package/lib/compiler/internal/runBuild.d.ts +29 -1
  29. package/lib/compiler/internal/runBuild.js +117 -32
  30. package/lib/compiler/internal/runBuild.js.map +1 -1
  31. package/lib/compiler/internal/runSingleFileEmit.js +9 -2
  32. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  33. package/lib/compiler/internal/sharedHostHelpers.d.ts +28 -0
  34. package/lib/compiler/internal/sharedHostHelpers.js +39 -0
  35. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  36. package/lib/compiler/internal/startResidentTransform.js +12 -4
  37. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  38. package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -0
  39. package/lib/compiler/internal/transformProjectInMemory.js +239 -35
  40. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  41. package/lib/flags/schema.js +33 -5
  42. package/lib/flags/schema.js.map +1 -1
  43. package/lib/internal/createCanonicalTempDirectory.d.ts +17 -0
  44. package/lib/internal/createCanonicalTempDirectory.js +42 -0
  45. package/lib/internal/createCanonicalTempDirectory.js.map +1 -0
  46. package/lib/internal/projectInputPathIdentity.d.ts +3 -0
  47. package/lib/internal/projectInputPathIdentity.js +11 -5
  48. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  49. package/lib/internal/resolveNodeBinary.d.ts +14 -0
  50. package/lib/internal/resolveNodeBinary.js +150 -0
  51. package/lib/internal/resolveNodeBinary.js.map +1 -0
  52. package/lib/internal/resolveSafeCacheCleanupTargets.d.ts +15 -0
  53. package/lib/internal/resolveSafeCacheCleanupTargets.js +61 -0
  54. package/lib/internal/resolveSafeCacheCleanupTargets.js.map +1 -0
  55. package/lib/launcher/internal/assertNoSolutionBuild.d.ts +31 -0
  56. package/lib/launcher/internal/assertNoSolutionBuild.js +38 -0
  57. package/lib/launcher/internal/assertNoSolutionBuild.js.map +1 -0
  58. package/lib/launcher/internal/prepareExecution.d.ts +3 -0
  59. package/lib/launcher/internal/prepareExecution.js +73 -26
  60. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  61. package/lib/launcher/internal/runTtsc.js +27 -13
  62. package/lib/launcher/internal/runTtsc.js.map +1 -1
  63. package/lib/launcher/internal/runTtscserver.js +18 -3
  64. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  65. package/lib/launcher/internal/runTtsx.js +4 -0
  66. package/lib/launcher/internal/runTtsx.js.map +1 -1
  67. package/lib/launcher/internal/runtimeHooks.d.ts +37 -1
  68. package/lib/launcher/internal/runtimeHooks.js +660 -62
  69. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  70. package/lib/plugin/internal/buildSourcePlugin.d.ts +62 -8
  71. package/lib/plugin/internal/buildSourcePlugin.js +754 -72
  72. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  73. package/lib/plugin/internal/loadProjectPlugins.d.ts +17 -5
  74. package/lib/plugin/internal/loadProjectPlugins.js +1339 -77
  75. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  76. package/lib/register.d.ts +1 -0
  77. package/lib/register.js +46 -0
  78. package/lib/register.js.map +1 -0
  79. package/lib/structures/ITtscCompilerContext.d.ts +6 -3
  80. package/lib/structures/ITtscCompilerTransformation.d.ts +31 -0
  81. package/lib/structures/ITtscPlugin.d.ts +33 -0
  82. package/package.json +16 -9
  83. package/shim/checker/go.mod +9 -0
  84. package/shim/checker/go.sum +22 -0
  85. package/shim/checker/shim.go +75 -0
  86. package/shim/compiler/emit_sourcemap.go +56 -15
  87. package/shim/compiler/incremental.go +45 -8
  88. package/src/TtscCompiler.ts +55 -33
  89. package/src/compiler/internal/captureProcessOutput.ts +3 -2
  90. package/src/compiler/internal/compileProjectInMemory.ts +11 -3
  91. package/src/compiler/internal/resolveEmittedJavaScript.ts +6 -0
  92. package/src/compiler/internal/runBuild.ts +172 -36
  93. package/src/compiler/internal/runSingleFileEmit.ts +9 -2
  94. package/src/compiler/internal/sharedHostHelpers.ts +44 -0
  95. package/src/compiler/internal/startResidentTransform.ts +12 -3
  96. package/src/compiler/internal/transformProjectInMemory.ts +410 -39
  97. package/src/flags/schema.ts +35 -5
  98. package/src/internal/createCanonicalTempDirectory.ts +53 -0
  99. package/src/internal/projectInputPathIdentity.ts +17 -4
  100. package/src/internal/resolveNodeBinary.ts +173 -0
  101. package/src/internal/resolveSafeCacheCleanupTargets.ts +85 -0
  102. package/src/launcher/internal/assertNoSolutionBuild.ts +40 -0
  103. package/src/launcher/internal/prepareExecution.ts +78 -26
  104. package/src/launcher/internal/runTtsc.ts +26 -13
  105. package/src/launcher/internal/runTtscserver.ts +18 -3
  106. package/src/launcher/internal/runTtsx.ts +4 -0
  107. package/src/launcher/internal/runtimeHooks.ts +830 -66
  108. package/src/plugin/internal/buildSourcePlugin.ts +984 -79
  109. package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
  110. package/src/register.ts +47 -0
  111. package/src/structures/ITtscCompilerContext.ts +6 -3
  112. package/src/structures/ITtscCompilerTransformation.ts +39 -0
  113. package/src/structures/ITtscPlugin.ts +36 -0
  114. package/tools/shim_audit/baseline.json +0 -1
  115. package/utility/host.go +9 -3
  116. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +0 -5
  117. package/lib/internal/assertSafeExplicitCacheDirectory.js +0 -26
  118. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +0 -1
  119. package/src/internal/assertSafeExplicitCacheDirectory.ts +0 -29
@@ -0,0 +1,284 @@
1
+ package driver
2
+
3
+ import (
4
+ "strings"
5
+
6
+ "github.com/microsoft/typescript-go/shim/ast"
7
+ "github.com/microsoft/typescript-go/shim/core"
8
+ shimparser "github.com/microsoft/typescript-go/shim/parser"
9
+ )
10
+
11
+ // Source-preamble correction for compiler diagnostics.
12
+ //
13
+ // ttsc injects a plugin's source preamble (e.g. @ttsc/banner's copyright block)
14
+ // at the SOURCE level: sourcePreambleFS prepends it before TypeScript-Go parses,
15
+ // so the preamble participates in comment emission, removeComments, JSDoc
16
+ // association, and `.d.ts` emit naturally. The side effect is that every
17
+ // position tsgo records — including every diagnostic's — is shifted down by the
18
+ // preamble's line count, while the file the user reads has no preamble. The
19
+ // emitted source map already undoes that shift (AdjustEmittedSourceMap); this is
20
+ // the same invariant applied to the other coordinate the user is shown.
21
+ //
22
+ // The correction lives here, at the Program boundary where the preamble is
23
+ // known, rather than in each renderer. `WritePrettyDiagnostics`, the structured
24
+ // diagnostic list the JS launcher serializes, the graph snapshot, and any future
25
+ // consumer all read a Diagnostic that is already authored-relative, so none of
26
+ // them has to remember.
27
+
28
+ // preambleView is the authored (preamble-free) view of one preamble-injected
29
+ // source file, plus the byte region the preamble occupies in the buffer tsgo
30
+ // parsed.
31
+ //
32
+ // file is a re-parse of the authored text. The diagnostic renderer reads a
33
+ // source file's FileName, Text, and ECMA line map to place a location and quote
34
+ // a code frame, and all three have to describe the text the user wrote — moving
35
+ // the position alone would still quote the preamble-bearing buffer. Re-parsing
36
+ // is what produces a line map tsgo itself computed rather than one this package
37
+ // re-derived, and it costs one parse per file that actually carries a
38
+ // diagnostic.
39
+ type preambleView struct {
40
+ file *ast.SourceFile
41
+ // start is the byte offset where the preamble was inserted, and length its
42
+ // byte length. ApplySourcePreamble inserts after a BOM and after a hashbang
43
+ // line, so start is not always zero.
44
+ start int
45
+ length int
46
+ }
47
+
48
+ // authoredPos maps a position in the preamble-bearing buffer to the authored
49
+ // file. ok is false for a position inside the injected region, which has no
50
+ // authored counterpart at all.
51
+ func (v *preambleView) authoredPos(pos int) (int, bool) {
52
+ if pos < v.start {
53
+ return pos, true
54
+ }
55
+ if pos >= v.start+v.length {
56
+ return pos - v.length, true
57
+ }
58
+ return 0, false
59
+ }
60
+
61
+ // authoredEnd maps an exclusive end offset. An end inside the injected region is
62
+ // truncated to the region's authored boundary, so a range that starts before the
63
+ // preamble never stretches over text the user did not write.
64
+ func (v *preambleView) authoredEnd(end int) int {
65
+ if end <= v.start {
66
+ return end
67
+ }
68
+ if end >= v.start+v.length {
69
+ return end - v.length
70
+ }
71
+ return v.start
72
+ }
73
+
74
+ // sourcePreambleBOM is the UTF-8 byte order mark ApplySourcePreamble keeps
75
+ // ahead of an injected preamble.
76
+ const sourcePreambleBOM = "\ufeff"
77
+
78
+ // sourcePreambleRegion locates the injected preamble inside the text tsgo
79
+ // parsed, mirroring ApplySourcePreamble's insertion point: after a BOM, and
80
+ // after a hashbang line when one is present.
81
+ //
82
+ // The region is read back out of the parsed text rather than recomputed from the
83
+ // on-disk file, so it stays correct whichever of the BOM or hashbang the host
84
+ // stripped before parsing, and the leading-prefix check makes the result
85
+ // self-verifying: a file the preamble was never injected into (a declaration
86
+ // file, a `.json` input, a library file) simply does not match and is reported
87
+ // as having no region.
88
+ //
89
+ // One degenerate shape is nominally off by one, matching the same allowance
90
+ // AdjustSourceMapForPreamble states: a file whose entire content is a hashbang
91
+ // with no trailing newline gets the preamble appended after an inserted newline,
92
+ // which is indistinguishable from an authored trailing newline. Only a position
93
+ // at that file's very end could differ, and such a file has no code to diagnose.
94
+ func sourcePreambleRegion(filePath, text, preamble string) (start int, length int, ok bool) {
95
+ if preamble == "" || !isSourcePreambleTarget(filePath) {
96
+ return 0, 0, false
97
+ }
98
+ start = 0
99
+ if strings.HasPrefix(text, sourcePreambleBOM) {
100
+ start = len(sourcePreambleBOM)
101
+ }
102
+ if strings.HasPrefix(text[start:], "#!") {
103
+ newline := strings.IndexByte(text[start:], '\n')
104
+ if newline < 0 {
105
+ return 0, 0, false
106
+ }
107
+ start += newline + 1
108
+ }
109
+ if !strings.HasPrefix(text[start:], preamble) {
110
+ return 0, 0, false
111
+ }
112
+ return start, len(preamble), true
113
+ }
114
+
115
+ // preambleMapper rebuilds diagnostics against the authored source files of one
116
+ // preamble-bearing Program. It caches one authored view per source file so every
117
+ // diagnostic in a file shares a single re-parsed file object — the renderer's
118
+ // "Found N errors in M files" summary groups by that object's identity.
119
+ //
120
+ // A mapper is built per diagnostics call and thrown away with it: it holds
121
+ // re-parsed source files, which have no reason to outlive the report they were
122
+ // built for.
123
+ type preambleMapper struct {
124
+ preamble string
125
+ views map[*ast.SourceFile]*preambleView
126
+ }
127
+
128
+ // newPreambleMapper returns nil when this Program has no source preamble, which
129
+ // is the signal to leave every diagnostic exactly as tsgo produced it.
130
+ func (p *Program) newPreambleMapper() *preambleMapper {
131
+ if p == nil || p.SourcePreamble == "" {
132
+ return nil
133
+ }
134
+ return &preambleMapper{
135
+ preamble: p.SourcePreamble,
136
+ views: map[*ast.SourceFile]*preambleView{},
137
+ }
138
+ }
139
+
140
+ // viewOf returns the authored view of file, or nil when the preamble was never
141
+ // injected into it.
142
+ func (m *preambleMapper) viewOf(file *ast.SourceFile) *preambleView {
143
+ if m == nil || file == nil {
144
+ return nil
145
+ }
146
+ if view, cached := m.views[file]; cached {
147
+ return view
148
+ }
149
+ view := newPreambleView(file, m.preamble)
150
+ m.views[file] = view
151
+ return view
152
+ }
153
+
154
+ // newPreambleView re-parses file without its injected preamble. It returns nil
155
+ // when the file carries no preamble, or when the re-parse yields nothing to
156
+ // anchor against — an uncorrected position is still better than a nil file.
157
+ func newPreambleView(file *ast.SourceFile, preamble string) *preambleView {
158
+ text := file.Text()
159
+ start, length, ok := sourcePreambleRegion(file.FileName(), text, preamble)
160
+ if !ok {
161
+ return nil
162
+ }
163
+ authored := shimparser.ParseSourceFile(file.ParseOptions(), text[:start]+text[start+length:], file.ScriptKind)
164
+ if authored == nil {
165
+ return nil
166
+ }
167
+ return &preambleView{file: authored, start: start, length: length}
168
+ }
169
+
170
+ // remapDiagnostic returns d anchored to its authored source file. ok is false
171
+ // when d's own position falls inside the injected preamble, which is the one
172
+ // case with no authored answer; see unanchoredPreambleDiagnostic for what the
173
+ // caller does with it.
174
+ //
175
+ // The original diagnostic is never mutated: it belongs to the Program's own
176
+ // diagnostic collection, which other callers read.
177
+ func (m *preambleMapper) remapDiagnostic(d *ast.Diagnostic) (*ast.Diagnostic, bool) {
178
+ if m == nil || d == nil {
179
+ return d, d != nil
180
+ }
181
+ related, relatedChanged := m.remapRelated(d.RelatedInformation())
182
+ view := m.viewOf(d.File())
183
+ if view == nil {
184
+ if !relatedChanged {
185
+ return d, true
186
+ }
187
+ clone := d.Clone()
188
+ clone.SetRelatedInfo(related)
189
+ return clone, true
190
+ }
191
+ pos, ok := view.authoredPos(d.Pos())
192
+ if !ok {
193
+ return nil, false
194
+ }
195
+ clone := d.Clone()
196
+ clone.SetFile(view.file)
197
+ clone.SetLocation(core.NewTextRange(pos, view.authoredEnd(d.End())))
198
+ if relatedChanged {
199
+ clone.SetRelatedInfo(related)
200
+ }
201
+ return clone, true
202
+ }
203
+
204
+ // remapRelated remaps a diagnostic's related information, each entry against its
205
+ // own file. An entry with no authored counterpart is dropped rather than
206
+ // reported without a position: related information exists only to point at a
207
+ // second location, and the primary diagnostic still carries the message.
208
+ func (m *preambleMapper) remapRelated(in []*ast.Diagnostic) ([]*ast.Diagnostic, bool) {
209
+ if len(in) == 0 {
210
+ return nil, false
211
+ }
212
+ out := make([]*ast.Diagnostic, 0, len(in))
213
+ changed := false
214
+ for _, related := range in {
215
+ mapped, ok := m.remapDiagnostic(related)
216
+ if !ok {
217
+ changed = true
218
+ continue
219
+ }
220
+ if mapped != related {
221
+ changed = true
222
+ }
223
+ out = append(out, mapped)
224
+ }
225
+ return out, changed
226
+ }
227
+
228
+ // unanchoredPreambleDiagnostic reports a diagnostic whose position lies inside
229
+ // an injected source preamble.
230
+ //
231
+ // The preamble is text the user never wrote, so there is no authored line to
232
+ // name. The source-map lane drops such segments, but a diagnostic is not a
233
+ // mapping: dropping it would leave a failing build with nothing on stderr to
234
+ // explain the failure, and clamping it to the top of the file would print a
235
+ // coordinate that is simply false — the exact defect this correction exists to
236
+ // remove. So the report is kept and only the coordinate is dropped: the message,
237
+ // the code, and the file the preamble was injected into survive, and the
238
+ // diagnostic renders through the anchor-less `path: message` form ttsc already
239
+ // uses for diagnostics that have no source range. It counts as an error exactly
240
+ // as it did before, so the exit code is unchanged.
241
+ //
242
+ // One consequence to know: the JS launcher recovers structured diagnostics by
243
+ // parsing rendered text, and the anchor-less form carries no `line:col` for that
244
+ // parser to match — so a preamble-region diagnostic reaches stderr and the exit
245
+ // code but not `IFailure.diagnostics`. That is the same deal every anchor-less
246
+ // driver diagnostic has always had (`driver: linked plugins failed to apply`),
247
+ // and it is preferable to publishing a structured position that is not real.
248
+ func unanchoredPreambleDiagnostic(d *ast.Diagnostic) Diagnostic {
249
+ out := Diagnostic{
250
+ Code: d.Code(),
251
+ Message: d.String(),
252
+ Severity: SeverityError,
253
+ }
254
+ if file := d.File(); file != nil {
255
+ out.File = file.FileName()
256
+ }
257
+ return out
258
+ }
259
+
260
+ // convertProgramDiagnostics converts tsgo diagnostics produced by this Program,
261
+ // undoing any source-preamble shift first. Every path that turns this Program's
262
+ // raw diagnostics into driver Diagnostics goes through here, so the correction
263
+ // cannot be forgotten by a new caller. Diagnostics that never pass through a
264
+ // Program — a tsconfig parse failure, a bad CLI flag — keep using
265
+ // convertDiagnostics directly, because no preamble is ever injected into them.
266
+ func (p *Program) convertProgramDiagnostics(in []*ast.Diagnostic) []Diagnostic {
267
+ mapper := p.newPreambleMapper()
268
+ if mapper == nil {
269
+ return convertDiagnostics(in)
270
+ }
271
+ out := make([]Diagnostic, 0, len(in))
272
+ for _, d := range in {
273
+ if d == nil {
274
+ continue
275
+ }
276
+ mapped, ok := mapper.remapDiagnostic(d)
277
+ if !ok {
278
+ out = append(out, unanchoredPreambleDiagnostic(d))
279
+ continue
280
+ }
281
+ out = append(out, convertDiagnostic(mapped))
282
+ }
283
+ return out
284
+ }
@@ -1,7 +1,6 @@
1
1
  package driver
2
2
 
3
3
  import (
4
- "context"
5
4
  "errors"
6
5
  "strings"
7
6
  "sync"
@@ -133,13 +132,16 @@ func restoreOriginalDeclarationSymbols(ec *shimprinter.EmitContext, node *shimas
133
132
  // EmitTransformPlugins are chained after the caller's transforms in
134
133
  // registration order.
135
134
  //
136
- // Because the JavaScript side bypasses tsgo's own emitter, it reproduces the
137
- // emitter's source-map step via PrintFileWithSourceMap: a `sourceMap` /
138
- // `inlineSourceMap` build emits a `.js.map` (and `//# sourceMappingURL=`
139
- // trailer) just like a plain build, even when a transform expanded one source
140
- // line into many. All non-JavaScript outputs stay delegated to tsgo's normal
141
- // dts-only emitter so declaration files, declaration maps, and any future
142
- // declaration-lane outputs are not silently lost by the hand-assembled JS path.
135
+ // Because the JavaScript side bypasses tsgo's own emitter, it reproduces that
136
+ // emitter's whole printSourceFile step via PrintFileWithSourceMap: a
137
+ // `sourceMap` / `inlineSourceMap` build emits a `.js.map` (and
138
+ // `//# sourceMappingURL=` trailer) just like a plain build, even when a
139
+ // transform expanded one source line into many; an `emitBOM` build still starts
140
+ // with the byte order mark; and the caller's WriteFile still receives the
141
+ // WriteFileData the emitter would hand it. All non-JavaScript outputs stay
142
+ // delegated to tsgo's normal dts-only emitter so declaration files, declaration
143
+ // maps, and any future declaration-lane outputs are not silently lost by the
144
+ // hand-assembled JS path.
143
145
  func (p *Program) EmitWithPluginTransformers(transforms []PluginTransform, writeFile shimcompiler.WriteFile) ([]Diagnostic, error) {
144
146
  if p == nil || p.TSProgram == nil {
145
147
  return nil, errors.New("driver: nil program")
@@ -183,10 +185,10 @@ func (p *Program) EmitWithPluginTransformers(transforms []PluginTransform, write
183
185
  }
184
186
  // Print through the source-map-aware helper so a `sourceMap` /
185
187
  // `inlineSourceMap` build still gets its `.js.map` and sourceMappingURL
186
- // trailer: the hand-assembled emit pipeline does not run tsgo's emitter,
187
- // so the source-map step printSourceFile would otherwise perform has to
188
- // happen here. With maps off this is the same bare-printer output as
189
- // before.
188
+ // trailer, and an `emitBOM` build its leading mark: the hand-assembled
189
+ // emit pipeline does not run tsgo's emitter, so everything printSourceFile
190
+ // would otherwise do around the printer has to happen here. With maps and
191
+ // emitBOM off this is the same bare-printer output as before.
190
192
  printed := shimcompiler.PrintFileWithSourceMap(ec, out.AsNode(), out, options, host, paths.JsFilePath(), paths.SourceMapFilePath())
191
193
  // A source-level preamble (e.g. @ttsc/banner linked into a typia host)
192
194
  // shifts the map's source coordinates; correct them here too, so the
@@ -204,20 +206,45 @@ func (p *Program) EmitWithPluginTransformers(transforms []PluginTransform, write
204
206
  }
205
207
  }
206
208
  }
207
- if err := p.writePluginEmitOutput(paths.JsFilePath(), printed.JS, writeFile); err != nil {
209
+ // The emitter hands its writeFile callback a WriteFileData for the
210
+ // JavaScript and a nil one for the map (printSourceFile:
211
+ // `writeText(sourceMapFilePath, sourceMap, nil)`), so this lane does the
212
+ // same. See writePluginEmitOutput for what the struct carries here and
213
+ // why the remaining fields stay zero.
214
+ if err := p.writePluginEmitOutput(paths.JsFilePath(), printed.JS, &shimcompiler.WriteFileData{
215
+ SourceMapUrlPos: printed.SourceMapUrlPos,
216
+ }, writeFile); err != nil {
208
217
  return nil, err
209
218
  }
210
- if err := p.writePluginEmitOutput(printed.MapPath, printed.MapText, writeFile); err != nil {
219
+ if err := p.writePluginEmitOutput(printed.MapPath, printed.MapText, nil, writeFile); err != nil {
211
220
  return nil, err
212
221
  }
213
222
  }
214
223
  }
215
- if !options.GetEmitDeclarations() {
224
+ // The declaration pass below doubles as this lane's build-information pass,
225
+ // so it also runs for a JavaScript-only `incremental` / `composite` project
226
+ // that has no declarations to write at all.
227
+ if !options.GetEmitDeclarations() && !p.emitsBuildInfo() {
216
228
  return nil, nil
217
229
  }
218
230
 
231
+ // What the build information this pass writes does and does not claim.
232
+ //
233
+ // The JavaScript above was hand-assembled and written outside tsgo's
234
+ // emitter, so tsgo's snapshot never saw it happen and records the JS emit of
235
+ // every file as still pending. That error is one-directional and safe: a
236
+ // consumer reading it can only decide to emit again, never to skip a file
237
+ // ttsc actually transformed. Making the record exact would mean running
238
+ // tsgo's own JavaScript emit a second time and discarding its output, paying
239
+ // a full emit to describe work already done. Everything else in the file —
240
+ // the compiler version, the resolved options, per-file versions and
241
+ // signatures, and the declaration state this pass does produce — is accurate.
242
+ //
243
+ // ttsc itself never reads build information back (see
244
+ // `shimcompiler.EmitFreshWithBuildInfo`), so this asymmetry costs a ttsc
245
+ // rebuild nothing.
219
246
  var wfMu sync.Mutex
220
- result := p.TSProgram.Emit(context.Background(), shimcompiler.EmitOptions{
247
+ result := p.emitProgram(shimcompiler.EmitOptions{
221
248
  EmitOnly: shimcompiler.EmitOnlyDts,
222
249
  WriteFile: func(fileName string, text string, data *shimcompiler.WriteFileData) error {
223
250
  wfMu.Lock()
@@ -235,17 +262,42 @@ func (p *Program) EmitWithPluginTransformers(transforms []PluginTransform, write
235
262
  },
236
263
  })
237
264
  if result != nil && len(result.Diagnostics) != 0 {
238
- return convertDiagnostics(result.Diagnostics), nil
265
+ return p.convertProgramDiagnostics(result.Diagnostics), nil
239
266
  }
240
267
  return nil, nil
241
268
  }
242
269
 
243
- func (p *Program) writePluginEmitOutput(fileName, text string, writeFile shimcompiler.WriteFile) error {
270
+ // writePluginEmitOutput writes one artifact of the hand-assembled emit, passing
271
+ // the caller's WriteFile the same WriteFileData tsgo's emitter would.
272
+ //
273
+ // What this lane can populate, and what it deliberately cannot:
274
+ //
275
+ // - SourceMapUrlPos: the offset of the `//# sourceMappingURL=` trailer, or -1
276
+ // when none was written. PrintFileWithSourceMap records it exactly where
277
+ // printSourceFile does, so a consumer that relocates or rewrites the trailer
278
+ // works the same on both lanes. This is why a nil data was a real loss and
279
+ // not merely a cosmetic one.
280
+ // - Diagnostics: always empty. The emitter's field carries its accumulated
281
+ // emitterDiagnostics, which on the JavaScript lane are its own write
282
+ // failures; here a write failure is returned as an `error` from
283
+ // EmitWithPluginTransformers instead, and the declaration lane's diagnostics
284
+ // reach the caller through tsgo's own EmitOnlyDts pass and its own
285
+ // WriteFileData. Empty therefore means "nothing to report", not "never
286
+ // populated".
287
+ // - BuildInfo: always nil. It is the `.tsbuildinfo` payload, and this lane
288
+ // emits only JavaScript and its map.
289
+ // - SkippedDtsWrite: an out-parameter for the callee, left zero. Each write
290
+ // gets its own struct so one file's callback cannot observe another's.
291
+ //
292
+ // Nothing on this lane reads the struct back afterwards: unlike tsgo's emitter,
293
+ // EmitWithPluginTransformers builds no EmitResult, so there is no EmittedFiles
294
+ // list for a callee-set SkippedDtsWrite to keep a file out of.
295
+ func (p *Program) writePluginEmitOutput(fileName, text string, data *shimcompiler.WriteFileData, writeFile shimcompiler.WriteFile) error {
244
296
  if fileName == "" || p.outputEscapesOutDir(fileName) {
245
297
  return nil
246
298
  }
247
299
  if writeFile != nil {
248
- return writeFile(fileName, text, nil)
300
+ return writeFile(fileName, text, data)
249
301
  }
250
302
  return DefaultWriteFile(fileName, text)
251
303
  }
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 {