ttsc 0.21.0 → 0.23.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 (119) 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/go.mod +1 -1
  5. package/internal/lspserver/lsp_document_diagnostics_omits_cached_project_when_current_result_has_none_test.go +48 -0
  6. package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go +98 -0
  7. package/internal/lspserver/lsp_external_watched_change_retains_program_test.go +53 -0
  8. package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +9 -0
  9. package/internal/lspserver/lsp_launcher_reload_directory_fingerprint_parity_windows_test.go +94 -0
  10. package/internal/lspserver/lsp_native_plugin_source.go +171 -55
  11. package/internal/lspserver/lsp_native_resident.go +110 -9
  12. package/internal/lspserver/lsp_plugin.go +6 -0
  13. package/internal/lspserver/lsp_project_context_transport_identity_test.go +43 -0
  14. package/internal/lspserver/lsp_project_diagnostics.go +235 -0
  15. package/internal/lspserver/lsp_project_diagnostics_capability_is_independent_test.go +67 -0
  16. package/internal/lspserver/lsp_project_diagnostics_program_input_overlap_refreshes_all_producers_test.go +78 -0
  17. package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go +65 -0
  18. package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go +90 -0
  19. package/internal/lspserver/lsp_project_diagnostics_refreshes_only_input_owners_test.go +62 -0
  20. package/internal/lspserver/lsp_project_diagnostics_resident_unsupported_falls_back_test.go +123 -0
  21. package/internal/lspserver/lsp_project_diagnostics_resume_preserves_generation_test.go +52 -0
  22. package/internal/lspserver/lsp_project_diagnostics_translates_relative_client_project_test.go +90 -0
  23. package/internal/lspserver/lsp_project_input_glob_path_other.go +16 -0
  24. package/internal/lspserver/lsp_project_input_glob_path_windows.go +36 -0
  25. package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
  26. package/internal/lspserver/lsp_project_input_path_key_other.go +17 -0
  27. package/internal/lspserver/lsp_project_input_path_key_windows.go +228 -0
  28. package/internal/lspserver/lsp_project_input_path_key_windows_test.go +460 -0
  29. package/internal/lspserver/lsp_project_input_registration.go +504 -0
  30. package/internal/lspserver/lsp_project_input_registration_rechecks_selection_baseline_test.go +71 -0
  31. package/internal/lspserver/lsp_project_input_registration_reconciles_rejected_pending_request_test.go +268 -0
  32. package/internal/lspserver/lsp_project_input_registration_shapes_patterns_test.go +94 -0
  33. package/internal/lspserver/lsp_project_input_watchers_anchor_missing_paths_test.go +48 -0
  34. package/internal/lspserver/lsp_project_inputs.go +1012 -0
  35. package/internal/lspserver/lsp_project_inputs_match_missing_exact_and_glob_test.go +129 -0
  36. package/internal/lspserver/lsp_project_inputs_reject_invalid_and_foreign_snapshots_test.go +110 -0
  37. package/internal/lspserver/lsp_proxy.go +591 -28
  38. package/internal/lspserver/lsp_reload_directories_compare_immediate_topology_test.go +188 -0
  39. package/internal/lspserver/lsp_reload_directories_separate_glob_territory_test.go +181 -0
  40. package/internal/lspserver/lsp_reload_file_fingerprint_preserves_raw_symlink_target_test.go +47 -0
  41. package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +1 -1
  42. package/internal/lspserver/lsp_resident_invalidation_broadcasts_program_input_overlap_test.go +60 -0
  43. package/internal/lspserver/lsp_resident_invalidation_targets_project_input_owners_test.go +51 -0
  44. package/internal/lspserver/lsp_same_transport_entries_are_deduplicated_test.go +66 -0
  45. package/lib/compiler/internal/project/readProjectConfig.d.ts +5 -3
  46. package/lib/compiler/internal/project/readProjectConfig.js +46 -14
  47. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  48. package/lib/compiler/internal/residentCheckProcess.d.ts +46 -0
  49. package/lib/compiler/internal/residentCheckProcess.js +227 -0
  50. package/lib/compiler/internal/residentCheckProcess.js.map +1 -0
  51. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -1
  52. package/lib/compiler/internal/resolveEmittedJavaScript.js +35 -18
  53. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  54. package/lib/compiler/internal/runBuild.d.ts +64 -1
  55. package/lib/compiler/internal/runBuild.js +484 -7
  56. package/lib/compiler/internal/runBuild.js.map +1 -1
  57. package/lib/compiler/internal/runSingleFileEmit.js +1 -0
  58. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  59. package/lib/flags/parser.js +13 -17
  60. package/lib/flags/parser.js.map +1 -1
  61. package/lib/flags/schema.d.ts +10 -3
  62. package/lib/flags/schema.js +99 -0
  63. package/lib/flags/schema.js.map +1 -1
  64. package/lib/internal/projectInputPathIdentity.d.ts +31 -0
  65. package/lib/internal/projectInputPathIdentity.js +276 -0
  66. package/lib/internal/projectInputPathIdentity.js.map +1 -0
  67. package/lib/internal/windowsDirectoryCaseSensitivity.d.ts +10 -0
  68. package/lib/internal/windowsDirectoryCaseSensitivity.js +34 -0
  69. package/lib/internal/windowsDirectoryCaseSensitivity.js.map +1 -0
  70. package/lib/launcher/internal/runTtsc.d.ts +17 -0
  71. package/lib/launcher/internal/runTtsc.js +126 -88
  72. package/lib/launcher/internal/runTtsc.js.map +1 -1
  73. package/lib/launcher/internal/runTtscserver.d.ts +13 -1
  74. package/lib/launcher/internal/runTtscserver.js +468 -47
  75. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  76. package/lib/launcher/internal/runTtsx.js +61 -7
  77. package/lib/launcher/internal/runTtsx.js.map +1 -1
  78. package/lib/launcher/internal/runtimeHooks.d.ts +6 -5
  79. package/lib/launcher/internal/runtimeHooks.js +7 -15
  80. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  81. package/lib/launcher/internal/singleFileOutput.d.ts +16 -0
  82. package/lib/launcher/internal/singleFileOutput.js +103 -0
  83. package/lib/launcher/internal/singleFileOutput.js.map +1 -0
  84. package/lib/launcher/internal/watchTopology.d.ts +288 -2
  85. package/lib/launcher/internal/watchTopology.js +1921 -54
  86. package/lib/launcher/internal/watchTopology.js.map +1 -1
  87. package/lib/plugin/internal/descriptorProcessFailure.d.ts +32 -0
  88. package/lib/plugin/internal/descriptorProcessFailure.js +73 -0
  89. package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -0
  90. package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
  91. package/lib/plugin/internal/loadProjectPlugins.js +34 -8
  92. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  93. package/lib/structures/ITtscPluginCapabilities.d.ts +31 -0
  94. package/lib/structures/internal/ITtscProjectInputSnapshot.d.ts +29 -0
  95. package/lib/structures/internal/ITtscProjectInputSnapshot.js +3 -0
  96. package/lib/structures/internal/ITtscProjectInputSnapshot.js.map +1 -0
  97. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -0
  98. package/package.json +12 -8
  99. package/src/compiler/internal/project/readProjectConfig.ts +62 -8
  100. package/src/compiler/internal/residentCheckProcess.ts +279 -0
  101. package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -21
  102. package/src/compiler/internal/runBuild.ts +728 -12
  103. package/src/compiler/internal/runSingleFileEmit.ts +1 -0
  104. package/src/flags/parser.ts +17 -17
  105. package/src/flags/schema.ts +112 -3
  106. package/src/internal/projectInputPathIdentity.ts +377 -0
  107. package/src/internal/windowsDirectoryCaseSensitivity.ts +33 -0
  108. package/src/launcher/internal/runTtsc.ts +148 -117
  109. package/src/launcher/internal/runTtscserver.ts +633 -56
  110. package/src/launcher/internal/runTtsx.ts +87 -10
  111. package/src/launcher/internal/runtimeHooks.ts +16 -16
  112. package/src/launcher/internal/singleFileOutput.ts +133 -0
  113. package/src/launcher/internal/watchTopology.ts +2494 -74
  114. package/src/plugin/internal/descriptorProcessFailure.ts +100 -0
  115. package/src/plugin/internal/loadProjectPlugins.ts +49 -8
  116. package/src/structures/ITtscPluginCapabilities.ts +34 -0
  117. package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
  118. package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
  119. package/tools/shim_audit/baseline.json +0 -9
@@ -4,12 +4,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.WatchTopology = void 0;
7
+ exports.reloadInputsForFailedTopologyRefresh = reloadInputsForFailedTopologyRefresh;
8
+ exports.syncWatchers = syncWatchers;
9
+ exports.literalGlobRoot = literalGlobRoot;
10
+ exports.projectInputWatchDirectories = projectInputWatchDirectories;
11
+ exports.projectInputActiveWatchDirectories = projectInputActiveWatchDirectories;
12
+ exports.projectInputAvailableWatchDirectory = projectInputAvailableWatchDirectory;
13
+ exports.projectInputReplacementStrandsWatchers = projectInputReplacementStrandsWatchers;
14
+ exports.projectInputTopologyMayAffect = projectInputTopologyMayAffect;
15
+ exports.projectInputEventShouldNotify = projectInputEventShouldNotify;
16
+ exports.projectInputReloadEventShouldNotify = projectInputReloadEventShouldNotify;
17
+ exports.projectInputMembershipInvalidatesProgram = projectInputMembershipInvalidatesProgram;
18
+ exports.planCompilerDirectoryWatchEvent = planCompilerDirectoryWatchEvent;
19
+ const node_crypto_1 = __importDefault(require("node:crypto"));
7
20
  const node_fs_1 = __importDefault(require("node:fs"));
8
21
  const node_path_1 = __importDefault(require("node:path"));
9
22
  const readConfigJson_1 = require("../../compiler/internal/project/readConfigJson");
10
23
  const readProjectConfig_1 = require("../../compiler/internal/project/readProjectConfig");
11
24
  const resolveTsgo_1 = require("../../compiler/internal/resolveTsgo");
12
25
  const spawnNative_1 = require("../../compiler/internal/spawnNative");
26
+ const schema_1 = require("../../flags/schema");
27
+ const projectInputPathIdentity_1 = require("../../internal/projectInputPathIdentity");
28
+ const singleFileOutput_1 = require("./singleFileOutput");
13
29
  /**
14
30
  * Keeps the launcher watch set aligned with the compiler's current program.
15
31
  *
@@ -21,34 +37,180 @@ const spawnNative_1 = require("../../compiler/internal/spawnNative");
21
37
  class WatchTopology {
22
38
  options;
23
39
  callbacks;
40
+ analysisOnly = false;
41
+ closed = false;
42
+ compilerPostRegistrationMembershipRefresh = false;
43
+ compilerPostRegistrationReconciliationScheduled = false;
44
+ compilerPostRegistrationSkipUnobservedProjectInputWatchRoots = true;
24
45
  directories = new Map();
25
46
  directoryWatchers = new Map();
26
47
  extraInputs = [];
27
48
  extraWatchers = new Map();
49
+ compilerFileSnapshots = new Map();
28
50
  files = new Map();
29
51
  fileWatchers = new Map();
30
52
  observedDirectories = new Map();
53
+ outputFiles = new Map();
31
54
  outputs = new Map();
55
+ projectInputFingerprints = new Map();
56
+ projectInputMatches = new Map();
57
+ projectInputs = {
58
+ files: [],
59
+ globs: [],
60
+ reloadDirectories: [],
61
+ reloadFiles: [],
62
+ root: "",
63
+ };
64
+ declaredProjectInputs = {
65
+ files: [],
66
+ globs: [],
67
+ reloadDirectories: [],
68
+ reloadFiles: [],
69
+ root: "",
70
+ };
71
+ projectInputRecoveryScheduled = false;
72
+ projectInputPostRegistrationReconciliationScheduled = false;
73
+ projectInputRejectedWatchRoots = new Set();
74
+ projectInputRequiredWatchRoots = new Map();
75
+ projectInputUnobservedWatchRoots = new Map();
76
+ projectInputWatchRoots = new Map();
77
+ projectInputWatchers = new Map();
78
+ projectInputLinkWatchers = new Map();
79
+ projectInputCompilerOutputOverlaps = new WeakMap();
80
+ projectInputCompilerAcknowledgements = new Map();
81
+ reloadFiles = new Map();
32
82
  constructor(options, callbacks) {
33
83
  this.options = options;
34
84
  this.callbacks = callbacks;
35
85
  }
36
86
  /** Re-resolve compiler inputs and notify only when their membership changed. */
37
87
  refresh(notify) {
88
+ this.refreshCompilerInputs(notify, false);
89
+ }
90
+ refreshCompilerInputs(notify, skipUnobservedProjectInputWatchRoots) {
38
91
  const next = resolveWatchTopology(this.options, this.extraInputs);
39
- const changed = mapsEqual(this.files, next.files) === false ||
92
+ const compilerProgramMembershipChange = next.analysisOnly &&
93
+ mapsEqual(this.reloadFiles, next.reloadFiles) &&
94
+ mapsEqual(this.outputFiles, next.outputFiles) &&
95
+ mapsEqual(this.outputs, next.outputs)
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
103
+ : undefined;
104
+ const changed = this.analysisOnly !== next.analysisOnly ||
105
+ mapsEqual(this.files, next.files) === false ||
40
106
  mapsEqual(this.directories, next.directories) === false ||
41
- mapsEqual(this.outputs, next.outputs) === false;
107
+ mapsEqual(this.outputFiles, next.outputFiles) === false ||
108
+ mapsEqual(this.outputs, next.outputs) === false ||
109
+ mapsEqual(this.reloadFiles, next.reloadFiles) === false;
110
+ this.analysisOnly = next.analysisOnly;
42
111
  this.files = next.files;
112
+ // Stamp the tracked set as it is resolved, so the first event that cannot
113
+ // name what changed compares against the state the compiler just saw rather
114
+ // than against nothing, which would make it nominate everything once.
115
+ for (const key of [...this.compilerFileSnapshots.keys()]) {
116
+ if (!next.files.has(key))
117
+ this.compilerFileSnapshots.delete(key);
118
+ }
119
+ for (const [key, file] of next.files) {
120
+ // Only a file with no stamp yet is seeded. Restamping one that already
121
+ // has a baseline would advance it past a change nobody reported, and the
122
+ // next unnamed event would then read that change as no change at all.
123
+ if (!this.compilerFileSnapshots.has(key)) {
124
+ this.compilerFileSnapshots.set(key, compilerFileSnapshot(file));
125
+ }
126
+ }
43
127
  this.directories = next.directories;
128
+ this.outputFiles = next.outputFiles;
44
129
  this.outputs = next.outputs;
45
- this.syncFileWatchers();
46
- this.syncDirectoryWatchers();
130
+ this.reloadFiles = next.reloadFiles;
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();
47
141
  this.syncExtraWatchers();
142
+ this.syncProjectInputWatchers(skipUnobservedProjectInputWatchRoots);
143
+ if (fileWatcherRegistered || directoryWatcherRegistered) {
144
+ this.scheduleCompilerPostRegistrationReconciliation(directoryWatcherRegistered, skipUnobservedProjectInputWatchRoots);
145
+ }
48
146
  if (notify && changed) {
49
- this.callbacks.onTopologyChange();
147
+ if (projectInputProgramChange !== undefined) {
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
+ });
158
+ }
159
+ else {
160
+ this.callbacks.onTopologyChange();
161
+ }
50
162
  }
51
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
+ }
52
214
  /** Add Go plugin source trees discovered by the real build lane. */
53
215
  setExtraInputs(inputs) {
54
216
  const next = uniqueExistingPaths(inputs);
@@ -57,41 +219,249 @@ class WatchTopology {
57
219
  this.extraInputs = next;
58
220
  this.refresh(false);
59
221
  }
222
+ /**
223
+ * Reconcile project-rule dependencies, retaining absent files and empty glob
224
+ * populations as live topology.
225
+ */
226
+ setProjectInputs(inputs) {
227
+ const next = normalizeProjectInputSnapshot(inputs);
228
+ // The declared spellings are recorded even when the normalized snapshot did
229
+ // not move, because a republication can carry a new alias for identities
230
+ // that already matched, and anchoring the spelling that was retired would
231
+ // leave the live one unwatched.
232
+ this.declaredProjectInputs =
233
+ inputs.declared === undefined
234
+ ? inputs
235
+ : { ...inputs.declared, root: inputs.root };
236
+ if (projectInputSnapshotsEqual(this.projectInputs, next)) {
237
+ this.syncProjectInputWatchers();
238
+ return;
239
+ }
240
+ this.projectInputs = next;
241
+ this.projectInputRejectedWatchRoots.clear();
242
+ this.pruneProjectInputWatchRoots([
243
+ next,
244
+ inputs,
245
+ { ...(inputs.declared ?? inputs), root: inputs.root },
246
+ ]);
247
+ this.projectInputMatches = this.collectProjectInputMatches();
248
+ this.projectInputFingerprints = fingerprintProjectInputMatches(this.projectInputMatches);
249
+ this.syncProjectInputWatchers();
250
+ }
60
251
  /** Close every watcher so SIGINT/SIGTERM can drain the event loop. */
61
252
  close() {
253
+ this.closed = true;
62
254
  closeWatchers(this.fileWatchers);
63
255
  closeWatchers(this.directoryWatchers);
64
256
  closeWatchers(this.extraWatchers);
257
+ closeWatchers(this.projectInputWatchers);
258
+ closeWatchers(this.projectInputLinkWatchers);
259
+ }
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;
267
+ syncWatchers(this.fileWatchers, files, (location) => node_fs_1.default.watch(watcherRegistrationPath(location), { persistent: true }, () => {
268
+ // A per-file watcher fires on any filesystem attention its target
269
+ // receives, and it carries no filename to distinguish an edit from
270
+ // a touch. It answers the same question the unnamed directory event
271
+ // answers, so it answers it the same way: from the bytes.
272
+ const movement = this.compilerFileMovement(location);
273
+ if (movement.owner)
274
+ this.rearmFileWatchers([location], true);
275
+ if (!movement.content)
276
+ return;
277
+ this.callbacks.onInputChange({
278
+ kind: this.classifyCompilerInput(location),
279
+ path: location,
280
+ });
281
+ }), (location, error) => this.callbacks.onError(location, error), () => this.closed === false);
282
+ return [...this.fileWatchers].some(([key, watcher]) => previous.get(key) !== watcher);
65
283
  }
66
- syncFileWatchers() {
67
- syncWatchers(this.fileWatchers, this.files, (location) => node_fs_1.default.watch(location, { persistent: true }, () => {
68
- this.callbacks.onInputChange();
69
- }), (location, error) => this.callbacks.onError(location, error));
284
+ /** Compare a tracked file's content and physical owner with its snapshot. */
285
+ compilerFileMovement(location) {
286
+ const key = pathKey(location);
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
+ };
70
294
  }
71
295
  syncDirectoryWatchers() {
296
+ const previous = new Map(this.directoryWatchers);
72
297
  const desired = new Map(this.directories);
73
298
  for (const [key, location] of this.observedDirectories) {
74
299
  if (isDirectory(location) === false ||
75
- this.isCompilerOutputDirectory(location)) {
300
+ this.isCompilerOutputDirectory(location) ||
301
+ this.isProjectInputDirectory(location)) {
76
302
  this.observedDirectories.delete(key);
77
303
  continue;
78
304
  }
79
305
  desired.set(key, location);
80
306
  }
81
- syncWatchers(this.directoryWatchers, desired, (location) => node_fs_1.default.watch(location, { persistent: true }, (_event, filename) => {
307
+ if (process.platform === "win32") {
308
+ for (const [key, location] of desired) {
309
+ if ([...desired].some(([candidateKey, candidate]) => candidateKey !== key && isPathWithin(candidate, location))) {
310
+ desired.delete(key);
311
+ }
312
+ }
313
+ }
314
+ syncWatchers(this.directoryWatchers, desired, (location) => node_fs_1.default.watch(watcherRegistrationPath(location), {
315
+ persistent: true,
316
+ recursive: process.platform === "win32",
317
+ }, (event, filename) => {
82
318
  const changed = filename === null
83
319
  ? undefined
84
320
  : node_path_1.default.resolve(location, filename.toString());
85
- // File watchers own ordinary source/config edits. Directory watchers
86
- // only reconcile membership, so an emit in an unrelated output folder
87
- // cannot schedule another build.
88
- if (changed !== undefined &&
89
- this.files.has(pathKey(changed)) &&
90
- node_fs_1.default.existsSync(changed)) {
321
+ const pluginInput = changed ?? location;
322
+ if (this.isPluginInput(pluginInput)) {
323
+ this.callbacks.onInputChange({
324
+ kind: "plugin",
325
+ path: pluginInput,
326
+ });
91
327
  return;
92
328
  }
93
- this.refreshFromDirectory(location, changed);
94
- }), (location, error) => this.callbacks.onError(location, error));
329
+ const plan = planCompilerDirectoryWatchEvent({
330
+ changed,
331
+ event,
332
+ exists: node_fs_1.default.existsSync,
333
+ location,
334
+ platform: process.platform,
335
+ trackedFiles: this.files,
336
+ });
337
+ this.rearmFileWatchers(plan.rearm);
338
+ for (const file of this.compilerChangesToReport(plan.changes, changed, event)) {
339
+ this.callbacks.onInputChange({
340
+ kind: this.classifyCompilerInput(file),
341
+ path: file,
342
+ });
343
+ }
344
+ if (plan.refresh)
345
+ this.refreshFromDirectory(location, changed);
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
+ });
419
+ }
420
+ /**
421
+ * Narrow a plan's changes to the tracked files that actually moved.
422
+ *
423
+ * A backend that cannot name what changed forces the plan to nominate every
424
+ * tracked file under the watched directory, which is the only safe answer it
425
+ * can give from an event carrying no filename. macOS delivers such events for
426
+ * ordinary activity elsewhere in the project, so the compiler lane would wake
427
+ * for sources nobody touched. Only a content notification passes through: it
428
+ * is the one event that claims the bytes moved. A rename claims the directory
429
+ * entry was rewritten and an unnamed event claims nothing, so both are
430
+ * decided from the bytes, which is the question neither of them answered.
431
+ */
432
+ compilerChangesToReport(changes, changed, event) {
433
+ // A content notification is taken at its word: the backend is telling us
434
+ // these bytes changed, and second-guessing it would lose an edit that
435
+ // landed inside the clock's resolution. A rename says the directory entry
436
+ // was rewritten, which is a different claim — a file can be moved back, or
437
+ // replaced by an identical copy, without its content moving at all — and an
438
+ // event that cannot name anything makes no claim about content either.
439
+ // Those two are decided from the bytes, and the rearm they drive is
440
+ // unaffected, because rebinding is about the inode and not the content.
441
+ if (changed !== undefined && event !== "rename") {
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
+ });
450
+ }
451
+ return changes.filter((file) => this.compilerFileMovement(file).content);
452
+ }
453
+ recordCompilerFileSnapshot(file) {
454
+ this.compilerFileSnapshots.set(pathKey(file), compilerFileSnapshot(file));
455
+ }
456
+ rearmFileWatchers(files, skipMissing = false) {
457
+ for (const file of files) {
458
+ const key = pathKey(file);
459
+ this.fileWatchers.get(key)?.close();
460
+ this.fileWatchers.delete(key);
461
+ }
462
+ if (this.syncFileWatchers(skipMissing)) {
463
+ this.scheduleCompilerPostRegistrationReconciliation(false, true);
464
+ }
95
465
  }
96
466
  syncExtraWatchers() {
97
467
  const directories = new Map();
@@ -100,31 +470,592 @@ class WatchTopology {
100
470
  directories.set(pathKey(directory), directory);
101
471
  }
102
472
  }
103
- syncWatchers(this.extraWatchers, directories, (location) => node_fs_1.default.watch(location, { persistent: true }, () => {
104
- this.callbacks.onInputChange();
105
- }), (location, error) => this.callbacks.onError(location, error));
473
+ syncWatchers(this.extraWatchers, directories, (location) => node_fs_1.default.watch(watcherRegistrationPath(location), { persistent: true }, (_event, filename) => {
474
+ const changed = filename === null
475
+ ? undefined
476
+ : node_path_1.default.resolve(location, filename.toString());
477
+ this.callbacks.onInputChange({
478
+ kind: "plugin",
479
+ path: changed ?? location,
480
+ });
481
+ }), (location, error) => this.callbacks.onError(location, error), () => this.closed === false);
482
+ }
483
+ syncProjectInputWatchers(skipUnobservedProjectInputWatchRoots = false) {
484
+ if (this.closed)
485
+ return;
486
+ const previous = new Map(this.projectInputWatchers);
487
+ const previousLinks = new Map(this.projectInputLinkWatchers);
488
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
489
+ const desired = new Map();
490
+ const required = new Map();
491
+ for (const file of this.projectInputDeclarations("file")) {
492
+ if (this.isProjectInputCompilerOutput(file, identities))
493
+ continue;
494
+ const location = this.projectInputWatchRoot("file", file, node_path_1.default.dirname(file));
495
+ this.retainProjectInputWatchRoot(required, desired, identities, location, node_path_1.default.dirname(file), skipUnobservedProjectInputWatchRoots);
496
+ }
497
+ for (const glob of this.projectInputDeclarations("glob")) {
498
+ const root = literalGlobRoot(glob);
499
+ if (this.isProjectInputCompilerOutputDirectory(root, identities)) {
500
+ continue;
501
+ }
502
+ const location = this.projectInputWatchRoot("glob", glob, root);
503
+ this.retainProjectInputWatchRoot(required, desired, identities, location, root, skipUnobservedProjectInputWatchRoots);
504
+ }
505
+ for (const file of this.projectInputDeclarations("reload")) {
506
+ if (this.isProjectInputCompilerOutput(file, identities))
507
+ continue;
508
+ const location = this.projectInputWatchRoot("reload", file, node_path_1.default.dirname(file));
509
+ this.retainProjectInputWatchRoot(required, desired, identities, location, node_path_1.default.dirname(file), skipUnobservedProjectInputWatchRoots);
510
+ }
511
+ for (const directory of this.projectInputDeclarations("reload-directory")) {
512
+ if (this.isProjectInputCompilerOutputDirectory(directory, identities)) {
513
+ continue;
514
+ }
515
+ const location = this.projectInputWatchRoot("reload-directory", directory, directory);
516
+ this.retainProjectInputWatchRoot(required, desired, identities, location, directory, skipUnobservedProjectInputWatchRoots);
517
+ }
518
+ const active = new Map();
519
+ for (const location of projectInputActiveWatchDirectories(desired.values(), identities)) {
520
+ const identity = identities.resolve(location);
521
+ active.set(identity.key, identity.path);
522
+ }
523
+ this.projectInputRequiredWatchRoots = required;
524
+ syncWatchers(this.projectInputWatchers, active, (location) => node_fs_1.default.watch(watcherRegistrationPath(location), { persistent: true, recursive: true }, (_event, filename) => {
525
+ const changed = filename === null
526
+ ? undefined
527
+ : node_path_1.default.resolve(location, filename.toString());
528
+ this.refreshProjectInputs(location, changed);
529
+ }), (location, error) => {
530
+ const key = identities.resolve(location).key;
531
+ const firstFailure = !this.projectInputRejectedWatchRoots.has(key);
532
+ this.projectInputRejectedWatchRoots.add(key);
533
+ this.callbacks.onError(location, error);
534
+ if (firstFailure && !this.closed) {
535
+ this.scheduleProjectInputWatcherRecovery();
536
+ }
537
+ }, () => this.closed === false);
538
+ if (this.closed)
539
+ return;
540
+ if (!this.projectInputRecoveryScheduled) {
541
+ this.reportUnobservedProjectInputWatchRoots(required);
542
+ }
543
+ this.syncProjectInputLinkWatchers(identities);
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());
552
+ }
553
+ /**
554
+ * Watch the directory that holds a declaration which is itself a link.
555
+ *
556
+ * A recursive watcher cannot report the link being replaced. The backend that
557
+ * keys its handles by path skips an entry it already knows, and the handle it
558
+ * put on the entry followed the link to the target's inode, which unlinking
559
+ * and recreating the link never touches. A plain directory watch has neither
560
+ * property: it reports the entry by name the moment it moves. These are kept
561
+ * apart from the recursive roots because they are not roots — they observe
562
+ * one directory, they are never reported as watch roots, and an ancestor
563
+ * covering them does not make them redundant.
564
+ */
565
+ syncProjectInputLinkWatchers(identities) {
566
+ const desired = new Map();
567
+ for (const kind of ["file", "reload"]) {
568
+ for (const declaration of this.projectInputDeclarations(kind)) {
569
+ const declared = node_path_1.default.resolve(declaration);
570
+ // The test is whether the declaration is itself a link, not whether its
571
+ // spelling is canonical. Comparing against the resolved identity would
572
+ // admit every declaration whose ancestor is aliased — which on macOS is
573
+ // every declaration under the system temporary directory — and it would
574
+ // still miss the retarget, because the watcher goes below the link.
575
+ if (!isSymbolicLink(declared))
576
+ continue;
577
+ if (this.isProjectInputCompilerOutput(declared, identities))
578
+ continue;
579
+ const parent = nearestExistingDirectory(node_path_1.default.dirname(declared));
580
+ if (parent === undefined)
581
+ continue;
582
+ desired.set(identities.resolve(parent).key, parent);
583
+ }
584
+ }
585
+ syncWatchers(this.projectInputLinkWatchers, desired, (location) => node_fs_1.default.watch(watcherRegistrationPath(location), { persistent: true }, (_event, filename) => {
586
+ const changed = filename === null
587
+ ? undefined
588
+ : node_path_1.default.resolve(location, filename.toString());
589
+ this.refreshProjectInputs(location, changed);
590
+ }), (location, error) => this.callbacks.onError(location, error), () => this.closed === false);
591
+ }
592
+ /** Drop the watcher that just reported a directory replacement. */
593
+ retireProjectInputWatcher(location, identities) {
594
+ const key = identities.resolve(location).key;
595
+ // A plain directory watcher binds an inode, so a replacement strands it
596
+ // exactly as it strands a recursive root. Both maps are keyed the same way,
597
+ // so both are retired together and the next sync reinstalls whichever the
598
+ // declarations still call for.
599
+ for (const watchers of [
600
+ this.projectInputWatchers,
601
+ this.projectInputLinkWatchers,
602
+ ]) {
603
+ const watcher = watchers.get(key);
604
+ if (watcher === undefined)
605
+ continue;
606
+ watcher.close();
607
+ watchers.delete(key);
608
+ }
609
+ }
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
+ }
625
+ const available = projectInputAvailableWatchDirectory(location, this.projectInputRejectedWatchRoots, identities, this.projectInputs.root);
626
+ if (available === undefined)
627
+ return;
628
+ const identity = identities.resolve(available);
629
+ desired.set(identity.key, identity.path);
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
+ }
731
+ /**
732
+ * One snapshot holding every spelling of every declaration.
733
+ *
734
+ * Consumers that decide from a population rather than from a single path have
735
+ * to see both, or half of them answer from the file a link pointed at when
736
+ * the snapshot was published while the event they are judging resolved to the
737
+ * file it points at now.
738
+ */
739
+ projectInputPopulation() {
740
+ return {
741
+ files: this.projectInputDeclarations("file"),
742
+ globs: this.projectInputDeclarations("glob"),
743
+ reloadDirectories: this.projectInputDeclarations("reload-directory"),
744
+ reloadFiles: this.projectInputDeclarations("reload"),
745
+ root: this.projectInputs.root,
746
+ };
747
+ }
748
+ /**
749
+ * Every spelling of one declaration that has to be anchored separately.
750
+ *
751
+ * The retained snapshot is normalized to physical identities, which is what
752
+ * every comparison needs but not what every watcher needs: a declaration
753
+ * reached through a symlink resolves to its target's directory, so anchoring
754
+ * the normalized form alone watches the bytes and never the link. Retargeting
755
+ * or replacing the link then goes unobserved, even though it is exactly what
756
+ * decides which bytes the declaration names next. Both spellings are planned
757
+ * through the same root selection, so the project-root hoist and the
758
+ * nearest-existing-ancestor boundary still bound each of them, and the active
759
+ * set drops one again whenever they coincide or share an ancestor.
760
+ */
761
+ projectInputDeclarations(kind) {
762
+ const select = (snapshot) => kind === "file"
763
+ ? snapshot.files
764
+ : kind === "glob"
765
+ ? snapshot.globs
766
+ : kind === "reload"
767
+ ? (snapshot.reloadFiles ?? [])
768
+ : (snapshot.reloadDirectories ?? []);
769
+ const seen = new Set();
770
+ const declarations = [];
771
+ for (const entry of [
772
+ ...select(this.projectInputs),
773
+ ...select(this.declaredProjectInputs),
774
+ ]) {
775
+ const key = (0, projectInputPathIdentity_1.resolveProjectInputPath)(entry);
776
+ if (seen.has(key))
777
+ continue;
778
+ seen.add(key);
779
+ declarations.push(entry);
780
+ }
781
+ return declarations;
782
+ }
783
+ projectInputWatchRoot(kind, declaration, target) {
784
+ const key = projectInputDeclarationKey(kind, declaration);
785
+ const retained = this.projectInputWatchRoots.get(key);
786
+ if (retained !== undefined && isDirectory(retained))
787
+ return retained;
788
+ const resolved = projectInputRecursiveWatchRoot(target, this.projectInputs.root);
789
+ if (resolved !== undefined)
790
+ this.projectInputWatchRoots.set(key, resolved);
791
+ return resolved;
792
+ }
793
+ refreshProjectInputs(location, changed, skipUnobservedProjectInputWatchRoots = false) {
794
+ try {
795
+ const previous = this.projectInputMatches;
796
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
797
+ // One population for the whole decision. Every question below is asked of
798
+ // the same declarations, and rebuilding it per question would both cost
799
+ // more and let two answers disagree about what was declared.
800
+ const population = this.projectInputPopulation();
801
+ const directlyMatched = changed !== undefined &&
802
+ (previous.has(identities.resolve(changed).key) ||
803
+ matchesProjectInput(population, changed, identities));
804
+ const topologyMatched = changed !== undefined &&
805
+ projectInputTopologyMayAffect(population, changed, previous, identities);
806
+ if (changed !== undefined &&
807
+ (this.isProjectInputCompilerOutput(changed, identities) ||
808
+ (directlyMatched === false && topologyMatched === false))) {
809
+ return;
810
+ }
811
+ // Rearm before snapshotting. A watcher that has to be replaced stops
812
+ // delivering the moment it is closed, so a scan taken first would become
813
+ // the baseline for a window in which nothing was watched, and anything
814
+ // written there would never be announced again. Reinstalling first makes
815
+ // the scan below observe whatever the gap swallowed.
816
+ if (changed !== undefined &&
817
+ projectInputReplacementStrandsWatchers(population, changed, identities)) {
818
+ this.retireProjectInputWatcher(location, identities);
819
+ this.syncProjectInputWatchers();
820
+ }
821
+ const next = this.collectProjectInputMatches();
822
+ const membershipChanged = mapsEqual(previous, next) === false;
823
+ const nextFingerprints = changed === undefined ||
824
+ membershipChanged ||
825
+ directlyMatched ||
826
+ topologyMatched
827
+ ? fingerprintProjectInputMatches(next)
828
+ : this.projectInputFingerprints;
829
+ const contentChanged = mapsEqual(this.projectInputFingerprints, nextFingerprints) === false;
830
+ const changedInputs = projectInputChangedPaths({
831
+ next,
832
+ nextFingerprints,
833
+ previous,
834
+ previousFingerprints: this.projectInputFingerprints,
835
+ });
836
+ const reconciledChange = changed ?? (changedInputs.length === 1 ? changedInputs[0] : undefined);
837
+ // Both spellings classify the event. The normalized form names the file a
838
+ // link pointed at when the snapshot was published, so after a retarget it
839
+ // names the wrong one; only the declared form resolves to what the link
840
+ // points at now, which is the selection this lane exists to protect.
841
+ const reload = projectInputReloadEventShouldNotify({
842
+ changed: reconciledChange,
843
+ changedInputs,
844
+ globs: population.globs,
845
+ reloadDirectories: population.reloadDirectories ?? [],
846
+ reloadFiles: population.reloadFiles ?? [],
847
+ });
848
+ const invalidate = projectInputMembershipInvalidatesProgram({
849
+ changed: reconciledChange,
850
+ changedInputs,
851
+ contentChanged,
852
+ next,
853
+ previous,
854
+ });
855
+ this.projectInputMatches = next;
856
+ this.projectInputFingerprints = nextFingerprints;
857
+ this.syncProjectInputWatchers(skipUnobservedProjectInputWatchRoots);
858
+ // A JSON/TS/JS project-input member can simultaneously enter or leave
859
+ // the compiler Program. Reconcile the compiler watch snapshot before
860
+ // scheduling its resident invalidation, so runWatch's post-cycle refresh
861
+ // does not rediscover the same delta as a broader execution reload.
862
+ if (invalidate) {
863
+ this.refreshCompilerInputs(false, skipUnobservedProjectInputWatchRoots);
864
+ }
865
+ if (projectInputEventShouldNotify({
866
+ contentChanged,
867
+ directlyMatched,
868
+ membershipChanged,
869
+ }) &&
870
+ (reconciledChange === undefined ||
871
+ this.isProjectInputCompilerOutput(reconciledChange, identities) ===
872
+ false)) {
873
+ this.callbacks.onInputChange(reload
874
+ ? { kind: "config", path: reconciledChange }
875
+ : {
876
+ ...(invalidate ? { invalidate: true } : {}),
877
+ kind: "project",
878
+ path: reconciledChange,
879
+ });
880
+ }
881
+ }
882
+ catch (error) {
883
+ // A rename can invalidate the old filesystem object before the
884
+ // replacement is readable. Rebind ancestor ownership even when the
885
+ // population scan races that transient gap, so a later create cannot be
886
+ // stranded without a watcher.
887
+ this.syncProjectInputWatchers(skipUnobservedProjectInputWatchRoots);
888
+ this.callbacks.onError(location, error);
889
+ }
890
+ }
891
+ collectProjectInputMatches() {
892
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
893
+ const matches = new Map();
894
+ // Both spellings are scanned, and each is resolved here rather than when
895
+ // the snapshot arrived. A declaration reached through a symlink otherwise
896
+ // keeps the identity it had when it was published, so retargeting the link
897
+ // moves no key, changes no fingerprint, and the cycle never learns that the
898
+ // bytes it depends on are now a different file.
899
+ for (const file of this.projectInputDeclarations("file")) {
900
+ if (node_fs_1.default.existsSync(file) &&
901
+ this.isProjectInputCompilerOutput(file, identities) === false) {
902
+ const identity = identities.resolve(file);
903
+ matches.set(identity.key, identity.path);
904
+ }
905
+ }
906
+ for (const file of this.projectInputDeclarations("reload")) {
907
+ if (node_fs_1.default.existsSync(file) &&
908
+ this.isProjectInputCompilerOutput(file, identities) === false) {
909
+ const identity = identities.resolve(file);
910
+ matches.set(identity.key, identity.path);
911
+ }
912
+ }
913
+ for (const directory of this.projectInputDeclarations("reload-directory")) {
914
+ if (isDirectory(directory) &&
915
+ this.isProjectInputCompilerOutputDirectory(directory, identities) ===
916
+ false) {
917
+ const identity = identities.resolve(directory);
918
+ matches.set(identity.key, identity.path);
919
+ }
920
+ }
921
+ for (const glob of this.projectInputDeclarations("glob")) {
922
+ const root = literalGlobRoot(glob);
923
+ if (isDirectory(root) === false ||
924
+ this.isProjectInputCompilerOutputDirectory(root, identities)) {
925
+ continue;
926
+ }
927
+ const stack = [root];
928
+ while (stack.length !== 0) {
929
+ const current = stack.pop();
930
+ let entries;
931
+ try {
932
+ entries = node_fs_1.default.readdirSync(current, { withFileTypes: true });
933
+ }
934
+ catch (error) {
935
+ if (isVanishedFilesystemEntry(error))
936
+ continue;
937
+ throw error;
938
+ }
939
+ for (const entry of entries) {
940
+ const location = node_path_1.default.join(current, entry.name);
941
+ if (this.isProjectInputCompilerOutput(location, identities)) {
942
+ continue;
943
+ }
944
+ if (entry.isDirectory()) {
945
+ stack.push(location);
946
+ }
947
+ else if (entry.isFile() &&
948
+ matchesProjectInputGlob(glob, location, identities)) {
949
+ const identity = identities.resolve(location);
950
+ matches.set(identity.key, identity.path);
951
+ }
952
+ }
953
+ }
954
+ }
955
+ return matches;
106
956
  }
107
957
  refreshFromDirectory(location, changed) {
108
958
  if (changed !== undefined &&
109
959
  isDirectory(changed) &&
110
- this.isCompilerOutputDirectory(changed) === false) {
960
+ this.isCompilerOutputDirectory(changed) === false &&
961
+ this.isProjectInputDirectory(changed) === false) {
111
962
  this.observedDirectories.set(pathKey(changed), changed);
112
963
  }
113
964
  try {
114
965
  this.refresh(true);
115
966
  }
116
967
  catch (error) {
968
+ for (const reload of reloadInputsForFailedTopologyRefresh(this.reloadFiles.values(), changed)) {
969
+ this.callbacks.onInputChange({ kind: "config", path: reload });
970
+ }
117
971
  this.callbacks.onError(location, error);
118
972
  }
119
973
  }
120
974
  isCompilerOutputDirectory(location) {
121
975
  return [...this.outputs.values()].some((output) => isPathWithin(output, location));
122
976
  }
977
+ isCompilerOutput(location) {
978
+ return (this.outputFiles.has(pathKey(location)) ||
979
+ this.isCompilerOutputDirectory(location));
980
+ }
981
+ isProjectInputCompilerOutputDirectory(location, identities) {
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
+ }
988
+ return [...this.outputs.values()].some((output) => {
989
+ if (!identities.isWithin(output, location))
990
+ return false;
991
+ // An output directory that is the project itself, or holds it, is not a
992
+ // place where only build products live -- the sources are there too. A
993
+ // project emitting in place declares exactly that, and honouring it
994
+ // literally would classify every declared input as a product and leave
995
+ // the whole lane unwatched without anything failing to say so. Watching
996
+ // is the safe side here: a product that gets watched costs an extra
997
+ // rebuild, while an input that does not is never seen again.
998
+ if (root !== "" && identities.isWithin(output, root))
999
+ return false;
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;
1008
+ });
1009
+ }
1010
+ isProjectInputCompilerOutput(location, identities) {
1011
+ const key = identities.resolve(location).key;
1012
+ return ([...this.outputFiles.values()].some((output) => identities.resolve(output).key === key) || this.isProjectInputCompilerOutputDirectory(location, identities));
1013
+ }
1014
+ isProjectInputDirectory(location) {
1015
+ const resolved = node_path_1.default.resolve(location);
1016
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
1017
+ return (this.projectInputs.files.some((file) => identities.isWithin(resolved, file) ||
1018
+ identities.isWithin(node_path_1.default.dirname(file), resolved)) ||
1019
+ (this.projectInputs.reloadFiles ?? []).some((file) => identities.isWithin(resolved, file) ||
1020
+ identities.isWithin(node_path_1.default.dirname(file), resolved)) ||
1021
+ (this.projectInputs.reloadDirectories ?? []).some((directory) => identities.isWithin(resolved, directory) ||
1022
+ identities.isWithin(directory, resolved)) ||
1023
+ this.projectInputs.globs.some((glob) => {
1024
+ const root = literalGlobRoot(glob);
1025
+ return (identities.isWithin(root, resolved) ||
1026
+ identities.isWithin(resolved, root));
1027
+ }));
1028
+ }
1029
+ classifyCompilerInput(location) {
1030
+ if (this.isPluginInput(location))
1031
+ return "plugin";
1032
+ return this.reloadFiles.has(pathKey(location)) ? "config" : "compiler";
1033
+ }
1034
+ isPluginInput(location) {
1035
+ const resolved = node_path_1.default.resolve(location);
1036
+ return this.extraInputs.some((input) => pathKey(input) === pathKey(resolved) || isPathWithin(input, resolved));
1037
+ }
123
1038
  }
124
1039
  exports.WatchTopology = WatchTopology;
1040
+ function reloadInputsForFailedTopologyRefresh(reloadFiles, changed) {
1041
+ const changedKey = changed === undefined ? undefined : pathKey(changed);
1042
+ const reloads = new Map();
1043
+ for (const location of reloadFiles) {
1044
+ const resolved = node_path_1.default.resolve(location);
1045
+ const key = pathKey(resolved);
1046
+ if ((changedKey !== undefined && key === changedKey) ||
1047
+ node_fs_1.default.existsSync(resolved) === false) {
1048
+ reloads.set(key, resolved);
1049
+ }
1050
+ }
1051
+ return [...reloads.values()].sort();
1052
+ }
125
1053
  function resolveWatchTopology(options, extraInputs) {
1054
+ let analysisOnly = options.emit === false;
126
1055
  const files = new Map();
1056
+ const outputFiles = new Map();
127
1057
  const outputs = new Map();
1058
+ const reloadFiles = new Map();
128
1059
  const roots = [];
129
1060
  if (options.files.length !== 0) {
130
1061
  const project = (0, readProjectConfig_1.readProjectConfig)({
@@ -132,26 +1063,59 @@ function resolveWatchTopology(options, extraInputs) {
132
1063
  projectRoot: options.projectRoot,
133
1064
  tsconfig: options.tsconfig,
134
1065
  });
1066
+ analysisOnly = watchTopologyAnalysisOnly(options, project);
135
1067
  roots.push(project.root);
136
1068
  addPaths(files, project.configPaths);
137
- addPaths(outputs, resolveCompilerOutputs(project, options).directories);
138
- addPaths(files, options.files.map((file) => node_path_1.default.resolve(options.cwd, file)));
1069
+ addPaths(reloadFiles, project.configPaths);
1070
+ const positionalInputs = options.files.map((file) => node_path_1.default.resolve(options.cwd, file));
1071
+ if (positionalInputs.length === 1 &&
1072
+ (options.emit ?? project.compilerOptions.noEmit !== true)) {
1073
+ addPaths(outputFiles, [
1074
+ (0, singleFileOutput_1.resolveSingleFileOutput)({
1075
+ cliOutDir: options.outDir,
1076
+ cwd: options.cwd,
1077
+ file: positionalInputs[0],
1078
+ passthrough: options.passthrough,
1079
+ tsconfig: options.tsconfig,
1080
+ }),
1081
+ ]);
1082
+ }
1083
+ addPaths(files, positionalInputs);
139
1084
  }
140
1085
  else {
141
- for (const project of readReferencedProjects(options)) {
1086
+ const projects = readReferencedProjects(options);
1087
+ if (projects[0] !== undefined) {
1088
+ analysisOnly = watchTopologyAnalysisOnly(options, projects[0]);
1089
+ }
1090
+ for (const project of projects) {
142
1091
  roots.push(project.root);
143
1092
  addPaths(files, project.configPaths);
144
- addPaths(outputs, resolveCompilerOutputs(project, options).directories);
145
- addPaths(files, listCompilerInputs(project, options));
1093
+ addPaths(reloadFiles, project.configPaths);
1094
+ const compilerInputs = listCompilerInputs(project, options);
1095
+ const compilerOutputs = resolveCompilerOutputs(project, options);
1096
+ addPaths(outputFiles, compilerOutputs.files);
1097
+ addPaths(outputFiles, inferPerSourceCompilerOutputs(project, options, compilerInputs));
1098
+ addPaths(outputs, compilerOutputs.directories);
1099
+ addPaths(files, compilerInputs);
146
1100
  }
147
1101
  }
148
1102
  addPaths(files, extraInputs);
149
1103
  return {
1104
+ analysisOnly,
150
1105
  directories: collectTopologyDirectories(files.values(), roots),
151
1106
  files,
1107
+ outputFiles,
152
1108
  outputs,
1109
+ reloadFiles,
153
1110
  };
154
1111
  }
1112
+ function watchTopologyAnalysisOnly(options, project) {
1113
+ if (options.emit !== undefined)
1114
+ return options.emit === false;
1115
+ const noEmit = passthroughBooleanOption(options.passthrough, "--noEmit") ??
1116
+ project.compilerOptions.noEmit === true;
1117
+ return noEmit;
1118
+ }
155
1119
  function readReferencedProjects(options) {
156
1120
  const root = (0, readProjectConfig_1.readProjectConfig)({
157
1121
  cwd: options.cwd,
@@ -215,35 +1179,263 @@ function listCompilerInputs(project, options) {
215
1179
  if (result.error) {
216
1180
  throw new Error(`ttsc: failed to list compiler inputs: ${result.error.message}`);
217
1181
  }
218
- 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.
219
1187
  const inputs = (0, spawnNative_1.outputText)(result.stdout)
220
1188
  .split(/\r?\n/)
221
1189
  .map((line) => line.trim())
222
1190
  .filter((line) => node_path_1.default.isAbsolute(line))
223
- .map((line) => node_path_1.default.resolve(line))
224
- .filter((file) => isCompilerOutput(file, outputs) === false);
1191
+ .map((line) => node_path_1.default.resolve(line));
225
1192
  if (result.status !== 0 && inputs.length === 0) {
226
1193
  throw new Error(`ttsc: failed to list compiler inputs:\n${(0, spawnNative_1.outputText)(result.stderr) || (0, spawnNative_1.outputText)(result.stdout)}`);
227
1194
  }
228
1195
  return inputs;
229
1196
  }
230
1197
  function resolveCompilerOutputs(project, options) {
1198
+ const emit = effectiveCompilerEmit(project, options);
1199
+ const directories = new Set();
1200
+ const files = new Set();
1201
+ if (emit.outDir !== undefined &&
1202
+ (emit.javascript || (emit.declaration && emit.declarationDir === undefined))) {
1203
+ directories.add(emit.outDir);
1204
+ }
1205
+ if (emit.declaration && emit.declarationDir !== undefined) {
1206
+ directories.add(emit.declarationDir);
1207
+ }
1208
+ if (emit.incremental) {
1209
+ files.add(defaultTsBuildInfoFile(project, emit));
1210
+ }
1211
+ return {
1212
+ directories: [...directories],
1213
+ files: [...files],
1214
+ };
1215
+ }
1216
+ function inferPerSourceCompilerOutputs(project, options, inputs) {
1217
+ const emit = effectiveCompilerEmit(project, options);
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;
1224
+ for (const input of inputs) {
1225
+ const extension = node_path_1.default.extname(input).toLowerCase();
1226
+ if (/\.d\.(?:ts|mts|cts)$/i.test(input))
1227
+ continue;
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;
1249
+ if (emit.javascript &&
1250
+ (emit.outDir !== undefined ||
1251
+ !isJavaScriptSourceExtension(extension) ||
1252
+ (extension === ".jsx" && emit.jsx !== "preserve"))) {
1253
+ const javascriptExtension = extension === ".mts" || extension === ".mjs"
1254
+ ? ".mjs"
1255
+ : extension === ".cts" || extension === ".cjs"
1256
+ ? ".cjs"
1257
+ : (extension === ".tsx" || extension === ".jsx") &&
1258
+ emit.jsx === "preserve"
1259
+ ? ".jsx"
1260
+ : ".js";
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) {
1270
+ const declarationExtension = extension === ".mts" || extension === ".mjs"
1271
+ ? ".d.mts"
1272
+ : extension === ".cts" || extension === ".cjs"
1273
+ ? ".d.cts"
1274
+ : ".d.ts";
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
+ }
1282
+ }
1283
+ }
1284
+ return [...outputs];
1285
+ }
1286
+ function isCompilerEmittableSourceExtension(extension) {
1287
+ return [
1288
+ ".cjs",
1289
+ ".cts",
1290
+ ".js",
1291
+ ".jsx",
1292
+ ".mjs",
1293
+ ".mts",
1294
+ ".ts",
1295
+ ".tsx",
1296
+ ].includes(extension);
1297
+ }
1298
+ function isJavaScriptSourceExtension(extension) {
1299
+ return [".cjs", ".js", ".jsx", ".mjs"].includes(extension);
1300
+ }
1301
+ function effectiveCompilerEmit(project, options) {
231
1302
  const compilerOptions = project.compilerOptions;
232
- const outDir = options.outDir
233
- ? node_path_1.default.resolve(options.cwd, options.outDir)
234
- : compilerOptions.outDir;
1303
+ const passthrough = options.passthrough;
1304
+ const noEmit = passthroughBooleanOption(passthrough, "--noEmit") ??
1305
+ (options.emit === false
1306
+ ? true
1307
+ : options.emit === true
1308
+ ? false
1309
+ : compilerOptions.noEmit === true);
1310
+ const composite = passthroughBooleanOption(passthrough, "--composite") ??
1311
+ compilerOptions.composite === true;
1312
+ const incremental = composite ||
1313
+ (passthroughBooleanOption(passthrough, "--incremental") ??
1314
+ compilerOptions.incremental === true);
1315
+ const emitDeclarationOnly = passthroughBooleanOption(passthrough, "--emitDeclarationOnly") ??
1316
+ (options.emit === true
1317
+ ? false
1318
+ : compilerOptions.emitDeclarationOnly === true);
1319
+ const declaration = !noEmit &&
1320
+ (composite ||
1321
+ (passthroughBooleanOption(passthrough, "--declaration") ??
1322
+ compilerOptions.declaration === true));
1323
+ const javascript = !noEmit && !emitDeclarationOnly;
1324
+ const sourceMap = javascript &&
1325
+ (passthroughBooleanOption(passthrough, "--sourceMap") ??
1326
+ compilerOptions.sourceMap === true) &&
1327
+ !(passthroughBooleanOption(passthrough, "--inlineSourceMap") ??
1328
+ compilerOptions.inlineSourceMap === true);
1329
+ const declarationMap = declaration &&
1330
+ (passthroughBooleanOption(passthrough, "--declarationMap") ??
1331
+ compilerOptions.declarationMap === true);
1332
+ const cliOutDir = passthroughPathOption(passthrough, "--outDir");
1333
+ const cliDeclarationDir = passthroughPathOption(passthrough, "--declarationDir");
1334
+ const cliRootDir = passthroughPathOption(passthrough, "--rootDir");
1335
+ const cliTsBuildInfoFile = passthroughPathOption(passthrough, "--tsBuildInfoFile");
1336
+ const jsx = passthroughStringOption(passthrough, "--jsx") ?? compilerOptions.jsx;
1337
+ const compilerCwd = project.root;
235
1338
  return {
236
- directories: [outDir, compilerOptions.declarationDir]
237
- .filter((value) => typeof value === "string")
238
- .map((value) => node_path_1.default.resolve(value)),
239
- files: [compilerOptions.outFile, compilerOptions.tsBuildInfoFile]
240
- .filter((value) => typeof value === "string")
241
- .map((value) => node_path_1.default.resolve(project.root, value)),
1339
+ declaration,
1340
+ declarationDir: cliDeclarationDir === null
1341
+ ? undefined
1342
+ : cliDeclarationDir !== undefined
1343
+ ? node_path_1.default.resolve(compilerCwd, cliDeclarationDir)
1344
+ : typeof compilerOptions.declarationDir === "string"
1345
+ ? node_path_1.default.resolve(compilerOptions.declarationDir)
1346
+ : undefined,
1347
+ declarationMap,
1348
+ incremental,
1349
+ javascript,
1350
+ outDir: cliOutDir === null
1351
+ ? undefined
1352
+ : cliOutDir !== undefined
1353
+ ? node_path_1.default.resolve(compilerCwd, cliOutDir)
1354
+ : options.outDir !== undefined
1355
+ ? node_path_1.default.resolve(options.cwd, options.outDir)
1356
+ : typeof compilerOptions.outDir === "string"
1357
+ ? node_path_1.default.resolve(compilerOptions.outDir)
1358
+ : undefined,
1359
+ resolveJsonModule: passthroughBooleanOption(passthrough, "--resolveJsonModule") ??
1360
+ compilerOptions.resolveJsonModule === true,
1361
+ rootDir: cliRootDir === null
1362
+ ? undefined
1363
+ : cliRootDir !== undefined
1364
+ ? node_path_1.default.resolve(compilerCwd, cliRootDir)
1365
+ : typeof compilerOptions.rootDir === "string"
1366
+ ? node_path_1.default.resolve(compilerOptions.rootDir)
1367
+ : undefined,
1368
+ sourceMap,
1369
+ tsBuildInfoFile: cliTsBuildInfoFile === null
1370
+ ? undefined
1371
+ : cliTsBuildInfoFile !== undefined
1372
+ ? node_path_1.default.resolve(compilerCwd, cliTsBuildInfoFile)
1373
+ : typeof compilerOptions.tsBuildInfoFile === "string"
1374
+ ? node_path_1.default.resolve(compilerOptions.tsBuildInfoFile)
1375
+ : undefined,
1376
+ jsx,
242
1377
  };
243
1378
  }
244
- function isCompilerOutput(file, outputs) {
245
- return (outputs.files.some((output) => pathKey(output) === pathKey(file)) ||
246
- outputs.directories.some((directory) => isPathWithin(directory, file)));
1379
+ function defaultTsBuildInfoFile(project, emit) {
1380
+ if (emit.tsBuildInfoFile !== undefined)
1381
+ return emit.tsBuildInfoFile;
1382
+ const configWithoutExtension = replaceOutputExtension(project.path, "");
1383
+ if (emit.outDir === undefined)
1384
+ return `${configWithoutExtension}.tsbuildinfo`;
1385
+ const relative = emit.rootDir === undefined
1386
+ ? node_path_1.default.basename(configWithoutExtension)
1387
+ : node_path_1.default.relative(emit.rootDir, configWithoutExtension);
1388
+ return node_path_1.default.resolve(emit.outDir, `${relative}.tsbuildinfo`);
1389
+ }
1390
+ function replaceOutputExtension(location, extension) {
1391
+ const current = node_path_1.default.extname(location);
1392
+ return current === ""
1393
+ ? `${location}${extension}`
1394
+ : `${location.slice(0, -current.length)}${extension}`;
1395
+ }
1396
+ function passthroughBooleanOption(tokens, name) {
1397
+ let value;
1398
+ for (let index = 0; index < (tokens?.length ?? 0); index++) {
1399
+ const token = tokens?.[index];
1400
+ if (token === undefined)
1401
+ continue;
1402
+ if (!passthroughOptionMatches(token, name))
1403
+ continue;
1404
+ const next = tokens?.[index + 1];
1405
+ if (next === "true" || next === "false" || next === "null") {
1406
+ value = next === "true";
1407
+ index++;
1408
+ }
1409
+ else {
1410
+ value = true;
1411
+ }
1412
+ }
1413
+ return value;
1414
+ }
1415
+ function passthroughPathOption(tokens, name) {
1416
+ let value;
1417
+ for (let index = 0; index < (tokens?.length ?? 0); index++) {
1418
+ const token = tokens?.[index];
1419
+ if (token === undefined)
1420
+ continue;
1421
+ if (!passthroughOptionMatches(token, name))
1422
+ continue;
1423
+ if (index + 1 < (tokens?.length ?? 0)) {
1424
+ const next = tokens?.[++index];
1425
+ value = next === "null" ? null : next;
1426
+ }
1427
+ }
1428
+ return value;
1429
+ }
1430
+ function passthroughStringOption(tokens, name) {
1431
+ return passthroughPathOption(tokens, name)?.toLowerCase() ?? undefined;
1432
+ }
1433
+ function passthroughOptionMatches(token, name) {
1434
+ if (!token.startsWith("-"))
1435
+ return false;
1436
+ if (token.includes("="))
1437
+ return false;
1438
+ return (0, schema_1.resolveFlagSpec)(token)?.name === (0, schema_1.resolveFlagSpec)(name)?.name;
247
1439
  }
248
1440
  function collectTopologyDirectories(files, roots) {
249
1441
  const directories = new Map();
@@ -278,7 +1470,16 @@ function collectInputDirectories(input) {
278
1470
  while (stack.length !== 0) {
279
1471
  const current = stack.pop();
280
1472
  directories.push(current);
281
- for (const entry of node_fs_1.default.readdirSync(current, { withFileTypes: true })) {
1473
+ let entries;
1474
+ try {
1475
+ entries = node_fs_1.default.readdirSync(current, { withFileTypes: true });
1476
+ }
1477
+ catch (error) {
1478
+ if (isVanishedFilesystemEntry(error))
1479
+ continue;
1480
+ throw error;
1481
+ }
1482
+ for (const entry of entries) {
282
1483
  if (entry.isDirectory()) {
283
1484
  stack.push(node_path_1.default.join(current, entry.name));
284
1485
  }
@@ -286,6 +1487,11 @@ function collectInputDirectories(input) {
286
1487
  }
287
1488
  return directories;
288
1489
  }
1490
+ function isVanishedFilesystemEntry(error) {
1491
+ return (error instanceof Error &&
1492
+ "code" in error &&
1493
+ (error.code === "ENOENT" || error.code === "ENOTDIR"));
1494
+ }
289
1495
  function isDirectory(location) {
290
1496
  try {
291
1497
  return node_fs_1.default.statSync(location).isDirectory();
@@ -294,25 +1500,40 @@ function isDirectory(location) {
294
1500
  return false;
295
1501
  }
296
1502
  }
297
- function syncWatchers(watchers, desired, create, onError) {
298
- for (const [key, watcher] of watchers) {
299
- if (desired.has(key))
300
- continue;
301
- watcher.close();
302
- watchers.delete(key);
303
- }
1503
+ function syncWatchers(watchers, desired, create, onError, shouldContinue = () => true) {
1504
+ let complete = true;
304
1505
  for (const [key, location] of desired) {
1506
+ if (!shouldContinue()) {
1507
+ complete = false;
1508
+ break;
1509
+ }
305
1510
  if (watchers.has(key))
306
1511
  continue;
307
1512
  try {
308
- const watcher = create(location);
309
- watcher.on("error", (error) => onError(location, error));
1513
+ const watcher = create(location, key);
1514
+ watcher.on("error", (error) => {
1515
+ if (watchers.get(key) === watcher) {
1516
+ watchers.delete(key);
1517
+ }
1518
+ watcher.close();
1519
+ onError(location, error);
1520
+ });
310
1521
  watchers.set(key, watcher);
311
1522
  }
312
1523
  catch (error) {
1524
+ complete = false;
313
1525
  onError(location, error);
314
1526
  }
315
1527
  }
1528
+ if (!complete)
1529
+ return false;
1530
+ for (const [key, watcher] of watchers) {
1531
+ if (desired.has(key))
1532
+ continue;
1533
+ watcher.close();
1534
+ watchers.delete(key);
1535
+ }
1536
+ return true;
316
1537
  }
317
1538
  function closeWatchers(watchers) {
318
1539
  for (const watcher of watchers.values())
@@ -335,6 +1556,588 @@ function uniqueExistingPaths(paths) {
335
1556
  }
336
1557
  return [...unique.values()];
337
1558
  }
1559
+ function normalizeProjectInputSnapshot(snapshot) {
1560
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
1561
+ const files = new Map();
1562
+ const globs = new Map();
1563
+ const reloadDirectories = new Map();
1564
+ const reloadFiles = new Map();
1565
+ for (const file of snapshot.files) {
1566
+ const identity = identities.resolve(file);
1567
+ files.set(identity.key, identity.path);
1568
+ }
1569
+ for (const glob of snapshot.globs) {
1570
+ const identity = identities.resolve(glob);
1571
+ globs.set(identity.key, identity.path.split(node_path_1.default.sep).join("/"));
1572
+ }
1573
+ for (const file of snapshot.reloadFiles ?? []) {
1574
+ const identity = identities.resolve(file);
1575
+ reloadFiles.set(identity.key, identity.path);
1576
+ }
1577
+ for (const directory of snapshot.reloadDirectories ?? []) {
1578
+ const identity = identities.resolve(directory);
1579
+ reloadDirectories.set(identity.key, identity.path);
1580
+ }
1581
+ return {
1582
+ files: [...files.values()].sort(),
1583
+ globs: [...globs.values()].sort(),
1584
+ reloadDirectories: [...reloadDirectories.values()].sort(),
1585
+ reloadFiles: [...reloadFiles.values()].sort(),
1586
+ root: identities.resolve(snapshot.root).path,
1587
+ };
1588
+ }
1589
+ function projectInputSnapshotsEqual(left, right) {
1590
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
1591
+ return (
1592
+ // Every sibling comparison in this class decides on identity, and this one
1593
+ // is only safe today because both operands come from the same normalizer.
1594
+ identities.resolve(left.root || ".").key ===
1595
+ identities.resolve(right.root || ".").key &&
1596
+ arraysEqual(left.files, right.files) &&
1597
+ arraysEqual(left.globs, right.globs) &&
1598
+ arraysEqual(left.reloadDirectories ?? [], right.reloadDirectories ?? []) &&
1599
+ arraysEqual(left.reloadFiles ?? [], right.reloadFiles ?? []));
1600
+ }
1601
+ function literalGlobRoot(pattern) {
1602
+ const resolved = node_path_1.default.resolve(pattern);
1603
+ const normalized = resolved.split("\\").join("/");
1604
+ const wildcard = normalized.search(/[*?]/);
1605
+ if (wildcard === -1)
1606
+ return node_path_1.default.dirname(resolved);
1607
+ const separator = normalized.lastIndexOf("/", wildcard);
1608
+ const prefix = separator < 0 ? "." : normalized.slice(0, separator);
1609
+ const volumeRoot = node_path_1.default.parse(resolved).root;
1610
+ const normalizedVolumeRoot = volumeRoot.split("\\").join("/");
1611
+ if (prefix.length === 0 ||
1612
+ prefix === normalizedVolumeRoot.replace(/\/$/, "")) {
1613
+ return volumeRoot;
1614
+ }
1615
+ return node_path_1.default.resolve(prefix);
1616
+ }
1617
+ function projectInputDeclarationKey(kind, declaration) {
1618
+ return `${kind}\0${(0, projectInputPathIdentity_1.resolveProjectInputPath)(declaration)}`;
1619
+ }
1620
+ /**
1621
+ * Chooses the one stable recursive watcher root owned by a project-input
1622
+ * declaration.
1623
+ *
1624
+ * Inputs inside the project share its physical root so directory replacement
1625
+ * cannot strand a child handle. External inputs use the nearest existing
1626
+ * ancestor of their declared parent, which is the explicit boundary for
1627
+ * observing a currently missing external tree without polling every file --
1628
+ * except that the boundary never rises to a directory holding the project,
1629
+ * since such a root outranks the project's own in the active merge and leaves
1630
+ * one handle over a shared system directory to carry everything. An external
1631
+ * declaration that can only be owned that way falls back to its own tree, and
1632
+ * is left unwatched when even that would contain the project.
1633
+ */
1634
+ function projectInputWatchDirectories(target, projectRoot) {
1635
+ const root = projectInputRecursiveWatchRoot(target, projectRoot);
1636
+ return root === undefined ? [] : [root];
1637
+ }
1638
+ /**
1639
+ * Removes recursive roots already covered by an ancestor without rewriting the
1640
+ * declaration-specific roots retained by WatchTopology.
1641
+ */
1642
+ function projectInputActiveWatchDirectories(directories, identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)()) {
1643
+ const unique = new Map();
1644
+ for (const directory of directories) {
1645
+ // Coverage is decided on physical identity so two spellings of one
1646
+ // directory cannot both survive, but the surviving entry keeps the
1647
+ // caller's spelling: this function selects roots, it does not rename them.
1648
+ const identity = identities.resolve(directory);
1649
+ if (!unique.has(identity.key))
1650
+ unique.set(identity.key, directory);
1651
+ }
1652
+ return [...unique]
1653
+ .filter(([key]) => {
1654
+ let ancestor = node_path_1.default.dirname(key);
1655
+ while (ancestor !== key) {
1656
+ if (unique.has(ancestor))
1657
+ return false;
1658
+ const parent = node_path_1.default.dirname(ancestor);
1659
+ if (parent === ancestor)
1660
+ break;
1661
+ ancestor = parent;
1662
+ }
1663
+ return true;
1664
+ })
1665
+ .map(([, directory]) => directory);
1666
+ }
1667
+ function projectInputAvailableWatchDirectory(location, rejected, identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)(), projectRoot) {
1668
+ const resolvedProjectRoot = projectRoot === undefined ? undefined : node_path_1.default.resolve(projectRoot);
1669
+ let current = node_path_1.default.resolve(location);
1670
+ while (true) {
1671
+ const identity = identities.resolve(current);
1672
+ // Escalation obeys the same ceiling root selection does. A watcher that
1673
+ // failed once would otherwise be replaced by one over a directory holding
1674
+ // the project, which outranks the project's own root in the active merge
1675
+ // and re-opens, through the recovery path, the exact swallow that selection
1676
+ // refuses to create. Giving up the failed root is the lesser loss.
1677
+ if (resolvedProjectRoot !== undefined &&
1678
+ identity.key !== identities.resolve(resolvedProjectRoot).key &&
1679
+ identities.isWithin(identity.path, resolvedProjectRoot)) {
1680
+ return undefined;
1681
+ }
1682
+ if (!rejected.has(identity.key))
1683
+ return identity.path;
1684
+ const parent = node_path_1.default.dirname(current);
1685
+ if (parent === current)
1686
+ return undefined;
1687
+ const fallback = nearestExistingDirectory(parent);
1688
+ if (fallback === undefined)
1689
+ return undefined;
1690
+ current = fallback;
1691
+ }
1692
+ }
1693
+ function projectInputRecursiveWatchRoot(target, projectRoot, identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)()) {
1694
+ const resolvedTarget = node_path_1.default.resolve(target);
1695
+ const resolvedProjectRoot = node_path_1.default.resolve(projectRoot);
1696
+ if (identities.isWithin(resolvedProjectRoot, resolvedTarget)) {
1697
+ return nearestExistingDirectory(resolvedProjectRoot);
1698
+ }
1699
+ // An external anchor rises to the declared parent so a tree that does not
1700
+ // exist yet is still observed, and so siblings under it share one handle. It
1701
+ // may not rise past the project, though: a directory that contains the
1702
+ // project swallows the project's own root when the two are merged, and every
1703
+ // in-project declaration then rides one recursive handle over a shared system
1704
+ // directory — a temp root, or the filesystem root itself — which delivers
1705
+ // nothing. Prefer the declared parent, fall back to the target's own tree,
1706
+ // and decline rather than widen past the project.
1707
+ for (const candidate of [
1708
+ nearestExistingDirectory(node_path_1.default.dirname(resolvedTarget)),
1709
+ nearestExistingDirectory(resolvedTarget),
1710
+ ]) {
1711
+ if (candidate === undefined)
1712
+ continue;
1713
+ // The project root cannot outrank itself in the merge, so it is the one
1714
+ // container that is never a swallow — it is the owner the internal branch
1715
+ // would have chosen anyway. A declaration reached through an in-project
1716
+ // directory symlink lands here, and rejecting it would drop the hoist that
1717
+ // keeps a replaced directory from stranding a child handle.
1718
+ if (identities.resolve(candidate).key !==
1719
+ identities.resolve(resolvedProjectRoot).key &&
1720
+ identities.isWithin(candidate, resolvedProjectRoot)) {
1721
+ continue;
1722
+ }
1723
+ return candidate;
1724
+ }
1725
+ return undefined;
1726
+ }
1727
+ /**
1728
+ * Cheap content and physical-owner identities for a tracked file.
1729
+ *
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.
1734
+ */
1735
+ function compilerFileSnapshot(location) {
1736
+ try {
1737
+ const stats = node_fs_1.default.statSync(location);
1738
+ return {
1739
+ content: `${stats.mtimeMs}:${stats.size}`,
1740
+ owner: `${stats.dev}:${stats.ino}`,
1741
+ };
1742
+ }
1743
+ catch {
1744
+ return { content: "", owner: "" };
1745
+ }
1746
+ }
1747
+ function isSymbolicLink(location) {
1748
+ try {
1749
+ return node_fs_1.default.lstatSync(location).isSymbolicLink();
1750
+ }
1751
+ catch {
1752
+ return false;
1753
+ }
1754
+ }
1755
+ function nearestExistingDirectory(location) {
1756
+ let current = node_path_1.default.resolve(location);
1757
+ while (true) {
1758
+ if (isDirectory(current))
1759
+ return current;
1760
+ const parent = node_path_1.default.dirname(current);
1761
+ if (parent === current)
1762
+ return undefined;
1763
+ current = parent;
1764
+ }
1765
+ }
1766
+ function matchesProjectInput(snapshot, location, identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)()) {
1767
+ const key = identities.resolve(location).key;
1768
+ return (snapshot.files.some((file) => identities.resolve(file).key === key) ||
1769
+ (snapshot.reloadFiles ?? []).some((file) => identities.resolve(file).key === key) ||
1770
+ (snapshot.reloadDirectories ?? []).some((directory) => identities.isWithin(directory, location)) ||
1771
+ snapshot.globs.some((glob) => matchesProjectInputGlob(glob, location, identities)));
1772
+ }
1773
+ /**
1774
+ * Whether a replacement at this path leaves a recursive watcher bound to the
1775
+ * object that was replaced.
1776
+ *
1777
+ * Only one backend needs the answer. Node routes a recursive watch to its own
1778
+ * per-directory implementation when the platform is neither macOS nor Windows,
1779
+ * and that implementation keys its handles by path: the handle for a directory
1780
+ * renamed away stays bound to the object that left, and any child whose name
1781
+ * survives the swap is skipped as already known. The native subtree backends
1782
+ * both other platforms use follow the path, so retiring their watcher would buy
1783
+ * nothing and would open a window in which no events are delivered.
1784
+ *
1785
+ * The answer is deliberately narrower than the rescan rule, because
1786
+ * reinstalling a root costs one watch descriptor per entry beneath it, which an
1787
+ * install storm would pay thousands of times. A directory appearing inside a
1788
+ * glob root deserves a rescan but replaces nothing a root stands on, and a
1789
+ * reload directory anchors the directory that contains it rather than itself,
1790
+ * since its fingerprint is a digest of its own immediate entries and nothing
1791
+ * below it can reach the declared corpus.
1792
+ */
1793
+ function projectInputReplacementStrandsWatchers(snapshot, location, identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)(), platform = process.platform) {
1794
+ if (platform === "darwin" || platform === "win32")
1795
+ return false;
1796
+ const changed = node_path_1.default.resolve(location);
1797
+ if (!isDirectory(changed))
1798
+ return false;
1799
+ return projectInputAnchorsDeclaration(snapshot, node_path_1.default.dirname(changed), identities);
1800
+ }
1801
+ function projectInputAnchorsDeclaration(snapshot, directory, identities) {
1802
+ return (snapshot.files.some((file) => identities.isWithin(directory, file)) ||
1803
+ (snapshot.reloadFiles ?? []).some((file) => identities.isWithin(directory, file)) ||
1804
+ (snapshot.reloadDirectories ?? []).some((entry) =>
1805
+ // A reload directory anchors the directory that contains it, never
1806
+ // itself. Its own fingerprint is a digest of its immediate entries, so
1807
+ // nothing below it can reach the corpus, and treating it as its own
1808
+ // anchor would rearm for every entry created directly inside it —
1809
+ // including `node_modules`, which contributors publish as one.
1810
+ identities.isWithin(directory, entry) &&
1811
+ identities.resolve(directory).key !== identities.resolve(entry).key) ||
1812
+ snapshot.globs.some((glob) => identities.isWithin(directory, literalGlobRoot(glob))));
1813
+ }
1814
+ /**
1815
+ * Decide whether an event that named no declared input can still have moved
1816
+ * one.
1817
+ *
1818
+ * The admitted set is the only bound on how often a watch session re-reads and
1819
+ * re-hashes its declared corpus, and both directions cost: too narrow drops an
1820
+ * atomic replacement, too wide re-fingerprints on every entry an install
1821
+ * creates. Exported so that boundary is pinned directly instead of being
1822
+ * inferred from a rebuild that a silent rescan and a skipped rescan produce
1823
+ * identically.
1824
+ */
1825
+ function projectInputTopologyMayAffect(snapshot, location, previous, identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)()) {
1826
+ const changed = node_path_1.default.resolve(location);
1827
+ const anchors = (directory) => projectInputAnchorsDeclaration(snapshot, directory, identities);
1828
+ // An atomic replacement never names the declared file whose bytes it changed;
1829
+ // it names the directory that was swapped, and that directory can be one the
1830
+ // declaration does not contain — renaming `docs` away reports the arriving
1831
+ // `docs-old`, not `docs`. So a directory event is admitted from where it
1832
+ // happened rather than from what it contains: its own parent must already lie
1833
+ // on the path to a declared input. A tree no declaration reaches, such as
1834
+ // `node_modules` under an ordinary project, then costs nothing per created
1835
+ // entry instead of a population rescan and a full content re-fingerprint. A
1836
+ // glob whose literal root covers that tree still admits every directory
1837
+ // beneath it through the branch below, because a directory appearing inside a
1838
+ // glob root can hold matches; the declaration decides that reach, not this
1839
+ // rule.
1840
+ if (isDirectory(changed) && anchors(node_path_1.default.dirname(changed)))
1841
+ return true;
1842
+ return (anchors(changed) ||
1843
+ (snapshot.reloadDirectories ?? []).some((directory) => identities.isWithin(directory, changed)) ||
1844
+ snapshot.globs.some((glob) => {
1845
+ const root = literalGlobRoot(glob);
1846
+ if (identities.isWithin(changed, root))
1847
+ return true;
1848
+ if (identities.isWithin(root, changed) === false)
1849
+ return false;
1850
+ if (isDirectory(changed))
1851
+ return true;
1852
+ return [...previous.values()].some((input) => identities.isWithin(changed, input));
1853
+ }));
1854
+ }
1855
+ function projectInputEventShouldNotify(input) {
1856
+ return input.contentChanged || input.membershipChanged;
1857
+ }
1858
+ function projectInputChangedPaths(input) {
1859
+ const changed = new Map();
1860
+ const keys = new Set([
1861
+ ...input.previous.keys(),
1862
+ ...input.next.keys(),
1863
+ ...input.previousFingerprints.keys(),
1864
+ ...input.nextFingerprints.keys(),
1865
+ ]);
1866
+ for (const key of keys) {
1867
+ if (input.previous.has(key) === input.next.has(key) &&
1868
+ input.previousFingerprints.get(key) === input.nextFingerprints.get(key)) {
1869
+ continue;
1870
+ }
1871
+ const location = input.next.get(key) ?? input.previous.get(key);
1872
+ if (location !== undefined)
1873
+ changed.set(key, location);
1874
+ }
1875
+ return [...changed.values()];
1876
+ }
1877
+ /**
1878
+ * Classify an exact execution-selection input ahead of ordinary project data.
1879
+ *
1880
+ * `changedInputs` carries fingerprint or membership deltas, so a filename-less
1881
+ * event can still select the cold lane. A named exact event selects the cold
1882
+ * lane only after the surrounding change detector admits the event; unchanged
1883
+ * bytes remain quiet before this classifier is observed.
1884
+ */
1885
+ function projectInputReloadEventShouldNotify(input) {
1886
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
1887
+ const reloadFiles = new Set(input.reloadFiles.map((location) => identities.resolve(location).key));
1888
+ const reloadDirectories = (input.reloadDirectories ?? []).map((location) => identities.resolve(location));
1889
+ // Territory a declared glob is rooted at, judged against the resolution
1890
+ // directory that would otherwise claim it. A project root is published as a
1891
+ // resolution directory because the config lives there, so the directory a
1892
+ // glob is rooted at reads as a new immediate entry the moment it appears --
1893
+ // and appearing is what a declared population does. The data lane already
1894
+ // reports it, with program invalidation when the membership moved, which is
1895
+ // the cold-Program-same-process transition that belongs to data.
1896
+ //
1897
+ // Data can only carve out strictly below a resolution directory. A glob
1898
+ // rooted on that directory, or above it -- `literalGlobRoot` answers with the
1899
+ // volume root for a pattern with no literal prefix -- would otherwise exempt
1900
+ // everything the directory exists to classify, and the selection lane would
1901
+ // retire in silence. Its role predates any glob drawn around it.
1902
+ //
1903
+ // Only glob roots, either way. A declared file sitting directly in a
1904
+ // resolution directory is still a selection surface: its own bytes are what a
1905
+ // project rule reads to decide, once per execution.
1906
+ const globRoots = (input.globs ?? []).map((glob) => identities.resolve(literalGlobRoot(glob)));
1907
+ const exemptedFrom = (directory, location) => globRoots.some((globRoot) => globRoot.key !== directory.key &&
1908
+ identities.isWithin(directory.path, globRoot.path) &&
1909
+ identities.isWithin(globRoot.path, location));
1910
+ // A reload directory is a non-recursive surface: its digest covers its
1911
+ // immediate entries' names, kinds, and link targets, so it moves only when the
1912
+ // directory itself moves or when an entry it holds directly does. Matching the
1913
+ // whole subtree instead makes a resolution ancestor -- the lint config graph
1914
+ // publishes every node_modules level it searched, up to the filesystem root --
1915
+ // read every edit beneath it as a selection change, which restarts the sidecar
1916
+ // on each keystroke and ends warm reuse for any project inside one.
1917
+ const holdsAsImmediateEntry = (location) => {
1918
+ const parent = identities.resolve(node_path_1.default.dirname(location)).key;
1919
+ return reloadDirectories.some((directory) => directory.key === parent && exemptedFrom(directory, location) === false);
1920
+ };
1921
+ const namesDirectory = (location) => {
1922
+ const key = identities.resolve(location).key;
1923
+ return reloadDirectories.some((directory) => directory.key === key);
1924
+ };
1925
+ // A digest delta on the directory itself is the only signal there is when the
1926
+ // entry that appeared is not a declared match, so it cannot simply be dropped
1927
+ // -- but it also cannot say what appeared. The event being classified in the
1928
+ // same pass can: when it names data, the delta it caused is that data's, and
1929
+ // when it names anything else, or nothing at all, the safe reading is that
1930
+ // resolution moved. A missed reselection is a wrong answer; a spare restart is
1931
+ // a slow one.
1932
+ const explains = (directory) => {
1933
+ const causes = input.causedBy ?? (input.changed === undefined ? [] : [input.changed]);
1934
+ // Only an immediate entry can move this directory's digest, so only an
1935
+ // immediate entry can account for the delta. Asking whether the event was
1936
+ // data somewhere else answers a different directory's question, and one
1937
+ // exemption would then cancel every other directory's evidence.
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
+ });
1942
+ };
1943
+ const isReloadDirectoryEvent = (location) => namesDirectory(location) || holdsAsImmediateEntry(location);
1944
+ const isReloadDirectoryDelta = (location) => {
1945
+ if (holdsAsImmediateEntry(location))
1946
+ return true;
1947
+ const key = identities.resolve(location).key;
1948
+ return reloadDirectories.some((directory) => directory.key === key && explains(directory) === false);
1949
+ };
1950
+ return ((input.changed !== undefined &&
1951
+ (reloadFiles.has(identities.resolve(input.changed).key) ||
1952
+ isReloadDirectoryEvent(input.changed))) ||
1953
+ input.changedInputs.some((location) => reloadFiles.has(identities.resolve(location).key) ||
1954
+ isReloadDirectoryDelta(location)));
1955
+ }
1956
+ /**
1957
+ * Return whether a project-input population transition can reshape a Program.
1958
+ *
1959
+ * JSON is data to a ProjectRule but may simultaneously be a `resolveJsonModule`
1960
+ * source. TypeScript and JavaScript paths can likewise overlap a project-input
1961
+ * declaration. Their creation or deletion therefore requires a cold Program
1962
+ * inside the existing resident process. A filename-less event cannot identify
1963
+ * the changed member and is conservatively invalidating whenever the population
1964
+ * moved.
1965
+ */
1966
+ function projectInputMembershipInvalidatesProgram(input) {
1967
+ if (input.contentChanged === true &&
1968
+ (input.changedInputs ??
1969
+ (input.changed === undefined ? [] : [input.changed])).some((location) => node_path_1.default.basename(location).toLowerCase() === "package.json")) {
1970
+ return true;
1971
+ }
1972
+ if (mapsEqual(input.previous, input.next))
1973
+ return false;
1974
+ if (input.changed === undefined)
1975
+ return true;
1976
+ for (const [key, location] of input.previous) {
1977
+ if (input.next.has(key) === false &&
1978
+ projectInputPathMayAffectProgram(location)) {
1979
+ return true;
1980
+ }
1981
+ }
1982
+ for (const [key, location] of input.next) {
1983
+ if (input.previous.has(key) === false &&
1984
+ projectInputPathMayAffectProgram(location)) {
1985
+ return true;
1986
+ }
1987
+ }
1988
+ return false;
1989
+ }
1990
+ function projectInputPathMayAffectProgram(location) {
1991
+ const extension = node_path_1.default.extname(location).toLowerCase();
1992
+ return extension === ".json" || isCompilerEmittableSourceExtension(extension);
1993
+ }
1994
+ function compilerMembershipChange(previous, next) {
1995
+ const changed = new Map();
1996
+ for (const [key, location] of previous) {
1997
+ if (next.has(key) === false)
1998
+ changed.set(key, location);
1999
+ }
2000
+ for (const [key, location] of next) {
2001
+ if (previous.has(key) === false)
2002
+ changed.set(key, location);
2003
+ }
2004
+ return [...changed.values()].sort();
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
+ }
2033
+ function fingerprintProjectInputMatches(matches) {
2034
+ const fingerprints = new Map();
2035
+ for (const [key, location] of matches) {
2036
+ fingerprints.set(key, isDirectory(location)
2037
+ ? fingerprintProjectInputDirectory(location)
2038
+ : fingerprintProjectInputFile(location));
2039
+ }
2040
+ return fingerprints;
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
+ }
2053
+ function fingerprintProjectInputDirectory(location) {
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
+ }
2073
+ }
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
+ }
2082
+ }
2083
+ function matchesProjectInputGlob(pattern, location, identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)()) {
2084
+ const root = identities.resolve(literalGlobRoot(pattern));
2085
+ const candidate = identities.resolve(location);
2086
+ if (!(0, projectInputPathIdentity_1.isProjectInputPathIdentityWithin)(root.key, candidate.key))
2087
+ return false;
2088
+ const sensitive = identities.caseSensitive(root.path);
2089
+ const patternParts = node_path_1.default
2090
+ .relative(root.path, identities.resolve(pattern).path)
2091
+ .split(node_path_1.default.sep);
2092
+ const candidateParts = node_path_1.default
2093
+ .relative(root.path, candidate.path)
2094
+ .split(node_path_1.default.sep);
2095
+ return matchProjectInputGlobParts(sensitive
2096
+ ? patternParts
2097
+ : patternParts.map((segment) => segment.toLowerCase()), sensitive
2098
+ ? candidateParts
2099
+ : candidateParts.map((segment) => segment.toLowerCase()));
2100
+ }
2101
+ function matchProjectInputGlobParts(pattern, candidate) {
2102
+ const memo = new Map();
2103
+ const visit = (patternIndex, candidateIndex) => {
2104
+ const key = `${String(patternIndex)}:${String(candidateIndex)}`;
2105
+ const cached = memo.get(key);
2106
+ if (cached !== undefined)
2107
+ return cached;
2108
+ let matched;
2109
+ if (patternIndex === pattern.length) {
2110
+ matched = candidateIndex === candidate.length;
2111
+ }
2112
+ else if (pattern[patternIndex] === "**") {
2113
+ matched =
2114
+ visit(patternIndex + 1, candidateIndex) ||
2115
+ (candidateIndex !== candidate.length &&
2116
+ visit(patternIndex, candidateIndex + 1));
2117
+ }
2118
+ else {
2119
+ matched =
2120
+ candidateIndex !== candidate.length &&
2121
+ matchProjectInputGlobSegment(pattern[patternIndex], candidate[candidateIndex]) &&
2122
+ visit(patternIndex + 1, candidateIndex + 1);
2123
+ }
2124
+ memo.set(key, matched);
2125
+ return matched;
2126
+ };
2127
+ return visit(0, 0);
2128
+ }
2129
+ function matchProjectInputGlobSegment(pattern, candidate) {
2130
+ let source = "^";
2131
+ for (const char of pattern) {
2132
+ if (char === "*")
2133
+ source += ".*";
2134
+ else if (char === "?")
2135
+ source += ".";
2136
+ else
2137
+ source += char.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
2138
+ }
2139
+ return new RegExp(`${source}$`, "u").test(candidate);
2140
+ }
338
2141
  function mapsEqual(left, right) {
339
2142
  if (left.size !== right.size)
340
2143
  return false;
@@ -350,11 +2153,75 @@ function isPathWithin(root, candidate) {
350
2153
  relative.startsWith(`..${node_path_1.default.sep}`) === false &&
351
2154
  !node_path_1.default.isAbsolute(relative)));
352
2155
  }
2156
+ /**
2157
+ * Plan one compiler-directory event without relying on backend timing.
2158
+ *
2159
+ * POSIX file watchers own ordinary content changes. A named rename re-arms the
2160
+ * replaced file; an unnamed event conservatively re-arms and reports every
2161
+ * surviving tracked input below the watch root. Windows has no per-file
2162
+ * watchers here, so both named and unnamed directory events report inputs.
2163
+ */
2164
+ function planCompilerDirectoryWatchEvent(input) {
2165
+ const candidates = input.changed === undefined
2166
+ ? [...input.trackedFiles.values()].filter((file) => input.exists(file) &&
2167
+ isPathWithin(input.location, node_path_1.default.resolve(file)))
2168
+ : input.trackedFiles.has(pathKeyForPlatform(input.changed, input.platform)) && input.exists(input.changed)
2169
+ ? [input.changed]
2170
+ : [];
2171
+ if (input.changed === undefined) {
2172
+ return {
2173
+ changes: candidates,
2174
+ rearm: input.platform === "win32" ? [] : candidates,
2175
+ refresh: true,
2176
+ };
2177
+ }
2178
+ if (candidates.length === 0) {
2179
+ return { changes: [], rearm: [], refresh: true };
2180
+ }
2181
+ if (input.platform === "win32") {
2182
+ return { changes: candidates, rearm: [], refresh: false };
2183
+ }
2184
+ if (input.event === "rename") {
2185
+ return { changes: candidates, rearm: candidates, refresh: false };
2186
+ }
2187
+ return { changes: [], rearm: [], refresh: false };
2188
+ }
353
2189
  function isRecord(value) {
354
2190
  return typeof value === "object" && value !== null;
355
2191
  }
2192
+ /**
2193
+ * Resolve the spelling a filesystem watcher must be registered under.
2194
+ *
2195
+ * A watch declaration keeps its lexical spelling, because classification,
2196
+ * containment, and notification are all expressed in the caller's own paths.
2197
+ * The backend needs the canonical spelling instead. libuv stores the directory
2198
+ * string it was handed, expands each delivered event that still exists on disk
2199
+ * to its long path, and then requires the stored string to be that expansion's
2200
+ * prefix. A short (8.3) component makes the two disagree, and the resulting
2201
+ * assertion aborts the whole process (libuv issue 5010; upstream now returns an
2202
+ * error instead, but the bundled libuv every supported Node ships still
2203
+ * asserts). `os.tmpdir()` routinely yields such a path, so ordinary projects
2204
+ * reach it, not only tests. macOS matches events against the watched path
2205
+ * resolved through symlinks, so `/var` and `/private/var` must not be mixed
2206
+ * either. Resolving here keeps every backend comparing two canonical spellings
2207
+ * while callers keep resolving events against what they declared.
2208
+ */
2209
+ function watcherRegistrationPath(location) {
2210
+ try {
2211
+ return node_fs_1.default.realpathSync.native?.(location) ?? node_fs_1.default.realpathSync(location);
2212
+ }
2213
+ catch {
2214
+ // A vanished target makes fs.watch throw anyway, which syncWatchers already
2215
+ // routes to the error path; an unreadable ancestor is better watched under
2216
+ // its declared spelling than not watched at all.
2217
+ return location;
2218
+ }
2219
+ }
356
2220
  function pathKey(location) {
2221
+ return pathKeyForPlatform(location, process.platform);
2222
+ }
2223
+ function pathKeyForPlatform(location, platform) {
357
2224
  const resolved = node_path_1.default.resolve(location);
358
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
2225
+ return platform === "win32" ? resolved.toLowerCase() : resolved;
359
2226
  }
360
2227
  //# sourceMappingURL=watchTopology.js.map