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
@@ -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 {
@@ -0,0 +1,283 @@
1
+ package driver
2
+
3
+ import (
4
+ "sort"
5
+ "sync"
6
+ )
7
+
8
+ // TransformDependencies is the transform envelope's dependency side channel:
9
+ // the per-file input lists a producer reports and the subset of files whose
10
+ // list is complete.
11
+ //
12
+ // A producer that declares nothing leaves both fields empty, so its envelope is
13
+ // byte-identical to what it was before this existed. The lane with no linked
14
+ // plugin at all is the deliberate exception: nothing there can contribute to a
15
+ // file, so every file is listed.
16
+ type TransformDependencies struct {
17
+ // Complete lists the files whose Dependencies entry is the whole input set
18
+ // beyond the file itself and the universal compiler-option chain.
19
+ Complete []string
20
+ // Dependencies maps a transformed file to the files whose content influenced
21
+ // its output, keyed and valued like every other envelope section.
22
+ Dependencies map[string][]string
23
+ }
24
+
25
+ // TransformDependenciesFor computes the dependency side channel for the files
26
+ // this program transforms, keyed against cwd.
27
+ //
28
+ // **The rule that makes any of this declarable.** ttsc's own source-to-source
29
+ // transform is syntactic: the built-in native host answers with each file's
30
+ // parsed text, and the linked-plugin generic host prints the parsed AST through
31
+ // a printer that is handed neither a checker nor an emit resolver. Neither lane
32
+ // runs the emit transformer chain, so none of the type-driven lowerings happen
33
+ // there — no type-driven import elision, no `design:type` metadata, no `enum`,
34
+ // `namespace`, or JSX lowering, and no declaration emit. The output of a file
35
+ // the host alone produced is therefore a function of that file's own text and
36
+ // the compiler options, and nothing the type system knows about any other file
37
+ // can change it.
38
+ //
39
+ // What that leaves is the plugins. A source preamble is prepended to every
40
+ // file's text before parsing, and a program plugin mutates the parsed AST, so
41
+ // either can make an output depend on anything it consulted — including, for a
42
+ // checker-driven plugin, the whole type graph. Such a plugin is the only party
43
+ // that knows what it read, which is why the declaration is per (plugin, file)
44
+ // and why a file is complete only when every plugin that can contribute to it
45
+ // declared it. An emit-only plugin is not a contributor here: its transform runs
46
+ // in `build`, which produces no envelope.
47
+ //
48
+ // With no linked plugin at all the contributor set is empty and every file is
49
+ // complete with an empty list, which is the rule above stated for the lane that
50
+ // has nothing but the host in it. A contributor that declares nothing leaves
51
+ // every file unlisted, which is exactly the behaviour of every producer written
52
+ // before this existed.
53
+ //
54
+ // An embedder that supplies LoadProgramOptions.SourcePreamble itself, rather
55
+ // than obtaining it from a linked plugin, makes the same claim about that text
56
+ // by calling this: the preamble must be a function of inputs its envelope
57
+ // reports elsewhere, the way a plugin's preamble is a function of the config
58
+ // files it reports as host inputs.
59
+ func (p *Program) TransformDependenciesFor(cwd string) TransformDependencies {
60
+ if p == nil {
61
+ return TransformDependencies{}
62
+ }
63
+ // Declarations are made from inside the plugin hooks, so they exist only
64
+ // after those hooks ran. The apply is latched, so this cannot re-run them.
65
+ _ = p.ApplyLinkedPlugins()
66
+ files := p.sourceFilesRaw()
67
+ keys := make([]string, 0, len(files))
68
+ for _, file := range files {
69
+ keys = append(keys, TransformOutputKey(cwd, file.FileName()))
70
+ }
71
+ return p.plugins.transformDependencies(keys)
72
+ }
73
+
74
+ // transformDependencies aggregates every contributing plugin's declarations
75
+ // over the envelope keys of the transformed files.
76
+ func (state linkedPluginState) transformDependencies(keys []string) TransformDependencies {
77
+ return aggregateTransformDependencies(keys, state.transformContributors(), state.declarations)
78
+ }
79
+
80
+ // aggregateTransformDependencies folds the declarations of the given
81
+ // contributors into one envelope side channel.
82
+ //
83
+ // Separated from the entry classification above so the aggregation rule can be
84
+ // exercised without the process-wide plugin registry that classification reads.
85
+ func aggregateTransformDependencies(keys []string, contributors []int, declarations *pluginFileDeclarations) TransformDependencies {
86
+ out := TransformDependencies{}
87
+ for _, key := range keys {
88
+ declared := 0
89
+ inputs := map[string]struct{}{}
90
+ for _, index := range contributors {
91
+ declaration := declarations.lookup(index)
92
+ if declaration == nil {
93
+ continue
94
+ }
95
+ for _, input := range declaration.dependenciesOf(key) {
96
+ // A file never depends on itself: the file's own text is outside the
97
+ // completeness contract by construction, and a self-edge would only
98
+ // make consumers register the module they are already transforming.
99
+ if input != key {
100
+ inputs[input] = struct{}{}
101
+ }
102
+ }
103
+ if declaration.declaresComplete(key) {
104
+ declared++
105
+ }
106
+ }
107
+ if len(inputs) != 0 {
108
+ out.Dependencies = appendDependencyEntry(out.Dependencies, key, inputs)
109
+ }
110
+ if declared == len(contributors) {
111
+ out.Complete = append(out.Complete, key)
112
+ }
113
+ }
114
+ sort.Strings(out.Complete)
115
+ return out
116
+ }
117
+
118
+ // appendDependencyEntry records one file's sorted dependency list, allocating
119
+ // the map only for a producer that reported something.
120
+ func appendDependencyEntry(into map[string][]string, key string, inputs map[string]struct{}) map[string][]string {
121
+ if into == nil {
122
+ into = map[string][]string{}
123
+ }
124
+ entry := make([]string, 0, len(inputs))
125
+ for input := range inputs {
126
+ entry = append(entry, input)
127
+ }
128
+ sort.Strings(entry)
129
+ into[key] = entry
130
+ return into
131
+ }
132
+
133
+ // transformContributors returns the indexes of the linked plugin entries that
134
+ // can influence source-to-source transform output.
135
+ //
136
+ // An entry with no registered plugin cannot be inspected, so it counts as a
137
+ // contributor that declared nothing; the host fails that entry elsewhere, and
138
+ // until it does, silence is the conservative answer.
139
+ func (state linkedPluginState) transformContributors() []int {
140
+ contributors := make([]int, 0, len(state.entries))
141
+ for index := range state.entries {
142
+ plugin, ok := registeredPlugin(index)
143
+ if !ok {
144
+ contributors = append(contributors, index)
145
+ continue
146
+ }
147
+ _, preamble := plugin.(SourcePreamblePlugin)
148
+ _, program := plugin.(ProgramPlugin)
149
+ if preamble || program {
150
+ contributors = append(contributors, index)
151
+ }
152
+ }
153
+ return contributors
154
+ }
155
+
156
+ // pluginFileDeclarations holds one declaration record per linked plugin entry.
157
+ type pluginFileDeclarations struct {
158
+ mu sync.Mutex
159
+ plugins map[int]*pluginFileDeclaration
160
+ }
161
+
162
+ // pluginFileDeclaration is one plugin's reported dependencies and completeness
163
+ // claims, keyed by envelope key.
164
+ type pluginFileDeclaration struct {
165
+ complete map[string]struct{}
166
+ completeAll bool
167
+ dependencies map[string]map[string]struct{}
168
+ mu sync.Mutex
169
+ // rejected holds the files whose reported list this plugin could not state
170
+ // in full, because one of its members was unusable as a key.
171
+ rejected map[string]struct{}
172
+ }
173
+
174
+ func newPluginFileDeclarations() *pluginFileDeclarations {
175
+ return &pluginFileDeclarations{plugins: map[int]*pluginFileDeclaration{}}
176
+ }
177
+
178
+ // forPlugin returns the declaration record of one plugin entry, creating it on
179
+ // first use. A nil ledger still answers a usable record so a hand-built state
180
+ // (unit tests, embedders) never panics through a plugin's reporting call.
181
+ func (declarations *pluginFileDeclarations) forPlugin(index int) *pluginFileDeclaration {
182
+ record := &pluginFileDeclaration{
183
+ complete: map[string]struct{}{},
184
+ dependencies: map[string]map[string]struct{}{},
185
+ rejected: map[string]struct{}{},
186
+ }
187
+ if declarations == nil {
188
+ return record
189
+ }
190
+ declarations.mu.Lock()
191
+ defer declarations.mu.Unlock()
192
+ if existing, ok := declarations.plugins[index]; ok {
193
+ return existing
194
+ }
195
+ declarations.plugins[index] = record
196
+ return record
197
+ }
198
+
199
+ // lookup returns the declaration record of one plugin entry, or nil when that
200
+ // entry never reported anything.
201
+ func (declarations *pluginFileDeclarations) lookup(index int) *pluginFileDeclaration {
202
+ if declarations == nil {
203
+ return nil
204
+ }
205
+ declarations.mu.Lock()
206
+ defer declarations.mu.Unlock()
207
+ return declarations.plugins[index]
208
+ }
209
+
210
+ func (declaration *pluginFileDeclaration) addDependency(file string, dependency string) {
211
+ if declaration == nil {
212
+ return
213
+ }
214
+ declaration.mu.Lock()
215
+ defer declaration.mu.Unlock()
216
+ entry, ok := declaration.dependencies[file]
217
+ if !ok {
218
+ entry = map[string]struct{}{}
219
+ declaration.dependencies[file] = entry
220
+ }
221
+ entry[dependency] = struct{}{}
222
+ }
223
+
224
+ // rejectDependency withdraws this plugin's completeness claim for one file.
225
+ func (declaration *pluginFileDeclaration) rejectDependency(file string) {
226
+ if declaration == nil {
227
+ return
228
+ }
229
+ declaration.mu.Lock()
230
+ defer declaration.mu.Unlock()
231
+ declaration.rejected[file] = struct{}{}
232
+ }
233
+
234
+ func (declaration *pluginFileDeclaration) addComplete(file string) {
235
+ if declaration == nil {
236
+ return
237
+ }
238
+ declaration.mu.Lock()
239
+ defer declaration.mu.Unlock()
240
+ declaration.complete[file] = struct{}{}
241
+ }
242
+
243
+ func (declaration *pluginFileDeclaration) completeEveryFile() {
244
+ if declaration == nil {
245
+ return
246
+ }
247
+ declaration.mu.Lock()
248
+ defer declaration.mu.Unlock()
249
+ declaration.completeAll = true
250
+ }
251
+
252
+ func (declaration *pluginFileDeclaration) dependenciesOf(file string) []string {
253
+ if declaration == nil {
254
+ return nil
255
+ }
256
+ declaration.mu.Lock()
257
+ defer declaration.mu.Unlock()
258
+ entry, ok := declaration.dependencies[file]
259
+ if !ok {
260
+ return nil
261
+ }
262
+ out := make([]string, 0, len(entry))
263
+ for input := range entry {
264
+ out = append(out, input)
265
+ }
266
+ return out
267
+ }
268
+
269
+ func (declaration *pluginFileDeclaration) declaresComplete(file string) bool {
270
+ if declaration == nil {
271
+ return false
272
+ }
273
+ declaration.mu.Lock()
274
+ defer declaration.mu.Unlock()
275
+ if _, rejected := declaration.rejected[file]; rejected {
276
+ return false
277
+ }
278
+ if declaration.completeAll {
279
+ return true
280
+ }
281
+ _, ok := declaration.complete[file]
282
+ return ok
283
+ }
@@ -0,0 +1,28 @@
1
+ package driver
2
+
3
+ import (
4
+ "reflect"
5
+ "testing"
6
+ )
7
+
8
+ // TestTransformDependenciesDeclaresEveryFileWithoutAContributor verifies the
9
+ // rule the host declares under when nothing else can contribute: a
10
+ // source-to-source transform is syntactic, so a file the host alone produced
11
+ // depends on its own text and the compiler options, and on nothing else.
12
+ //
13
+ // With an empty contributor set the aggregation is vacuously satisfied for
14
+ // every file, which is what lets a plugin-free project's consumer stop
15
+ // revalidating each delivered module's whole reference closure
16
+ // (samchon/ttsc#1259).
17
+ func TestTransformDependenciesDeclaresEveryFileWithoutAContributor(t *testing.T) {
18
+ keys := []string{"src/main.ts", "src/types.ts"}
19
+
20
+ out := aggregateTransformDependencies(keys, nil, newPluginFileDeclarations())
21
+
22
+ if !reflect.DeepEqual(out.Complete, []string{"src/main.ts", "src/types.ts"}) {
23
+ t.Fatalf("expected every file declared complete, got %v", out.Complete)
24
+ }
25
+ if out.Dependencies != nil {
26
+ t.Fatalf("expected no dependency entries, got %v", out.Dependencies)
27
+ }
28
+ }
@@ -0,0 +1,21 @@
1
+ package driver
2
+
3
+ import "testing"
4
+
5
+ // TestTransformDependenciesDropsASelfReportedDependency verifies a file never
6
+ // enters its own dependency list.
7
+ //
8
+ // The file's own text sits outside the completeness contract by construction —
9
+ // every consumer compares it before anything else — so a self-edge would only
10
+ // make a bundler register the module it is already transforming as one of that
11
+ // module's watch inputs.
12
+ func TestTransformDependenciesDropsASelfReportedDependency(t *testing.T) {
13
+ declarations := newPluginFileDeclarations()
14
+ declarations.forPlugin(0).addDependency("src/main.ts", "src/main.ts")
15
+
16
+ out := aggregateTransformDependencies([]string{"src/main.ts"}, []int{0}, declarations)
17
+
18
+ if len(out.Dependencies) != 0 {
19
+ t.Fatalf("expected the self-reported dependency to be dropped, got %v", out.Dependencies)
20
+ }
21
+ }
@@ -0,0 +1,46 @@
1
+ package driver
2
+
3
+ import (
4
+ "path/filepath"
5
+ "reflect"
6
+ "testing"
7
+ )
8
+
9
+ // TestTransformDependenciesKeysAReportedPathLikeTheEnvelope verifies a plugin's
10
+ // reported paths reach the envelope under the same key convention as every other
11
+ // section.
12
+ //
13
+ // A plugin reports the spellings its host handed it — absolute program file
14
+ // names, which the compiler normalizes to forward slashes on every platform, or
15
+ // paths relative to the plugin's cwd. Keying either differently from
16
+ // `typescript` would leave a consumer unable to join the sections at all, and on
17
+ // Windows the two spellings differ by separator alone, which is exactly the kind
18
+ // of mismatch that passes on POSIX CI and fails on a developer's machine.
19
+ func TestTransformDependenciesKeysAReportedPathLikeTheEnvelope(t *testing.T) {
20
+ // A real directory, so the absolute spellings below are absolute under the
21
+ // running platform's own rule rather than under POSIX's alone.
22
+ cwd := t.TempDir()
23
+ declarations := newPluginFileDeclarations()
24
+ ctx := PluginContext{
25
+ Cwd: cwd,
26
+ reportFileDependency: declarations.forPlugin(0).addDependency,
27
+ reportFileComplete: declarations.forPlugin(0).addComplete,
28
+ }
29
+
30
+ // The compiler's own spelling of a source file, and a cwd-relative one.
31
+ ctx.ReportFileDependency(
32
+ filepath.ToSlash(filepath.Join(cwd, "src", "main.ts")),
33
+ filepath.Join("src", "consulted.d.ts"),
34
+ )
35
+ ctx.ReportFileDependenciesComplete(filepath.Join(cwd, "src", "main.ts"))
36
+ ctx.ReportFileDependency(" ", "src/ignored.d.ts")
37
+
38
+ out := aggregateTransformDependencies([]string{"src/main.ts"}, []int{0}, declarations)
39
+
40
+ if !reflect.DeepEqual(out.Complete, []string{"src/main.ts"}) {
41
+ t.Fatalf("expected the reported file to key like the envelope, got %v", out.Complete)
42
+ }
43
+ if !reflect.DeepEqual(out.Dependencies["src/main.ts"], []string{"src/consulted.d.ts"}) {
44
+ t.Fatalf("expected the reported dependency to key like the envelope, got %v", out.Dependencies)
45
+ }
46
+ }
@@ -0,0 +1,38 @@
1
+ package driver
2
+
3
+ import (
4
+ "reflect"
5
+ "testing"
6
+ )
7
+
8
+ // TestTransformDependenciesRequiresEveryContributorToDeclare verifies the
9
+ // aggregation rule over a composed plugin set: a file is complete only when
10
+ // every contributing plugin declared it, because a consumer cannot attribute
11
+ // one plugin's reported inputs back to it.
12
+ //
13
+ // It also pins that reporting and declaring are separate acts: a plugin's
14
+ // dependency list widens what consumers invalidate on even while the file
15
+ // itself stays unlisted (samchon/ttsc#1263).
16
+ func TestTransformDependenciesRequiresEveryContributorToDeclare(t *testing.T) {
17
+ declarations := newPluginFileDeclarations()
18
+ first := declarations.forPlugin(0)
19
+ first.addDependency("src/main.ts", "src/consulted.d.ts")
20
+ first.addComplete("src/main.ts")
21
+ keys := []string{"src/main.ts"}
22
+
23
+ partial := aggregateTransformDependencies(keys, []int{0, 1}, declarations)
24
+
25
+ if len(partial.Complete) != 0 {
26
+ t.Fatalf("expected the silent contributor to block the claim, got %v", partial.Complete)
27
+ }
28
+ if !reflect.DeepEqual(partial.Dependencies["src/main.ts"], []string{"src/consulted.d.ts"}) {
29
+ t.Fatalf("expected the reported dependency to survive, got %v", partial.Dependencies)
30
+ }
31
+
32
+ declarations.forPlugin(1).completeEveryFile()
33
+ full := aggregateTransformDependencies(keys, []int{0, 1}, declarations)
34
+
35
+ if !reflect.DeepEqual(full.Complete, []string{"src/main.ts"}) {
36
+ t.Fatalf("expected both contributors to complete the claim, got %v", full.Complete)
37
+ }
38
+ }
@@ -0,0 +1,23 @@
1
+ package driver
2
+
3
+ import "testing"
4
+
5
+ // TestTransformDependenciesStaysSilentForAnUndeclaredContributor verifies that
6
+ // a plugin able to change transform output blocks every completeness claim
7
+ // until it makes one itself.
8
+ //
9
+ // This is the compatibility half of the contract: every producer written before
10
+ // the declaration existed keeps the host-owned bound exactly, and a plugin whose
11
+ // output depends on the type graph cannot have that bound narrowed on its
12
+ // behalf (samchon/ttsc#1263).
13
+ func TestTransformDependenciesStaysSilentForAnUndeclaredContributor(t *testing.T) {
14
+ out := aggregateTransformDependencies(
15
+ []string{"src/main.ts"},
16
+ []int{0},
17
+ newPluginFileDeclarations(),
18
+ )
19
+
20
+ if len(out.Complete) != 0 {
21
+ t.Fatalf("expected no completeness declaration, got %v", out.Complete)
22
+ }
23
+ }
@@ -0,0 +1,37 @@
1
+ package driver
2
+
3
+ import "testing"
4
+
5
+ // TestTransformDependenciesWithdrawsCompletenessAfterAnUnusableDependency
6
+ // verifies a dependency the host could not key withdraws the claim it belonged
7
+ // to, instead of leaving a complete list with a member missing from it.
8
+ //
9
+ // A plugin whose own lookup answered nothing for one input is the case: it
10
+ // reports what it has and declares the file complete, and a host that dropped
11
+ // the unusable member would publish exactly the under-declaration the protocol
12
+ // blames on the plugin — manufactured by the host. Only the affected file loses
13
+ // the claim; the plugin's other files keep theirs.
14
+ func TestTransformDependenciesWithdrawsCompletenessAfterAnUnusableDependency(t *testing.T) {
15
+ cwd := t.TempDir()
16
+ declarations := newPluginFileDeclarations()
17
+ record := declarations.forPlugin(0)
18
+ ctx := PluginContext{
19
+ Cwd: cwd,
20
+ reportFileDependency: record.addDependency,
21
+ reportFileDependencyRejected: record.rejectDependency,
22
+ reportEveryFileComplete: record.completeEveryFile,
23
+ }
24
+
25
+ ctx.ReportDependenciesComplete()
26
+ ctx.ReportFileDependency("src/main.ts", "")
27
+
28
+ out := aggregateTransformDependencies(
29
+ []string{"src/main.ts", "src/other.ts"},
30
+ []int{0},
31
+ declarations,
32
+ )
33
+
34
+ if len(out.Complete) != 1 || out.Complete[0] != "src/other.ts" {
35
+ t.Fatalf("expected only the unaffected file to stay complete, got %v", out.Complete)
36
+ }
37
+ }