ttsc 0.22.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.
- package/go.mod +1 -1
- package/internal/lspserver/lsp_launcher_reload_directory_fingerprint_parity_windows_test.go +94 -0
- package/internal/lspserver/lsp_project_input_glob_path_other.go +16 -0
- package/internal/lspserver/lsp_project_input_glob_path_windows.go +36 -0
- package/internal/lspserver/lsp_project_input_path_key_other.go +17 -0
- package/internal/lspserver/lsp_project_input_path_key_windows.go +228 -0
- package/internal/lspserver/lsp_project_input_path_key_windows_test.go +460 -0
- package/internal/lspserver/lsp_project_inputs.go +117 -49
- package/internal/lspserver/lsp_reload_directories_separate_glob_territory_test.go +181 -0
- package/lib/flags/schema.js +1 -1
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/projectInputPathIdentity.d.ts +13 -5
- package/lib/internal/projectInputPathIdentity.js +95 -41
- package/lib/internal/projectInputPathIdentity.js.map +1 -1
- package/lib/internal/windowsDirectoryCaseSensitivity.d.ts +10 -0
- package/lib/internal/windowsDirectoryCaseSensitivity.js +34 -0
- package/lib/internal/windowsDirectoryCaseSensitivity.js.map +1 -0
- package/lib/launcher/internal/runTtsc.js +5 -0
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.js +59 -8
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +61 -7
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +6 -5
- package/lib/launcher/internal/runtimeHooks.js +7 -15
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/launcher/internal/watchTopology.d.ts +82 -8
- package/lib/launcher/internal/watchTopology.js +535 -187
- package/lib/launcher/internal/watchTopology.js.map +1 -1
- package/lib/plugin/internal/descriptorProcessFailure.d.ts +32 -0
- package/lib/plugin/internal/descriptorProcessFailure.js +73 -0
- package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -0
- package/lib/plugin/internal/loadProjectPlugins.js +29 -8
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/package.json +12 -8
- package/src/flags/schema.ts +2 -1
- package/src/internal/projectInputPathIdentity.ts +150 -46
- package/src/internal/windowsDirectoryCaseSensitivity.ts +33 -0
- package/src/launcher/internal/runTtsc.ts +7 -0
- package/src/launcher/internal/runTtscserver.ts +81 -11
- package/src/launcher/internal/runTtsx.ts +87 -10
- package/src/launcher/internal/runtimeHooks.ts +16 -16
- package/src/launcher/internal/watchTopology.ts +679 -208
- package/src/plugin/internal/descriptorProcessFailure.ts +100 -0
- package/src/plugin/internal/loadProjectPlugins.ts +41 -8
- package/tools/shim_audit/baseline.json +0 -9
|
@@ -36,6 +36,7 @@ type WatchTopologyOptions = Pick<
|
|
|
36
36
|
type WatchTopologyCallbacks = {
|
|
37
37
|
onError(location: string, error: unknown): void;
|
|
38
38
|
onInputChange(change: WatchInputChange): void;
|
|
39
|
+
onProjectInputWatchUnavailable?(roots: readonly string[]): void;
|
|
39
40
|
onProjectInputWatchRoots?(roots: readonly string[]): void;
|
|
40
41
|
onTopologyChange(): void;
|
|
41
42
|
};
|
|
@@ -56,6 +57,16 @@ type ResolvedWatchTopology = {
|
|
|
56
57
|
reloadFiles: Map<string, string>;
|
|
57
58
|
};
|
|
58
59
|
|
|
60
|
+
type CompilerFileSnapshot = {
|
|
61
|
+
content: string;
|
|
62
|
+
owner: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type CompilerFileMovement = {
|
|
66
|
+
content: boolean;
|
|
67
|
+
owner: boolean;
|
|
68
|
+
};
|
|
69
|
+
|
|
59
70
|
export type CompilerDirectoryWatchEventPlan = {
|
|
60
71
|
changes: string[];
|
|
61
72
|
rearm: string[];
|
|
@@ -73,11 +84,14 @@ export type CompilerDirectoryWatchEventPlan = {
|
|
|
73
84
|
export class WatchTopology {
|
|
74
85
|
private analysisOnly = false;
|
|
75
86
|
private closed = false;
|
|
87
|
+
private compilerPostRegistrationMembershipRefresh = false;
|
|
88
|
+
private compilerPostRegistrationReconciliationScheduled = false;
|
|
89
|
+
private compilerPostRegistrationSkipUnobservedProjectInputWatchRoots = true;
|
|
76
90
|
private directories = new Map<string, string>();
|
|
77
91
|
private directoryWatchers = new Map<string, fs.FSWatcher>();
|
|
78
92
|
private extraInputs: readonly string[] = [];
|
|
79
93
|
private extraWatchers = new Map<string, fs.FSWatcher>();
|
|
80
|
-
private
|
|
94
|
+
private compilerFileSnapshots = new Map<string, CompilerFileSnapshot>();
|
|
81
95
|
private files = new Map<string, string>();
|
|
82
96
|
private fileWatchers = new Map<string, fs.FSWatcher>();
|
|
83
97
|
private observedDirectories = new Map<string, string>();
|
|
@@ -99,10 +113,19 @@ export class WatchTopology {
|
|
|
99
113
|
reloadFiles: [],
|
|
100
114
|
root: "",
|
|
101
115
|
};
|
|
116
|
+
private projectInputRecoveryScheduled = false;
|
|
117
|
+
private projectInputPostRegistrationReconciliationScheduled = false;
|
|
102
118
|
private projectInputRejectedWatchRoots = new Set<string>();
|
|
119
|
+
private projectInputRequiredWatchRoots = new Map<string, string>();
|
|
120
|
+
private projectInputUnobservedWatchRoots = new Map<string, string>();
|
|
103
121
|
private projectInputWatchRoots = new Map<string, string>();
|
|
104
122
|
private projectInputWatchers = new Map<string, fs.FSWatcher>();
|
|
105
123
|
private projectInputLinkWatchers = new Map<string, fs.FSWatcher>();
|
|
124
|
+
private projectInputCompilerOutputOverlaps = new WeakMap<
|
|
125
|
+
ProjectInputPathIdentityContext,
|
|
126
|
+
Map<string, boolean>
|
|
127
|
+
>();
|
|
128
|
+
private projectInputCompilerAcknowledgements = new Map<string, string>();
|
|
106
129
|
private reloadFiles = new Map<string, string>();
|
|
107
130
|
|
|
108
131
|
public constructor(
|
|
@@ -112,17 +135,30 @@ export class WatchTopology {
|
|
|
112
135
|
|
|
113
136
|
/** Re-resolve compiler inputs and notify only when their membership changed. */
|
|
114
137
|
public refresh(notify: boolean): void {
|
|
138
|
+
this.refreshCompilerInputs(notify, false);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private refreshCompilerInputs(
|
|
142
|
+
notify: boolean,
|
|
143
|
+
skipUnobservedProjectInputWatchRoots: boolean,
|
|
144
|
+
): void {
|
|
115
145
|
const next = resolveWatchTopology(this.options, this.extraInputs);
|
|
116
|
-
const
|
|
146
|
+
const compilerProgramMembershipChange =
|
|
117
147
|
next.analysisOnly &&
|
|
118
148
|
mapsEqual(this.reloadFiles, next.reloadFiles) &&
|
|
119
149
|
mapsEqual(this.outputFiles, next.outputFiles) &&
|
|
120
150
|
mapsEqual(this.outputs, next.outputs)
|
|
121
|
-
?
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
151
|
+
? compilerMembershipChange(this.files, next.files)
|
|
152
|
+
: [];
|
|
153
|
+
const projectInputProgramOverlap = projectInputCompilerMembershipChange(
|
|
154
|
+
this.projectInputs,
|
|
155
|
+
compilerProgramMembershipChange,
|
|
156
|
+
);
|
|
157
|
+
const projectInputProgramChange =
|
|
158
|
+
compilerProgramMembershipChange.length !== 0 &&
|
|
159
|
+
projectInputProgramOverlap.length ===
|
|
160
|
+
compilerProgramMembershipChange.length
|
|
161
|
+
? projectInputProgramOverlap
|
|
126
162
|
: undefined;
|
|
127
163
|
const changed =
|
|
128
164
|
this.analysisOnly !== next.analysisOnly ||
|
|
@@ -136,41 +172,118 @@ export class WatchTopology {
|
|
|
136
172
|
// Stamp the tracked set as it is resolved, so the first event that cannot
|
|
137
173
|
// name what changed compares against the state the compiler just saw rather
|
|
138
174
|
// than against nothing, which would make it nominate everything once.
|
|
139
|
-
for (const key of [...this.
|
|
140
|
-
if (!next.files.has(key)) this.
|
|
175
|
+
for (const key of [...this.compilerFileSnapshots.keys()]) {
|
|
176
|
+
if (!next.files.has(key)) this.compilerFileSnapshots.delete(key);
|
|
141
177
|
}
|
|
142
178
|
for (const [key, file] of next.files) {
|
|
143
179
|
// Only a file with no stamp yet is seeded. Restamping one that already
|
|
144
180
|
// has a baseline would advance it past a change nobody reported, and the
|
|
145
181
|
// next unnamed event would then read that change as no change at all.
|
|
146
|
-
if (!this.
|
|
147
|
-
this.
|
|
182
|
+
if (!this.compilerFileSnapshots.has(key)) {
|
|
183
|
+
this.compilerFileSnapshots.set(key, compilerFileSnapshot(file));
|
|
148
184
|
}
|
|
149
185
|
}
|
|
150
186
|
this.directories = next.directories;
|
|
151
187
|
this.outputFiles = next.outputFiles;
|
|
152
188
|
this.outputs = next.outputs;
|
|
153
189
|
this.reloadFiles = next.reloadFiles;
|
|
154
|
-
this.
|
|
155
|
-
|
|
190
|
+
for (const key of this.projectInputCompilerAcknowledgements.keys()) {
|
|
191
|
+
if (!next.files.has(key)) {
|
|
192
|
+
this.projectInputCompilerAcknowledgements.delete(key);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const projectInputProgramReload =
|
|
196
|
+
projectInputProgramOverlap.length === 0
|
|
197
|
+
? false
|
|
198
|
+
: this.acknowledgeProjectInputCompilerMembership(
|
|
199
|
+
projectInputProgramOverlap,
|
|
200
|
+
);
|
|
201
|
+
const fileWatcherRegistered = this.syncFileWatchers();
|
|
202
|
+
const directoryWatcherRegistered = this.syncDirectoryWatchers();
|
|
156
203
|
this.syncExtraWatchers();
|
|
157
|
-
this.syncProjectInputWatchers();
|
|
204
|
+
this.syncProjectInputWatchers(skipUnobservedProjectInputWatchRoots);
|
|
205
|
+
if (fileWatcherRegistered || directoryWatcherRegistered) {
|
|
206
|
+
this.scheduleCompilerPostRegistrationReconciliation(
|
|
207
|
+
directoryWatcherRegistered,
|
|
208
|
+
skipUnobservedProjectInputWatchRoots,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
158
211
|
if (notify && changed) {
|
|
159
212
|
if (projectInputProgramChange !== undefined) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
213
|
+
const changedPath =
|
|
214
|
+
projectInputProgramChange.length === 1
|
|
215
|
+
? projectInputProgramChange[0]
|
|
216
|
+
: undefined;
|
|
217
|
+
this.callbacks.onInputChange(
|
|
218
|
+
projectInputProgramReload
|
|
219
|
+
? { kind: "config", path: changedPath }
|
|
220
|
+
: {
|
|
221
|
+
invalidate: true,
|
|
222
|
+
kind: "project",
|
|
223
|
+
path: changedPath,
|
|
224
|
+
},
|
|
225
|
+
);
|
|
168
226
|
} else {
|
|
169
227
|
this.callbacks.onTopologyChange();
|
|
170
228
|
}
|
|
171
229
|
}
|
|
172
230
|
}
|
|
173
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Hand one Program-membership transition from the compiler lane to the
|
|
234
|
+
* overlapping project-input lane.
|
|
235
|
+
*
|
|
236
|
+
* Windows can deliver the compiler membership refresh before the recursive
|
|
237
|
+
* project watcher names the same creation. The rebuild scheduled here already
|
|
238
|
+
* consumes the current project bytes, so publishing their strong fingerprints
|
|
239
|
+
* keeps the later parent event from rediscovering the same population delta.
|
|
240
|
+
* A newly tracked compiler file also remembers that fingerprint until its
|
|
241
|
+
* first named content delivery; identical bytes are the delayed creation,
|
|
242
|
+
* while different bytes are a real later edit and remain observable even
|
|
243
|
+
* inside filesystem timestamp resolution.
|
|
244
|
+
*/
|
|
245
|
+
private acknowledgeProjectInputCompilerMembership(
|
|
246
|
+
changed: readonly string[],
|
|
247
|
+
): boolean {
|
|
248
|
+
const matches = this.collectProjectInputMatches();
|
|
249
|
+
const fingerprints = fingerprintProjectInputMatches(matches);
|
|
250
|
+
const identities = createProjectInputPathIdentityContext();
|
|
251
|
+
const changedInputs = projectInputChangedPaths({
|
|
252
|
+
next: matches,
|
|
253
|
+
nextFingerprints: fingerprints,
|
|
254
|
+
previous: this.projectInputMatches,
|
|
255
|
+
previousFingerprints: this.projectInputFingerprints,
|
|
256
|
+
});
|
|
257
|
+
const population = this.projectInputPopulation();
|
|
258
|
+
const causedBy = projectInputCompilerMembershipProjectChanges(
|
|
259
|
+
changed,
|
|
260
|
+
population.globs,
|
|
261
|
+
);
|
|
262
|
+
const reload = projectInputReloadEventShouldNotify({
|
|
263
|
+
causedBy,
|
|
264
|
+
changed: causedBy.length === 1 ? causedBy[0] : undefined,
|
|
265
|
+
changedInputs,
|
|
266
|
+
globs: population.globs,
|
|
267
|
+
reloadDirectories: population.reloadDirectories,
|
|
268
|
+
reloadFiles: population.reloadFiles ?? [],
|
|
269
|
+
});
|
|
270
|
+
// The callback below consumes the complete population observed by this
|
|
271
|
+
// scan. Crucially, reload classification runs against the old baseline
|
|
272
|
+
// first, so a concurrent selection delta becomes one cold transition
|
|
273
|
+
// instead of disappearing behind the warm compiler-membership handoff.
|
|
274
|
+
this.projectInputMatches = matches;
|
|
275
|
+
this.projectInputFingerprints = fingerprints;
|
|
276
|
+
for (const location of changed) {
|
|
277
|
+
const compilerKey = pathKey(location);
|
|
278
|
+
if (!this.files.has(compilerKey)) continue;
|
|
279
|
+
const fingerprint = fingerprints.get(identities.resolve(location).key);
|
|
280
|
+
if (fingerprint !== undefined && fingerprint !== "") {
|
|
281
|
+
this.projectInputCompilerAcknowledgements.set(compilerKey, fingerprint);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return reload;
|
|
285
|
+
}
|
|
286
|
+
|
|
174
287
|
/** Add Go plugin source trees discovered by the real build lane. */
|
|
175
288
|
public setExtraInputs(inputs: readonly string[]): void {
|
|
176
289
|
const next = uniqueExistingPaths(inputs);
|
|
@@ -199,29 +312,11 @@ export class WatchTopology {
|
|
|
199
312
|
}
|
|
200
313
|
this.projectInputs = next;
|
|
201
314
|
this.projectInputRejectedWatchRoots.clear();
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
].flatMap((snapshot) => [
|
|
208
|
-
...snapshot.files.map((file) =>
|
|
209
|
-
projectInputDeclarationKey("file", file),
|
|
210
|
-
),
|
|
211
|
-
...snapshot.globs.map((glob) =>
|
|
212
|
-
projectInputDeclarationKey("glob", glob),
|
|
213
|
-
),
|
|
214
|
-
...(snapshot.reloadFiles ?? []).map((file) =>
|
|
215
|
-
projectInputDeclarationKey("reload", file),
|
|
216
|
-
),
|
|
217
|
-
...(snapshot.reloadDirectories ?? []).map((directory) =>
|
|
218
|
-
projectInputDeclarationKey("reload-directory", directory),
|
|
219
|
-
),
|
|
220
|
-
]),
|
|
221
|
-
);
|
|
222
|
-
for (const key of this.projectInputWatchRoots.keys()) {
|
|
223
|
-
if (!declarations.has(key)) this.projectInputWatchRoots.delete(key);
|
|
224
|
-
}
|
|
315
|
+
this.pruneProjectInputWatchRoots([
|
|
316
|
+
next,
|
|
317
|
+
inputs,
|
|
318
|
+
{ ...(inputs.declared ?? inputs), root: inputs.root },
|
|
319
|
+
]);
|
|
225
320
|
this.projectInputMatches = this.collectProjectInputMatches();
|
|
226
321
|
this.projectInputFingerprints = fingerprintProjectInputMatches(
|
|
227
322
|
this.projectInputMatches,
|
|
@@ -239,9 +334,16 @@ export class WatchTopology {
|
|
|
239
334
|
closeWatchers(this.projectInputLinkWatchers);
|
|
240
335
|
}
|
|
241
336
|
|
|
242
|
-
private syncFileWatchers():
|
|
337
|
+
private syncFileWatchers(skipMissing = false): boolean {
|
|
338
|
+
const previous = new Map(this.fileWatchers);
|
|
243
339
|
const files =
|
|
244
|
-
process.platform === "win32"
|
|
340
|
+
process.platform === "win32"
|
|
341
|
+
? new Map<string, string>()
|
|
342
|
+
: skipMissing
|
|
343
|
+
? new Map(
|
|
344
|
+
[...this.files].filter(([, location]) => fs.existsSync(location)),
|
|
345
|
+
)
|
|
346
|
+
: this.files;
|
|
245
347
|
syncWatchers(
|
|
246
348
|
this.fileWatchers,
|
|
247
349
|
files,
|
|
@@ -254,7 +356,9 @@ export class WatchTopology {
|
|
|
254
356
|
// receives, and it carries no filename to distinguish an edit from
|
|
255
357
|
// a touch. It answers the same question the unnamed directory event
|
|
256
358
|
// answers, so it answers it the same way: from the bytes.
|
|
257
|
-
|
|
359
|
+
const movement = this.compilerFileMovement(location);
|
|
360
|
+
if (movement.owner) this.rearmFileWatchers([location], true);
|
|
361
|
+
if (!movement.content) return;
|
|
258
362
|
this.callbacks.onInputChange({
|
|
259
363
|
kind: this.classifyCompilerInput(location),
|
|
260
364
|
path: location,
|
|
@@ -262,19 +366,27 @@ export class WatchTopology {
|
|
|
262
366
|
},
|
|
263
367
|
),
|
|
264
368
|
(location, error) => this.callbacks.onError(location, error),
|
|
369
|
+
() => this.closed === false,
|
|
370
|
+
);
|
|
371
|
+
return [...this.fileWatchers].some(
|
|
372
|
+
([key, watcher]) => previous.get(key) !== watcher,
|
|
265
373
|
);
|
|
266
374
|
}
|
|
267
375
|
|
|
268
|
-
/**
|
|
269
|
-
private
|
|
376
|
+
/** Compare a tracked file's content and physical owner with its snapshot. */
|
|
377
|
+
private compilerFileMovement(location: string): CompilerFileMovement {
|
|
270
378
|
const key = pathKey(location);
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
this.
|
|
274
|
-
return
|
|
379
|
+
const previous = this.compilerFileSnapshots.get(key);
|
|
380
|
+
const next = compilerFileSnapshot(location);
|
|
381
|
+
this.compilerFileSnapshots.set(key, next);
|
|
382
|
+
return {
|
|
383
|
+
content: previous?.content !== next.content,
|
|
384
|
+
owner: previous?.owner !== next.owner,
|
|
385
|
+
};
|
|
275
386
|
}
|
|
276
387
|
|
|
277
|
-
private syncDirectoryWatchers():
|
|
388
|
+
private syncDirectoryWatchers(): boolean {
|
|
389
|
+
const previous = new Map(this.directoryWatchers);
|
|
278
390
|
const desired = new Map(this.directories);
|
|
279
391
|
for (const [key, location] of this.observedDirectories) {
|
|
280
392
|
if (
|
|
@@ -345,9 +457,89 @@ export class WatchTopology {
|
|
|
345
457
|
},
|
|
346
458
|
),
|
|
347
459
|
(location, error) => this.callbacks.onError(location, error),
|
|
460
|
+
() => this.closed === false,
|
|
461
|
+
);
|
|
462
|
+
return [...this.directoryWatchers].some(
|
|
463
|
+
([key, watcher]) => previous.get(key) !== watcher,
|
|
348
464
|
);
|
|
349
465
|
}
|
|
350
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Reconcile tracked compiler files after a newly registered watcher returns.
|
|
469
|
+
*
|
|
470
|
+
* A file or directory watcher can be returned before its backend is ready to
|
|
471
|
+
* deliver the first event. The compiler-file stamps were captured before
|
|
472
|
+
* registration, so one coalesced microtask can recover a change in that
|
|
473
|
+
* handoff window. A real event updates the same stamp first and makes this
|
|
474
|
+
* bounded scan a no-op.
|
|
475
|
+
*/
|
|
476
|
+
private scheduleCompilerPostRegistrationReconciliation(
|
|
477
|
+
refreshMembership: boolean,
|
|
478
|
+
skipUnobservedProjectInputWatchRoots: boolean,
|
|
479
|
+
): void {
|
|
480
|
+
if (this.closed) return;
|
|
481
|
+
if (refreshMembership) {
|
|
482
|
+
this.compilerPostRegistrationMembershipRefresh = true;
|
|
483
|
+
this.compilerPostRegistrationSkipUnobservedProjectInputWatchRoots =
|
|
484
|
+
this.compilerPostRegistrationSkipUnobservedProjectInputWatchRoots &&
|
|
485
|
+
skipUnobservedProjectInputWatchRoots;
|
|
486
|
+
}
|
|
487
|
+
if (this.compilerPostRegistrationReconciliationScheduled) return;
|
|
488
|
+
this.compilerPostRegistrationReconciliationScheduled = true;
|
|
489
|
+
queueMicrotask(() => {
|
|
490
|
+
this.compilerPostRegistrationReconciliationScheduled = false;
|
|
491
|
+
if (this.closed) return;
|
|
492
|
+
const refreshCompilerMembership =
|
|
493
|
+
this.compilerPostRegistrationMembershipRefresh;
|
|
494
|
+
const skipUnobservedProjectInputWatchRoots =
|
|
495
|
+
this.compilerPostRegistrationSkipUnobservedProjectInputWatchRoots;
|
|
496
|
+
this.compilerPostRegistrationMembershipRefresh = false;
|
|
497
|
+
this.compilerPostRegistrationSkipUnobservedProjectInputWatchRoots = true;
|
|
498
|
+
|
|
499
|
+
const changed: string[] = [];
|
|
500
|
+
const rearm: string[] = [];
|
|
501
|
+
for (const file of this.files.values()) {
|
|
502
|
+
const movement = this.compilerFileMovement(file);
|
|
503
|
+
if (movement.content) changed.push(file);
|
|
504
|
+
if (movement.owner) rearm.push(file);
|
|
505
|
+
}
|
|
506
|
+
// A replacement can move the path to a new inode without changing its
|
|
507
|
+
// cheap content stamp or topology key. Rebind its physical owner without
|
|
508
|
+
// inventing a content notification. Missing entries remain covered by
|
|
509
|
+
// their parent directory and are retried when recreation is observed.
|
|
510
|
+
this.rearmFileWatchers(rearm, true);
|
|
511
|
+
for (const file of changed) {
|
|
512
|
+
this.callbacks.onInputChange({
|
|
513
|
+
kind: this.classifyCompilerInput(file),
|
|
514
|
+
path: file,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
if (!refreshCompilerMembership) return;
|
|
518
|
+
try {
|
|
519
|
+
// Directory watchers own files not present in the current Program.
|
|
520
|
+
// Re-resolve even when every tracked stamp is unchanged so a swallowed
|
|
521
|
+
// startup event cannot strand a newly included source.
|
|
522
|
+
this.refreshCompilerInputs(true, skipUnobservedProjectInputWatchRoots);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
const reported = new Set(changed.map(pathKey));
|
|
525
|
+
const reconciledChange = changed.length === 1 ? changed[0]! : undefined;
|
|
526
|
+
for (const reload of reloadInputsForFailedTopologyRefresh(
|
|
527
|
+
this.reloadFiles.values(),
|
|
528
|
+
reconciledChange,
|
|
529
|
+
)) {
|
|
530
|
+
if (reported.has(pathKey(reload))) continue;
|
|
531
|
+
this.callbacks.onInputChange({ kind: "config", path: reload });
|
|
532
|
+
}
|
|
533
|
+
this.callbacks.onError(
|
|
534
|
+
reconciledChange === undefined
|
|
535
|
+
? (this.options.projectRoot ?? this.options.cwd)
|
|
536
|
+
: path.dirname(reconciledChange),
|
|
537
|
+
error,
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
351
543
|
/**
|
|
352
544
|
* Narrow a plan's changes to the tracked files that actually moved.
|
|
353
545
|
*
|
|
@@ -374,23 +566,36 @@ export class WatchTopology {
|
|
|
374
566
|
// Those two are decided from the bytes, and the rearm they drive is
|
|
375
567
|
// unaffected, because rebinding is about the inode and not the content.
|
|
376
568
|
if (changed !== undefined && event !== "rename") {
|
|
377
|
-
|
|
378
|
-
|
|
569
|
+
return changes.filter((file) => {
|
|
570
|
+
const key = pathKey(file);
|
|
571
|
+
const acknowledged = this.projectInputCompilerAcknowledgements.get(key);
|
|
572
|
+
this.projectInputCompilerAcknowledgements.delete(key);
|
|
573
|
+
this.recordCompilerFileSnapshot(file);
|
|
574
|
+
return (
|
|
575
|
+
acknowledged === undefined ||
|
|
576
|
+
acknowledged !== fingerprintProjectInputFile(file)
|
|
577
|
+
);
|
|
578
|
+
});
|
|
379
579
|
}
|
|
380
|
-
return changes.filter((file) => this.
|
|
580
|
+
return changes.filter((file) => this.compilerFileMovement(file).content);
|
|
381
581
|
}
|
|
382
582
|
|
|
383
|
-
private
|
|
384
|
-
this.
|
|
583
|
+
private recordCompilerFileSnapshot(file: string): void {
|
|
584
|
+
this.compilerFileSnapshots.set(pathKey(file), compilerFileSnapshot(file));
|
|
385
585
|
}
|
|
386
586
|
|
|
387
|
-
private rearmFileWatchers(
|
|
587
|
+
private rearmFileWatchers(
|
|
588
|
+
files: readonly string[],
|
|
589
|
+
skipMissing = false,
|
|
590
|
+
): void {
|
|
388
591
|
for (const file of files) {
|
|
389
592
|
const key = pathKey(file);
|
|
390
593
|
this.fileWatchers.get(key)?.close();
|
|
391
594
|
this.fileWatchers.delete(key);
|
|
392
595
|
}
|
|
393
|
-
this.syncFileWatchers()
|
|
596
|
+
if (this.syncFileWatchers(skipMissing)) {
|
|
597
|
+
this.scheduleCompilerPostRegistrationReconciliation(false, true);
|
|
598
|
+
}
|
|
394
599
|
}
|
|
395
600
|
|
|
396
601
|
private syncExtraWatchers(): void {
|
|
@@ -419,13 +624,19 @@ export class WatchTopology {
|
|
|
419
624
|
},
|
|
420
625
|
),
|
|
421
626
|
(location, error) => this.callbacks.onError(location, error),
|
|
627
|
+
() => this.closed === false,
|
|
422
628
|
);
|
|
423
629
|
}
|
|
424
630
|
|
|
425
|
-
private syncProjectInputWatchers(
|
|
631
|
+
private syncProjectInputWatchers(
|
|
632
|
+
skipUnobservedProjectInputWatchRoots = false,
|
|
633
|
+
): void {
|
|
426
634
|
if (this.closed) return;
|
|
635
|
+
const previous = new Map(this.projectInputWatchers);
|
|
636
|
+
const previousLinks = new Map(this.projectInputLinkWatchers);
|
|
427
637
|
const identities = createProjectInputPathIdentityContext();
|
|
428
638
|
const desired = new Map<string, string>();
|
|
639
|
+
const required = new Map<string, string>();
|
|
429
640
|
for (const file of this.projectInputDeclarations("file")) {
|
|
430
641
|
if (this.isProjectInputCompilerOutput(file, identities)) continue;
|
|
431
642
|
const location = this.projectInputWatchRoot(
|
|
@@ -433,9 +644,14 @@ export class WatchTopology {
|
|
|
433
644
|
file,
|
|
434
645
|
path.dirname(file),
|
|
435
646
|
);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
647
|
+
this.retainProjectInputWatchRoot(
|
|
648
|
+
required,
|
|
649
|
+
desired,
|
|
650
|
+
identities,
|
|
651
|
+
location,
|
|
652
|
+
path.dirname(file),
|
|
653
|
+
skipUnobservedProjectInputWatchRoots,
|
|
654
|
+
);
|
|
439
655
|
}
|
|
440
656
|
for (const glob of this.projectInputDeclarations("glob")) {
|
|
441
657
|
const root = literalGlobRoot(glob);
|
|
@@ -443,9 +659,14 @@ export class WatchTopology {
|
|
|
443
659
|
continue;
|
|
444
660
|
}
|
|
445
661
|
const location = this.projectInputWatchRoot("glob", glob, root);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
662
|
+
this.retainProjectInputWatchRoot(
|
|
663
|
+
required,
|
|
664
|
+
desired,
|
|
665
|
+
identities,
|
|
666
|
+
location,
|
|
667
|
+
root,
|
|
668
|
+
skipUnobservedProjectInputWatchRoots,
|
|
669
|
+
);
|
|
449
670
|
}
|
|
450
671
|
for (const file of this.projectInputDeclarations("reload")) {
|
|
451
672
|
if (this.isProjectInputCompilerOutput(file, identities)) continue;
|
|
@@ -454,9 +675,14 @@ export class WatchTopology {
|
|
|
454
675
|
file,
|
|
455
676
|
path.dirname(file),
|
|
456
677
|
);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
678
|
+
this.retainProjectInputWatchRoot(
|
|
679
|
+
required,
|
|
680
|
+
desired,
|
|
681
|
+
identities,
|
|
682
|
+
location,
|
|
683
|
+
path.dirname(file),
|
|
684
|
+
skipUnobservedProjectInputWatchRoots,
|
|
685
|
+
);
|
|
460
686
|
}
|
|
461
687
|
for (const directory of this.projectInputDeclarations("reload-directory")) {
|
|
462
688
|
if (this.isProjectInputCompilerOutputDirectory(directory, identities)) {
|
|
@@ -467,9 +693,14 @@ export class WatchTopology {
|
|
|
467
693
|
directory,
|
|
468
694
|
directory,
|
|
469
695
|
);
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
696
|
+
this.retainProjectInputWatchRoot(
|
|
697
|
+
required,
|
|
698
|
+
desired,
|
|
699
|
+
identities,
|
|
700
|
+
location,
|
|
701
|
+
directory,
|
|
702
|
+
skipUnobservedProjectInputWatchRoots,
|
|
703
|
+
);
|
|
473
704
|
}
|
|
474
705
|
const active = new Map<string, string>();
|
|
475
706
|
for (const location of projectInputActiveWatchDirectories(
|
|
@@ -479,6 +710,7 @@ export class WatchTopology {
|
|
|
479
710
|
const identity = identities.resolve(location);
|
|
480
711
|
active.set(identity.key, identity.path);
|
|
481
712
|
}
|
|
713
|
+
this.projectInputRequiredWatchRoots = required;
|
|
482
714
|
syncWatchers(
|
|
483
715
|
this.projectInputWatchers,
|
|
484
716
|
active,
|
|
@@ -499,14 +731,31 @@ export class WatchTopology {
|
|
|
499
731
|
const firstFailure = !this.projectInputRejectedWatchRoots.has(key);
|
|
500
732
|
this.projectInputRejectedWatchRoots.add(key);
|
|
501
733
|
this.callbacks.onError(location, error);
|
|
502
|
-
if (firstFailure) {
|
|
503
|
-
|
|
734
|
+
if (firstFailure && !this.closed) {
|
|
735
|
+
this.scheduleProjectInputWatcherRecovery();
|
|
504
736
|
}
|
|
505
737
|
},
|
|
738
|
+
() => this.closed === false,
|
|
506
739
|
);
|
|
740
|
+
if (this.closed) return;
|
|
741
|
+
if (!this.projectInputRecoveryScheduled) {
|
|
742
|
+
this.reportUnobservedProjectInputWatchRoots(required);
|
|
743
|
+
}
|
|
507
744
|
this.syncProjectInputLinkWatchers(identities);
|
|
745
|
+
const watcherRegistered =
|
|
746
|
+
[...this.projectInputWatchers].some(
|
|
747
|
+
([key, watcher]) => previous.get(key) !== watcher,
|
|
748
|
+
) ||
|
|
749
|
+
[...this.projectInputLinkWatchers].some(
|
|
750
|
+
([key, watcher]) => previousLinks.get(key) !== watcher,
|
|
751
|
+
);
|
|
752
|
+
if (watcherRegistered) {
|
|
753
|
+
this.scheduleProjectInputPostRegistrationReconciliation();
|
|
754
|
+
}
|
|
508
755
|
this.callbacks.onProjectInputWatchRoots?.(
|
|
509
|
-
[...this.projectInputWatchers.keys()]
|
|
756
|
+
[...this.projectInputWatchers.keys()]
|
|
757
|
+
.map((key) => active.get(key) ?? identities.resolve(key).path)
|
|
758
|
+
.sort(),
|
|
510
759
|
);
|
|
511
760
|
}
|
|
512
761
|
|
|
@@ -557,6 +806,7 @@ export class WatchTopology {
|
|
|
557
806
|
},
|
|
558
807
|
),
|
|
559
808
|
(location, error) => this.callbacks.onError(location, error),
|
|
809
|
+
() => this.closed === false,
|
|
560
810
|
);
|
|
561
811
|
}
|
|
562
812
|
|
|
@@ -582,10 +832,31 @@ export class WatchTopology {
|
|
|
582
832
|
}
|
|
583
833
|
|
|
584
834
|
private retainProjectInputWatchRoot(
|
|
835
|
+
required: Map<string, string>,
|
|
585
836
|
desired: Map<string, string>,
|
|
586
837
|
identities: ProjectInputPathIdentityContext,
|
|
587
|
-
location: string,
|
|
838
|
+
location: string | undefined,
|
|
839
|
+
target: string,
|
|
840
|
+
skipUnobservedProjectInputWatchRoots: boolean,
|
|
588
841
|
): void {
|
|
842
|
+
const requiredIdentity = identities.resolve(location ?? target);
|
|
843
|
+
required.set(requiredIdentity.key, requiredIdentity.path);
|
|
844
|
+
if (skipUnobservedProjectInputWatchRoots) {
|
|
845
|
+
const retainedActiveRoot = [...this.projectInputWatchers.keys()].find(
|
|
846
|
+
(root) => isProjectInputPathIdentityWithin(root, requiredIdentity.key),
|
|
847
|
+
);
|
|
848
|
+
if (retainedActiveRoot !== undefined) {
|
|
849
|
+
desired.set(retainedActiveRoot, retainedActiveRoot);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (
|
|
854
|
+
location === undefined ||
|
|
855
|
+
(skipUnobservedProjectInputWatchRoots &&
|
|
856
|
+
this.projectInputUnobservedWatchRoots.has(requiredIdentity.key))
|
|
857
|
+
) {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
589
860
|
const available = projectInputAvailableWatchDirectory(
|
|
590
861
|
location,
|
|
591
862
|
this.projectInputRejectedWatchRoots,
|
|
@@ -597,6 +868,130 @@ export class WatchTopology {
|
|
|
597
868
|
desired.set(identity.key, identity.path);
|
|
598
869
|
}
|
|
599
870
|
|
|
871
|
+
/**
|
|
872
|
+
* Retry a failed root on the next reconciliation instead of retiring it for
|
|
873
|
+
* the session.
|
|
874
|
+
*
|
|
875
|
+
* This immediate recovery pass still honors the rejected root so it can
|
|
876
|
+
* install a safe ancestor where one exists. Only the recovery fixpoint
|
|
877
|
+
* reports a genuinely uncovered lane; transient gaps between fallback
|
|
878
|
+
* candidates are not user-visible. The rejection then expires. A later
|
|
879
|
+
* compiler refresh or an unchanged project-input republication can retry the
|
|
880
|
+
* original root, while a permanently failing backend costs at most one
|
|
881
|
+
* attempt per sync.
|
|
882
|
+
*/
|
|
883
|
+
private scheduleProjectInputWatcherRecovery(): void {
|
|
884
|
+
if (this.projectInputRecoveryScheduled) return;
|
|
885
|
+
this.projectInputRecoveryScheduled = true;
|
|
886
|
+
queueMicrotask(() => {
|
|
887
|
+
try {
|
|
888
|
+
let previousRejectionCount = -1;
|
|
889
|
+
while (
|
|
890
|
+
this.closed === false &&
|
|
891
|
+
previousRejectionCount !== this.projectInputRejectedWatchRoots.size
|
|
892
|
+
) {
|
|
893
|
+
previousRejectionCount = this.projectInputRejectedWatchRoots.size;
|
|
894
|
+
this.syncProjectInputWatchers();
|
|
895
|
+
}
|
|
896
|
+
} finally {
|
|
897
|
+
this.projectInputRejectedWatchRoots.clear();
|
|
898
|
+
this.projectInputRecoveryScheduled = false;
|
|
899
|
+
if (!this.closed) {
|
|
900
|
+
this.reportUnobservedProjectInputWatchRoots(
|
|
901
|
+
this.projectInputRequiredWatchRoots,
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Reconcile the snapshot-to-watcher handoff after the caller's current turn.
|
|
910
|
+
*
|
|
911
|
+
* A recursive watcher can return before its backend is ready to deliver the
|
|
912
|
+
* first event. The publication baseline is necessarily captured before that
|
|
913
|
+
* watcher exists, so an input materialized synchronously after
|
|
914
|
+
* `setProjectInputs()` would otherwise depend entirely on that startup event.
|
|
915
|
+
* The ordinary fingerprint update makes this scan and a real backend event
|
|
916
|
+
* race safely: whichever arrives first records the new population and the
|
|
917
|
+
* other becomes a no-op.
|
|
918
|
+
*/
|
|
919
|
+
private scheduleProjectInputPostRegistrationReconciliation(): void {
|
|
920
|
+
if (
|
|
921
|
+
this.closed ||
|
|
922
|
+
this.projectInputPostRegistrationReconciliationScheduled
|
|
923
|
+
) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
this.projectInputPostRegistrationReconciliationScheduled = true;
|
|
927
|
+
queueMicrotask(() => {
|
|
928
|
+
this.projectInputPostRegistrationReconciliationScheduled = false;
|
|
929
|
+
if (this.closed) return;
|
|
930
|
+
this.refreshPublishedProjectInputIdentities();
|
|
931
|
+
this.refreshProjectInputs(this.projectInputs.root, undefined, true);
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Re-resolve declarations after watcher registration.
|
|
937
|
+
*
|
|
938
|
+
* A missing path can become a symlink before the handoff scan. The retained
|
|
939
|
+
* normalized snapshot still names the pre-link spelling in that case, so a
|
|
940
|
+
* scan can find the first target file without installing the physical owner
|
|
941
|
+
* that must observe later target changes.
|
|
942
|
+
*/
|
|
943
|
+
private refreshPublishedProjectInputIdentities(): void {
|
|
944
|
+
const next = normalizeProjectInputSnapshot(this.declaredProjectInputs);
|
|
945
|
+
if (projectInputSnapshotsEqual(this.projectInputs, next)) return;
|
|
946
|
+
this.projectInputs = next;
|
|
947
|
+
this.pruneProjectInputWatchRoots([next, this.declaredProjectInputs]);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/** Drop retained owner choices for declarations no longer published. */
|
|
951
|
+
private pruneProjectInputWatchRoots(
|
|
952
|
+
snapshots: readonly ITtscProjectInputSnapshot[],
|
|
953
|
+
): void {
|
|
954
|
+
const declarations = new Set(
|
|
955
|
+
snapshots.flatMap((snapshot) => [
|
|
956
|
+
...snapshot.files.map((file) =>
|
|
957
|
+
projectInputDeclarationKey("file", file),
|
|
958
|
+
),
|
|
959
|
+
...snapshot.globs.map((glob) =>
|
|
960
|
+
projectInputDeclarationKey("glob", glob),
|
|
961
|
+
),
|
|
962
|
+
...(snapshot.reloadFiles ?? []).map((file) =>
|
|
963
|
+
projectInputDeclarationKey("reload", file),
|
|
964
|
+
),
|
|
965
|
+
...(snapshot.reloadDirectories ?? []).map((directory) =>
|
|
966
|
+
projectInputDeclarationKey("reload-directory", directory),
|
|
967
|
+
),
|
|
968
|
+
]),
|
|
969
|
+
);
|
|
970
|
+
for (const key of this.projectInputWatchRoots.keys()) {
|
|
971
|
+
if (!declarations.has(key)) this.projectInputWatchRoots.delete(key);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** Report only newly uncovered project-input roots as an observation loss. */
|
|
976
|
+
private reportUnobservedProjectInputWatchRoots(
|
|
977
|
+
required: ReadonlyMap<string, string>,
|
|
978
|
+
): void {
|
|
979
|
+
const active = [...this.projectInputWatchers.keys()];
|
|
980
|
+
const unavailable = new Map(
|
|
981
|
+
[...required].filter(([key]) =>
|
|
982
|
+
active.every((root) => !isProjectInputPathIdentityWithin(root, key)),
|
|
983
|
+
),
|
|
984
|
+
);
|
|
985
|
+
const newlyUnavailable = [...unavailable]
|
|
986
|
+
.filter(([key]) => !this.projectInputUnobservedWatchRoots.has(key))
|
|
987
|
+
.map(([, location]) => location)
|
|
988
|
+
.sort();
|
|
989
|
+
this.projectInputUnobservedWatchRoots = unavailable;
|
|
990
|
+
if (newlyUnavailable.length !== 0) {
|
|
991
|
+
this.callbacks.onProjectInputWatchUnavailable?.(newlyUnavailable);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
600
995
|
/**
|
|
601
996
|
* One snapshot holding every spelling of every declaration.
|
|
602
997
|
*
|
|
@@ -669,7 +1064,11 @@ export class WatchTopology {
|
|
|
669
1064
|
return resolved;
|
|
670
1065
|
}
|
|
671
1066
|
|
|
672
|
-
private refreshProjectInputs(
|
|
1067
|
+
private refreshProjectInputs(
|
|
1068
|
+
location: string,
|
|
1069
|
+
changed?: string,
|
|
1070
|
+
skipUnobservedProjectInputWatchRoots = false,
|
|
1071
|
+
): void {
|
|
673
1072
|
try {
|
|
674
1073
|
const previous = this.projectInputMatches;
|
|
675
1074
|
const identities = createProjectInputPathIdentityContext();
|
|
@@ -725,19 +1124,21 @@ export class WatchTopology {
|
|
|
725
1124
|
previous,
|
|
726
1125
|
previousFingerprints: this.projectInputFingerprints,
|
|
727
1126
|
});
|
|
1127
|
+
const reconciledChange =
|
|
1128
|
+
changed ?? (changedInputs.length === 1 ? changedInputs[0] : undefined);
|
|
728
1129
|
// Both spellings classify the event. The normalized form names the file a
|
|
729
1130
|
// link pointed at when the snapshot was published, so after a retarget it
|
|
730
1131
|
// names the wrong one; only the declared form resolves to what the link
|
|
731
1132
|
// points at now, which is the selection this lane exists to protect.
|
|
732
1133
|
const reload = projectInputReloadEventShouldNotify({
|
|
733
|
-
changed,
|
|
1134
|
+
changed: reconciledChange,
|
|
734
1135
|
changedInputs,
|
|
735
1136
|
globs: population.globs,
|
|
736
1137
|
reloadDirectories: population.reloadDirectories ?? [],
|
|
737
1138
|
reloadFiles: population.reloadFiles ?? [],
|
|
738
1139
|
});
|
|
739
1140
|
const invalidate = projectInputMembershipInvalidatesProgram({
|
|
740
|
-
changed,
|
|
1141
|
+
changed: reconciledChange,
|
|
741
1142
|
changedInputs,
|
|
742
1143
|
contentChanged,
|
|
743
1144
|
next,
|
|
@@ -745,28 +1146,31 @@ export class WatchTopology {
|
|
|
745
1146
|
});
|
|
746
1147
|
this.projectInputMatches = next;
|
|
747
1148
|
this.projectInputFingerprints = nextFingerprints;
|
|
748
|
-
this.syncProjectInputWatchers();
|
|
1149
|
+
this.syncProjectInputWatchers(skipUnobservedProjectInputWatchRoots);
|
|
749
1150
|
// A JSON/TS/JS project-input member can simultaneously enter or leave
|
|
750
1151
|
// the compiler Program. Reconcile the compiler watch snapshot before
|
|
751
1152
|
// scheduling its resident invalidation, so runWatch's post-cycle refresh
|
|
752
1153
|
// does not rediscover the same delta as a broader execution reload.
|
|
753
|
-
if (invalidate)
|
|
1154
|
+
if (invalidate) {
|
|
1155
|
+
this.refreshCompilerInputs(false, skipUnobservedProjectInputWatchRoots);
|
|
1156
|
+
}
|
|
754
1157
|
if (
|
|
755
1158
|
projectInputEventShouldNotify({
|
|
756
1159
|
contentChanged,
|
|
757
1160
|
directlyMatched,
|
|
758
1161
|
membershipChanged,
|
|
759
1162
|
}) &&
|
|
760
|
-
(
|
|
761
|
-
this.isProjectInputCompilerOutput(
|
|
1163
|
+
(reconciledChange === undefined ||
|
|
1164
|
+
this.isProjectInputCompilerOutput(reconciledChange, identities) ===
|
|
1165
|
+
false)
|
|
762
1166
|
) {
|
|
763
1167
|
this.callbacks.onInputChange(
|
|
764
1168
|
reload
|
|
765
|
-
? { kind: "config", path:
|
|
1169
|
+
? { kind: "config", path: reconciledChange }
|
|
766
1170
|
: {
|
|
767
1171
|
...(invalidate ? { invalidate: true } : {}),
|
|
768
1172
|
kind: "project",
|
|
769
|
-
path:
|
|
1173
|
+
path: reconciledChange,
|
|
770
1174
|
},
|
|
771
1175
|
);
|
|
772
1176
|
}
|
|
@@ -775,7 +1179,7 @@ export class WatchTopology {
|
|
|
775
1179
|
// replacement is readable. Rebind ancestor ownership even when the
|
|
776
1180
|
// population scan races that transient gap, so a later create cannot be
|
|
777
1181
|
// stranded without a watcher.
|
|
778
|
-
this.syncProjectInputWatchers();
|
|
1182
|
+
this.syncProjectInputWatchers(skipUnobservedProjectInputWatchRoots);
|
|
779
1183
|
this.callbacks.onError(location, error);
|
|
780
1184
|
}
|
|
781
1185
|
}
|
|
@@ -894,7 +1298,13 @@ export class WatchTopology {
|
|
|
894
1298
|
identities: ProjectInputPathIdentityContext,
|
|
895
1299
|
): boolean {
|
|
896
1300
|
const root = this.projectInputs.root;
|
|
1301
|
+
let overlaps = this.projectInputCompilerOutputOverlaps.get(identities);
|
|
1302
|
+
if (overlaps === undefined) {
|
|
1303
|
+
overlaps = new Map();
|
|
1304
|
+
this.projectInputCompilerOutputOverlaps.set(identities, overlaps);
|
|
1305
|
+
}
|
|
897
1306
|
return [...this.outputs.values()].some((output) => {
|
|
1307
|
+
if (!identities.isWithin(output, location)) return false;
|
|
898
1308
|
// An output directory that is the project itself, or holds it, is not a
|
|
899
1309
|
// place where only build products live -- the sources are there too. A
|
|
900
1310
|
// project emitting in place declares exactly that, and honouring it
|
|
@@ -903,7 +1313,15 @@ export class WatchTopology {
|
|
|
903
1313
|
// is the safe side here: a product that gets watched costs an extra
|
|
904
1314
|
// rebuild, while an input that does not is never seen again.
|
|
905
1315
|
if (root !== "" && identities.isWithin(output, root)) return false;
|
|
906
|
-
|
|
1316
|
+
let overlapsCompilerInput = overlaps.get(output);
|
|
1317
|
+
if (overlapsCompilerInput === undefined) {
|
|
1318
|
+
overlapsCompilerInput = [...this.files.values()].some((input) =>
|
|
1319
|
+
identities.isWithin(output, input),
|
|
1320
|
+
);
|
|
1321
|
+
overlaps.set(output, overlapsCompilerInput);
|
|
1322
|
+
}
|
|
1323
|
+
if (overlapsCompilerInput) return false;
|
|
1324
|
+
return true;
|
|
907
1325
|
});
|
|
908
1326
|
}
|
|
909
1327
|
|
|
@@ -1035,7 +1453,7 @@ function resolveWatchTopology(
|
|
|
1035
1453
|
addPaths(outputFiles, compilerOutputs.files);
|
|
1036
1454
|
addPaths(
|
|
1037
1455
|
outputFiles,
|
|
1038
|
-
|
|
1456
|
+
inferPerSourceCompilerOutputs(project, options, compilerInputs),
|
|
1039
1457
|
);
|
|
1040
1458
|
addPaths(outputs, compilerOutputs.directories);
|
|
1041
1459
|
addPaths(files, compilerInputs);
|
|
@@ -1144,13 +1562,16 @@ function listCompilerInputs(
|
|
|
1144
1562
|
`ttsc: failed to list compiler inputs: ${result.error.message}`,
|
|
1145
1563
|
);
|
|
1146
1564
|
}
|
|
1147
|
-
|
|
1565
|
+
// `--listFilesOnly` is the authority for compiler inputs. A path may also be
|
|
1566
|
+
// a predicted product of another input, but that collision does not revoke
|
|
1567
|
+
// its Program membership—especially while the compiler is reporting an
|
|
1568
|
+
// overwrite diagnostic. Product inference is used only to classify future
|
|
1569
|
+
// filesystem events, never to subtract from the compiler's answer.
|
|
1148
1570
|
const inputs = outputText(result.stdout)
|
|
1149
1571
|
.split(/\r?\n/)
|
|
1150
1572
|
.map((line) => line.trim())
|
|
1151
1573
|
.filter((line) => path.isAbsolute(line))
|
|
1152
|
-
.map((line) => path.resolve(line))
|
|
1153
|
-
.filter((file) => isCompilerOutput(file, outputs) === false);
|
|
1574
|
+
.map((line) => path.resolve(line));
|
|
1154
1575
|
if (result.status !== 0 && inputs.length === 0) {
|
|
1155
1576
|
throw new Error(
|
|
1156
1577
|
`ttsc: failed to list compiler inputs:\n${outputText(result.stderr) || outputText(result.stdout)}`,
|
|
@@ -1175,17 +1596,6 @@ function resolveCompilerOutputs(
|
|
|
1175
1596
|
if (emit.declaration && emit.declarationDir !== undefined) {
|
|
1176
1597
|
directories.add(emit.declarationDir);
|
|
1177
1598
|
}
|
|
1178
|
-
if (emit.outFile !== undefined) {
|
|
1179
|
-
if (emit.javascript) {
|
|
1180
|
-
files.add(emit.outFile);
|
|
1181
|
-
if (emit.sourceMap) files.add(`${emit.outFile}.map`);
|
|
1182
|
-
}
|
|
1183
|
-
if (emit.declaration) {
|
|
1184
|
-
const declaration = replaceOutputExtension(emit.outFile, ".d.ts");
|
|
1185
|
-
files.add(declaration);
|
|
1186
|
-
if (emit.declarationMap) files.add(`${declaration}.map`);
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
1599
|
if (emit.incremental) {
|
|
1190
1600
|
files.add(defaultTsBuildInfoFile(project, emit));
|
|
1191
1601
|
}
|
|
@@ -1195,54 +1605,82 @@ function resolveCompilerOutputs(
|
|
|
1195
1605
|
};
|
|
1196
1606
|
}
|
|
1197
1607
|
|
|
1198
|
-
function
|
|
1608
|
+
function inferPerSourceCompilerOutputs(
|
|
1199
1609
|
project: ITtscParsedProjectConfig,
|
|
1200
1610
|
options: WatchTopologyOptions,
|
|
1201
1611
|
inputs: readonly string[],
|
|
1202
1612
|
): string[] {
|
|
1203
1613
|
const emit = effectiveCompilerEmit(project, options);
|
|
1204
1614
|
const outputs = new Set<string>();
|
|
1615
|
+
// TypeScript-Go 7 requires an explicit rootDir when output layout would
|
|
1616
|
+
// otherwise need inference. While reporting TS5011 it still emits relative
|
|
1617
|
+
// to the config directory, so the watch model must use that same recovery
|
|
1618
|
+
// layout rather than the legacy common-source-directory rule.
|
|
1619
|
+
const sourceRoot = emit.rootDir ?? project.root;
|
|
1205
1620
|
for (const input of inputs) {
|
|
1206
1621
|
const extension = path.extname(input).toLowerCase();
|
|
1207
|
-
if (!isCompilerEmittableSourceExtension(extension)) {
|
|
1208
|
-
continue;
|
|
1209
|
-
}
|
|
1210
1622
|
if (/\.d\.(?:ts|mts|cts)$/i.test(input)) continue;
|
|
1211
1623
|
const stem = input.slice(0, -extension.length);
|
|
1624
|
+
const mappedStem = (
|
|
1625
|
+
directory: string | undefined,
|
|
1626
|
+
adjacentWhenOutside: boolean,
|
|
1627
|
+
): string | undefined => {
|
|
1628
|
+
if (directory === undefined) return stem;
|
|
1629
|
+
if (!isPathWithin(sourceRoot, input)) {
|
|
1630
|
+
return adjacentWhenOutside ? stem : undefined;
|
|
1631
|
+
}
|
|
1632
|
+
return path.resolve(directory, path.relative(sourceRoot, stem));
|
|
1633
|
+
};
|
|
1634
|
+
if (extension === ".json") {
|
|
1635
|
+
if (
|
|
1636
|
+
emit.javascript &&
|
|
1637
|
+
emit.resolveJsonModule &&
|
|
1638
|
+
emit.outDir !== undefined
|
|
1639
|
+
) {
|
|
1640
|
+
const jsonStem = mappedStem(emit.outDir, false);
|
|
1641
|
+
if (jsonStem !== undefined) outputs.add(`${jsonStem}.json`);
|
|
1642
|
+
}
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
if (!isCompilerEmittableSourceExtension(extension)) continue;
|
|
1212
1646
|
if (
|
|
1213
1647
|
emit.javascript &&
|
|
1214
|
-
emit.outDir
|
|
1215
|
-
|
|
1216
|
-
(!isJavaScriptSourceExtension(extension) ||
|
|
1648
|
+
(emit.outDir !== undefined ||
|
|
1649
|
+
!isJavaScriptSourceExtension(extension) ||
|
|
1217
1650
|
(extension === ".jsx" && emit.jsx !== "preserve"))
|
|
1218
1651
|
) {
|
|
1219
1652
|
const javascriptExtension =
|
|
1220
|
-
extension === ".mts"
|
|
1653
|
+
extension === ".mts" || extension === ".mjs"
|
|
1221
1654
|
? ".mjs"
|
|
1222
|
-
: extension === ".cts"
|
|
1655
|
+
: extension === ".cts" || extension === ".cjs"
|
|
1223
1656
|
? ".cjs"
|
|
1224
|
-
: extension === ".tsx"
|
|
1657
|
+
: (extension === ".tsx" || extension === ".jsx") &&
|
|
1658
|
+
emit.jsx === "preserve"
|
|
1225
1659
|
? ".jsx"
|
|
1226
1660
|
: ".js";
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1661
|
+
const javascriptStem = mappedStem(emit.outDir, true);
|
|
1662
|
+
if (javascriptStem !== undefined) {
|
|
1663
|
+
const javascript = javascriptStem + javascriptExtension;
|
|
1664
|
+
outputs.add(javascript);
|
|
1665
|
+
if (emit.sourceMap) outputs.add(`${javascript}.map`);
|
|
1666
|
+
}
|
|
1230
1667
|
}
|
|
1231
|
-
if (
|
|
1232
|
-
emit.declaration &&
|
|
1233
|
-
emit.outDir === undefined &&
|
|
1234
|
-
emit.declarationDir === undefined &&
|
|
1235
|
-
emit.outFile === undefined
|
|
1236
|
-
) {
|
|
1668
|
+
if (emit.declaration) {
|
|
1237
1669
|
const declarationExtension =
|
|
1238
1670
|
extension === ".mts" || extension === ".mjs"
|
|
1239
1671
|
? ".d.mts"
|
|
1240
1672
|
: extension === ".cts" || extension === ".cjs"
|
|
1241
1673
|
? ".d.cts"
|
|
1242
1674
|
: ".d.ts";
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1245
|
-
|
|
1675
|
+
const declarationStem = mappedStem(
|
|
1676
|
+
emit.declarationDir ?? emit.outDir,
|
|
1677
|
+
true,
|
|
1678
|
+
);
|
|
1679
|
+
if (declarationStem !== undefined) {
|
|
1680
|
+
const declaration = declarationStem + declarationExtension;
|
|
1681
|
+
outputs.add(declaration);
|
|
1682
|
+
if (emit.declarationMap) outputs.add(`${declaration}.map`);
|
|
1683
|
+
}
|
|
1246
1684
|
}
|
|
1247
1685
|
}
|
|
1248
1686
|
return [...outputs];
|
|
@@ -1273,7 +1711,7 @@ type EffectiveCompilerEmit = {
|
|
|
1273
1711
|
javascript: boolean;
|
|
1274
1712
|
jsx?: unknown;
|
|
1275
1713
|
outDir?: string;
|
|
1276
|
-
|
|
1714
|
+
resolveJsonModule: boolean;
|
|
1277
1715
|
rootDir?: string;
|
|
1278
1716
|
sourceMap: boolean;
|
|
1279
1717
|
tsBuildInfoFile?: string;
|
|
@@ -1327,7 +1765,6 @@ function effectiveCompilerEmit(
|
|
|
1327
1765
|
passthrough,
|
|
1328
1766
|
"--declarationDir",
|
|
1329
1767
|
);
|
|
1330
|
-
const cliOutFile = passthroughPathOption(passthrough, "--outFile");
|
|
1331
1768
|
const cliRootDir = passthroughPathOption(passthrough, "--rootDir");
|
|
1332
1769
|
const cliTsBuildInfoFile = passthroughPathOption(
|
|
1333
1770
|
passthrough,
|
|
@@ -1335,13 +1772,14 @@ function effectiveCompilerEmit(
|
|
|
1335
1772
|
);
|
|
1336
1773
|
const jsx =
|
|
1337
1774
|
passthroughStringOption(passthrough, "--jsx") ?? compilerOptions.jsx;
|
|
1775
|
+
const compilerCwd = project.root;
|
|
1338
1776
|
return {
|
|
1339
1777
|
declaration,
|
|
1340
1778
|
declarationDir:
|
|
1341
1779
|
cliDeclarationDir === null
|
|
1342
1780
|
? undefined
|
|
1343
1781
|
: cliDeclarationDir !== undefined
|
|
1344
|
-
? path.resolve(
|
|
1782
|
+
? path.resolve(compilerCwd, cliDeclarationDir)
|
|
1345
1783
|
: typeof compilerOptions.declarationDir === "string"
|
|
1346
1784
|
? path.resolve(compilerOptions.declarationDir)
|
|
1347
1785
|
: undefined,
|
|
@@ -1352,25 +1790,20 @@ function effectiveCompilerEmit(
|
|
|
1352
1790
|
cliOutDir === null
|
|
1353
1791
|
? undefined
|
|
1354
1792
|
: cliOutDir !== undefined
|
|
1355
|
-
? path.resolve(
|
|
1793
|
+
? path.resolve(compilerCwd, cliOutDir)
|
|
1356
1794
|
: options.outDir !== undefined
|
|
1357
1795
|
? path.resolve(options.cwd, options.outDir)
|
|
1358
1796
|
: typeof compilerOptions.outDir === "string"
|
|
1359
1797
|
? path.resolve(compilerOptions.outDir)
|
|
1360
1798
|
: undefined,
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
: cliOutFile !== undefined
|
|
1365
|
-
? path.resolve(options.cwd, cliOutFile)
|
|
1366
|
-
: typeof compilerOptions.outFile === "string"
|
|
1367
|
-
? path.resolve(compilerOptions.outFile)
|
|
1368
|
-
: undefined,
|
|
1799
|
+
resolveJsonModule:
|
|
1800
|
+
passthroughBooleanOption(passthrough, "--resolveJsonModule") ??
|
|
1801
|
+
compilerOptions.resolveJsonModule === true,
|
|
1369
1802
|
rootDir:
|
|
1370
1803
|
cliRootDir === null
|
|
1371
1804
|
? undefined
|
|
1372
1805
|
: cliRootDir !== undefined
|
|
1373
|
-
? path.resolve(
|
|
1806
|
+
? path.resolve(compilerCwd, cliRootDir)
|
|
1374
1807
|
: typeof compilerOptions.rootDir === "string"
|
|
1375
1808
|
? path.resolve(compilerOptions.rootDir)
|
|
1376
1809
|
: undefined,
|
|
@@ -1379,7 +1812,7 @@ function effectiveCompilerEmit(
|
|
|
1379
1812
|
cliTsBuildInfoFile === null
|
|
1380
1813
|
? undefined
|
|
1381
1814
|
: cliTsBuildInfoFile !== undefined
|
|
1382
|
-
? path.resolve(
|
|
1815
|
+
? path.resolve(compilerCwd, cliTsBuildInfoFile)
|
|
1383
1816
|
: typeof compilerOptions.tsBuildInfoFile === "string"
|
|
1384
1817
|
? path.resolve(compilerOptions.tsBuildInfoFile)
|
|
1385
1818
|
: undefined,
|
|
@@ -1458,16 +1891,6 @@ function passthroughOptionMatches(token: string, name: string): boolean {
|
|
|
1458
1891
|
return resolveFlagSpec(token)?.name === resolveFlagSpec(name)?.name;
|
|
1459
1892
|
}
|
|
1460
1893
|
|
|
1461
|
-
function isCompilerOutput(
|
|
1462
|
-
file: string,
|
|
1463
|
-
outputs: { directories: readonly string[]; files: readonly string[] },
|
|
1464
|
-
): boolean {
|
|
1465
|
-
return (
|
|
1466
|
-
outputs.files.some((output) => pathKey(output) === pathKey(file)) ||
|
|
1467
|
-
outputs.directories.some((directory) => isPathWithin(directory, file))
|
|
1468
|
-
);
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
1894
|
function collectTopologyDirectories(
|
|
1472
1895
|
files: Iterable<string>,
|
|
1473
1896
|
roots: readonly string[],
|
|
@@ -1544,9 +1967,14 @@ export function syncWatchers<T extends SynchronizedWatcher>(
|
|
|
1544
1967
|
desired: ReadonlyMap<string, string>,
|
|
1545
1968
|
create: (location: string, key: string) => T,
|
|
1546
1969
|
onError: (location: string, error: unknown) => void,
|
|
1970
|
+
shouldContinue: () => boolean = () => true,
|
|
1547
1971
|
): boolean {
|
|
1548
1972
|
let complete = true;
|
|
1549
1973
|
for (const [key, location] of desired) {
|
|
1974
|
+
if (!shouldContinue()) {
|
|
1975
|
+
complete = false;
|
|
1976
|
+
break;
|
|
1977
|
+
}
|
|
1550
1978
|
if (watchers.has(key)) continue;
|
|
1551
1979
|
try {
|
|
1552
1980
|
const watcher = create(location, key);
|
|
@@ -1794,18 +2222,22 @@ function projectInputRecursiveWatchRoot(
|
|
|
1794
2222
|
}
|
|
1795
2223
|
|
|
1796
2224
|
/**
|
|
1797
|
-
*
|
|
2225
|
+
* Cheap content and physical-owner identities for a tracked file.
|
|
1798
2226
|
*
|
|
1799
|
-
* Modification time and size
|
|
1800
|
-
*
|
|
1801
|
-
* answers
|
|
2227
|
+
* Modification time and size answer "did the bytes move" without reading the
|
|
2228
|
+
* file. Device and inode answer whether a POSIX per-file watcher still owns the
|
|
2229
|
+
* path. Keeping the answers separate lets an identical atomic replacement
|
|
2230
|
+
* rebind its watcher without inventing a compiler notification.
|
|
1802
2231
|
*/
|
|
1803
|
-
function
|
|
2232
|
+
function compilerFileSnapshot(location: string): CompilerFileSnapshot {
|
|
1804
2233
|
try {
|
|
1805
2234
|
const stats = fs.statSync(location);
|
|
1806
|
-
return
|
|
2235
|
+
return {
|
|
2236
|
+
content: `${stats.mtimeMs}:${stats.size}`,
|
|
2237
|
+
owner: `${stats.dev}:${stats.ino}`,
|
|
2238
|
+
};
|
|
1807
2239
|
} catch {
|
|
1808
|
-
return "";
|
|
2240
|
+
return { content: "", owner: "" };
|
|
1809
2241
|
}
|
|
1810
2242
|
}
|
|
1811
2243
|
|
|
@@ -2002,6 +2434,7 @@ function projectInputChangedPaths(input: {
|
|
|
2002
2434
|
* bytes remain quiet before this classifier is observed.
|
|
2003
2435
|
*/
|
|
2004
2436
|
export function projectInputReloadEventShouldNotify(input: {
|
|
2437
|
+
causedBy?: readonly string[];
|
|
2005
2438
|
changed?: string;
|
|
2006
2439
|
changedInputs: readonly string[];
|
|
2007
2440
|
reloadDirectories?: readonly string[];
|
|
@@ -2071,13 +2504,16 @@ export function projectInputReloadEventShouldNotify(input: {
|
|
|
2071
2504
|
// resolution moved. A missed reselection is a wrong answer; a spare restart is
|
|
2072
2505
|
// a slow one.
|
|
2073
2506
|
const explains = (directory: ProjectInputPathIdentity): boolean => {
|
|
2074
|
-
|
|
2507
|
+
const causes =
|
|
2508
|
+
input.causedBy ?? (input.changed === undefined ? [] : [input.changed]);
|
|
2075
2509
|
// Only an immediate entry can move this directory's digest, so only an
|
|
2076
2510
|
// immediate entry can account for the delta. Asking whether the event was
|
|
2077
2511
|
// data somewhere else answers a different directory's question, and one
|
|
2078
2512
|
// exemption would then cancel every other directory's evidence.
|
|
2079
|
-
|
|
2080
|
-
|
|
2513
|
+
return causes.some((location) => {
|
|
2514
|
+
const parent = identities.resolve(path.dirname(location)).key;
|
|
2515
|
+
return parent === directory.key && exemptedFrom(directory, location);
|
|
2516
|
+
});
|
|
2081
2517
|
};
|
|
2082
2518
|
const isReloadDirectoryEvent = (location: string): boolean =>
|
|
2083
2519
|
namesDirectory(location) || holdsAsImmediateEntry(location);
|
|
@@ -2154,11 +2590,10 @@ function projectInputPathMayAffectProgram(location: string): boolean {
|
|
|
2154
2590
|
return extension === ".json" || isCompilerEmittableSourceExtension(extension);
|
|
2155
2591
|
}
|
|
2156
2592
|
|
|
2157
|
-
function
|
|
2158
|
-
snapshot: ITtscProjectInputSnapshot,
|
|
2593
|
+
function compilerMembershipChange(
|
|
2159
2594
|
previous: ReadonlyMap<string, string>,
|
|
2160
2595
|
next: ReadonlyMap<string, string>,
|
|
2161
|
-
): string[]
|
|
2596
|
+
): string[] {
|
|
2162
2597
|
const changed = new Map<string, string>();
|
|
2163
2598
|
for (const [key, location] of previous) {
|
|
2164
2599
|
if (next.has(key) === false) changed.set(key, location);
|
|
@@ -2166,64 +2601,100 @@ function projectInputCompilerMembershipChange(
|
|
|
2166
2601
|
for (const [key, location] of next) {
|
|
2167
2602
|
if (previous.has(key) === false) changed.set(key, location);
|
|
2168
2603
|
}
|
|
2169
|
-
if (
|
|
2170
|
-
changed.size === 0 ||
|
|
2171
|
-
[...changed.values()].some(
|
|
2172
|
-
(location) =>
|
|
2173
|
-
matchesProjectInput(snapshot, location) === false ||
|
|
2174
|
-
projectInputPathMayAffectProgram(location) === false,
|
|
2175
|
-
)
|
|
2176
|
-
) {
|
|
2177
|
-
return undefined;
|
|
2178
|
-
}
|
|
2179
2604
|
return [...changed.values()].sort();
|
|
2180
2605
|
}
|
|
2181
2606
|
|
|
2607
|
+
function projectInputCompilerMembershipChange(
|
|
2608
|
+
snapshot: ITtscProjectInputSnapshot,
|
|
2609
|
+
changed: readonly string[],
|
|
2610
|
+
): string[] {
|
|
2611
|
+
return changed.filter(
|
|
2612
|
+
(location) =>
|
|
2613
|
+
matchesProjectInput(snapshot, location) &&
|
|
2614
|
+
projectInputPathMayAffectProgram(location),
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
/**
|
|
2619
|
+
* Name a compiler-membership transition as the recursive project watcher does.
|
|
2620
|
+
*
|
|
2621
|
+
* Creating the first member below a missing glob root makes Windows report the
|
|
2622
|
+
* root directory before it reports the member. Reload-directory fingerprints
|
|
2623
|
+
* must see that same causal path: the new root then explains its ancestor's
|
|
2624
|
+
* immediate-entry delta instead of turning ordinary project data into a cold
|
|
2625
|
+
* selection reload. The deepest matching glob owns the most specific delivery.
|
|
2626
|
+
*/
|
|
2627
|
+
function projectInputCompilerMembershipProjectChanges(
|
|
2628
|
+
locations: readonly string[],
|
|
2629
|
+
globs: readonly string[],
|
|
2630
|
+
): string[] {
|
|
2631
|
+
const identities = createProjectInputPathIdentityContext();
|
|
2632
|
+
const roots = globs.map((glob) => identities.resolve(literalGlobRoot(glob)));
|
|
2633
|
+
const changes = new Map<string, string>();
|
|
2634
|
+
for (const location of locations) {
|
|
2635
|
+
const candidate = identities.resolve(location);
|
|
2636
|
+
const root = roots
|
|
2637
|
+
.filter((entry) => identities.isWithin(entry.path, candidate.path))
|
|
2638
|
+
.sort((left, right) => right.path.length - left.path.length)[0];
|
|
2639
|
+
const change = root ?? candidate;
|
|
2640
|
+
changes.set(change.key, change.path);
|
|
2641
|
+
}
|
|
2642
|
+
return [...changes.values()].sort();
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2182
2645
|
function fingerprintProjectInputMatches(
|
|
2183
2646
|
matches: ReadonlyMap<string, string>,
|
|
2184
2647
|
): Map<string, string> {
|
|
2185
2648
|
const fingerprints = new Map<string, string>();
|
|
2186
2649
|
for (const [key, location] of matches) {
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
.createHash("sha256")
|
|
2194
|
-
.update(fs.readFileSync(location))
|
|
2195
|
-
.digest("hex"),
|
|
2196
|
-
);
|
|
2197
|
-
} catch {
|
|
2198
|
-
fingerprints.set(key, "");
|
|
2199
|
-
}
|
|
2650
|
+
fingerprints.set(
|
|
2651
|
+
key,
|
|
2652
|
+
isDirectory(location)
|
|
2653
|
+
? fingerprintProjectInputDirectory(location)
|
|
2654
|
+
: fingerprintProjectInputFile(location),
|
|
2655
|
+
);
|
|
2200
2656
|
}
|
|
2201
2657
|
return fingerprints;
|
|
2202
2658
|
}
|
|
2203
2659
|
|
|
2660
|
+
function fingerprintProjectInputFile(location: string): string {
|
|
2661
|
+
try {
|
|
2662
|
+
return crypto
|
|
2663
|
+
.createHash("sha256")
|
|
2664
|
+
.update(fs.readFileSync(location))
|
|
2665
|
+
.digest("hex");
|
|
2666
|
+
} catch {
|
|
2667
|
+
return "";
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2204
2671
|
function fingerprintProjectInputDirectory(location: string): string {
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2672
|
+
try {
|
|
2673
|
+
const entries = fs
|
|
2674
|
+
.readdirSync(location, { withFileTypes: true })
|
|
2675
|
+
.map((entry) => {
|
|
2676
|
+
const kind = entry.isDirectory()
|
|
2677
|
+
? "directory"
|
|
2678
|
+
: entry.isFile()
|
|
2679
|
+
? "file"
|
|
2680
|
+
: entry.isSymbolicLink()
|
|
2681
|
+
? "symlink"
|
|
2682
|
+
: "other";
|
|
2683
|
+
let target = "";
|
|
2684
|
+
if (entry.isSymbolicLink()) {
|
|
2685
|
+
try {
|
|
2686
|
+
target = fs.readlinkSync(path.join(location, entry.name));
|
|
2687
|
+
} catch {
|
|
2688
|
+
target = "<unreadable>";
|
|
2689
|
+
}
|
|
2221
2690
|
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
.
|
|
2226
|
-
|
|
2691
|
+
return entry.name + "\0" + kind + "\0" + target;
|
|
2692
|
+
})
|
|
2693
|
+
.sort();
|
|
2694
|
+
return crypto.createHash("sha256").update(entries.join("\0")).digest("hex");
|
|
2695
|
+
} catch {
|
|
2696
|
+
return "";
|
|
2697
|
+
}
|
|
2227
2698
|
}
|
|
2228
2699
|
|
|
2229
2700
|
function matchesProjectInputGlob(
|