ttsc 0.18.3 → 0.19.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 (173) hide show
  1. package/README.md +5 -6
  2. package/cmd/graphbench/main.go +1 -1
  3. package/cmd/platform/main.go +1 -48
  4. package/cmd/ttsc/build_branches_test.go +5 -1
  5. package/cmd/ttsc/main.go +0 -50
  6. package/cmd/ttsc/main_alias_branches_test.go +6 -12
  7. package/cmd/ttscgraph/dump.go +13 -3
  8. package/cmd/ttscgraph/main.go +18 -12
  9. package/cmd/ttscgraph/run_help_version_and_bad_flags_exit_codes_test.go +23 -5
  10. package/cmd/ttscgraph/serve.go +863 -0
  11. package/cmd/ttscgraph/serve_protocol_rejects_invalid_request_line_test.go +43 -0
  12. package/cmd/ttscgraph/serve_protocol_retries_session_creation_test.go +83 -0
  13. package/cmd/ttscgraph/serve_protocol_reuses_session_test.go +35 -0
  14. package/cmd/ttscgraph/serve_session_applies_source_edit_incrementally_test.go +55 -0
  15. package/cmd/ttscgraph/serve_session_fails_closed_on_deleted_config_test.go +68 -0
  16. package/cmd/ttscgraph/serve_session_fails_closed_on_invalid_config_test.go +50 -0
  17. package/cmd/ttscgraph/serve_session_keeps_missing_literal_root_unchanged_test.go +46 -0
  18. package/cmd/ttscgraph/serve_session_rebuilds_import_graph_change_test.go +60 -0
  19. package/cmd/ttscgraph/serve_session_reloads_created_literal_root_test.go +44 -0
  20. package/cmd/ttscgraph/serve_session_reloads_extended_config_change_test.go +45 -0
  21. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_conditional_export_test.go +60 -0
  22. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_config_types_test.go +44 -0
  23. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_excluded_import_test.go +62 -0
  24. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_package_export_test.go +46 -0
  25. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_package_import_test.go +46 -0
  26. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_path_alias_test.go +47 -0
  27. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_reference_path_test.go +42 -0
  28. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_root_dirs_target_test.go +46 -0
  29. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_type_reference_test.go +42 -0
  30. package/cmd/ttscgraph/serve_session_reloads_root_file_set_test.go +52 -0
  31. package/cmd/ttscgraph/serve_session_reloads_source_deleted_during_load_test.go +60 -0
  32. package/cmd/ttscgraph/serve_session_reloads_transitive_project_reference_test.go +55 -0
  33. package/cmd/ttscgraph/serve_session_reloads_when_program_plugin_linked_test.go +53 -0
  34. package/cmd/ttscgraph/serve_session_reuses_unchanged_preamble_source_test.go +45 -0
  35. package/cmd/ttscgraph/serve_session_reuses_unchanged_snapshot_test.go +52 -0
  36. package/cmd/ttscgraph/serve_session_revisits_source_edited_during_load_test.go +61 -0
  37. package/cmd/ttscgraph/serve_session_tolerates_os_invalid_resolution_candidate_test.go +56 -0
  38. package/cmd/ttscgraph/serve_session_tracks_all_module_specifier_forms_test.go +41 -0
  39. package/cmd/ttscgraph/serve_session_tracks_diamond_project_references_once_test.go +61 -0
  40. package/cmd/ttscserver/main.go +5 -3
  41. package/driver/lsp.go +13 -1
  42. package/driver/plugins.go +56 -5
  43. package/driver/program.go +25 -10
  44. package/driver/rewrite.go +381 -21
  45. package/driver/session.go +11 -3
  46. package/go.mod +1 -0
  47. package/internal/graph/build.go +297 -3
  48. package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +16 -2
  49. package/internal/graph/diagnostics.go +5 -4
  50. package/internal/graph/dump.go +60 -4
  51. package/internal/graph/dump_is_a_function_of_the_source_test.go +71 -0
  52. package/internal/graph/dump_serializes_the_full_graph_test.go +16 -4
  53. package/internal/graph/edges.go +122 -11
  54. package/internal/graph/exports.go +74 -17
  55. package/internal/graph/graph.go +33 -1
  56. package/internal/graph/node_identity_is_position_invariant_test.go +4 -2
  57. package/internal/graph/resolve.go +24 -0
  58. package/internal/graphsymbols/provider.go +50 -14
  59. package/internal/lspserver/lsp_native_plugin_source.go +77 -23
  60. package/internal/lspserver/lsp_plugin.go +19 -2
  61. package/internal/lspserver/lsp_proxy.go +244 -52
  62. package/internal/lspserver/lsp_server.go +35 -27
  63. package/internal/lspserver/lsp_symbols.go +25 -16
  64. package/lib/TtscCompiler.d.ts +13 -2
  65. package/lib/TtscCompiler.js +30 -20
  66. package/lib/TtscCompiler.js.map +1 -1
  67. package/lib/TtscService.js +9 -7
  68. package/lib/TtscService.js.map +1 -1
  69. package/lib/compiler/internal/compileProjectInMemory.js +15 -1
  70. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  71. package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +3 -0
  72. package/lib/compiler/internal/project/createNativeProjectContextArgs.js +14 -0
  73. package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -0
  74. package/lib/compiler/internal/project/readProjectConfig.js +53 -4
  75. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  76. package/lib/compiler/internal/project/resolveProjectConfig.d.ts +9 -2
  77. package/lib/compiler/internal/project/resolveProjectConfig.js +45 -16
  78. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  79. package/lib/compiler/internal/residentTransformProcess.d.ts +16 -5
  80. package/lib/compiler/internal/residentTransformProcess.js +68 -20
  81. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  82. package/lib/compiler/internal/runBuild.d.ts +18 -0
  83. package/lib/compiler/internal/runBuild.js +220 -24
  84. package/lib/compiler/internal/runBuild.js.map +1 -1
  85. package/lib/compiler/internal/runSingleFileEmit.js +7 -4
  86. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  87. package/lib/compiler/internal/sharedHostHelpers.d.ts +16 -0
  88. package/lib/compiler/internal/sharedHostHelpers.js +23 -0
  89. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  90. package/lib/compiler/internal/startResidentTransform.js +18 -1
  91. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  92. package/lib/compiler/internal/transformProjectInMemory.js +34 -8
  93. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  94. package/lib/flags/parser.d.ts +17 -0
  95. package/lib/flags/parser.js +11 -1
  96. package/lib/flags/parser.js.map +1 -1
  97. package/lib/flags/schema.js +7 -0
  98. package/lib/flags/schema.js.map +1 -1
  99. package/lib/launcher/internal/prepareExecution.js +21 -2
  100. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  101. package/lib/launcher/internal/runTtsc.js +13 -53
  102. package/lib/launcher/internal/runTtsc.js.map +1 -1
  103. package/lib/launcher/internal/runTtscserver.js +11 -0
  104. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  105. package/lib/launcher/internal/runTtsx.js +27 -11
  106. package/lib/launcher/internal/runTtsx.js.map +1 -1
  107. package/lib/launcher/internal/runtimeHooks.d.ts +81 -0
  108. package/lib/launcher/internal/runtimeHooks.js +632 -43
  109. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  110. package/lib/launcher/internal/servedSourceMap.d.ts +25 -0
  111. package/lib/launcher/internal/servedSourceMap.js +164 -0
  112. package/lib/launcher/internal/servedSourceMap.js.map +1 -0
  113. package/lib/plugin/internal/buildSourcePlugin.d.ts +146 -2
  114. package/lib/plugin/internal/buildSourcePlugin.js +651 -113
  115. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  116. package/lib/plugin/internal/loadProjectPlugins.d.ts +7 -0
  117. package/lib/plugin/internal/loadProjectPlugins.js +33 -10
  118. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  119. package/lib/structures/ITtscCompilerContext.d.ts +16 -2
  120. package/lib/structures/ITtscCompilerResult.d.ts +8 -5
  121. package/lib/structures/ITtscPluginCapabilities.d.ts +8 -0
  122. package/lib/structures/ITtscPluginFactoryContext.d.ts +14 -0
  123. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +3 -0
  124. package/lib/structures/internal/ITtscProjectIdentity.d.ts +17 -0
  125. package/lib/structures/internal/ITtscProjectIdentity.js +3 -0
  126. package/lib/structures/internal/ITtscProjectIdentity.js.map +1 -0
  127. package/lib/structures/internal/TtscCommonOptions.d.ts +8 -0
  128. package/package.json +11 -8
  129. package/shim/ast/lint.go +10 -0
  130. package/shim/ast/shim.go +44 -0
  131. package/shim/checker/enums_gen.go +19 -0
  132. package/shim/checker/shim.go +130 -0
  133. package/shim/diagnosticwriter/lint.go +30 -3
  134. package/shim/printer/shim.go +22 -4
  135. package/shim/scanner/regexp.go +27 -0
  136. package/src/TtscCompiler.ts +29 -22
  137. package/src/TtscService.ts +15 -7
  138. package/src/compiler/internal/compileProjectInMemory.ts +18 -1
  139. package/src/compiler/internal/project/createNativeProjectContextArgs.ts +15 -0
  140. package/src/compiler/internal/project/readProjectConfig.ts +61 -5
  141. package/src/compiler/internal/project/resolveProjectConfig.ts +51 -21
  142. package/src/compiler/internal/residentTransformProcess.ts +95 -21
  143. package/src/compiler/internal/runBuild.ts +313 -32
  144. package/src/compiler/internal/runSingleFileEmit.ts +7 -4
  145. package/src/compiler/internal/sharedHostHelpers.ts +24 -0
  146. package/src/compiler/internal/startResidentTransform.ts +21 -1
  147. package/src/compiler/internal/transformProjectInMemory.ts +49 -6
  148. package/src/flags/parser.ts +28 -1
  149. package/src/flags/schema.ts +8 -0
  150. package/src/launcher/internal/prepareExecution.ts +22 -2
  151. package/src/launcher/internal/runTtsc.ts +13 -61
  152. package/src/launcher/internal/runTtscserver.ts +14 -0
  153. package/src/launcher/internal/runTtsx.ts +28 -13
  154. package/src/launcher/internal/runtimeHooks.ts +771 -53
  155. package/src/launcher/internal/servedSourceMap.ts +183 -0
  156. package/src/plugin/internal/buildSourcePlugin.ts +859 -122
  157. package/src/plugin/internal/loadProjectPlugins.ts +36 -6
  158. package/src/structures/ITtscCompilerContext.ts +17 -2
  159. package/src/structures/ITtscCompilerResult.ts +8 -5
  160. package/src/structures/ITtscPluginCapabilities.ts +9 -0
  161. package/src/structures/ITtscPluginFactoryContext.ts +15 -0
  162. package/src/structures/internal/ITtscParsedProjectConfig.ts +3 -0
  163. package/src/structures/internal/ITtscProjectIdentity.ts +17 -0
  164. package/src/structures/internal/TtscCommonOptions.ts +8 -0
  165. package/tools/shim_audit/baseline.json +16 -7
  166. package/tools/shim_audit/main.go +777 -71
  167. package/tools/shim_audit/main_test.go +28 -1
  168. package/tools/shim_audit/producer_exemptions_require_rationale_test.go +42 -0
  169. package/tools/shim_audit/producer_surface_includes_exposed_methods_test.go +154 -0
  170. package/tools/shim_audit/producer_surface_rejects_unproducible_reference_test.go +142 -0
  171. package/utility/host.go +46 -20
  172. package/utility/serve.go +1 -1
  173. package/shim/checker/signature_api_completeness.go +0 -58
@@ -0,0 +1,863 @@
1
+ package main
2
+
3
+ import (
4
+ "bufio"
5
+ "crypto/sha256"
6
+ "encoding/json"
7
+ "errors"
8
+ "flag"
9
+ "fmt"
10
+ "io"
11
+ "os"
12
+ "path/filepath"
13
+ "slices"
14
+ "sort"
15
+ "strings"
16
+
17
+ shimast "github.com/microsoft/typescript-go/shim/ast"
18
+ shimtsoptions "github.com/microsoft/typescript-go/shim/tsoptions"
19
+ shimtspath "github.com/microsoft/typescript-go/shim/tspath"
20
+
21
+ "github.com/samchon/ttsc/packages/ttsc/driver"
22
+ "github.com/samchon/ttsc/packages/ttsc/internal/graph"
23
+ )
24
+
25
+ type serveRequest struct {
26
+ ID int `json:"id"`
27
+ }
28
+
29
+ type serveResponse struct {
30
+ Dump *graph.Dump `json:"dump,omitempty"`
31
+ Error string `json:"error,omitempty"`
32
+ ID int `json:"id"`
33
+ Mode string `json:"mode,omitempty"`
34
+ Changed bool `json:"changed"`
35
+ }
36
+
37
+ type graphSession struct {
38
+ cwd string
39
+ tsconfig string
40
+ compiler *driver.Session
41
+ configHashes map[string][sha256.Size]byte
42
+ auxStates map[string]diskState
43
+ sourceHashes map[string][sha256.Size]byte
44
+ rootFiles []string
45
+ initialized bool
46
+ }
47
+
48
+ func newGraphSession(cwd, tsconfig string) (*graphSession, error) {
49
+ session := &graphSession{cwd: cwd, tsconfig: tsconfig}
50
+ if err := session.reload(); err != nil {
51
+ return nil, err
52
+ }
53
+ return session, nil
54
+ }
55
+
56
+ func (s *graphSession) Close() error {
57
+ if s.compiler == nil {
58
+ return nil
59
+ }
60
+ return s.compiler.Close()
61
+ }
62
+
63
+ func (s *graphSession) Snapshot() (*graph.Dump, string, bool, error) {
64
+ if !s.initialized {
65
+ dump := s.buildDump()
66
+ s.initialized = true
67
+ return &dump, "initial", true, nil
68
+ }
69
+
70
+ configChanged, err := hashesChanged(s.configHashes)
71
+ if err != nil {
72
+ return nil, "", false, err
73
+ }
74
+ if configChanged {
75
+ if err := s.reload(); err != nil {
76
+ return nil, "", false, err
77
+ }
78
+ dump := s.buildDump()
79
+ return &dump, "reload", true, nil
80
+ }
81
+
82
+ if diskStatesChanged(s.auxStates) {
83
+ if err := s.reload(); err != nil {
84
+ return nil, "", false, err
85
+ }
86
+ dump := s.buildDump()
87
+ return &dump, "reload", true, nil
88
+ }
89
+
90
+ roots, err := projectRootFiles(s.compiler.Program(), true)
91
+ if err != nil {
92
+ return nil, "", false, err
93
+ }
94
+ if !slices.Equal(s.rootFiles, roots) {
95
+ if err := s.reload(); err != nil {
96
+ return nil, "", false, err
97
+ }
98
+ dump := s.buildDump()
99
+ return &dump, "reload", true, nil
100
+ }
101
+
102
+ changed, deleted, err := changedSources(s.sourceHashes)
103
+ if err != nil {
104
+ return nil, "", false, err
105
+ }
106
+ if deleted {
107
+ if err := s.reload(); err != nil {
108
+ return nil, "", false, err
109
+ }
110
+ dump := s.buildDump()
111
+ return &dump, "reload", true, nil
112
+ }
113
+ if len(changed) == 0 {
114
+ return nil, "unchanged", false, nil
115
+ }
116
+ if s.compiler.Program().HasLinkedProgramPlugins() {
117
+ if err := s.reload(); err != nil {
118
+ return nil, "", false, err
119
+ }
120
+ dump := s.buildDump()
121
+ return &dump, "reload", true, nil
122
+ }
123
+
124
+ mode := "incremental"
125
+ paths := make([]string, 0, len(changed))
126
+ for path := range changed {
127
+ paths = append(paths, path)
128
+ }
129
+ sort.Strings(paths)
130
+ for _, path := range paths {
131
+ if reused := s.compiler.Apply(path, changed[path]); !reused {
132
+ mode = "rebuild"
133
+ }
134
+ current, exists := s.compiler.SourceText(path)
135
+ expected := driver.ApplySourcePreambleToFile(path, changed[path], s.compiler.Program().SourcePreamble)
136
+ if !exists || current != expected {
137
+ if err := s.reload(); err != nil {
138
+ return nil, "", false, err
139
+ }
140
+ dump := s.buildDump()
141
+ return &dump, "reload", true, nil
142
+ }
143
+ }
144
+ if err := s.captureState(); err != nil {
145
+ return nil, "", false, err
146
+ }
147
+ dump := s.buildDump()
148
+ return &dump, mode, true, nil
149
+ }
150
+
151
+ func (s *graphSession) reload() error {
152
+ next, diags, err := driver.NewSession(s.cwd, s.tsconfig, driver.LoadProgramOptions{})
153
+ if err != nil {
154
+ return err
155
+ }
156
+ if next == nil {
157
+ if len(diags) == 0 {
158
+ return errors.New("ttscgraph: compiler session was not created")
159
+ }
160
+ return invalidProjectError(diags)
161
+ }
162
+ previous := s.compiler
163
+ s.compiler = next
164
+ if err := s.captureState(); err != nil {
165
+ _ = next.Close()
166
+ s.compiler = previous
167
+ return err
168
+ }
169
+ if previous != nil {
170
+ _ = previous.Close()
171
+ }
172
+ return nil
173
+ }
174
+
175
+ func (s *graphSession) captureState() error {
176
+ program := s.compiler.Program()
177
+ configs, err := parsedConfigs(program)
178
+ if err != nil {
179
+ return err
180
+ }
181
+ configHashes, err := hashFiles(configFiles(configs))
182
+ if err != nil {
183
+ return err
184
+ }
185
+ sourceHashes, err := hashProgramSources(program)
186
+ if err != nil {
187
+ return err
188
+ }
189
+ inputs := auxiliaryInputs(program, configs, s.cwd)
190
+ inputs = append(inputs, missingRootInputs(configs, sourceHashes)...)
191
+ s.configHashes = configHashes
192
+ s.auxStates = captureDiskStates(compactSortedStrings(inputs))
193
+ s.sourceHashes = sourceHashes
194
+ s.rootFiles = projectRootFilesFromConfigs(configs, false)
195
+ return nil
196
+ }
197
+
198
+ // missingRootInputs returns config root files absent from the loaded program.
199
+ // A literal `files` entry keeps its name whether or not the file exists, so
200
+ // neither the root-set comparison nor source hashing notices when such a root
201
+ // is created later; tracking the missing path as a freshness input does.
202
+ func missingRootInputs(configs []*shimtsoptions.ParsedCommandLine, sourceHashes map[string][sha256.Size]byte) []string {
203
+ missing := []string{}
204
+ for _, parsed := range configs {
205
+ for _, file := range parsed.FileNames() {
206
+ if _, tracked := sourceHashes[file]; !tracked {
207
+ missing = append(missing, file)
208
+ }
209
+ }
210
+ }
211
+ return missing
212
+ }
213
+
214
+ func (s *graphSession) buildDump() graph.Dump {
215
+ program := s.compiler.Program()
216
+ built := graph.Build(program)
217
+ return graph.NewDump(
218
+ built,
219
+ s.cwd,
220
+ s.tsconfig,
221
+ graph.GitIgnoredFiles(s.cwd, built),
222
+ graph.SourceTexts(program),
223
+ )
224
+ }
225
+
226
+ func configFiles(configs []*shimtsoptions.ParsedCommandLine) []string {
227
+ files := []string{}
228
+ for _, parsed := range configs {
229
+ files = append(files, parsed.ConfigName())
230
+ files = append(files, parsed.ExtendedSourceFiles()...)
231
+ }
232
+ return compactSortedStrings(files)
233
+ }
234
+
235
+ func projectRootFiles(program *driver.Program, reload bool) ([]string, error) {
236
+ configs, err := parsedConfigs(program)
237
+ if err != nil {
238
+ return nil, err
239
+ }
240
+ return projectRootFilesFromConfigs(configs, reload), nil
241
+ }
242
+
243
+ func projectRootFilesFromConfigs(configs []*shimtsoptions.ParsedCommandLine, reload bool) []string {
244
+ roots := []string{}
245
+ for _, parsed := range configs {
246
+ current := parsed
247
+ if reload {
248
+ current = parsed.ReloadFileNamesOfParsedCommandLine(driver.DefaultFS())
249
+ }
250
+ config := current.ConfigName()
251
+ for _, file := range current.FileNames() {
252
+ roots = append(roots, config+"\x00"+file)
253
+ }
254
+ }
255
+ return compactSortedStrings(roots)
256
+ }
257
+
258
+ func parsedConfigs(program *driver.Program) ([]*shimtsoptions.ParsedCommandLine, error) {
259
+ if program == nil || program.ParsedConfig == nil {
260
+ return nil, errors.New("ttscgraph: compiler program omitted its parsed config")
261
+ }
262
+ resolved := make(map[string]*shimtsoptions.ParsedCommandLine)
263
+ for _, parsed := range program.TSProgram.GetResolvedProjectReferences() {
264
+ if parsed != nil {
265
+ resolved[shimtspath.ResolvePath(parsed.ConfigName())] = parsed
266
+ }
267
+ }
268
+ configs := []*shimtsoptions.ParsedCommandLine{}
269
+ pending := []*shimtsoptions.ParsedCommandLine{program.ParsedConfig}
270
+ seen := make(map[string]struct{})
271
+ for len(pending) > 0 {
272
+ parsed := pending[0]
273
+ pending = pending[1:]
274
+ config := shimtspath.ResolvePath(parsed.ConfigName())
275
+ if _, exists := seen[config]; exists {
276
+ continue
277
+ }
278
+ seen[config] = struct{}{}
279
+ configs = append(configs, parsed)
280
+ for _, reference := range parsed.ResolvedProjectReferencePaths() {
281
+ reference = shimtspath.ResolvePath(reference)
282
+ child := resolved[reference]
283
+ if child == nil {
284
+ fs := program.FS
285
+ cwd := filepath.Dir(reference)
286
+ var diags []driver.Diagnostic
287
+ var err error
288
+ child, diags, err = driver.ParseTSConfig(fs, cwd, reference, driver.DefaultHost(cwd, fs), nil)
289
+ if err != nil {
290
+ return nil, err
291
+ }
292
+ if child == nil {
293
+ if len(diags) == 0 {
294
+ return nil, fmt.Errorf("ttscgraph: project reference was not parsed: %s", reference)
295
+ }
296
+ return nil, invalidProjectError(diags)
297
+ }
298
+ resolved[reference] = child
299
+ }
300
+ pending = append(pending, child)
301
+ }
302
+ }
303
+ return configs, nil
304
+ }
305
+
306
+ func invalidProjectError(diags []driver.Diagnostic) error {
307
+ messages := make([]string, len(diags))
308
+ for i, diag := range diags {
309
+ messages[i] = diag.String()
310
+ }
311
+ return fmt.Errorf("ttscgraph: invalid project: %s", strings.Join(messages, "; "))
312
+ }
313
+
314
+ func hashProgramSources(program *driver.Program) (map[string][sha256.Size]byte, error) {
315
+ hashes := make(map[string][sha256.Size]byte)
316
+ for _, source := range program.TSProgram.SourceFiles() {
317
+ // Virtual sources (tsgo's `bundled:///` libs) have no on-disk identity;
318
+ // real project files always carry an absolute path.
319
+ if source == nil || !filepath.IsAbs(source.FileName()) {
320
+ continue
321
+ }
322
+ info, err := os.Stat(source.FileName())
323
+ if err != nil {
324
+ if errors.Is(err, os.ErrNotExist) {
325
+ // The file vanished while the compiler session was loading. Hash the
326
+ // resident text so the next snapshot revisits the path, observes the
327
+ // deletion, and reloads instead of serving the vanished file forever.
328
+ hashes[source.FileName()] = sha256.Sum256([]byte(source.Text()))
329
+ }
330
+ continue
331
+ }
332
+ if info.IsDir() {
333
+ continue
334
+ }
335
+ content, err := os.ReadFile(source.FileName())
336
+ if err != nil {
337
+ return nil, fmt.Errorf("ttscgraph: read %s: %w", source.FileName(), err)
338
+ }
339
+ rawHash := sha256.Sum256(content)
340
+ expected := driver.ApplySourcePreambleToFile(source.FileName(), string(content), program.SourcePreamble)
341
+ if source.Text() == expected {
342
+ hashes[source.FileName()] = rawHash
343
+ } else {
344
+ // Force the next snapshot to revisit a file that changed while the
345
+ // compiler session was loading instead of blessing mismatched disk text.
346
+ hashes[source.FileName()] = sha256.Sum256([]byte(source.Text()))
347
+ }
348
+ }
349
+ return hashes, nil
350
+ }
351
+
352
+ func hashFiles(paths []string) (map[string][sha256.Size]byte, error) {
353
+ hashes := make(map[string][sha256.Size]byte, len(paths))
354
+ for _, path := range paths {
355
+ content, err := os.ReadFile(path)
356
+ if err != nil {
357
+ return nil, fmt.Errorf("ttscgraph: read %s: %w", path, err)
358
+ }
359
+ hashes[path] = sha256.Sum256(content)
360
+ }
361
+ return hashes, nil
362
+ }
363
+
364
+ func hashesChanged(previous map[string][sha256.Size]byte) (bool, error) {
365
+ for path, oldHash := range previous {
366
+ content, err := os.ReadFile(path)
367
+ if err != nil {
368
+ if errors.Is(err, os.ErrNotExist) {
369
+ return true, nil
370
+ }
371
+ return false, fmt.Errorf("ttscgraph: read %s: %w", path, err)
372
+ }
373
+ if sha256.Sum256(content) != oldHash {
374
+ return true, nil
375
+ }
376
+ }
377
+ return false, nil
378
+ }
379
+
380
+ func changedSources(previous map[string][sha256.Size]byte) (map[string]string, bool, error) {
381
+ changed := map[string]string{}
382
+ for path, oldHash := range previous {
383
+ content, err := os.ReadFile(path)
384
+ if err != nil {
385
+ if errors.Is(err, os.ErrNotExist) {
386
+ return nil, true, nil
387
+ }
388
+ return nil, false, fmt.Errorf("ttscgraph: read %s: %w", path, err)
389
+ }
390
+ if sha256.Sum256(content) != oldHash {
391
+ changed[path] = string(content)
392
+ }
393
+ }
394
+ return changed, false, nil
395
+ }
396
+
397
+ type diskState struct {
398
+ Hash [sha256.Size]byte
399
+ Exists bool
400
+ }
401
+
402
+ type packageTarget struct {
403
+ path string
404
+ wildcard string
405
+ }
406
+
407
+ // captureDiskStates records the freshness state of speculative resolution
408
+ // candidates. Most candidates do not exist, and a module specifier can name a
409
+ // path the host OS cannot even parse (`./style.css?inline`, a `data:` URL on
410
+ // Windows), so any path that is neither a readable file nor a directory is
411
+ // recorded as absent instead of failing the snapshot: the recorded state only
412
+ // needs to flip when the candidate becomes resolvable.
413
+ func captureDiskStates(paths []string) map[string]diskState {
414
+ states := make(map[string]diskState, len(paths))
415
+ for _, path := range paths {
416
+ content, err := os.ReadFile(path)
417
+ if err != nil {
418
+ if info, statErr := os.Stat(path); statErr == nil && info.IsDir() {
419
+ states[path] = diskState{Exists: true}
420
+ } else {
421
+ states[path] = diskState{}
422
+ }
423
+ continue
424
+ }
425
+ states[path] = diskState{Hash: sha256.Sum256(content), Exists: true}
426
+ }
427
+ return states
428
+ }
429
+
430
+ func diskStatesChanged(previous map[string]diskState) bool {
431
+ paths := make([]string, 0, len(previous))
432
+ for path := range previous {
433
+ paths = append(paths, path)
434
+ }
435
+ current := captureDiskStates(paths)
436
+ for path, state := range previous {
437
+ if current[path] != state {
438
+ return true
439
+ }
440
+ }
441
+ return false
442
+ }
443
+
444
+ func auxiliaryInputs(program *driver.Program, configs []*shimtsoptions.ParsedCommandLine, cwd string) []string {
445
+ inputs := []string{
446
+ filepath.Join(cwd, ".gitignore"),
447
+ filepath.Join(cwd, ".git", "info", "exclude"),
448
+ filepath.Join(cwd, "package.json"),
449
+ filepath.Join(cwd, "package-lock.json"),
450
+ filepath.Join(cwd, "pnpm-lock.yaml"),
451
+ filepath.Join(cwd, "yarn.lock"),
452
+ filepath.Join(cwd, "bun.lock"),
453
+ filepath.Join(cwd, "bun.lockb"),
454
+ }
455
+ for _, source := range program.TSProgram.SourceFiles() {
456
+ file := source.FileName()
457
+ if file == "" || strings.HasPrefix(file, "bundled:///") {
458
+ continue
459
+ }
460
+ directory := filepath.Dir(file)
461
+ inputs = appendAncestorInputs(inputs, directory, cwd)
462
+ for _, reference := range source.ReferencedFiles {
463
+ inputs = append(inputs, fileCandidates(filepath.Join(directory, filepath.FromSlash(reference.FileName)))...)
464
+ }
465
+ for _, reference := range source.TypeReferenceDirectives {
466
+ inputs = append(inputs, typeReferenceCandidates(configs, directory, cwd, reference.FileName)...)
467
+ }
468
+ for _, specifier := range sourceModuleSpecifiers(source) {
469
+ resolved := program.TSProgram.GetResolvedModuleFromModuleSpecifier(source, specifier)
470
+ if resolved != nil && resolved.IsResolved() {
471
+ continue
472
+ }
473
+ inputs = append(inputs, unresolvedModuleCandidates(configs, directory, cwd, specifier.Text())...)
474
+ }
475
+ }
476
+ // Config `types` entries request type packages without any source syntax, so
477
+ // a missing one (e.g. a generated typeRoots package) must contribute the same
478
+ // candidates as a triple-slash type directive.
479
+ for _, parsed := range configs {
480
+ if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
481
+ continue
482
+ }
483
+ for _, name := range parsed.ParsedConfig.CompilerOptions.Types {
484
+ inputs = append(inputs, typeReferenceCandidates(configs, parsed.GetCurrentDirectory(), cwd, name)...)
485
+ }
486
+ }
487
+ return compactSortedStrings(inputs)
488
+ }
489
+
490
+ func typeReferenceCandidates(configs []*shimtsoptions.ParsedCommandLine, directory, cwd, name string) []string {
491
+ candidates := []string{}
492
+ for _, parsed := range configs {
493
+ if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
494
+ continue
495
+ }
496
+ for _, root := range parsed.ParsedConfig.CompilerOptions.TypeRoots {
497
+ candidates = append(candidates, fileCandidates(filepath.Join(root, filepath.FromSlash(name)))...)
498
+ }
499
+ }
500
+ for current := filepath.Clean(directory); ; current = filepath.Dir(current) {
501
+ candidates = append(candidates, fileCandidates(filepath.Join(current, "node_modules", "@types", filepath.FromSlash(name)))...)
502
+ if current == filepath.Clean(cwd) || filepath.Dir(current) == current {
503
+ return candidates
504
+ }
505
+ }
506
+ }
507
+
508
+ func sourceModuleSpecifiers(source *shimast.SourceFile) []*shimast.Node {
509
+ if source == nil {
510
+ return nil
511
+ }
512
+ specifiers := []*shimast.Node{}
513
+ var walk func(*shimast.Node) bool
514
+ walk = func(node *shimast.Node) bool {
515
+ if node == nil {
516
+ return false
517
+ }
518
+ var specifier *shimast.Node
519
+ switch node.Kind {
520
+ case shimast.KindImportDeclaration, shimast.KindJSImportDeclaration:
521
+ specifier = node.AsImportDeclaration().ModuleSpecifier
522
+ case shimast.KindExportDeclaration:
523
+ specifier = node.AsExportDeclaration().ModuleSpecifier
524
+ case shimast.KindImportEqualsDeclaration:
525
+ reference := node.AsImportEqualsDeclaration().ModuleReference
526
+ if reference != nil && reference.Kind == shimast.KindExternalModuleReference {
527
+ specifier = reference.AsExternalModuleReference().Expression
528
+ }
529
+ case shimast.KindImportType:
530
+ argument := node.AsImportTypeNode().Argument
531
+ if argument != nil && argument.Kind == shimast.KindLiteralType {
532
+ specifier = argument.AsLiteralTypeNode().Literal
533
+ }
534
+ case shimast.KindCallExpression:
535
+ call := node.AsCallExpression()
536
+ if isModuleSpecifierCall(call) && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
537
+ specifier = call.Arguments.Nodes[0]
538
+ }
539
+ }
540
+ if specifier != nil && (specifier.Kind == shimast.KindStringLiteral || specifier.Kind == shimast.KindNoSubstitutionTemplateLiteral) {
541
+ specifiers = append(specifiers, specifier)
542
+ }
543
+ node.ForEachChild(walk)
544
+ return false
545
+ }
546
+ walk(source.AsNode())
547
+ return specifiers
548
+ }
549
+
550
+ func isModuleSpecifierCall(call *shimast.CallExpression) bool {
551
+ if call == nil || call.Expression == nil {
552
+ return false
553
+ }
554
+ if call.Expression.Kind == shimast.KindImportKeyword {
555
+ return true
556
+ }
557
+ return call.Expression.Kind == shimast.KindIdentifier && call.Expression.Text() == "require"
558
+ }
559
+
560
+ func appendAncestorInputs(inputs []string, directory, stop string) []string {
561
+ stop = filepath.Clean(stop)
562
+ for current := filepath.Clean(directory); ; current = filepath.Dir(current) {
563
+ inputs = append(inputs, filepath.Join(current, "package.json"), filepath.Join(current, ".gitignore"))
564
+ if current == stop || filepath.Dir(current) == current {
565
+ return inputs
566
+ }
567
+ }
568
+ }
569
+
570
+ func unresolvedModuleCandidates(configs []*shimtsoptions.ParsedCommandLine, directory, cwd, specifier string) []string {
571
+ if specifier == "" {
572
+ return nil
573
+ }
574
+ if strings.HasPrefix(specifier, ".") {
575
+ candidates := fileCandidates(filepath.Clean(filepath.Join(directory, filepath.FromSlash(specifier))))
576
+ return append(candidates, rootDirsCandidates(configs, directory, specifier)...)
577
+ }
578
+ candidates := compilerOptionCandidates(configs, specifier)
579
+ if strings.HasPrefix(specifier, "#") {
580
+ for current := filepath.Clean(directory); ; current = filepath.Dir(current) {
581
+ candidates = append(candidates, packageManifestCandidates(current, specifier)...)
582
+ if current == filepath.Clean(cwd) || filepath.Dir(current) == current {
583
+ return candidates
584
+ }
585
+ }
586
+ }
587
+ for current := filepath.Clean(directory); ; current = filepath.Dir(current) {
588
+ base := filepath.Join(current, "node_modules", filepath.FromSlash(specifier))
589
+ root := packageRoot(base, specifier)
590
+ candidates = append(candidates, fileCandidates(base)...)
591
+ candidates = append(candidates, filepath.Join(root, "package.json"))
592
+ candidates = append(candidates, packageManifestCandidates(root, packageSubpath(specifier))...)
593
+ if current == filepath.Clean(cwd) || filepath.Dir(current) == current {
594
+ break
595
+ }
596
+ }
597
+ return candidates
598
+ }
599
+
600
+ func packageManifestCandidates(root, wildcard string) []string {
601
+ content, err := os.ReadFile(filepath.Join(root, "package.json"))
602
+ if err != nil {
603
+ return nil
604
+ }
605
+ var manifest struct {
606
+ Main string `json:"main"`
607
+ Module string `json:"module"`
608
+ Types string `json:"types"`
609
+ Typings string `json:"typings"`
610
+ Exports any `json:"exports"`
611
+ Imports any `json:"imports"`
612
+ TypesVersions any `json:"typesVersions"`
613
+ }
614
+ if json.Unmarshal(content, &manifest) != nil {
615
+ return nil
616
+ }
617
+ defaultWildcard := strings.TrimPrefix(strings.TrimPrefix(wildcard, "./"), "#")
618
+ targets := []packageTarget{
619
+ {path: manifest.Main, wildcard: defaultWildcard},
620
+ {path: manifest.Module, wildcard: defaultWildcard},
621
+ {path: manifest.Types, wildcard: defaultWildcard},
622
+ {path: manifest.Typings, wildcard: defaultWildcard},
623
+ }
624
+ exportRequest := "."
625
+ if wildcard != "" && !strings.HasPrefix(wildcard, "#") {
626
+ exportRequest = "./" + strings.TrimPrefix(wildcard, "./")
627
+ }
628
+ collectPackageMappingTargets(manifest.Exports, exportRequest, defaultWildcard, &targets)
629
+ collectPackageMappingTargets(manifest.Imports, wildcard, defaultWildcard, &targets)
630
+ collectPackageTargets(manifest.TypesVersions, defaultWildcard, &targets)
631
+ candidates := []string{}
632
+ for _, target := range targets {
633
+ if target.path == "" || filepath.IsAbs(target.path) || strings.Contains(target.path, "://") {
634
+ continue
635
+ }
636
+ path := strings.Replace(target.path, "*", target.wildcard, 1)
637
+ candidates = append(candidates, fileCandidates(filepath.Join(root, filepath.FromSlash(path)))...)
638
+ }
639
+ return candidates
640
+ }
641
+
642
+ func collectPackageMappingTargets(value any, request, wildcard string, targets *[]packageTarget) {
643
+ if object, ok := value.(map[string]any); ok {
644
+ mapping := false
645
+ for key := range object {
646
+ if strings.HasPrefix(key, ".") || strings.HasPrefix(key, "#") {
647
+ mapping = true
648
+ break
649
+ }
650
+ }
651
+ if mapping {
652
+ for pattern, child := range object {
653
+ matched, ok := matchPathPattern(pattern, request)
654
+ if ok {
655
+ collectPackageTargets(child, matched, targets)
656
+ }
657
+ }
658
+ return
659
+ }
660
+ }
661
+ collectPackageTargets(value, wildcard, targets)
662
+ }
663
+
664
+ func collectPackageTargets(value any, wildcard string, targets *[]packageTarget) {
665
+ switch value := value.(type) {
666
+ case string:
667
+ *targets = append(*targets, packageTarget{path: value, wildcard: wildcard})
668
+ case []any:
669
+ for _, child := range value {
670
+ collectPackageTargets(child, wildcard, targets)
671
+ }
672
+ case map[string]any:
673
+ for _, child := range value {
674
+ collectPackageTargets(child, wildcard, targets)
675
+ }
676
+ }
677
+ }
678
+
679
+ func packageSubpath(specifier string) string {
680
+ parts := strings.Split(filepath.ToSlash(specifier), "/")
681
+ count := 1
682
+ if strings.HasPrefix(specifier, "@") && len(parts) > 1 {
683
+ count = 2
684
+ }
685
+ if len(parts) <= count {
686
+ return ""
687
+ }
688
+ return strings.Join(parts[count:], "/")
689
+ }
690
+
691
+ func compilerOptionCandidates(configs []*shimtsoptions.ParsedCommandLine, specifier string) []string {
692
+ candidates := []string{}
693
+ for _, parsed := range configs {
694
+ if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
695
+ continue
696
+ }
697
+ options := parsed.ParsedConfig.CompilerOptions
698
+ if options.BaseUrl != "" {
699
+ candidates = append(candidates, fileCandidates(filepath.Join(options.BaseUrl, filepath.FromSlash(specifier)))...)
700
+ }
701
+ if options.Paths == nil {
702
+ continue
703
+ }
704
+ base := options.GetPathsBasePath(parsed.GetCurrentDirectory())
705
+ for pattern, targets := range options.Paths.Entries() {
706
+ matched, ok := matchPathPattern(pattern, specifier)
707
+ if !ok {
708
+ continue
709
+ }
710
+ for _, target := range targets {
711
+ target = strings.Replace(target, "*", matched, 1)
712
+ candidates = append(candidates, fileCandidates(filepath.Join(base, filepath.FromSlash(target)))...)
713
+ }
714
+ }
715
+ }
716
+ return candidates
717
+ }
718
+
719
+ func rootDirsCandidates(configs []*shimtsoptions.ParsedCommandLine, directory, specifier string) []string {
720
+ candidates := []string{}
721
+ for _, parsed := range configs {
722
+ if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
723
+ continue
724
+ }
725
+ roots := parsed.ParsedConfig.CompilerOptions.RootDirs
726
+ for _, sourceRoot := range roots {
727
+ relative, err := filepath.Rel(sourceRoot, directory)
728
+ if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
729
+ continue
730
+ }
731
+ suffix := filepath.Join(relative, filepath.FromSlash(specifier))
732
+ for _, targetRoot := range roots {
733
+ candidates = append(candidates, fileCandidates(filepath.Join(targetRoot, suffix))...)
734
+ }
735
+ }
736
+ }
737
+ return candidates
738
+ }
739
+
740
+ func matchPathPattern(pattern, specifier string) (string, bool) {
741
+ star := strings.IndexByte(pattern, '*')
742
+ if star < 0 {
743
+ return "", pattern == specifier
744
+ }
745
+ prefix := pattern[:star]
746
+ suffix := pattern[star+1:]
747
+ if len(specifier) < len(prefix)+len(suffix) || !strings.HasPrefix(specifier, prefix) || !strings.HasSuffix(specifier, suffix) {
748
+ return "", false
749
+ }
750
+ return specifier[len(prefix) : len(specifier)-len(suffix)], true
751
+ }
752
+
753
+ func packageRoot(base, specifier string) string {
754
+ parts := strings.Split(filepath.ToSlash(specifier), "/")
755
+ count := 1
756
+ if strings.HasPrefix(specifier, "@") && len(parts) > 1 {
757
+ count = 2
758
+ }
759
+ suffixCount := len(parts) - count
760
+ root := base
761
+ for range suffixCount {
762
+ root = filepath.Dir(root)
763
+ }
764
+ return root
765
+ }
766
+
767
+ func fileCandidates(base string) []string {
768
+ candidates := []string{base}
769
+ extension := strings.ToLower(filepath.Ext(base))
770
+ if extension == ".js" || extension == ".jsx" || extension == ".mjs" || extension == ".cjs" {
771
+ base = strings.TrimSuffix(base, filepath.Ext(base))
772
+ }
773
+ for _, suffix := range []string{".ts", ".tsx", ".mts", ".cts", ".d.ts", ".d.mts", ".d.cts", ".js", ".jsx", ".mjs", ".cjs", ".json"} {
774
+ candidates = append(candidates, base+suffix, filepath.Join(base, "index"+suffix))
775
+ }
776
+ candidates = append(candidates, filepath.Join(base, "package.json"))
777
+ return candidates
778
+ }
779
+
780
+ func compactSortedStrings(input []string) []string {
781
+ out := make([]string, 0, len(input))
782
+ for _, value := range input {
783
+ if strings.TrimSpace(value) != "" {
784
+ out = append(out, value)
785
+ }
786
+ }
787
+ sort.Strings(out)
788
+ return slices.Compact(out)
789
+ }
790
+
791
+ func runServe(args []string) int {
792
+ fs := flag.NewFlagSet("ttscgraph serve", flag.ContinueOnError)
793
+ fs.SetOutput(stderr)
794
+ cwdFlag := fs.String("cwd", "", "project root (defaults to process cwd)")
795
+ tsconfigFlag := fs.String("tsconfig", "tsconfig.json", "project tsconfig path")
796
+ if err := fs.Parse(args); err != nil {
797
+ return 2
798
+ }
799
+
800
+ cwd := strings.TrimSpace(*cwdFlag)
801
+ if cwd == "" {
802
+ resolved, err := getwd()
803
+ if err != nil {
804
+ fmt.Fprintf(stderr, "ttscgraph: could not resolve working directory: %v\n", err)
805
+ return 2
806
+ }
807
+ cwd = resolved
808
+ }
809
+ if abs, err := filepath.Abs(cwd); err == nil {
810
+ cwd = abs
811
+ }
812
+ cwd = shimtspath.ResolvePath(cwd)
813
+ tsconfig := strings.TrimSpace(*tsconfigFlag)
814
+
815
+ return serveSnapshots(os.Stdin, stdout, cwd, tsconfig)
816
+ }
817
+
818
+ func serveSnapshots(input io.Reader, output io.Writer, cwd, tsconfig string) int {
819
+ scanner := bufio.NewScanner(input)
820
+ scanner.Buffer(make([]byte, 64*1024), 1024*1024)
821
+ encoder := json.NewEncoder(output)
822
+ var session *graphSession
823
+ defer func() {
824
+ if session != nil {
825
+ _ = session.Close()
826
+ }
827
+ }()
828
+
829
+ for scanner.Scan() {
830
+ line := strings.TrimSpace(scanner.Text())
831
+ if line == "" {
832
+ continue
833
+ }
834
+ var request serveRequest
835
+ if err := json.Unmarshal([]byte(line), &request); err != nil {
836
+ _ = encoder.Encode(serveResponse{Error: fmt.Sprintf("invalid request: %v", err)})
837
+ continue
838
+ }
839
+ if session == nil {
840
+ created, err := newGraphSession(cwd, tsconfig)
841
+ if err != nil {
842
+ _ = encoder.Encode(serveResponse{ID: request.ID, Error: err.Error()})
843
+ continue
844
+ }
845
+ session = created
846
+ }
847
+ dump, mode, changed, err := session.Snapshot()
848
+ response := serveResponse{ID: request.ID, Dump: dump, Mode: mode, Changed: changed}
849
+ if err != nil {
850
+ response.Error = err.Error()
851
+ response.Dump = nil
852
+ }
853
+ if err := encoder.Encode(response); err != nil {
854
+ fmt.Fprintf(stderr, "ttscgraph: write serve response: %v\n", err)
855
+ return 1
856
+ }
857
+ }
858
+ if err := scanner.Err(); err != nil {
859
+ fmt.Fprintf(stderr, "ttscgraph: read serve request: %v\n", err)
860
+ return 1
861
+ }
862
+ return 0
863
+ }