ttsc 0.22.0 → 0.24.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 (91) hide show
  1. package/cmd/ttscgraph/serve.go +264 -48
  2. package/cmd/ttscgraph/serve_early_error_encode_failure_is_observable_test.go +40 -0
  3. package/cmd/ttscgraph/serve_graph_snapshot_version_matches_typescript_client_test.go +32 -0
  4. package/cmd/ttscgraph/serve_phase_trace_accounts_for_load_failure_test.go +45 -0
  5. package/cmd/ttscgraph/serve_phase_trace_is_opt_in_and_payload_free_test.go +52 -0
  6. package/cmd/ttscgraph/serve_protocol_negotiates_incremental_shards_test.go +44 -0
  7. package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +2 -2
  8. package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +46 -20
  9. package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +9 -9
  10. package/cmd/ttscgraph/serve_shards.go +1390 -0
  11. package/cmd/ttscgraph/serve_shards_abandoned_generation_converges_on_retry_test.go +92 -0
  12. package/cmd/ttscgraph/serve_shards_delete_removed_source_test.go +57 -0
  13. package/cmd/ttscgraph/serve_shards_external_endpoints_test.go +53 -0
  14. package/cmd/ttscgraph/serve_shards_keep_source_distributed_dependencies_at_the_boundary_test.go +108 -0
  15. package/cmd/ttscgraph/serve_shards_match_full_dump_oracle_test.go +88 -0
  16. package/cmd/ttscgraph/serve_shards_merge_pending_and_new_edits_test.go +50 -0
  17. package/cmd/ttscgraph/serve_shards_prune_unreferenced_external_endpoints_test.go +65 -0
  18. package/cmd/ttscgraph/serve_shards_publish_created_source_test.go +92 -0
  19. package/cmd/ttscgraph/serve_shards_rebuild_declaration_edit_test.go +43 -0
  20. package/cmd/ttscgraph/serve_shards_refresh_cross_file_implementation_test.go +63 -0
  21. package/cmd/ttscgraph/serve_shards_refresh_dependent_diagnostics_test.go +80 -0
  22. package/cmd/ttscgraph/serve_shards_rename_moves_source_identity_test.go +95 -0
  23. package/cmd/ttscgraph/serve_shards_retry_preserves_committed_generation_test.go +48 -0
  24. package/cmd/ttscgraph/serve_shards_reuse_unaffected_sources_test.go +121 -0
  25. package/driver/declaration_shape.go +85 -0
  26. package/driver/program.go +39 -10
  27. package/go.mod +1 -1
  28. package/internal/graph/build.go +62 -10
  29. package/internal/graph/build_files_reuses_committed_cross_file_endpoints_test.go +87 -0
  30. package/internal/graph/build_keeps_source_distributed_dependencies_at_the_external_boundary_test.go +74 -0
  31. package/internal/graph/dump.go +84 -43
  32. package/internal/graph/dump_path_mapper_caches_alias_resolution_test.go +29 -0
  33. package/internal/graph/dump_paths.go +134 -11
  34. package/internal/graph/dump_paths_canonicalize_windows_short_root_test.go +39 -0
  35. package/internal/graph/dump_rejects_provenance_only_path_errors_test.go +32 -0
  36. package/internal/graph/dump_serializes_the_full_graph_test.go +2 -2
  37. package/internal/graph/dump_symlink_project_publishes_matching_base_test.go +82 -0
  38. package/internal/graph/edges.go +58 -16
  39. package/internal/graph/exports.go +9 -2
  40. package/internal/graph/graph.go +47 -7
  41. package/internal/graph/graph_kind_contracts_match_their_producers_test.go +121 -0
  42. package/internal/graph/node_modifiers_emit_union_strings_test.go +1 -1
  43. package/internal/graph/provenance.go +11 -1
  44. package/internal/graph/resolve.go +1 -4
  45. package/internal/graph/source.go +23 -0
  46. package/internal/graph/wire_node_ids_share_alias_resolution_test.go +29 -0
  47. package/internal/lspserver/lsp_launcher_reload_directory_fingerprint_parity_windows_test.go +94 -0
  48. package/internal/lspserver/lsp_project_input_glob_path_other.go +16 -0
  49. package/internal/lspserver/lsp_project_input_glob_path_windows.go +36 -0
  50. package/internal/lspserver/lsp_project_input_path_key_other.go +17 -0
  51. package/internal/lspserver/lsp_project_input_path_key_windows.go +228 -0
  52. package/internal/lspserver/lsp_project_input_path_key_windows_test.go +460 -0
  53. package/internal/lspserver/lsp_project_inputs.go +117 -49
  54. package/internal/lspserver/lsp_reload_directories_separate_glob_territory_test.go +181 -0
  55. package/lib/flags/schema.js +1 -1
  56. package/lib/flags/schema.js.map +1 -1
  57. package/lib/internal/projectInputPathIdentity.d.ts +13 -5
  58. package/lib/internal/projectInputPathIdentity.js +95 -41
  59. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  60. package/lib/internal/windowsDirectoryCaseSensitivity.d.ts +10 -0
  61. package/lib/internal/windowsDirectoryCaseSensitivity.js +34 -0
  62. package/lib/internal/windowsDirectoryCaseSensitivity.js.map +1 -0
  63. package/lib/launcher/internal/runTtsc.js +5 -0
  64. package/lib/launcher/internal/runTtsc.js.map +1 -1
  65. package/lib/launcher/internal/runTtscserver.js +59 -8
  66. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  67. package/lib/launcher/internal/runTtsx.js +61 -7
  68. package/lib/launcher/internal/runTtsx.js.map +1 -1
  69. package/lib/launcher/internal/runtimeHooks.d.ts +6 -5
  70. package/lib/launcher/internal/runtimeHooks.js +7 -15
  71. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  72. package/lib/launcher/internal/watchTopology.d.ts +82 -8
  73. package/lib/launcher/internal/watchTopology.js +535 -187
  74. package/lib/launcher/internal/watchTopology.js.map +1 -1
  75. package/lib/plugin/internal/descriptorProcessFailure.d.ts +32 -0
  76. package/lib/plugin/internal/descriptorProcessFailure.js +73 -0
  77. package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -0
  78. package/lib/plugin/internal/loadProjectPlugins.js +29 -8
  79. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  80. package/package.json +12 -8
  81. package/src/flags/schema.ts +2 -1
  82. package/src/internal/projectInputPathIdentity.ts +150 -46
  83. package/src/internal/windowsDirectoryCaseSensitivity.ts +33 -0
  84. package/src/launcher/internal/runTtsc.ts +7 -0
  85. package/src/launcher/internal/runTtscserver.ts +81 -11
  86. package/src/launcher/internal/runTtsx.ts +87 -10
  87. package/src/launcher/internal/runtimeHooks.ts +16 -16
  88. package/src/launcher/internal/watchTopology.ts +679 -208
  89. package/src/plugin/internal/descriptorProcessFailure.ts +100 -0
  90. package/src/plugin/internal/loadProjectPlugins.ts +41 -8
  91. package/tools/shim_audit/baseline.json +0 -9
@@ -39,11 +39,14 @@ class WatchTopology {
39
39
  callbacks;
40
40
  analysisOnly = false;
41
41
  closed = false;
42
+ compilerPostRegistrationMembershipRefresh = false;
43
+ compilerPostRegistrationReconciliationScheduled = false;
44
+ compilerPostRegistrationSkipUnobservedProjectInputWatchRoots = true;
42
45
  directories = new Map();
43
46
  directoryWatchers = new Map();
44
47
  extraInputs = [];
45
48
  extraWatchers = new Map();
46
- compilerFileStamps = new Map();
49
+ compilerFileSnapshots = new Map();
47
50
  files = new Map();
48
51
  fileWatchers = new Map();
49
52
  observedDirectories = new Map();
@@ -65,10 +68,16 @@ class WatchTopology {
65
68
  reloadFiles: [],
66
69
  root: "",
67
70
  };
71
+ projectInputRecoveryScheduled = false;
72
+ projectInputPostRegistrationReconciliationScheduled = false;
68
73
  projectInputRejectedWatchRoots = new Set();
74
+ projectInputRequiredWatchRoots = new Map();
75
+ projectInputUnobservedWatchRoots = new Map();
69
76
  projectInputWatchRoots = new Map();
70
77
  projectInputWatchers = new Map();
71
78
  projectInputLinkWatchers = new Map();
79
+ projectInputCompilerOutputOverlaps = new WeakMap();
80
+ projectInputCompilerAcknowledgements = new Map();
72
81
  reloadFiles = new Map();
73
82
  constructor(options, callbacks) {
74
83
  this.options = options;
@@ -76,12 +85,21 @@ class WatchTopology {
76
85
  }
77
86
  /** Re-resolve compiler inputs and notify only when their membership changed. */
78
87
  refresh(notify) {
88
+ this.refreshCompilerInputs(notify, false);
89
+ }
90
+ refreshCompilerInputs(notify, skipUnobservedProjectInputWatchRoots) {
79
91
  const next = resolveWatchTopology(this.options, this.extraInputs);
80
- const projectInputProgramChange = next.analysisOnly &&
92
+ const compilerProgramMembershipChange = next.analysisOnly &&
81
93
  mapsEqual(this.reloadFiles, next.reloadFiles) &&
82
94
  mapsEqual(this.outputFiles, next.outputFiles) &&
83
95
  mapsEqual(this.outputs, next.outputs)
84
- ? projectInputCompilerMembershipChange(this.projectInputs, this.files, next.files)
96
+ ? compilerMembershipChange(this.files, next.files)
97
+ : [];
98
+ const projectInputProgramOverlap = projectInputCompilerMembershipChange(this.projectInputs, compilerProgramMembershipChange);
99
+ const projectInputProgramChange = compilerProgramMembershipChange.length !== 0 &&
100
+ projectInputProgramOverlap.length ===
101
+ compilerProgramMembershipChange.length
102
+ ? projectInputProgramOverlap
85
103
  : undefined;
86
104
  const changed = this.analysisOnly !== next.analysisOnly ||
87
105
  mapsEqual(this.files, next.files) === false ||
@@ -94,41 +112,105 @@ class WatchTopology {
94
112
  // Stamp the tracked set as it is resolved, so the first event that cannot
95
113
  // name what changed compares against the state the compiler just saw rather
96
114
  // than against nothing, which would make it nominate everything once.
97
- for (const key of [...this.compilerFileStamps.keys()]) {
115
+ for (const key of [...this.compilerFileSnapshots.keys()]) {
98
116
  if (!next.files.has(key))
99
- this.compilerFileStamps.delete(key);
117
+ this.compilerFileSnapshots.delete(key);
100
118
  }
101
119
  for (const [key, file] of next.files) {
102
120
  // Only a file with no stamp yet is seeded. Restamping one that already
103
121
  // has a baseline would advance it past a change nobody reported, and the
104
122
  // next unnamed event would then read that change as no change at all.
105
- if (!this.compilerFileStamps.has(key)) {
106
- this.compilerFileStamps.set(key, compilerFileStamp(file));
123
+ if (!this.compilerFileSnapshots.has(key)) {
124
+ this.compilerFileSnapshots.set(key, compilerFileSnapshot(file));
107
125
  }
108
126
  }
109
127
  this.directories = next.directories;
110
128
  this.outputFiles = next.outputFiles;
111
129
  this.outputs = next.outputs;
112
130
  this.reloadFiles = next.reloadFiles;
113
- this.syncFileWatchers();
114
- this.syncDirectoryWatchers();
131
+ for (const key of this.projectInputCompilerAcknowledgements.keys()) {
132
+ if (!next.files.has(key)) {
133
+ this.projectInputCompilerAcknowledgements.delete(key);
134
+ }
135
+ }
136
+ const projectInputProgramReload = projectInputProgramOverlap.length === 0
137
+ ? false
138
+ : this.acknowledgeProjectInputCompilerMembership(projectInputProgramOverlap);
139
+ const fileWatcherRegistered = this.syncFileWatchers();
140
+ const directoryWatcherRegistered = this.syncDirectoryWatchers();
115
141
  this.syncExtraWatchers();
116
- this.syncProjectInputWatchers();
142
+ this.syncProjectInputWatchers(skipUnobservedProjectInputWatchRoots);
143
+ if (fileWatcherRegistered || directoryWatcherRegistered) {
144
+ this.scheduleCompilerPostRegistrationReconciliation(directoryWatcherRegistered, skipUnobservedProjectInputWatchRoots);
145
+ }
117
146
  if (notify && changed) {
118
147
  if (projectInputProgramChange !== undefined) {
119
- this.callbacks.onInputChange({
120
- invalidate: true,
121
- kind: "project",
122
- path: projectInputProgramChange.length === 1
123
- ? projectInputProgramChange[0]
124
- : undefined,
125
- });
148
+ const changedPath = projectInputProgramChange.length === 1
149
+ ? projectInputProgramChange[0]
150
+ : undefined;
151
+ this.callbacks.onInputChange(projectInputProgramReload
152
+ ? { kind: "config", path: changedPath }
153
+ : {
154
+ invalidate: true,
155
+ kind: "project",
156
+ path: changedPath,
157
+ });
126
158
  }
127
159
  else {
128
160
  this.callbacks.onTopologyChange();
129
161
  }
130
162
  }
131
163
  }
164
+ /**
165
+ * Hand one Program-membership transition from the compiler lane to the
166
+ * overlapping project-input lane.
167
+ *
168
+ * Windows can deliver the compiler membership refresh before the recursive
169
+ * project watcher names the same creation. The rebuild scheduled here already
170
+ * consumes the current project bytes, so publishing their strong fingerprints
171
+ * keeps the later parent event from rediscovering the same population delta.
172
+ * A newly tracked compiler file also remembers that fingerprint until its
173
+ * first named content delivery; identical bytes are the delayed creation,
174
+ * while different bytes are a real later edit and remain observable even
175
+ * inside filesystem timestamp resolution.
176
+ */
177
+ acknowledgeProjectInputCompilerMembership(changed) {
178
+ const matches = this.collectProjectInputMatches();
179
+ const fingerprints = fingerprintProjectInputMatches(matches);
180
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
181
+ const changedInputs = projectInputChangedPaths({
182
+ next: matches,
183
+ nextFingerprints: fingerprints,
184
+ previous: this.projectInputMatches,
185
+ previousFingerprints: this.projectInputFingerprints,
186
+ });
187
+ const population = this.projectInputPopulation();
188
+ const causedBy = projectInputCompilerMembershipProjectChanges(changed, population.globs);
189
+ const reload = projectInputReloadEventShouldNotify({
190
+ causedBy,
191
+ changed: causedBy.length === 1 ? causedBy[0] : undefined,
192
+ changedInputs,
193
+ globs: population.globs,
194
+ reloadDirectories: population.reloadDirectories,
195
+ reloadFiles: population.reloadFiles ?? [],
196
+ });
197
+ // The callback below consumes the complete population observed by this
198
+ // scan. Crucially, reload classification runs against the old baseline
199
+ // first, so a concurrent selection delta becomes one cold transition
200
+ // instead of disappearing behind the warm compiler-membership handoff.
201
+ this.projectInputMatches = matches;
202
+ this.projectInputFingerprints = fingerprints;
203
+ for (const location of changed) {
204
+ const compilerKey = pathKey(location);
205
+ if (!this.files.has(compilerKey))
206
+ continue;
207
+ const fingerprint = fingerprints.get(identities.resolve(location).key);
208
+ if (fingerprint !== undefined && fingerprint !== "") {
209
+ this.projectInputCompilerAcknowledgements.set(compilerKey, fingerprint);
210
+ }
211
+ }
212
+ return reload;
213
+ }
132
214
  /** Add Go plugin source trees discovered by the real build lane. */
133
215
  setExtraInputs(inputs) {
134
216
  const next = uniqueExistingPaths(inputs);
@@ -157,20 +239,11 @@ class WatchTopology {
157
239
  }
158
240
  this.projectInputs = next;
159
241
  this.projectInputRejectedWatchRoots.clear();
160
- const declarations = new Set([
242
+ this.pruneProjectInputWatchRoots([
161
243
  next,
162
244
  inputs,
163
245
  { ...(inputs.declared ?? inputs), root: inputs.root },
164
- ].flatMap((snapshot) => [
165
- ...snapshot.files.map((file) => projectInputDeclarationKey("file", file)),
166
- ...snapshot.globs.map((glob) => projectInputDeclarationKey("glob", glob)),
167
- ...(snapshot.reloadFiles ?? []).map((file) => projectInputDeclarationKey("reload", file)),
168
- ...(snapshot.reloadDirectories ?? []).map((directory) => projectInputDeclarationKey("reload-directory", directory)),
169
- ]));
170
- for (const key of this.projectInputWatchRoots.keys()) {
171
- if (!declarations.has(key))
172
- this.projectInputWatchRoots.delete(key);
173
- }
246
+ ]);
174
247
  this.projectInputMatches = this.collectProjectInputMatches();
175
248
  this.projectInputFingerprints = fingerprintProjectInputMatches(this.projectInputMatches);
176
249
  this.syncProjectInputWatchers();
@@ -184,31 +257,43 @@ class WatchTopology {
184
257
  closeWatchers(this.projectInputWatchers);
185
258
  closeWatchers(this.projectInputLinkWatchers);
186
259
  }
187
- syncFileWatchers() {
188
- const files = process.platform === "win32" ? new Map() : this.files;
260
+ syncFileWatchers(skipMissing = false) {
261
+ const previous = new Map(this.fileWatchers);
262
+ const files = process.platform === "win32"
263
+ ? new Map()
264
+ : skipMissing
265
+ ? new Map([...this.files].filter(([, location]) => node_fs_1.default.existsSync(location)))
266
+ : this.files;
189
267
  syncWatchers(this.fileWatchers, files, (location) => node_fs_1.default.watch(watcherRegistrationPath(location), { persistent: true }, () => {
190
268
  // A per-file watcher fires on any filesystem attention its target
191
269
  // receives, and it carries no filename to distinguish an edit from
192
270
  // a touch. It answers the same question the unnamed directory event
193
271
  // answers, so it answers it the same way: from the bytes.
194
- if (!this.compilerFileMoved(location))
272
+ const movement = this.compilerFileMovement(location);
273
+ if (movement.owner)
274
+ this.rearmFileWatchers([location], true);
275
+ if (!movement.content)
195
276
  return;
196
277
  this.callbacks.onInputChange({
197
278
  kind: this.classifyCompilerInput(location),
198
279
  path: location,
199
280
  });
200
- }), (location, error) => this.callbacks.onError(location, error));
281
+ }), (location, error) => this.callbacks.onError(location, error), () => this.closed === false);
282
+ return [...this.fileWatchers].some(([key, watcher]) => previous.get(key) !== watcher);
201
283
  }
202
- /** Whether a tracked file's bytes differ from the last stamp taken. */
203
- compilerFileMoved(location) {
284
+ /** Compare a tracked file's content and physical owner with its snapshot. */
285
+ compilerFileMovement(location) {
204
286
  const key = pathKey(location);
205
- const stamp = compilerFileStamp(location);
206
- if (this.compilerFileStamps.get(key) === stamp)
207
- return false;
208
- this.compilerFileStamps.set(key, stamp);
209
- return true;
287
+ const previous = this.compilerFileSnapshots.get(key);
288
+ const next = compilerFileSnapshot(location);
289
+ this.compilerFileSnapshots.set(key, next);
290
+ return {
291
+ content: previous?.content !== next.content,
292
+ owner: previous?.owner !== next.owner,
293
+ };
210
294
  }
211
295
  syncDirectoryWatchers() {
296
+ const previous = new Map(this.directoryWatchers);
212
297
  const desired = new Map(this.directories);
213
298
  for (const [key, location] of this.observedDirectories) {
214
299
  if (isDirectory(location) === false ||
@@ -258,7 +343,79 @@ class WatchTopology {
258
343
  }
259
344
  if (plan.refresh)
260
345
  this.refreshFromDirectory(location, changed);
261
- }), (location, error) => this.callbacks.onError(location, error));
346
+ }), (location, error) => this.callbacks.onError(location, error), () => this.closed === false);
347
+ return [...this.directoryWatchers].some(([key, watcher]) => previous.get(key) !== watcher);
348
+ }
349
+ /**
350
+ * Reconcile tracked compiler files after a newly registered watcher returns.
351
+ *
352
+ * A file or directory watcher can be returned before its backend is ready to
353
+ * deliver the first event. The compiler-file stamps were captured before
354
+ * registration, so one coalesced microtask can recover a change in that
355
+ * handoff window. A real event updates the same stamp first and makes this
356
+ * bounded scan a no-op.
357
+ */
358
+ scheduleCompilerPostRegistrationReconciliation(refreshMembership, skipUnobservedProjectInputWatchRoots) {
359
+ if (this.closed)
360
+ return;
361
+ if (refreshMembership) {
362
+ this.compilerPostRegistrationMembershipRefresh = true;
363
+ this.compilerPostRegistrationSkipUnobservedProjectInputWatchRoots =
364
+ this.compilerPostRegistrationSkipUnobservedProjectInputWatchRoots &&
365
+ skipUnobservedProjectInputWatchRoots;
366
+ }
367
+ if (this.compilerPostRegistrationReconciliationScheduled)
368
+ return;
369
+ this.compilerPostRegistrationReconciliationScheduled = true;
370
+ queueMicrotask(() => {
371
+ this.compilerPostRegistrationReconciliationScheduled = false;
372
+ if (this.closed)
373
+ return;
374
+ const refreshCompilerMembership = this.compilerPostRegistrationMembershipRefresh;
375
+ const skipUnobservedProjectInputWatchRoots = this.compilerPostRegistrationSkipUnobservedProjectInputWatchRoots;
376
+ this.compilerPostRegistrationMembershipRefresh = false;
377
+ this.compilerPostRegistrationSkipUnobservedProjectInputWatchRoots = true;
378
+ const changed = [];
379
+ const rearm = [];
380
+ for (const file of this.files.values()) {
381
+ const movement = this.compilerFileMovement(file);
382
+ if (movement.content)
383
+ changed.push(file);
384
+ if (movement.owner)
385
+ rearm.push(file);
386
+ }
387
+ // A replacement can move the path to a new inode without changing its
388
+ // cheap content stamp or topology key. Rebind its physical owner without
389
+ // inventing a content notification. Missing entries remain covered by
390
+ // their parent directory and are retried when recreation is observed.
391
+ this.rearmFileWatchers(rearm, true);
392
+ for (const file of changed) {
393
+ this.callbacks.onInputChange({
394
+ kind: this.classifyCompilerInput(file),
395
+ path: file,
396
+ });
397
+ }
398
+ if (!refreshCompilerMembership)
399
+ return;
400
+ try {
401
+ // Directory watchers own files not present in the current Program.
402
+ // Re-resolve even when every tracked stamp is unchanged so a swallowed
403
+ // startup event cannot strand a newly included source.
404
+ this.refreshCompilerInputs(true, skipUnobservedProjectInputWatchRoots);
405
+ }
406
+ catch (error) {
407
+ const reported = new Set(changed.map(pathKey));
408
+ const reconciledChange = changed.length === 1 ? changed[0] : undefined;
409
+ for (const reload of reloadInputsForFailedTopologyRefresh(this.reloadFiles.values(), reconciledChange)) {
410
+ if (reported.has(pathKey(reload)))
411
+ continue;
412
+ this.callbacks.onInputChange({ kind: "config", path: reload });
413
+ }
414
+ this.callbacks.onError(reconciledChange === undefined
415
+ ? (this.options.projectRoot ?? this.options.cwd)
416
+ : node_path_1.default.dirname(reconciledChange), error);
417
+ }
418
+ });
262
419
  }
263
420
  /**
264
421
  * Narrow a plan's changes to the tracked files that actually moved.
@@ -282,22 +439,29 @@ class WatchTopology {
282
439
  // Those two are decided from the bytes, and the rearm they drive is
283
440
  // unaffected, because rebinding is about the inode and not the content.
284
441
  if (changed !== undefined && event !== "rename") {
285
- for (const file of changes)
286
- this.recordCompilerFileStamp(file);
287
- return [...changes];
442
+ return changes.filter((file) => {
443
+ const key = pathKey(file);
444
+ const acknowledged = this.projectInputCompilerAcknowledgements.get(key);
445
+ this.projectInputCompilerAcknowledgements.delete(key);
446
+ this.recordCompilerFileSnapshot(file);
447
+ return (acknowledged === undefined ||
448
+ acknowledged !== fingerprintProjectInputFile(file));
449
+ });
288
450
  }
289
- return changes.filter((file) => this.compilerFileMoved(file));
451
+ return changes.filter((file) => this.compilerFileMovement(file).content);
290
452
  }
291
- recordCompilerFileStamp(file) {
292
- this.compilerFileStamps.set(pathKey(file), compilerFileStamp(file));
453
+ recordCompilerFileSnapshot(file) {
454
+ this.compilerFileSnapshots.set(pathKey(file), compilerFileSnapshot(file));
293
455
  }
294
- rearmFileWatchers(files) {
456
+ rearmFileWatchers(files, skipMissing = false) {
295
457
  for (const file of files) {
296
458
  const key = pathKey(file);
297
459
  this.fileWatchers.get(key)?.close();
298
460
  this.fileWatchers.delete(key);
299
461
  }
300
- this.syncFileWatchers();
462
+ if (this.syncFileWatchers(skipMissing)) {
463
+ this.scheduleCompilerPostRegistrationReconciliation(false, true);
464
+ }
301
465
  }
302
466
  syncExtraWatchers() {
303
467
  const directories = new Map();
@@ -314,20 +478,21 @@ class WatchTopology {
314
478
  kind: "plugin",
315
479
  path: changed ?? location,
316
480
  });
317
- }), (location, error) => this.callbacks.onError(location, error));
481
+ }), (location, error) => this.callbacks.onError(location, error), () => this.closed === false);
318
482
  }
319
- syncProjectInputWatchers() {
483
+ syncProjectInputWatchers(skipUnobservedProjectInputWatchRoots = false) {
320
484
  if (this.closed)
321
485
  return;
486
+ const previous = new Map(this.projectInputWatchers);
487
+ const previousLinks = new Map(this.projectInputLinkWatchers);
322
488
  const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
323
489
  const desired = new Map();
490
+ const required = new Map();
324
491
  for (const file of this.projectInputDeclarations("file")) {
325
492
  if (this.isProjectInputCompilerOutput(file, identities))
326
493
  continue;
327
494
  const location = this.projectInputWatchRoot("file", file, node_path_1.default.dirname(file));
328
- if (location !== undefined) {
329
- this.retainProjectInputWatchRoot(desired, identities, location);
330
- }
495
+ this.retainProjectInputWatchRoot(required, desired, identities, location, node_path_1.default.dirname(file), skipUnobservedProjectInputWatchRoots);
331
496
  }
332
497
  for (const glob of this.projectInputDeclarations("glob")) {
333
498
  const root = literalGlobRoot(glob);
@@ -335,32 +500,27 @@ class WatchTopology {
335
500
  continue;
336
501
  }
337
502
  const location = this.projectInputWatchRoot("glob", glob, root);
338
- if (location !== undefined) {
339
- this.retainProjectInputWatchRoot(desired, identities, location);
340
- }
503
+ this.retainProjectInputWatchRoot(required, desired, identities, location, root, skipUnobservedProjectInputWatchRoots);
341
504
  }
342
505
  for (const file of this.projectInputDeclarations("reload")) {
343
506
  if (this.isProjectInputCompilerOutput(file, identities))
344
507
  continue;
345
508
  const location = this.projectInputWatchRoot("reload", file, node_path_1.default.dirname(file));
346
- if (location !== undefined) {
347
- this.retainProjectInputWatchRoot(desired, identities, location);
348
- }
509
+ this.retainProjectInputWatchRoot(required, desired, identities, location, node_path_1.default.dirname(file), skipUnobservedProjectInputWatchRoots);
349
510
  }
350
511
  for (const directory of this.projectInputDeclarations("reload-directory")) {
351
512
  if (this.isProjectInputCompilerOutputDirectory(directory, identities)) {
352
513
  continue;
353
514
  }
354
515
  const location = this.projectInputWatchRoot("reload-directory", directory, directory);
355
- if (location !== undefined) {
356
- this.retainProjectInputWatchRoot(desired, identities, location);
357
- }
516
+ this.retainProjectInputWatchRoot(required, desired, identities, location, directory, skipUnobservedProjectInputWatchRoots);
358
517
  }
359
518
  const active = new Map();
360
519
  for (const location of projectInputActiveWatchDirectories(desired.values(), identities)) {
361
520
  const identity = identities.resolve(location);
362
521
  active.set(identity.key, identity.path);
363
522
  }
523
+ this.projectInputRequiredWatchRoots = required;
364
524
  syncWatchers(this.projectInputWatchers, active, (location) => node_fs_1.default.watch(watcherRegistrationPath(location), { persistent: true, recursive: true }, (_event, filename) => {
365
525
  const changed = filename === null
366
526
  ? undefined
@@ -371,12 +531,24 @@ class WatchTopology {
371
531
  const firstFailure = !this.projectInputRejectedWatchRoots.has(key);
372
532
  this.projectInputRejectedWatchRoots.add(key);
373
533
  this.callbacks.onError(location, error);
374
- if (firstFailure) {
375
- queueMicrotask(() => this.syncProjectInputWatchers());
534
+ if (firstFailure && !this.closed) {
535
+ this.scheduleProjectInputWatcherRecovery();
376
536
  }
377
- });
537
+ }, () => this.closed === false);
538
+ if (this.closed)
539
+ return;
540
+ if (!this.projectInputRecoveryScheduled) {
541
+ this.reportUnobservedProjectInputWatchRoots(required);
542
+ }
378
543
  this.syncProjectInputLinkWatchers(identities);
379
- this.callbacks.onProjectInputWatchRoots?.([...this.projectInputWatchers.keys()].sort());
544
+ const watcherRegistered = [...this.projectInputWatchers].some(([key, watcher]) => previous.get(key) !== watcher) ||
545
+ [...this.projectInputLinkWatchers].some(([key, watcher]) => previousLinks.get(key) !== watcher);
546
+ if (watcherRegistered) {
547
+ this.scheduleProjectInputPostRegistrationReconciliation();
548
+ }
549
+ this.callbacks.onProjectInputWatchRoots?.([...this.projectInputWatchers.keys()]
550
+ .map((key) => active.get(key) ?? identities.resolve(key).path)
551
+ .sort());
380
552
  }
381
553
  /**
382
554
  * Watch the directory that holds a declaration which is itself a link.
@@ -415,7 +587,7 @@ class WatchTopology {
415
587
  ? undefined
416
588
  : node_path_1.default.resolve(location, filename.toString());
417
589
  this.refreshProjectInputs(location, changed);
418
- }), (location, error) => this.callbacks.onError(location, error));
590
+ }), (location, error) => this.callbacks.onError(location, error), () => this.closed === false);
419
591
  }
420
592
  /** Drop the watcher that just reported a directory replacement. */
421
593
  retireProjectInputWatcher(location, identities) {
@@ -435,13 +607,127 @@ class WatchTopology {
435
607
  watchers.delete(key);
436
608
  }
437
609
  }
438
- retainProjectInputWatchRoot(desired, identities, location) {
610
+ retainProjectInputWatchRoot(required, desired, identities, location, target, skipUnobservedProjectInputWatchRoots) {
611
+ const requiredIdentity = identities.resolve(location ?? target);
612
+ required.set(requiredIdentity.key, requiredIdentity.path);
613
+ if (skipUnobservedProjectInputWatchRoots) {
614
+ const retainedActiveRoot = [...this.projectInputWatchers.keys()].find((root) => (0, projectInputPathIdentity_1.isProjectInputPathIdentityWithin)(root, requiredIdentity.key));
615
+ if (retainedActiveRoot !== undefined) {
616
+ desired.set(retainedActiveRoot, retainedActiveRoot);
617
+ return;
618
+ }
619
+ }
620
+ if (location === undefined ||
621
+ (skipUnobservedProjectInputWatchRoots &&
622
+ this.projectInputUnobservedWatchRoots.has(requiredIdentity.key))) {
623
+ return;
624
+ }
439
625
  const available = projectInputAvailableWatchDirectory(location, this.projectInputRejectedWatchRoots, identities, this.projectInputs.root);
440
626
  if (available === undefined)
441
627
  return;
442
628
  const identity = identities.resolve(available);
443
629
  desired.set(identity.key, identity.path);
444
630
  }
631
+ /**
632
+ * Retry a failed root on the next reconciliation instead of retiring it for
633
+ * the session.
634
+ *
635
+ * This immediate recovery pass still honors the rejected root so it can
636
+ * install a safe ancestor where one exists. Only the recovery fixpoint
637
+ * reports a genuinely uncovered lane; transient gaps between fallback
638
+ * candidates are not user-visible. The rejection then expires. A later
639
+ * compiler refresh or an unchanged project-input republication can retry the
640
+ * original root, while a permanently failing backend costs at most one
641
+ * attempt per sync.
642
+ */
643
+ scheduleProjectInputWatcherRecovery() {
644
+ if (this.projectInputRecoveryScheduled)
645
+ return;
646
+ this.projectInputRecoveryScheduled = true;
647
+ queueMicrotask(() => {
648
+ try {
649
+ let previousRejectionCount = -1;
650
+ while (this.closed === false &&
651
+ previousRejectionCount !== this.projectInputRejectedWatchRoots.size) {
652
+ previousRejectionCount = this.projectInputRejectedWatchRoots.size;
653
+ this.syncProjectInputWatchers();
654
+ }
655
+ }
656
+ finally {
657
+ this.projectInputRejectedWatchRoots.clear();
658
+ this.projectInputRecoveryScheduled = false;
659
+ if (!this.closed) {
660
+ this.reportUnobservedProjectInputWatchRoots(this.projectInputRequiredWatchRoots);
661
+ }
662
+ }
663
+ });
664
+ }
665
+ /**
666
+ * Reconcile the snapshot-to-watcher handoff after the caller's current turn.
667
+ *
668
+ * A recursive watcher can return before its backend is ready to deliver the
669
+ * first event. The publication baseline is necessarily captured before that
670
+ * watcher exists, so an input materialized synchronously after
671
+ * `setProjectInputs()` would otherwise depend entirely on that startup event.
672
+ * The ordinary fingerprint update makes this scan and a real backend event
673
+ * race safely: whichever arrives first records the new population and the
674
+ * other becomes a no-op.
675
+ */
676
+ scheduleProjectInputPostRegistrationReconciliation() {
677
+ if (this.closed ||
678
+ this.projectInputPostRegistrationReconciliationScheduled) {
679
+ return;
680
+ }
681
+ this.projectInputPostRegistrationReconciliationScheduled = true;
682
+ queueMicrotask(() => {
683
+ this.projectInputPostRegistrationReconciliationScheduled = false;
684
+ if (this.closed)
685
+ return;
686
+ this.refreshPublishedProjectInputIdentities();
687
+ this.refreshProjectInputs(this.projectInputs.root, undefined, true);
688
+ });
689
+ }
690
+ /**
691
+ * Re-resolve declarations after watcher registration.
692
+ *
693
+ * A missing path can become a symlink before the handoff scan. The retained
694
+ * normalized snapshot still names the pre-link spelling in that case, so a
695
+ * scan can find the first target file without installing the physical owner
696
+ * that must observe later target changes.
697
+ */
698
+ refreshPublishedProjectInputIdentities() {
699
+ const next = normalizeProjectInputSnapshot(this.declaredProjectInputs);
700
+ if (projectInputSnapshotsEqual(this.projectInputs, next))
701
+ return;
702
+ this.projectInputs = next;
703
+ this.pruneProjectInputWatchRoots([next, this.declaredProjectInputs]);
704
+ }
705
+ /** Drop retained owner choices for declarations no longer published. */
706
+ pruneProjectInputWatchRoots(snapshots) {
707
+ const declarations = new Set(snapshots.flatMap((snapshot) => [
708
+ ...snapshot.files.map((file) => projectInputDeclarationKey("file", file)),
709
+ ...snapshot.globs.map((glob) => projectInputDeclarationKey("glob", glob)),
710
+ ...(snapshot.reloadFiles ?? []).map((file) => projectInputDeclarationKey("reload", file)),
711
+ ...(snapshot.reloadDirectories ?? []).map((directory) => projectInputDeclarationKey("reload-directory", directory)),
712
+ ]));
713
+ for (const key of this.projectInputWatchRoots.keys()) {
714
+ if (!declarations.has(key))
715
+ this.projectInputWatchRoots.delete(key);
716
+ }
717
+ }
718
+ /** Report only newly uncovered project-input roots as an observation loss. */
719
+ reportUnobservedProjectInputWatchRoots(required) {
720
+ const active = [...this.projectInputWatchers.keys()];
721
+ const unavailable = new Map([...required].filter(([key]) => active.every((root) => !(0, projectInputPathIdentity_1.isProjectInputPathIdentityWithin)(root, key))));
722
+ const newlyUnavailable = [...unavailable]
723
+ .filter(([key]) => !this.projectInputUnobservedWatchRoots.has(key))
724
+ .map(([, location]) => location)
725
+ .sort();
726
+ this.projectInputUnobservedWatchRoots = unavailable;
727
+ if (newlyUnavailable.length !== 0) {
728
+ this.callbacks.onProjectInputWatchUnavailable?.(newlyUnavailable);
729
+ }
730
+ }
445
731
  /**
446
732
  * One snapshot holding every spelling of every declaration.
447
733
  *
@@ -504,7 +790,7 @@ class WatchTopology {
504
790
  this.projectInputWatchRoots.set(key, resolved);
505
791
  return resolved;
506
792
  }
507
- refreshProjectInputs(location, changed) {
793
+ refreshProjectInputs(location, changed, skipUnobservedProjectInputWatchRoots = false) {
508
794
  try {
509
795
  const previous = this.projectInputMatches;
510
796
  const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
@@ -547,19 +833,20 @@ class WatchTopology {
547
833
  previous,
548
834
  previousFingerprints: this.projectInputFingerprints,
549
835
  });
836
+ const reconciledChange = changed ?? (changedInputs.length === 1 ? changedInputs[0] : undefined);
550
837
  // Both spellings classify the event. The normalized form names the file a
551
838
  // link pointed at when the snapshot was published, so after a retarget it
552
839
  // names the wrong one; only the declared form resolves to what the link
553
840
  // points at now, which is the selection this lane exists to protect.
554
841
  const reload = projectInputReloadEventShouldNotify({
555
- changed,
842
+ changed: reconciledChange,
556
843
  changedInputs,
557
844
  globs: population.globs,
558
845
  reloadDirectories: population.reloadDirectories ?? [],
559
846
  reloadFiles: population.reloadFiles ?? [],
560
847
  });
561
848
  const invalidate = projectInputMembershipInvalidatesProgram({
562
- changed,
849
+ changed: reconciledChange,
563
850
  changedInputs,
564
851
  contentChanged,
565
852
  next,
@@ -567,26 +854,28 @@ class WatchTopology {
567
854
  });
568
855
  this.projectInputMatches = next;
569
856
  this.projectInputFingerprints = nextFingerprints;
570
- this.syncProjectInputWatchers();
857
+ this.syncProjectInputWatchers(skipUnobservedProjectInputWatchRoots);
571
858
  // A JSON/TS/JS project-input member can simultaneously enter or leave
572
859
  // the compiler Program. Reconcile the compiler watch snapshot before
573
860
  // scheduling its resident invalidation, so runWatch's post-cycle refresh
574
861
  // does not rediscover the same delta as a broader execution reload.
575
- if (invalidate)
576
- this.refresh(false);
862
+ if (invalidate) {
863
+ this.refreshCompilerInputs(false, skipUnobservedProjectInputWatchRoots);
864
+ }
577
865
  if (projectInputEventShouldNotify({
578
866
  contentChanged,
579
867
  directlyMatched,
580
868
  membershipChanged,
581
869
  }) &&
582
- (changed === undefined ||
583
- this.isProjectInputCompilerOutput(changed, identities) === false)) {
870
+ (reconciledChange === undefined ||
871
+ this.isProjectInputCompilerOutput(reconciledChange, identities) ===
872
+ false)) {
584
873
  this.callbacks.onInputChange(reload
585
- ? { kind: "config", path: changed }
874
+ ? { kind: "config", path: reconciledChange }
586
875
  : {
587
876
  ...(invalidate ? { invalidate: true } : {}),
588
877
  kind: "project",
589
- path: changed,
878
+ path: reconciledChange,
590
879
  });
591
880
  }
592
881
  }
@@ -595,7 +884,7 @@ class WatchTopology {
595
884
  // replacement is readable. Rebind ancestor ownership even when the
596
885
  // population scan races that transient gap, so a later create cannot be
597
886
  // stranded without a watcher.
598
- this.syncProjectInputWatchers();
887
+ this.syncProjectInputWatchers(skipUnobservedProjectInputWatchRoots);
599
888
  this.callbacks.onError(location, error);
600
889
  }
601
890
  }
@@ -691,7 +980,14 @@ class WatchTopology {
691
980
  }
692
981
  isProjectInputCompilerOutputDirectory(location, identities) {
693
982
  const root = this.projectInputs.root;
983
+ let overlaps = this.projectInputCompilerOutputOverlaps.get(identities);
984
+ if (overlaps === undefined) {
985
+ overlaps = new Map();
986
+ this.projectInputCompilerOutputOverlaps.set(identities, overlaps);
987
+ }
694
988
  return [...this.outputs.values()].some((output) => {
989
+ if (!identities.isWithin(output, location))
990
+ return false;
695
991
  // An output directory that is the project itself, or holds it, is not a
696
992
  // place where only build products live -- the sources are there too. A
697
993
  // project emitting in place declares exactly that, and honouring it
@@ -701,7 +997,14 @@ class WatchTopology {
701
997
  // rebuild, while an input that does not is never seen again.
702
998
  if (root !== "" && identities.isWithin(output, root))
703
999
  return false;
704
- return identities.isWithin(output, location);
1000
+ let overlapsCompilerInput = overlaps.get(output);
1001
+ if (overlapsCompilerInput === undefined) {
1002
+ overlapsCompilerInput = [...this.files.values()].some((input) => identities.isWithin(output, input));
1003
+ overlaps.set(output, overlapsCompilerInput);
1004
+ }
1005
+ if (overlapsCompilerInput)
1006
+ return false;
1007
+ return true;
705
1008
  });
706
1009
  }
707
1010
  isProjectInputCompilerOutput(location, identities) {
@@ -791,7 +1094,7 @@ function resolveWatchTopology(options, extraInputs) {
791
1094
  const compilerInputs = listCompilerInputs(project, options);
792
1095
  const compilerOutputs = resolveCompilerOutputs(project, options);
793
1096
  addPaths(outputFiles, compilerOutputs.files);
794
- addPaths(outputFiles, inferAdjacentCompilerOutputs(project, options, compilerInputs));
1097
+ addPaths(outputFiles, inferPerSourceCompilerOutputs(project, options, compilerInputs));
795
1098
  addPaths(outputs, compilerOutputs.directories);
796
1099
  addPaths(files, compilerInputs);
797
1100
  }
@@ -876,13 +1179,16 @@ function listCompilerInputs(project, options) {
876
1179
  if (result.error) {
877
1180
  throw new Error(`ttsc: failed to list compiler inputs: ${result.error.message}`);
878
1181
  }
879
- const outputs = resolveCompilerOutputs(project, options);
1182
+ // `--listFilesOnly` is the authority for compiler inputs. A path may also be
1183
+ // a predicted product of another input, but that collision does not revoke
1184
+ // its Program membership—especially while the compiler is reporting an
1185
+ // overwrite diagnostic. Product inference is used only to classify future
1186
+ // filesystem events, never to subtract from the compiler's answer.
880
1187
  const inputs = (0, spawnNative_1.outputText)(result.stdout)
881
1188
  .split(/\r?\n/)
882
1189
  .map((line) => line.trim())
883
1190
  .filter((line) => node_path_1.default.isAbsolute(line))
884
- .map((line) => node_path_1.default.resolve(line))
885
- .filter((file) => isCompilerOutput(file, outputs) === false);
1191
+ .map((line) => node_path_1.default.resolve(line));
886
1192
  if (result.status !== 0 && inputs.length === 0) {
887
1193
  throw new Error(`ttsc: failed to list compiler inputs:\n${(0, spawnNative_1.outputText)(result.stderr) || (0, spawnNative_1.outputText)(result.stdout)}`);
888
1194
  }
@@ -899,19 +1205,6 @@ function resolveCompilerOutputs(project, options) {
899
1205
  if (emit.declaration && emit.declarationDir !== undefined) {
900
1206
  directories.add(emit.declarationDir);
901
1207
  }
902
- if (emit.outFile !== undefined) {
903
- if (emit.javascript) {
904
- files.add(emit.outFile);
905
- if (emit.sourceMap)
906
- files.add(`${emit.outFile}.map`);
907
- }
908
- if (emit.declaration) {
909
- const declaration = replaceOutputExtension(emit.outFile, ".d.ts");
910
- files.add(declaration);
911
- if (emit.declarationMap)
912
- files.add(`${declaration}.map`);
913
- }
914
- }
915
1208
  if (emit.incremental) {
916
1209
  files.add(defaultTsBuildInfoFile(project, emit));
917
1210
  }
@@ -920,47 +1213,72 @@ function resolveCompilerOutputs(project, options) {
920
1213
  files: [...files],
921
1214
  };
922
1215
  }
923
- function inferAdjacentCompilerOutputs(project, options, inputs) {
1216
+ function inferPerSourceCompilerOutputs(project, options, inputs) {
924
1217
  const emit = effectiveCompilerEmit(project, options);
925
1218
  const outputs = new Set();
1219
+ // TypeScript-Go 7 requires an explicit rootDir when output layout would
1220
+ // otherwise need inference. While reporting TS5011 it still emits relative
1221
+ // to the config directory, so the watch model must use that same recovery
1222
+ // layout rather than the legacy common-source-directory rule.
1223
+ const sourceRoot = emit.rootDir ?? project.root;
926
1224
  for (const input of inputs) {
927
1225
  const extension = node_path_1.default.extname(input).toLowerCase();
928
- if (!isCompilerEmittableSourceExtension(extension)) {
929
- continue;
930
- }
931
1226
  if (/\.d\.(?:ts|mts|cts)$/i.test(input))
932
1227
  continue;
933
1228
  const stem = input.slice(0, -extension.length);
1229
+ const mappedStem = (directory, adjacentWhenOutside) => {
1230
+ if (directory === undefined)
1231
+ return stem;
1232
+ if (!isPathWithin(sourceRoot, input)) {
1233
+ return adjacentWhenOutside ? stem : undefined;
1234
+ }
1235
+ return node_path_1.default.resolve(directory, node_path_1.default.relative(sourceRoot, stem));
1236
+ };
1237
+ if (extension === ".json") {
1238
+ if (emit.javascript &&
1239
+ emit.resolveJsonModule &&
1240
+ emit.outDir !== undefined) {
1241
+ const jsonStem = mappedStem(emit.outDir, false);
1242
+ if (jsonStem !== undefined)
1243
+ outputs.add(`${jsonStem}.json`);
1244
+ }
1245
+ continue;
1246
+ }
1247
+ if (!isCompilerEmittableSourceExtension(extension))
1248
+ continue;
934
1249
  if (emit.javascript &&
935
- emit.outDir === undefined &&
936
- emit.outFile === undefined &&
937
- (!isJavaScriptSourceExtension(extension) ||
1250
+ (emit.outDir !== undefined ||
1251
+ !isJavaScriptSourceExtension(extension) ||
938
1252
  (extension === ".jsx" && emit.jsx !== "preserve"))) {
939
- const javascriptExtension = extension === ".mts"
1253
+ const javascriptExtension = extension === ".mts" || extension === ".mjs"
940
1254
  ? ".mjs"
941
- : extension === ".cts"
1255
+ : extension === ".cts" || extension === ".cjs"
942
1256
  ? ".cjs"
943
- : extension === ".tsx" && emit.jsx === "preserve"
1257
+ : (extension === ".tsx" || extension === ".jsx") &&
1258
+ emit.jsx === "preserve"
944
1259
  ? ".jsx"
945
1260
  : ".js";
946
- const javascript = stem + javascriptExtension;
947
- outputs.add(javascript);
948
- if (emit.sourceMap)
949
- outputs.add(`${javascript}.map`);
950
- }
951
- if (emit.declaration &&
952
- emit.outDir === undefined &&
953
- emit.declarationDir === undefined &&
954
- emit.outFile === undefined) {
1261
+ const javascriptStem = mappedStem(emit.outDir, true);
1262
+ if (javascriptStem !== undefined) {
1263
+ const javascript = javascriptStem + javascriptExtension;
1264
+ outputs.add(javascript);
1265
+ if (emit.sourceMap)
1266
+ outputs.add(`${javascript}.map`);
1267
+ }
1268
+ }
1269
+ if (emit.declaration) {
955
1270
  const declarationExtension = extension === ".mts" || extension === ".mjs"
956
1271
  ? ".d.mts"
957
1272
  : extension === ".cts" || extension === ".cjs"
958
1273
  ? ".d.cts"
959
1274
  : ".d.ts";
960
- const declaration = stem + declarationExtension;
961
- outputs.add(declaration);
962
- if (emit.declarationMap)
963
- outputs.add(`${declaration}.map`);
1275
+ const declarationStem = mappedStem(emit.declarationDir ?? emit.outDir, true);
1276
+ if (declarationStem !== undefined) {
1277
+ const declaration = declarationStem + declarationExtension;
1278
+ outputs.add(declaration);
1279
+ if (emit.declarationMap)
1280
+ outputs.add(`${declaration}.map`);
1281
+ }
964
1282
  }
965
1283
  }
966
1284
  return [...outputs];
@@ -1013,16 +1331,16 @@ function effectiveCompilerEmit(project, options) {
1013
1331
  compilerOptions.declarationMap === true);
1014
1332
  const cliOutDir = passthroughPathOption(passthrough, "--outDir");
1015
1333
  const cliDeclarationDir = passthroughPathOption(passthrough, "--declarationDir");
1016
- const cliOutFile = passthroughPathOption(passthrough, "--outFile");
1017
1334
  const cliRootDir = passthroughPathOption(passthrough, "--rootDir");
1018
1335
  const cliTsBuildInfoFile = passthroughPathOption(passthrough, "--tsBuildInfoFile");
1019
1336
  const jsx = passthroughStringOption(passthrough, "--jsx") ?? compilerOptions.jsx;
1337
+ const compilerCwd = project.root;
1020
1338
  return {
1021
1339
  declaration,
1022
1340
  declarationDir: cliDeclarationDir === null
1023
1341
  ? undefined
1024
1342
  : cliDeclarationDir !== undefined
1025
- ? node_path_1.default.resolve(options.cwd, cliDeclarationDir)
1343
+ ? node_path_1.default.resolve(compilerCwd, cliDeclarationDir)
1026
1344
  : typeof compilerOptions.declarationDir === "string"
1027
1345
  ? node_path_1.default.resolve(compilerOptions.declarationDir)
1028
1346
  : undefined,
@@ -1032,23 +1350,18 @@ function effectiveCompilerEmit(project, options) {
1032
1350
  outDir: cliOutDir === null
1033
1351
  ? undefined
1034
1352
  : cliOutDir !== undefined
1035
- ? node_path_1.default.resolve(options.cwd, cliOutDir)
1353
+ ? node_path_1.default.resolve(compilerCwd, cliOutDir)
1036
1354
  : options.outDir !== undefined
1037
1355
  ? node_path_1.default.resolve(options.cwd, options.outDir)
1038
1356
  : typeof compilerOptions.outDir === "string"
1039
1357
  ? node_path_1.default.resolve(compilerOptions.outDir)
1040
1358
  : undefined,
1041
- outFile: cliOutFile === null
1042
- ? undefined
1043
- : cliOutFile !== undefined
1044
- ? node_path_1.default.resolve(options.cwd, cliOutFile)
1045
- : typeof compilerOptions.outFile === "string"
1046
- ? node_path_1.default.resolve(compilerOptions.outFile)
1047
- : undefined,
1359
+ resolveJsonModule: passthroughBooleanOption(passthrough, "--resolveJsonModule") ??
1360
+ compilerOptions.resolveJsonModule === true,
1048
1361
  rootDir: cliRootDir === null
1049
1362
  ? undefined
1050
1363
  : cliRootDir !== undefined
1051
- ? node_path_1.default.resolve(options.cwd, cliRootDir)
1364
+ ? node_path_1.default.resolve(compilerCwd, cliRootDir)
1052
1365
  : typeof compilerOptions.rootDir === "string"
1053
1366
  ? node_path_1.default.resolve(compilerOptions.rootDir)
1054
1367
  : undefined,
@@ -1056,7 +1369,7 @@ function effectiveCompilerEmit(project, options) {
1056
1369
  tsBuildInfoFile: cliTsBuildInfoFile === null
1057
1370
  ? undefined
1058
1371
  : cliTsBuildInfoFile !== undefined
1059
- ? node_path_1.default.resolve(options.cwd, cliTsBuildInfoFile)
1372
+ ? node_path_1.default.resolve(compilerCwd, cliTsBuildInfoFile)
1060
1373
  : typeof compilerOptions.tsBuildInfoFile === "string"
1061
1374
  ? node_path_1.default.resolve(compilerOptions.tsBuildInfoFile)
1062
1375
  : undefined,
@@ -1124,10 +1437,6 @@ function passthroughOptionMatches(token, name) {
1124
1437
  return false;
1125
1438
  return (0, schema_1.resolveFlagSpec)(token)?.name === (0, schema_1.resolveFlagSpec)(name)?.name;
1126
1439
  }
1127
- function isCompilerOutput(file, outputs) {
1128
- return (outputs.files.some((output) => pathKey(output) === pathKey(file)) ||
1129
- outputs.directories.some((directory) => isPathWithin(directory, file)));
1130
- }
1131
1440
  function collectTopologyDirectories(files, roots) {
1132
1441
  const directories = new Map();
1133
1442
  for (const root of roots) {
@@ -1191,9 +1500,13 @@ function isDirectory(location) {
1191
1500
  return false;
1192
1501
  }
1193
1502
  }
1194
- function syncWatchers(watchers, desired, create, onError) {
1503
+ function syncWatchers(watchers, desired, create, onError, shouldContinue = () => true) {
1195
1504
  let complete = true;
1196
1505
  for (const [key, location] of desired) {
1506
+ if (!shouldContinue()) {
1507
+ complete = false;
1508
+ break;
1509
+ }
1197
1510
  if (watchers.has(key))
1198
1511
  continue;
1199
1512
  try {
@@ -1412,19 +1725,23 @@ function projectInputRecursiveWatchRoot(target, projectRoot, identities = (0, pr
1412
1725
  return undefined;
1413
1726
  }
1414
1727
  /**
1415
- * A cheap identity for a tracked file's current bytes.
1728
+ * Cheap content and physical-owner identities for a tracked file.
1416
1729
  *
1417
- * Modification time and size together answer "did this move" without reading
1418
- * the file, which is all the unnamed-event path needs; a file that vanished
1419
- * answers as absent rather than as unchanged.
1730
+ * Modification time and size answer "did the bytes move" without reading the
1731
+ * file. Device and inode answer whether a POSIX per-file watcher still owns the
1732
+ * path. Keeping the answers separate lets an identical atomic replacement
1733
+ * rebind its watcher without inventing a compiler notification.
1420
1734
  */
1421
- function compilerFileStamp(location) {
1735
+ function compilerFileSnapshot(location) {
1422
1736
  try {
1423
1737
  const stats = node_fs_1.default.statSync(location);
1424
- return `${stats.mtimeMs}:${stats.size}`;
1738
+ return {
1739
+ content: `${stats.mtimeMs}:${stats.size}`,
1740
+ owner: `${stats.dev}:${stats.ino}`,
1741
+ };
1425
1742
  }
1426
1743
  catch {
1427
- return "";
1744
+ return { content: "", owner: "" };
1428
1745
  }
1429
1746
  }
1430
1747
  function isSymbolicLink(location) {
@@ -1613,14 +1930,15 @@ function projectInputReloadEventShouldNotify(input) {
1613
1930
  // resolution moved. A missed reselection is a wrong answer; a spare restart is
1614
1931
  // a slow one.
1615
1932
  const explains = (directory) => {
1616
- if (input.changed === undefined)
1617
- return false;
1933
+ const causes = input.causedBy ?? (input.changed === undefined ? [] : [input.changed]);
1618
1934
  // Only an immediate entry can move this directory's digest, so only an
1619
1935
  // immediate entry can account for the delta. Asking whether the event was
1620
1936
  // data somewhere else answers a different directory's question, and one
1621
1937
  // exemption would then cancel every other directory's evidence.
1622
- const parent = identities.resolve(node_path_1.default.dirname(input.changed)).key;
1623
- return parent === directory.key && exemptedFrom(directory, input.changed);
1938
+ return causes.some((location) => {
1939
+ const parent = identities.resolve(node_path_1.default.dirname(location)).key;
1940
+ return parent === directory.key && exemptedFrom(directory, location);
1941
+ });
1624
1942
  };
1625
1943
  const isReloadDirectoryEvent = (location) => namesDirectory(location) || holdsAsImmediateEntry(location);
1626
1944
  const isReloadDirectoryDelta = (location) => {
@@ -1673,7 +1991,7 @@ function projectInputPathMayAffectProgram(location) {
1673
1991
  const extension = node_path_1.default.extname(location).toLowerCase();
1674
1992
  return extension === ".json" || isCompilerEmittableSourceExtension(extension);
1675
1993
  }
1676
- function projectInputCompilerMembershipChange(snapshot, previous, next) {
1994
+ function compilerMembershipChange(previous, next) {
1677
1995
  const changed = new Map();
1678
1996
  for (const [key, location] of previous) {
1679
1997
  if (next.has(key) === false)
@@ -1683,54 +2001,84 @@ function projectInputCompilerMembershipChange(snapshot, previous, next) {
1683
2001
  if (previous.has(key) === false)
1684
2002
  changed.set(key, location);
1685
2003
  }
1686
- if (changed.size === 0 ||
1687
- [...changed.values()].some((location) => matchesProjectInput(snapshot, location) === false ||
1688
- projectInputPathMayAffectProgram(location) === false)) {
1689
- return undefined;
1690
- }
1691
2004
  return [...changed.values()].sort();
1692
2005
  }
2006
+ function projectInputCompilerMembershipChange(snapshot, changed) {
2007
+ return changed.filter((location) => matchesProjectInput(snapshot, location) &&
2008
+ projectInputPathMayAffectProgram(location));
2009
+ }
2010
+ /**
2011
+ * Name a compiler-membership transition as the recursive project watcher does.
2012
+ *
2013
+ * Creating the first member below a missing glob root makes Windows report the
2014
+ * root directory before it reports the member. Reload-directory fingerprints
2015
+ * must see that same causal path: the new root then explains its ancestor's
2016
+ * immediate-entry delta instead of turning ordinary project data into a cold
2017
+ * selection reload. The deepest matching glob owns the most specific delivery.
2018
+ */
2019
+ function projectInputCompilerMembershipProjectChanges(locations, globs) {
2020
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
2021
+ const roots = globs.map((glob) => identities.resolve(literalGlobRoot(glob)));
2022
+ const changes = new Map();
2023
+ for (const location of locations) {
2024
+ const candidate = identities.resolve(location);
2025
+ const root = roots
2026
+ .filter((entry) => identities.isWithin(entry.path, candidate.path))
2027
+ .sort((left, right) => right.path.length - left.path.length)[0];
2028
+ const change = root ?? candidate;
2029
+ changes.set(change.key, change.path);
2030
+ }
2031
+ return [...changes.values()].sort();
2032
+ }
1693
2033
  function fingerprintProjectInputMatches(matches) {
1694
2034
  const fingerprints = new Map();
1695
2035
  for (const [key, location] of matches) {
1696
- try {
1697
- fingerprints.set(key, isDirectory(location)
1698
- ? fingerprintProjectInputDirectory(location)
1699
- : node_crypto_1.default
1700
- .createHash("sha256")
1701
- .update(node_fs_1.default.readFileSync(location))
1702
- .digest("hex"));
1703
- }
1704
- catch {
1705
- fingerprints.set(key, "");
1706
- }
2036
+ fingerprints.set(key, isDirectory(location)
2037
+ ? fingerprintProjectInputDirectory(location)
2038
+ : fingerprintProjectInputFile(location));
1707
2039
  }
1708
2040
  return fingerprints;
1709
2041
  }
2042
+ function fingerprintProjectInputFile(location) {
2043
+ try {
2044
+ return node_crypto_1.default
2045
+ .createHash("sha256")
2046
+ .update(node_fs_1.default.readFileSync(location))
2047
+ .digest("hex");
2048
+ }
2049
+ catch {
2050
+ return "";
2051
+ }
2052
+ }
1710
2053
  function fingerprintProjectInputDirectory(location) {
1711
- const entries = node_fs_1.default
1712
- .readdirSync(location, { withFileTypes: true })
1713
- .map((entry) => {
1714
- const kind = entry.isDirectory()
1715
- ? "directory"
1716
- : entry.isFile()
1717
- ? "file"
1718
- : entry.isSymbolicLink()
1719
- ? "symlink"
1720
- : "other";
1721
- let target = "";
1722
- if (entry.isSymbolicLink()) {
1723
- try {
1724
- target = node_fs_1.default.readlinkSync(node_path_1.default.join(location, entry.name));
1725
- }
1726
- catch {
1727
- target = "<unreadable>";
2054
+ try {
2055
+ const entries = node_fs_1.default
2056
+ .readdirSync(location, { withFileTypes: true })
2057
+ .map((entry) => {
2058
+ const kind = entry.isDirectory()
2059
+ ? "directory"
2060
+ : entry.isFile()
2061
+ ? "file"
2062
+ : entry.isSymbolicLink()
2063
+ ? "symlink"
2064
+ : "other";
2065
+ let target = "";
2066
+ if (entry.isSymbolicLink()) {
2067
+ try {
2068
+ target = node_fs_1.default.readlinkSync(node_path_1.default.join(location, entry.name));
2069
+ }
2070
+ catch {
2071
+ target = "<unreadable>";
2072
+ }
1728
2073
  }
1729
- }
1730
- return entry.name + "\0" + kind + "\0" + target;
1731
- })
1732
- .sort();
1733
- return node_crypto_1.default.createHash("sha256").update(entries.join("\0")).digest("hex");
2074
+ return entry.name + "\0" + kind + "\0" + target;
2075
+ })
2076
+ .sort();
2077
+ return node_crypto_1.default.createHash("sha256").update(entries.join("\0")).digest("hex");
2078
+ }
2079
+ catch {
2080
+ return "";
2081
+ }
1734
2082
  }
1735
2083
  function matchesProjectInputGlob(pattern, location, identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)()) {
1736
2084
  const root = identities.resolve(literalGlobRoot(pattern));