ttsc 0.20.1 → 0.22.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 (95) hide show
  1. package/cmd/ttscserver/main.go +79 -3
  2. package/cmd/ttscserver/run_lsp_uses_native_plugin_manifest_source_test.go +96 -16
  3. package/driver/lsp.go +4 -0
  4. package/internal/lspserver/lsp_document_diagnostics_omits_cached_project_when_current_result_has_none_test.go +48 -0
  5. package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go +98 -0
  6. package/internal/lspserver/lsp_external_watched_change_retains_program_test.go +53 -0
  7. package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +9 -0
  8. package/internal/lspserver/lsp_native_plugin_source.go +171 -55
  9. package/internal/lspserver/lsp_native_resident.go +110 -9
  10. package/internal/lspserver/lsp_plugin.go +6 -0
  11. package/internal/lspserver/lsp_project_context_transport_identity_test.go +43 -0
  12. package/internal/lspserver/lsp_project_diagnostics.go +235 -0
  13. package/internal/lspserver/lsp_project_diagnostics_capability_is_independent_test.go +67 -0
  14. package/internal/lspserver/lsp_project_diagnostics_program_input_overlap_refreshes_all_producers_test.go +78 -0
  15. package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go +65 -0
  16. package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go +90 -0
  17. package/internal/lspserver/lsp_project_diagnostics_refreshes_only_input_owners_test.go +62 -0
  18. package/internal/lspserver/lsp_project_diagnostics_resident_unsupported_falls_back_test.go +123 -0
  19. package/internal/lspserver/lsp_project_diagnostics_resume_preserves_generation_test.go +52 -0
  20. package/internal/lspserver/lsp_project_diagnostics_translates_relative_client_project_test.go +90 -0
  21. package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
  22. package/internal/lspserver/lsp_project_input_registration.go +504 -0
  23. package/internal/lspserver/lsp_project_input_registration_rechecks_selection_baseline_test.go +71 -0
  24. package/internal/lspserver/lsp_project_input_registration_reconciles_rejected_pending_request_test.go +268 -0
  25. package/internal/lspserver/lsp_project_input_registration_shapes_patterns_test.go +94 -0
  26. package/internal/lspserver/lsp_project_input_watchers_anchor_missing_paths_test.go +48 -0
  27. package/internal/lspserver/lsp_project_inputs.go +944 -0
  28. package/internal/lspserver/lsp_project_inputs_match_missing_exact_and_glob_test.go +129 -0
  29. package/internal/lspserver/lsp_project_inputs_reject_invalid_and_foreign_snapshots_test.go +110 -0
  30. package/internal/lspserver/lsp_proxy.go +591 -28
  31. package/internal/lspserver/lsp_reload_directories_compare_immediate_topology_test.go +188 -0
  32. package/internal/lspserver/lsp_reload_file_fingerprint_preserves_raw_symlink_target_test.go +47 -0
  33. package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +1 -1
  34. package/internal/lspserver/lsp_resident_invalidation_broadcasts_program_input_overlap_test.go +60 -0
  35. package/internal/lspserver/lsp_resident_invalidation_targets_project_input_owners_test.go +51 -0
  36. package/internal/lspserver/lsp_same_transport_entries_are_deduplicated_test.go +66 -0
  37. package/lib/compiler/internal/project/readProjectConfig.d.ts +5 -3
  38. package/lib/compiler/internal/project/readProjectConfig.js +46 -14
  39. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  40. package/lib/compiler/internal/residentCheckProcess.d.ts +46 -0
  41. package/lib/compiler/internal/residentCheckProcess.js +227 -0
  42. package/lib/compiler/internal/residentCheckProcess.js.map +1 -0
  43. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -1
  44. package/lib/compiler/internal/resolveEmittedJavaScript.js +35 -18
  45. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  46. package/lib/compiler/internal/runBuild.d.ts +64 -1
  47. package/lib/compiler/internal/runBuild.js +484 -7
  48. package/lib/compiler/internal/runBuild.js.map +1 -1
  49. package/lib/compiler/internal/runSingleFileEmit.js +1 -0
  50. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  51. package/lib/flags/parser.js +13 -17
  52. package/lib/flags/parser.js.map +1 -1
  53. package/lib/flags/schema.d.ts +10 -3
  54. package/lib/flags/schema.js +99 -0
  55. package/lib/flags/schema.js.map +1 -1
  56. package/lib/internal/projectInputPathIdentity.d.ts +23 -0
  57. package/lib/internal/projectInputPathIdentity.js +222 -0
  58. package/lib/internal/projectInputPathIdentity.js.map +1 -0
  59. package/lib/launcher/internal/runTtsc.d.ts +17 -0
  60. package/lib/launcher/internal/runTtsc.js +121 -88
  61. package/lib/launcher/internal/runTtsc.js.map +1 -1
  62. package/lib/launcher/internal/runTtscserver.d.ts +13 -1
  63. package/lib/launcher/internal/runTtscserver.js +415 -45
  64. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  65. package/lib/launcher/internal/singleFileOutput.d.ts +16 -0
  66. package/lib/launcher/internal/singleFileOutput.js +103 -0
  67. package/lib/launcher/internal/singleFileOutput.js.map +1 -0
  68. package/lib/launcher/internal/watchTopology.d.ts +214 -2
  69. package/lib/launcher/internal/watchTopology.js +1560 -41
  70. package/lib/launcher/internal/watchTopology.js.map +1 -1
  71. package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
  72. package/lib/plugin/internal/loadProjectPlugins.js +5 -0
  73. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  74. package/lib/structures/ITtscPluginCapabilities.d.ts +31 -0
  75. package/lib/structures/internal/ITtscProjectInputSnapshot.d.ts +29 -0
  76. package/lib/structures/internal/ITtscProjectInputSnapshot.js +3 -0
  77. package/lib/structures/internal/ITtscProjectInputSnapshot.js.map +1 -0
  78. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -0
  79. package/package.json +8 -8
  80. package/src/compiler/internal/project/readProjectConfig.ts +62 -8
  81. package/src/compiler/internal/residentCheckProcess.ts +279 -0
  82. package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -21
  83. package/src/compiler/internal/runBuild.ts +728 -12
  84. package/src/compiler/internal/runSingleFileEmit.ts +1 -0
  85. package/src/flags/parser.ts +17 -17
  86. package/src/flags/schema.ts +111 -3
  87. package/src/internal/projectInputPathIdentity.ts +273 -0
  88. package/src/launcher/internal/runTtsc.ts +141 -117
  89. package/src/launcher/internal/runTtscserver.ts +561 -54
  90. package/src/launcher/internal/singleFileOutput.ts +133 -0
  91. package/src/launcher/internal/watchTopology.ts +2008 -59
  92. package/src/plugin/internal/loadProjectPlugins.ts +8 -0
  93. package/src/structures/ITtscPluginCapabilities.ts +34 -0
  94. package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
  95. package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createProjectInputPathIdentityContext = createProjectInputPathIdentityContext;
7
+ exports.isProjectInputPathIdentityWithin = isProjectInputPathIdentityWithin;
8
+ exports.resolveProjectInputPath = resolveProjectInputPath;
9
+ const node_child_process_1 = __importDefault(require("node:child_process"));
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ /**
13
+ * Create one filesystem-identity resolver for a project-input transaction.
14
+ *
15
+ * Existing segments use their physical spelling. A missing suffix keeps exact
16
+ * spelling only under a case-sensitive directory; otherwise its canonical
17
+ * spelling is folded so aliases remain one declaration before they exist.
18
+ */
19
+ function createProjectInputPathIdentityContext(operations = {}) {
20
+ const identities = new Map();
21
+ const realpaths = new Map();
22
+ const sensitivities = new Map();
23
+ const realpath = operations.realpath ?? physicalRealpath;
24
+ const caseSensitive = operations.caseSensitive ?? filesystemDirectoryIsCaseSensitive;
25
+ const resolve = (location) => {
26
+ const normalized = resolveProjectInputPath(location);
27
+ const cached = identities.get(normalized);
28
+ if (cached !== undefined)
29
+ return cached.identity;
30
+ let existing = normalized;
31
+ const missing = [];
32
+ while (true) {
33
+ const physical = cachedRealpath(realpaths, realpath, existing);
34
+ if (physical !== undefined) {
35
+ const sensitive = missing.length === 0
36
+ ? true
37
+ : cachedCaseSensitivity(sensitivities, caseSensitive, physical);
38
+ const suffix = sensitive
39
+ ? missing
40
+ : missing.map((segment) => segment.toLowerCase());
41
+ const canonical = node_path_1.default.resolve(physical, ...suffix);
42
+ const identity = {
43
+ key: canonical,
44
+ path: canonical,
45
+ };
46
+ identities.set(normalized, {
47
+ ancestor: physical,
48
+ identity,
49
+ });
50
+ return identity;
51
+ }
52
+ const parent = node_path_1.default.dirname(existing);
53
+ if (parent === existing) {
54
+ const identity = {
55
+ key: normalized,
56
+ path: normalized,
57
+ };
58
+ identities.set(normalized, {
59
+ ancestor: normalized,
60
+ identity,
61
+ });
62
+ return identity;
63
+ }
64
+ missing.unshift(node_path_1.default.basename(existing));
65
+ existing = parent;
66
+ }
67
+ };
68
+ return {
69
+ caseSensitive: (directory) => {
70
+ const normalized = resolveProjectInputPath(directory);
71
+ resolve(normalized);
72
+ return cachedCaseSensitivity(sensitivities, caseSensitive, identities.get(normalized).ancestor);
73
+ },
74
+ isWithin: (root, candidate) => isProjectInputPathIdentityWithin(resolve(root).key, resolve(candidate).key),
75
+ resolve,
76
+ };
77
+ }
78
+ function isProjectInputPathIdentityWithin(root, candidate) {
79
+ if (candidate === root)
80
+ return true;
81
+ return candidate.startsWith(root.endsWith(node_path_1.default.sep) ? root : `${root}${node_path_1.default.sep}`);
82
+ }
83
+ function resolveProjectInputPath(location) {
84
+ if (process.platform !== "win32") {
85
+ return node_path_1.default.resolve(location);
86
+ }
87
+ const normalized = location.replaceAll("/", "\\");
88
+ if (normalized.toLowerCase().startsWith("\\\\?\\unc\\")) {
89
+ return node_path_1.default.resolve(`\\\\${normalized.slice(8)}`);
90
+ }
91
+ if (normalized.startsWith("\\\\?\\") &&
92
+ /^[A-Za-z]:\\/.test(normalized.slice(4))) {
93
+ return node_path_1.default.resolve(normalized.slice(4));
94
+ }
95
+ return node_path_1.default.resolve(normalized);
96
+ }
97
+ function cachedRealpath(cache, realpath, location) {
98
+ const cached = cache.get(location);
99
+ if (cached !== undefined)
100
+ return cached.found ? cached.path : undefined;
101
+ try {
102
+ const physical = resolveProjectInputPath(realpath(location));
103
+ cache.set(location, { found: true, path: physical });
104
+ return physical;
105
+ }
106
+ catch (error) {
107
+ if (isMissingFilesystemEntry(error) === false)
108
+ throw error;
109
+ cache.set(location, { found: false });
110
+ return undefined;
111
+ }
112
+ }
113
+ function cachedCaseSensitivity(cache, caseSensitive, directory) {
114
+ const cached = cache.get(directory);
115
+ if (cached !== undefined)
116
+ return cached;
117
+ const sensitive = caseSensitive(directory);
118
+ cache.set(directory, sensitive);
119
+ return sensitive;
120
+ }
121
+ function physicalRealpath(location) {
122
+ return node_fs_1.default.realpathSync.native?.(location) ?? node_fs_1.default.realpathSync(location);
123
+ }
124
+ function filesystemDirectoryIsCaseSensitive(directory) {
125
+ let entries;
126
+ try {
127
+ entries = node_fs_1.default.readdirSync(directory);
128
+ }
129
+ catch {
130
+ return unprovenCaseSensitivity();
131
+ }
132
+ const foldedNames = new Map();
133
+ for (const name of entries) {
134
+ const folded = name.toLowerCase();
135
+ const previous = foldedNames.get(folded);
136
+ if (previous !== undefined && previous !== name)
137
+ return true;
138
+ foldedNames.set(folded, name);
139
+ }
140
+ for (const name of entries) {
141
+ const alternate = alternateCase(name);
142
+ if (alternate === name)
143
+ continue;
144
+ try {
145
+ physicalRealpath(node_path_1.default.join(directory, alternate));
146
+ return false;
147
+ }
148
+ catch (error) {
149
+ if (isMissingFilesystemEntry(error))
150
+ continue;
151
+ throw error;
152
+ }
153
+ }
154
+ if (process.platform === "darwin") {
155
+ // APFS/HFS case semantics are volume-wide. An empty directory has no child
156
+ // name to probe, so ask the same read-only question of its existing name in
157
+ // the parent and walk upward until a name with ASCII case is available.
158
+ // This avoids a write probe while still distinguishing default APFS from a
159
+ // case-sensitive volume.
160
+ let current = resolveProjectInputPath(directory);
161
+ while (true) {
162
+ const parent = node_path_1.default.dirname(current);
163
+ if (parent === current)
164
+ break;
165
+ const name = node_path_1.default.basename(current);
166
+ const alternate = alternateCase(name);
167
+ if (alternate !== name) {
168
+ try {
169
+ physicalRealpath(node_path_1.default.join(parent, alternate));
170
+ return false;
171
+ }
172
+ catch (error) {
173
+ if (isMissingFilesystemEntry(error) === false)
174
+ throw error;
175
+ }
176
+ }
177
+ current = parent;
178
+ }
179
+ return unprovenCaseSensitivity();
180
+ }
181
+ if (process.platform !== "win32")
182
+ return true;
183
+ // Node does not expose the Windows per-directory flag. Prefer fsutil's
184
+ // read-only answer, then conservatively preserve distinct declarations.
185
+ const result = node_child_process_1.default.spawnSync("fsutil.exe", ["file", "queryCaseSensitiveInfo", directory], { encoding: "utf8", windowsHide: true });
186
+ if (result.error === undefined && result.status === 0) {
187
+ if (/\bdisabled\b/iu.test(result.stdout))
188
+ return false;
189
+ if (/\benabled\b/iu.test(result.stdout))
190
+ return true;
191
+ }
192
+ return unprovenCaseSensitivity();
193
+ }
194
+ /**
195
+ * What to assume when the volume refused to answer.
196
+ *
197
+ * Every probe above returns a proof: two names that fold together prove the
198
+ * volume keeps them apart, and a name that opens under the other case proves it
199
+ * does not. This is the remaining case — an empty or unreadable directory, a
200
+ * name with no letter to alter, a Windows build whose `fsutil` lacks the
201
+ * subcommand — and the only honest answer is the platform's own default.
202
+ *
203
+ * NTFS and APFS are case-insensitive unless a volume or a directory was opted
204
+ * out, so guessing sensitive there splits one file into two identities under
205
+ * two spellings, which is the failure this module exists to remove. The rarer
206
+ * mistake, on a volume that really was opted out, merges two files differing
207
+ * only in case into one watched identity.
208
+ */
209
+ function unprovenCaseSensitivity() {
210
+ return process.platform !== "win32" && process.platform !== "darwin";
211
+ }
212
+ function alternateCase(value) {
213
+ return value.replace(/[A-Za-z]/g, (character) => character === character.toLowerCase()
214
+ ? character.toUpperCase()
215
+ : character.toLowerCase());
216
+ }
217
+ function isMissingFilesystemEntry(error) {
218
+ return (error instanceof Error &&
219
+ "code" in error &&
220
+ (error.code === "ENOENT" || error.code === "ENOTDIR"));
221
+ }
222
+ //# sourceMappingURL=projectInputPathIdentity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projectInputPathIdentity.js","sourceRoot":"","sources":["../../src/internal/projectInputPathIdentity.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,5 @@
1
+ import { type ResidentCheckWatchChange } from "../../compiler/internal/runBuild";
2
+ import { type WatchInputChange } from "./watchTopology";
1
3
  /**
2
4
  * CLI entry point for `ttsc`. Dispatches argv to the appropriate build lane
3
5
  * (build, check, fix, format, prepare, clean, or native-delegate) and returns
@@ -9,3 +11,18 @@
9
11
  * error or build failure.
10
12
  */
11
13
  export declare function runTtsc(argv?: readonly string[]): number;
14
+ /**
15
+ * Coalesces filesystem events until the next resident check-watch cycle.
16
+ *
17
+ * A full reload dominates every narrower signal. Program invalidation remains
18
+ * distinct so a project-input module creation/deletion can cold-load the
19
+ * Program without discarding the selected execution or restarting the sidecar.
20
+ */
21
+ export declare class PendingResidentCheckWatchChanges {
22
+ private readonly changed;
23
+ private readonly external;
24
+ private invalidate;
25
+ private reload;
26
+ push(change?: WatchInputChange, reload?: boolean): void;
27
+ take(): ResidentCheckWatchChange;
28
+ }
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PendingResidentCheckWatchChanges = void 0;
6
7
  exports.runTtsc = runTtsc;
7
8
  const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const node_path_1 = __importDefault(require("node:path"));
@@ -17,6 +18,7 @@ const assertSafeExplicitCacheDirectory_1 = require("../../internal/assertSafeExp
17
18
  const buildSourcePlugin_1 = require("../../plugin/internal/buildSourcePlugin");
18
19
  const getCompilerVersionText_1 = require("./getCompilerVersionText");
19
20
  const resolveCacheDir_1 = require("./resolveCacheDir");
21
+ const singleFileOutput_1 = require("./singleFileOutput");
20
22
  const watchTopology_1 = require("./watchTopology");
21
23
  /**
22
24
  * CLI entry point for `ttsc`. Dispatches argv to the appropriate build lane
@@ -493,10 +495,11 @@ function runSingleFile(options) {
493
495
  const file = node_path_1.default.resolve(cwd, options.files[0]);
494
496
  const emit = singleFileShouldEmit(options, cwd, file);
495
497
  const out = emit
496
- ? resolveSingleFileOut({
498
+ ? (0, singleFileOutput_1.resolveSingleFileOutput)({
497
499
  cliOutDir: options.outDir,
498
500
  cwd,
499
501
  file,
502
+ passthrough: options.passthrough,
500
503
  tsconfig: options.tsconfig,
501
504
  })
502
505
  : undefined;
@@ -505,6 +508,8 @@ function runSingleFile(options) {
505
508
  checkers: options.checkers,
506
509
  cwd,
507
510
  file,
511
+ onProjectInputs: options.onProjectInputs,
512
+ onWatchInputs: options.onWatchInputs,
508
513
  out,
509
514
  passthrough: options.passthrough,
510
515
  singleThreaded: options.singleThreaded,
@@ -536,80 +541,6 @@ function singleFileShouldEmit(options, cwd, file) {
536
541
  });
537
542
  return project.compilerOptions.noEmit !== true;
538
543
  }
539
- function resolveSingleFileOut(opts) {
540
- const jsBasename = node_path_1.default.basename(opts.file).replace(/\.[cm]?tsx?$/i, "") +
541
- singleFileJsExtension(opts.file);
542
- // Explicit CLI --outDir wins. Mirrors the CWD-relative source layout under
543
- // the requested directory so existing single-file invocations don't shift.
544
- if (opts.cliOutDir) {
545
- const relative = node_path_1.default.relative(opts.cwd, opts.file);
546
- const jsRelative = relative.slice(0, relative.length - node_path_1.default.extname(relative).length) +
547
- singleFileJsExtension(opts.file);
548
- return node_path_1.default.resolve(opts.cwd, opts.cliOutDir, jsRelative);
549
- }
550
- // No CLI override: honor tsconfig's outDir so `ttsc src/foo.ts` lands the
551
- // emitted JS at `<outDir>/<relative-from-rootDir>.js` instead of dropping
552
- // it next to the source file. This matches how project mode emits and how
553
- // `tsc <file>` would behave with the same tsconfig.
554
- const projectOutDir = readProjectOutDir({
555
- cwd: opts.cwd,
556
- file: opts.file,
557
- tsconfig: opts.tsconfig,
558
- });
559
- if (projectOutDir !== null) {
560
- const fromRoot = node_path_1.default.relative(projectOutDir.rootDir, opts.file);
561
- if (fromRoot !== "" && !isOutsideSingleFileLayout(fromRoot)) {
562
- const jsRelative = fromRoot.slice(0, fromRoot.length - node_path_1.default.extname(fromRoot).length) +
563
- singleFileJsExtension(opts.file);
564
- return node_path_1.default.resolve(projectOutDir.outDir, jsRelative);
565
- }
566
- return node_path_1.default.resolve(projectOutDir.outDir, jsBasename);
567
- }
568
- // Last resort (no tsconfig outDir at all): emit next to the source. This
569
- // preserves the legacy `ttsc <file.ts>` → `<file.js>` behavior for projects
570
- // that intentionally don't configure outDir.
571
- return opts.file.replace(/\.[cm]?tsx?$/i, singleFileJsExtension(opts.file));
572
- }
573
- function readProjectOutDir(opts) {
574
- try {
575
- const project = (0, readProjectConfig_1.readProjectConfig)({
576
- cwd: opts.cwd,
577
- file: opts.file,
578
- tsconfig: opts.tsconfig,
579
- });
580
- const outDir = project.compilerOptions.outDir;
581
- if (typeof outDir !== "string" || outDir.length === 0) {
582
- return null;
583
- }
584
- const rawRoot = project.compilerOptions.rootDir;
585
- const rootDir = typeof rawRoot === "string" && rawRoot.length !== 0
586
- ? node_path_1.default.isAbsolute(rawRoot)
587
- ? rawRoot
588
- : node_path_1.default.resolve(project.root, rawRoot)
589
- : project.root;
590
- return { outDir, rootDir };
591
- }
592
- catch {
593
- // Missing or unreadable tsconfig: fall back to the legacy behavior so
594
- // `ttsc <file>` still works outside a configured project.
595
- return null;
596
- }
597
- }
598
- function isOutsideSingleFileLayout(relative) {
599
- return (relative === ".." ||
600
- relative.startsWith(`..${node_path_1.default.sep}`) ||
601
- node_path_1.default.isAbsolute(relative));
602
- }
603
- function singleFileJsExtension(file) {
604
- switch (node_path_1.default.extname(file).toLowerCase()) {
605
- case ".mts":
606
- return ".mjs";
607
- case ".cts":
608
- return ".cjs";
609
- default:
610
- return ".js";
611
- }
612
- }
613
544
  function runWatch(options, checkOnly) {
614
545
  const cwd = node_path_1.default.resolve(options.cwd ?? process.cwd());
615
546
  let topology;
@@ -619,6 +550,9 @@ function runWatch(options, checkOnly) {
619
550
  onWatchInputs: (inputs) => {
620
551
  topology?.setExtraInputs(inputs);
621
552
  },
553
+ onProjectInputs: (inputs) => {
554
+ topology?.setProjectInputs(inputs);
555
+ },
622
556
  quiet: true,
623
557
  };
624
558
  const root = node_path_1.default.dirname((0, resolveProjectConfig_1.resolveProjectConfig)({
@@ -628,11 +562,14 @@ function runWatch(options, checkOnly) {
628
562
  let running = false;
629
563
  let rerun = false;
630
564
  let timer = null;
565
+ const resident = invocation.files.length === 0 ? new runBuild_1.ResidentCheckWatchSession() : undefined;
566
+ const pendingChanges = new PendingResidentCheckWatchChanges();
631
567
  // Tracks the most recent build's exit code so the watch session can exit
632
568
  // non-zero when its latest rebuild failed, instead of always reporting 0.
633
569
  let lastStatus = 0;
634
- const runOnce = () => {
570
+ const runOnce = async () => {
635
571
  running = true;
572
+ const change = pendingChanges.take();
636
573
  let completed = false;
637
574
  try {
638
575
  if (!options.preserveWatchOutput) {
@@ -645,22 +582,38 @@ function runWatch(options, checkOnly) {
645
582
  // to invalid JSON, single-file invariant). Catch here so a throwing
646
583
  // rebuild reports a clear failure and the watcher keeps running instead
647
584
  // of crashing the process with an uncaught exception.
648
- const status = invocation.files.length !== 0
649
- ? runSingleFile(invocation)
650
- : (() => {
651
- const result = (0, runBuild_1.runBuild)(checkOnly ? { ...invocation, emit: false } : invocation);
585
+ const status = await (invocation.files.length !== 0
586
+ ? Promise.resolve(runSingleFile(invocation))
587
+ : (async () => {
588
+ const buildOptions = checkOnly
589
+ ? { ...invocation, emit: false }
590
+ : invocation;
591
+ const result = resident === undefined
592
+ ? (0, runBuild_1.runBuild)(buildOptions)
593
+ : await resident.run(buildOptions, change);
594
+ // Watch output is one ordered stream. The diagnostics a build
595
+ // produced and the "[ttsc] watch build …" marker that concludes
596
+ // them reach a single merged reader, and on two pipes the OS may
597
+ // hand it the marker first — a reader that counts the build on the
598
+ // marker then snapshots before the diagnostics land and sees an
599
+ // empty build. The rebuilding banner, the screen clear, and the
600
+ // marker are already on stdout, so the diagnostics join them there,
601
+ // the way tsc --watch prints everything to one place.
652
602
  if (result.stdout)
653
603
  process.stdout.write(result.stdout);
654
604
  if (result.stderr)
655
- process.stderr.write(result.stderr);
605
+ process.stdout.write(result.stderr);
656
606
  return result.status;
657
- })();
607
+ })());
658
608
  lastStatus = status;
659
609
  completed = true;
660
610
  process.stdout.write(`[ttsc] ${status === 0 ? "watch build complete" : "watch build failed"}\n`);
661
611
  }
662
612
  catch (error) {
663
- process.stderr.write(`${formatError(error)}\n`);
613
+ // Same ordered-stream rule as the build path: the failure text goes on
614
+ // the stream the marker below uses, so a merged reader sees them in order
615
+ // rather than racing two pipes.
616
+ process.stdout.write(`${formatError(error)}\n`);
664
617
  lastStatus = lastStatus === 0 ? 2 : lastStatus;
665
618
  process.stdout.write(`[ttsc] watch build failed\n`);
666
619
  }
@@ -684,27 +637,37 @@ function runWatch(options, checkOnly) {
684
637
  trigger();
685
638
  }
686
639
  };
687
- const trigger = () => {
640
+ const trigger = (change, reload = false) => {
641
+ pendingChanges.push(change, reload);
688
642
  if (running) {
689
643
  rerun = true;
690
644
  return;
691
645
  }
692
646
  if (timer)
693
647
  clearTimeout(timer);
694
- timer = setTimeout(runOnce, 60);
648
+ timer = setTimeout(() => void runOnce(), 60);
695
649
  };
696
650
  topology = new watchTopology_1.WatchTopology(invocation, {
697
651
  onError: (location, error) => {
698
652
  process.stderr.write(`[ttsc] watch error on ${node_path_1.default.relative(cwd, location) || "."}: ${formatError(error)}\n`);
699
653
  },
700
- onInputChange: trigger,
701
- onTopologyChange: trigger,
654
+ onInputChange: (change) => {
655
+ debugWatchInputs(`change ${change.kind}${change.invalidate === true ? " invalidate" : ""} ${change.path === undefined
656
+ ? "(unnamed)"
657
+ : node_path_1.default.relative(cwd, change.path)}`);
658
+ trigger(change);
659
+ },
660
+ onProjectInputWatchRoots: (roots) => {
661
+ debugWatchInputs(`roots ${JSON.stringify(roots.map((root) => node_path_1.default.relative(cwd, root) || "."))}`);
662
+ },
663
+ onTopologyChange: () => trigger(undefined, true),
702
664
  });
703
665
  topology.refresh(false);
704
666
  const close = () => {
705
667
  if (timer)
706
668
  clearTimeout(timer);
707
669
  topology?.close();
670
+ resident?.dispose();
708
671
  };
709
672
  process.on("SIGINT", () => {
710
673
  close();
@@ -716,7 +679,7 @@ function runWatch(options, checkOnly) {
716
679
  });
717
680
  process.stdout.write(`[ttsc] watching ${node_path_1.default.relative(cwd, root) || "."}\n`);
718
681
  try {
719
- runOnce();
682
+ void runOnce();
720
683
  }
721
684
  catch (error) {
722
685
  // runOnce already swallows build throws, but guard against any unforeseen
@@ -729,11 +692,81 @@ function runWatch(options, checkOnly) {
729
692
  }
730
693
  return toExitCode(lastStatus);
731
694
  }
695
+ /**
696
+ * Coalesces filesystem events until the next resident check-watch cycle.
697
+ *
698
+ * A full reload dominates every narrower signal. Program invalidation remains
699
+ * distinct so a project-input module creation/deletion can cold-load the
700
+ * Program without discarding the selected execution or restarting the sidecar.
701
+ */
702
+ class PendingResidentCheckWatchChanges {
703
+ changed = new Set();
704
+ external = new Set();
705
+ invalidate = false;
706
+ reload = false;
707
+ push(change, reload = false) {
708
+ if (reload || change?.kind === "config" || change?.kind === "plugin") {
709
+ this.reload = true;
710
+ this.invalidate = false;
711
+ this.changed.clear();
712
+ this.external.clear();
713
+ return;
714
+ }
715
+ if (this.reload)
716
+ return;
717
+ if (change?.invalidate === true)
718
+ this.invalidate = true;
719
+ if (change?.path === undefined) {
720
+ if (change?.kind === "compiler") {
721
+ this.reload = true;
722
+ this.invalidate = false;
723
+ this.changed.clear();
724
+ this.external.clear();
725
+ }
726
+ return;
727
+ }
728
+ this.changed.add(change.path);
729
+ if (change.kind === "project")
730
+ this.external.add(change.path);
731
+ }
732
+ take() {
733
+ const change = {
734
+ ...(this.reload ? { reload: true } : {}),
735
+ ...(this.invalidate ? { invalidate: true } : {}),
736
+ ...(this.changed.size === 0 ? {} : { changed: [...this.changed].sort() }),
737
+ ...(this.external.size === 0
738
+ ? {}
739
+ : { external: [...this.external].sort() }),
740
+ };
741
+ this.reload = false;
742
+ this.invalidate = false;
743
+ this.changed.clear();
744
+ this.external.clear();
745
+ return change;
746
+ }
747
+ }
748
+ exports.PendingResidentCheckWatchChanges = PendingResidentCheckWatchChanges;
732
749
  // Coerces a build status into a valid process exit code: 0 stays 0, any
733
750
  // non-zero (or non-finite) status collapses to 1 so the session signals failure.
734
751
  function toExitCode(status) {
735
752
  return Number.isInteger(status) && status >= 0 && status <= 255 ? status : 1;
736
753
  }
754
+ /**
755
+ * Opt-in report of what the project-input layer is watching and announcing.
756
+ *
757
+ * A watch that goes quiet is indistinguishable from a watch with nothing to
758
+ * say: the build simply never runs again, and the transcript that survives says
759
+ * only that. This separates the two — the roots line states what is covered,
760
+ * and a change line states what was announced — so a missing rebuild can be
761
+ * attributed to the watch that never saw the file or to the decision that
762
+ * declined to report it. Silent unless asked, and on the same ordered stream as
763
+ * the rest of the watch output so it interleaves with the builds it explains.
764
+ */
765
+ function debugWatchInputs(message) {
766
+ if (!process.env.TTSC_WATCH_DEBUG_INPUTS)
767
+ return;
768
+ process.stdout.write(`[ttsc:debug] ${message}\n`);
769
+ }
737
770
  function formatError(error) {
738
771
  if (error instanceof Error) {
739
772
  return error.message;