@ttsc/unplugin 0.30.2 → 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/bun.js +3 -16
- package/lib/bun.js.map +1 -1
- package/lib/bun.mjs +3 -16
- package/lib/bun.mjs.map +1 -1
- package/lib/core/esbuild.d.cts +4 -0
- package/lib/core/esbuild.d.mts +4 -0
- package/lib/core/esbuild.d.ts +4 -0
- package/lib/core/esbuild.js +110 -0
- package/lib/core/esbuild.js.map +1 -0
- package/lib/core/esbuild.mjs +108 -0
- package/lib/core/esbuild.mjs.map +1 -0
- package/lib/core/index.js +60 -89
- package/lib/core/index.js.map +1 -1
- package/lib/core/index.mjs +61 -90
- package/lib/core/index.mjs.map +1 -1
- package/lib/core/transform.d.cts +18 -8
- package/lib/core/transform.d.mts +18 -8
- package/lib/core/transform.d.ts +18 -8
- package/lib/core/transform.js +472 -125
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +472 -125
- package/lib/core/transform.mjs.map +1 -1
- package/lib/core/viteServe.d.cts +35 -38
- package/lib/core/viteServe.d.mts +35 -38
- package/lib/core/viteServe.d.ts +35 -38
- package/lib/core/viteServe.js +731 -94
- package/lib/core/viteServe.js.map +1 -1
- package/lib/core/viteServe.mjs +732 -94
- package/lib/core/viteServe.mjs.map +1 -1
- package/package.json +3 -3
- package/src/bun.ts +2 -17
- package/src/core/esbuild.ts +127 -0
- package/src/core/index.ts +62 -100
- package/src/core/transform.ts +620 -170
- package/src/core/viteServe.ts +838 -157
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
|
|
|
@@ -239,14 +243,13 @@ class TtscUnstableGenerationError extends TtscTerminalGenerationError {
|
|
|
239
243
|
/**
|
|
240
244
|
* A compile this pass already attempted, whose envelope failed outright.
|
|
241
245
|
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
* made a single broken save cost one compile per delivered module
|
|
246
|
+
* Native project diagnostics carry a structured failure envelope; setup and
|
|
247
|
+
* host failures can still arrive as opaque exceptions. Both settle the current
|
|
248
|
+
* attempt, so the adapter uses the delivery boundary it owns rather than
|
|
249
|
+
* guessing retryability from a diagnostic message. Inside a pass the answer is
|
|
250
|
+
* already settled, so every later module replays it instead of repeating a
|
|
251
|
+
* whole-project transform to reach the same verdict, which is what made a
|
|
252
|
+
* single broken save cost one compile per delivered module
|
|
250
253
|
* (samchon/ttsc#1303).
|
|
251
254
|
*
|
|
252
255
|
* The scope is exactly the pass. A host whose `buildStart` repeats drops the
|
|
@@ -316,6 +319,9 @@ const MAX_GENERATION_PROOF_FAILURES = 8;
|
|
|
316
319
|
/** Maximum exact mutation paths kept after a tracker already proved a change. */
|
|
317
320
|
const MAX_GENERATION_MUTATION_PATHS = 8;
|
|
318
321
|
|
|
322
|
+
/** Maximum unrelated roots watched before snapshot validation takes over. */
|
|
323
|
+
const MAX_HOST_INPUT_WATCH_SCOPES = 16;
|
|
324
|
+
|
|
319
325
|
/** One retry absorbs a transient watch write without admitting an infinite loop. */
|
|
320
326
|
const TRANSFORM_GENERATION_ATTEMPTS = 2;
|
|
321
327
|
|
|
@@ -563,6 +569,7 @@ export interface TtscTransformFilesystemOperations {
|
|
|
563
569
|
directory: string,
|
|
564
570
|
listener: (eventType: string, filename: string | null) => void,
|
|
565
571
|
onError: () => void,
|
|
572
|
+
recursive?: boolean,
|
|
566
573
|
): { close: () => void };
|
|
567
574
|
}
|
|
568
575
|
|
|
@@ -811,10 +818,12 @@ export interface TtscTransformHooks {
|
|
|
811
818
|
addWatchFile?: (file: string, evidence?: TtscWatchInputEvidence) => void;
|
|
812
819
|
/**
|
|
813
820
|
* Batched form of {@link addWatchFile}. When supplied, the transform calls it
|
|
814
|
-
* once per delivered module and does not call `addWatchFile` for that
|
|
815
|
-
*
|
|
821
|
+
* once per delivered module and does not call `addWatchFile` for that module.
|
|
822
|
+
* `failed` marks a recovery batch: a failed compiler can omit inputs from its
|
|
823
|
+
* previous successful result, so replacing hosts should retain those
|
|
824
|
+
* spellings until the next successful delivery.
|
|
816
825
|
*/
|
|
817
|
-
addWatchFiles?: (inputs: readonly TtscWatchInput[]) => void;
|
|
826
|
+
addWatchFiles?: (inputs: readonly TtscWatchInput[], failed?: boolean) => void;
|
|
818
827
|
/**
|
|
819
828
|
* Invoked when the plugin declared the transformed file volatile (the
|
|
820
829
|
* envelope's `volatile` list): its output depends on non-file inputs that no
|
|
@@ -985,6 +994,11 @@ export async function transformTtsc(
|
|
|
985
994
|
deliveryEpoch: epoch,
|
|
986
995
|
filesystem,
|
|
987
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,
|
|
988
1002
|
trackProjectMembership: cache !== undefined,
|
|
989
1003
|
tsconfig,
|
|
990
1004
|
});
|
|
@@ -1317,10 +1331,14 @@ interface TtscHostInputValidation {
|
|
|
1317
1331
|
{
|
|
1318
1332
|
path: string;
|
|
1319
1333
|
/**
|
|
1320
|
-
* Whether the recorded state of this input
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1323
|
-
* 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.
|
|
1324
1342
|
*/
|
|
1325
1343
|
readable: boolean;
|
|
1326
1344
|
realpath: string | null;
|
|
@@ -1947,11 +1965,11 @@ function collectDeclaredIdentities(
|
|
|
1947
1965
|
* invalidated, and the error stays on screen (samchon/ttsc#1312).
|
|
1948
1966
|
*
|
|
1949
1967
|
* A failure envelope can retain exact external input spellings from its graph
|
|
1950
|
-
* and host metadata
|
|
1951
|
-
*
|
|
1952
|
-
*
|
|
1953
|
-
*
|
|
1954
|
-
*
|
|
1968
|
+
* and host metadata, including missing resolution candidates on native
|
|
1969
|
+
* typecheck failures. For hosts that return no graph, structured diagnostics or
|
|
1970
|
+
* standard diagnostic lines provide only the paths they actually name. The cost
|
|
1971
|
+
* is paid only on a failure, and only until the next compile succeeds and
|
|
1972
|
+
* narrows the set back to the derived inputs.
|
|
1955
1973
|
*/
|
|
1956
1974
|
function notifyFailedGenerationInputs(
|
|
1957
1975
|
hooks: TtscTransformHooks | undefined,
|
|
@@ -2002,7 +2020,7 @@ function notifyFailedGenerationInputs(
|
|
|
2002
2020
|
}
|
|
2003
2021
|
}
|
|
2004
2022
|
if (addWatchFiles !== undefined) {
|
|
2005
|
-
addWatchFiles(inputs);
|
|
2023
|
+
addWatchFiles(inputs, true);
|
|
2006
2024
|
return;
|
|
2007
2025
|
}
|
|
2008
2026
|
for (const input of inputs) {
|
|
@@ -2749,6 +2767,28 @@ function matchesNarrowPersistentInputs(
|
|
|
2749
2767
|
return true;
|
|
2750
2768
|
}
|
|
2751
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
|
+
|
|
2752
2792
|
/**
|
|
2753
2793
|
* Validate one derived input against the generation, skipping the content read
|
|
2754
2794
|
* while the recorded metadata signature still holds and its freshly minted
|
|
@@ -2776,6 +2816,12 @@ function matchesProvenInput(
|
|
|
2776
2816
|
state: TtscEnvelopeDerivation,
|
|
2777
2817
|
input: string,
|
|
2778
2818
|
): boolean {
|
|
2819
|
+
if (
|
|
2820
|
+
trackerProvesInputUnchanged(cached.projectMutationTracker, input) ||
|
|
2821
|
+
trackerProvesInputUnchanged(cached.hostInputMutationTracker, input)
|
|
2822
|
+
) {
|
|
2823
|
+
return true;
|
|
2824
|
+
}
|
|
2779
2825
|
const observation = cached.externalInputObservations?.[path.resolve(input)];
|
|
2780
2826
|
if (observation !== undefined) {
|
|
2781
2827
|
if (
|
|
@@ -2785,7 +2831,25 @@ function matchesProvenInput(
|
|
|
2785
2831
|
) {
|
|
2786
2832
|
return true;
|
|
2787
2833
|
}
|
|
2788
|
-
|
|
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;
|
|
2789
2853
|
}
|
|
2790
2854
|
const slot = inputSignatureSlot(cached, state, input);
|
|
2791
2855
|
if (slot === undefined) {
|
|
@@ -2839,12 +2903,7 @@ function notifiesAbsence(
|
|
|
2839
2903
|
cached: TtscCachedProjectTransform,
|
|
2840
2904
|
input: string,
|
|
2841
2905
|
): boolean {
|
|
2842
|
-
|
|
2843
|
-
return (
|
|
2844
|
-
tracker !== undefined &&
|
|
2845
|
-
!tracker.failed &&
|
|
2846
|
-
tracker.covered?.has(path.resolve(input)) === true
|
|
2847
|
-
);
|
|
2906
|
+
return trackerProvesInputUnchanged(cached.candidateMutationTracker, input);
|
|
2848
2907
|
}
|
|
2849
2908
|
|
|
2850
2909
|
/**
|
|
@@ -2905,6 +2964,14 @@ function matchesUniversalHostInputs(
|
|
|
2905
2964
|
cached: TtscCachedProjectTransform,
|
|
2906
2965
|
validation: TtscHostInputValidation,
|
|
2907
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;
|
|
2908
2975
|
return (
|
|
2909
2976
|
matchesUniversalHostInputEntries(cached, validation) &&
|
|
2910
2977
|
matchesUniversalHostInputProbes(cached, validation)
|
|
@@ -2939,6 +3006,15 @@ function matchesUniversalHostInputEntries(
|
|
|
2939
3006
|
if (!matchesRecordedInput(cached, entry.path)) {
|
|
2940
3007
|
return false;
|
|
2941
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
|
+
}
|
|
2942
3018
|
if (evidence === undefined) return false;
|
|
2943
3019
|
// Re-earn the proof under the rules the capture applies: an entry whose
|
|
2944
3020
|
// recorded state came from reading nothing keeps its content comparison, a
|
|
@@ -3177,6 +3253,43 @@ function captureUniversalHostInputValidation(
|
|
|
3177
3253
|
return { failures, validation };
|
|
3178
3254
|
}
|
|
3179
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
|
+
|
|
3180
3293
|
/**
|
|
3181
3294
|
* The recorded state of an input the generation read nothing from: absent, or
|
|
3182
3295
|
* present but unreadable. It is deliberately not a hash, so no signature may
|
|
@@ -3313,6 +3426,8 @@ function disposeFilesystemClockReference(referenceDirectory: string): void {
|
|
|
3313
3426
|
interface TtscInputMetadataEvidence {
|
|
3314
3427
|
/** The joined metadata signature of the lexical path and its link target. */
|
|
3315
3428
|
signature: string;
|
|
3429
|
+
/** Whether a directory observer can account for every content mutation. */
|
|
3430
|
+
notificationAuthoritative: boolean;
|
|
3316
3431
|
/**
|
|
3317
3432
|
* Whether a later write is guaranteed to move this signature. Only a
|
|
3318
3433
|
* signature captured with this evidence may be recorded to stand in for a
|
|
@@ -3358,6 +3473,7 @@ function inputMetadataEvidence(
|
|
|
3358
3473
|
link.ctimeNs,
|
|
3359
3474
|
"missing-target",
|
|
3360
3475
|
].join(":"),
|
|
3476
|
+
notificationAuthoritative: false,
|
|
3361
3477
|
separable: false,
|
|
3362
3478
|
};
|
|
3363
3479
|
}
|
|
@@ -3366,17 +3482,26 @@ function inputMetadataEvidence(
|
|
|
3366
3482
|
signature: [
|
|
3367
3483
|
link.dev,
|
|
3368
3484
|
link.ino,
|
|
3485
|
+
link.nlink,
|
|
3369
3486
|
link.mode,
|
|
3370
3487
|
link.size,
|
|
3371
3488
|
link.mtimeNs,
|
|
3372
3489
|
link.ctimeNs,
|
|
3373
3490
|
target.dev,
|
|
3374
3491
|
target.ino,
|
|
3492
|
+
target.nlink,
|
|
3375
3493
|
target.mode,
|
|
3376
3494
|
target.size,
|
|
3377
3495
|
target.mtimeNs,
|
|
3378
3496
|
target.ctimeNs,
|
|
3379
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)),
|
|
3380
3505
|
// Both halves must be separable: a write remints the target's stamp, a
|
|
3381
3506
|
// link retarget the link's own, and either one hiding inside its recorded
|
|
3382
3507
|
// tick would evade the skipped content and realpath comparisons.
|
|
@@ -3902,12 +4027,14 @@ function captureExternalInputSnapshot(
|
|
|
3902
4027
|
});
|
|
3903
4028
|
continue;
|
|
3904
4029
|
}
|
|
4030
|
+
const before = inputMetadataEvidence(input, filesystem);
|
|
3905
4031
|
const mismatches = graphInputObservationFailures(
|
|
3906
4032
|
input,
|
|
3907
4033
|
predicateObservation,
|
|
3908
4034
|
filesystem,
|
|
3909
4035
|
state.identityContext,
|
|
3910
4036
|
);
|
|
4037
|
+
const after = inputMetadataSignature(input, filesystem);
|
|
3911
4038
|
if (mismatches.length !== 0) complete = false;
|
|
3912
4039
|
for (const kind of mismatches) {
|
|
3913
4040
|
recordGenerationProofFailure(failures, {
|
|
@@ -3916,6 +4043,7 @@ function captureExternalInputSnapshot(
|
|
|
3916
4043
|
path: input,
|
|
3917
4044
|
});
|
|
3918
4045
|
}
|
|
4046
|
+
if (mismatches.length === 0) record(input, before, after);
|
|
3919
4047
|
observations[spelling] = predicateObservation;
|
|
3920
4048
|
continue;
|
|
3921
4049
|
}
|
|
@@ -4291,12 +4419,14 @@ function collectProjectInputSnapshot(
|
|
|
4291
4419
|
directoryComplete: boolean;
|
|
4292
4420
|
fileSignatures: Record<string, string>;
|
|
4293
4421
|
hashes: Record<string, string>;
|
|
4422
|
+
notificationUnsafeInputs: Set<string>;
|
|
4294
4423
|
projectDirectories: TtscProjectDirectorySnapshot[];
|
|
4295
4424
|
provenSignatures: Record<string, string>;
|
|
4296
4425
|
unstableFiles: Set<string>;
|
|
4297
4426
|
walkFailures: TtscProjectWalkFailure[];
|
|
4298
4427
|
} {
|
|
4299
4428
|
const hashes: Record<string, string> = {};
|
|
4429
|
+
const notificationUnsafeInputs = new Set<string>();
|
|
4300
4430
|
const fileSignatures: Record<string, string> = {};
|
|
4301
4431
|
const provenSignatures: Record<string, string> = {};
|
|
4302
4432
|
const unstableFiles = new Set<string>();
|
|
@@ -4321,6 +4451,9 @@ function collectProjectInputSnapshot(
|
|
|
4321
4451
|
continue;
|
|
4322
4452
|
}
|
|
4323
4453
|
const before = inputMetadataEvidence(file, filesystem);
|
|
4454
|
+
if (before?.notificationAuthoritative !== true) {
|
|
4455
|
+
notificationUnsafeInputs.add(key);
|
|
4456
|
+
}
|
|
4324
4457
|
// A file whose signature still equals the one captured around the read
|
|
4325
4458
|
// that produced the recorded hash carries that content, so the whole
|
|
4326
4459
|
// project does not have to be re-read to prove one delivery. Recheck the
|
|
@@ -4339,24 +4472,27 @@ function collectProjectInputSnapshot(
|
|
|
4339
4472
|
continue;
|
|
4340
4473
|
}
|
|
4341
4474
|
const contents = filesystem.readFile(file);
|
|
4342
|
-
const after =
|
|
4475
|
+
const after = inputMetadataEvidence(file, filesystem);
|
|
4476
|
+
if (after?.notificationAuthoritative !== true) {
|
|
4477
|
+
notificationUnsafeInputs.add(key);
|
|
4478
|
+
}
|
|
4343
4479
|
hashes[key] = hashText(contents);
|
|
4344
4480
|
if (
|
|
4345
4481
|
before === undefined ||
|
|
4346
4482
|
after === undefined ||
|
|
4347
|
-
before.signature !== after
|
|
4483
|
+
before.signature !== after.signature
|
|
4348
4484
|
) {
|
|
4349
4485
|
complete = false;
|
|
4350
4486
|
unstableFiles.add(key);
|
|
4351
4487
|
walkFailures.push({ kind: "file-changed-during-read", path: file });
|
|
4352
4488
|
} else {
|
|
4353
|
-
fileSignatures[key] = after;
|
|
4489
|
+
fileSignatures[key] = after.signature;
|
|
4354
4490
|
// Only a signature whose stamp's tick the filesystem's clock provably
|
|
4355
4491
|
// left before this read may later stand in for the content comparison
|
|
4356
4492
|
// ({@link stampSeparable}); the raw signature above still participates
|
|
4357
4493
|
// in the generation-time stability comparison.
|
|
4358
4494
|
if (before.separable) {
|
|
4359
|
-
provenSignatures[key] = after;
|
|
4495
|
+
provenSignatures[key] = after.signature;
|
|
4360
4496
|
}
|
|
4361
4497
|
}
|
|
4362
4498
|
} catch {
|
|
@@ -4378,6 +4514,7 @@ function collectProjectInputSnapshot(
|
|
|
4378
4514
|
directoryComplete: walked.complete && attributed,
|
|
4379
4515
|
fileSignatures,
|
|
4380
4516
|
hashes,
|
|
4517
|
+
notificationUnsafeInputs,
|
|
4381
4518
|
projectDirectories: walked.directories,
|
|
4382
4519
|
provenSignatures,
|
|
4383
4520
|
unstableFiles,
|
|
@@ -4618,13 +4755,14 @@ function openDirectoryWatch(
|
|
|
4618
4755
|
directory: string,
|
|
4619
4756
|
listener: (eventType: string, filename: string | null) => void,
|
|
4620
4757
|
onError: () => void,
|
|
4758
|
+
recursive = false,
|
|
4621
4759
|
): { close: () => void } {
|
|
4622
4760
|
if (filesystem.watch !== undefined) {
|
|
4623
|
-
return filesystem.watch(directory, listener, onError);
|
|
4761
|
+
return filesystem.watch(directory, listener, onError, recursive);
|
|
4624
4762
|
}
|
|
4625
4763
|
const watcher = fs.watch(
|
|
4626
4764
|
directory,
|
|
4627
|
-
{ persistent: false },
|
|
4765
|
+
{ persistent: false, recursive },
|
|
4628
4766
|
(eventType, filename) =>
|
|
4629
4767
|
listener(eventType, filename === null ? null : String(filename)),
|
|
4630
4768
|
);
|
|
@@ -4653,140 +4791,289 @@ function closeDirectoryWatches(watchers: { close: () => void }[]): void {
|
|
|
4653
4791
|
/** Watch every walked directory for membership changes after generation. */
|
|
4654
4792
|
async function createProjectMutationTracker(
|
|
4655
4793
|
directories: readonly TtscProjectDirectorySnapshot[],
|
|
4794
|
+
covered: ReadonlySet<string>,
|
|
4656
4795
|
filesystem: TtscTransformFilesystemOperations = DEFAULT_FILESYSTEM_OPERATIONS,
|
|
4657
4796
|
policy: ITtscProjectMembershipPolicy = PERMISSIVE_PROJECT_MEMBERSHIP_POLICY,
|
|
4658
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;
|
|
4659
4812
|
const tracker: TtscProjectMutationTracker = {
|
|
4660
4813
|
changes: new Set(),
|
|
4661
4814
|
changesOmitted: false,
|
|
4662
|
-
close: () =>
|
|
4815
|
+
close: () => {
|
|
4816
|
+
tracker.failed = true;
|
|
4817
|
+
},
|
|
4818
|
+
covered: authoritative,
|
|
4663
4819
|
failed: false,
|
|
4664
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
|
+
);
|
|
4665
4852
|
};
|
|
4666
4853
|
if (process.platform === "win32" && filesystem.watch === undefined) {
|
|
4667
4854
|
await registerWindowsProjectMutationTracker(
|
|
4668
4855
|
tracker,
|
|
4669
|
-
|
|
4856
|
+
[{ directory: root, recursive: true }],
|
|
4670
4857
|
false,
|
|
4671
4858
|
filesystem,
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
policy,
|
|
4677
|
-
filesystem,
|
|
4678
|
-
),
|
|
4859
|
+
reportsMembership,
|
|
4860
|
+
(_location, filename) =>
|
|
4861
|
+
isPossibleProgramFileName(path.basename(filename), policy),
|
|
4862
|
+
reportsNewMembership,
|
|
4679
4863
|
);
|
|
4680
4864
|
return tracker;
|
|
4681
4865
|
}
|
|
4682
4866
|
const watchers: { close: () => void }[] = [];
|
|
4683
|
-
tracker.close = () =>
|
|
4684
|
-
|
|
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
|
-
} catch {
|
|
4718
|
-
tracker.failed = true;
|
|
4719
|
-
}
|
|
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;
|
|
4720
4901
|
}
|
|
4721
4902
|
return tracker;
|
|
4722
4903
|
}
|
|
4723
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
|
+
|
|
4724
4922
|
/** Watch exact universal inputs, or their nearest existing parent if missing. */
|
|
4725
4923
|
async function createHostInputMutationTracker(
|
|
4726
4924
|
inputs: readonly string[],
|
|
4727
4925
|
filesystem: TtscTransformFilesystemOperations,
|
|
4728
4926
|
covered: ReadonlySet<string>,
|
|
4729
4927
|
events: "all" | "rename" = "all",
|
|
4928
|
+
preferredRoot?: string,
|
|
4730
4929
|
): Promise<TtscProjectMutationTracker> {
|
|
4731
4930
|
const identities = createHostPathIdentityContext(filesystem);
|
|
4732
|
-
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<
|
|
4733
4962
|
string,
|
|
4734
|
-
{
|
|
4963
|
+
{
|
|
4964
|
+
directory: string;
|
|
4965
|
+
names: Set<string>;
|
|
4966
|
+
paths?: Set<string>;
|
|
4967
|
+
recursive?: boolean;
|
|
4968
|
+
}
|
|
4735
4969
|
>();
|
|
4970
|
+
const internalRoot =
|
|
4971
|
+
preferredRoot === undefined ? undefined : path.resolve(preferredRoot);
|
|
4736
4972
|
for (const input of inputs) {
|
|
4737
4973
|
const absolute = path.resolve(input);
|
|
4738
4974
|
const probe = filesystem.exists(absolute)
|
|
4739
4975
|
? { directory: path.dirname(absolute), name: path.basename(absolute) }
|
|
4740
4976
|
: missingPathProbe(absolute, filesystem);
|
|
4741
|
-
const
|
|
4742
|
-
|
|
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);
|
|
4743
4982
|
if (location === undefined) {
|
|
4744
4983
|
location = {
|
|
4745
4984
|
directory: directoryIdentity.path,
|
|
4746
4985
|
names: new Set<string>(),
|
|
4986
|
+
...(internal ? { paths: new Set<string>(), recursive: true } : {}),
|
|
4747
4987
|
};
|
|
4748
|
-
|
|
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
|
+
);
|
|
4749
5001
|
}
|
|
4750
|
-
location.names.add(
|
|
4751
|
-
normalizeHostInputName(
|
|
4752
|
-
probe.name,
|
|
4753
|
-
identities.caseSensitive(directoryIdentity.path),
|
|
4754
|
-
),
|
|
4755
|
-
);
|
|
4756
5002
|
}
|
|
4757
|
-
const locations = [...
|
|
4758
|
-
directory: location.directory,
|
|
4759
|
-
names: [...location.names],
|
|
4760
|
-
}));
|
|
5003
|
+
const locations = [...locationsByDirectory.values()];
|
|
4761
5004
|
const tracker: TtscProjectMutationTracker = {
|
|
4762
5005
|
changes: new Set(),
|
|
4763
5006
|
changesOmitted: false,
|
|
4764
|
-
close: () =>
|
|
5007
|
+
close: () => {
|
|
5008
|
+
tracker.failed = true;
|
|
5009
|
+
},
|
|
4765
5010
|
// Coverage is the caller's claim, and it is required rather than derived
|
|
4766
5011
|
// from the input list: an input is watched by its exact name here, but only
|
|
4767
5012
|
// the caller knows whether the path leading to it is watched as well, which
|
|
4768
5013
|
// is what a later validation needs before it trusts the watcher instead of
|
|
4769
5014
|
// probing the path again. Deriving it here would hand that claim to every
|
|
4770
5015
|
// future caller by default (samchon/ttsc#1261).
|
|
4771
|
-
covered,
|
|
5016
|
+
covered: authoritative,
|
|
5017
|
+
contentAuthoritative: filesystem.watch === undefined,
|
|
4772
5018
|
failed: false,
|
|
4773
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
|
+
);
|
|
4774
5053
|
};
|
|
4775
5054
|
if (process.platform === "win32" && filesystem.watch === undefined) {
|
|
4776
5055
|
await registerWindowsProjectMutationTracker(
|
|
4777
5056
|
tracker,
|
|
4778
|
-
locations
|
|
5057
|
+
locations.map((location) => ({
|
|
5058
|
+
directory: location.directory,
|
|
5059
|
+
...(location.recursive === true
|
|
5060
|
+
? { recursive: true }
|
|
5061
|
+
: { names: [...location.names] }),
|
|
5062
|
+
})),
|
|
4779
5063
|
events === "all",
|
|
4780
5064
|
filesystem,
|
|
5065
|
+
matches,
|
|
5066
|
+
matches,
|
|
4781
5067
|
);
|
|
4782
5068
|
return tracker;
|
|
4783
5069
|
}
|
|
4784
5070
|
const watchers: { close: () => void }[] = [];
|
|
4785
|
-
tracker.close = () =>
|
|
5071
|
+
tracker.close = () => {
|
|
5072
|
+
tracker.failed = true;
|
|
5073
|
+
closeDirectoryWatches(watchers);
|
|
5074
|
+
};
|
|
4786
5075
|
for (const location of locations) {
|
|
4787
5076
|
try {
|
|
4788
|
-
const names = new Set(location.names);
|
|
4789
|
-
const caseSensitive = identities.caseSensitive(location.directory);
|
|
4790
5077
|
watchers.push(
|
|
4791
5078
|
openDirectoryWatch(
|
|
4792
5079
|
filesystem,
|
|
@@ -4795,22 +5082,22 @@ async function createHostInputMutationTracker(
|
|
|
4795
5082
|
if (events === "rename" && eventType !== "rename") {
|
|
4796
5083
|
return;
|
|
4797
5084
|
}
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
? null
|
|
4801
|
-
: normalizeHostInputName(filename, caseSensitive);
|
|
4802
|
-
if (reported === null || names.has(reported)) {
|
|
4803
|
-
recordProjectMutation(
|
|
4804
|
-
tracker,
|
|
5085
|
+
if (filename === null || matches(location.directory, filename)) {
|
|
5086
|
+
const changed =
|
|
4805
5087
|
filename === null
|
|
4806
5088
|
? location.directory
|
|
4807
|
-
: path.join(location.directory, filename)
|
|
4808
|
-
)
|
|
5089
|
+
: path.join(location.directory, filename);
|
|
5090
|
+
if (eventType === "rename") {
|
|
5091
|
+
recordProjectMutation(tracker, changed);
|
|
5092
|
+
} else {
|
|
5093
|
+
recordProjectChange(tracker, changed);
|
|
5094
|
+
}
|
|
4809
5095
|
}
|
|
4810
5096
|
},
|
|
4811
5097
|
() => {
|
|
4812
5098
|
tracker.failed = true;
|
|
4813
5099
|
},
|
|
5100
|
+
location.recursive === true,
|
|
4814
5101
|
),
|
|
4815
5102
|
);
|
|
4816
5103
|
} catch {
|
|
@@ -4820,6 +5107,47 @@ async function createHostInputMutationTracker(
|
|
|
4820
5107
|
return tracker;
|
|
4821
5108
|
}
|
|
4822
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
|
+
|
|
4823
5151
|
/**
|
|
4824
5152
|
* Whether a path lies inside a directory the configuration excludes.
|
|
4825
5153
|
*
|
|
@@ -4914,6 +5242,14 @@ function recordProjectMutation(
|
|
|
4914
5242
|
changed: string,
|
|
4915
5243
|
): void {
|
|
4916
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 {
|
|
4917
5253
|
if (tracker.changes.has(changed)) return;
|
|
4918
5254
|
if (tracker.changes.size < MAX_GENERATION_MUTATION_PATHS) {
|
|
4919
5255
|
tracker.changes.add(changed);
|
|
@@ -4941,6 +5277,10 @@ interface WindowsProjectMutationBroker {
|
|
|
4941
5277
|
* have already narrowed theirs by construction.
|
|
4942
5278
|
*/
|
|
4943
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;
|
|
4944
5284
|
ready: () => void;
|
|
4945
5285
|
/**
|
|
4946
5286
|
* The walk's own spelling for each canonical directory the child watches,
|
|
@@ -4962,6 +5302,7 @@ let windowsProjectMutationBroker: WindowsProjectMutationBroker | undefined;
|
|
|
4962
5302
|
interface WindowsMutationLocation {
|
|
4963
5303
|
directory: string;
|
|
4964
5304
|
names?: string[];
|
|
5305
|
+
recursive?: boolean;
|
|
4965
5306
|
}
|
|
4966
5307
|
|
|
4967
5308
|
/**
|
|
@@ -4982,6 +5323,8 @@ async function registerWindowsProjectMutationTracker(
|
|
|
4982
5323
|
* name-watching trackers pass none, since they already watch exact names.
|
|
4983
5324
|
*/
|
|
4984
5325
|
membership?: (location: string, filename: string) => boolean,
|
|
5326
|
+
content?: (location: string, filename: string) => boolean,
|
|
5327
|
+
changeAddsMembership?: (location: string, filename: string) => boolean,
|
|
4985
5328
|
): Promise<void> {
|
|
4986
5329
|
const broker = getWindowsProjectMutationBroker();
|
|
4987
5330
|
// The child watches canonical directories, and reports its events under that
|
|
@@ -5002,24 +5345,30 @@ async function registerWindowsProjectMutationTracker(
|
|
|
5002
5345
|
return {
|
|
5003
5346
|
directory,
|
|
5004
5347
|
...(location.names === undefined ? {} : { names: location.names }),
|
|
5348
|
+
...(location.recursive === true ? { recursive: true } : {}),
|
|
5005
5349
|
};
|
|
5006
5350
|
});
|
|
5007
5351
|
broker.pendingRegistrations += 1;
|
|
5008
5352
|
broker.child.ref();
|
|
5009
|
-
|
|
5353
|
+
// Bun's IPC channel omits Node's Control.ref/unref methods. The child itself
|
|
5354
|
+
// still owns the outstanding acknowledgement on that runtime.
|
|
5355
|
+
broker.child.channel?.ref?.();
|
|
5010
5356
|
const id = broker.nextId++;
|
|
5011
5357
|
let resolveReady!: () => void;
|
|
5012
5358
|
const ready = new Promise<void>((resolve) => {
|
|
5013
5359
|
resolveReady = resolve;
|
|
5014
5360
|
});
|
|
5015
5361
|
broker.trackers.set(id, {
|
|
5016
|
-
|
|
5362
|
+
...(changeAddsMembership === undefined ? {} : { changeAddsMembership }),
|
|
5363
|
+
...(content === undefined ? {} : { content }),
|
|
5364
|
+
...(membership === undefined ? {} : { membership }),
|
|
5017
5365
|
ready: resolveReady,
|
|
5018
5366
|
spellings,
|
|
5019
5367
|
tracker,
|
|
5020
5368
|
});
|
|
5021
5369
|
tracker.drain = () => drainWindowsProjectMutationBroker(broker);
|
|
5022
5370
|
tracker.close = () => {
|
|
5371
|
+
tracker.failed = true;
|
|
5023
5372
|
const active = broker.trackers.get(id);
|
|
5024
5373
|
if (active === undefined) return;
|
|
5025
5374
|
broker.trackers.delete(id);
|
|
@@ -5071,7 +5420,7 @@ async function registerWindowsProjectMutationTracker(
|
|
|
5071
5420
|
// exit mid-build.
|
|
5072
5421
|
if (broker.pendingRegistrations === 0 && broker.pendingDrains === 0) {
|
|
5073
5422
|
broker.child.unref();
|
|
5074
|
-
broker.child.channel?.unref();
|
|
5423
|
+
broker.child.channel?.unref?.();
|
|
5075
5424
|
}
|
|
5076
5425
|
}
|
|
5077
5426
|
}
|
|
@@ -5116,6 +5465,7 @@ function getWindowsProjectMutationBroker(): WindowsProjectMutationBroker {
|
|
|
5116
5465
|
drained?: boolean;
|
|
5117
5466
|
failed?: boolean;
|
|
5118
5467
|
filename?: string | null;
|
|
5468
|
+
eventType?: string;
|
|
5119
5469
|
id?: number;
|
|
5120
5470
|
ready?: boolean;
|
|
5121
5471
|
};
|
|
@@ -5138,19 +5488,33 @@ function getWindowsProjectMutationBroker(): WindowsProjectMutationBroker {
|
|
|
5138
5488
|
// comparison and every recorded witness downstream expects.
|
|
5139
5489
|
const reported =
|
|
5140
5490
|
registration.spellings.get(record.directory) ?? record.directory;
|
|
5491
|
+
const changed =
|
|
5492
|
+
typeof record.filename === "string"
|
|
5493
|
+
? path.join(reported, record.filename)
|
|
5494
|
+
: reported;
|
|
5141
5495
|
if (
|
|
5142
5496
|
typeof record.filename === "string" &&
|
|
5143
|
-
registration.membership !== undefined
|
|
5144
|
-
!registration.membership(reported, record.filename)
|
|
5497
|
+
registration.membership !== undefined
|
|
5145
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
|
+
}
|
|
5146
5515
|
return;
|
|
5147
5516
|
}
|
|
5148
|
-
recordProjectMutation(
|
|
5149
|
-
registration.tracker,
|
|
5150
|
-
typeof record.filename === "string"
|
|
5151
|
-
? path.join(reported, record.filename)
|
|
5152
|
-
: reported,
|
|
5153
|
-
);
|
|
5517
|
+
recordProjectMutation(registration.tracker, changed);
|
|
5154
5518
|
} else {
|
|
5155
5519
|
registration.tracker.membershipChanged = true;
|
|
5156
5520
|
}
|
|
@@ -5199,7 +5563,7 @@ function startWindowsProjectMutationDrain(
|
|
|
5199
5563
|
broker.pendingDrains -= 1;
|
|
5200
5564
|
if (broker.pendingDrains === 0 && broker.pendingRegistrations === 0) {
|
|
5201
5565
|
broker.child.unref();
|
|
5202
|
-
broker.child.channel?.unref();
|
|
5566
|
+
broker.child.channel?.unref?.();
|
|
5203
5567
|
}
|
|
5204
5568
|
resolve();
|
|
5205
5569
|
};
|
|
@@ -5210,7 +5574,7 @@ function startWindowsProjectMutationDrain(
|
|
|
5210
5574
|
// process exits mid-build with nothing to report.
|
|
5211
5575
|
broker.pendingDrains += 1;
|
|
5212
5576
|
broker.child.ref();
|
|
5213
|
-
broker.child.channel?.ref();
|
|
5577
|
+
broker.child.channel?.ref?.();
|
|
5214
5578
|
const timer = setTimeout(release, WINDOWS_MUTATION_DRAIN_FALLBACK_MS);
|
|
5215
5579
|
broker.drains.set(id, release);
|
|
5216
5580
|
if (broker.child.send?.({ id, op: "drain" }) !== true) {
|
|
@@ -5242,9 +5606,9 @@ const WINDOWS_WATCH_BROKER_SOURCE = [
|
|
|
5242
5606
|
" for (const location of message.locations) {",
|
|
5243
5607
|
" try {",
|
|
5244
5608
|
" const names = location.names === undefined ? undefined : new Set(location.names.map((name) => name.toLowerCase()));",
|
|
5245
|
-
" 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) => {",
|
|
5246
5610
|
" const matches = names === undefined || filename === null || names.has(String(filename).toLowerCase());",
|
|
5247
|
-
' 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 });',
|
|
5248
5612
|
" });",
|
|
5249
5613
|
' watcher.on("error", () => process.send?.({ failed: true, id: message.id }));',
|
|
5250
5614
|
" watchers.push(watcher);",
|
|
@@ -5266,16 +5630,18 @@ const WINDOWS_WATCH_BROKER_SOURCE = [
|
|
|
5266
5630
|
].join("\n");
|
|
5267
5631
|
|
|
5268
5632
|
/**
|
|
5269
|
-
* Report whether
|
|
5270
|
-
*
|
|
5271
|
-
*
|
|
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.
|
|
5272
5642
|
*/
|
|
5273
5643
|
function reportsMembershipChange(cached: TtscCachedProjectTransform): boolean {
|
|
5274
|
-
return
|
|
5275
|
-
cached.projectMutationTracker?.membershipChanged === true ||
|
|
5276
|
-
cached.hostInputMutationTracker?.membershipChanged === true ||
|
|
5277
|
-
cached.candidateMutationTracker?.membershipChanged === true
|
|
5278
|
-
);
|
|
5644
|
+
return cached.projectMutationTracker?.membershipChanged === true;
|
|
5279
5645
|
}
|
|
5280
5646
|
|
|
5281
5647
|
/**
|
|
@@ -5326,11 +5692,18 @@ function drainOnNextTurn(): Promise<void> {
|
|
|
5326
5692
|
async function settleProjectMutationEvents(
|
|
5327
5693
|
cached: TtscCachedProjectTransform,
|
|
5328
5694
|
): Promise<void> {
|
|
5329
|
-
|
|
5695
|
+
await settleMutationTrackers([
|
|
5330
5696
|
cached.projectMutationTracker,
|
|
5331
5697
|
cached.hostInputMutationTracker,
|
|
5332
5698
|
cached.candidateMutationTracker,
|
|
5333
|
-
]
|
|
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(
|
|
5334
5707
|
(tracker): tracker is TtscProjectMutationTracker => tracker !== undefined,
|
|
5335
5708
|
);
|
|
5336
5709
|
await Promise.all(
|
|
@@ -5712,15 +6085,26 @@ function matchesCachedExternalInputs(cached: TtscCachedProjectTransform): {
|
|
|
5712
6085
|
const spelling = path.resolve(file);
|
|
5713
6086
|
const observation = cached.externalInputObservations?.[spelling];
|
|
5714
6087
|
if (observation !== undefined) {
|
|
6088
|
+
const before = inputMetadataEvidence(file, filesystem);
|
|
5715
6089
|
if (
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
filesystem,
|
|
5720
|
-
state.identityContext,
|
|
5721
|
-
)
|
|
6090
|
+
before !== undefined &&
|
|
6091
|
+
before.separable &&
|
|
6092
|
+
recordedSignatures[spelling] === before.signature
|
|
5722
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) {
|
|
5723
6105
|
matches = false;
|
|
6106
|
+
} else if (before?.separable === true && before.signature === after) {
|
|
6107
|
+
signatures[spelling] = after;
|
|
5724
6108
|
}
|
|
5725
6109
|
continue;
|
|
5726
6110
|
}
|
|
@@ -6328,6 +6712,7 @@ function declaredProjectInputKeys(
|
|
|
6328
6712
|
if (state.declaredInputKeysBuilt !== true) {
|
|
6329
6713
|
state.declaredInputKeys = selectDeclaredProjectInputKeys({
|
|
6330
6714
|
identities: state.identityContext,
|
|
6715
|
+
projectInputHashes: cached.inputHashes,
|
|
6331
6716
|
projectRoot: cached.projectRoot,
|
|
6332
6717
|
result: cached.result,
|
|
6333
6718
|
scratchDirectory: cached.scratchDirectory,
|
|
@@ -6340,11 +6725,16 @@ function declaredProjectInputKeys(
|
|
|
6340
6725
|
/**
|
|
6341
6726
|
* Project-walk keys of every input the envelope declares: the reference graph's
|
|
6342
6727
|
* edge endpoints, globals, config chain, and resolution candidates, plus the
|
|
6343
|
-
* universal host inputs
|
|
6344
|
-
*
|
|
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.
|
|
6345
6734
|
*/
|
|
6346
6735
|
function selectDeclaredProjectInputKeys(props: {
|
|
6347
6736
|
identities: FilesystemPathIdentityContext;
|
|
6737
|
+
projectInputHashes: Readonly<Record<string, string>>;
|
|
6348
6738
|
projectRoot: string;
|
|
6349
6739
|
result: ITtscCompilerTransformation;
|
|
6350
6740
|
scratchDirectory?: string;
|
|
@@ -6358,7 +6748,10 @@ function selectDeclaredProjectInputKeys(props: {
|
|
|
6358
6748
|
if (typeof entry !== "string" || entry.length === 0) return;
|
|
6359
6749
|
const absolute = path.resolve(props.projectRoot, entry);
|
|
6360
6750
|
if (isTransformScratchInput(absolute, props.scratchDirectory)) return;
|
|
6361
|
-
|
|
6751
|
+
const key = toProjectKey(props.projectRoot, absolute, props.identities);
|
|
6752
|
+
if (Object.prototype.hasOwnProperty.call(props.projectInputHashes, key)) {
|
|
6753
|
+
keys.add(key);
|
|
6754
|
+
}
|
|
6362
6755
|
};
|
|
6363
6756
|
for (const [source, targets] of Object.entries(graph.edges ?? {})) {
|
|
6364
6757
|
add(source);
|
|
@@ -6690,6 +7083,7 @@ async function transformProject(props: {
|
|
|
6690
7083
|
deliveryEpoch?: number;
|
|
6691
7084
|
filesystem: TtscTransformFilesystemOperations;
|
|
6692
7085
|
plugins?: ResolvedTtscUnpluginOptions["plugins"];
|
|
7086
|
+
retainProjectMembership: boolean;
|
|
6693
7087
|
trackProjectMembership: boolean;
|
|
6694
7088
|
tsconfig: string;
|
|
6695
7089
|
}): Promise<TtscCachedProjectTransform> {
|
|
@@ -6698,8 +7092,7 @@ async function transformProject(props: {
|
|
|
6698
7092
|
const cached = await captureTransformGeneration(props);
|
|
6699
7093
|
if (
|
|
6700
7094
|
cached.configStateComplete !== false &&
|
|
6701
|
-
(
|
|
6702
|
-
cached.result.type !== "success" ||
|
|
7095
|
+
(cached.result.type !== "success" ||
|
|
6703
7096
|
cached.projectSnapshotComplete === true)
|
|
6704
7097
|
) {
|
|
6705
7098
|
return cached;
|
|
@@ -6738,6 +7131,7 @@ async function captureTransformGeneration(props: {
|
|
|
6738
7131
|
deliveryEpoch?: number;
|
|
6739
7132
|
filesystem: TtscTransformFilesystemOperations;
|
|
6740
7133
|
plugins?: ResolvedTtscUnpluginOptions["plugins"];
|
|
7134
|
+
retainProjectMembership: boolean;
|
|
6741
7135
|
trackProjectMembership: boolean;
|
|
6742
7136
|
tsconfig: string;
|
|
6743
7137
|
}): Promise<TtscCachedProjectTransform> {
|
|
@@ -6756,7 +7150,7 @@ async function captureTransformGeneration(props: {
|
|
|
6756
7150
|
let retainCandidateTracker = false;
|
|
6757
7151
|
let captured: TtscCachedProjectTransform | undefined;
|
|
6758
7152
|
try {
|
|
6759
|
-
if (props.
|
|
7153
|
+
if (props.retainProjectMembership) {
|
|
6760
7154
|
try {
|
|
6761
7155
|
clockReferenceDirectory = createTransformScratchDirectory(
|
|
6762
7156
|
projectRoot,
|
|
@@ -6807,6 +7201,11 @@ async function captureTransformGeneration(props: {
|
|
|
6807
7201
|
tracker = props.trackProjectMembership
|
|
6808
7202
|
? await createProjectMutationTracker(
|
|
6809
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
|
+
),
|
|
6810
7209
|
props.filesystem,
|
|
6811
7210
|
membershipPolicy,
|
|
6812
7211
|
)
|
|
@@ -6846,15 +7245,26 @@ async function captureTransformGeneration(props: {
|
|
|
6846
7245
|
scratchDirectory,
|
|
6847
7246
|
temporaryTsconfig,
|
|
6848
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
|
+
];
|
|
6849
7259
|
// The generation's absent resolution candidates, which get a watcher of
|
|
6850
7260
|
// their own below; watching one is what lets a delivery stop probing it
|
|
6851
7261
|
// (samchon/ttsc#1261). The validation manifest stays built from the
|
|
6852
7262
|
// universal inputs alone, so nothing else about a candidate changes.
|
|
6853
7263
|
//
|
|
6854
|
-
// Derived only where a tracker could carry it: a build-scoped
|
|
6855
|
-
//
|
|
6856
|
-
// whose answer nothing can read.
|
|
6857
|
-
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
|
|
6858
7268
|
? selectNotifiableAbsentInputs({
|
|
6859
7269
|
filesystem: props.filesystem,
|
|
6860
7270
|
projectRoot,
|
|
@@ -6863,14 +7273,18 @@ async function captureTransformGeneration(props: {
|
|
|
6863
7273
|
temporaryTsconfig,
|
|
6864
7274
|
})
|
|
6865
7275
|
: { candidates: [], watched: [] };
|
|
6866
|
-
hostInputTracker = props.
|
|
7276
|
+
hostInputTracker = props.retainProjectMembership
|
|
6867
7277
|
? await createHostInputMutationTracker(
|
|
6868
|
-
|
|
7278
|
+
persistentValidationInputs,
|
|
6869
7279
|
props.filesystem,
|
|
6870
7280
|
// A universal input never reaches the per-input loop that consults a
|
|
6871
7281
|
// coverage claim: an absent one is proven by its directory listing
|
|
6872
7282
|
// instead, which re-resolves the spelling every delivery.
|
|
6873
|
-
new Set(
|
|
7283
|
+
new Set(
|
|
7284
|
+
persistentValidationInputs.map((input) => path.resolve(input)),
|
|
7285
|
+
),
|
|
7286
|
+
"all",
|
|
7287
|
+
projectRoot,
|
|
6874
7288
|
)
|
|
6875
7289
|
: undefined;
|
|
6876
7290
|
// The candidates and the directories carrying them get their own tracker,
|
|
@@ -6887,16 +7301,9 @@ async function captureTransformGeneration(props: {
|
|
|
6887
7301
|
props.filesystem,
|
|
6888
7302
|
new Set(notifiableAbsence.candidates),
|
|
6889
7303
|
"rename",
|
|
7304
|
+
projectRoot,
|
|
6890
7305
|
)
|
|
6891
7306
|
: undefined;
|
|
6892
|
-
const externalInputPaths = selectExternalInputPaths({
|
|
6893
|
-
filesystem: props.filesystem,
|
|
6894
|
-
membershipPolicy,
|
|
6895
|
-
projectRoot,
|
|
6896
|
-
result,
|
|
6897
|
-
scratchDirectory,
|
|
6898
|
-
temporaryTsconfig,
|
|
6899
|
-
});
|
|
6900
7307
|
const inputSnapshot = collectProjectInputSnapshot(
|
|
6901
7308
|
projectRoot,
|
|
6902
7309
|
identities,
|
|
@@ -6911,10 +7318,16 @@ async function captureTransformGeneration(props: {
|
|
|
6911
7318
|
// provable from its own recorded state.
|
|
6912
7319
|
const declaredInputs = selectDeclaredProjectInputKeys({
|
|
6913
7320
|
identities,
|
|
7321
|
+
projectInputHashes: inputSnapshot.hashes,
|
|
6914
7322
|
projectRoot,
|
|
6915
7323
|
result,
|
|
6916
7324
|
scratchDirectory,
|
|
6917
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]);
|
|
6918
7331
|
const walkStable =
|
|
6919
7332
|
configStable &&
|
|
6920
7333
|
walkSnapshotComplete(before, declaredInputs) &&
|
|
@@ -6929,6 +7342,11 @@ async function captureTransformGeneration(props: {
|
|
|
6929
7342
|
before.projectDirectories,
|
|
6930
7343
|
inputSnapshot.projectDirectories,
|
|
6931
7344
|
) &&
|
|
7345
|
+
!trackerChangedDeclaredProjectInput(
|
|
7346
|
+
tracker,
|
|
7347
|
+
declaredInputs,
|
|
7348
|
+
projectRoot,
|
|
7349
|
+
) &&
|
|
6932
7350
|
tracker?.membershipChanged !== true &&
|
|
6933
7351
|
hostInputTracker?.membershipChanged !== true &&
|
|
6934
7352
|
candidateTracker?.membershipChanged !== true;
|
|
@@ -7027,6 +7445,11 @@ async function captureTransformGeneration(props: {
|
|
|
7027
7445
|
cached,
|
|
7028
7446
|
props.currentFile,
|
|
7029
7447
|
);
|
|
7448
|
+
restrictNotificationCoverageToProvenInputs(
|
|
7449
|
+
hostInputTracker,
|
|
7450
|
+
cached,
|
|
7451
|
+
universalInputCapture.validation,
|
|
7452
|
+
);
|
|
7030
7453
|
mergeGenerationProofFailures(failures, universalInputCapture.failures);
|
|
7031
7454
|
const graphProofs =
|
|
7032
7455
|
graphFailures.entries.length === 0 && graphFailures.omitted === 0;
|
|
@@ -7059,7 +7482,10 @@ async function captureTransformGeneration(props: {
|
|
|
7059
7482
|
// Attach notifications only while they can actually prove membership. A
|
|
7060
7483
|
// generation that could not open its watchers keeps its recorded snapshot
|
|
7061
7484
|
// and validates through it, rather than losing the cache entirely.
|
|
7062
|
-
const notifying =
|
|
7485
|
+
const notifying =
|
|
7486
|
+
props.retainProjectMembership &&
|
|
7487
|
+
stableProjectSnapshot &&
|
|
7488
|
+
notificationsAvailable;
|
|
7063
7489
|
if (notifying && tracker !== undefined) {
|
|
7064
7490
|
cached.projectMutationTracker = tracker;
|
|
7065
7491
|
}
|
|
@@ -7152,6 +7578,31 @@ async function captureTransformGeneration(props: {
|
|
|
7152
7578
|
return captured;
|
|
7153
7579
|
}
|
|
7154
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
|
+
|
|
7155
7606
|
/** Exclude disposed transform scratch from live host-input tracking. */
|
|
7156
7607
|
function selectPersistentHostInputs(props: {
|
|
7157
7608
|
filesystem: TtscTransformFilesystemOperations;
|
|
@@ -7858,13 +8309,12 @@ function formatUnknownError(error: unknown): string {
|
|
|
7858
8309
|
/**
|
|
7859
8310
|
* Remove terminal colour and cursor sequences from text the adapter surfaces.
|
|
7860
8311
|
*
|
|
7861
|
-
* An
|
|
7862
|
-
*
|
|
7863
|
-
*
|
|
7864
|
-
*
|
|
7865
|
-
*
|
|
7866
|
-
*
|
|
7867
|
-
* (samchon/ttsc#1312).
|
|
8312
|
+
* An opaque host exception can contain the host's own rendered output, colour
|
|
8313
|
+
* and all, with no structured diagnostics to format instead. What the adapter
|
|
8314
|
+
* hands back is not going to a terminal: it becomes the `Error` a bundler
|
|
8315
|
+
* reports, so it lands in a Vite overlay, a webpack error report or a CI
|
|
8316
|
+
* annotation, where the escapes render as literal noise around the file and
|
|
8317
|
+
* line the reader needs (samchon/ttsc#1312).
|
|
7868
8318
|
*
|
|
7869
8319
|
* The colour originates in the host's rendering rather than in anything this
|
|
7870
8320
|
* adapter configures, so this is the adapter-side repair, applied to every
|