ttsc 0.16.2 → 0.16.5

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 (81) hide show
  1. package/README.md +96 -25
  2. package/cmd/graphbench/main.go +135 -0
  3. package/cmd/graphdump/main.go +60 -0
  4. package/cmd/ttscgraph/dump.go +69 -0
  5. package/cmd/ttscgraph/main.go +91 -0
  6. package/cmd/ttscgraph/run_dump_prints_the_graph_as_json_test.go +80 -0
  7. package/cmd/ttscgraph/run_help_version_and_bad_flags_exit_codes_test.go +75 -0
  8. package/cmd/utility-host/main.go +4 -1
  9. package/driver/overlay.go +64 -0
  10. package/driver/program.go +8 -1
  11. package/driver/session.go +85 -0
  12. package/internal/graph/build.go +330 -0
  13. package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +96 -0
  14. package/internal/graph/decorators.go +122 -0
  15. package/internal/graph/decorators_capture_name_and_literal_arguments_test.go +98 -0
  16. package/internal/graph/diagnostics.go +23 -0
  17. package/internal/graph/dump.go +390 -0
  18. package/internal/graph/dump_evidence_skips_leading_trivia_test.go +92 -0
  19. package/internal/graph/dump_serializes_the_full_graph_test.go +146 -0
  20. package/internal/graph/edge_spans_point_to_resolved_usage_test.go +90 -0
  21. package/internal/graph/edges.go +538 -0
  22. package/internal/graph/edges_cover_class_level_references_test.go +93 -0
  23. package/internal/graph/enum_initializer_calls_are_edges_test.go +63 -0
  24. package/internal/graph/exports.go +60 -0
  25. package/internal/graph/file_diagnostics_match_tsgo_code_and_location_test.go +59 -0
  26. package/internal/graph/file_diagnostics_report_code_column_and_message_test.go +69 -0
  27. package/internal/graph/function_locals_do_not_collide_test.go +66 -0
  28. package/internal/graph/gitignore.go +51 -0
  29. package/internal/graph/graph.go +119 -0
  30. package/internal/graph/heritage_edges_cover_implements_and_interface_extends_test.go +76 -0
  31. package/internal/graph/heritage_edges_keep_extends_and_implements_to_same_base_test.go +61 -0
  32. package/internal/graph/heritage_edges_record_extends_versus_implements_origin_test.go +54 -0
  33. package/internal/graph/heritage_edges_traverse_barrels_and_stop_at_external_leaves_test.go +112 -0
  34. package/internal/graph/namespace_dotted_form_qualifies_deeply_test.go +60 -0
  35. package/internal/graph/namespace_members_become_qualified_nodes_and_edges_test.go +100 -0
  36. package/internal/graph/node_identity_is_position_invariant_test.go +84 -0
  37. package/internal/graph/node_modifiers_emit_union_strings_test.go +118 -0
  38. package/internal/graph/nodes_are_marked_exported_through_the_export_table_test.go +61 -0
  39. package/internal/graph/overload_implementation_replaces_signature_test.go +54 -0
  40. package/internal/graph/property_member_edges_remain_owner_visible_test.go +83 -0
  41. package/internal/graph/resolve.go +102 -0
  42. package/internal/graph/resolve_prefers_implementation_declaration_test.go +50 -0
  43. package/internal/graph/resolve_unwraps_barrel_reexport_to_real_declaration_test.go +230 -0
  44. package/internal/graph/type_ref_edges_cover_typeof_as_satisfies_test.go +79 -0
  45. package/internal/graph/type_ref_edges_resolve_named_types_across_files_test.go +52 -0
  46. package/internal/graph/value_access_edges_cover_properties_test.go +96 -0
  47. package/internal/graph/value_call_edges_cover_jsx_components_test.go +82 -0
  48. package/internal/graph/value_call_edges_cover_methods_and_constructors_test.go +70 -0
  49. package/internal/graph/value_call_edges_cover_variable_bound_callables_test.go +46 -0
  50. package/internal/graph/value_call_edges_do_not_double_record_invoked_member_access_test.go +65 -0
  51. package/internal/graph/value_call_edges_record_construct_versus_call_origin_test.go +75 -0
  52. package/internal/graph/value_call_edges_resolve_across_files_and_dedupe_test.go +55 -0
  53. package/internal/graph/value_call_edges_skip_self_and_unresolved_test.go +74 -0
  54. package/lib/TtscService.d.ts +55 -0
  55. package/lib/TtscService.js +93 -0
  56. package/lib/TtscService.js.map +1 -0
  57. package/lib/compiler/internal/residentTransformProcess.d.ts +47 -0
  58. package/lib/compiler/internal/residentTransformProcess.js +187 -0
  59. package/lib/compiler/internal/residentTransformProcess.js.map +1 -0
  60. package/lib/compiler/internal/startResidentTransform.d.ts +26 -0
  61. package/lib/compiler/internal/startResidentTransform.js +98 -0
  62. package/lib/compiler/internal/startResidentTransform.js.map +1 -0
  63. package/lib/index.d.ts +1 -0
  64. package/lib/index.js +1 -0
  65. package/lib/index.js.map +1 -1
  66. package/lib/plugin/internal/loadProjectPlugins.js +17 -3
  67. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  68. package/lib/structures/ITtscPlugin.d.ts +7 -5
  69. package/lib/structures/ITtscPluginFactoryContext.d.ts +24 -1
  70. package/package.json +8 -8
  71. package/shim/ast/shim.go +6 -0
  72. package/shim/checker/shim.go +22 -12
  73. package/src/TtscService.ts +94 -0
  74. package/src/compiler/internal/residentTransformProcess.ts +216 -0
  75. package/src/compiler/internal/startResidentTransform.ts +126 -0
  76. package/src/index.ts +1 -0
  77. package/src/plugin/internal/loadProjectPlugins.ts +17 -3
  78. package/src/structures/ITtscPlugin.ts +7 -5
  79. package/src/structures/ITtscPluginFactoryContext.ts +26 -1
  80. package/utility/host.go +8 -2
  81. package/utility/serve.go +158 -0
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  A `typescript-go` toolchain for compiler-powered plugins and type-safe execution.
8
8
 
9
- Benchmarked against the legacy `tsc` + `eslint`/`prettier` path on real repositories; see the [benchmark guide](https://ttsc.dev/docs/benchmark) for per-project ratios.
9
+ Benchmarked against the legacy `tsc` + `eslint`/`prettier` path on real repositories; see the [benchmark guide](https://ttsc.dev/docs/benchmark/performance) for per-project ratios.
10
10
 
11
11
  - **`ttsc`**: build, check, and transform.
12
12
  - **`ttsx`**: execute TypeScript with type checking.
@@ -15,6 +15,8 @@ Benchmarked against the legacy `tsc` + `eslint`/`prettier` path on real reposito
15
15
  - **`@ttsc/lint`**: replaces `eslint` and `prettier`.
16
16
  - lint violations as TS compile errors.
17
17
  - format autofixes via `ttsc format`.
18
+ - **`@ttsc/graph`**: a code map for coding agents, over MCP.
19
+ - what calls what, and a change's blast radius, from the type checker.
18
20
  - **plugin support**: compiler-powered libraries, such as `typia`.
19
21
 
20
22
  ## Setup
@@ -49,29 +51,6 @@ Rewrite source files in place with the `@ttsc/lint` format rules:
49
51
  npx ttsc format
50
52
  ```
51
53
 
52
- ### VS Code Extension
53
-
54
- Install the VS Code extension for live TypeScript-Go editor features plus saved-state ttsc plugin diagnostics and actions.
55
-
56
- Install it from the VS Code Marketplace by searching `ttsc`, or run:
57
-
58
- ```bash
59
- npx @ttsc/vscode
60
- ```
61
-
62
- Then turn on format-on-save in `.vscode/settings.json`:
63
-
64
- ```jsonc
65
- "[typescript][typescriptreact]": {
66
- "editor.defaultFormatter": "samchon.ttsc",
67
- "editor.formatOnSave": true
68
- }
69
- ```
70
-
71
- Lint fixes stay off-save by default; opt in with `"editor.codeActionsOnSave": { "source.fixAll.ttsc": "explicit" }`.
72
-
73
- See [`@ttsc/vscode`](https://github.com/samchon/ttsc/tree/master/packages/vscode) for requirements and settings.
74
-
75
54
  ### Bundlers
76
55
 
77
56
  Use `@ttsc/unplugin` when a bundler owns your build.
@@ -109,6 +88,79 @@ Supported bundlers:
109
88
 
110
89
  See [`@ttsc/unplugin`](https://github.com/samchon/ttsc/tree/master/packages/unplugin) for full setup and adapter options.
111
90
 
91
+ ### React Native / Expo
92
+
93
+ React Native and Expo bundle with Metro, so the `ttsc` CLI and `@ttsc/unplugin` never run. `@ttsc/metro` is a Metro transformer that runs `ttsc` plugins on each TypeScript file, then hands the result to your existing Expo or React-Native Babel transformer.
94
+
95
+ ```bash
96
+ npm install -D ttsc @ttsc/lint typescript@rc
97
+ npm install -D @ttsc/metro
98
+ ```
99
+
100
+ Wrap your Metro config (CommonJS, the standard for `metro.config.js`):
101
+
102
+ ```js
103
+ // metro.config.js (Expo)
104
+ const { getDefaultConfig } = require("expo/metro-config");
105
+ const { withTtsc } = require("@ttsc/metro");
106
+
107
+ module.exports = withTtsc(getDefaultConfig(__dirname));
108
+ ```
109
+
110
+ For bare React Native, wrap `getDefaultConfig` from `@react-native/metro-config` instead. See [`@ttsc/metro`](https://github.com/samchon/ttsc/tree/master/packages/metro) for options and the v1 caveats.
111
+
112
+ ### VS Code Extension
113
+
114
+ Install the VS Code extension for live TypeScript-Go editor features plus saved-state ttsc plugin diagnostics and actions.
115
+
116
+ Install it from the VS Code Marketplace by searching `ttsc`, or run:
117
+
118
+ ```bash
119
+ npx @ttsc/vscode
120
+ ```
121
+
122
+ Then turn on format-on-save in `.vscode/settings.json`:
123
+
124
+ ```jsonc
125
+ "[typescript][typescriptreact]": {
126
+ "editor.defaultFormatter": "samchon.ttsc",
127
+ "editor.formatOnSave": true
128
+ }
129
+ ```
130
+
131
+ Lint fixes stay off-save by default; opt in with `"editor.codeActionsOnSave": { "source.fixAll.ttsc": "explicit" }`.
132
+
133
+ See [`@ttsc/vscode`](https://github.com/samchon/ttsc/tree/master/packages/vscode) for requirements and settings.
134
+
135
+ ### Coding Agents (MCP)
136
+
137
+ `@ttsc/graph` gives a coding agent a checker-resolved map of your project, over MCP.
138
+
139
+ It answers what relates to a symbol and what a change affects, straight from the type checker, so the agent stops grepping and re-reading files.
140
+
141
+ It also carries the project's full diagnostics: type errors, `@ttsc/lint` violations, and plugin findings. They are fused onto the graph, so an agent sees a change's reach over what is already broken before editing.
142
+
143
+ ```bash
144
+ npm install -D ttsc @ttsc/graph typescript@rc
145
+ ```
146
+
147
+ Point your agent's MCP client at it. For Claude Code:
148
+
149
+ ```json
150
+ {
151
+ "mcpServers": {
152
+ "ttsc-graph": {
153
+ "command": "npx",
154
+ "args": ["-y", "@ttsc/graph"]
155
+ }
156
+ }
157
+ }
158
+ ```
159
+
160
+ On codegraph's own agent-cost benchmark, Claude agents answer reading zero files, cutting tokens by 77% to 86% and tool calls by 94% to 95%. See [`@ttsc/graph`](https://github.com/samchon/ttsc/tree/master/packages/graph) and the [benchmark](https://ttsc.dev/docs/benchmark/graph).
161
+
162
+ Your agent picks the tools up from the MCP handshake and uses them on its own. See [Setup](https://ttsc.dev/docs/setup#coding-agents-ttscgraph) for the full walk-through.
163
+
112
164
  ## Plugins
113
165
 
114
166
  Plugins let libraries add compile-time checks, transforms, and type-driven code generation to normal `ttsc` and `ttsx` runs.
@@ -196,6 +248,23 @@ if (result.type === "success") {
196
248
  }
197
249
  ```
198
250
 
251
+ When one process transforms a project's files repeatedly (a watch server, an editor, a codegen tool), `TtscService` keeps the compiler host warm instead of recompiling per call. It compiles the project once, then answers per-file transform requests, and reflects in-memory edits through `updateFile`:
252
+
253
+ ```ts
254
+ import { TtscService } from "ttsc";
255
+
256
+ const service = new TtscService({ cwd: "./project" });
257
+ try {
258
+ const code = await service.transformFile("src/index.ts");
259
+ await service.updateFile("src/index.ts", "export const x: number = 2;\n");
260
+ const next = await service.transformFile("src/index.ts");
261
+ } finally {
262
+ service.dispose();
263
+ }
264
+ ```
265
+
266
+ `TtscService` runs through the linked-plugin host, so the project must declare at least one transform-stage plugin.
267
+
199
268
  See the [Programmatic API guide](https://ttsc.dev/docs/ttsc/api) for the full lifecycle, plugin overrides, and patterns. For browser embedding, see [`@ttsc/wasm`](https://github.com/samchon/ttsc/tree/master/packages/wasm) and the higher-level [`@ttsc/playground`](https://github.com/samchon/ttsc/tree/master/packages/playground) package.
200
269
 
201
270
  ### List of Plugins
@@ -204,9 +273,11 @@ See the [Programmatic API guide](https://ttsc.dev/docs/ttsc/api) for the full li
204
273
 
205
274
  - [`@ttsc/banner`](https://github.com/samchon/ttsc/tree/master/packages/banner): adds `@packageDocumentation` JSDoc banners.
206
275
  - [`@ttsc/lint`](https://github.com/samchon/ttsc/tree/master/packages/lint): lints and formats TypeScript source.
276
+ - [`@ttsc/graph`](https://github.com/samchon/ttsc/tree/master/packages/graph): MCP server exposing a checker-resolved code graph and diagnostics to coding agents.
207
277
  - [`@ttsc/paths`](https://github.com/samchon/ttsc/tree/master/packages/paths): rewrites source path aliases so JS and declaration emit receive relative imports.
208
278
  - [`@ttsc/strip`](https://github.com/samchon/ttsc/tree/master/packages/strip): removes configured calls and `debugger` statements.
209
279
  - [`@ttsc/unplugin`](https://github.com/samchon/ttsc/tree/master/packages/unplugin): runs `ttsc` plugins inside bundlers supported by `unplugin`.
280
+ - [`@ttsc/metro`](https://github.com/samchon/ttsc/tree/master/packages/metro): runs `ttsc` plugins inside Metro for React Native and Expo.
210
281
 
211
282
  Plugin authors should start from the [`Guide Documents`](https://ttsc.dev/docs).
212
283
 
@@ -227,4 +298,4 @@ Your [donation](https://github.com/sponsors/samchon) encourages `ttsc` developme
227
298
 
228
299
  - TypeScript runners: [`ts-node`](https://github.com/TypeStrong/ts-node) and [`tsx`](https://github.com/privatenumber/tsx)
229
300
  - Transformer tooling: [`ttypescript`](https://github.com/cevek/ttypescript) and [`ts-patch`](https://github.com/nonara/ts-patch)
230
- - Inspired by: [`typical`](https://github.com/elliots/typical) and [`tsgonest`](https://github.com/tsgonest/tsgonest)
301
+ - Inspired by: [`typical`](https://github.com/elliots/typical), [`tsgonest`](https://github.com/tsgonest/tsgonest) and [`codegraph`](https://github.com/colbymchenry/codegraph).
@@ -0,0 +1,135 @@
1
+ // Command graphbench measures @ttsc/graph against a real project: how long the
2
+ // resident Program takes to load, how long graph extraction takes on top of that
3
+ // already-built Program, how many nodes and edges result, and the codegraph-style
4
+ // "fair coverage" (share of symbol-bearing source files with at least one
5
+ // resolved cross-file edge). It prints one JSON line so an orchestrator can run
6
+ // it N times and take medians.
7
+ //
8
+ // Counts and coverage are deterministic; timings are indicative and only
9
+ // meaningful on a quiet host (see .codex/skills/benchmark).
10
+ package main
11
+
12
+ import (
13
+ "encoding/json"
14
+ "flag"
15
+ "fmt"
16
+ "os"
17
+ "time"
18
+
19
+ "github.com/samchon/ttsc/packages/ttsc/driver"
20
+ "github.com/samchon/ttsc/packages/ttsc/internal/graph"
21
+ )
22
+
23
+ type metrics struct {
24
+ Project string `json:"project"`
25
+ LoadMs float64 `json:"loadMs"`
26
+ BuildMs float64 `json:"buildMs"`
27
+ BuildShare float64 `json:"buildShareOfLoad"`
28
+ SourceFiles int `json:"sourceFiles"`
29
+ Nodes int `json:"nodes"`
30
+ ExternalNodes int `json:"externalNodes"`
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"`
36
+ }
37
+
38
+ func main() {
39
+ os.Exit(run())
40
+ }
41
+
42
+ func run() int {
43
+ cwd := flag.String("cwd", ".", "project root")
44
+ tsconfig := flag.String("tsconfig", "tsconfig.json", "tsconfig path, relative to cwd")
45
+ flag.Parse()
46
+
47
+ loadStart := time.Now()
48
+ prog, _, err := driver.LoadProgram(*cwd, *tsconfig, driver.LoadProgramOptions{})
49
+ loadMs := time.Since(loadStart).Seconds() * 1000
50
+ if err != nil {
51
+ fmt.Fprintf(os.Stderr, "graphbench: could not load %s/%s: %v\n", *cwd, *tsconfig, err)
52
+ return 1
53
+ }
54
+ if prog == nil {
55
+ fmt.Fprintf(os.Stderr, "graphbench: could not load %s/%s\n", *cwd, *tsconfig)
56
+ return 1
57
+ }
58
+ defer func() { _ = prog.Close() }()
59
+
60
+ buildStart := time.Now()
61
+ g := graph.Build(prog)
62
+ buildMs := time.Since(buildStart).Seconds() * 1000
63
+
64
+ m := summarize(g, len(prog.SourceFiles()))
65
+ m.Project = *cwd
66
+ m.LoadMs = loadMs
67
+ m.BuildMs = buildMs
68
+ if loadMs > 0 {
69
+ m.BuildShare = buildMs / loadMs
70
+ }
71
+
72
+ out, err := json.Marshal(m)
73
+ if err != nil {
74
+ fmt.Fprintf(os.Stderr, "graphbench: %v\n", err)
75
+ return 1
76
+ }
77
+ fmt.Println(string(out))
78
+ return 0
79
+ }
80
+
81
+ // summarize reduces a built graph to the reported metrics: node and external
82
+ // counts, edges per kind, and fair coverage (workspace files holding at least one
83
+ // node that an edge connects to a node in another file).
84
+ 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,
89
+ }
90
+ for _, edge := range g.Edges {
91
+ edges[string(edge.Kind)]++
92
+ }
93
+
94
+ fileOf := make(map[string]string, len(g.Nodes))
95
+ symbolFiles := map[string]bool{}
96
+ external := 0
97
+ for id, node := range g.Nodes {
98
+ fileOf[id] = node.File
99
+ if node.External {
100
+ external++
101
+ continue
102
+ }
103
+ symbolFiles[node.File] = true
104
+ }
105
+
106
+ coveredFiles := map[string]bool{}
107
+ for _, edge := range g.Edges {
108
+ from, to := fileOf[edge.From], fileOf[edge.To]
109
+ if from == "" || to == "" || from == to {
110
+ continue
111
+ }
112
+ if symbolFiles[from] {
113
+ coveredFiles[from] = true
114
+ }
115
+ if symbolFiles[to] {
116
+ coveredFiles[to] = true
117
+ }
118
+ }
119
+
120
+ coverage := 0.0
121
+ if len(symbolFiles) > 0 {
122
+ coverage = float64(len(coveredFiles)) / float64(len(symbolFiles))
123
+ }
124
+
125
+ 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,
134
+ }
135
+ }
@@ -0,0 +1,60 @@
1
+ // Command graphdump builds the @ttsc/graph code graph for a project and prints
2
+ // the entire graph (every node and every edge, with none of the MCP response
3
+ // caps) as one JSON document. It is the repo-internal one-shot used by the
4
+ // graph-bench viewer pipeline; the shipped equivalent is `ttscgraph dump`. Both
5
+ // serialize through graph.MarshalDump.
6
+ package main
7
+
8
+ import (
9
+ "flag"
10
+ "fmt"
11
+ "os"
12
+ "path/filepath"
13
+
14
+ shimtspath "github.com/microsoft/typescript-go/shim/tspath"
15
+
16
+ "github.com/samchon/ttsc/packages/ttsc/driver"
17
+ "github.com/samchon/ttsc/packages/ttsc/internal/graph"
18
+ )
19
+
20
+ func main() {
21
+ os.Exit(run())
22
+ }
23
+
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()
29
+
30
+ // Resolve the project root the same way LoadProgram does (absolute, then
31
+ // tsgo-normalized) so it shares the node file paths' drive-letter case on
32
+ // Windows; otherwise the dump's prefix-based relativization would miss and
33
+ // leave every path absolute.
34
+ root := *cwd
35
+ if abs, err := filepath.Abs(root); err == nil {
36
+ root = abs
37
+ }
38
+ root = shimtspath.ResolvePath(root)
39
+
40
+ prog, _, err := driver.LoadProgram(root, *tsconfig, driver.LoadProgramOptions{})
41
+ if err != nil {
42
+ fmt.Fprintf(os.Stderr, "graphdump: could not load %s/%s: %v\n", root, *tsconfig, err)
43
+ return 1
44
+ }
45
+ if prog == nil {
46
+ fmt.Fprintf(os.Stderr, "graphdump: could not load %s/%s\n", root, *tsconfig)
47
+ return 1
48
+ }
49
+ defer func() { _ = prog.Close() }()
50
+
51
+ g := graph.Build(prog)
52
+ ignored := graph.GitIgnoredFiles(root, g)
53
+ data, err := graph.MarshalDump(g, root, *tsconfig, ignored, graph.SourceTexts(prog), *pretty)
54
+ if err != nil {
55
+ fmt.Fprintf(os.Stderr, "graphdump: %v\n", err)
56
+ return 1
57
+ }
58
+ fmt.Println(string(data))
59
+ return 0
60
+ }
@@ -0,0 +1,69 @@
1
+ package main
2
+
3
+ import (
4
+ "flag"
5
+ "fmt"
6
+ "path/filepath"
7
+ "strings"
8
+
9
+ shimtspath "github.com/microsoft/typescript-go/shim/tspath"
10
+
11
+ "github.com/samchon/ttsc/packages/ttsc/driver"
12
+ "github.com/samchon/ttsc/packages/ttsc/internal/graph"
13
+ )
14
+
15
+ // runDump builds the full code graph for a project and prints it as JSON to
16
+ // stdout, then exits. Unlike serve it does not stay resident: it is the one-shot
17
+ // `ttscgraph dump` a user pipes into a file to feed the 3D viewer or any other
18
+ // tooling. Every node and edge is included, with none of the MCP response caps.
19
+ // Returns 0 on success, 1 on a load or serialize error, 2 on invalid invocation.
20
+ func runDump(args []string) int {
21
+ fs := flag.NewFlagSet("ttscgraph dump", flag.ContinueOnError)
22
+ fs.SetOutput(stderr)
23
+ cwdFlag := fs.String("cwd", "", "project root (defaults to process cwd)")
24
+ tsconfigFlag := fs.String("tsconfig", "tsconfig.json", "project tsconfig path")
25
+ prettyFlag := fs.Bool("pretty", false, "indent the JSON output")
26
+ if err := fs.Parse(args); err != nil {
27
+ return 2
28
+ }
29
+
30
+ cwd := strings.TrimSpace(*cwdFlag)
31
+ if cwd == "" {
32
+ resolved, err := getwd()
33
+ if err != nil {
34
+ fmt.Fprintf(stderr, "ttscgraph: could not resolve working directory: %v\n", err)
35
+ return 2
36
+ }
37
+ cwd = resolved
38
+ }
39
+ // Resolve the project root the same way LoadProgram does (absolute, then
40
+ // tsgo-normalized) so it shares the node file paths' drive-letter case on
41
+ // Windows; otherwise the dump's prefix-based relativization would miss and
42
+ // leave every path absolute.
43
+ if abs, err := filepath.Abs(cwd); err == nil {
44
+ cwd = abs
45
+ }
46
+ cwd = shimtspath.ResolvePath(cwd)
47
+ tsconfig := strings.TrimSpace(*tsconfigFlag)
48
+
49
+ prog, _, err := driver.LoadProgram(cwd, tsconfig, driver.LoadProgramOptions{})
50
+ if err != nil {
51
+ fmt.Fprintf(stderr, "ttscgraph: could not load %s/%s: %v\n", cwd, tsconfig, err)
52
+ return 1
53
+ }
54
+ if prog == nil {
55
+ fmt.Fprintf(stderr, "ttscgraph: could not load %s/%s\n", cwd, tsconfig)
56
+ return 1
57
+ }
58
+ defer func() { _ = prog.Close() }()
59
+
60
+ g := graph.Build(prog)
61
+ ignored := graph.GitIgnoredFiles(cwd, g)
62
+ data, err := graph.MarshalDump(g, cwd, tsconfig, ignored, graph.SourceTexts(prog), *prettyFlag)
63
+ if err != nil {
64
+ fmt.Fprintf(stderr, "ttscgraph: %v\n", err)
65
+ return 1
66
+ }
67
+ fmt.Fprintln(stdout, string(data))
68
+ return 0
69
+ }
@@ -0,0 +1,91 @@
1
+ // Command ttscgraph builds the checker-resolved code graph for a project and
2
+ // prints it as JSON (`ttscgraph dump`). It is the native data provider for
3
+ // @ttsc/graph: the JavaScript launcher runs `ttscgraph dump` once, then serves
4
+ // the Model Context Protocol from the in-memory graph itself. The MCP server,
5
+ // its tools, and their schemas live in @ttsc/graph (TypeScript); nothing here
6
+ // answers tool calls.
7
+ package main
8
+
9
+ import (
10
+ "fmt"
11
+ "io"
12
+ "os"
13
+ "runtime"
14
+ "strings"
15
+ )
16
+
17
+ // Build metadata; overwritten via -ldflags in release builds.
18
+ var (
19
+ version = "0.0.0-dev"
20
+ commit = "dev"
21
+ date = "unknown"
22
+ )
23
+
24
+ // Package-level streams so command tests can capture I/O without patching the
25
+ // os globals.
26
+ var (
27
+ stdout io.Writer = os.Stdout
28
+ stderr io.Writer = os.Stderr
29
+ )
30
+
31
+ // getwd is the seam command tests use to simulate a working-directory failure.
32
+ var getwd = os.Getwd
33
+
34
+ func main() {
35
+ os.Exit(run(os.Args[1:]))
36
+ }
37
+
38
+ // run dispatches the top-level command and returns an exit code. ttscgraph is
39
+ // dump-only; anything that is not help/version/dump prints usage and exits 2,
40
+ // since serving MCP is the launcher's job.
41
+ func run(args []string) int {
42
+ if len(args) > 0 {
43
+ switch args[0] {
44
+ case "-h", "--help", "help":
45
+ printHelp(stdout)
46
+ return 0
47
+ case "-v", "--version", "version":
48
+ printVersion(stdout)
49
+ return 0
50
+ case "dump":
51
+ return runDump(args[1:])
52
+ }
53
+ }
54
+ printHelp(stderr)
55
+ return 2
56
+ }
57
+
58
+ func printVersion(w io.Writer) {
59
+ fmt.Fprintf(
60
+ w,
61
+ "ttscgraph %s (commit %s, built %s, %s/%s, go %s)\n",
62
+ version,
63
+ commit,
64
+ date,
65
+ runtime.GOOS,
66
+ runtime.GOARCH,
67
+ runtime.Version(),
68
+ )
69
+ }
70
+
71
+ func printHelp(w io.Writer) {
72
+ fmt.Fprintln(w, strings.TrimSpace(`
73
+ ttscgraph — checker-resolved code graph for ttsc.
74
+
75
+ Builds the project's code graph and prints it as JSON. The @ttsc/graph launcher
76
+ runs this and serves the Model Context Protocol from the result; the MCP tools
77
+ live in @ttsc/graph, not here.
78
+
79
+ Usage:
80
+ ttscgraph dump [--cwd <dir>] [--tsconfig <path>] [--pretty] > graph.json
81
+ ttscgraph --version
82
+ ttscgraph --help
83
+
84
+ Dump:
85
+ dump Build the whole graph and print it as JSON to stdout, then
86
+ exit. Every node and edge, none of the MCP response caps.
87
+ --cwd <dir> Project root (defaults to the process working directory).
88
+ --tsconfig <path> Project tsconfig path (default: tsconfig.json).
89
+ --pretty Indent the JSON output.
90
+ `))
91
+ }
@@ -0,0 +1,80 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "os"
7
+ "path/filepath"
8
+ "testing"
9
+ )
10
+
11
+ // writeGraphFile writes content to path under a fixture project, creating parent
12
+ // directories as needed.
13
+ func writeGraphFile(t *testing.T, path, content string) {
14
+ t.Helper()
15
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
16
+ t.Fatal(err)
17
+ }
18
+ if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
19
+ t.Fatal(err)
20
+ }
21
+ }
22
+
23
+ // TestRunDumpPrintsTheGraphAsJSON verifies the `dump` subcommand end to end: run
24
+ // dispatches it, it loads the project, builds the graph, and prints the export
25
+ // JSON to stdout with exit 0. This is the shipped path a user runs as
26
+ // `npx @ttsc/graph dump > graph.json` to feed the 3D viewer, so a regression that
27
+ // stops it printing, or makes it serve instead of exit, would break that flow
28
+ // silently.
29
+ //
30
+ // 1. Write a two-function fixture (one call) under a temp project.
31
+ // 2. run dump --cwd <root> --tsconfig tsconfig.json, capturing stdout.
32
+ // 3. Assert exit 0 and that stdout is the graph envelope with nodes and edges.
33
+ func TestRunDumpPrintsTheGraphAsJSON(t *testing.T) {
34
+ root := t.TempDir()
35
+ writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{
36
+ "compilerOptions": {
37
+ "target": "ES2022",
38
+ "module": "commonjs",
39
+ "strict": true,
40
+ "rootDir": "src",
41
+ "outDir": "dist"
42
+ },
43
+ "files": ["src/main.ts"]
44
+ }
45
+ `)
46
+ writeGraphFile(t, filepath.Join(root, "src", "main.ts"), `export function helper(): void {}
47
+ export function main(): void {
48
+ helper();
49
+ }
50
+ `)
51
+
52
+ oldStdout, oldStderr := stdout, stderr
53
+ defer func() { stdout, stderr = oldStdout, oldStderr }()
54
+ var out, errBuf bytes.Buffer
55
+ stdout, stderr = &out, &errBuf
56
+
57
+ if code := run([]string{"dump", "--cwd", root, "--tsconfig", "tsconfig.json"}); code != 0 {
58
+ t.Fatalf("run dump exit = %d, want 0; stderr:\n%s", code, errBuf.String())
59
+ }
60
+
61
+ var dump struct {
62
+ Nodes []map[string]any `json:"nodes"`
63
+ Edges []map[string]any `json:"edges"`
64
+ }
65
+ if err := json.Unmarshal(out.Bytes(), &dump); err != nil {
66
+ t.Fatalf("dump output is not valid JSON: %v\n%s", err, out.String())
67
+ }
68
+ if len(dump.Nodes) == 0 || len(dump.Edges) == 0 {
69
+ t.Fatalf("expected nodes and edges, got %d/%d", len(dump.Nodes), len(dump.Edges))
70
+ }
71
+ // Each edge carries its endpoints and kind, and nothing more — the graph is
72
+ // wholly checker-resolved, so there is no per-edge trust flag to negotiate.
73
+ edge := dump.Edges[0]
74
+ if _, ok := edge["from"].(string); !ok {
75
+ t.Fatalf("edge missing from: %v", edge)
76
+ }
77
+ if _, ok := edge["kind"].(string); !ok {
78
+ t.Fatalf("edge missing kind: %v", edge)
79
+ }
80
+ }
@@ -0,0 +1,75 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "errors"
6
+ "strings"
7
+ "testing"
8
+ )
9
+
10
+ // TestRunHelpVersionAndBadFlagsExitCodes verifies the top-level run dispatcher
11
+ // returns the documented exit codes for the dump-only command surface: help and
12
+ // version short-circuit to 0, an unknown command prints usage and exits 2, and a
13
+ // dump that cannot resolve its working directory exits 2 with an explanation.
14
+ //
15
+ // These are the non-load paths a user hits with a typo or `--help`; each must
16
+ // resolve to a code without building a Program. The getwd seam stands in for an
17
+ // unreadable working directory so the resolve-failure branch is reachable
18
+ // without a real filesystem fault.
19
+ //
20
+ // 1. run --help and run --version exit 0, printing the command name / version.
21
+ // 2. run --bogus (unknown command) exits 2 and prints usage.
22
+ // 3. run dump --nope exits 2 (dump flag parse failure).
23
+ // 4. With getwd forced to fail, run dump exits 2 and explains why.
24
+ func TestRunHelpVersionAndBadFlagsExitCodes(t *testing.T) {
25
+ oldStdout, oldStderr, oldGetwd := stdout, stderr, getwd
26
+ defer func() { stdout, stderr, getwd = oldStdout, oldStderr, oldGetwd }()
27
+
28
+ // --help exits 0 and names the command on stdout.
29
+ var helpOut bytes.Buffer
30
+ stdout = &helpOut
31
+ if code := run([]string{"--help"}); code != 0 {
32
+ t.Fatalf("run --help exit = %d, want 0", code)
33
+ }
34
+ if !strings.Contains(helpOut.String(), "ttscgraph") {
35
+ t.Fatalf("run --help did not print the command name:\n%s", helpOut.String())
36
+ }
37
+
38
+ // --version exits 0 and prints a version-ish string.
39
+ var versionOut bytes.Buffer
40
+ stdout = &versionOut
41
+ if code := run([]string{"--version"}); code != 0 {
42
+ t.Fatalf("run --version exit = %d, want 0", code)
43
+ }
44
+ if got := versionOut.String(); !strings.Contains(got, "ttscgraph") || !strings.Contains(got, version) {
45
+ t.Fatalf("run --version did not print a version string:\n%s", got)
46
+ }
47
+
48
+ // An unknown command is an invalid invocation: print usage and exit 2.
49
+ var badErr bytes.Buffer
50
+ stderr = &badErr
51
+ if code := run([]string{"--bogus"}); code != 2 {
52
+ t.Fatalf("run --bogus exit = %d, want 2", code)
53
+ }
54
+ if !strings.Contains(badErr.String(), "Usage:") {
55
+ t.Fatalf("run --bogus exited 2 but did not print usage:\n%s", badErr.String())
56
+ }
57
+
58
+ // An unknown dump flag is an invalid invocation: dump's flag.Parse fails -> 2.
59
+ var dumpFlagErr bytes.Buffer
60
+ stderr = &dumpFlagErr
61
+ if code := run([]string{"dump", "--nope"}); code != 2 {
62
+ t.Fatalf("run dump --nope exit = %d, want 2", code)
63
+ }
64
+
65
+ // A getwd failure (no --cwd given) is an invalid invocation -> 2, explained.
66
+ var wdErr bytes.Buffer
67
+ stderr = &wdErr
68
+ getwd = func() (string, error) { return "", errors.New("boom") }
69
+ if code := run([]string{"dump"}); code != 2 {
70
+ t.Fatalf("run dump with getwd failure exit = %d, want 2", code)
71
+ }
72
+ if !strings.Contains(wdErr.String(), "could not resolve working directory") {
73
+ t.Fatalf("getwd failure did not explain itself:\n%s", wdErr.String())
74
+ }
75
+ }