ttsc 0.26.2 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +9 -0
  2. package/cmd/graphbench/main.go +26 -16
  3. package/cmd/graphdump/main.go +37 -12
  4. package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
  5. package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
  6. package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
  7. package/cmd/ttsc/api_transform.go +28 -3
  8. package/cmd/ttscgraph/auxiliary_identity_state_tracks_retargets_not_source_contents_test.go +85 -0
  9. package/cmd/ttscgraph/disk_realpath_other.go +16 -0
  10. package/cmd/ttscgraph/disk_realpath_windows.go +56 -0
  11. package/cmd/ttscgraph/dump.go +43 -11
  12. package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
  13. package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
  14. package/cmd/ttscgraph/serve.go +357 -60
  15. package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
  16. package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
  17. package/cmd/ttscgraph/serve_shards.go +31 -9
  18. package/driver/config_discovery.go +121 -0
  19. package/driver/emit_plugin.go +77 -11
  20. package/driver/graph.go +64 -11
  21. package/driver/input_observation.go +273 -0
  22. package/driver/input_observation_test.go +149 -0
  23. package/driver/plugins.go +506 -15
  24. package/driver/program.go +54 -1
  25. package/driver/resolution_candidates.go +13 -5
  26. package/driver/rewrite.go +45 -2
  27. package/driver/transform_dependencies.go +283 -0
  28. package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
  29. package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
  30. package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
  31. package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
  32. package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
  33. package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
  34. package/internal/graph/artifacts.go +230 -0
  35. package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
  36. package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
  37. package/internal/graph/build.go +35 -0
  38. package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
  39. package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
  40. package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
  41. package/internal/graph/doc_tags.go +416 -0
  42. package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
  43. package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
  44. package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
  45. package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
  46. package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
  47. package/internal/graph/dump.go +60 -13
  48. package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
  49. package/internal/graph/dump_paths.go +2 -2
  50. package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
  51. package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
  52. package/internal/graph/edges.go +1 -0
  53. package/internal/graph/graph.go +79 -4
  54. package/internal/graph/provenance.go +29 -2
  55. package/lib/TtscCompiler.d.ts +12 -11
  56. package/lib/TtscCompiler.js +44 -29
  57. package/lib/TtscCompiler.js.map +1 -1
  58. package/lib/compiler/internal/captureProcessOutput.js +2 -2
  59. package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
  60. package/lib/compiler/internal/compileProjectInMemory.js +4 -3
  61. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  62. package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +9 -0
  63. package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
  64. package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -1
  65. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -0
  66. package/lib/compiler/internal/resolveEmittedJavaScript.js +3 -0
  67. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  68. package/lib/compiler/internal/runBuild.d.ts +1 -1
  69. package/lib/compiler/internal/runBuild.js +80 -34
  70. package/lib/compiler/internal/runBuild.js.map +1 -1
  71. package/lib/compiler/internal/runSingleFileEmit.js +2 -2
  72. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  73. package/lib/compiler/internal/sharedHostHelpers.d.ts +28 -0
  74. package/lib/compiler/internal/sharedHostHelpers.js +39 -0
  75. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  76. package/lib/compiler/internal/startResidentTransform.js +12 -4
  77. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  78. package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -0
  79. package/lib/compiler/internal/transformProjectInMemory.js +239 -35
  80. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  81. package/lib/flags/schema.js +12 -5
  82. package/lib/flags/schema.js.map +1 -1
  83. package/lib/index.d.ts +1 -0
  84. package/lib/index.js +1 -0
  85. package/lib/index.js.map +1 -1
  86. package/lib/internal/createCanonicalTempDirectory.d.ts +17 -0
  87. package/lib/internal/createCanonicalTempDirectory.js +42 -0
  88. package/lib/internal/createCanonicalTempDirectory.js.map +1 -0
  89. package/lib/internal/projectInputPathIdentity.d.ts +3 -0
  90. package/lib/internal/projectInputPathIdentity.js +11 -5
  91. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  92. package/lib/internal/resolveNodeBinary.d.ts +14 -0
  93. package/lib/internal/resolveNodeBinary.js +150 -0
  94. package/lib/internal/resolveNodeBinary.js.map +1 -0
  95. package/lib/internal/resolveSafeCacheCleanupTargets.d.ts +15 -0
  96. package/lib/internal/resolveSafeCacheCleanupTargets.js +61 -0
  97. package/lib/internal/resolveSafeCacheCleanupTargets.js.map +1 -0
  98. package/lib/launcher/internal/prepareExecution.d.ts +3 -0
  99. package/lib/launcher/internal/prepareExecution.js +42 -14
  100. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  101. package/lib/launcher/internal/runTtsc.js +25 -13
  102. package/lib/launcher/internal/runTtsc.js.map +1 -1
  103. package/lib/launcher/internal/runTtscserver.js +18 -3
  104. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  105. package/lib/launcher/internal/runTtsx.js +2 -0
  106. package/lib/launcher/internal/runTtsx.js.map +1 -1
  107. package/lib/launcher/internal/runtimeHooks.d.ts +47 -3
  108. package/lib/launcher/internal/runtimeHooks.js +746 -64
  109. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  110. package/lib/plugin/internal/buildSourcePlugin.d.ts +62 -8
  111. package/lib/plugin/internal/buildSourcePlugin.js +754 -72
  112. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  113. package/lib/plugin/internal/capabilityResolutionCache.d.ts +76 -0
  114. package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
  115. package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
  116. package/lib/plugin/internal/loadProjectPlugins.d.ts +17 -5
  117. package/lib/plugin/internal/loadProjectPlugins.js +1339 -77
  118. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  119. package/lib/plugin/resolveCapabilityPlugins.d.ts +55 -0
  120. package/lib/plugin/resolveCapabilityPlugins.js +118 -0
  121. package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
  122. package/lib/register.d.ts +1 -0
  123. package/lib/register.js +46 -0
  124. package/lib/register.js.map +1 -0
  125. package/lib/structures/ITtscCompilerContext.d.ts +6 -3
  126. package/lib/structures/ITtscCompilerTransformation.d.ts +38 -0
  127. package/lib/structures/ITtscPlugin.d.ts +33 -0
  128. package/package.json +16 -9
  129. package/shim/ast/parent.go +16 -3
  130. package/shim/checker/enums_gen.go +11 -0
  131. package/shim/checker/go.mod +9 -0
  132. package/shim/checker/go.sum +22 -0
  133. package/shim/checker/shim.go +78 -0
  134. package/shim/compiler/emithook.go +10 -0
  135. package/shim/compiler/incremental.go +45 -8
  136. package/src/TtscCompiler.ts +55 -33
  137. package/src/compiler/internal/captureProcessOutput.ts +3 -2
  138. package/src/compiler/internal/compileProjectInMemory.ts +4 -3
  139. package/src/compiler/internal/project/createNativeProjectContextArgs.ts +19 -4
  140. package/src/compiler/internal/resolveEmittedJavaScript.ts +6 -0
  141. package/src/compiler/internal/runBuild.ts +102 -39
  142. package/src/compiler/internal/runSingleFileEmit.ts +2 -2
  143. package/src/compiler/internal/sharedHostHelpers.ts +44 -0
  144. package/src/compiler/internal/startResidentTransform.ts +12 -3
  145. package/src/compiler/internal/transformProjectInMemory.ts +410 -39
  146. package/src/flags/schema.ts +12 -5
  147. package/src/index.ts +1 -0
  148. package/src/internal/createCanonicalTempDirectory.ts +53 -0
  149. package/src/internal/projectInputPathIdentity.ts +17 -4
  150. package/src/internal/resolveNodeBinary.ts +173 -0
  151. package/src/internal/resolveSafeCacheCleanupTargets.ts +85 -0
  152. package/src/launcher/internal/prepareExecution.ts +50 -14
  153. package/src/launcher/internal/runTtsc.ts +24 -13
  154. package/src/launcher/internal/runTtscserver.ts +18 -3
  155. package/src/launcher/internal/runTtsx.ts +2 -0
  156. package/src/launcher/internal/runtimeHooks.ts +934 -68
  157. package/src/plugin/internal/buildSourcePlugin.ts +984 -79
  158. package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
  159. package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
  160. package/src/plugin/resolveCapabilityPlugins.ts +171 -0
  161. package/src/register.ts +47 -0
  162. package/src/structures/ITtscCompilerContext.ts +6 -3
  163. package/src/structures/ITtscCompilerTransformation.ts +46 -0
  164. package/src/structures/ITtscPlugin.ts +36 -0
  165. package/tools/shim_audit/baseline.json +0 -2
  166. package/utility/host.go +18 -3
  167. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +0 -5
  168. package/lib/internal/assertSafeExplicitCacheDirectory.js +0 -26
  169. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +0 -1
  170. package/src/internal/assertSafeExplicitCacheDirectory.ts +0 -29
package/README.md CHANGED
@@ -30,6 +30,15 @@ npx ttsc --watch # rebuild on save
30
30
 
31
31
  `ttsx` runs a file the way `tsx` or `ts-node` does, but it type-checks the whole project first, so a type error stops the run before anything executes.
32
32
 
33
+ Preload the same checked runtime when Node or a test runner owns the process:
34
+
35
+ ```bash
36
+ node --require ttsc/register src/index.ts
37
+ mocha --require ttsc/register --extension ts,tsx "test/**/*.ts"
38
+ ```
39
+
40
+ `ttsc/register` discovers the nearest `tsconfig.json` for each TypeScript root, applies its plugins, and refuses to execute when the project does not type-check.
41
+
33
42
  That covers the CLI. The integrations each have a short guide:
34
43
 
35
44
  - [`@ttsc/unplugin`](https://ttsc.dev/docs/setup/unplugin): Vite, Rollup, Rolldown, esbuild, webpack, Rspack, Next.js, Turbopack, Farm, and Bun.
@@ -29,10 +29,16 @@ type metrics struct {
29
29
  Nodes int `json:"nodes"`
30
30
  ExternalNodes int `json:"externalNodes"`
31
31
  Edges map[string]int `json:"edges"`
32
- TotalEdges int `json:"totalEdges"`
33
- SymbolFiles int `json:"symbolFiles"`
34
- CoveredFiles int `json:"coveredFiles"`
35
- Coverage float64 `json:"coverage"`
32
+ // EdgeVocabulary names which of the three edge-kind vocabularies Edges is
33
+ // keyed by, so a reader comparing these numbers against a dump does not
34
+ // compare the wrong things. Build produces *internal* kinds; a dump carries
35
+ // finer *wire* kinds (value-call splits into calls/instantiates/renders);
36
+ // the viewer folds wire kinds onto *display* families.
37
+ EdgeVocabulary string `json:"edgeVocabulary"`
38
+ TotalEdges int `json:"totalEdges"`
39
+ SymbolFiles int `json:"symbolFiles"`
40
+ CoveredFiles int `json:"coveredFiles"`
41
+ Coverage float64 `json:"coverage"`
36
42
  }
37
43
 
38
44
  func main() {
@@ -82,10 +88,13 @@ func run() int {
82
88
  // counts, edges per kind, and fair coverage (workspace files holding at least one
83
89
  // node that an edge connects to a node in another file).
84
90
  func summarize(g *graph.Graph, sourceFiles int) metrics {
85
- edges := map[string]int{
86
- string(graph.EdgeHeritage): 0,
87
- string(graph.EdgeValueCall): 0,
88
- string(graph.EdgeTypeRef): 0,
91
+ // Seeded with every kind Build can produce, so a family that happened not to
92
+ // occur reports 0 rather than vanishing from the object. Three of the seven
93
+ // used to be seeded and the map grew the rest on first sight, which meant the
94
+ // reported shape depended on the project measured.
95
+ edges := map[string]int{}
96
+ for _, kind := range graph.EdgeKinds() {
97
+ edges[string(kind)] = 0
89
98
  }
90
99
  for _, edge := range g.Edges {
91
100
  edges[string(edge.Kind)]++
@@ -123,13 +132,14 @@ func summarize(g *graph.Graph, sourceFiles int) metrics {
123
132
  }
124
133
 
125
134
  return metrics{
126
- SourceFiles: sourceFiles,
127
- Nodes: len(g.Nodes),
128
- ExternalNodes: external,
129
- Edges: edges,
130
- TotalEdges: len(g.Edges),
131
- SymbolFiles: len(symbolFiles),
132
- CoveredFiles: len(coveredFiles),
133
- Coverage: coverage,
135
+ SourceFiles: sourceFiles,
136
+ Nodes: len(g.Nodes),
137
+ ExternalNodes: external,
138
+ Edges: edges,
139
+ EdgeVocabulary: "internal",
140
+ TotalEdges: len(g.Edges),
141
+ SymbolFiles: len(symbolFiles),
142
+ CoveredFiles: len(coveredFiles),
143
+ Coverage: coverage,
134
144
  }
135
145
  }
@@ -6,8 +6,10 @@
6
6
  package main
7
7
 
8
8
  import (
9
+ "errors"
9
10
  "flag"
10
11
  "fmt"
12
+ "io"
11
13
  "os"
12
14
  "path/filepath"
13
15
 
@@ -17,18 +19,37 @@ import (
17
19
  "github.com/samchon/ttsc/packages/ttsc/internal/graph"
18
20
  )
19
21
 
22
+ // Package-level streams so command tests can capture I/O without patching the
23
+ // os globals, and argv arrives as a parameter for the same reason. Both mirror
24
+ // the shipped sibling in cmd/ttscgraph, whose capability claim is tested the
25
+ // same way; this command's claim went undefended because it had neither seam.
26
+ var (
27
+ stdout io.Writer = os.Stdout
28
+ stderr io.Writer = os.Stderr
29
+ )
30
+
20
31
  func main() {
21
- os.Exit(run())
32
+ os.Exit(run(os.Args[1:]))
22
33
  }
23
34
 
24
- func run() int {
25
- cwd := flag.String("cwd", ".", "project root")
26
- tsconfig := flag.String("tsconfig", "tsconfig.json", "tsconfig path, relative to cwd")
27
- pretty := flag.Bool("pretty", false, "indent the JSON output")
28
- flag.Parse()
35
+ func run(args []string) int {
36
+ fs := flag.NewFlagSet("graphdump", flag.ContinueOnError)
37
+ fs.SetOutput(stderr)
38
+ cwd := fs.String("cwd", ".", "project root")
39
+ tsconfig := fs.String("tsconfig", "tsconfig.json", "tsconfig path, relative to cwd")
40
+ pretty := fs.Bool("pretty", false, "indent the JSON output")
41
+ if err := fs.Parse(args); err != nil {
42
+ // `-h` asks for the usage this just printed, so it is a request that
43
+ // succeeded rather than an argument that failed. Under the global flag set
44
+ // this command used to read, `ExitOnError` already exited 0 for it.
45
+ if errors.Is(err, flag.ErrHelp) {
46
+ return 0
47
+ }
48
+ return 2
49
+ }
29
50
 
30
51
  // Resolve the project root the same way LoadProgram does (absolute, then
31
- // tsgo-normalized) so the schema-v6 mapper receives the same canonical root
52
+ // tsgo-normalized) so the dump path mapper receives the same canonical root
32
53
  // grammar and drive-letter case as the compiler's source paths.
33
54
  root := *cwd
34
55
  if abs, err := filepath.Abs(root); err == nil {
@@ -38,11 +59,11 @@ func run() int {
38
59
 
39
60
  prog, _, err := driver.LoadProgram(root, *tsconfig, driver.LoadProgramOptions{})
40
61
  if err != nil {
41
- fmt.Fprintf(os.Stderr, "graphdump: could not load %s/%s: %v\n", root, *tsconfig, err)
62
+ fmt.Fprintf(stderr, "graphdump: could not load %s/%s: %v\n", root, *tsconfig, err)
42
63
  return 1
43
64
  }
44
65
  if prog == nil {
45
- fmt.Fprintf(os.Stderr, "graphdump: could not load %s/%s\n", root, *tsconfig)
66
+ fmt.Fprintf(stderr, "graphdump: could not load %s/%s\n", root, *tsconfig)
46
67
  return 1
47
68
  }
48
69
  defer func() { _ = prog.Close() }()
@@ -62,7 +83,11 @@ func run() int {
62
83
  // No version: this tool is built from the tree on demand and never
63
84
  // stamped, and an invented one would be worse than an absent one.
64
85
  graph.Producer{Tool: "graphdump", Typescript: graph.TypescriptVersion()},
65
- []string{graph.CapabilitySourceDigests},
86
+ // The declaration walk is the shipped one, so this dump carries every
87
+ // documentation tag and must say so: under the capability contract an
88
+ // absent claim means the producer never looked, which would make a
89
+ // consumer read a tagged declaration as citing nothing.
90
+ []string{graph.CapabilitySourceDigests, graph.CapabilityDocTags},
66
91
  nil,
67
92
  nil,
68
93
  texts,
@@ -70,9 +95,9 @@ func run() int {
70
95
  ),
71
96
  }, *pretty)
72
97
  if err != nil {
73
- fmt.Fprintf(os.Stderr, "graphdump: %v\n", err)
98
+ fmt.Fprintf(stderr, "graphdump: %v\n", err)
74
99
  return 1
75
100
  }
76
- fmt.Println(string(data))
101
+ fmt.Fprintln(stdout, string(data))
77
102
  return 0
78
103
  }
@@ -0,0 +1,137 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "os"
7
+ "path/filepath"
8
+ "slices"
9
+ "strings"
10
+ "testing"
11
+
12
+ "github.com/samchon/ttsc/packages/ttsc/internal/graph"
13
+ )
14
+
15
+ // TestRunDeclaresTheCapabilityItsOutputCarries verifies that every capability
16
+ // this command declares is paired with the output that backs it, and that a
17
+ // project carrying no documentation tag emits that field nowhere.
18
+ //
19
+ // The claim is a separate literal from the shipped `ttscgraph dump`'s, so the
20
+ // test that guards that one guards nothing here: deleting graph.CapabilityDocTags
21
+ // from this command's list used to break nothing at all, in a package that had
22
+ // never had a test file. The claim matters because the two states a consumer must
23
+ // distinguish — "this declaration cites nothing" and "this producer never
24
+ // looked" — are the same absent field, so a dump that carries tags and forgets to
25
+ // say so is read as a repository where nothing cites anything.
26
+ //
27
+ // The declared set is compared whole rather than member by member, so a
28
+ // capability added to the list arrives with the evidence for it or fails here.
29
+ //
30
+ // 1. Run the command over a project with a tag, capturing stdout.
31
+ // 2. Assert the declared set is exactly what this command claims, and that each
32
+ // claim is backed: a tag in the output, and a digested source.
33
+ // 3. Run it over a project with no tag and assert the field appears nowhere,
34
+ // while both claims still do.
35
+ func TestRunDeclaresTheCapabilityItsOutputCarries(t *testing.T) {
36
+ tagged := runGraphdump(t, `/** @evidence docs/a.md#x Cited. */
37
+ export function subject(): void {}
38
+ `)
39
+ untagged := runGraphdump(t, `/** Ordinary documentation. */
40
+ export function subject(): void {}
41
+ `)
42
+
43
+ declared := []string{graph.CapabilityDocTags, graph.CapabilitySourceDigests}
44
+ slices.Sort(declared)
45
+ for name, raw := range map[string]string{"tagged": tagged, "untagged": untagged} {
46
+ var parsed struct {
47
+ Provenance struct {
48
+ Capabilities []string `json:"capabilities"`
49
+ Sources []struct {
50
+ File string `json:"file"`
51
+ CheckerDigest string `json:"checkerDigest"`
52
+ } `json:"sources"`
53
+ } `json:"provenance"`
54
+ }
55
+ if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
56
+ t.Fatalf("%s dump is not JSON: %v", name, err)
57
+ }
58
+ got := slices.Clone(parsed.Provenance.Capabilities)
59
+ slices.Sort(got)
60
+ if !slices.Equal(got, declared) {
61
+ t.Fatalf("%s dump declares %v, want %v; an absent claim means the producer "+
62
+ "never looked, so a tagged declaration reads as citing nothing and a "+
63
+ "digested source reads as unread",
64
+ name, got, declared)
65
+ }
66
+ // The sourceDigests claim, backed the same way the docTags claim is: a
67
+ // declared capability whose output carries nothing is the tautology this
68
+ // test exists to avoid.
69
+ digested := 0
70
+ for _, source := range parsed.Provenance.Sources {
71
+ if source.CheckerDigest != "" {
72
+ digested++
73
+ }
74
+ }
75
+ if digested == 0 {
76
+ t.Fatalf("%s dump declares %q over %d sources, none of which carries a checker digest",
77
+ name, graph.CapabilitySourceDigests, len(parsed.Provenance.Sources))
78
+ }
79
+ }
80
+
81
+ if !strings.Contains(tagged, `"docs/a.md#x Cited."`) {
82
+ t.Fatalf("the tagged dump carried no tag text")
83
+ }
84
+ // Read the raw document rather than a decoded slice: an absent field and an
85
+ // empty one decode identically, and absent is what the wire contract says. The
86
+ // key is matched with its colon, because the capability shares the name and
87
+ // rides the same document as a bare string.
88
+ if strings.Contains(untagged, `"docTags":`) {
89
+ t.Fatalf("the untagged dump carried a docTags field; it must be absent")
90
+ }
91
+ if !strings.Contains(tagged, `"docTags":`) {
92
+ t.Fatalf("the tagged dump carried no docTags field, so the check above proves nothing")
93
+ }
94
+ }
95
+
96
+ // runGraphdump runs the command over a one-file project and returns its stdout.
97
+ //
98
+ // It goes through run(args) and the package streams rather than through os.Args,
99
+ // flag.CommandLine, and an os.Stdout pipe. Those seams exist on the shipped
100
+ // sibling for this reason, and this command grew them so its own claim could be
101
+ // asserted against its own output without patching process globals.
102
+ func runGraphdump(t *testing.T, source string) string {
103
+ t.Helper()
104
+ root := t.TempDir()
105
+ writeGraphdumpFile(t, filepath.Join(root, "tsconfig.json"), `{
106
+ "compilerOptions": {
107
+ "target": "ES2022",
108
+ "module": "commonjs",
109
+ "strict": true,
110
+ "rootDir": "src",
111
+ "outDir": "dist"
112
+ },
113
+ "files": ["src/main.ts"]
114
+ }
115
+ `)
116
+ writeGraphdumpFile(t, filepath.Join(root, "src", "main.ts"), source)
117
+
118
+ var out, errOut bytes.Buffer
119
+ restoreStdout, restoreStderr := stdout, stderr
120
+ stdout, stderr = &out, &errOut
121
+ defer func() { stdout, stderr = restoreStdout, restoreStderr }()
122
+
123
+ if code := run([]string{"--cwd", root, "--tsconfig", "tsconfig.json"}); code != 0 {
124
+ t.Fatalf("graphdump exited %d: %s", code, errOut.String())
125
+ }
126
+ return out.String()
127
+ }
128
+
129
+ func writeGraphdumpFile(t *testing.T, path, content string) {
130
+ t.Helper()
131
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
132
+ t.Fatal(err)
133
+ }
134
+ if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
135
+ t.Fatal(err)
136
+ }
137
+ }
@@ -0,0 +1,56 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "strings"
6
+ "testing"
7
+ )
8
+
9
+ // TestRunReportsAnUnknownFlagWithoutExitingTheProcess verifies that a bad
10
+ // argument returns an exit code and writes usage to the command's own stream,
11
+ // instead of terminating the process.
12
+ //
13
+ // This is the negative twin of the capability case and the reason the seams
14
+ // exist. The command used to parse flag.CommandLine, whose default error
15
+ // handling is flag.ExitOnError: a malformed argument called os.Exit(2) from
16
+ // inside run, which no test can observe and which would take a test binary down
17
+ // with it. Nothing else about the command is reachable while that is true.
18
+ //
19
+ // 1. Run the command with an unknown flag, capturing both streams.
20
+ // 2. Assert it returns 2 rather than exiting, and that nothing reached stdout.
21
+ // 3. Assert the usage text went to the command's stderr stream.
22
+ // 4. Assert `-h` still succeeds, because asking for usage is not an error.
23
+ func TestRunReportsAnUnknownFlagWithoutExitingTheProcess(t *testing.T) {
24
+ var out, errOut bytes.Buffer
25
+ restoreStdout, restoreStderr := stdout, stderr
26
+ stdout, stderr = &out, &errOut
27
+ defer func() { stdout, stderr = restoreStdout, restoreStderr }()
28
+
29
+ code := run([]string{"--not-a-flag"})
30
+
31
+ if code != 2 {
32
+ t.Fatalf("graphdump exited %d for an unknown flag, want 2", code)
33
+ }
34
+ if out.Len() != 0 {
35
+ t.Fatalf("a rejected invocation wrote %q to stdout; the viewer pipeline parses that stream as JSON", out.String())
36
+ }
37
+ if !strings.Contains(errOut.String(), "not-a-flag") {
38
+ t.Fatalf("stderr does not name the rejected flag: %q", errOut.String())
39
+ }
40
+ if !strings.Contains(errOut.String(), "-tsconfig") {
41
+ t.Fatalf("stderr carried no usage text: %q", errOut.String())
42
+ }
43
+
44
+ // The negative twin of the rejection: `flag.ContinueOnError` reports `-h` as
45
+ // an error too, and mapping every parse failure to 2 would turn the help flag
46
+ // into a failed invocation. The global flag set this command used to read
47
+ // exited 0 for it.
48
+ out.Reset()
49
+ errOut.Reset()
50
+ if code := run([]string{"-h"}); code != 0 {
51
+ t.Fatalf("graphdump exited %d for -h, want 0", code)
52
+ }
53
+ if !strings.Contains(errOut.String(), "-tsconfig") {
54
+ t.Fatalf("-h printed no usage text: %q", errOut.String())
55
+ }
56
+ }
@@ -0,0 +1,46 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "strings"
6
+ "testing"
7
+ )
8
+
9
+ // TestRunReportsAnUnloadableProjectWithoutPrintingADump verifies that a project
10
+ // the command cannot load exits non-zero, prints nothing to stdout, and names
11
+ // what it could not load.
12
+ //
13
+ // The benchmark viewer pipeline runs this command through `execFileSync`, which
14
+ // raises only on a non-zero exit and otherwise parses stdout as JSON. Every
15
+ // failure path here therefore has to be a non-zero exit: turning one into 0
16
+ // hands the pipeline an empty string, and the caller dies inside its JSON parse
17
+ // with an opaque error instead of reading the diagnostic this command already
18
+ // wrote. All three failure paths used to be unpinned, so replacing every
19
+ // `return 1` with `return 0` broke no test.
20
+ //
21
+ // 1. Run the command against a tsconfig that does not exist.
22
+ // 2. Assert it returns 1 and wrote nothing to stdout.
23
+ // 3. Assert stderr names the command and the path it could not load.
24
+ func TestRunReportsAnUnloadableProjectWithoutPrintingADump(t *testing.T) {
25
+ root := t.TempDir()
26
+
27
+ var out, errOut bytes.Buffer
28
+ restoreStdout, restoreStderr := stdout, stderr
29
+ stdout, stderr = &out, &errOut
30
+ defer func() { stdout, stderr = restoreStdout, restoreStderr }()
31
+
32
+ code := run([]string{"--cwd", root, "--tsconfig", "absent.json"})
33
+
34
+ if code != 1 {
35
+ t.Fatalf("graphdump exited %d for an unloadable project, want 1", code)
36
+ }
37
+ if out.Len() != 0 {
38
+ t.Fatalf("a failed run wrote %q to stdout; the viewer pipeline parses that stream as JSON", out.String())
39
+ }
40
+ if !strings.Contains(errOut.String(), "graphdump:") {
41
+ t.Fatalf("stderr does not identify the command: %q", errOut.String())
42
+ }
43
+ if !strings.Contains(errOut.String(), "absent.json") {
44
+ t.Fatalf("stderr does not name the tsconfig it could not load: %q", errOut.String())
45
+ }
46
+ }
@@ -26,6 +26,18 @@ type apiTransformResult struct {
26
26
  // content can influence a transformed module, so bundler caches invalidate
27
27
  // soundly without per-plugin reporting.
28
28
  Graph *driver.TransformGraph `json:"graph,omitempty"`
29
+ // Dependencies and DependenciesComplete are the envelope's dependency side
30
+ // channel. This lane runs no plugin transform at all — each output is the
31
+ // file's own parsed text — so every file is complete with an empty list
32
+ // unless a linked plugin is active and declares otherwise; see
33
+ // driver.Program.TransformDependenciesFor.
34
+ Dependencies map[string][]string `json:"dependencies,omitempty"`
35
+ DependenciesComplete []string `json:"dependenciesComplete,omitempty"`
36
+ // HostInputs are absolute native plugin config files evaluated while this
37
+ // generation loaded. JavaScript hosts merge them with descriptor inputs.
38
+ HostInputs []string `json:"hostInputs,omitempty"`
39
+ HostInputHashes map[string]*string `json:"hostInputHashes,omitempty"`
40
+ HostInputRealpaths map[string]*string `json:"hostInputRealpaths,omitempty"`
29
41
  }
30
42
 
31
43
  // runAPITransform implements the `api-transform` sub-command. It loads the
@@ -69,7 +81,11 @@ func runAPITransform(args []string) int {
69
81
  return 2
70
82
  }
71
83
  typescript := map[string]string{}
84
+ var dependencies driver.TransformDependencies
72
85
  var graph *driver.TransformGraph
86
+ var hostInputs []string
87
+ var hostInputHashes map[string]*string
88
+ var hostInputRealpaths map[string]*string
73
89
  if prog != nil {
74
90
  defer prog.Close()
75
91
  // Compute the reference graph before SourceFiles() runs linked plugin
@@ -81,12 +97,21 @@ func runAPITransform(args []string) int {
81
97
  typescript[apiOutputKey(cwd, file.FileName())] = file.Text()
82
98
  }
83
99
  diags = append(diags, prog.Diagnostics()...)
100
+ dependencies = prog.TransformDependenciesFor(cwd)
101
+ hostInputs = prog.PluginHostInputs()
102
+ hostInputHashes = prog.PluginHostInputHashes()
103
+ hostInputRealpaths = prog.PluginHostInputRealpaths()
84
104
  }
85
105
 
86
106
  result := apiTransformResult{
87
- Diagnostics: make([]apiCompileDiagnostic, 0, len(diags)),
88
- TypeScript: typescript,
89
- Graph: graph,
107
+ Dependencies: dependencies.Dependencies,
108
+ DependenciesComplete: dependencies.Complete,
109
+ Diagnostics: make([]apiCompileDiagnostic, 0, len(diags)),
110
+ Graph: graph,
111
+ HostInputs: hostInputs,
112
+ HostInputHashes: hostInputHashes,
113
+ HostInputRealpaths: hostInputRealpaths,
114
+ TypeScript: typescript,
90
115
  }
91
116
  for _, diag := range diags {
92
117
  result.Diagnostics = append(result.Diagnostics, toAPICompileDiagnostic(diag))
@@ -0,0 +1,85 @@
1
+ package main
2
+
3
+ import (
4
+ "os"
5
+ "os/exec"
6
+ "path/filepath"
7
+ "runtime"
8
+ "testing"
9
+ )
10
+
11
+ // TestAuxiliaryIdentityStateTracksRetargetsNotSourceContents proves the two
12
+ // owners of a selected lexical module path stay separate:
13
+ //
14
+ // 1. ordinary source bytes are owned by graphSession.sourceHashes, so an
15
+ // identity-only auxiliary state must not turn their edit into a reload;
16
+ // 2. retargeting the same lexical symlink or junction must invalidate even
17
+ // when the old and new target bytes agree.
18
+ func TestAuxiliaryIdentityStateTracksRetargetsNotSourceContents(t *testing.T) {
19
+ root := t.TempDir()
20
+ first := filepath.Join(root, "first")
21
+ second := filepath.Join(root, "second")
22
+ if err := os.MkdirAll(first, 0o755); err != nil {
23
+ t.Fatal(err)
24
+ }
25
+ if err := os.MkdirAll(second, 0o755); err != nil {
26
+ t.Fatal(err)
27
+ }
28
+ for _, directory := range []string{first, second} {
29
+ if err := os.WriteFile(filepath.Join(directory, "selection.ts"), []byte("export const value = 1;\n"), 0o644); err != nil {
30
+ t.Fatal(err)
31
+ }
32
+ }
33
+
34
+ link := filepath.Join(root, "linked")
35
+ createAuxiliaryDirectoryLink(t, first, link)
36
+ selected := filepath.Join(link, "selection.ts")
37
+ previous := captureDiskStates([]auxiliaryInput{{path: selected, identityOnly: true}})
38
+ initial := previous[selected]
39
+ if !initial.Exists || initial.Realpath == "" || !initial.IdentityOnly {
40
+ t.Fatalf("initial identity state = %+v", initial)
41
+ }
42
+
43
+ if err := os.WriteFile(filepath.Join(first, "selection.ts"), []byte("export const value = 2;\n"), 0o644); err != nil {
44
+ t.Fatal(err)
45
+ }
46
+ if diskStatesChanged(previous) {
47
+ t.Fatal("selected source contents leaked into identity-only auxiliary invalidation")
48
+ }
49
+
50
+ if err := os.Remove(link); err != nil {
51
+ t.Fatal(err)
52
+ }
53
+ createAuxiliaryDirectoryLink(t, second, link)
54
+ if !diskStatesChanged(previous) {
55
+ t.Fatal("same-byte lexical link retarget did not invalidate its physical identity")
56
+ }
57
+
58
+ merged := compactAuxiliaryInputs([]auxiliaryInput{
59
+ {path: selected, identityOnly: true},
60
+ {path: selected},
61
+ })
62
+ if len(merged) != 1 || merged[0].identityOnly {
63
+ t.Fatalf("content-sensitive duplicate did not win: %+v", merged)
64
+ }
65
+ }
66
+
67
+ func createAuxiliaryDirectoryLink(t *testing.T, target, link string) {
68
+ t.Helper()
69
+ if runtime.GOOS == "windows" {
70
+ command := exec.Command(
71
+ "node",
72
+ "-e",
73
+ `require("node:fs").symlinkSync(process.argv[1], process.argv[2], "junction")`,
74
+ target,
75
+ link,
76
+ )
77
+ if output, err := command.CombinedOutput(); err != nil {
78
+ t.Skipf("directory junction unavailable on this host: %v: %s", err, output)
79
+ }
80
+ return
81
+ }
82
+ if err := os.Symlink(target, link); err != nil {
83
+ t.Skipf("directory symlink unavailable on this host: %v", err)
84
+ }
85
+ }
@@ -0,0 +1,16 @@
1
+ //go:build !windows
2
+
3
+ package main
4
+
5
+ import "path/filepath"
6
+
7
+ func diskRealpath(path string) string {
8
+ resolved, err := filepath.EvalSymlinks(path)
9
+ if err != nil {
10
+ return ""
11
+ }
12
+ if absolute, err := filepath.Abs(resolved); err == nil {
13
+ resolved = absolute
14
+ }
15
+ return filepath.Clean(resolved)
16
+ }
@@ -0,0 +1,56 @@
1
+ //go:build windows
2
+
3
+ package main
4
+
5
+ import (
6
+ "path/filepath"
7
+ "strings"
8
+
9
+ "golang.org/x/sys/windows"
10
+ )
11
+
12
+ // diskRealpath asks the opened object for its final path. filepath.EvalSymlinks
13
+ // can leave a readable file below a directory junction unresolved on Windows,
14
+ // which would erase the physical identity this state exists to retain.
15
+ func diskRealpath(path string) string {
16
+ pointer, err := windows.UTF16PtrFromString(path)
17
+ if err != nil {
18
+ return ""
19
+ }
20
+ handle, err := windows.CreateFile(
21
+ pointer,
22
+ windows.FILE_READ_ATTRIBUTES,
23
+ windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
24
+ nil,
25
+ windows.OPEN_EXISTING,
26
+ windows.FILE_FLAG_BACKUP_SEMANTICS,
27
+ 0,
28
+ )
29
+ if err != nil {
30
+ return ""
31
+ }
32
+ defer windows.CloseHandle(handle)
33
+
34
+ buffer := make([]uint16, windows.MAX_PATH)
35
+ for {
36
+ length, err := windows.GetFinalPathNameByHandle(
37
+ handle,
38
+ &buffer[0],
39
+ uint32(len(buffer)),
40
+ 0,
41
+ )
42
+ if err != nil {
43
+ return ""
44
+ }
45
+ if length < uint32(len(buffer)) {
46
+ resolved := windows.UTF16ToString(buffer[:length])
47
+ if strings.HasPrefix(resolved, `\\?\UNC\`) {
48
+ resolved = `\\` + strings.TrimPrefix(resolved, `\\?\UNC\`)
49
+ } else {
50
+ resolved = strings.TrimPrefix(resolved, `\\?\`)
51
+ }
52
+ return filepath.Clean(resolved)
53
+ }
54
+ buffer = make([]uint16, length+1)
55
+ }
56
+ }