@ttsc/unplugin 0.30.3 → 0.30.4
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/lib/core/index.js +12 -1
- package/lib/core/index.js.map +1 -1
- package/lib/core/index.mjs +12 -1
- package/lib/core/index.mjs.map +1 -1
- package/lib/core/transform.d.cts +13 -5
- package/lib/core/transform.d.mts +13 -5
- package/lib/core/transform.d.ts +13 -5
- package/lib/core/transform.js +447 -100
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +447 -100
- package/lib/core/transform.mjs.map +1 -1
- package/lib/core/viteServe.d.cts +25 -6
- package/lib/core/viteServe.d.mts +25 -6
- package/lib/core/viteServe.d.ts +25 -6
- package/lib/core/viteServe.js +595 -94
- package/lib/core/viteServe.js.map +1 -1
- package/lib/core/viteServe.mjs +595 -94
- package/lib/core/viteServe.mjs.map +1 -1
- package/package.json +3 -4
- package/src/core/index.ts +12 -1
- package/src/core/transform.ts +590 -142
- package/src/core/viteServe.ts +671 -101
package/src/core/transform.ts
CHANGED
|
@@ -129,6 +129,8 @@ interface TtscProjectMutationTracker {
|
|
|
129
129
|
* whole, which cannot answer for one name.
|
|
130
130
|
*/
|
|
131
131
|
covered?: ReadonlySet<string>;
|
|
132
|
+
/** Whether this is the repository-owned backend with content-event coverage. */
|
|
133
|
+
contentAuthoritative?: boolean;
|
|
132
134
|
/**
|
|
133
135
|
* Wait until every event this tracker's watcher has already dispatched has
|
|
134
136
|
* been applied to it.
|
|
@@ -142,6 +144,8 @@ interface TtscProjectMutationTracker {
|
|
|
142
144
|
drain?: () => Promise<void>;
|
|
143
145
|
failed: boolean;
|
|
144
146
|
membershipChanged: boolean;
|
|
147
|
+
/** Compare event and input paths through this tracker's filesystem identity. */
|
|
148
|
+
overlaps?: (input: string, changed: string) => boolean;
|
|
145
149
|
settle?: Promise<void>;
|
|
146
150
|
}
|
|
147
151
|
|
|
@@ -315,6 +319,9 @@ const MAX_GENERATION_PROOF_FAILURES = 8;
|
|
|
315
319
|
/** Maximum exact mutation paths kept after a tracker already proved a change. */
|
|
316
320
|
const MAX_GENERATION_MUTATION_PATHS = 8;
|
|
317
321
|
|
|
322
|
+
/** Maximum unrelated roots watched before snapshot validation takes over. */
|
|
323
|
+
const MAX_HOST_INPUT_WATCH_SCOPES = 16;
|
|
324
|
+
|
|
318
325
|
/** One retry absorbs a transient watch write without admitting an infinite loop. */
|
|
319
326
|
const TRANSFORM_GENERATION_ATTEMPTS = 2;
|
|
320
327
|
|
|
@@ -562,6 +569,7 @@ export interface TtscTransformFilesystemOperations {
|
|
|
562
569
|
directory: string,
|
|
563
570
|
listener: (eventType: string, filename: string | null) => void,
|
|
564
571
|
onError: () => void,
|
|
572
|
+
recursive?: boolean,
|
|
565
573
|
): { close: () => void };
|
|
566
574
|
}
|
|
567
575
|
|
|
@@ -986,6 +994,11 @@ export async function transformTtsc(
|
|
|
986
994
|
deliveryEpoch: epoch,
|
|
987
995
|
filesystem,
|
|
988
996
|
plugins: options.plugins,
|
|
997
|
+
// One bounded recursive project observer witnesses content restored
|
|
998
|
+
// during the compile itself. Build-scoped adapters close it with the
|
|
999
|
+
// attempt; persistent adapters retain it to make later validations
|
|
1000
|
+
// constant-cost while the generation remains live.
|
|
1001
|
+
retainProjectMembership: cache !== undefined && epoch === undefined,
|
|
989
1002
|
trackProjectMembership: cache !== undefined,
|
|
990
1003
|
tsconfig,
|
|
991
1004
|
});
|
|
@@ -1318,10 +1331,14 @@ interface TtscHostInputValidation {
|
|
|
1318
1331
|
{
|
|
1319
1332
|
path: string;
|
|
1320
1333
|
/**
|
|
1321
|
-
* Whether the recorded state of this input
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1324
|
-
* bytes
|
|
1334
|
+
* Whether the recorded state of this input has been matched to readable
|
|
1335
|
+
* bytes. Capture sets it immediately for compiler-proven inputs; a
|
|
1336
|
+
* current module supplied from an editor buffer can earn it later after
|
|
1337
|
+
* its disk bytes match the recorded source.
|
|
1338
|
+
*
|
|
1339
|
+
* An input that still cannot be read records a missing state, so no
|
|
1340
|
+
* signature may stand in for it: its metadata holds still while the bytes
|
|
1341
|
+
* behind it appear.
|
|
1325
1342
|
*/
|
|
1326
1343
|
readable: boolean;
|
|
1327
1344
|
realpath: string | null;
|
|
@@ -2750,6 +2767,28 @@ function matchesNarrowPersistentInputs(
|
|
|
2750
2767
|
return true;
|
|
2751
2768
|
}
|
|
2752
2769
|
|
|
2770
|
+
/** Whether a healthy notification scope proves one exact input unchanged. */
|
|
2771
|
+
function trackerProvesInputUnchanged(
|
|
2772
|
+
tracker: TtscProjectMutationTracker | undefined,
|
|
2773
|
+
input: string,
|
|
2774
|
+
): boolean {
|
|
2775
|
+
if (tracker === undefined || tracker.failed || tracker.changesOmitted) {
|
|
2776
|
+
return false;
|
|
2777
|
+
}
|
|
2778
|
+
if (tracker.contentAuthoritative !== true) return false;
|
|
2779
|
+
const absolute = path.resolve(input);
|
|
2780
|
+
if (tracker.covered?.has(absolute) !== true) return false;
|
|
2781
|
+
for (const changed of tracker.changes) {
|
|
2782
|
+
if (
|
|
2783
|
+
tracker.overlaps?.(absolute, changed) ??
|
|
2784
|
+
(pathIsWithin(absolute, changed) || pathIsWithin(changed, absolute))
|
|
2785
|
+
) {
|
|
2786
|
+
return false;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
return true;
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2753
2792
|
/**
|
|
2754
2793
|
* Validate one derived input against the generation, skipping the content read
|
|
2755
2794
|
* while the recorded metadata signature still holds and its freshly minted
|
|
@@ -2777,6 +2816,12 @@ function matchesProvenInput(
|
|
|
2777
2816
|
state: TtscEnvelopeDerivation,
|
|
2778
2817
|
input: string,
|
|
2779
2818
|
): boolean {
|
|
2819
|
+
if (
|
|
2820
|
+
trackerProvesInputUnchanged(cached.projectMutationTracker, input) ||
|
|
2821
|
+
trackerProvesInputUnchanged(cached.hostInputMutationTracker, input)
|
|
2822
|
+
) {
|
|
2823
|
+
return true;
|
|
2824
|
+
}
|
|
2780
2825
|
const observation = cached.externalInputObservations?.[path.resolve(input)];
|
|
2781
2826
|
if (observation !== undefined) {
|
|
2782
2827
|
if (
|
|
@@ -2786,7 +2831,25 @@ function matchesProvenInput(
|
|
|
2786
2831
|
) {
|
|
2787
2832
|
return true;
|
|
2788
2833
|
}
|
|
2789
|
-
|
|
2834
|
+
const filesystem = resultFilesystem(cached.result);
|
|
2835
|
+
const spelling = path.resolve(input);
|
|
2836
|
+
const before = inputMetadataEvidence(input, filesystem);
|
|
2837
|
+
if (
|
|
2838
|
+
before !== undefined &&
|
|
2839
|
+
before.separable &&
|
|
2840
|
+
cached.externalInputSignatures?.[spelling] === before.signature
|
|
2841
|
+
) {
|
|
2842
|
+
return true;
|
|
2843
|
+
}
|
|
2844
|
+
if (!matchesRecordedInput(cached, input)) return false;
|
|
2845
|
+
const after = inputMetadataSignature(input, filesystem);
|
|
2846
|
+
const signatures = (cached.externalInputSignatures ??= {});
|
|
2847
|
+
if (before?.separable === true && before.signature === after) {
|
|
2848
|
+
signatures[spelling] = after;
|
|
2849
|
+
} else {
|
|
2850
|
+
delete signatures[spelling];
|
|
2851
|
+
}
|
|
2852
|
+
return true;
|
|
2790
2853
|
}
|
|
2791
2854
|
const slot = inputSignatureSlot(cached, state, input);
|
|
2792
2855
|
if (slot === undefined) {
|
|
@@ -2840,12 +2903,7 @@ function notifiesAbsence(
|
|
|
2840
2903
|
cached: TtscCachedProjectTransform,
|
|
2841
2904
|
input: string,
|
|
2842
2905
|
): boolean {
|
|
2843
|
-
|
|
2844
|
-
return (
|
|
2845
|
-
tracker !== undefined &&
|
|
2846
|
-
!tracker.failed &&
|
|
2847
|
-
tracker.covered?.has(path.resolve(input)) === true
|
|
2848
|
-
);
|
|
2906
|
+
return trackerProvesInputUnchanged(cached.candidateMutationTracker, input);
|
|
2849
2907
|
}
|
|
2850
2908
|
|
|
2851
2909
|
/**
|
|
@@ -2906,6 +2964,14 @@ function matchesUniversalHostInputs(
|
|
|
2906
2964
|
cached: TtscCachedProjectTransform,
|
|
2907
2965
|
validation: TtscHostInputValidation,
|
|
2908
2966
|
): boolean {
|
|
2967
|
+
let notificationsProveAll = true;
|
|
2968
|
+
for (const input of validation.covered) {
|
|
2969
|
+
if (!trackerProvesInputUnchanged(cached.hostInputMutationTracker, input)) {
|
|
2970
|
+
notificationsProveAll = false;
|
|
2971
|
+
break;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
if (notificationsProveAll) return true;
|
|
2909
2975
|
return (
|
|
2910
2976
|
matchesUniversalHostInputEntries(cached, validation) &&
|
|
2911
2977
|
matchesUniversalHostInputProbes(cached, validation)
|
|
@@ -2940,6 +3006,15 @@ function matchesUniversalHostInputEntries(
|
|
|
2940
3006
|
if (!matchesRecordedInput(cached, entry.path)) {
|
|
2941
3007
|
return false;
|
|
2942
3008
|
}
|
|
3009
|
+
if (entry.readable === false) {
|
|
3010
|
+
const slot = inputSignatureSlot(
|
|
3011
|
+
cached,
|
|
3012
|
+
envelopeDerivation(cached),
|
|
3013
|
+
entry.path,
|
|
3014
|
+
);
|
|
3015
|
+
entry.readable =
|
|
3016
|
+
slot !== undefined && slot.recorded !== MISSING_INPUT_STATE;
|
|
3017
|
+
}
|
|
2943
3018
|
if (evidence === undefined) return false;
|
|
2944
3019
|
// Re-earn the proof under the rules the capture applies: an entry whose
|
|
2945
3020
|
// recorded state came from reading nothing keeps its content comparison, a
|
|
@@ -3178,6 +3253,43 @@ function captureUniversalHostInputValidation(
|
|
|
3178
3253
|
return { failures, validation };
|
|
3179
3254
|
}
|
|
3180
3255
|
|
|
3256
|
+
/**
|
|
3257
|
+
* Keep watcher silence as a content proof only for inputs whose bytes capture
|
|
3258
|
+
* actually proved.
|
|
3259
|
+
*
|
|
3260
|
+
* The observers must open before the post-compile snapshots so they can witness
|
|
3261
|
+
* an A-B-A race. Their exact coverage can only be narrowed after those reads.
|
|
3262
|
+
* Existing-but-unreadable and absent inputs record the same `missing` state;
|
|
3263
|
+
* neither may inherit a content shortcut because its metadata can stay fixed
|
|
3264
|
+
* while bytes become readable. Missing resolver candidates inside the project
|
|
3265
|
+
* retain their separate component-aware candidate tracker, while every other
|
|
3266
|
+
* such input continues through the recorded predicate or directory-list proof.
|
|
3267
|
+
*/
|
|
3268
|
+
function restrictNotificationCoverageToProvenInputs(
|
|
3269
|
+
tracker: TtscProjectMutationTracker | undefined,
|
|
3270
|
+
cached: TtscCachedProjectTransform,
|
|
3271
|
+
hostValidation: TtscHostInputValidation | undefined,
|
|
3272
|
+
): void {
|
|
3273
|
+
if (!(tracker?.covered instanceof Set)) return;
|
|
3274
|
+
const covered = tracker.covered as Set<string>;
|
|
3275
|
+
const state = envelopeDerivation(cached);
|
|
3276
|
+
for (const input of [...covered]) {
|
|
3277
|
+
const absolute = path.resolve(input);
|
|
3278
|
+
const hostEntry = hostValidation?.entries.get(absolute);
|
|
3279
|
+
if (
|
|
3280
|
+
(hostValidation?.covered.has(absolute) === true &&
|
|
3281
|
+
(hostEntry === undefined ||
|
|
3282
|
+
(hostEntry.readable === false && hostEntry.strict !== true))) ||
|
|
3283
|
+
cached.externalInputHashes?.[derivationIdentity(state, absolute)] ===
|
|
3284
|
+
MISSING_INPUT_STATE ||
|
|
3285
|
+
cached.externalInputObservations?.[absolute]?.readFile?.ok === false ||
|
|
3286
|
+
cached.externalInputObservations?.[absolute]?.fileExists === false
|
|
3287
|
+
) {
|
|
3288
|
+
covered.delete(input);
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3181
3293
|
/**
|
|
3182
3294
|
* The recorded state of an input the generation read nothing from: absent, or
|
|
3183
3295
|
* present but unreadable. It is deliberately not a hash, so no signature may
|
|
@@ -3314,6 +3426,8 @@ function disposeFilesystemClockReference(referenceDirectory: string): void {
|
|
|
3314
3426
|
interface TtscInputMetadataEvidence {
|
|
3315
3427
|
/** The joined metadata signature of the lexical path and its link target. */
|
|
3316
3428
|
signature: string;
|
|
3429
|
+
/** Whether a directory observer can account for every content mutation. */
|
|
3430
|
+
notificationAuthoritative: boolean;
|
|
3317
3431
|
/**
|
|
3318
3432
|
* Whether a later write is guaranteed to move this signature. Only a
|
|
3319
3433
|
* signature captured with this evidence may be recorded to stand in for a
|
|
@@ -3359,6 +3473,7 @@ function inputMetadataEvidence(
|
|
|
3359
3473
|
link.ctimeNs,
|
|
3360
3474
|
"missing-target",
|
|
3361
3475
|
].join(":"),
|
|
3476
|
+
notificationAuthoritative: false,
|
|
3362
3477
|
separable: false,
|
|
3363
3478
|
};
|
|
3364
3479
|
}
|
|
@@ -3367,17 +3482,26 @@ function inputMetadataEvidence(
|
|
|
3367
3482
|
signature: [
|
|
3368
3483
|
link.dev,
|
|
3369
3484
|
link.ino,
|
|
3485
|
+
link.nlink,
|
|
3370
3486
|
link.mode,
|
|
3371
3487
|
link.size,
|
|
3372
3488
|
link.mtimeNs,
|
|
3373
3489
|
link.ctimeNs,
|
|
3374
3490
|
target.dev,
|
|
3375
3491
|
target.ino,
|
|
3492
|
+
target.nlink,
|
|
3376
3493
|
target.mode,
|
|
3377
3494
|
target.size,
|
|
3378
3495
|
target.mtimeNs,
|
|
3379
3496
|
target.ctimeNs,
|
|
3380
3497
|
].join(":"),
|
|
3498
|
+
// A write through a hardlink outside the watched tree changes this inode
|
|
3499
|
+
// without notifying either the original directory or a watcher opened on
|
|
3500
|
+
// the original path. Keep such files on metadata validation. Symlinks are
|
|
3501
|
+
// likewise governed by the target path, outside the lexical observer.
|
|
3502
|
+
notificationAuthoritative:
|
|
3503
|
+
!link.isSymbolicLink() &&
|
|
3504
|
+
!(link.isFile() && (link.nlink > 1n || target.nlink > 1n)),
|
|
3381
3505
|
// Both halves must be separable: a write remints the target's stamp, a
|
|
3382
3506
|
// link retarget the link's own, and either one hiding inside its recorded
|
|
3383
3507
|
// tick would evade the skipped content and realpath comparisons.
|
|
@@ -3903,12 +4027,14 @@ function captureExternalInputSnapshot(
|
|
|
3903
4027
|
});
|
|
3904
4028
|
continue;
|
|
3905
4029
|
}
|
|
4030
|
+
const before = inputMetadataEvidence(input, filesystem);
|
|
3906
4031
|
const mismatches = graphInputObservationFailures(
|
|
3907
4032
|
input,
|
|
3908
4033
|
predicateObservation,
|
|
3909
4034
|
filesystem,
|
|
3910
4035
|
state.identityContext,
|
|
3911
4036
|
);
|
|
4037
|
+
const after = inputMetadataSignature(input, filesystem);
|
|
3912
4038
|
if (mismatches.length !== 0) complete = false;
|
|
3913
4039
|
for (const kind of mismatches) {
|
|
3914
4040
|
recordGenerationProofFailure(failures, {
|
|
@@ -3917,6 +4043,7 @@ function captureExternalInputSnapshot(
|
|
|
3917
4043
|
path: input,
|
|
3918
4044
|
});
|
|
3919
4045
|
}
|
|
4046
|
+
if (mismatches.length === 0) record(input, before, after);
|
|
3920
4047
|
observations[spelling] = predicateObservation;
|
|
3921
4048
|
continue;
|
|
3922
4049
|
}
|
|
@@ -4292,12 +4419,14 @@ function collectProjectInputSnapshot(
|
|
|
4292
4419
|
directoryComplete: boolean;
|
|
4293
4420
|
fileSignatures: Record<string, string>;
|
|
4294
4421
|
hashes: Record<string, string>;
|
|
4422
|
+
notificationUnsafeInputs: Set<string>;
|
|
4295
4423
|
projectDirectories: TtscProjectDirectorySnapshot[];
|
|
4296
4424
|
provenSignatures: Record<string, string>;
|
|
4297
4425
|
unstableFiles: Set<string>;
|
|
4298
4426
|
walkFailures: TtscProjectWalkFailure[];
|
|
4299
4427
|
} {
|
|
4300
4428
|
const hashes: Record<string, string> = {};
|
|
4429
|
+
const notificationUnsafeInputs = new Set<string>();
|
|
4301
4430
|
const fileSignatures: Record<string, string> = {};
|
|
4302
4431
|
const provenSignatures: Record<string, string> = {};
|
|
4303
4432
|
const unstableFiles = new Set<string>();
|
|
@@ -4322,6 +4451,9 @@ function collectProjectInputSnapshot(
|
|
|
4322
4451
|
continue;
|
|
4323
4452
|
}
|
|
4324
4453
|
const before = inputMetadataEvidence(file, filesystem);
|
|
4454
|
+
if (before?.notificationAuthoritative !== true) {
|
|
4455
|
+
notificationUnsafeInputs.add(key);
|
|
4456
|
+
}
|
|
4325
4457
|
// A file whose signature still equals the one captured around the read
|
|
4326
4458
|
// that produced the recorded hash carries that content, so the whole
|
|
4327
4459
|
// project does not have to be re-read to prove one delivery. Recheck the
|
|
@@ -4340,24 +4472,27 @@ function collectProjectInputSnapshot(
|
|
|
4340
4472
|
continue;
|
|
4341
4473
|
}
|
|
4342
4474
|
const contents = filesystem.readFile(file);
|
|
4343
|
-
const after =
|
|
4475
|
+
const after = inputMetadataEvidence(file, filesystem);
|
|
4476
|
+
if (after?.notificationAuthoritative !== true) {
|
|
4477
|
+
notificationUnsafeInputs.add(key);
|
|
4478
|
+
}
|
|
4344
4479
|
hashes[key] = hashText(contents);
|
|
4345
4480
|
if (
|
|
4346
4481
|
before === undefined ||
|
|
4347
4482
|
after === undefined ||
|
|
4348
|
-
before.signature !== after
|
|
4483
|
+
before.signature !== after.signature
|
|
4349
4484
|
) {
|
|
4350
4485
|
complete = false;
|
|
4351
4486
|
unstableFiles.add(key);
|
|
4352
4487
|
walkFailures.push({ kind: "file-changed-during-read", path: file });
|
|
4353
4488
|
} else {
|
|
4354
|
-
fileSignatures[key] = after;
|
|
4489
|
+
fileSignatures[key] = after.signature;
|
|
4355
4490
|
// Only a signature whose stamp's tick the filesystem's clock provably
|
|
4356
4491
|
// left before this read may later stand in for the content comparison
|
|
4357
4492
|
// ({@link stampSeparable}); the raw signature above still participates
|
|
4358
4493
|
// in the generation-time stability comparison.
|
|
4359
4494
|
if (before.separable) {
|
|
4360
|
-
provenSignatures[key] = after;
|
|
4495
|
+
provenSignatures[key] = after.signature;
|
|
4361
4496
|
}
|
|
4362
4497
|
}
|
|
4363
4498
|
} catch {
|
|
@@ -4379,6 +4514,7 @@ function collectProjectInputSnapshot(
|
|
|
4379
4514
|
directoryComplete: walked.complete && attributed,
|
|
4380
4515
|
fileSignatures,
|
|
4381
4516
|
hashes,
|
|
4517
|
+
notificationUnsafeInputs,
|
|
4382
4518
|
projectDirectories: walked.directories,
|
|
4383
4519
|
provenSignatures,
|
|
4384
4520
|
unstableFiles,
|
|
@@ -4619,13 +4755,14 @@ function openDirectoryWatch(
|
|
|
4619
4755
|
directory: string,
|
|
4620
4756
|
listener: (eventType: string, filename: string | null) => void,
|
|
4621
4757
|
onError: () => void,
|
|
4758
|
+
recursive = false,
|
|
4622
4759
|
): { close: () => void } {
|
|
4623
4760
|
if (filesystem.watch !== undefined) {
|
|
4624
|
-
return filesystem.watch(directory, listener, onError);
|
|
4761
|
+
return filesystem.watch(directory, listener, onError, recursive);
|
|
4625
4762
|
}
|
|
4626
4763
|
const watcher = fs.watch(
|
|
4627
4764
|
directory,
|
|
4628
|
-
{ persistent: false },
|
|
4765
|
+
{ persistent: false, recursive },
|
|
4629
4766
|
(eventType, filename) =>
|
|
4630
4767
|
listener(eventType, filename === null ? null : String(filename)),
|
|
4631
4768
|
);
|
|
@@ -4654,140 +4791,289 @@ function closeDirectoryWatches(watchers: { close: () => void }[]): void {
|
|
|
4654
4791
|
/** Watch every walked directory for membership changes after generation. */
|
|
4655
4792
|
async function createProjectMutationTracker(
|
|
4656
4793
|
directories: readonly TtscProjectDirectorySnapshot[],
|
|
4794
|
+
covered: ReadonlySet<string>,
|
|
4657
4795
|
filesystem: TtscTransformFilesystemOperations = DEFAULT_FILESYSTEM_OPERATIONS,
|
|
4658
4796
|
policy: ITtscProjectMembershipPolicy = PERMISSIVE_PROJECT_MEMBERSHIP_POLICY,
|
|
4659
4797
|
): Promise<TtscProjectMutationTracker> {
|
|
4798
|
+
const identities = createHostPathIdentityContext(filesystem);
|
|
4799
|
+
const root = commonDirectoryRoot(
|
|
4800
|
+
directories.map((directory) => directory.path),
|
|
4801
|
+
);
|
|
4802
|
+
const authoritative =
|
|
4803
|
+
root !== undefined &&
|
|
4804
|
+
filesystem.watch === undefined &&
|
|
4805
|
+
pathTraversesSymbolicLink(
|
|
4806
|
+
path.join(root, ".ttsc-notification-authority"),
|
|
4807
|
+
filesystem,
|
|
4808
|
+
new Map(),
|
|
4809
|
+
)
|
|
4810
|
+
? new Set<string>()
|
|
4811
|
+
: covered;
|
|
4660
4812
|
const tracker: TtscProjectMutationTracker = {
|
|
4661
4813
|
changes: new Set(),
|
|
4662
4814
|
changesOmitted: false,
|
|
4663
|
-
close: () =>
|
|
4815
|
+
close: () => {
|
|
4816
|
+
tracker.failed = true;
|
|
4817
|
+
},
|
|
4818
|
+
covered: authoritative,
|
|
4664
4819
|
failed: false,
|
|
4665
4820
|
membershipChanged: false,
|
|
4821
|
+
overlaps: (input, changed) =>
|
|
4822
|
+
identities.isWithin(input, changed) ||
|
|
4823
|
+
identities.isWithin(changed, input),
|
|
4824
|
+
contentAuthoritative: filesystem.watch === undefined,
|
|
4825
|
+
};
|
|
4826
|
+
if (root === undefined) return tracker;
|
|
4827
|
+
const knownDirectories = new Set(
|
|
4828
|
+
directories
|
|
4829
|
+
.filter((directory) => directory.relevant)
|
|
4830
|
+
.map((directory) => path.resolve(directory.path)),
|
|
4831
|
+
);
|
|
4832
|
+
const reportsMembership = (location: string, filename: string): boolean => {
|
|
4833
|
+
const changed = path.join(location, filename);
|
|
4834
|
+
return (
|
|
4835
|
+
knownDirectories.has(path.resolve(changed)) ||
|
|
4836
|
+
reportsProgramMembership(
|
|
4837
|
+
changed,
|
|
4838
|
+
path.basename(filename),
|
|
4839
|
+
policy,
|
|
4840
|
+
filesystem,
|
|
4841
|
+
)
|
|
4842
|
+
);
|
|
4843
|
+
};
|
|
4844
|
+
const reportsNewMembership = (
|
|
4845
|
+
location: string,
|
|
4846
|
+
filename: string,
|
|
4847
|
+
): boolean => {
|
|
4848
|
+
const changed = path.resolve(location, filename);
|
|
4849
|
+
return (
|
|
4850
|
+
!knownDirectories.has(changed) && tracker.covered?.has(changed) !== true
|
|
4851
|
+
);
|
|
4666
4852
|
};
|
|
4667
4853
|
if (process.platform === "win32" && filesystem.watch === undefined) {
|
|
4668
4854
|
await registerWindowsProjectMutationTracker(
|
|
4669
4855
|
tracker,
|
|
4670
|
-
|
|
4856
|
+
[{ directory: root, recursive: true }],
|
|
4671
4857
|
false,
|
|
4672
4858
|
filesystem,
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
policy,
|
|
4678
|
-
filesystem,
|
|
4679
|
-
),
|
|
4859
|
+
reportsMembership,
|
|
4860
|
+
(_location, filename) =>
|
|
4861
|
+
isPossibleProgramFileName(path.basename(filename), policy),
|
|
4862
|
+
reportsNewMembership,
|
|
4680
4863
|
);
|
|
4681
4864
|
return tracker;
|
|
4682
4865
|
}
|
|
4683
4866
|
const watchers: { close: () => void }[] = [];
|
|
4684
|
-
tracker.close = () =>
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
} catch {
|
|
4719
|
-
tracker.failed = true;
|
|
4720
|
-
}
|
|
4867
|
+
tracker.close = () => {
|
|
4868
|
+
tracker.failed = true;
|
|
4869
|
+
closeDirectoryWatches(watchers);
|
|
4870
|
+
};
|
|
4871
|
+
try {
|
|
4872
|
+
watchers.push(
|
|
4873
|
+
openDirectoryWatch(
|
|
4874
|
+
filesystem,
|
|
4875
|
+
root,
|
|
4876
|
+
(eventType, filename) => {
|
|
4877
|
+
const changed = filename === null ? root : path.join(root, filename);
|
|
4878
|
+
const membership =
|
|
4879
|
+
filename === null || reportsMembership(root, filename);
|
|
4880
|
+
if (
|
|
4881
|
+
membership &&
|
|
4882
|
+
(eventType === "rename" ||
|
|
4883
|
+
reportsNewMembership(root, filename ?? ""))
|
|
4884
|
+
) {
|
|
4885
|
+
recordProjectMutation(tracker, changed);
|
|
4886
|
+
} else if (
|
|
4887
|
+
filename === null ||
|
|
4888
|
+
isPossibleProgramFileName(path.basename(filename), policy)
|
|
4889
|
+
) {
|
|
4890
|
+
recordProjectChange(tracker, changed);
|
|
4891
|
+
}
|
|
4892
|
+
},
|
|
4893
|
+
() => {
|
|
4894
|
+
tracker.failed = true;
|
|
4895
|
+
},
|
|
4896
|
+
true,
|
|
4897
|
+
),
|
|
4898
|
+
);
|
|
4899
|
+
} catch {
|
|
4900
|
+
tracker.failed = true;
|
|
4721
4901
|
}
|
|
4722
4902
|
return tracker;
|
|
4723
4903
|
}
|
|
4724
4904
|
|
|
4905
|
+
/** Common ancestor owned by every project directory snapshot. */
|
|
4906
|
+
function commonDirectoryRoot(
|
|
4907
|
+
directories: readonly string[],
|
|
4908
|
+
): string | undefined {
|
|
4909
|
+
if (directories.length === 0) return undefined;
|
|
4910
|
+
let root = path.resolve(directories[0]!);
|
|
4911
|
+
for (const directory of directories.slice(1)) {
|
|
4912
|
+
const absolute = path.resolve(directory);
|
|
4913
|
+
while (!pathIsWithin(absolute, root)) {
|
|
4914
|
+
const parent = path.dirname(root);
|
|
4915
|
+
if (parent === root) return root;
|
|
4916
|
+
root = parent;
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
return root;
|
|
4920
|
+
}
|
|
4921
|
+
|
|
4725
4922
|
/** Watch exact universal inputs, or their nearest existing parent if missing. */
|
|
4726
4923
|
async function createHostInputMutationTracker(
|
|
4727
4924
|
inputs: readonly string[],
|
|
4728
4925
|
filesystem: TtscTransformFilesystemOperations,
|
|
4729
4926
|
covered: ReadonlySet<string>,
|
|
4730
4927
|
events: "all" | "rename" = "all",
|
|
4928
|
+
preferredRoot?: string,
|
|
4731
4929
|
): Promise<TtscProjectMutationTracker> {
|
|
4732
4930
|
const identities = createHostPathIdentityContext(filesystem);
|
|
4733
|
-
const
|
|
4931
|
+
const linkedAncestors = new Map<string, boolean>();
|
|
4932
|
+
const authoritative = new Set(
|
|
4933
|
+
[...covered]
|
|
4934
|
+
.map((input) => path.resolve(input))
|
|
4935
|
+
.filter((input) => {
|
|
4936
|
+
if (pathTraversesSymbolicLink(input, filesystem, linkedAncestors)) {
|
|
4937
|
+
return false;
|
|
4938
|
+
}
|
|
4939
|
+
try {
|
|
4940
|
+
// A lexical symlink can keep its own directory silent while a target
|
|
4941
|
+
// elsewhere changes or appears. Its joined metadata proof must stay
|
|
4942
|
+
// on the validation path, including while the link is broken.
|
|
4943
|
+
const lexical = filesystem.lstat(input);
|
|
4944
|
+
if (
|
|
4945
|
+
lexical.isSymbolicLink() ||
|
|
4946
|
+
(lexical.isFile() && lexical.nlink > 1n)
|
|
4947
|
+
)
|
|
4948
|
+
return false;
|
|
4949
|
+
} catch {
|
|
4950
|
+
// A genuinely absent lexical path is covered by its nearest existing
|
|
4951
|
+
// ancestor and remains eligible for notification proof.
|
|
4952
|
+
}
|
|
4953
|
+
const target = hostInputRealpath(input, filesystem);
|
|
4954
|
+
return (
|
|
4955
|
+
target === null ||
|
|
4956
|
+
pathIdentityKey(target, identities) ===
|
|
4957
|
+
pathIdentityKey(input, identities)
|
|
4958
|
+
);
|
|
4959
|
+
}),
|
|
4960
|
+
);
|
|
4961
|
+
const locationsByDirectory = new Map<
|
|
4734
4962
|
string,
|
|
4735
|
-
{
|
|
4963
|
+
{
|
|
4964
|
+
directory: string;
|
|
4965
|
+
names: Set<string>;
|
|
4966
|
+
paths?: Set<string>;
|
|
4967
|
+
recursive?: boolean;
|
|
4968
|
+
}
|
|
4736
4969
|
>();
|
|
4970
|
+
const internalRoot =
|
|
4971
|
+
preferredRoot === undefined ? undefined : path.resolve(preferredRoot);
|
|
4737
4972
|
for (const input of inputs) {
|
|
4738
4973
|
const absolute = path.resolve(input);
|
|
4739
4974
|
const probe = filesystem.exists(absolute)
|
|
4740
4975
|
? { directory: path.dirname(absolute), name: path.basename(absolute) }
|
|
4741
4976
|
: missingPathProbe(absolute, filesystem);
|
|
4742
|
-
const
|
|
4743
|
-
|
|
4977
|
+
const internal =
|
|
4978
|
+
internalRoot !== undefined && pathIsWithin(absolute, internalRoot);
|
|
4979
|
+
const directory = internal ? internalRoot : probe.directory;
|
|
4980
|
+
const directoryIdentity = identities.resolve(directory);
|
|
4981
|
+
let location = locationsByDirectory.get(directoryIdentity.key);
|
|
4744
4982
|
if (location === undefined) {
|
|
4745
4983
|
location = {
|
|
4746
4984
|
directory: directoryIdentity.path,
|
|
4747
4985
|
names: new Set<string>(),
|
|
4986
|
+
...(internal ? { paths: new Set<string>(), recursive: true } : {}),
|
|
4748
4987
|
};
|
|
4749
|
-
|
|
4988
|
+
locationsByDirectory.set(directoryIdentity.key, location);
|
|
4989
|
+
}
|
|
4990
|
+
if (location.paths !== undefined) {
|
|
4991
|
+
location.paths.add(
|
|
4992
|
+
pathIdentityKey(path.resolve(probe.directory, probe.name), identities),
|
|
4993
|
+
);
|
|
4994
|
+
} else {
|
|
4995
|
+
location.names.add(
|
|
4996
|
+
normalizeHostInputName(
|
|
4997
|
+
probe.name,
|
|
4998
|
+
identities.caseSensitive(directoryIdentity.path),
|
|
4999
|
+
),
|
|
5000
|
+
);
|
|
4750
5001
|
}
|
|
4751
|
-
location.names.add(
|
|
4752
|
-
normalizeHostInputName(
|
|
4753
|
-
probe.name,
|
|
4754
|
-
identities.caseSensitive(directoryIdentity.path),
|
|
4755
|
-
),
|
|
4756
|
-
);
|
|
4757
5002
|
}
|
|
4758
|
-
const locations = [...
|
|
4759
|
-
directory: location.directory,
|
|
4760
|
-
names: [...location.names],
|
|
4761
|
-
}));
|
|
5003
|
+
const locations = [...locationsByDirectory.values()];
|
|
4762
5004
|
const tracker: TtscProjectMutationTracker = {
|
|
4763
5005
|
changes: new Set(),
|
|
4764
5006
|
changesOmitted: false,
|
|
4765
|
-
close: () =>
|
|
5007
|
+
close: () => {
|
|
5008
|
+
tracker.failed = true;
|
|
5009
|
+
},
|
|
4766
5010
|
// Coverage is the caller's claim, and it is required rather than derived
|
|
4767
5011
|
// from the input list: an input is watched by its exact name here, but only
|
|
4768
5012
|
// the caller knows whether the path leading to it is watched as well, which
|
|
4769
5013
|
// is what a later validation needs before it trusts the watcher instead of
|
|
4770
5014
|
// probing the path again. Deriving it here would hand that claim to every
|
|
4771
5015
|
// future caller by default (samchon/ttsc#1261).
|
|
4772
|
-
covered,
|
|
5016
|
+
covered: authoritative,
|
|
5017
|
+
contentAuthoritative: filesystem.watch === undefined,
|
|
4773
5018
|
failed: false,
|
|
4774
5019
|
membershipChanged: false,
|
|
5020
|
+
overlaps: (input, changed) =>
|
|
5021
|
+
identities.isWithin(input, changed) ||
|
|
5022
|
+
identities.isWithin(changed, input),
|
|
5023
|
+
};
|
|
5024
|
+
if (locations.length > MAX_HOST_INPUT_WATCH_SCOPES) {
|
|
5025
|
+
// A graph spread over unrelated external roots cannot be folded into one
|
|
5026
|
+
// recursive observer without watching an arbitrarily broad filesystem
|
|
5027
|
+
// ancestor. Decline the notification proof and use the recorded snapshots;
|
|
5028
|
+
// descriptor count must never scale with an adversarial input graph.
|
|
5029
|
+
tracker.failed = true;
|
|
5030
|
+
return tracker;
|
|
5031
|
+
}
|
|
5032
|
+
const matches = (directory: string, filename: string): boolean => {
|
|
5033
|
+
const location = locationsByDirectory.get(
|
|
5034
|
+
identities.resolve(directory).key,
|
|
5035
|
+
);
|
|
5036
|
+
if (location === undefined) return false;
|
|
5037
|
+
if (location.paths !== undefined) {
|
|
5038
|
+
let changed = path.resolve(directory, filename);
|
|
5039
|
+
for (;;) {
|
|
5040
|
+
if (location.paths.has(pathIdentityKey(changed, identities))) {
|
|
5041
|
+
return true;
|
|
5042
|
+
}
|
|
5043
|
+
const parent = path.dirname(changed);
|
|
5044
|
+
if (parent === changed || !pathIsWithin(parent, location.directory)) {
|
|
5045
|
+
return false;
|
|
5046
|
+
}
|
|
5047
|
+
changed = parent;
|
|
5048
|
+
}
|
|
5049
|
+
}
|
|
5050
|
+
return location.names.has(
|
|
5051
|
+
normalizeHostInputName(filename, identities.caseSensitive(directory)),
|
|
5052
|
+
);
|
|
4775
5053
|
};
|
|
4776
5054
|
if (process.platform === "win32" && filesystem.watch === undefined) {
|
|
4777
5055
|
await registerWindowsProjectMutationTracker(
|
|
4778
5056
|
tracker,
|
|
4779
|
-
locations
|
|
5057
|
+
locations.map((location) => ({
|
|
5058
|
+
directory: location.directory,
|
|
5059
|
+
...(location.recursive === true
|
|
5060
|
+
? { recursive: true }
|
|
5061
|
+
: { names: [...location.names] }),
|
|
5062
|
+
})),
|
|
4780
5063
|
events === "all",
|
|
4781
5064
|
filesystem,
|
|
5065
|
+
matches,
|
|
5066
|
+
matches,
|
|
4782
5067
|
);
|
|
4783
5068
|
return tracker;
|
|
4784
5069
|
}
|
|
4785
5070
|
const watchers: { close: () => void }[] = [];
|
|
4786
|
-
tracker.close = () =>
|
|
5071
|
+
tracker.close = () => {
|
|
5072
|
+
tracker.failed = true;
|
|
5073
|
+
closeDirectoryWatches(watchers);
|
|
5074
|
+
};
|
|
4787
5075
|
for (const location of locations) {
|
|
4788
5076
|
try {
|
|
4789
|
-
const names = new Set(location.names);
|
|
4790
|
-
const caseSensitive = identities.caseSensitive(location.directory);
|
|
4791
5077
|
watchers.push(
|
|
4792
5078
|
openDirectoryWatch(
|
|
4793
5079
|
filesystem,
|
|
@@ -4796,22 +5082,22 @@ async function createHostInputMutationTracker(
|
|
|
4796
5082
|
if (events === "rename" && eventType !== "rename") {
|
|
4797
5083
|
return;
|
|
4798
5084
|
}
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
? null
|
|
4802
|
-
: normalizeHostInputName(filename, caseSensitive);
|
|
4803
|
-
if (reported === null || names.has(reported)) {
|
|
4804
|
-
recordProjectMutation(
|
|
4805
|
-
tracker,
|
|
5085
|
+
if (filename === null || matches(location.directory, filename)) {
|
|
5086
|
+
const changed =
|
|
4806
5087
|
filename === null
|
|
4807
5088
|
? location.directory
|
|
4808
|
-
: path.join(location.directory, filename)
|
|
4809
|
-
)
|
|
5089
|
+
: path.join(location.directory, filename);
|
|
5090
|
+
if (eventType === "rename") {
|
|
5091
|
+
recordProjectMutation(tracker, changed);
|
|
5092
|
+
} else {
|
|
5093
|
+
recordProjectChange(tracker, changed);
|
|
5094
|
+
}
|
|
4810
5095
|
}
|
|
4811
5096
|
},
|
|
4812
5097
|
() => {
|
|
4813
5098
|
tracker.failed = true;
|
|
4814
5099
|
},
|
|
5100
|
+
location.recursive === true,
|
|
4815
5101
|
),
|
|
4816
5102
|
);
|
|
4817
5103
|
} catch {
|
|
@@ -4821,6 +5107,47 @@ async function createHostInputMutationTracker(
|
|
|
4821
5107
|
return tracker;
|
|
4822
5108
|
}
|
|
4823
5109
|
|
|
5110
|
+
/**
|
|
5111
|
+
* Whether any lexical component above an input is a symbolic link or junction.
|
|
5112
|
+
*
|
|
5113
|
+
* A recursive observer follows the component to its current physical target.
|
|
5114
|
+
* Retargeting it can therefore move the input without producing an event on the
|
|
5115
|
+
* old target. Keep those spellings on metadata/realpath validation. The memo
|
|
5116
|
+
* makes this one lstat per distinct directory component for the generation,
|
|
5117
|
+
* rather than one walk per graph input.
|
|
5118
|
+
*/
|
|
5119
|
+
function pathTraversesSymbolicLink(
|
|
5120
|
+
input: string,
|
|
5121
|
+
filesystem: TtscTransformFilesystemOperations,
|
|
5122
|
+
memo: Map<string, boolean>,
|
|
5123
|
+
): boolean {
|
|
5124
|
+
const visited: string[] = [];
|
|
5125
|
+
let current = path.dirname(path.resolve(input));
|
|
5126
|
+
let linked = false;
|
|
5127
|
+
for (;;) {
|
|
5128
|
+
const known = memo.get(current);
|
|
5129
|
+
if (known !== undefined) {
|
|
5130
|
+
linked = known;
|
|
5131
|
+
break;
|
|
5132
|
+
}
|
|
5133
|
+
visited.push(current);
|
|
5134
|
+
try {
|
|
5135
|
+
if (filesystem.lstat(current).isSymbolicLink()) {
|
|
5136
|
+
linked = true;
|
|
5137
|
+
break;
|
|
5138
|
+
}
|
|
5139
|
+
} catch {
|
|
5140
|
+
// A missing component is not a link. Its nearest existing ancestor will
|
|
5141
|
+
// still be inspected before the walk reaches the volume root.
|
|
5142
|
+
}
|
|
5143
|
+
const parent = path.dirname(current);
|
|
5144
|
+
if (parent === current) break;
|
|
5145
|
+
current = parent;
|
|
5146
|
+
}
|
|
5147
|
+
for (const directory of visited) memo.set(directory, linked);
|
|
5148
|
+
return linked;
|
|
5149
|
+
}
|
|
5150
|
+
|
|
4824
5151
|
/**
|
|
4825
5152
|
* Whether a path lies inside a directory the configuration excludes.
|
|
4826
5153
|
*
|
|
@@ -4915,6 +5242,14 @@ function recordProjectMutation(
|
|
|
4915
5242
|
changed: string,
|
|
4916
5243
|
): void {
|
|
4917
5244
|
tracker.membershipChanged = true;
|
|
5245
|
+
recordProjectChange(tracker, changed);
|
|
5246
|
+
}
|
|
5247
|
+
|
|
5248
|
+
/** Record a content event without classifying it as a membership change. */
|
|
5249
|
+
function recordProjectChange(
|
|
5250
|
+
tracker: TtscProjectMutationTracker,
|
|
5251
|
+
changed: string,
|
|
5252
|
+
): void {
|
|
4918
5253
|
if (tracker.changes.has(changed)) return;
|
|
4919
5254
|
if (tracker.changes.size < MAX_GENERATION_MUTATION_PATHS) {
|
|
4920
5255
|
tracker.changes.add(changed);
|
|
@@ -4942,6 +5277,10 @@ interface WindowsProjectMutationBroker {
|
|
|
4942
5277
|
* have already narrowed theirs by construction.
|
|
4943
5278
|
*/
|
|
4944
5279
|
membership?: (location: string, filename: string) => boolean;
|
|
5280
|
+
/** Whether one named event can change compiler-consumed content. */
|
|
5281
|
+
content?: (location: string, filename: string) => boolean;
|
|
5282
|
+
/** Classify a backend `change` that can add one unknown program path. */
|
|
5283
|
+
changeAddsMembership?: (location: string, filename: string) => boolean;
|
|
4945
5284
|
ready: () => void;
|
|
4946
5285
|
/**
|
|
4947
5286
|
* The walk's own spelling for each canonical directory the child watches,
|
|
@@ -4963,6 +5302,7 @@ let windowsProjectMutationBroker: WindowsProjectMutationBroker | undefined;
|
|
|
4963
5302
|
interface WindowsMutationLocation {
|
|
4964
5303
|
directory: string;
|
|
4965
5304
|
names?: string[];
|
|
5305
|
+
recursive?: boolean;
|
|
4966
5306
|
}
|
|
4967
5307
|
|
|
4968
5308
|
/**
|
|
@@ -4983,6 +5323,8 @@ async function registerWindowsProjectMutationTracker(
|
|
|
4983
5323
|
* name-watching trackers pass none, since they already watch exact names.
|
|
4984
5324
|
*/
|
|
4985
5325
|
membership?: (location: string, filename: string) => boolean,
|
|
5326
|
+
content?: (location: string, filename: string) => boolean,
|
|
5327
|
+
changeAddsMembership?: (location: string, filename: string) => boolean,
|
|
4986
5328
|
): Promise<void> {
|
|
4987
5329
|
const broker = getWindowsProjectMutationBroker();
|
|
4988
5330
|
// The child watches canonical directories, and reports its events under that
|
|
@@ -5003,6 +5345,7 @@ async function registerWindowsProjectMutationTracker(
|
|
|
5003
5345
|
return {
|
|
5004
5346
|
directory,
|
|
5005
5347
|
...(location.names === undefined ? {} : { names: location.names }),
|
|
5348
|
+
...(location.recursive === true ? { recursive: true } : {}),
|
|
5006
5349
|
};
|
|
5007
5350
|
});
|
|
5008
5351
|
broker.pendingRegistrations += 1;
|
|
@@ -5016,13 +5359,16 @@ async function registerWindowsProjectMutationTracker(
|
|
|
5016
5359
|
resolveReady = resolve;
|
|
5017
5360
|
});
|
|
5018
5361
|
broker.trackers.set(id, {
|
|
5019
|
-
|
|
5362
|
+
...(changeAddsMembership === undefined ? {} : { changeAddsMembership }),
|
|
5363
|
+
...(content === undefined ? {} : { content }),
|
|
5364
|
+
...(membership === undefined ? {} : { membership }),
|
|
5020
5365
|
ready: resolveReady,
|
|
5021
5366
|
spellings,
|
|
5022
5367
|
tracker,
|
|
5023
5368
|
});
|
|
5024
5369
|
tracker.drain = () => drainWindowsProjectMutationBroker(broker);
|
|
5025
5370
|
tracker.close = () => {
|
|
5371
|
+
tracker.failed = true;
|
|
5026
5372
|
const active = broker.trackers.get(id);
|
|
5027
5373
|
if (active === undefined) return;
|
|
5028
5374
|
broker.trackers.delete(id);
|
|
@@ -5119,6 +5465,7 @@ function getWindowsProjectMutationBroker(): WindowsProjectMutationBroker {
|
|
|
5119
5465
|
drained?: boolean;
|
|
5120
5466
|
failed?: boolean;
|
|
5121
5467
|
filename?: string | null;
|
|
5468
|
+
eventType?: string;
|
|
5122
5469
|
id?: number;
|
|
5123
5470
|
ready?: boolean;
|
|
5124
5471
|
};
|
|
@@ -5141,19 +5488,33 @@ function getWindowsProjectMutationBroker(): WindowsProjectMutationBroker {
|
|
|
5141
5488
|
// comparison and every recorded witness downstream expects.
|
|
5142
5489
|
const reported =
|
|
5143
5490
|
registration.spellings.get(record.directory) ?? record.directory;
|
|
5491
|
+
const changed =
|
|
5492
|
+
typeof record.filename === "string"
|
|
5493
|
+
? path.join(reported, record.filename)
|
|
5494
|
+
: reported;
|
|
5144
5495
|
if (
|
|
5145
5496
|
typeof record.filename === "string" &&
|
|
5146
|
-
registration.membership !== undefined
|
|
5147
|
-
!registration.membership(reported, record.filename)
|
|
5497
|
+
registration.membership !== undefined
|
|
5148
5498
|
) {
|
|
5499
|
+
const membership = registration.membership(reported, record.filename);
|
|
5500
|
+
if (
|
|
5501
|
+
membership &&
|
|
5502
|
+
(record.eventType === "rename" ||
|
|
5503
|
+
registration.changeAddsMembership?.(reported, record.filename) ===
|
|
5504
|
+
true)
|
|
5505
|
+
) {
|
|
5506
|
+
recordProjectMutation(registration.tracker, changed);
|
|
5507
|
+
return;
|
|
5508
|
+
}
|
|
5509
|
+
if (
|
|
5510
|
+
record.eventType !== "rename" &&
|
|
5511
|
+
registration.content?.(reported, record.filename) === true
|
|
5512
|
+
) {
|
|
5513
|
+
recordProjectChange(registration.tracker, changed);
|
|
5514
|
+
}
|
|
5149
5515
|
return;
|
|
5150
5516
|
}
|
|
5151
|
-
recordProjectMutation(
|
|
5152
|
-
registration.tracker,
|
|
5153
|
-
typeof record.filename === "string"
|
|
5154
|
-
? path.join(reported, record.filename)
|
|
5155
|
-
: reported,
|
|
5156
|
-
);
|
|
5517
|
+
recordProjectMutation(registration.tracker, changed);
|
|
5157
5518
|
} else {
|
|
5158
5519
|
registration.tracker.membershipChanged = true;
|
|
5159
5520
|
}
|
|
@@ -5245,9 +5606,9 @@ const WINDOWS_WATCH_BROKER_SOURCE = [
|
|
|
5245
5606
|
" for (const location of message.locations) {",
|
|
5246
5607
|
" try {",
|
|
5247
5608
|
" const names = location.names === undefined ? undefined : new Set(location.names.map((name) => name.toLowerCase()));",
|
|
5248
|
-
" const watcher = fs.watch(location.directory, { persistent: false }, (event, filename) => {",
|
|
5609
|
+
" const watcher = fs.watch(location.directory, { persistent: false, recursive: location.recursive === true }, (event, filename) => {",
|
|
5249
5610
|
" const matches = names === undefined || filename === null || names.has(String(filename).toLowerCase());",
|
|
5250
|
-
' if (matches && (message.allEvents || event === "rename")) process.send?.({ directory: location.directory, filename: filename === null ? null : String(filename), id: message.id });',
|
|
5611
|
+
' if (matches && (message.allEvents || event === "rename" || location.recursive === true)) process.send?.({ directory: location.directory, eventType: event, filename: filename === null ? null : String(filename), id: message.id });',
|
|
5251
5612
|
" });",
|
|
5252
5613
|
' watcher.on("error", () => process.send?.({ failed: true, id: message.id }));',
|
|
5253
5614
|
" watchers.push(watcher);",
|
|
@@ -5269,16 +5630,18 @@ const WINDOWS_WATCH_BROKER_SOURCE = [
|
|
|
5269
5630
|
].join("\n");
|
|
5270
5631
|
|
|
5271
5632
|
/**
|
|
5272
|
-
* Report whether
|
|
5273
|
-
*
|
|
5274
|
-
*
|
|
5633
|
+
* Report whether the project walk observed a membership event. This is positive
|
|
5634
|
+
* evidence that the program's root set changed, so it outranks the question of
|
|
5635
|
+
* whether the notifications still work.
|
|
5636
|
+
*
|
|
5637
|
+
* Host and resolution-candidate trackers cover the union of every module's
|
|
5638
|
+
* inputs. Their events remain path witnesses: the requested module's narrow
|
|
5639
|
+
* validation decides whether that path is relevant. Promoting one of them to a
|
|
5640
|
+
* project-wide verdict would discard a generation when an unreachable external
|
|
5641
|
+
* input changes, defeating per-file completeness and doing needless compiles.
|
|
5275
5642
|
*/
|
|
5276
5643
|
function reportsMembershipChange(cached: TtscCachedProjectTransform): boolean {
|
|
5277
|
-
return
|
|
5278
|
-
cached.projectMutationTracker?.membershipChanged === true ||
|
|
5279
|
-
cached.hostInputMutationTracker?.membershipChanged === true ||
|
|
5280
|
-
cached.candidateMutationTracker?.membershipChanged === true
|
|
5281
|
-
);
|
|
5644
|
+
return cached.projectMutationTracker?.membershipChanged === true;
|
|
5282
5645
|
}
|
|
5283
5646
|
|
|
5284
5647
|
/**
|
|
@@ -5329,11 +5692,18 @@ function drainOnNextTurn(): Promise<void> {
|
|
|
5329
5692
|
async function settleProjectMutationEvents(
|
|
5330
5693
|
cached: TtscCachedProjectTransform,
|
|
5331
5694
|
): Promise<void> {
|
|
5332
|
-
|
|
5695
|
+
await settleMutationTrackers([
|
|
5333
5696
|
cached.projectMutationTracker,
|
|
5334
5697
|
cached.hostInputMutationTracker,
|
|
5335
5698
|
cached.candidateMutationTracker,
|
|
5336
|
-
]
|
|
5699
|
+
]);
|
|
5700
|
+
}
|
|
5701
|
+
|
|
5702
|
+
/** Settle one set of optional trackers through one shared barrier each. */
|
|
5703
|
+
async function settleMutationTrackers(
|
|
5704
|
+
candidates: readonly (TtscProjectMutationTracker | undefined)[],
|
|
5705
|
+
): Promise<void> {
|
|
5706
|
+
const trackers = candidates.filter(
|
|
5337
5707
|
(tracker): tracker is TtscProjectMutationTracker => tracker !== undefined,
|
|
5338
5708
|
);
|
|
5339
5709
|
await Promise.all(
|
|
@@ -5715,15 +6085,26 @@ function matchesCachedExternalInputs(cached: TtscCachedProjectTransform): {
|
|
|
5715
6085
|
const spelling = path.resolve(file);
|
|
5716
6086
|
const observation = cached.externalInputObservations?.[spelling];
|
|
5717
6087
|
if (observation !== undefined) {
|
|
6088
|
+
const before = inputMetadataEvidence(file, filesystem);
|
|
5718
6089
|
if (
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
filesystem,
|
|
5723
|
-
state.identityContext,
|
|
5724
|
-
)
|
|
6090
|
+
before !== undefined &&
|
|
6091
|
+
before.separable &&
|
|
6092
|
+
recordedSignatures[spelling] === before.signature
|
|
5725
6093
|
) {
|
|
6094
|
+
signatures[spelling] = before.signature;
|
|
6095
|
+
continue;
|
|
6096
|
+
}
|
|
6097
|
+
const observed = matchesGraphInputObservation(
|
|
6098
|
+
file,
|
|
6099
|
+
observation,
|
|
6100
|
+
filesystem,
|
|
6101
|
+
state.identityContext,
|
|
6102
|
+
);
|
|
6103
|
+
const after = inputMetadataSignature(file, filesystem);
|
|
6104
|
+
if (!observed) {
|
|
5726
6105
|
matches = false;
|
|
6106
|
+
} else if (before?.separable === true && before.signature === after) {
|
|
6107
|
+
signatures[spelling] = after;
|
|
5727
6108
|
}
|
|
5728
6109
|
continue;
|
|
5729
6110
|
}
|
|
@@ -6331,6 +6712,7 @@ function declaredProjectInputKeys(
|
|
|
6331
6712
|
if (state.declaredInputKeysBuilt !== true) {
|
|
6332
6713
|
state.declaredInputKeys = selectDeclaredProjectInputKeys({
|
|
6333
6714
|
identities: state.identityContext,
|
|
6715
|
+
projectInputHashes: cached.inputHashes,
|
|
6334
6716
|
projectRoot: cached.projectRoot,
|
|
6335
6717
|
result: cached.result,
|
|
6336
6718
|
scratchDirectory: cached.scratchDirectory,
|
|
@@ -6343,11 +6725,16 @@ function declaredProjectInputKeys(
|
|
|
6343
6725
|
/**
|
|
6344
6726
|
* Project-walk keys of every input the envelope declares: the reference graph's
|
|
6345
6727
|
* edge endpoints, globals, config chain, and resolution candidates, plus the
|
|
6346
|
-
* universal host inputs
|
|
6347
|
-
*
|
|
6728
|
+
* universal host inputs, intersected with the files the project walk actually
|
|
6729
|
+
* hashed. Out-of-walk declarations and candidates carry their own graph proof;
|
|
6730
|
+
* asking the project observer to witness them as well makes unrelated activity
|
|
6731
|
+
* in ignored directories invalidate an otherwise complete generation. Returns
|
|
6732
|
+
* `undefined` for an envelope with no graph, which declares no input set and
|
|
6733
|
+
* therefore keeps whole-walk comparison.
|
|
6348
6734
|
*/
|
|
6349
6735
|
function selectDeclaredProjectInputKeys(props: {
|
|
6350
6736
|
identities: FilesystemPathIdentityContext;
|
|
6737
|
+
projectInputHashes: Readonly<Record<string, string>>;
|
|
6351
6738
|
projectRoot: string;
|
|
6352
6739
|
result: ITtscCompilerTransformation;
|
|
6353
6740
|
scratchDirectory?: string;
|
|
@@ -6361,7 +6748,10 @@ function selectDeclaredProjectInputKeys(props: {
|
|
|
6361
6748
|
if (typeof entry !== "string" || entry.length === 0) return;
|
|
6362
6749
|
const absolute = path.resolve(props.projectRoot, entry);
|
|
6363
6750
|
if (isTransformScratchInput(absolute, props.scratchDirectory)) return;
|
|
6364
|
-
|
|
6751
|
+
const key = toProjectKey(props.projectRoot, absolute, props.identities);
|
|
6752
|
+
if (Object.prototype.hasOwnProperty.call(props.projectInputHashes, key)) {
|
|
6753
|
+
keys.add(key);
|
|
6754
|
+
}
|
|
6365
6755
|
};
|
|
6366
6756
|
for (const [source, targets] of Object.entries(graph.edges ?? {})) {
|
|
6367
6757
|
add(source);
|
|
@@ -6693,6 +7083,7 @@ async function transformProject(props: {
|
|
|
6693
7083
|
deliveryEpoch?: number;
|
|
6694
7084
|
filesystem: TtscTransformFilesystemOperations;
|
|
6695
7085
|
plugins?: ResolvedTtscUnpluginOptions["plugins"];
|
|
7086
|
+
retainProjectMembership: boolean;
|
|
6696
7087
|
trackProjectMembership: boolean;
|
|
6697
7088
|
tsconfig: string;
|
|
6698
7089
|
}): Promise<TtscCachedProjectTransform> {
|
|
@@ -6701,8 +7092,7 @@ async function transformProject(props: {
|
|
|
6701
7092
|
const cached = await captureTransformGeneration(props);
|
|
6702
7093
|
if (
|
|
6703
7094
|
cached.configStateComplete !== false &&
|
|
6704
|
-
(
|
|
6705
|
-
cached.result.type !== "success" ||
|
|
7095
|
+
(cached.result.type !== "success" ||
|
|
6706
7096
|
cached.projectSnapshotComplete === true)
|
|
6707
7097
|
) {
|
|
6708
7098
|
return cached;
|
|
@@ -6741,6 +7131,7 @@ async function captureTransformGeneration(props: {
|
|
|
6741
7131
|
deliveryEpoch?: number;
|
|
6742
7132
|
filesystem: TtscTransformFilesystemOperations;
|
|
6743
7133
|
plugins?: ResolvedTtscUnpluginOptions["plugins"];
|
|
7134
|
+
retainProjectMembership: boolean;
|
|
6744
7135
|
trackProjectMembership: boolean;
|
|
6745
7136
|
tsconfig: string;
|
|
6746
7137
|
}): Promise<TtscCachedProjectTransform> {
|
|
@@ -6759,7 +7150,7 @@ async function captureTransformGeneration(props: {
|
|
|
6759
7150
|
let retainCandidateTracker = false;
|
|
6760
7151
|
let captured: TtscCachedProjectTransform | undefined;
|
|
6761
7152
|
try {
|
|
6762
|
-
if (props.
|
|
7153
|
+
if (props.retainProjectMembership) {
|
|
6763
7154
|
try {
|
|
6764
7155
|
clockReferenceDirectory = createTransformScratchDirectory(
|
|
6765
7156
|
projectRoot,
|
|
@@ -6810,6 +7201,11 @@ async function captureTransformGeneration(props: {
|
|
|
6810
7201
|
tracker = props.trackProjectMembership
|
|
6811
7202
|
? await createProjectMutationTracker(
|
|
6812
7203
|
before.projectDirectories,
|
|
7204
|
+
new Set(
|
|
7205
|
+
Object.keys(before.hashes)
|
|
7206
|
+
.filter((key) => !before.notificationUnsafeInputs.has(key))
|
|
7207
|
+
.map((key) => path.resolve(projectRoot, key)),
|
|
7208
|
+
),
|
|
6813
7209
|
props.filesystem,
|
|
6814
7210
|
membershipPolicy,
|
|
6815
7211
|
)
|
|
@@ -6849,15 +7245,26 @@ async function captureTransformGeneration(props: {
|
|
|
6849
7245
|
scratchDirectory,
|
|
6850
7246
|
temporaryTsconfig,
|
|
6851
7247
|
});
|
|
7248
|
+
const externalInputPaths = selectExternalInputPaths({
|
|
7249
|
+
filesystem: props.filesystem,
|
|
7250
|
+
membershipPolicy,
|
|
7251
|
+
projectRoot,
|
|
7252
|
+
result,
|
|
7253
|
+
scratchDirectory,
|
|
7254
|
+
temporaryTsconfig,
|
|
7255
|
+
});
|
|
7256
|
+
const persistentValidationInputs = [
|
|
7257
|
+
...new Set([...persistentHostInputs, ...externalInputPaths]),
|
|
7258
|
+
];
|
|
6852
7259
|
// The generation's absent resolution candidates, which get a watcher of
|
|
6853
7260
|
// their own below; watching one is what lets a delivery stop probing it
|
|
6854
7261
|
// (samchon/ttsc#1261). The validation manifest stays built from the
|
|
6855
7262
|
// universal inputs alone, so nothing else about a candidate changes.
|
|
6856
7263
|
//
|
|
6857
|
-
// Derived only where a tracker could carry it: a build-scoped
|
|
6858
|
-
//
|
|
6859
|
-
// whose answer nothing can read.
|
|
6860
|
-
const notifiableAbsence = props.
|
|
7264
|
+
// Derived only where a retained tracker could carry it: a build-scoped
|
|
7265
|
+
// adapter keeps only the compile-time project observer, so probing every
|
|
7266
|
+
// candidate here would be work whose answer nothing can later read.
|
|
7267
|
+
const notifiableAbsence = props.retainProjectMembership
|
|
6861
7268
|
? selectNotifiableAbsentInputs({
|
|
6862
7269
|
filesystem: props.filesystem,
|
|
6863
7270
|
projectRoot,
|
|
@@ -6866,14 +7273,18 @@ async function captureTransformGeneration(props: {
|
|
|
6866
7273
|
temporaryTsconfig,
|
|
6867
7274
|
})
|
|
6868
7275
|
: { candidates: [], watched: [] };
|
|
6869
|
-
hostInputTracker = props.
|
|
7276
|
+
hostInputTracker = props.retainProjectMembership
|
|
6870
7277
|
? await createHostInputMutationTracker(
|
|
6871
|
-
|
|
7278
|
+
persistentValidationInputs,
|
|
6872
7279
|
props.filesystem,
|
|
6873
7280
|
// A universal input never reaches the per-input loop that consults a
|
|
6874
7281
|
// coverage claim: an absent one is proven by its directory listing
|
|
6875
7282
|
// instead, which re-resolves the spelling every delivery.
|
|
6876
|
-
new Set(
|
|
7283
|
+
new Set(
|
|
7284
|
+
persistentValidationInputs.map((input) => path.resolve(input)),
|
|
7285
|
+
),
|
|
7286
|
+
"all",
|
|
7287
|
+
projectRoot,
|
|
6877
7288
|
)
|
|
6878
7289
|
: undefined;
|
|
6879
7290
|
// The candidates and the directories carrying them get their own tracker,
|
|
@@ -6890,16 +7301,9 @@ async function captureTransformGeneration(props: {
|
|
|
6890
7301
|
props.filesystem,
|
|
6891
7302
|
new Set(notifiableAbsence.candidates),
|
|
6892
7303
|
"rename",
|
|
7304
|
+
projectRoot,
|
|
6893
7305
|
)
|
|
6894
7306
|
: undefined;
|
|
6895
|
-
const externalInputPaths = selectExternalInputPaths({
|
|
6896
|
-
filesystem: props.filesystem,
|
|
6897
|
-
membershipPolicy,
|
|
6898
|
-
projectRoot,
|
|
6899
|
-
result,
|
|
6900
|
-
scratchDirectory,
|
|
6901
|
-
temporaryTsconfig,
|
|
6902
|
-
});
|
|
6903
7307
|
const inputSnapshot = collectProjectInputSnapshot(
|
|
6904
7308
|
projectRoot,
|
|
6905
7309
|
identities,
|
|
@@ -6914,10 +7318,16 @@ async function captureTransformGeneration(props: {
|
|
|
6914
7318
|
// provable from its own recorded state.
|
|
6915
7319
|
const declaredInputs = selectDeclaredProjectInputKeys({
|
|
6916
7320
|
identities,
|
|
7321
|
+
projectInputHashes: inputSnapshot.hashes,
|
|
6917
7322
|
projectRoot,
|
|
6918
7323
|
result,
|
|
6919
7324
|
scratchDirectory,
|
|
6920
7325
|
});
|
|
7326
|
+
// The before/after snapshots prove bytes and metadata. Drain the watcher
|
|
7327
|
+
// opened before compilation as the independent A-B-A witness: a producer
|
|
7328
|
+
// can restore both bytes and timestamps before the second walk, but it
|
|
7329
|
+
// cannot withdraw the already queued content event.
|
|
7330
|
+
await settleMutationTrackers([tracker, hostInputTracker, candidateTracker]);
|
|
6921
7331
|
const walkStable =
|
|
6922
7332
|
configStable &&
|
|
6923
7333
|
walkSnapshotComplete(before, declaredInputs) &&
|
|
@@ -6932,6 +7342,11 @@ async function captureTransformGeneration(props: {
|
|
|
6932
7342
|
before.projectDirectories,
|
|
6933
7343
|
inputSnapshot.projectDirectories,
|
|
6934
7344
|
) &&
|
|
7345
|
+
!trackerChangedDeclaredProjectInput(
|
|
7346
|
+
tracker,
|
|
7347
|
+
declaredInputs,
|
|
7348
|
+
projectRoot,
|
|
7349
|
+
) &&
|
|
6935
7350
|
tracker?.membershipChanged !== true &&
|
|
6936
7351
|
hostInputTracker?.membershipChanged !== true &&
|
|
6937
7352
|
candidateTracker?.membershipChanged !== true;
|
|
@@ -7030,6 +7445,11 @@ async function captureTransformGeneration(props: {
|
|
|
7030
7445
|
cached,
|
|
7031
7446
|
props.currentFile,
|
|
7032
7447
|
);
|
|
7448
|
+
restrictNotificationCoverageToProvenInputs(
|
|
7449
|
+
hostInputTracker,
|
|
7450
|
+
cached,
|
|
7451
|
+
universalInputCapture.validation,
|
|
7452
|
+
);
|
|
7033
7453
|
mergeGenerationProofFailures(failures, universalInputCapture.failures);
|
|
7034
7454
|
const graphProofs =
|
|
7035
7455
|
graphFailures.entries.length === 0 && graphFailures.omitted === 0;
|
|
@@ -7062,7 +7482,10 @@ async function captureTransformGeneration(props: {
|
|
|
7062
7482
|
// Attach notifications only while they can actually prove membership. A
|
|
7063
7483
|
// generation that could not open its watchers keeps its recorded snapshot
|
|
7064
7484
|
// and validates through it, rather than losing the cache entirely.
|
|
7065
|
-
const notifying =
|
|
7485
|
+
const notifying =
|
|
7486
|
+
props.retainProjectMembership &&
|
|
7487
|
+
stableProjectSnapshot &&
|
|
7488
|
+
notificationsAvailable;
|
|
7066
7489
|
if (notifying && tracker !== undefined) {
|
|
7067
7490
|
cached.projectMutationTracker = tracker;
|
|
7068
7491
|
}
|
|
@@ -7155,6 +7578,31 @@ async function captureTransformGeneration(props: {
|
|
|
7155
7578
|
return captured;
|
|
7156
7579
|
}
|
|
7157
7580
|
|
|
7581
|
+
/** Whether a compile-time content event overlaps any declared project input. */
|
|
7582
|
+
function trackerChangedDeclaredProjectInput(
|
|
7583
|
+
tracker: TtscProjectMutationTracker | undefined,
|
|
7584
|
+
declared: ReadonlySet<string> | undefined,
|
|
7585
|
+
projectRoot: string,
|
|
7586
|
+
): boolean {
|
|
7587
|
+
if (tracker === undefined) return false;
|
|
7588
|
+
if (tracker.changesOmitted) return true;
|
|
7589
|
+
if (tracker.changes.size === 0) return false;
|
|
7590
|
+
if (declared === undefined) return true;
|
|
7591
|
+
const inputs = [...declared].map((input) => path.resolve(projectRoot, input));
|
|
7592
|
+
for (const changed of tracker.changes) {
|
|
7593
|
+
if (
|
|
7594
|
+
inputs.some(
|
|
7595
|
+
(input) =>
|
|
7596
|
+
tracker.overlaps?.(input, changed) ??
|
|
7597
|
+
(pathIsWithin(input, changed) || pathIsWithin(changed, input)),
|
|
7598
|
+
)
|
|
7599
|
+
) {
|
|
7600
|
+
return true;
|
|
7601
|
+
}
|
|
7602
|
+
}
|
|
7603
|
+
return false;
|
|
7604
|
+
}
|
|
7605
|
+
|
|
7158
7606
|
/** Exclude disposed transform scratch from live host-input tracking. */
|
|
7159
7607
|
function selectPersistentHostInputs(props: {
|
|
7160
7608
|
filesystem: TtscTransformFilesystemOperations;
|