ttsc 0.19.3 → 0.20.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 +7 -5
  2. package/cmd/graphdump/main.go +2 -4
  3. package/cmd/ttsc/filter.go +12 -4
  4. package/cmd/ttsc/filter_resolves_flag_names_case_insensitively_test.go +53 -0
  5. package/cmd/ttsc/flags_gen.go +3 -3
  6. package/cmd/ttscgraph/dump.go +5 -7
  7. package/cmd/ttscgraph/run_dump_emits_only_checker_verified_member_relations_test.go +118 -0
  8. package/cmd/ttscgraph/serve.go +58 -308
  9. package/cmd/ttscgraph/serve_session_keeps_base_url_fallback_unchanged_test.go +49 -0
  10. package/cmd/ttscgraph/serve_session_keeps_esm_directory_candidate_unchanged_test.go +61 -0
  11. package/cmd/ttscgraph/serve_session_keeps_exports_blocked_file_candidates_unchanged_test.go +64 -0
  12. package/cmd/ttscgraph/serve_session_keeps_inactive_conditional_export_unchanged_test.go +66 -0
  13. package/cmd/ttscgraph/serve_session_keeps_lower_priority_module_candidate_unchanged_test.go +67 -0
  14. package/cmd/ttscgraph/serve_session_keeps_lower_priority_paths_pattern_unchanged_test.go +58 -0
  15. package/cmd/ttscgraph/serve_session_keeps_nonlongest_root_dirs_candidate_unchanged_test.go +54 -0
  16. package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +50 -0
  17. package/cmd/ttscgraph/serve_session_reloads_module_suffix_candidate_test.go +41 -0
  18. package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +216 -0
  19. package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +103 -0
  20. package/driver/graph.go +12 -6
  21. package/driver/lsp.go +30 -0
  22. package/driver/program.go +37 -1
  23. package/driver/resolution_candidates.go +819 -0
  24. package/driver/windowsjunction/windowsjunction.go +40 -0
  25. package/go.mod +2 -0
  26. package/internal/graph/build.go +45 -11
  27. package/internal/graph/dump.go +276 -100
  28. package/internal/graph/dump_checkout_paths_are_stable_test.go +210 -0
  29. package/internal/graph/dump_diagnostics_ride_the_generation_that_built_it_test.go +9 -3
  30. package/internal/graph/dump_paths.go +172 -0
  31. package/internal/graph/dump_paths_are_portable_and_injective_test.go +93 -0
  32. package/internal/graph/dump_uses_ecma_line_terminators_test.go +87 -0
  33. package/internal/graph/duplicate_variable_object_members_keep_first_declaration_test.go +61 -0
  34. package/internal/graph/edges.go +208 -0
  35. package/internal/graph/graph.go +143 -5
  36. package/internal/graph/line_model.go +122 -0
  37. package/internal/graph/member_relation_edges_follow_checker_validity_test.go +85 -0
  38. package/internal/graph/member_relation_edges_preserve_checker_boundaries_test.go +191 -0
  39. package/internal/graph/member_relation_edges_reject_checker_invalid_pairs_test.go +119 -0
  40. package/internal/graph/node_ids_preserve_hash_bearing_components_test.go +41 -0
  41. package/internal/graph/node_modifiers_emit_union_strings_test.go +4 -1
  42. package/internal/graph/object_literal_members_ride_on_the_snapshot_test.go +192 -0
  43. package/internal/graph/object_member_signatures_preserve_literal_whitespace_test.go +67 -0
  44. package/internal/graph/object_members.go +229 -0
  45. package/internal/graph/provenance.go +88 -30
  46. package/internal/graph/source_texts_cover_every_resident_program_source_test.go +73 -0
  47. package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +66 -0
  48. package/internal/graphsymbols/provider.go +26 -91
  49. package/internal/lspserver/lsp_clean_didopen_refreshes_compiler_state_test.go +144 -0
  50. package/internal/lspserver/lsp_completion.go +109 -0
  51. package/internal/lspserver/lsp_completion_gate_never_refuses_a_matching_line_test.go +50 -0
  52. package/internal/lspserver/lsp_completion_hints_longest_trigger_wins_test.go +149 -0
  53. package/internal/lspserver/lsp_completion_hints_nearest_trigger_wins_test.go +37 -0
  54. package/internal/lspserver/lsp_completion_merges_into_upstream_response_test.go +149 -0
  55. package/internal/lspserver/lsp_completion_nearest_hint_replaces_nearest_filter_test.go +43 -0
  56. package/internal/lspserver/lsp_completion_preserves_completion_list_extensions_test.go +99 -0
  57. package/internal/lspserver/lsp_completion_refuses_hints_inside_a_string_literal_test.go +69 -0
  58. package/internal/lspserver/lsp_completion_replaces_full_filter_in_rank_order_test.go +85 -0
  59. package/internal/lspserver/lsp_completion_request.go +455 -0
  60. package/internal/lspserver/lsp_completion_scope.go +530 -0
  61. package/internal/lspserver/lsp_completion_scope_bench_test.go +41 -0
  62. package/internal/lspserver/lsp_completion_scope_classifies_regex_statement_and_operator_boundaries_test.go +39 -0
  63. package/internal/lspserver/lsp_completion_scope_ignores_jsdoc_bytes_in_literals_test.go +108 -0
  64. package/internal/lspserver/lsp_completion_scope_refuses_regex_hints_test.go +52 -0
  65. package/internal/lspserver/lsp_completion_trigger_uses_last_rune_test.go +24 -0
  66. package/internal/lspserver/lsp_deleted_watched_file_withdraws_plugin_diagnostics_test.go +110 -0
  67. package/internal/lspserver/lsp_didsave_keeps_the_incremental_changed_uri_path_test.go +78 -0
  68. package/internal/lspserver/lsp_hints_absence_is_not_an_error_test.go +46 -0
  69. package/internal/lspserver/lsp_hints_refresh_coalesces_concurrent_requests_test.go +82 -0
  70. package/internal/lspserver/lsp_hints_refresh_ignores_a_superseded_generation_test.go +40 -0
  71. package/internal/lspserver/lsp_hints_refresh_keeps_each_producer_isolated_test.go +64 -0
  72. package/internal/lspserver/lsp_hints_refresh_replaces_the_session_snapshot_test.go +66 -0
  73. package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +90 -0
  74. package/internal/lspserver/lsp_hints_refresh_serves_completion_while_running_test.go +91 -0
  75. package/internal/lspserver/lsp_index_byte_from_bounds_its_search_start_test.go +38 -0
  76. package/internal/lspserver/lsp_initialize_constrains_position_encoding_to_utf16_test.go +205 -0
  77. package/internal/lspserver/lsp_late_completion_trigger_is_reported_once_test.go +105 -0
  78. package/internal/lspserver/lsp_native_completion_hints_decode_flat_lint_wire_test.go +66 -0
  79. package/internal/lspserver/lsp_native_completion_hints_grouped_compatibility_boundary_test.go +60 -0
  80. package/internal/lspserver/lsp_native_plugin_source.go +315 -2
  81. package/internal/lspserver/lsp_native_resident.go +304 -0
  82. package/internal/lspserver/lsp_plugin.go +85 -8
  83. package/internal/lspserver/lsp_plugin_completion_resolve_stays_local_test.go +82 -0
  84. package/internal/lspserver/lsp_position_conversions_span_astral_and_combining_runes_test.go +119 -0
  85. package/internal/lspserver/lsp_proxy.go +601 -9
  86. package/internal/lspserver/lsp_refresh.go +70 -0
  87. package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +43 -0
  88. package/internal/lspserver/lsp_resident_maps_verb_args_test.go +34 -0
  89. package/internal/lspserver/lsp_server.go +13 -1
  90. package/internal/lspserver/lsp_watched_file_changes_reach_compiler_caches_test.go +172 -0
  91. package/lib/TtscCompiler.d.ts +3 -1
  92. package/lib/TtscCompiler.js +5 -1
  93. package/lib/TtscCompiler.js.map +1 -1
  94. package/lib/TtscService.d.ts +13 -3
  95. package/lib/TtscService.js +6 -6
  96. package/lib/TtscService.js.map +1 -1
  97. package/lib/compiler/internal/project/readConfigJson.d.ts +30 -0
  98. package/lib/compiler/internal/project/readConfigJson.js +208 -0
  99. package/lib/compiler/internal/project/readConfigJson.js.map +1 -0
  100. package/lib/compiler/internal/project/readProjectConfig.js +53 -149
  101. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  102. package/lib/compiler/internal/residentTransformProcess.d.ts +15 -2
  103. package/lib/compiler/internal/residentTransformProcess.js +159 -39
  104. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  105. package/lib/compiler/internal/runBuild.d.ts +6 -0
  106. package/lib/compiler/internal/runBuild.js +143 -44
  107. package/lib/compiler/internal/runBuild.js.map +1 -1
  108. package/lib/compiler/internal/startResidentTransform.d.ts +5 -1
  109. package/lib/compiler/internal/startResidentTransform.js +3 -1
  110. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  111. package/lib/compiler/internal/transformProjectInMemory.js +12 -2
  112. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  113. package/lib/flags/parser.d.ts +19 -0
  114. package/lib/flags/parser.js +78 -36
  115. package/lib/flags/parser.js.map +1 -1
  116. package/lib/flags/schema.d.ts +49 -11
  117. package/lib/flags/schema.js +94 -22
  118. package/lib/flags/schema.js.map +1 -1
  119. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +5 -0
  120. package/lib/internal/assertSafeExplicitCacheDirectory.js +26 -0
  121. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +1 -0
  122. package/lib/launcher/internal/runTtsc.js +107 -69
  123. package/lib/launcher/internal/runTtsc.js.map +1 -1
  124. package/lib/launcher/internal/runTtsx.js +28 -41
  125. package/lib/launcher/internal/runTtsx.js.map +1 -1
  126. package/lib/launcher/internal/runtimeHooks.d.ts +15 -0
  127. package/lib/launcher/internal/runtimeHooks.js +18 -1
  128. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  129. package/lib/launcher/internal/watchTopology.d.ts +43 -0
  130. package/lib/launcher/internal/watchTopology.js +360 -0
  131. package/lib/launcher/internal/watchTopology.js.map +1 -0
  132. package/lib/plugin/internal/buildSourcePlugin.d.ts +14 -3
  133. package/lib/plugin/internal/buildSourcePlugin.js +247 -61
  134. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  135. package/lib/plugin/internal/loadProjectPlugins.js +63 -2
  136. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  137. package/lib/structures/ITtscCompilerTransformation.d.ts +16 -0
  138. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
  139. package/package.json +8 -8
  140. package/shim/ast/shim.go +48 -3
  141. package/shim/astnav/go.mod +14 -0
  142. package/shim/astnav/go.sum +20 -0
  143. package/shim/astnav/shim.go +15 -0
  144. package/shim/checker/shim.go +100 -0
  145. package/shim/core/shim.go +47 -0
  146. package/shim/diagnosticwriter/lint.go +50 -0
  147. package/src/TtscCompiler.ts +5 -1
  148. package/src/TtscService.ts +37 -10
  149. package/src/compiler/internal/project/readConfigJson.ts +209 -0
  150. package/src/compiler/internal/project/readProjectConfig.ts +62 -158
  151. package/src/compiler/internal/residentTransformProcess.ts +197 -39
  152. package/src/compiler/internal/runBuild.ts +169 -47
  153. package/src/compiler/internal/startResidentTransform.ts +12 -1
  154. package/src/compiler/internal/transformProjectInMemory.ts +12 -1
  155. package/src/flags/parser.ts +113 -42
  156. package/src/flags/schema.ts +113 -31
  157. package/src/internal/assertSafeExplicitCacheDirectory.ts +29 -0
  158. package/src/launcher/internal/runTtsc.ts +117 -68
  159. package/src/launcher/internal/runTtsx.ts +29 -44
  160. package/src/launcher/internal/runtimeHooks.ts +30 -2
  161. package/src/launcher/internal/watchTopology.ts +466 -0
  162. package/src/plugin/internal/buildSourcePlugin.ts +326 -64
  163. package/src/plugin/internal/loadProjectPlugins.ts +80 -2
  164. package/src/structures/ITtscCompilerTransformation.ts +17 -0
  165. package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
  166. package/tools/shim_audit/baseline.json +0 -3
  167. package/tools/shim_audit/main.go +42 -14
  168. package/tools/shim_audit/shim_dir_coverage_test.go +25 -0
  169. package/utility/flags_gen.go +3 -3
  170. package/utility/host.go +9 -3
@@ -12,6 +12,8 @@ import {
12
12
  getString,
13
13
  parseFlags,
14
14
  } from "../../flags/parser";
15
+ import { resolveFlagSpec } from "../../flags/schema";
16
+ import { assertSafeExplicitCacheDirectory } from "../../internal/assertSafeExplicitCacheDirectory";
15
17
  import {
16
18
  isPathWithin,
17
19
  legacyGlobalCacheTargets,
@@ -21,6 +23,7 @@ import {
21
23
  import type { TtscBuildOptions } from "../../structures/internal/TtscBuildOptions";
22
24
  import { getCompilerVersionText } from "./getCompilerVersionText";
23
25
  import { resolveCacheDir } from "./resolveCacheDir";
26
+ import { WatchTopology } from "./watchTopology";
24
27
 
25
28
  /**
26
29
  * CLI entry point for `ttsc`. Dispatches argv to the appropriate build lane
@@ -41,13 +44,17 @@ export function runTtsc(
41
44
  }
42
45
 
43
46
  const [command, ...rest] = argv as [string, ...string[]];
44
- switch (command) {
45
- case "-h":
47
+ // A leading flag is dispatched by its schema identity, so every spelling the
48
+ // compiler accepts (`--HELP`, `-Version`) reaches the same branch instead of
49
+ // falling through to the build lane where nothing reads it back out.
50
+ // `resolveFlagSpec` ignores dash-less tokens, so the bare `help` / `version`
51
+ // subcommand words below keep their own meaning.
52
+ const leadingFlag = resolveFlagSpec(command)?.name;
53
+ switch (leadingFlag ?? command) {
46
54
  case "--help":
47
55
  case "help":
48
56
  printHelp();
49
57
  return 0;
50
- case "-v":
51
58
  case "--version":
52
59
  case "version":
53
60
  process.stdout.write(`${getCompilerVersionText()}\n`);
@@ -66,8 +73,7 @@ export function runTtsc(
66
73
  return runClean(rest);
67
74
  case "prepare":
68
75
  return runPrepare(rest);
69
- case "-p":
70
- case "--project":
76
+ case "--tsconfig":
71
77
  return runCompatibleBuild(argv, "build");
72
78
  default:
73
79
  if (isBuildAlias(command)) {
@@ -104,7 +110,7 @@ type TtscMode = "build" | "check" | "fix" | "format";
104
110
 
105
111
  function runCompatibleBuild(argv: readonly string[], mode: TtscMode): number {
106
112
  const checkOnly = mode !== "build";
107
- const options = normalizeBuildOptions(parseBuildArgs(argv, checkOnly));
113
+ const options = normalizeBuildOptions(parseBuildArgs(argv));
108
114
  if (mode === "fix") {
109
115
  if (options.emit === true) {
110
116
  throw new Error("ttsc: fix and --emit are mutually exclusive");
@@ -132,16 +138,17 @@ function runCompatibleBuild(argv: readonly string[], mode: TtscMode): number {
132
138
  }
133
139
  return runWatch(options, checkOnly);
134
140
  }
135
- if (options.files.length !== 0) {
141
+ const buildOptions = checkOnly ? { ...options, emit: false } : options;
142
+ if (buildOptions.files.length !== 0) {
136
143
  if (mode === "fix") {
137
144
  throw new Error("ttsc: fix requires a project, not single-file mode");
138
145
  }
139
146
  if (mode === "format") {
140
147
  throw new Error("ttsc: format requires a project, not single-file mode");
141
148
  }
142
- return runSingleFile(options);
149
+ return runSingleFile(buildOptions);
143
150
  }
144
- const result = runBuild(checkOnly ? { ...options, emit: false } : options);
151
+ const result = runBuild(buildOptions);
145
152
  if (result.stdout) process.stdout.write(result.stdout);
146
153
  if (result.stderr) process.stderr.write(result.stderr);
147
154
  return result.status;
@@ -189,12 +196,18 @@ function runClean(argv: readonly string[]): number {
189
196
  const options = parseProjectArgs(argv);
190
197
  const cwd = path.resolve(options.cwd ?? process.cwd());
191
198
  const projectRoot = resolveCleanProjectRoot(cwd, options.tsconfig);
192
- const targets = options.cacheDir
199
+ const explicitCacheDir = options.cacheDir
200
+ ? path.resolve(cwd, options.cacheDir)
201
+ : undefined;
202
+ if (explicitCacheDir !== undefined) {
203
+ assertSafeExplicitCacheDirectory(projectRoot, explicitCacheDir);
204
+ }
205
+ const targets = explicitCacheDir
193
206
  ? // Explicit `ttsc clean --cache-dir X`: the user names X as the cache to
194
207
  // remove for this command, so remove it wholesale plus the legacy
195
208
  // project-local caches.
196
209
  [
197
- path.resolve(cwd, options.cacheDir),
210
+ explicitCacheDir,
198
211
  path.join(projectRoot, "node_modules", ".ttsc"),
199
212
  path.join(projectRoot, ".ttsc"),
200
213
  ]
@@ -303,8 +316,14 @@ function parseCachePathsArgs(argv: readonly string[]): {
303
316
  const rest = [...argv];
304
317
  while (rest.length !== 0) {
305
318
  const token = rest.shift()!;
306
- const [flag, inlineValue] = splitInlineFlag(token);
307
- switch (flag) {
319
+ const [rawFlag, inlineValue] = splitInlineFlag(token);
320
+ const flag = resolveFlagSpec(rawFlag);
321
+ if (flag?.subcommands.includes("cache") !== true) {
322
+ throw new Error(
323
+ `ttsc: cache paths does not support ${JSON.stringify(token)}`,
324
+ );
325
+ }
326
+ switch (flag.name) {
308
327
  case "--json":
309
328
  if (inlineValue !== undefined) {
310
329
  throw new Error("ttsc: --json does not take a value");
@@ -312,15 +331,13 @@ function parseCachePathsArgs(argv: readonly string[]): {
312
331
  out.json = true;
313
332
  break;
314
333
  case "--cache-dir":
315
- out.cacheDir = readCachePathsValue(flag, inlineValue, rest);
334
+ out.cacheDir = readCachePathsValue(flag.name, inlineValue, rest);
316
335
  break;
317
336
  case "--cwd":
318
- out.cwd = readCachePathsValue(flag, inlineValue, rest);
337
+ out.cwd = readCachePathsValue(flag.name, inlineValue, rest);
319
338
  break;
320
- case "-p":
321
- case "--project":
322
339
  case "--tsconfig":
323
- out.tsconfig = readCachePathsValue(flag, inlineValue, rest);
340
+ out.tsconfig = readCachePathsValue(flag.name, inlineValue, rest);
324
341
  break;
325
342
  default:
326
343
  throw new Error(
@@ -405,7 +422,7 @@ function parseProjectArgs(argv: readonly string[]) {
405
422
  };
406
423
  }
407
424
 
408
- function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
425
+ function parseBuildArgs(argv: readonly string[]) {
409
426
  const result = parseFlags({
410
427
  argv,
411
428
  errorPrefix: "ttsc:",
@@ -418,17 +435,15 @@ function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
418
435
  subcommand: "build",
419
436
  });
420
437
  // Defaults: pinned by the previous hand-parser. `quiet` defaults true,
421
- // `--verbose` flips it to false; `emit` defaults `undefined` in build
422
- // mode (let tsconfig decide) and `false` in check/fix/format mode.
438
+ // `--verbose` flips it to false; `emit` defaults `undefined` so the resolved
439
+ // project controls ordinary build mode. `runCompatibleBuild` applies the
440
+ // check/fix/format no-emit decision before either execution lane runs.
423
441
  const verbose = getBoolean(result, "--verbose");
424
442
  const quietFlag = getBoolean(result, "--quiet");
425
443
  const quiet = verbose === true ? false : (quietFlag ?? true);
426
444
  const explicitEmit = getBoolean(result, "--emit");
427
445
  const explicitNoEmit = getBoolean(result, "--noEmit");
428
- let emit: boolean | undefined;
429
- if (explicitEmit === true) emit = true;
430
- else if (explicitNoEmit === true) emit = false;
431
- if (emit === undefined && checkOnly) emit = false;
446
+ const emit = resolveExplicitEmit(explicitEmit, explicitNoEmit);
432
447
 
433
448
  // `isPositional: looksLikeInputFile` guarantees every `result.positional`
434
449
  // token is a TypeScript input file; forwarded flag values already live in
@@ -457,6 +472,21 @@ function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
457
472
  };
458
473
  }
459
474
 
475
+ /**
476
+ * Collapse the two launcher-owned emit switches into the tri-state consumed by
477
+ * `runBuild` and the single-file lane. A specified boolean is significant even
478
+ * when it is `false`: `--emit=false` is analysis-only and `--noEmit=false`
479
+ * explicitly overrides a project's `noEmit`. `--emit` retains precedence when
480
+ * callers supply both switches, matching the legacy true-only resolution.
481
+ */
482
+ function resolveExplicitEmit(
483
+ explicitEmit: boolean | undefined,
484
+ explicitNoEmit: boolean | undefined,
485
+ ): boolean | undefined {
486
+ if (explicitEmit !== undefined) return explicitEmit;
487
+ return explicitNoEmit === undefined ? undefined : !explicitNoEmit;
488
+ }
489
+
460
490
  /**
461
491
  * Report whether a bare CLI token is a TypeScript source file ttsc should
462
492
  * compile in single-file mode. Anything without a TypeScript source extension
@@ -548,12 +578,15 @@ function runSingleFile(options: ReturnType<typeof parseBuildArgs>): number {
548
578
  }
549
579
  const cwd = path.resolve(options.cwd ?? process.cwd());
550
580
  const file = path.resolve(cwd, options.files[0]!);
551
- const out = resolveSingleFileOut({
552
- cliOutDir: options.outDir,
553
- cwd,
554
- file,
555
- tsconfig: options.tsconfig,
556
- });
581
+ const emit = singleFileShouldEmit(options, cwd, file);
582
+ const out = emit
583
+ ? resolveSingleFileOut({
584
+ cliOutDir: options.outDir,
585
+ cwd,
586
+ file,
587
+ tsconfig: options.tsconfig,
588
+ })
589
+ : undefined;
557
590
  const text = runSingleFileEmit({
558
591
  binary: options.binary,
559
592
  checkers: options.checkers,
@@ -564,14 +597,37 @@ function runSingleFile(options: ReturnType<typeof parseBuildArgs>): number {
564
597
  singleThreaded: options.singleThreaded,
565
598
  tsconfig: options.tsconfig,
566
599
  });
567
- if (!fs.existsSync(out)) {
600
+ if (out !== undefined && !fs.existsSync(out)) {
568
601
  fs.mkdirSync(path.dirname(out), { recursive: true });
569
602
  fs.writeFileSync(out, text, "utf8");
570
603
  }
571
- process.stdout.write(`${path.relative(cwd, out) || path.basename(out)}\n`);
604
+ if (out !== undefined) {
605
+ process.stdout.write(`${path.relative(cwd, out) || path.basename(out)}\n`);
606
+ }
572
607
  return 0;
573
608
  }
574
609
 
610
+ /**
611
+ * Resolve the same effective emit decision used by the project lane before
612
+ * entering the single-file compatibility path. The compatibility path still
613
+ * emits into a private temporary directory to obtain transformed text and
614
+ * diagnostics, but only this boundary is allowed to write into the user's
615
+ * tree.
616
+ */
617
+ function singleFileShouldEmit(
618
+ options: ReturnType<typeof parseBuildArgs>,
619
+ cwd: string,
620
+ file: string,
621
+ ): boolean {
622
+ if (options.emit !== undefined) return options.emit;
623
+ const project = readProjectConfig({
624
+ cwd,
625
+ file,
626
+ tsconfig: options.tsconfig,
627
+ });
628
+ return project.compilerOptions.noEmit !== true;
629
+ }
630
+
575
631
  function resolveSingleFileOut(opts: {
576
632
  cliOutDir?: string;
577
633
  cwd: string;
@@ -672,9 +728,13 @@ function runWatch(
672
728
  checkOnly: boolean,
673
729
  ): number {
674
730
  const cwd = path.resolve(options.cwd ?? process.cwd());
731
+ let topology: WatchTopology | undefined;
675
732
  const invocation = {
676
- ...options,
733
+ ...(checkOnly ? { ...options, emit: false } : options),
677
734
  cwd,
735
+ onWatchInputs: (inputs: readonly string[]) => {
736
+ topology?.setExtraInputs(inputs);
737
+ },
678
738
  quiet: true,
679
739
  };
680
740
  const root = path.dirname(
@@ -692,6 +752,7 @@ function runWatch(
692
752
 
693
753
  const runOnce = () => {
694
754
  running = true;
755
+ let completed = false;
695
756
  try {
696
757
  if (!options.preserveWatchOutput) {
697
758
  process.stdout.write("\x1bc");
@@ -717,6 +778,7 @@ function runWatch(
717
778
  return result.status;
718
779
  })();
719
780
  lastStatus = status;
781
+ completed = true;
720
782
  process.stdout.write(
721
783
  `[ttsc] ${status === 0 ? "watch build complete" : "watch build failed"}\n`,
722
784
  );
@@ -726,6 +788,19 @@ function runWatch(
726
788
  process.stdout.write(`[ttsc] watch build failed\n`);
727
789
  } finally {
728
790
  running = false;
791
+ if (completed) {
792
+ try {
793
+ // A filesystem event can arrive after the build reports completion
794
+ // but before this synchronous cleanup reaches its re-resolution.
795
+ // Notify for a membership change here as well, otherwise that new
796
+ // input is silently absorbed into the watch set without a rebuild.
797
+ topology?.refresh(true);
798
+ } catch (error) {
799
+ process.stderr.write(
800
+ `[ttsc] watch error on ${path.relative(cwd, root) || "."}: ${formatError(error)}\n`,
801
+ );
802
+ }
803
+ }
729
804
  }
730
805
  if (rerun) {
731
806
  rerun = false;
@@ -741,23 +816,20 @@ function runWatch(
741
816
  timer = setTimeout(runOnce, 60);
742
817
  };
743
818
 
744
- const directories = collectWatchDirectories(root);
745
- const watchers = directories.map((dir) => {
746
- const watcher = fs.watch(dir, { persistent: true }, () => trigger());
747
- // Without an error handler Node rethrows watcher errors as uncaught
748
- // exceptions, which would crash the session. inotify limits (ENOSPC) and
749
- // transient FS errors should be logged while the session stays alive.
750
- watcher.on("error", (err) => {
819
+ topology = new WatchTopology(invocation, {
820
+ onError: (location, error) => {
751
821
  process.stderr.write(
752
- `[ttsc] watch error on ${path.relative(cwd, dir) || "."}: ${formatError(err)}\n`,
822
+ `[ttsc] watch error on ${path.relative(cwd, location) || "."}: ${formatError(error)}\n`,
753
823
  );
754
- });
755
- return watcher;
824
+ },
825
+ onInputChange: trigger,
826
+ onTopologyChange: trigger,
756
827
  });
828
+ topology.refresh(false);
757
829
 
758
830
  const close = () => {
759
831
  if (timer) clearTimeout(timer);
760
- for (const watcher of watchers) watcher.close();
832
+ topology?.close();
761
833
  };
762
834
  process.on("SIGINT", () => {
763
835
  close();
@@ -789,29 +861,6 @@ function toExitCode(status: number): number {
789
861
  return Number.isInteger(status) && status >= 0 && status <= 255 ? status : 1;
790
862
  }
791
863
 
792
- function collectWatchDirectories(root: string): string[] {
793
- const out: string[] = [];
794
- const stack: string[] = [root];
795
- while (stack.length !== 0) {
796
- const current = stack.pop()!;
797
- out.push(current);
798
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
799
- if (
800
- entry.name === "node_modules" ||
801
- entry.name === ".git" ||
802
- entry.name === "lib" ||
803
- entry.name === "dist"
804
- ) {
805
- continue;
806
- }
807
- if (entry.isDirectory()) {
808
- stack.push(path.join(current, entry.name));
809
- }
810
- }
811
- }
812
- return out;
813
- }
814
-
815
864
  function formatError(error: unknown): string {
816
865
  if (error instanceof Error) {
817
866
  return error.message;
@@ -7,8 +7,10 @@ import {
7
7
  getBoolean,
8
8
  getNumber,
9
9
  getString,
10
+ getStringList,
10
11
  parseFlags,
11
12
  } from "../../flags/parser";
13
+ import { resolveFlagSpec } from "../../flags/schema";
12
14
  import { getCompilerVersionText } from "./getCompilerVersionText";
13
15
  import { prepareExecution } from "./prepareExecution";
14
16
  import { resolveCacheDir } from "./resolveCacheDir";
@@ -98,26 +100,23 @@ function parseCLI(argv: readonly string[]) {
98
100
  // tokens (entry file + flag values that aren't `.ts`) and a passthrough
99
101
  // list mirroring the pre-schema behaviour.
100
102
  //
101
- // Order pin: ttsx accepts `-P` as an alias for `--project`. The schema
102
- // declares `-p` for ttsc; ttsx's lowercase shape would collide on `-p`
103
- // → `--tsconfig`, so the legacy `-P` (uppercase) is treated as `--project`
104
- // via a manual rewrite before the engine sees argv. We preserve the
105
- // historical behaviour and emit a structural error otherwise.
106
- const rewritten = argv.map((token) =>
107
- token === "-P"
108
- ? "--project"
109
- : token.startsWith("-P=")
110
- ? `--project=${token.slice("-P=".length)}`
111
- : token,
112
- );
103
+ // The legacy uppercase `-P` spelling ttsx has always accepted needs no
104
+ // rewrite: the engine resolves a token to the flag the compiler resolves it
105
+ // to, so `-P` and `-P=<file>` reach `--tsconfig` by the same rule that makes
106
+ // `-p` reach it. A textual pre-rewrite here would be a second rule for a job
107
+ // the engine owns.
108
+ //
113
109
  // Terminal flags (--help / --version) short-circuit before parsing so
114
- // ttsx prints help text even when the entry file is missing.
115
- for (const token of rewritten) {
116
- if (token === "-h" || token === "--help") return "help" as const;
117
- if (token === "-v" || token === "--version") return "version" as const;
110
+ // ttsx prints help text even when the entry file is missing. Resolved
111
+ // through the schema so every spelling the compiler accepts (`--HELP`,
112
+ // `-Version`) reaches the same branch.
113
+ for (const token of argv) {
114
+ const flag = resolveFlagSpec(token)?.name;
115
+ if (flag === "--help") return "help" as const;
116
+ if (flag === "--version") return "version" as const;
118
117
  }
119
118
  const result = parseFlags({
120
- argv: rewritten,
119
+ argv,
121
120
  errorPrefix: "ttsx:",
122
121
  forwardAfterFirstPositional: true,
123
122
  honorDoubleDashSeparator: true,
@@ -145,34 +144,20 @@ function parseCLI(argv: readonly string[]) {
145
144
  // which MUST NOT reach tsgo.
146
145
  const postEntryArgs: string[] = [...result.tail];
147
146
 
148
- const preload: string[] = [];
149
- // `--require` accepts repeated values; the schema engine writes the
150
- // LAST one into `values`, so reconstruct the full list by scanning the
151
- // raw argv. Mirrors the legacy parser's `preload.push(takeValue(...))`
152
- // behaviour.
147
+ // `--require` is declared `repeatable`, so the engine records every accepted
148
+ // value in argv order and the launcher reads the list straight off the parse
149
+ // result.
153
150
  //
154
- // Stop the rescue scan at the first token that begins tail mode —
155
- // either the entry file or the `--` separator. Without this guard,
156
- // `ttsx entry.ts -r preload.cjs` would BOTH preload `preload.cjs` AND
157
- // forward `-r preload.cjs` to the entry's argv, double-effecting the
158
- // module load. The schema engine already routes post-entry tokens to
159
- // `result.tail`; the rescue scan must respect the same boundary.
160
- const scanEnd = rewritten.findIndex(
161
- (token) => looksLikeEntryFile(token) || token === "--",
162
- );
163
- const scanLimit = scanEnd === -1 ? rewritten.length : scanEnd;
164
- for (let i = 0; i < scanLimit; i += 1) {
165
- const token = rewritten[i]!;
166
- if (token === "-r" || token === "--require") {
167
- const value = rewritten[i + 1];
168
- if (value !== undefined && !value.startsWith("-")) {
169
- preload.push(value);
170
- i += 1;
171
- }
172
- } else if (token.startsWith("--require=")) {
173
- preload.push(token.slice("--require=".length));
174
- }
175
- }
151
+ // This replaces a second, hand-written scan over raw argv that re-derived the
152
+ // pre-entry boundary with `looksLikeEntryFile`. Applied to raw tokens that
153
+ // predicate cannot tell an entry from a `--require` value carrying a
154
+ // TypeScript extension, nor from an inline `--require=<x>.ts` token, so the
155
+ // scan stopped before the tokens it existed to collect and preloads were
156
+ // dropped silently. The engine already owns that boundary:
157
+ // `forwardAfterFirstPositional` routes every post-entry token to
158
+ // `result.tail` without parsing it, so `ttsx entry.ts -r preload.cjs` still
159
+ // forwards the pair to the program instead of preloading it.
160
+ const preload = getStringList(result, "--require");
176
161
 
177
162
  return {
178
163
  binary: getString(result, "--binary"),
@@ -1,6 +1,11 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
- import { Module, registerHooks, stripTypeScriptTypes } from "node:module";
3
+ import {
4
+ Module,
5
+ isBuiltin,
6
+ registerHooks,
7
+ stripTypeScriptTypes,
8
+ } from "node:module";
4
9
  import os from "node:os";
5
10
  import path from "node:path";
6
11
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -294,7 +299,10 @@ function resolve(
294
299
  ): ResolveResult {
295
300
  try {
296
301
  return rememberCommonJsNamedInterop(
297
- nextResolve(specifier, context),
302
+ restoreStrippedNodeBuiltinScheme(
303
+ specifier,
304
+ nextResolve(specifier, context),
305
+ ),
298
306
  context,
299
307
  );
300
308
  } catch (error) {
@@ -309,6 +317,26 @@ function resolve(
309
317
  }
310
318
  }
311
319
 
320
+ /**
321
+ * Restore a `node:` builtin URL when affected Node releases return the exact
322
+ * prefix-stripped spelling from their synchronous CommonJS resolver.
323
+ *
324
+ * Every other result passes through unchanged. In particular, a user hook that
325
+ * intentionally remaps a `node:` specifier to another URL retains ownership of
326
+ * that mapping, while ordinary and ESM builtin results already carrying the
327
+ * scheme avoid an unnecessary copy.
328
+ */
329
+ export function restoreStrippedNodeBuiltinScheme(
330
+ specifier: string,
331
+ result: ResolveResult,
332
+ ): ResolveResult {
333
+ return isBuiltin(specifier) &&
334
+ specifier.startsWith("node:") &&
335
+ result.url === specifier.slice("node:".length)
336
+ ? { ...result, url: specifier }
337
+ : result;
338
+ }
339
+
312
340
  /** Cache of built projects keyed by owning tsconfig path. */
313
341
  interface BuiltProject {
314
342
  emitDir: string;