ttsc 0.20.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/cmd/ttscserver/main.go +79 -3
  2. package/cmd/ttscserver/run_lsp_uses_native_plugin_manifest_source_test.go +96 -16
  3. package/driver/lsp.go +4 -0
  4. package/internal/lspserver/lsp_document_diagnostics_omits_cached_project_when_current_result_has_none_test.go +48 -0
  5. package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go +98 -0
  6. package/internal/lspserver/lsp_external_watched_change_retains_program_test.go +53 -0
  7. package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +9 -0
  8. package/internal/lspserver/lsp_native_plugin_source.go +171 -55
  9. package/internal/lspserver/lsp_native_resident.go +110 -9
  10. package/internal/lspserver/lsp_plugin.go +6 -0
  11. package/internal/lspserver/lsp_project_context_transport_identity_test.go +43 -0
  12. package/internal/lspserver/lsp_project_diagnostics.go +235 -0
  13. package/internal/lspserver/lsp_project_diagnostics_capability_is_independent_test.go +67 -0
  14. package/internal/lspserver/lsp_project_diagnostics_program_input_overlap_refreshes_all_producers_test.go +78 -0
  15. package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go +65 -0
  16. package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go +90 -0
  17. package/internal/lspserver/lsp_project_diagnostics_refreshes_only_input_owners_test.go +62 -0
  18. package/internal/lspserver/lsp_project_diagnostics_resident_unsupported_falls_back_test.go +123 -0
  19. package/internal/lspserver/lsp_project_diagnostics_resume_preserves_generation_test.go +52 -0
  20. package/internal/lspserver/lsp_project_diagnostics_translates_relative_client_project_test.go +90 -0
  21. package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
  22. package/internal/lspserver/lsp_project_input_registration.go +504 -0
  23. package/internal/lspserver/lsp_project_input_registration_rechecks_selection_baseline_test.go +71 -0
  24. package/internal/lspserver/lsp_project_input_registration_reconciles_rejected_pending_request_test.go +268 -0
  25. package/internal/lspserver/lsp_project_input_registration_shapes_patterns_test.go +94 -0
  26. package/internal/lspserver/lsp_project_input_watchers_anchor_missing_paths_test.go +48 -0
  27. package/internal/lspserver/lsp_project_inputs.go +944 -0
  28. package/internal/lspserver/lsp_project_inputs_match_missing_exact_and_glob_test.go +129 -0
  29. package/internal/lspserver/lsp_project_inputs_reject_invalid_and_foreign_snapshots_test.go +110 -0
  30. package/internal/lspserver/lsp_proxy.go +591 -28
  31. package/internal/lspserver/lsp_reload_directories_compare_immediate_topology_test.go +188 -0
  32. package/internal/lspserver/lsp_reload_file_fingerprint_preserves_raw_symlink_target_test.go +47 -0
  33. package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +1 -1
  34. package/internal/lspserver/lsp_resident_invalidation_broadcasts_program_input_overlap_test.go +60 -0
  35. package/internal/lspserver/lsp_resident_invalidation_targets_project_input_owners_test.go +51 -0
  36. package/internal/lspserver/lsp_same_transport_entries_are_deduplicated_test.go +66 -0
  37. package/lib/compiler/internal/project/readProjectConfig.d.ts +5 -3
  38. package/lib/compiler/internal/project/readProjectConfig.js +46 -14
  39. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  40. package/lib/compiler/internal/residentCheckProcess.d.ts +46 -0
  41. package/lib/compiler/internal/residentCheckProcess.js +227 -0
  42. package/lib/compiler/internal/residentCheckProcess.js.map +1 -0
  43. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -1
  44. package/lib/compiler/internal/resolveEmittedJavaScript.js +35 -18
  45. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  46. package/lib/compiler/internal/runBuild.d.ts +64 -1
  47. package/lib/compiler/internal/runBuild.js +484 -7
  48. package/lib/compiler/internal/runBuild.js.map +1 -1
  49. package/lib/compiler/internal/runSingleFileEmit.js +1 -0
  50. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  51. package/lib/flags/parser.js +13 -17
  52. package/lib/flags/parser.js.map +1 -1
  53. package/lib/flags/schema.d.ts +10 -3
  54. package/lib/flags/schema.js +99 -0
  55. package/lib/flags/schema.js.map +1 -1
  56. package/lib/internal/projectInputPathIdentity.d.ts +23 -0
  57. package/lib/internal/projectInputPathIdentity.js +222 -0
  58. package/lib/internal/projectInputPathIdentity.js.map +1 -0
  59. package/lib/launcher/internal/runTtsc.d.ts +17 -0
  60. package/lib/launcher/internal/runTtsc.js +121 -88
  61. package/lib/launcher/internal/runTtsc.js.map +1 -1
  62. package/lib/launcher/internal/runTtscserver.d.ts +13 -1
  63. package/lib/launcher/internal/runTtscserver.js +415 -45
  64. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  65. package/lib/launcher/internal/singleFileOutput.d.ts +16 -0
  66. package/lib/launcher/internal/singleFileOutput.js +103 -0
  67. package/lib/launcher/internal/singleFileOutput.js.map +1 -0
  68. package/lib/launcher/internal/watchTopology.d.ts +214 -2
  69. package/lib/launcher/internal/watchTopology.js +1560 -41
  70. package/lib/launcher/internal/watchTopology.js.map +1 -1
  71. package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
  72. package/lib/plugin/internal/loadProjectPlugins.js +5 -0
  73. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  74. package/lib/structures/ITtscPluginCapabilities.d.ts +31 -0
  75. package/lib/structures/internal/ITtscProjectInputSnapshot.d.ts +29 -0
  76. package/lib/structures/internal/ITtscProjectInputSnapshot.js +3 -0
  77. package/lib/structures/internal/ITtscProjectInputSnapshot.js.map +1 -0
  78. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -0
  79. package/package.json +8 -8
  80. package/src/compiler/internal/project/readProjectConfig.ts +62 -8
  81. package/src/compiler/internal/residentCheckProcess.ts +279 -0
  82. package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -21
  83. package/src/compiler/internal/runBuild.ts +728 -12
  84. package/src/compiler/internal/runSingleFileEmit.ts +1 -0
  85. package/src/flags/parser.ts +17 -17
  86. package/src/flags/schema.ts +111 -3
  87. package/src/internal/projectInputPathIdentity.ts +273 -0
  88. package/src/launcher/internal/runTtsc.ts +141 -117
  89. package/src/launcher/internal/runTtscserver.ts +561 -54
  90. package/src/launcher/internal/singleFileOutput.ts +133 -0
  91. package/src/launcher/internal/watchTopology.ts +2008 -59
  92. package/src/plugin/internal/loadProjectPlugins.ts +8 -0
  93. package/src/structures/ITtscPluginCapabilities.ts +34 -0
  94. package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
  95. package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
@@ -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,14 +37,39 @@ const spawnNative_1 = require("../../compiler/internal/spawnNative");
21
37
  class WatchTopology {
22
38
  options;
23
39
  callbacks;
40
+ analysisOnly = false;
41
+ closed = false;
24
42
  directories = new Map();
25
43
  directoryWatchers = new Map();
26
44
  extraInputs = [];
27
45
  extraWatchers = new Map();
46
+ compilerFileStamps = new Map();
28
47
  files = new Map();
29
48
  fileWatchers = new Map();
30
49
  observedDirectories = new Map();
50
+ outputFiles = new Map();
31
51
  outputs = new Map();
52
+ projectInputFingerprints = new Map();
53
+ projectInputMatches = new Map();
54
+ projectInputs = {
55
+ files: [],
56
+ globs: [],
57
+ reloadDirectories: [],
58
+ reloadFiles: [],
59
+ root: "",
60
+ };
61
+ declaredProjectInputs = {
62
+ files: [],
63
+ globs: [],
64
+ reloadDirectories: [],
65
+ reloadFiles: [],
66
+ root: "",
67
+ };
68
+ projectInputRejectedWatchRoots = new Set();
69
+ projectInputWatchRoots = new Map();
70
+ projectInputWatchers = new Map();
71
+ projectInputLinkWatchers = new Map();
72
+ reloadFiles = new Map();
32
73
  constructor(options, callbacks) {
33
74
  this.options = options;
34
75
  this.callbacks = callbacks;
@@ -36,17 +77,56 @@ class WatchTopology {
36
77
  /** Re-resolve compiler inputs and notify only when their membership changed. */
37
78
  refresh(notify) {
38
79
  const next = resolveWatchTopology(this.options, this.extraInputs);
39
- const changed = mapsEqual(this.files, next.files) === false ||
80
+ const projectInputProgramChange = next.analysisOnly &&
81
+ mapsEqual(this.reloadFiles, next.reloadFiles) &&
82
+ mapsEqual(this.outputFiles, next.outputFiles) &&
83
+ mapsEqual(this.outputs, next.outputs)
84
+ ? projectInputCompilerMembershipChange(this.projectInputs, this.files, next.files)
85
+ : undefined;
86
+ const changed = this.analysisOnly !== next.analysisOnly ||
87
+ mapsEqual(this.files, next.files) === false ||
40
88
  mapsEqual(this.directories, next.directories) === false ||
41
- mapsEqual(this.outputs, next.outputs) === false;
89
+ mapsEqual(this.outputFiles, next.outputFiles) === false ||
90
+ mapsEqual(this.outputs, next.outputs) === false ||
91
+ mapsEqual(this.reloadFiles, next.reloadFiles) === false;
92
+ this.analysisOnly = next.analysisOnly;
42
93
  this.files = next.files;
94
+ // Stamp the tracked set as it is resolved, so the first event that cannot
95
+ // name what changed compares against the state the compiler just saw rather
96
+ // than against nothing, which would make it nominate everything once.
97
+ for (const key of [...this.compilerFileStamps.keys()]) {
98
+ if (!next.files.has(key))
99
+ this.compilerFileStamps.delete(key);
100
+ }
101
+ for (const [key, file] of next.files) {
102
+ // Only a file with no stamp yet is seeded. Restamping one that already
103
+ // has a baseline would advance it past a change nobody reported, and the
104
+ // next unnamed event would then read that change as no change at all.
105
+ if (!this.compilerFileStamps.has(key)) {
106
+ this.compilerFileStamps.set(key, compilerFileStamp(file));
107
+ }
108
+ }
43
109
  this.directories = next.directories;
110
+ this.outputFiles = next.outputFiles;
44
111
  this.outputs = next.outputs;
112
+ this.reloadFiles = next.reloadFiles;
45
113
  this.syncFileWatchers();
46
114
  this.syncDirectoryWatchers();
47
115
  this.syncExtraWatchers();
116
+ this.syncProjectInputWatchers();
48
117
  if (notify && changed) {
49
- this.callbacks.onTopologyChange();
118
+ if (projectInputProgramChange !== undefined) {
119
+ this.callbacks.onInputChange({
120
+ invalidate: true,
121
+ kind: "project",
122
+ path: projectInputProgramChange.length === 1
123
+ ? projectInputProgramChange[0]
124
+ : undefined,
125
+ });
126
+ }
127
+ else {
128
+ this.callbacks.onTopologyChange();
129
+ }
50
130
  }
51
131
  }
52
132
  /** Add Go plugin source trees discovered by the real build lane. */
@@ -57,42 +137,168 @@ class WatchTopology {
57
137
  this.extraInputs = next;
58
138
  this.refresh(false);
59
139
  }
140
+ /**
141
+ * Reconcile project-rule dependencies, retaining absent files and empty glob
142
+ * populations as live topology.
143
+ */
144
+ setProjectInputs(inputs) {
145
+ const next = normalizeProjectInputSnapshot(inputs);
146
+ // The declared spellings are recorded even when the normalized snapshot did
147
+ // not move, because a republication can carry a new alias for identities
148
+ // that already matched, and anchoring the spelling that was retired would
149
+ // leave the live one unwatched.
150
+ this.declaredProjectInputs =
151
+ inputs.declared === undefined
152
+ ? inputs
153
+ : { ...inputs.declared, root: inputs.root };
154
+ if (projectInputSnapshotsEqual(this.projectInputs, next)) {
155
+ this.syncProjectInputWatchers();
156
+ return;
157
+ }
158
+ this.projectInputs = next;
159
+ this.projectInputRejectedWatchRoots.clear();
160
+ const declarations = new Set([
161
+ next,
162
+ inputs,
163
+ { ...(inputs.declared ?? inputs), root: inputs.root },
164
+ ].flatMap((snapshot) => [
165
+ ...snapshot.files.map((file) => projectInputDeclarationKey("file", file)),
166
+ ...snapshot.globs.map((glob) => projectInputDeclarationKey("glob", glob)),
167
+ ...(snapshot.reloadFiles ?? []).map((file) => projectInputDeclarationKey("reload", file)),
168
+ ...(snapshot.reloadDirectories ?? []).map((directory) => projectInputDeclarationKey("reload-directory", directory)),
169
+ ]));
170
+ for (const key of this.projectInputWatchRoots.keys()) {
171
+ if (!declarations.has(key))
172
+ this.projectInputWatchRoots.delete(key);
173
+ }
174
+ this.projectInputMatches = this.collectProjectInputMatches();
175
+ this.projectInputFingerprints = fingerprintProjectInputMatches(this.projectInputMatches);
176
+ this.syncProjectInputWatchers();
177
+ }
60
178
  /** Close every watcher so SIGINT/SIGTERM can drain the event loop. */
61
179
  close() {
180
+ this.closed = true;
62
181
  closeWatchers(this.fileWatchers);
63
182
  closeWatchers(this.directoryWatchers);
64
183
  closeWatchers(this.extraWatchers);
184
+ closeWatchers(this.projectInputWatchers);
185
+ closeWatchers(this.projectInputLinkWatchers);
65
186
  }
66
187
  syncFileWatchers() {
67
- syncWatchers(this.fileWatchers, this.files, (location) => node_fs_1.default.watch(location, { persistent: true }, () => {
68
- this.callbacks.onInputChange();
188
+ const files = process.platform === "win32" ? new Map() : this.files;
189
+ syncWatchers(this.fileWatchers, files, (location) => node_fs_1.default.watch(watcherRegistrationPath(location), { persistent: true }, () => {
190
+ // A per-file watcher fires on any filesystem attention its target
191
+ // receives, and it carries no filename to distinguish an edit from
192
+ // a touch. It answers the same question the unnamed directory event
193
+ // answers, so it answers it the same way: from the bytes.
194
+ if (!this.compilerFileMoved(location))
195
+ return;
196
+ this.callbacks.onInputChange({
197
+ kind: this.classifyCompilerInput(location),
198
+ path: location,
199
+ });
69
200
  }), (location, error) => this.callbacks.onError(location, error));
70
201
  }
202
+ /** Whether a tracked file's bytes differ from the last stamp taken. */
203
+ compilerFileMoved(location) {
204
+ const key = pathKey(location);
205
+ const stamp = compilerFileStamp(location);
206
+ if (this.compilerFileStamps.get(key) === stamp)
207
+ return false;
208
+ this.compilerFileStamps.set(key, stamp);
209
+ return true;
210
+ }
71
211
  syncDirectoryWatchers() {
72
212
  const desired = new Map(this.directories);
73
213
  for (const [key, location] of this.observedDirectories) {
74
214
  if (isDirectory(location) === false ||
75
- this.isCompilerOutputDirectory(location)) {
215
+ this.isCompilerOutputDirectory(location) ||
216
+ this.isProjectInputDirectory(location)) {
76
217
  this.observedDirectories.delete(key);
77
218
  continue;
78
219
  }
79
220
  desired.set(key, location);
80
221
  }
81
- syncWatchers(this.directoryWatchers, desired, (location) => node_fs_1.default.watch(location, { persistent: true }, (_event, filename) => {
222
+ if (process.platform === "win32") {
223
+ for (const [key, location] of desired) {
224
+ if ([...desired].some(([candidateKey, candidate]) => candidateKey !== key && isPathWithin(candidate, location))) {
225
+ desired.delete(key);
226
+ }
227
+ }
228
+ }
229
+ syncWatchers(this.directoryWatchers, desired, (location) => node_fs_1.default.watch(watcherRegistrationPath(location), {
230
+ persistent: true,
231
+ recursive: process.platform === "win32",
232
+ }, (event, filename) => {
82
233
  const changed = filename === null
83
234
  ? undefined
84
235
  : 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)) {
236
+ const pluginInput = changed ?? location;
237
+ if (this.isPluginInput(pluginInput)) {
238
+ this.callbacks.onInputChange({
239
+ kind: "plugin",
240
+ path: pluginInput,
241
+ });
91
242
  return;
92
243
  }
93
- this.refreshFromDirectory(location, changed);
244
+ const plan = planCompilerDirectoryWatchEvent({
245
+ changed,
246
+ event,
247
+ exists: node_fs_1.default.existsSync,
248
+ location,
249
+ platform: process.platform,
250
+ trackedFiles: this.files,
251
+ });
252
+ this.rearmFileWatchers(plan.rearm);
253
+ for (const file of this.compilerChangesToReport(plan.changes, changed, event)) {
254
+ this.callbacks.onInputChange({
255
+ kind: this.classifyCompilerInput(file),
256
+ path: file,
257
+ });
258
+ }
259
+ if (plan.refresh)
260
+ this.refreshFromDirectory(location, changed);
94
261
  }), (location, error) => this.callbacks.onError(location, error));
95
262
  }
263
+ /**
264
+ * Narrow a plan's changes to the tracked files that actually moved.
265
+ *
266
+ * A backend that cannot name what changed forces the plan to nominate every
267
+ * tracked file under the watched directory, which is the only safe answer it
268
+ * can give from an event carrying no filename. macOS delivers such events for
269
+ * ordinary activity elsewhere in the project, so the compiler lane would wake
270
+ * for sources nobody touched. Only a content notification passes through: it
271
+ * is the one event that claims the bytes moved. A rename claims the directory
272
+ * entry was rewritten and an unnamed event claims nothing, so both are
273
+ * decided from the bytes, which is the question neither of them answered.
274
+ */
275
+ compilerChangesToReport(changes, changed, event) {
276
+ // A content notification is taken at its word: the backend is telling us
277
+ // these bytes changed, and second-guessing it would lose an edit that
278
+ // landed inside the clock's resolution. A rename says the directory entry
279
+ // was rewritten, which is a different claim — a file can be moved back, or
280
+ // replaced by an identical copy, without its content moving at all — and an
281
+ // event that cannot name anything makes no claim about content either.
282
+ // Those two are decided from the bytes, and the rearm they drive is
283
+ // unaffected, because rebinding is about the inode and not the content.
284
+ if (changed !== undefined && event !== "rename") {
285
+ for (const file of changes)
286
+ this.recordCompilerFileStamp(file);
287
+ return [...changes];
288
+ }
289
+ return changes.filter((file) => this.compilerFileMoved(file));
290
+ }
291
+ recordCompilerFileStamp(file) {
292
+ this.compilerFileStamps.set(pathKey(file), compilerFileStamp(file));
293
+ }
294
+ rearmFileWatchers(files) {
295
+ for (const file of files) {
296
+ const key = pathKey(file);
297
+ this.fileWatchers.get(key)?.close();
298
+ this.fileWatchers.delete(key);
299
+ }
300
+ this.syncFileWatchers();
301
+ }
96
302
  syncExtraWatchers() {
97
303
  const directories = new Map();
98
304
  for (const input of this.extraInputs) {
@@ -100,31 +306,453 @@ class WatchTopology {
100
306
  directories.set(pathKey(directory), directory);
101
307
  }
102
308
  }
103
- syncWatchers(this.extraWatchers, directories, (location) => node_fs_1.default.watch(location, { persistent: true }, () => {
104
- this.callbacks.onInputChange();
309
+ syncWatchers(this.extraWatchers, directories, (location) => node_fs_1.default.watch(watcherRegistrationPath(location), { persistent: true }, (_event, filename) => {
310
+ const changed = filename === null
311
+ ? undefined
312
+ : node_path_1.default.resolve(location, filename.toString());
313
+ this.callbacks.onInputChange({
314
+ kind: "plugin",
315
+ path: changed ?? location,
316
+ });
317
+ }), (location, error) => this.callbacks.onError(location, error));
318
+ }
319
+ syncProjectInputWatchers() {
320
+ if (this.closed)
321
+ return;
322
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
323
+ const desired = new Map();
324
+ for (const file of this.projectInputDeclarations("file")) {
325
+ if (this.isProjectInputCompilerOutput(file, identities))
326
+ continue;
327
+ const location = this.projectInputWatchRoot("file", file, node_path_1.default.dirname(file));
328
+ if (location !== undefined) {
329
+ this.retainProjectInputWatchRoot(desired, identities, location);
330
+ }
331
+ }
332
+ for (const glob of this.projectInputDeclarations("glob")) {
333
+ const root = literalGlobRoot(glob);
334
+ if (this.isProjectInputCompilerOutputDirectory(root, identities)) {
335
+ continue;
336
+ }
337
+ const location = this.projectInputWatchRoot("glob", glob, root);
338
+ if (location !== undefined) {
339
+ this.retainProjectInputWatchRoot(desired, identities, location);
340
+ }
341
+ }
342
+ for (const file of this.projectInputDeclarations("reload")) {
343
+ if (this.isProjectInputCompilerOutput(file, identities))
344
+ continue;
345
+ const location = this.projectInputWatchRoot("reload", file, node_path_1.default.dirname(file));
346
+ if (location !== undefined) {
347
+ this.retainProjectInputWatchRoot(desired, identities, location);
348
+ }
349
+ }
350
+ for (const directory of this.projectInputDeclarations("reload-directory")) {
351
+ if (this.isProjectInputCompilerOutputDirectory(directory, identities)) {
352
+ continue;
353
+ }
354
+ const location = this.projectInputWatchRoot("reload-directory", directory, directory);
355
+ if (location !== undefined) {
356
+ this.retainProjectInputWatchRoot(desired, identities, location);
357
+ }
358
+ }
359
+ const active = new Map();
360
+ for (const location of projectInputActiveWatchDirectories(desired.values(), identities)) {
361
+ const identity = identities.resolve(location);
362
+ active.set(identity.key, identity.path);
363
+ }
364
+ syncWatchers(this.projectInputWatchers, active, (location) => node_fs_1.default.watch(watcherRegistrationPath(location), { persistent: true, recursive: true }, (_event, filename) => {
365
+ const changed = filename === null
366
+ ? undefined
367
+ : node_path_1.default.resolve(location, filename.toString());
368
+ this.refreshProjectInputs(location, changed);
369
+ }), (location, error) => {
370
+ const key = identities.resolve(location).key;
371
+ const firstFailure = !this.projectInputRejectedWatchRoots.has(key);
372
+ this.projectInputRejectedWatchRoots.add(key);
373
+ this.callbacks.onError(location, error);
374
+ if (firstFailure) {
375
+ queueMicrotask(() => this.syncProjectInputWatchers());
376
+ }
377
+ });
378
+ this.syncProjectInputLinkWatchers(identities);
379
+ this.callbacks.onProjectInputWatchRoots?.([...this.projectInputWatchers.keys()].sort());
380
+ }
381
+ /**
382
+ * Watch the directory that holds a declaration which is itself a link.
383
+ *
384
+ * A recursive watcher cannot report the link being replaced. The backend that
385
+ * keys its handles by path skips an entry it already knows, and the handle it
386
+ * put on the entry followed the link to the target's inode, which unlinking
387
+ * and recreating the link never touches. A plain directory watch has neither
388
+ * property: it reports the entry by name the moment it moves. These are kept
389
+ * apart from the recursive roots because they are not roots — they observe
390
+ * one directory, they are never reported as watch roots, and an ancestor
391
+ * covering them does not make them redundant.
392
+ */
393
+ syncProjectInputLinkWatchers(identities) {
394
+ const desired = new Map();
395
+ for (const kind of ["file", "reload"]) {
396
+ for (const declaration of this.projectInputDeclarations(kind)) {
397
+ const declared = node_path_1.default.resolve(declaration);
398
+ // The test is whether the declaration is itself a link, not whether its
399
+ // spelling is canonical. Comparing against the resolved identity would
400
+ // admit every declaration whose ancestor is aliased — which on macOS is
401
+ // every declaration under the system temporary directory — and it would
402
+ // still miss the retarget, because the watcher goes below the link.
403
+ if (!isSymbolicLink(declared))
404
+ continue;
405
+ if (this.isProjectInputCompilerOutput(declared, identities))
406
+ continue;
407
+ const parent = nearestExistingDirectory(node_path_1.default.dirname(declared));
408
+ if (parent === undefined)
409
+ continue;
410
+ desired.set(identities.resolve(parent).key, parent);
411
+ }
412
+ }
413
+ syncWatchers(this.projectInputLinkWatchers, desired, (location) => node_fs_1.default.watch(watcherRegistrationPath(location), { persistent: true }, (_event, filename) => {
414
+ const changed = filename === null
415
+ ? undefined
416
+ : node_path_1.default.resolve(location, filename.toString());
417
+ this.refreshProjectInputs(location, changed);
105
418
  }), (location, error) => this.callbacks.onError(location, error));
106
419
  }
420
+ /** Drop the watcher that just reported a directory replacement. */
421
+ retireProjectInputWatcher(location, identities) {
422
+ const key = identities.resolve(location).key;
423
+ // A plain directory watcher binds an inode, so a replacement strands it
424
+ // exactly as it strands a recursive root. Both maps are keyed the same way,
425
+ // so both are retired together and the next sync reinstalls whichever the
426
+ // declarations still call for.
427
+ for (const watchers of [
428
+ this.projectInputWatchers,
429
+ this.projectInputLinkWatchers,
430
+ ]) {
431
+ const watcher = watchers.get(key);
432
+ if (watcher === undefined)
433
+ continue;
434
+ watcher.close();
435
+ watchers.delete(key);
436
+ }
437
+ }
438
+ retainProjectInputWatchRoot(desired, identities, location) {
439
+ const available = projectInputAvailableWatchDirectory(location, this.projectInputRejectedWatchRoots, identities, this.projectInputs.root);
440
+ if (available === undefined)
441
+ return;
442
+ const identity = identities.resolve(available);
443
+ desired.set(identity.key, identity.path);
444
+ }
445
+ /**
446
+ * One snapshot holding every spelling of every declaration.
447
+ *
448
+ * Consumers that decide from a population rather than from a single path have
449
+ * to see both, or half of them answer from the file a link pointed at when
450
+ * the snapshot was published while the event they are judging resolved to the
451
+ * file it points at now.
452
+ */
453
+ projectInputPopulation() {
454
+ return {
455
+ files: this.projectInputDeclarations("file"),
456
+ globs: this.projectInputDeclarations("glob"),
457
+ reloadDirectories: this.projectInputDeclarations("reload-directory"),
458
+ reloadFiles: this.projectInputDeclarations("reload"),
459
+ root: this.projectInputs.root,
460
+ };
461
+ }
462
+ /**
463
+ * Every spelling of one declaration that has to be anchored separately.
464
+ *
465
+ * The retained snapshot is normalized to physical identities, which is what
466
+ * every comparison needs but not what every watcher needs: a declaration
467
+ * reached through a symlink resolves to its target's directory, so anchoring
468
+ * the normalized form alone watches the bytes and never the link. Retargeting
469
+ * or replacing the link then goes unobserved, even though it is exactly what
470
+ * decides which bytes the declaration names next. Both spellings are planned
471
+ * through the same root selection, so the project-root hoist and the
472
+ * nearest-existing-ancestor boundary still bound each of them, and the active
473
+ * set drops one again whenever they coincide or share an ancestor.
474
+ */
475
+ projectInputDeclarations(kind) {
476
+ const select = (snapshot) => kind === "file"
477
+ ? snapshot.files
478
+ : kind === "glob"
479
+ ? snapshot.globs
480
+ : kind === "reload"
481
+ ? (snapshot.reloadFiles ?? [])
482
+ : (snapshot.reloadDirectories ?? []);
483
+ const seen = new Set();
484
+ const declarations = [];
485
+ for (const entry of [
486
+ ...select(this.projectInputs),
487
+ ...select(this.declaredProjectInputs),
488
+ ]) {
489
+ const key = (0, projectInputPathIdentity_1.resolveProjectInputPath)(entry);
490
+ if (seen.has(key))
491
+ continue;
492
+ seen.add(key);
493
+ declarations.push(entry);
494
+ }
495
+ return declarations;
496
+ }
497
+ projectInputWatchRoot(kind, declaration, target) {
498
+ const key = projectInputDeclarationKey(kind, declaration);
499
+ const retained = this.projectInputWatchRoots.get(key);
500
+ if (retained !== undefined && isDirectory(retained))
501
+ return retained;
502
+ const resolved = projectInputRecursiveWatchRoot(target, this.projectInputs.root);
503
+ if (resolved !== undefined)
504
+ this.projectInputWatchRoots.set(key, resolved);
505
+ return resolved;
506
+ }
507
+ refreshProjectInputs(location, changed) {
508
+ try {
509
+ const previous = this.projectInputMatches;
510
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
511
+ // One population for the whole decision. Every question below is asked of
512
+ // the same declarations, and rebuilding it per question would both cost
513
+ // more and let two answers disagree about what was declared.
514
+ const population = this.projectInputPopulation();
515
+ const directlyMatched = changed !== undefined &&
516
+ (previous.has(identities.resolve(changed).key) ||
517
+ matchesProjectInput(population, changed, identities));
518
+ const topologyMatched = changed !== undefined &&
519
+ projectInputTopologyMayAffect(population, changed, previous, identities);
520
+ if (changed !== undefined &&
521
+ (this.isProjectInputCompilerOutput(changed, identities) ||
522
+ (directlyMatched === false && topologyMatched === false))) {
523
+ return;
524
+ }
525
+ // Rearm before snapshotting. A watcher that has to be replaced stops
526
+ // delivering the moment it is closed, so a scan taken first would become
527
+ // the baseline for a window in which nothing was watched, and anything
528
+ // written there would never be announced again. Reinstalling first makes
529
+ // the scan below observe whatever the gap swallowed.
530
+ if (changed !== undefined &&
531
+ projectInputReplacementStrandsWatchers(population, changed, identities)) {
532
+ this.retireProjectInputWatcher(location, identities);
533
+ this.syncProjectInputWatchers();
534
+ }
535
+ const next = this.collectProjectInputMatches();
536
+ const membershipChanged = mapsEqual(previous, next) === false;
537
+ const nextFingerprints = changed === undefined ||
538
+ membershipChanged ||
539
+ directlyMatched ||
540
+ topologyMatched
541
+ ? fingerprintProjectInputMatches(next)
542
+ : this.projectInputFingerprints;
543
+ const contentChanged = mapsEqual(this.projectInputFingerprints, nextFingerprints) === false;
544
+ const changedInputs = projectInputChangedPaths({
545
+ next,
546
+ nextFingerprints,
547
+ previous,
548
+ previousFingerprints: this.projectInputFingerprints,
549
+ });
550
+ // Both spellings classify the event. The normalized form names the file a
551
+ // link pointed at when the snapshot was published, so after a retarget it
552
+ // names the wrong one; only the declared form resolves to what the link
553
+ // points at now, which is the selection this lane exists to protect.
554
+ const reload = projectInputReloadEventShouldNotify({
555
+ changed,
556
+ changedInputs,
557
+ globs: population.globs,
558
+ reloadDirectories: population.reloadDirectories ?? [],
559
+ reloadFiles: population.reloadFiles ?? [],
560
+ });
561
+ const invalidate = projectInputMembershipInvalidatesProgram({
562
+ changed,
563
+ changedInputs,
564
+ contentChanged,
565
+ next,
566
+ previous,
567
+ });
568
+ this.projectInputMatches = next;
569
+ this.projectInputFingerprints = nextFingerprints;
570
+ this.syncProjectInputWatchers();
571
+ // A JSON/TS/JS project-input member can simultaneously enter or leave
572
+ // the compiler Program. Reconcile the compiler watch snapshot before
573
+ // scheduling its resident invalidation, so runWatch's post-cycle refresh
574
+ // does not rediscover the same delta as a broader execution reload.
575
+ if (invalidate)
576
+ this.refresh(false);
577
+ if (projectInputEventShouldNotify({
578
+ contentChanged,
579
+ directlyMatched,
580
+ membershipChanged,
581
+ }) &&
582
+ (changed === undefined ||
583
+ this.isProjectInputCompilerOutput(changed, identities) === false)) {
584
+ this.callbacks.onInputChange(reload
585
+ ? { kind: "config", path: changed }
586
+ : {
587
+ ...(invalidate ? { invalidate: true } : {}),
588
+ kind: "project",
589
+ path: changed,
590
+ });
591
+ }
592
+ }
593
+ catch (error) {
594
+ // A rename can invalidate the old filesystem object before the
595
+ // replacement is readable. Rebind ancestor ownership even when the
596
+ // population scan races that transient gap, so a later create cannot be
597
+ // stranded without a watcher.
598
+ this.syncProjectInputWatchers();
599
+ this.callbacks.onError(location, error);
600
+ }
601
+ }
602
+ collectProjectInputMatches() {
603
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
604
+ const matches = new Map();
605
+ // Both spellings are scanned, and each is resolved here rather than when
606
+ // the snapshot arrived. A declaration reached through a symlink otherwise
607
+ // keeps the identity it had when it was published, so retargeting the link
608
+ // moves no key, changes no fingerprint, and the cycle never learns that the
609
+ // bytes it depends on are now a different file.
610
+ for (const file of this.projectInputDeclarations("file")) {
611
+ if (node_fs_1.default.existsSync(file) &&
612
+ this.isProjectInputCompilerOutput(file, identities) === false) {
613
+ const identity = identities.resolve(file);
614
+ matches.set(identity.key, identity.path);
615
+ }
616
+ }
617
+ for (const file of this.projectInputDeclarations("reload")) {
618
+ if (node_fs_1.default.existsSync(file) &&
619
+ this.isProjectInputCompilerOutput(file, identities) === false) {
620
+ const identity = identities.resolve(file);
621
+ matches.set(identity.key, identity.path);
622
+ }
623
+ }
624
+ for (const directory of this.projectInputDeclarations("reload-directory")) {
625
+ if (isDirectory(directory) &&
626
+ this.isProjectInputCompilerOutputDirectory(directory, identities) ===
627
+ false) {
628
+ const identity = identities.resolve(directory);
629
+ matches.set(identity.key, identity.path);
630
+ }
631
+ }
632
+ for (const glob of this.projectInputDeclarations("glob")) {
633
+ const root = literalGlobRoot(glob);
634
+ if (isDirectory(root) === false ||
635
+ this.isProjectInputCompilerOutputDirectory(root, identities)) {
636
+ continue;
637
+ }
638
+ const stack = [root];
639
+ while (stack.length !== 0) {
640
+ const current = stack.pop();
641
+ let entries;
642
+ try {
643
+ entries = node_fs_1.default.readdirSync(current, { withFileTypes: true });
644
+ }
645
+ catch (error) {
646
+ if (isVanishedFilesystemEntry(error))
647
+ continue;
648
+ throw error;
649
+ }
650
+ for (const entry of entries) {
651
+ const location = node_path_1.default.join(current, entry.name);
652
+ if (this.isProjectInputCompilerOutput(location, identities)) {
653
+ continue;
654
+ }
655
+ if (entry.isDirectory()) {
656
+ stack.push(location);
657
+ }
658
+ else if (entry.isFile() &&
659
+ matchesProjectInputGlob(glob, location, identities)) {
660
+ const identity = identities.resolve(location);
661
+ matches.set(identity.key, identity.path);
662
+ }
663
+ }
664
+ }
665
+ }
666
+ return matches;
667
+ }
107
668
  refreshFromDirectory(location, changed) {
108
669
  if (changed !== undefined &&
109
670
  isDirectory(changed) &&
110
- this.isCompilerOutputDirectory(changed) === false) {
671
+ this.isCompilerOutputDirectory(changed) === false &&
672
+ this.isProjectInputDirectory(changed) === false) {
111
673
  this.observedDirectories.set(pathKey(changed), changed);
112
674
  }
113
675
  try {
114
676
  this.refresh(true);
115
677
  }
116
678
  catch (error) {
679
+ for (const reload of reloadInputsForFailedTopologyRefresh(this.reloadFiles.values(), changed)) {
680
+ this.callbacks.onInputChange({ kind: "config", path: reload });
681
+ }
117
682
  this.callbacks.onError(location, error);
118
683
  }
119
684
  }
120
685
  isCompilerOutputDirectory(location) {
121
686
  return [...this.outputs.values()].some((output) => isPathWithin(output, location));
122
687
  }
688
+ isCompilerOutput(location) {
689
+ return (this.outputFiles.has(pathKey(location)) ||
690
+ this.isCompilerOutputDirectory(location));
691
+ }
692
+ isProjectInputCompilerOutputDirectory(location, identities) {
693
+ const root = this.projectInputs.root;
694
+ return [...this.outputs.values()].some((output) => {
695
+ // An output directory that is the project itself, or holds it, is not a
696
+ // place where only build products live -- the sources are there too. A
697
+ // project emitting in place declares exactly that, and honouring it
698
+ // literally would classify every declared input as a product and leave
699
+ // the whole lane unwatched without anything failing to say so. Watching
700
+ // is the safe side here: a product that gets watched costs an extra
701
+ // rebuild, while an input that does not is never seen again.
702
+ if (root !== "" && identities.isWithin(output, root))
703
+ return false;
704
+ return identities.isWithin(output, location);
705
+ });
706
+ }
707
+ isProjectInputCompilerOutput(location, identities) {
708
+ const key = identities.resolve(location).key;
709
+ return ([...this.outputFiles.values()].some((output) => identities.resolve(output).key === key) || this.isProjectInputCompilerOutputDirectory(location, identities));
710
+ }
711
+ isProjectInputDirectory(location) {
712
+ const resolved = node_path_1.default.resolve(location);
713
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
714
+ return (this.projectInputs.files.some((file) => identities.isWithin(resolved, file) ||
715
+ identities.isWithin(node_path_1.default.dirname(file), resolved)) ||
716
+ (this.projectInputs.reloadFiles ?? []).some((file) => identities.isWithin(resolved, file) ||
717
+ identities.isWithin(node_path_1.default.dirname(file), resolved)) ||
718
+ (this.projectInputs.reloadDirectories ?? []).some((directory) => identities.isWithin(resolved, directory) ||
719
+ identities.isWithin(directory, resolved)) ||
720
+ this.projectInputs.globs.some((glob) => {
721
+ const root = literalGlobRoot(glob);
722
+ return (identities.isWithin(root, resolved) ||
723
+ identities.isWithin(resolved, root));
724
+ }));
725
+ }
726
+ classifyCompilerInput(location) {
727
+ if (this.isPluginInput(location))
728
+ return "plugin";
729
+ return this.reloadFiles.has(pathKey(location)) ? "config" : "compiler";
730
+ }
731
+ isPluginInput(location) {
732
+ const resolved = node_path_1.default.resolve(location);
733
+ return this.extraInputs.some((input) => pathKey(input) === pathKey(resolved) || isPathWithin(input, resolved));
734
+ }
123
735
  }
124
736
  exports.WatchTopology = WatchTopology;
737
+ function reloadInputsForFailedTopologyRefresh(reloadFiles, changed) {
738
+ const changedKey = changed === undefined ? undefined : pathKey(changed);
739
+ const reloads = new Map();
740
+ for (const location of reloadFiles) {
741
+ const resolved = node_path_1.default.resolve(location);
742
+ const key = pathKey(resolved);
743
+ if ((changedKey !== undefined && key === changedKey) ||
744
+ node_fs_1.default.existsSync(resolved) === false) {
745
+ reloads.set(key, resolved);
746
+ }
747
+ }
748
+ return [...reloads.values()].sort();
749
+ }
125
750
  function resolveWatchTopology(options, extraInputs) {
751
+ let analysisOnly = options.emit === false;
126
752
  const files = new Map();
753
+ const outputFiles = new Map();
127
754
  const outputs = new Map();
755
+ const reloadFiles = new Map();
128
756
  const roots = [];
129
757
  if (options.files.length !== 0) {
130
758
  const project = (0, readProjectConfig_1.readProjectConfig)({
@@ -132,26 +760,59 @@ function resolveWatchTopology(options, extraInputs) {
132
760
  projectRoot: options.projectRoot,
133
761
  tsconfig: options.tsconfig,
134
762
  });
763
+ analysisOnly = watchTopologyAnalysisOnly(options, project);
135
764
  roots.push(project.root);
136
765
  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)));
766
+ addPaths(reloadFiles, project.configPaths);
767
+ const positionalInputs = options.files.map((file) => node_path_1.default.resolve(options.cwd, file));
768
+ if (positionalInputs.length === 1 &&
769
+ (options.emit ?? project.compilerOptions.noEmit !== true)) {
770
+ addPaths(outputFiles, [
771
+ (0, singleFileOutput_1.resolveSingleFileOutput)({
772
+ cliOutDir: options.outDir,
773
+ cwd: options.cwd,
774
+ file: positionalInputs[0],
775
+ passthrough: options.passthrough,
776
+ tsconfig: options.tsconfig,
777
+ }),
778
+ ]);
779
+ }
780
+ addPaths(files, positionalInputs);
139
781
  }
140
782
  else {
141
- for (const project of readReferencedProjects(options)) {
783
+ const projects = readReferencedProjects(options);
784
+ if (projects[0] !== undefined) {
785
+ analysisOnly = watchTopologyAnalysisOnly(options, projects[0]);
786
+ }
787
+ for (const project of projects) {
142
788
  roots.push(project.root);
143
789
  addPaths(files, project.configPaths);
144
- addPaths(outputs, resolveCompilerOutputs(project, options).directories);
145
- addPaths(files, listCompilerInputs(project, options));
790
+ addPaths(reloadFiles, project.configPaths);
791
+ const compilerInputs = listCompilerInputs(project, options);
792
+ const compilerOutputs = resolveCompilerOutputs(project, options);
793
+ addPaths(outputFiles, compilerOutputs.files);
794
+ addPaths(outputFiles, inferAdjacentCompilerOutputs(project, options, compilerInputs));
795
+ addPaths(outputs, compilerOutputs.directories);
796
+ addPaths(files, compilerInputs);
146
797
  }
147
798
  }
148
799
  addPaths(files, extraInputs);
149
800
  return {
801
+ analysisOnly,
150
802
  directories: collectTopologyDirectories(files.values(), roots),
151
803
  files,
804
+ outputFiles,
152
805
  outputs,
806
+ reloadFiles,
153
807
  };
154
808
  }
809
+ function watchTopologyAnalysisOnly(options, project) {
810
+ if (options.emit !== undefined)
811
+ return options.emit === false;
812
+ const noEmit = passthroughBooleanOption(options.passthrough, "--noEmit") ??
813
+ project.compilerOptions.noEmit === true;
814
+ return noEmit;
815
+ }
155
816
  function readReferencedProjects(options) {
156
817
  const root = (0, readProjectConfig_1.readProjectConfig)({
157
818
  cwd: options.cwd,
@@ -228,19 +889,241 @@ function listCompilerInputs(project, options) {
228
889
  return inputs;
229
890
  }
230
891
  function resolveCompilerOutputs(project, options) {
892
+ const emit = effectiveCompilerEmit(project, options);
893
+ const directories = new Set();
894
+ const files = new Set();
895
+ if (emit.outDir !== undefined &&
896
+ (emit.javascript || (emit.declaration && emit.declarationDir === undefined))) {
897
+ directories.add(emit.outDir);
898
+ }
899
+ if (emit.declaration && emit.declarationDir !== undefined) {
900
+ directories.add(emit.declarationDir);
901
+ }
902
+ if (emit.outFile !== undefined) {
903
+ if (emit.javascript) {
904
+ files.add(emit.outFile);
905
+ if (emit.sourceMap)
906
+ files.add(`${emit.outFile}.map`);
907
+ }
908
+ if (emit.declaration) {
909
+ const declaration = replaceOutputExtension(emit.outFile, ".d.ts");
910
+ files.add(declaration);
911
+ if (emit.declarationMap)
912
+ files.add(`${declaration}.map`);
913
+ }
914
+ }
915
+ if (emit.incremental) {
916
+ files.add(defaultTsBuildInfoFile(project, emit));
917
+ }
918
+ return {
919
+ directories: [...directories],
920
+ files: [...files],
921
+ };
922
+ }
923
+ function inferAdjacentCompilerOutputs(project, options, inputs) {
924
+ const emit = effectiveCompilerEmit(project, options);
925
+ const outputs = new Set();
926
+ for (const input of inputs) {
927
+ const extension = node_path_1.default.extname(input).toLowerCase();
928
+ if (!isCompilerEmittableSourceExtension(extension)) {
929
+ continue;
930
+ }
931
+ if (/\.d\.(?:ts|mts|cts)$/i.test(input))
932
+ continue;
933
+ const stem = input.slice(0, -extension.length);
934
+ if (emit.javascript &&
935
+ emit.outDir === undefined &&
936
+ emit.outFile === undefined &&
937
+ (!isJavaScriptSourceExtension(extension) ||
938
+ (extension === ".jsx" && emit.jsx !== "preserve"))) {
939
+ const javascriptExtension = extension === ".mts"
940
+ ? ".mjs"
941
+ : extension === ".cts"
942
+ ? ".cjs"
943
+ : extension === ".tsx" && emit.jsx === "preserve"
944
+ ? ".jsx"
945
+ : ".js";
946
+ const javascript = stem + javascriptExtension;
947
+ outputs.add(javascript);
948
+ if (emit.sourceMap)
949
+ outputs.add(`${javascript}.map`);
950
+ }
951
+ if (emit.declaration &&
952
+ emit.outDir === undefined &&
953
+ emit.declarationDir === undefined &&
954
+ emit.outFile === undefined) {
955
+ const declarationExtension = extension === ".mts" || extension === ".mjs"
956
+ ? ".d.mts"
957
+ : extension === ".cts" || extension === ".cjs"
958
+ ? ".d.cts"
959
+ : ".d.ts";
960
+ const declaration = stem + declarationExtension;
961
+ outputs.add(declaration);
962
+ if (emit.declarationMap)
963
+ outputs.add(`${declaration}.map`);
964
+ }
965
+ }
966
+ return [...outputs];
967
+ }
968
+ function isCompilerEmittableSourceExtension(extension) {
969
+ return [
970
+ ".cjs",
971
+ ".cts",
972
+ ".js",
973
+ ".jsx",
974
+ ".mjs",
975
+ ".mts",
976
+ ".ts",
977
+ ".tsx",
978
+ ].includes(extension);
979
+ }
980
+ function isJavaScriptSourceExtension(extension) {
981
+ return [".cjs", ".js", ".jsx", ".mjs"].includes(extension);
982
+ }
983
+ function effectiveCompilerEmit(project, options) {
231
984
  const compilerOptions = project.compilerOptions;
232
- const outDir = options.outDir
233
- ? node_path_1.default.resolve(options.cwd, options.outDir)
234
- : compilerOptions.outDir;
985
+ const passthrough = options.passthrough;
986
+ const noEmit = passthroughBooleanOption(passthrough, "--noEmit") ??
987
+ (options.emit === false
988
+ ? true
989
+ : options.emit === true
990
+ ? false
991
+ : compilerOptions.noEmit === true);
992
+ const composite = passthroughBooleanOption(passthrough, "--composite") ??
993
+ compilerOptions.composite === true;
994
+ const incremental = composite ||
995
+ (passthroughBooleanOption(passthrough, "--incremental") ??
996
+ compilerOptions.incremental === true);
997
+ const emitDeclarationOnly = passthroughBooleanOption(passthrough, "--emitDeclarationOnly") ??
998
+ (options.emit === true
999
+ ? false
1000
+ : compilerOptions.emitDeclarationOnly === true);
1001
+ const declaration = !noEmit &&
1002
+ (composite ||
1003
+ (passthroughBooleanOption(passthrough, "--declaration") ??
1004
+ compilerOptions.declaration === true));
1005
+ const javascript = !noEmit && !emitDeclarationOnly;
1006
+ const sourceMap = javascript &&
1007
+ (passthroughBooleanOption(passthrough, "--sourceMap") ??
1008
+ compilerOptions.sourceMap === true) &&
1009
+ !(passthroughBooleanOption(passthrough, "--inlineSourceMap") ??
1010
+ compilerOptions.inlineSourceMap === true);
1011
+ const declarationMap = declaration &&
1012
+ (passthroughBooleanOption(passthrough, "--declarationMap") ??
1013
+ compilerOptions.declarationMap === true);
1014
+ const cliOutDir = passthroughPathOption(passthrough, "--outDir");
1015
+ const cliDeclarationDir = passthroughPathOption(passthrough, "--declarationDir");
1016
+ const cliOutFile = passthroughPathOption(passthrough, "--outFile");
1017
+ const cliRootDir = passthroughPathOption(passthrough, "--rootDir");
1018
+ const cliTsBuildInfoFile = passthroughPathOption(passthrough, "--tsBuildInfoFile");
1019
+ const jsx = passthroughStringOption(passthrough, "--jsx") ?? compilerOptions.jsx;
235
1020
  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)),
1021
+ declaration,
1022
+ declarationDir: cliDeclarationDir === null
1023
+ ? undefined
1024
+ : cliDeclarationDir !== undefined
1025
+ ? node_path_1.default.resolve(options.cwd, cliDeclarationDir)
1026
+ : typeof compilerOptions.declarationDir === "string"
1027
+ ? node_path_1.default.resolve(compilerOptions.declarationDir)
1028
+ : undefined,
1029
+ declarationMap,
1030
+ incremental,
1031
+ javascript,
1032
+ outDir: cliOutDir === null
1033
+ ? undefined
1034
+ : cliOutDir !== undefined
1035
+ ? node_path_1.default.resolve(options.cwd, cliOutDir)
1036
+ : options.outDir !== undefined
1037
+ ? node_path_1.default.resolve(options.cwd, options.outDir)
1038
+ : typeof compilerOptions.outDir === "string"
1039
+ ? node_path_1.default.resolve(compilerOptions.outDir)
1040
+ : undefined,
1041
+ outFile: cliOutFile === null
1042
+ ? undefined
1043
+ : cliOutFile !== undefined
1044
+ ? node_path_1.default.resolve(options.cwd, cliOutFile)
1045
+ : typeof compilerOptions.outFile === "string"
1046
+ ? node_path_1.default.resolve(compilerOptions.outFile)
1047
+ : undefined,
1048
+ rootDir: cliRootDir === null
1049
+ ? undefined
1050
+ : cliRootDir !== undefined
1051
+ ? node_path_1.default.resolve(options.cwd, cliRootDir)
1052
+ : typeof compilerOptions.rootDir === "string"
1053
+ ? node_path_1.default.resolve(compilerOptions.rootDir)
1054
+ : undefined,
1055
+ sourceMap,
1056
+ tsBuildInfoFile: cliTsBuildInfoFile === null
1057
+ ? undefined
1058
+ : cliTsBuildInfoFile !== undefined
1059
+ ? node_path_1.default.resolve(options.cwd, cliTsBuildInfoFile)
1060
+ : typeof compilerOptions.tsBuildInfoFile === "string"
1061
+ ? node_path_1.default.resolve(compilerOptions.tsBuildInfoFile)
1062
+ : undefined,
1063
+ jsx,
242
1064
  };
243
1065
  }
1066
+ function defaultTsBuildInfoFile(project, emit) {
1067
+ if (emit.tsBuildInfoFile !== undefined)
1068
+ return emit.tsBuildInfoFile;
1069
+ const configWithoutExtension = replaceOutputExtension(project.path, "");
1070
+ if (emit.outDir === undefined)
1071
+ return `${configWithoutExtension}.tsbuildinfo`;
1072
+ const relative = emit.rootDir === undefined
1073
+ ? node_path_1.default.basename(configWithoutExtension)
1074
+ : node_path_1.default.relative(emit.rootDir, configWithoutExtension);
1075
+ return node_path_1.default.resolve(emit.outDir, `${relative}.tsbuildinfo`);
1076
+ }
1077
+ function replaceOutputExtension(location, extension) {
1078
+ const current = node_path_1.default.extname(location);
1079
+ return current === ""
1080
+ ? `${location}${extension}`
1081
+ : `${location.slice(0, -current.length)}${extension}`;
1082
+ }
1083
+ function passthroughBooleanOption(tokens, name) {
1084
+ let value;
1085
+ for (let index = 0; index < (tokens?.length ?? 0); index++) {
1086
+ const token = tokens?.[index];
1087
+ if (token === undefined)
1088
+ continue;
1089
+ if (!passthroughOptionMatches(token, name))
1090
+ continue;
1091
+ const next = tokens?.[index + 1];
1092
+ if (next === "true" || next === "false" || next === "null") {
1093
+ value = next === "true";
1094
+ index++;
1095
+ }
1096
+ else {
1097
+ value = true;
1098
+ }
1099
+ }
1100
+ return value;
1101
+ }
1102
+ function passthroughPathOption(tokens, name) {
1103
+ let value;
1104
+ for (let index = 0; index < (tokens?.length ?? 0); index++) {
1105
+ const token = tokens?.[index];
1106
+ if (token === undefined)
1107
+ continue;
1108
+ if (!passthroughOptionMatches(token, name))
1109
+ continue;
1110
+ if (index + 1 < (tokens?.length ?? 0)) {
1111
+ const next = tokens?.[++index];
1112
+ value = next === "null" ? null : next;
1113
+ }
1114
+ }
1115
+ return value;
1116
+ }
1117
+ function passthroughStringOption(tokens, name) {
1118
+ return passthroughPathOption(tokens, name)?.toLowerCase() ?? undefined;
1119
+ }
1120
+ function passthroughOptionMatches(token, name) {
1121
+ if (!token.startsWith("-"))
1122
+ return false;
1123
+ if (token.includes("="))
1124
+ return false;
1125
+ return (0, schema_1.resolveFlagSpec)(token)?.name === (0, schema_1.resolveFlagSpec)(name)?.name;
1126
+ }
244
1127
  function isCompilerOutput(file, outputs) {
245
1128
  return (outputs.files.some((output) => pathKey(output) === pathKey(file)) ||
246
1129
  outputs.directories.some((directory) => isPathWithin(directory, file)));
@@ -278,7 +1161,16 @@ function collectInputDirectories(input) {
278
1161
  while (stack.length !== 0) {
279
1162
  const current = stack.pop();
280
1163
  directories.push(current);
281
- for (const entry of node_fs_1.default.readdirSync(current, { withFileTypes: true })) {
1164
+ let entries;
1165
+ try {
1166
+ entries = node_fs_1.default.readdirSync(current, { withFileTypes: true });
1167
+ }
1168
+ catch (error) {
1169
+ if (isVanishedFilesystemEntry(error))
1170
+ continue;
1171
+ throw error;
1172
+ }
1173
+ for (const entry of entries) {
282
1174
  if (entry.isDirectory()) {
283
1175
  stack.push(node_path_1.default.join(current, entry.name));
284
1176
  }
@@ -286,6 +1178,11 @@ function collectInputDirectories(input) {
286
1178
  }
287
1179
  return directories;
288
1180
  }
1181
+ function isVanishedFilesystemEntry(error) {
1182
+ return (error instanceof Error &&
1183
+ "code" in error &&
1184
+ (error.code === "ENOENT" || error.code === "ENOTDIR"));
1185
+ }
289
1186
  function isDirectory(location) {
290
1187
  try {
291
1188
  return node_fs_1.default.statSync(location).isDirectory();
@@ -295,24 +1192,35 @@ function isDirectory(location) {
295
1192
  }
296
1193
  }
297
1194
  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
- }
1195
+ let complete = true;
304
1196
  for (const [key, location] of desired) {
305
1197
  if (watchers.has(key))
306
1198
  continue;
307
1199
  try {
308
- const watcher = create(location);
309
- watcher.on("error", (error) => onError(location, error));
1200
+ const watcher = create(location, key);
1201
+ watcher.on("error", (error) => {
1202
+ if (watchers.get(key) === watcher) {
1203
+ watchers.delete(key);
1204
+ }
1205
+ watcher.close();
1206
+ onError(location, error);
1207
+ });
310
1208
  watchers.set(key, watcher);
311
1209
  }
312
1210
  catch (error) {
1211
+ complete = false;
313
1212
  onError(location, error);
314
1213
  }
315
1214
  }
1215
+ if (!complete)
1216
+ return false;
1217
+ for (const [key, watcher] of watchers) {
1218
+ if (desired.has(key))
1219
+ continue;
1220
+ watcher.close();
1221
+ watchers.delete(key);
1222
+ }
1223
+ return true;
316
1224
  }
317
1225
  function closeWatchers(watchers) {
318
1226
  for (const watcher of watchers.values())
@@ -335,6 +1243,553 @@ function uniqueExistingPaths(paths) {
335
1243
  }
336
1244
  return [...unique.values()];
337
1245
  }
1246
+ function normalizeProjectInputSnapshot(snapshot) {
1247
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
1248
+ const files = new Map();
1249
+ const globs = new Map();
1250
+ const reloadDirectories = new Map();
1251
+ const reloadFiles = new Map();
1252
+ for (const file of snapshot.files) {
1253
+ const identity = identities.resolve(file);
1254
+ files.set(identity.key, identity.path);
1255
+ }
1256
+ for (const glob of snapshot.globs) {
1257
+ const identity = identities.resolve(glob);
1258
+ globs.set(identity.key, identity.path.split(node_path_1.default.sep).join("/"));
1259
+ }
1260
+ for (const file of snapshot.reloadFiles ?? []) {
1261
+ const identity = identities.resolve(file);
1262
+ reloadFiles.set(identity.key, identity.path);
1263
+ }
1264
+ for (const directory of snapshot.reloadDirectories ?? []) {
1265
+ const identity = identities.resolve(directory);
1266
+ reloadDirectories.set(identity.key, identity.path);
1267
+ }
1268
+ return {
1269
+ files: [...files.values()].sort(),
1270
+ globs: [...globs.values()].sort(),
1271
+ reloadDirectories: [...reloadDirectories.values()].sort(),
1272
+ reloadFiles: [...reloadFiles.values()].sort(),
1273
+ root: identities.resolve(snapshot.root).path,
1274
+ };
1275
+ }
1276
+ function projectInputSnapshotsEqual(left, right) {
1277
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
1278
+ return (
1279
+ // Every sibling comparison in this class decides on identity, and this one
1280
+ // is only safe today because both operands come from the same normalizer.
1281
+ identities.resolve(left.root || ".").key ===
1282
+ identities.resolve(right.root || ".").key &&
1283
+ arraysEqual(left.files, right.files) &&
1284
+ arraysEqual(left.globs, right.globs) &&
1285
+ arraysEqual(left.reloadDirectories ?? [], right.reloadDirectories ?? []) &&
1286
+ arraysEqual(left.reloadFiles ?? [], right.reloadFiles ?? []));
1287
+ }
1288
+ function literalGlobRoot(pattern) {
1289
+ const resolved = node_path_1.default.resolve(pattern);
1290
+ const normalized = resolved.split("\\").join("/");
1291
+ const wildcard = normalized.search(/[*?]/);
1292
+ if (wildcard === -1)
1293
+ return node_path_1.default.dirname(resolved);
1294
+ const separator = normalized.lastIndexOf("/", wildcard);
1295
+ const prefix = separator < 0 ? "." : normalized.slice(0, separator);
1296
+ const volumeRoot = node_path_1.default.parse(resolved).root;
1297
+ const normalizedVolumeRoot = volumeRoot.split("\\").join("/");
1298
+ if (prefix.length === 0 ||
1299
+ prefix === normalizedVolumeRoot.replace(/\/$/, "")) {
1300
+ return volumeRoot;
1301
+ }
1302
+ return node_path_1.default.resolve(prefix);
1303
+ }
1304
+ function projectInputDeclarationKey(kind, declaration) {
1305
+ return `${kind}\0${(0, projectInputPathIdentity_1.resolveProjectInputPath)(declaration)}`;
1306
+ }
1307
+ /**
1308
+ * Chooses the one stable recursive watcher root owned by a project-input
1309
+ * declaration.
1310
+ *
1311
+ * Inputs inside the project share its physical root so directory replacement
1312
+ * cannot strand a child handle. External inputs use the nearest existing
1313
+ * ancestor of their declared parent, which is the explicit boundary for
1314
+ * observing a currently missing external tree without polling every file --
1315
+ * except that the boundary never rises to a directory holding the project,
1316
+ * since such a root outranks the project's own in the active merge and leaves
1317
+ * one handle over a shared system directory to carry everything. An external
1318
+ * declaration that can only be owned that way falls back to its own tree, and
1319
+ * is left unwatched when even that would contain the project.
1320
+ */
1321
+ function projectInputWatchDirectories(target, projectRoot) {
1322
+ const root = projectInputRecursiveWatchRoot(target, projectRoot);
1323
+ return root === undefined ? [] : [root];
1324
+ }
1325
+ /**
1326
+ * Removes recursive roots already covered by an ancestor without rewriting the
1327
+ * declaration-specific roots retained by WatchTopology.
1328
+ */
1329
+ function projectInputActiveWatchDirectories(directories, identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)()) {
1330
+ const unique = new Map();
1331
+ for (const directory of directories) {
1332
+ // Coverage is decided on physical identity so two spellings of one
1333
+ // directory cannot both survive, but the surviving entry keeps the
1334
+ // caller's spelling: this function selects roots, it does not rename them.
1335
+ const identity = identities.resolve(directory);
1336
+ if (!unique.has(identity.key))
1337
+ unique.set(identity.key, directory);
1338
+ }
1339
+ return [...unique]
1340
+ .filter(([key]) => {
1341
+ let ancestor = node_path_1.default.dirname(key);
1342
+ while (ancestor !== key) {
1343
+ if (unique.has(ancestor))
1344
+ return false;
1345
+ const parent = node_path_1.default.dirname(ancestor);
1346
+ if (parent === ancestor)
1347
+ break;
1348
+ ancestor = parent;
1349
+ }
1350
+ return true;
1351
+ })
1352
+ .map(([, directory]) => directory);
1353
+ }
1354
+ function projectInputAvailableWatchDirectory(location, rejected, identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)(), projectRoot) {
1355
+ const resolvedProjectRoot = projectRoot === undefined ? undefined : node_path_1.default.resolve(projectRoot);
1356
+ let current = node_path_1.default.resolve(location);
1357
+ while (true) {
1358
+ const identity = identities.resolve(current);
1359
+ // Escalation obeys the same ceiling root selection does. A watcher that
1360
+ // failed once would otherwise be replaced by one over a directory holding
1361
+ // the project, which outranks the project's own root in the active merge
1362
+ // and re-opens, through the recovery path, the exact swallow that selection
1363
+ // refuses to create. Giving up the failed root is the lesser loss.
1364
+ if (resolvedProjectRoot !== undefined &&
1365
+ identity.key !== identities.resolve(resolvedProjectRoot).key &&
1366
+ identities.isWithin(identity.path, resolvedProjectRoot)) {
1367
+ return undefined;
1368
+ }
1369
+ if (!rejected.has(identity.key))
1370
+ return identity.path;
1371
+ const parent = node_path_1.default.dirname(current);
1372
+ if (parent === current)
1373
+ return undefined;
1374
+ const fallback = nearestExistingDirectory(parent);
1375
+ if (fallback === undefined)
1376
+ return undefined;
1377
+ current = fallback;
1378
+ }
1379
+ }
1380
+ function projectInputRecursiveWatchRoot(target, projectRoot, identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)()) {
1381
+ const resolvedTarget = node_path_1.default.resolve(target);
1382
+ const resolvedProjectRoot = node_path_1.default.resolve(projectRoot);
1383
+ if (identities.isWithin(resolvedProjectRoot, resolvedTarget)) {
1384
+ return nearestExistingDirectory(resolvedProjectRoot);
1385
+ }
1386
+ // An external anchor rises to the declared parent so a tree that does not
1387
+ // exist yet is still observed, and so siblings under it share one handle. It
1388
+ // may not rise past the project, though: a directory that contains the
1389
+ // project swallows the project's own root when the two are merged, and every
1390
+ // in-project declaration then rides one recursive handle over a shared system
1391
+ // directory — a temp root, or the filesystem root itself — which delivers
1392
+ // nothing. Prefer the declared parent, fall back to the target's own tree,
1393
+ // and decline rather than widen past the project.
1394
+ for (const candidate of [
1395
+ nearestExistingDirectory(node_path_1.default.dirname(resolvedTarget)),
1396
+ nearestExistingDirectory(resolvedTarget),
1397
+ ]) {
1398
+ if (candidate === undefined)
1399
+ continue;
1400
+ // The project root cannot outrank itself in the merge, so it is the one
1401
+ // container that is never a swallow — it is the owner the internal branch
1402
+ // would have chosen anyway. A declaration reached through an in-project
1403
+ // directory symlink lands here, and rejecting it would drop the hoist that
1404
+ // keeps a replaced directory from stranding a child handle.
1405
+ if (identities.resolve(candidate).key !==
1406
+ identities.resolve(resolvedProjectRoot).key &&
1407
+ identities.isWithin(candidate, resolvedProjectRoot)) {
1408
+ continue;
1409
+ }
1410
+ return candidate;
1411
+ }
1412
+ return undefined;
1413
+ }
1414
+ /**
1415
+ * A cheap identity for a tracked file's current bytes.
1416
+ *
1417
+ * Modification time and size together answer "did this move" without reading
1418
+ * the file, which is all the unnamed-event path needs; a file that vanished
1419
+ * answers as absent rather than as unchanged.
1420
+ */
1421
+ function compilerFileStamp(location) {
1422
+ try {
1423
+ const stats = node_fs_1.default.statSync(location);
1424
+ return `${stats.mtimeMs}:${stats.size}`;
1425
+ }
1426
+ catch {
1427
+ return "";
1428
+ }
1429
+ }
1430
+ function isSymbolicLink(location) {
1431
+ try {
1432
+ return node_fs_1.default.lstatSync(location).isSymbolicLink();
1433
+ }
1434
+ catch {
1435
+ return false;
1436
+ }
1437
+ }
1438
+ function nearestExistingDirectory(location) {
1439
+ let current = node_path_1.default.resolve(location);
1440
+ while (true) {
1441
+ if (isDirectory(current))
1442
+ return current;
1443
+ const parent = node_path_1.default.dirname(current);
1444
+ if (parent === current)
1445
+ return undefined;
1446
+ current = parent;
1447
+ }
1448
+ }
1449
+ function matchesProjectInput(snapshot, location, identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)()) {
1450
+ const key = identities.resolve(location).key;
1451
+ return (snapshot.files.some((file) => identities.resolve(file).key === key) ||
1452
+ (snapshot.reloadFiles ?? []).some((file) => identities.resolve(file).key === key) ||
1453
+ (snapshot.reloadDirectories ?? []).some((directory) => identities.isWithin(directory, location)) ||
1454
+ snapshot.globs.some((glob) => matchesProjectInputGlob(glob, location, identities)));
1455
+ }
1456
+ /**
1457
+ * Whether a replacement at this path leaves a recursive watcher bound to the
1458
+ * object that was replaced.
1459
+ *
1460
+ * Only one backend needs the answer. Node routes a recursive watch to its own
1461
+ * per-directory implementation when the platform is neither macOS nor Windows,
1462
+ * and that implementation keys its handles by path: the handle for a directory
1463
+ * renamed away stays bound to the object that left, and any child whose name
1464
+ * survives the swap is skipped as already known. The native subtree backends
1465
+ * both other platforms use follow the path, so retiring their watcher would buy
1466
+ * nothing and would open a window in which no events are delivered.
1467
+ *
1468
+ * The answer is deliberately narrower than the rescan rule, because
1469
+ * reinstalling a root costs one watch descriptor per entry beneath it, which an
1470
+ * install storm would pay thousands of times. A directory appearing inside a
1471
+ * glob root deserves a rescan but replaces nothing a root stands on, and a
1472
+ * reload directory anchors the directory that contains it rather than itself,
1473
+ * since its fingerprint is a digest of its own immediate entries and nothing
1474
+ * below it can reach the declared corpus.
1475
+ */
1476
+ function projectInputReplacementStrandsWatchers(snapshot, location, identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)(), platform = process.platform) {
1477
+ if (platform === "darwin" || platform === "win32")
1478
+ return false;
1479
+ const changed = node_path_1.default.resolve(location);
1480
+ if (!isDirectory(changed))
1481
+ return false;
1482
+ return projectInputAnchorsDeclaration(snapshot, node_path_1.default.dirname(changed), identities);
1483
+ }
1484
+ function projectInputAnchorsDeclaration(snapshot, directory, identities) {
1485
+ return (snapshot.files.some((file) => identities.isWithin(directory, file)) ||
1486
+ (snapshot.reloadFiles ?? []).some((file) => identities.isWithin(directory, file)) ||
1487
+ (snapshot.reloadDirectories ?? []).some((entry) =>
1488
+ // A reload directory anchors the directory that contains it, never
1489
+ // itself. Its own fingerprint is a digest of its immediate entries, so
1490
+ // nothing below it can reach the corpus, and treating it as its own
1491
+ // anchor would rearm for every entry created directly inside it —
1492
+ // including `node_modules`, which contributors publish as one.
1493
+ identities.isWithin(directory, entry) &&
1494
+ identities.resolve(directory).key !== identities.resolve(entry).key) ||
1495
+ snapshot.globs.some((glob) => identities.isWithin(directory, literalGlobRoot(glob))));
1496
+ }
1497
+ /**
1498
+ * Decide whether an event that named no declared input can still have moved
1499
+ * one.
1500
+ *
1501
+ * The admitted set is the only bound on how often a watch session re-reads and
1502
+ * re-hashes its declared corpus, and both directions cost: too narrow drops an
1503
+ * atomic replacement, too wide re-fingerprints on every entry an install
1504
+ * creates. Exported so that boundary is pinned directly instead of being
1505
+ * inferred from a rebuild that a silent rescan and a skipped rescan produce
1506
+ * identically.
1507
+ */
1508
+ function projectInputTopologyMayAffect(snapshot, location, previous, identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)()) {
1509
+ const changed = node_path_1.default.resolve(location);
1510
+ const anchors = (directory) => projectInputAnchorsDeclaration(snapshot, directory, identities);
1511
+ // An atomic replacement never names the declared file whose bytes it changed;
1512
+ // it names the directory that was swapped, and that directory can be one the
1513
+ // declaration does not contain — renaming `docs` away reports the arriving
1514
+ // `docs-old`, not `docs`. So a directory event is admitted from where it
1515
+ // happened rather than from what it contains: its own parent must already lie
1516
+ // on the path to a declared input. A tree no declaration reaches, such as
1517
+ // `node_modules` under an ordinary project, then costs nothing per created
1518
+ // entry instead of a population rescan and a full content re-fingerprint. A
1519
+ // glob whose literal root covers that tree still admits every directory
1520
+ // beneath it through the branch below, because a directory appearing inside a
1521
+ // glob root can hold matches; the declaration decides that reach, not this
1522
+ // rule.
1523
+ if (isDirectory(changed) && anchors(node_path_1.default.dirname(changed)))
1524
+ return true;
1525
+ return (anchors(changed) ||
1526
+ (snapshot.reloadDirectories ?? []).some((directory) => identities.isWithin(directory, changed)) ||
1527
+ snapshot.globs.some((glob) => {
1528
+ const root = literalGlobRoot(glob);
1529
+ if (identities.isWithin(changed, root))
1530
+ return true;
1531
+ if (identities.isWithin(root, changed) === false)
1532
+ return false;
1533
+ if (isDirectory(changed))
1534
+ return true;
1535
+ return [...previous.values()].some((input) => identities.isWithin(changed, input));
1536
+ }));
1537
+ }
1538
+ function projectInputEventShouldNotify(input) {
1539
+ return input.contentChanged || input.membershipChanged;
1540
+ }
1541
+ function projectInputChangedPaths(input) {
1542
+ const changed = new Map();
1543
+ const keys = new Set([
1544
+ ...input.previous.keys(),
1545
+ ...input.next.keys(),
1546
+ ...input.previousFingerprints.keys(),
1547
+ ...input.nextFingerprints.keys(),
1548
+ ]);
1549
+ for (const key of keys) {
1550
+ if (input.previous.has(key) === input.next.has(key) &&
1551
+ input.previousFingerprints.get(key) === input.nextFingerprints.get(key)) {
1552
+ continue;
1553
+ }
1554
+ const location = input.next.get(key) ?? input.previous.get(key);
1555
+ if (location !== undefined)
1556
+ changed.set(key, location);
1557
+ }
1558
+ return [...changed.values()];
1559
+ }
1560
+ /**
1561
+ * Classify an exact execution-selection input ahead of ordinary project data.
1562
+ *
1563
+ * `changedInputs` carries fingerprint or membership deltas, so a filename-less
1564
+ * event can still select the cold lane. A named exact event selects the cold
1565
+ * lane only after the surrounding change detector admits the event; unchanged
1566
+ * bytes remain quiet before this classifier is observed.
1567
+ */
1568
+ function projectInputReloadEventShouldNotify(input) {
1569
+ const identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)();
1570
+ const reloadFiles = new Set(input.reloadFiles.map((location) => identities.resolve(location).key));
1571
+ const reloadDirectories = (input.reloadDirectories ?? []).map((location) => identities.resolve(location));
1572
+ // Territory a declared glob is rooted at, judged against the resolution
1573
+ // directory that would otherwise claim it. A project root is published as a
1574
+ // resolution directory because the config lives there, so the directory a
1575
+ // glob is rooted at reads as a new immediate entry the moment it appears --
1576
+ // and appearing is what a declared population does. The data lane already
1577
+ // reports it, with program invalidation when the membership moved, which is
1578
+ // the cold-Program-same-process transition that belongs to data.
1579
+ //
1580
+ // Data can only carve out strictly below a resolution directory. A glob
1581
+ // rooted on that directory, or above it -- `literalGlobRoot` answers with the
1582
+ // volume root for a pattern with no literal prefix -- would otherwise exempt
1583
+ // everything the directory exists to classify, and the selection lane would
1584
+ // retire in silence. Its role predates any glob drawn around it.
1585
+ //
1586
+ // Only glob roots, either way. A declared file sitting directly in a
1587
+ // resolution directory is still a selection surface: its own bytes are what a
1588
+ // project rule reads to decide, once per execution.
1589
+ const globRoots = (input.globs ?? []).map((glob) => identities.resolve(literalGlobRoot(glob)));
1590
+ const exemptedFrom = (directory, location) => globRoots.some((globRoot) => globRoot.key !== directory.key &&
1591
+ identities.isWithin(directory.path, globRoot.path) &&
1592
+ identities.isWithin(globRoot.path, location));
1593
+ // A reload directory is a non-recursive surface: its digest covers its
1594
+ // immediate entries' names, kinds, and link targets, so it moves only when the
1595
+ // directory itself moves or when an entry it holds directly does. Matching the
1596
+ // whole subtree instead makes a resolution ancestor -- the lint config graph
1597
+ // publishes every node_modules level it searched, up to the filesystem root --
1598
+ // read every edit beneath it as a selection change, which restarts the sidecar
1599
+ // on each keystroke and ends warm reuse for any project inside one.
1600
+ const holdsAsImmediateEntry = (location) => {
1601
+ const parent = identities.resolve(node_path_1.default.dirname(location)).key;
1602
+ return reloadDirectories.some((directory) => directory.key === parent && exemptedFrom(directory, location) === false);
1603
+ };
1604
+ const namesDirectory = (location) => {
1605
+ const key = identities.resolve(location).key;
1606
+ return reloadDirectories.some((directory) => directory.key === key);
1607
+ };
1608
+ // A digest delta on the directory itself is the only signal there is when the
1609
+ // entry that appeared is not a declared match, so it cannot simply be dropped
1610
+ // -- but it also cannot say what appeared. The event being classified in the
1611
+ // same pass can: when it names data, the delta it caused is that data's, and
1612
+ // when it names anything else, or nothing at all, the safe reading is that
1613
+ // resolution moved. A missed reselection is a wrong answer; a spare restart is
1614
+ // a slow one.
1615
+ const explains = (directory) => {
1616
+ if (input.changed === undefined)
1617
+ return false;
1618
+ // Only an immediate entry can move this directory's digest, so only an
1619
+ // immediate entry can account for the delta. Asking whether the event was
1620
+ // data somewhere else answers a different directory's question, and one
1621
+ // exemption would then cancel every other directory's evidence.
1622
+ const parent = identities.resolve(node_path_1.default.dirname(input.changed)).key;
1623
+ return parent === directory.key && exemptedFrom(directory, input.changed);
1624
+ };
1625
+ const isReloadDirectoryEvent = (location) => namesDirectory(location) || holdsAsImmediateEntry(location);
1626
+ const isReloadDirectoryDelta = (location) => {
1627
+ if (holdsAsImmediateEntry(location))
1628
+ return true;
1629
+ const key = identities.resolve(location).key;
1630
+ return reloadDirectories.some((directory) => directory.key === key && explains(directory) === false);
1631
+ };
1632
+ return ((input.changed !== undefined &&
1633
+ (reloadFiles.has(identities.resolve(input.changed).key) ||
1634
+ isReloadDirectoryEvent(input.changed))) ||
1635
+ input.changedInputs.some((location) => reloadFiles.has(identities.resolve(location).key) ||
1636
+ isReloadDirectoryDelta(location)));
1637
+ }
1638
+ /**
1639
+ * Return whether a project-input population transition can reshape a Program.
1640
+ *
1641
+ * JSON is data to a ProjectRule but may simultaneously be a `resolveJsonModule`
1642
+ * source. TypeScript and JavaScript paths can likewise overlap a project-input
1643
+ * declaration. Their creation or deletion therefore requires a cold Program
1644
+ * inside the existing resident process. A filename-less event cannot identify
1645
+ * the changed member and is conservatively invalidating whenever the population
1646
+ * moved.
1647
+ */
1648
+ function projectInputMembershipInvalidatesProgram(input) {
1649
+ if (input.contentChanged === true &&
1650
+ (input.changedInputs ??
1651
+ (input.changed === undefined ? [] : [input.changed])).some((location) => node_path_1.default.basename(location).toLowerCase() === "package.json")) {
1652
+ return true;
1653
+ }
1654
+ if (mapsEqual(input.previous, input.next))
1655
+ return false;
1656
+ if (input.changed === undefined)
1657
+ return true;
1658
+ for (const [key, location] of input.previous) {
1659
+ if (input.next.has(key) === false &&
1660
+ projectInputPathMayAffectProgram(location)) {
1661
+ return true;
1662
+ }
1663
+ }
1664
+ for (const [key, location] of input.next) {
1665
+ if (input.previous.has(key) === false &&
1666
+ projectInputPathMayAffectProgram(location)) {
1667
+ return true;
1668
+ }
1669
+ }
1670
+ return false;
1671
+ }
1672
+ function projectInputPathMayAffectProgram(location) {
1673
+ const extension = node_path_1.default.extname(location).toLowerCase();
1674
+ return extension === ".json" || isCompilerEmittableSourceExtension(extension);
1675
+ }
1676
+ function projectInputCompilerMembershipChange(snapshot, previous, next) {
1677
+ const changed = new Map();
1678
+ for (const [key, location] of previous) {
1679
+ if (next.has(key) === false)
1680
+ changed.set(key, location);
1681
+ }
1682
+ for (const [key, location] of next) {
1683
+ if (previous.has(key) === false)
1684
+ changed.set(key, location);
1685
+ }
1686
+ if (changed.size === 0 ||
1687
+ [...changed.values()].some((location) => matchesProjectInput(snapshot, location) === false ||
1688
+ projectInputPathMayAffectProgram(location) === false)) {
1689
+ return undefined;
1690
+ }
1691
+ return [...changed.values()].sort();
1692
+ }
1693
+ function fingerprintProjectInputMatches(matches) {
1694
+ const fingerprints = new Map();
1695
+ for (const [key, location] of matches) {
1696
+ try {
1697
+ fingerprints.set(key, isDirectory(location)
1698
+ ? fingerprintProjectInputDirectory(location)
1699
+ : node_crypto_1.default
1700
+ .createHash("sha256")
1701
+ .update(node_fs_1.default.readFileSync(location))
1702
+ .digest("hex"));
1703
+ }
1704
+ catch {
1705
+ fingerprints.set(key, "");
1706
+ }
1707
+ }
1708
+ return fingerprints;
1709
+ }
1710
+ function fingerprintProjectInputDirectory(location) {
1711
+ const entries = node_fs_1.default
1712
+ .readdirSync(location, { withFileTypes: true })
1713
+ .map((entry) => {
1714
+ const kind = entry.isDirectory()
1715
+ ? "directory"
1716
+ : entry.isFile()
1717
+ ? "file"
1718
+ : entry.isSymbolicLink()
1719
+ ? "symlink"
1720
+ : "other";
1721
+ let target = "";
1722
+ if (entry.isSymbolicLink()) {
1723
+ try {
1724
+ target = node_fs_1.default.readlinkSync(node_path_1.default.join(location, entry.name));
1725
+ }
1726
+ catch {
1727
+ target = "<unreadable>";
1728
+ }
1729
+ }
1730
+ return entry.name + "\0" + kind + "\0" + target;
1731
+ })
1732
+ .sort();
1733
+ return node_crypto_1.default.createHash("sha256").update(entries.join("\0")).digest("hex");
1734
+ }
1735
+ function matchesProjectInputGlob(pattern, location, identities = (0, projectInputPathIdentity_1.createProjectInputPathIdentityContext)()) {
1736
+ const root = identities.resolve(literalGlobRoot(pattern));
1737
+ const candidate = identities.resolve(location);
1738
+ if (!(0, projectInputPathIdentity_1.isProjectInputPathIdentityWithin)(root.key, candidate.key))
1739
+ return false;
1740
+ const sensitive = identities.caseSensitive(root.path);
1741
+ const patternParts = node_path_1.default
1742
+ .relative(root.path, identities.resolve(pattern).path)
1743
+ .split(node_path_1.default.sep);
1744
+ const candidateParts = node_path_1.default
1745
+ .relative(root.path, candidate.path)
1746
+ .split(node_path_1.default.sep);
1747
+ return matchProjectInputGlobParts(sensitive
1748
+ ? patternParts
1749
+ : patternParts.map((segment) => segment.toLowerCase()), sensitive
1750
+ ? candidateParts
1751
+ : candidateParts.map((segment) => segment.toLowerCase()));
1752
+ }
1753
+ function matchProjectInputGlobParts(pattern, candidate) {
1754
+ const memo = new Map();
1755
+ const visit = (patternIndex, candidateIndex) => {
1756
+ const key = `${String(patternIndex)}:${String(candidateIndex)}`;
1757
+ const cached = memo.get(key);
1758
+ if (cached !== undefined)
1759
+ return cached;
1760
+ let matched;
1761
+ if (patternIndex === pattern.length) {
1762
+ matched = candidateIndex === candidate.length;
1763
+ }
1764
+ else if (pattern[patternIndex] === "**") {
1765
+ matched =
1766
+ visit(patternIndex + 1, candidateIndex) ||
1767
+ (candidateIndex !== candidate.length &&
1768
+ visit(patternIndex, candidateIndex + 1));
1769
+ }
1770
+ else {
1771
+ matched =
1772
+ candidateIndex !== candidate.length &&
1773
+ matchProjectInputGlobSegment(pattern[patternIndex], candidate[candidateIndex]) &&
1774
+ visit(patternIndex + 1, candidateIndex + 1);
1775
+ }
1776
+ memo.set(key, matched);
1777
+ return matched;
1778
+ };
1779
+ return visit(0, 0);
1780
+ }
1781
+ function matchProjectInputGlobSegment(pattern, candidate) {
1782
+ let source = "^";
1783
+ for (const char of pattern) {
1784
+ if (char === "*")
1785
+ source += ".*";
1786
+ else if (char === "?")
1787
+ source += ".";
1788
+ else
1789
+ source += char.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
1790
+ }
1791
+ return new RegExp(`${source}$`, "u").test(candidate);
1792
+ }
338
1793
  function mapsEqual(left, right) {
339
1794
  if (left.size !== right.size)
340
1795
  return false;
@@ -350,11 +1805,75 @@ function isPathWithin(root, candidate) {
350
1805
  relative.startsWith(`..${node_path_1.default.sep}`) === false &&
351
1806
  !node_path_1.default.isAbsolute(relative)));
352
1807
  }
1808
+ /**
1809
+ * Plan one compiler-directory event without relying on backend timing.
1810
+ *
1811
+ * POSIX file watchers own ordinary content changes. A named rename re-arms the
1812
+ * replaced file; an unnamed event conservatively re-arms and reports every
1813
+ * surviving tracked input below the watch root. Windows has no per-file
1814
+ * watchers here, so both named and unnamed directory events report inputs.
1815
+ */
1816
+ function planCompilerDirectoryWatchEvent(input) {
1817
+ const candidates = input.changed === undefined
1818
+ ? [...input.trackedFiles.values()].filter((file) => input.exists(file) &&
1819
+ isPathWithin(input.location, node_path_1.default.resolve(file)))
1820
+ : input.trackedFiles.has(pathKeyForPlatform(input.changed, input.platform)) && input.exists(input.changed)
1821
+ ? [input.changed]
1822
+ : [];
1823
+ if (input.changed === undefined) {
1824
+ return {
1825
+ changes: candidates,
1826
+ rearm: input.platform === "win32" ? [] : candidates,
1827
+ refresh: true,
1828
+ };
1829
+ }
1830
+ if (candidates.length === 0) {
1831
+ return { changes: [], rearm: [], refresh: true };
1832
+ }
1833
+ if (input.platform === "win32") {
1834
+ return { changes: candidates, rearm: [], refresh: false };
1835
+ }
1836
+ if (input.event === "rename") {
1837
+ return { changes: candidates, rearm: candidates, refresh: false };
1838
+ }
1839
+ return { changes: [], rearm: [], refresh: false };
1840
+ }
353
1841
  function isRecord(value) {
354
1842
  return typeof value === "object" && value !== null;
355
1843
  }
1844
+ /**
1845
+ * Resolve the spelling a filesystem watcher must be registered under.
1846
+ *
1847
+ * A watch declaration keeps its lexical spelling, because classification,
1848
+ * containment, and notification are all expressed in the caller's own paths.
1849
+ * The backend needs the canonical spelling instead. libuv stores the directory
1850
+ * string it was handed, expands each delivered event that still exists on disk
1851
+ * to its long path, and then requires the stored string to be that expansion's
1852
+ * prefix. A short (8.3) component makes the two disagree, and the resulting
1853
+ * assertion aborts the whole process (libuv issue 5010; upstream now returns an
1854
+ * error instead, but the bundled libuv every supported Node ships still
1855
+ * asserts). `os.tmpdir()` routinely yields such a path, so ordinary projects
1856
+ * reach it, not only tests. macOS matches events against the watched path
1857
+ * resolved through symlinks, so `/var` and `/private/var` must not be mixed
1858
+ * either. Resolving here keeps every backend comparing two canonical spellings
1859
+ * while callers keep resolving events against what they declared.
1860
+ */
1861
+ function watcherRegistrationPath(location) {
1862
+ try {
1863
+ return node_fs_1.default.realpathSync.native?.(location) ?? node_fs_1.default.realpathSync(location);
1864
+ }
1865
+ catch {
1866
+ // A vanished target makes fs.watch throw anyway, which syncWatchers already
1867
+ // routes to the error path; an unreadable ancestor is better watched under
1868
+ // its declared spelling than not watched at all.
1869
+ return location;
1870
+ }
1871
+ }
356
1872
  function pathKey(location) {
1873
+ return pathKeyForPlatform(location, process.platform);
1874
+ }
1875
+ function pathKeyForPlatform(location, platform) {
357
1876
  const resolved = node_path_1.default.resolve(location);
358
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
1877
+ return platform === "win32" ? resolved.toLowerCase() : resolved;
359
1878
  }
360
1879
  //# sourceMappingURL=watchTopology.js.map