@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/lib/core/transform.js
CHANGED
|
@@ -65,14 +65,13 @@ class TtscUnstableGenerationError extends TtscTerminalGenerationError {
|
|
|
65
65
|
/**
|
|
66
66
|
* A compile this pass already attempted, whose envelope failed outright.
|
|
67
67
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* made a single broken save cost one compile per delivered module
|
|
68
|
+
* Native project diagnostics carry a structured failure envelope; setup and
|
|
69
|
+
* host failures can still arrive as opaque exceptions. Both settle the current
|
|
70
|
+
* attempt, so the adapter uses the delivery boundary it owns rather than
|
|
71
|
+
* guessing retryability from a diagnostic message. Inside a pass the answer is
|
|
72
|
+
* already settled, so every later module replays it instead of repeating a
|
|
73
|
+
* whole-project transform to reach the same verdict, which is what made a
|
|
74
|
+
* single broken save cost one compile per delivered module
|
|
76
75
|
* (samchon/ttsc#1303).
|
|
77
76
|
*
|
|
78
77
|
* The scope is exactly the pass. A host whose `buildStart` repeats drops the
|
|
@@ -121,6 +120,8 @@ const TERMINAL_TRANSFORM_GENERATIONS = new WeakMap();
|
|
|
121
120
|
const MAX_GENERATION_PROOF_FAILURES = 8;
|
|
122
121
|
/** Maximum exact mutation paths kept after a tracker already proved a change. */
|
|
123
122
|
const MAX_GENERATION_MUTATION_PATHS = 8;
|
|
123
|
+
/** Maximum unrelated roots watched before snapshot validation takes over. */
|
|
124
|
+
const MAX_HOST_INPUT_WATCH_SCOPES = 16;
|
|
124
125
|
/** One retry absorbs a transient watch write without admitting an infinite loop. */
|
|
125
126
|
const TRANSFORM_GENERATION_ATTEMPTS = 2;
|
|
126
127
|
const DEFAULT_FILESYSTEM_OPERATIONS = Object.freeze({
|
|
@@ -383,6 +384,11 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
|
|
|
383
384
|
deliveryEpoch: epoch,
|
|
384
385
|
filesystem,
|
|
385
386
|
plugins: options.plugins,
|
|
387
|
+
// One bounded recursive project observer witnesses content restored
|
|
388
|
+
// during the compile itself. Build-scoped adapters close it with the
|
|
389
|
+
// attempt; persistent adapters retain it to make later validations
|
|
390
|
+
// constant-cost while the generation remains live.
|
|
391
|
+
retainProjectMembership: cache !== undefined && epoch === undefined,
|
|
386
392
|
trackProjectMembership: cache !== undefined,
|
|
387
393
|
tsconfig,
|
|
388
394
|
});
|
|
@@ -1051,11 +1057,11 @@ function collectDeclaredIdentities(state, projectRoot, listed) {
|
|
|
1051
1057
|
* invalidated, and the error stays on screen (samchon/ttsc#1312).
|
|
1052
1058
|
*
|
|
1053
1059
|
* A failure envelope can retain exact external input spellings from its graph
|
|
1054
|
-
* and host metadata
|
|
1055
|
-
*
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
1058
|
-
*
|
|
1060
|
+
* and host metadata, including missing resolution candidates on native
|
|
1061
|
+
* typecheck failures. For hosts that return no graph, structured diagnostics or
|
|
1062
|
+
* standard diagnostic lines provide only the paths they actually name. The cost
|
|
1063
|
+
* is paid only on a failure, and only until the next compile succeeds and
|
|
1064
|
+
* narrows the set back to the derived inputs.
|
|
1059
1065
|
*/
|
|
1060
1066
|
function notifyFailedGenerationInputs(hooks, cached) {
|
|
1061
1067
|
const addWatchFile = hooks?.addWatchFile;
|
|
@@ -1100,7 +1106,7 @@ function notifyFailedGenerationInputs(hooks, cached) {
|
|
|
1100
1106
|
}
|
|
1101
1107
|
}
|
|
1102
1108
|
if (addWatchFiles !== undefined) {
|
|
1103
|
-
addWatchFiles(inputs);
|
|
1109
|
+
addWatchFiles(inputs, true);
|
|
1104
1110
|
return;
|
|
1105
1111
|
}
|
|
1106
1112
|
for (const input of inputs) {
|
|
@@ -1680,6 +1686,24 @@ function matchesNarrowPersistentInputs(cached, file) {
|
|
|
1680
1686
|
}
|
|
1681
1687
|
return true;
|
|
1682
1688
|
}
|
|
1689
|
+
/** Whether a healthy notification scope proves one exact input unchanged. */
|
|
1690
|
+
function trackerProvesInputUnchanged(tracker, input) {
|
|
1691
|
+
if (tracker === undefined || tracker.failed || tracker.changesOmitted) {
|
|
1692
|
+
return false;
|
|
1693
|
+
}
|
|
1694
|
+
if (tracker.contentAuthoritative !== true)
|
|
1695
|
+
return false;
|
|
1696
|
+
const absolute = path.resolve(input);
|
|
1697
|
+
if (tracker.covered?.has(absolute) !== true)
|
|
1698
|
+
return false;
|
|
1699
|
+
for (const changed of tracker.changes) {
|
|
1700
|
+
if (tracker.overlaps?.(absolute, changed) ??
|
|
1701
|
+
(pathIsWithin(absolute, changed) || pathIsWithin(changed, absolute))) {
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
return true;
|
|
1706
|
+
}
|
|
1683
1707
|
/**
|
|
1684
1708
|
* Validate one derived input against the generation, skipping the content read
|
|
1685
1709
|
* while the recorded metadata signature still holds and its freshly minted
|
|
@@ -1703,6 +1727,10 @@ function matchesNarrowPersistentInputs(cached, file) {
|
|
|
1703
1727
|
* junction moves it and the skipped realpath comparison cannot be evaded.
|
|
1704
1728
|
*/
|
|
1705
1729
|
function matchesProvenInput(cached, state, input) {
|
|
1730
|
+
if (trackerProvesInputUnchanged(cached.projectMutationTracker, input) ||
|
|
1731
|
+
trackerProvesInputUnchanged(cached.hostInputMutationTracker, input)) {
|
|
1732
|
+
return true;
|
|
1733
|
+
}
|
|
1706
1734
|
const observation = cached.externalInputObservations?.[path.resolve(input)];
|
|
1707
1735
|
if (observation !== undefined) {
|
|
1708
1736
|
if (observation.fileExists === false &&
|
|
@@ -1710,7 +1738,25 @@ function matchesProvenInput(cached, state, input) {
|
|
|
1710
1738
|
notifiesAbsence(cached, input)) {
|
|
1711
1739
|
return true;
|
|
1712
1740
|
}
|
|
1713
|
-
|
|
1741
|
+
const filesystem = resultFilesystem(cached.result);
|
|
1742
|
+
const spelling = path.resolve(input);
|
|
1743
|
+
const before = inputMetadataEvidence(input, filesystem);
|
|
1744
|
+
if (before !== undefined &&
|
|
1745
|
+
before.separable &&
|
|
1746
|
+
cached.externalInputSignatures?.[spelling] === before.signature) {
|
|
1747
|
+
return true;
|
|
1748
|
+
}
|
|
1749
|
+
if (!matchesRecordedInput(cached, input))
|
|
1750
|
+
return false;
|
|
1751
|
+
const after = inputMetadataSignature(input, filesystem);
|
|
1752
|
+
const signatures = (cached.externalInputSignatures ??= {});
|
|
1753
|
+
if (before?.separable === true && before.signature === after) {
|
|
1754
|
+
signatures[spelling] = after;
|
|
1755
|
+
}
|
|
1756
|
+
else {
|
|
1757
|
+
delete signatures[spelling];
|
|
1758
|
+
}
|
|
1759
|
+
return true;
|
|
1714
1760
|
}
|
|
1715
1761
|
const slot = inputSignatureSlot(cached, state, input);
|
|
1716
1762
|
if (slot === undefined) {
|
|
@@ -1758,10 +1804,7 @@ function matchesProvenInput(cached, state, input) {
|
|
|
1758
1804
|
* the whole generation back to complete-snapshot validation.
|
|
1759
1805
|
*/
|
|
1760
1806
|
function notifiesAbsence(cached, input) {
|
|
1761
|
-
|
|
1762
|
-
return (tracker !== undefined &&
|
|
1763
|
-
!tracker.failed &&
|
|
1764
|
-
tracker.covered?.has(path.resolve(input)) === true);
|
|
1807
|
+
return trackerProvesInputUnchanged(cached.candidateMutationTracker, input);
|
|
1765
1808
|
}
|
|
1766
1809
|
/**
|
|
1767
1810
|
* Locate the signature manifest that owns one recorded input, mirroring
|
|
@@ -1807,6 +1850,15 @@ function inputSignatureSlot(cached, state, input) {
|
|
|
1807
1850
|
* existing directory and checked through one exact membership listing.
|
|
1808
1851
|
*/
|
|
1809
1852
|
function matchesUniversalHostInputs(cached, validation) {
|
|
1853
|
+
let notificationsProveAll = true;
|
|
1854
|
+
for (const input of validation.covered) {
|
|
1855
|
+
if (!trackerProvesInputUnchanged(cached.hostInputMutationTracker, input)) {
|
|
1856
|
+
notificationsProveAll = false;
|
|
1857
|
+
break;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
if (notificationsProveAll)
|
|
1861
|
+
return true;
|
|
1810
1862
|
return (matchesUniversalHostInputEntries(cached, validation) &&
|
|
1811
1863
|
matchesUniversalHostInputProbes(cached, validation));
|
|
1812
1864
|
}
|
|
@@ -1834,6 +1886,11 @@ function matchesUniversalHostInputEntries(cached, validation) {
|
|
|
1834
1886
|
if (!matchesRecordedInput(cached, entry.path)) {
|
|
1835
1887
|
return false;
|
|
1836
1888
|
}
|
|
1889
|
+
if (entry.readable === false) {
|
|
1890
|
+
const slot = inputSignatureSlot(cached, envelopeDerivation(cached), entry.path);
|
|
1891
|
+
entry.readable =
|
|
1892
|
+
slot !== undefined && slot.recorded !== MISSING_INPUT_STATE;
|
|
1893
|
+
}
|
|
1837
1894
|
if (evidence === undefined)
|
|
1838
1895
|
return false;
|
|
1839
1896
|
// Re-earn the proof under the rules the capture applies: an entry whose
|
|
@@ -2042,6 +2099,37 @@ function captureUniversalHostInputValidation(cached, currentFile) {
|
|
|
2042
2099
|
cached.hostInputValidation = validation;
|
|
2043
2100
|
return { failures, validation };
|
|
2044
2101
|
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Keep watcher silence as a content proof only for inputs whose bytes capture
|
|
2104
|
+
* actually proved.
|
|
2105
|
+
*
|
|
2106
|
+
* The observers must open before the post-compile snapshots so they can witness
|
|
2107
|
+
* an A-B-A race. Their exact coverage can only be narrowed after those reads.
|
|
2108
|
+
* Existing-but-unreadable and absent inputs record the same `missing` state;
|
|
2109
|
+
* neither may inherit a content shortcut because its metadata can stay fixed
|
|
2110
|
+
* while bytes become readable. Missing resolver candidates inside the project
|
|
2111
|
+
* retain their separate component-aware candidate tracker, while every other
|
|
2112
|
+
* such input continues through the recorded predicate or directory-list proof.
|
|
2113
|
+
*/
|
|
2114
|
+
function restrictNotificationCoverageToProvenInputs(tracker, cached, hostValidation) {
|
|
2115
|
+
if (!(tracker?.covered instanceof Set))
|
|
2116
|
+
return;
|
|
2117
|
+
const covered = tracker.covered;
|
|
2118
|
+
const state = envelopeDerivation(cached);
|
|
2119
|
+
for (const input of [...covered]) {
|
|
2120
|
+
const absolute = path.resolve(input);
|
|
2121
|
+
const hostEntry = hostValidation?.entries.get(absolute);
|
|
2122
|
+
if ((hostValidation?.covered.has(absolute) === true &&
|
|
2123
|
+
(hostEntry === undefined ||
|
|
2124
|
+
(hostEntry.readable === false && hostEntry.strict !== true))) ||
|
|
2125
|
+
cached.externalInputHashes?.[derivationIdentity(state, absolute)] ===
|
|
2126
|
+
MISSING_INPUT_STATE ||
|
|
2127
|
+
cached.externalInputObservations?.[absolute]?.readFile?.ok === false ||
|
|
2128
|
+
cached.externalInputObservations?.[absolute]?.fileExists === false) {
|
|
2129
|
+
covered.delete(input);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2045
2133
|
/**
|
|
2046
2134
|
* The recorded state of an input the generation read nothing from: absent, or
|
|
2047
2135
|
* present but unreadable. It is deliberately not a hash, so no signature may
|
|
@@ -2188,6 +2276,7 @@ function inputMetadataEvidence(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS)
|
|
|
2188
2276
|
link.ctimeNs,
|
|
2189
2277
|
"missing-target",
|
|
2190
2278
|
].join(":"),
|
|
2279
|
+
notificationAuthoritative: false,
|
|
2191
2280
|
separable: false,
|
|
2192
2281
|
};
|
|
2193
2282
|
}
|
|
@@ -2196,17 +2285,25 @@ function inputMetadataEvidence(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS)
|
|
|
2196
2285
|
signature: [
|
|
2197
2286
|
link.dev,
|
|
2198
2287
|
link.ino,
|
|
2288
|
+
link.nlink,
|
|
2199
2289
|
link.mode,
|
|
2200
2290
|
link.size,
|
|
2201
2291
|
link.mtimeNs,
|
|
2202
2292
|
link.ctimeNs,
|
|
2203
2293
|
target.dev,
|
|
2204
2294
|
target.ino,
|
|
2295
|
+
target.nlink,
|
|
2205
2296
|
target.mode,
|
|
2206
2297
|
target.size,
|
|
2207
2298
|
target.mtimeNs,
|
|
2208
2299
|
target.ctimeNs,
|
|
2209
2300
|
].join(":"),
|
|
2301
|
+
// A write through a hardlink outside the watched tree changes this inode
|
|
2302
|
+
// without notifying either the original directory or a watcher opened on
|
|
2303
|
+
// the original path. Keep such files on metadata validation. Symlinks are
|
|
2304
|
+
// likewise governed by the target path, outside the lexical observer.
|
|
2305
|
+
notificationAuthoritative: !link.isSymbolicLink() &&
|
|
2306
|
+
!(link.isFile() && (link.nlink > 1n || target.nlink > 1n)),
|
|
2210
2307
|
// Both halves must be separable: a write remints the target's stamp, a
|
|
2211
2308
|
// link retarget the link's own, and either one hiding inside its recorded
|
|
2212
2309
|
// tick would evade the skipped content and realpath comparisons.
|
|
@@ -2601,7 +2698,9 @@ function captureExternalInputSnapshot(cached, paths) {
|
|
|
2601
2698
|
});
|
|
2602
2699
|
continue;
|
|
2603
2700
|
}
|
|
2701
|
+
const before = inputMetadataEvidence(input, filesystem);
|
|
2604
2702
|
const mismatches = graphInputObservationFailures(input, predicateObservation, filesystem, state.identityContext);
|
|
2703
|
+
const after = inputMetadataSignature(input, filesystem);
|
|
2605
2704
|
if (mismatches.length !== 0)
|
|
2606
2705
|
complete = false;
|
|
2607
2706
|
for (const kind of mismatches) {
|
|
@@ -2611,6 +2710,8 @@ function captureExternalInputSnapshot(cached, paths) {
|
|
|
2611
2710
|
path: input,
|
|
2612
2711
|
});
|
|
2613
2712
|
}
|
|
2713
|
+
if (mismatches.length === 0)
|
|
2714
|
+
record(input, before, after);
|
|
2614
2715
|
observations[spelling] = predicateObservation;
|
|
2615
2716
|
continue;
|
|
2616
2717
|
}
|
|
@@ -2878,6 +2979,7 @@ function collectProjectInputHashSnapshot(projectRoot, identities = createHostPat
|
|
|
2878
2979
|
/** Hash project files and snapshot the directory topology in one walk. */
|
|
2879
2980
|
function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, proven, options) {
|
|
2880
2981
|
const hashes = {};
|
|
2982
|
+
const notificationUnsafeInputs = new Set();
|
|
2881
2983
|
const fileSignatures = {};
|
|
2882
2984
|
const provenSignatures = {};
|
|
2883
2985
|
const unstableFiles = new Set();
|
|
@@ -2900,6 +3002,9 @@ function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAU
|
|
|
2900
3002
|
continue;
|
|
2901
3003
|
}
|
|
2902
3004
|
const before = inputMetadataEvidence(file, filesystem);
|
|
3005
|
+
if (before?.notificationAuthoritative !== true) {
|
|
3006
|
+
notificationUnsafeInputs.add(key);
|
|
3007
|
+
}
|
|
2903
3008
|
// A file whose signature still equals the one captured around the read
|
|
2904
3009
|
// that produced the recorded hash carries that content, so the whole
|
|
2905
3010
|
// project does not have to be re-read to prove one delivery. Recheck the
|
|
@@ -2916,23 +3021,26 @@ function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAU
|
|
|
2916
3021
|
continue;
|
|
2917
3022
|
}
|
|
2918
3023
|
const contents = filesystem.readFile(file);
|
|
2919
|
-
const after =
|
|
3024
|
+
const after = inputMetadataEvidence(file, filesystem);
|
|
3025
|
+
if (after?.notificationAuthoritative !== true) {
|
|
3026
|
+
notificationUnsafeInputs.add(key);
|
|
3027
|
+
}
|
|
2920
3028
|
hashes[key] = hashText(contents);
|
|
2921
3029
|
if (before === undefined ||
|
|
2922
3030
|
after === undefined ||
|
|
2923
|
-
before.signature !== after) {
|
|
3031
|
+
before.signature !== after.signature) {
|
|
2924
3032
|
complete = false;
|
|
2925
3033
|
unstableFiles.add(key);
|
|
2926
3034
|
walkFailures.push({ kind: "file-changed-during-read", path: file });
|
|
2927
3035
|
}
|
|
2928
3036
|
else {
|
|
2929
|
-
fileSignatures[key] = after;
|
|
3037
|
+
fileSignatures[key] = after.signature;
|
|
2930
3038
|
// Only a signature whose stamp's tick the filesystem's clock provably
|
|
2931
3039
|
// left before this read may later stand in for the content comparison
|
|
2932
3040
|
// ({@link stampSeparable}); the raw signature above still participates
|
|
2933
3041
|
// in the generation-time stability comparison.
|
|
2934
3042
|
if (before.separable) {
|
|
2935
|
-
provenSignatures[key] = after;
|
|
3043
|
+
provenSignatures[key] = after.signature;
|
|
2936
3044
|
}
|
|
2937
3045
|
}
|
|
2938
3046
|
}
|
|
@@ -2956,6 +3064,7 @@ function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAU
|
|
|
2956
3064
|
directoryComplete: walked.complete && attributed,
|
|
2957
3065
|
fileSignatures,
|
|
2958
3066
|
hashes,
|
|
3067
|
+
notificationUnsafeInputs,
|
|
2959
3068
|
projectDirectories: walked.directories,
|
|
2960
3069
|
provenSignatures,
|
|
2961
3070
|
unstableFiles,
|
|
@@ -3150,11 +3259,11 @@ function sameProjectDirectories(left, right) {
|
|
|
3150
3259
|
* underlying watch does, so callers classify a registration failure
|
|
3151
3260
|
* themselves.
|
|
3152
3261
|
*/
|
|
3153
|
-
function openDirectoryWatch(filesystem, directory, listener, onError) {
|
|
3262
|
+
function openDirectoryWatch(filesystem, directory, listener, onError, recursive = false) {
|
|
3154
3263
|
if (filesystem.watch !== undefined) {
|
|
3155
|
-
return filesystem.watch(directory, listener, onError);
|
|
3264
|
+
return filesystem.watch(directory, listener, onError, recursive);
|
|
3156
3265
|
}
|
|
3157
|
-
const watcher = fs.watch(directory, { persistent: false }, (eventType, filename) => listener(eventType, filename === null ? null : String(filename)));
|
|
3266
|
+
const watcher = fs.watch(directory, { persistent: false, recursive }, (eventType, filename) => listener(eventType, filename === null ? null : String(filename)));
|
|
3158
3267
|
watcher.on("error", onError);
|
|
3159
3268
|
return { close: () => watcher.close() };
|
|
3160
3269
|
}
|
|
@@ -3178,106 +3287,223 @@ function closeDirectoryWatches(watchers) {
|
|
|
3178
3287
|
throw failure;
|
|
3179
3288
|
}
|
|
3180
3289
|
/** Watch every walked directory for membership changes after generation. */
|
|
3181
|
-
async function createProjectMutationTracker(directories, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, policy = tsconfigPaths.PERMISSIVE_PROJECT_MEMBERSHIP_POLICY) {
|
|
3290
|
+
async function createProjectMutationTracker(directories, covered, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, policy = tsconfigPaths.PERMISSIVE_PROJECT_MEMBERSHIP_POLICY) {
|
|
3291
|
+
const identities = createHostPathIdentityContext(filesystem);
|
|
3292
|
+
const root = commonDirectoryRoot(directories.map((directory) => directory.path));
|
|
3293
|
+
const authoritative = root !== undefined &&
|
|
3294
|
+
filesystem.watch === undefined &&
|
|
3295
|
+
pathTraversesSymbolicLink(path.join(root, ".ttsc-notification-authority"), filesystem, new Map())
|
|
3296
|
+
? new Set()
|
|
3297
|
+
: covered;
|
|
3182
3298
|
const tracker = {
|
|
3183
3299
|
changes: new Set(),
|
|
3184
3300
|
changesOmitted: false,
|
|
3185
|
-
close: () =>
|
|
3301
|
+
close: () => {
|
|
3302
|
+
tracker.failed = true;
|
|
3303
|
+
},
|
|
3304
|
+
covered: authoritative,
|
|
3186
3305
|
failed: false,
|
|
3187
3306
|
membershipChanged: false,
|
|
3307
|
+
overlaps: (input, changed) => identities.isWithin(input, changed) ||
|
|
3308
|
+
identities.isWithin(changed, input),
|
|
3309
|
+
contentAuthoritative: filesystem.watch === undefined,
|
|
3310
|
+
};
|
|
3311
|
+
if (root === undefined)
|
|
3312
|
+
return tracker;
|
|
3313
|
+
const knownDirectories = new Set(directories
|
|
3314
|
+
.filter((directory) => directory.relevant)
|
|
3315
|
+
.map((directory) => path.resolve(directory.path)));
|
|
3316
|
+
const reportsMembership = (location, filename) => {
|
|
3317
|
+
const changed = path.join(location, filename);
|
|
3318
|
+
return (knownDirectories.has(path.resolve(changed)) ||
|
|
3319
|
+
reportsProgramMembership(changed, path.basename(filename), policy, filesystem));
|
|
3320
|
+
};
|
|
3321
|
+
const reportsNewMembership = (location, filename) => {
|
|
3322
|
+
const changed = path.resolve(location, filename);
|
|
3323
|
+
return (!knownDirectories.has(changed) && tracker.covered?.has(changed) !== true);
|
|
3188
3324
|
};
|
|
3189
3325
|
if (process.platform === "win32" && filesystem.watch === undefined) {
|
|
3190
|
-
await registerWindowsProjectMutationTracker(tracker,
|
|
3326
|
+
await registerWindowsProjectMutationTracker(tracker, [{ directory: root, recursive: true }], false, filesystem, reportsMembership, (_location, filename) => isPossibleProgramFileName(path.basename(filename), policy), reportsNewMembership);
|
|
3191
3327
|
return tracker;
|
|
3192
3328
|
}
|
|
3193
3329
|
const watchers = [];
|
|
3194
|
-
tracker.close = () =>
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
recordProjectMutation(tracker,
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
tracker
|
|
3210
|
-
}
|
|
3211
|
-
}
|
|
3212
|
-
catch {
|
|
3330
|
+
tracker.close = () => {
|
|
3331
|
+
tracker.failed = true;
|
|
3332
|
+
closeDirectoryWatches(watchers);
|
|
3333
|
+
};
|
|
3334
|
+
try {
|
|
3335
|
+
watchers.push(openDirectoryWatch(filesystem, root, (eventType, filename) => {
|
|
3336
|
+
const changed = filename === null ? root : path.join(root, filename);
|
|
3337
|
+
const membership = filename === null || reportsMembership(root, filename);
|
|
3338
|
+
if (membership &&
|
|
3339
|
+
(eventType === "rename" ||
|
|
3340
|
+
reportsNewMembership(root, filename ?? ""))) {
|
|
3341
|
+
recordProjectMutation(tracker, changed);
|
|
3342
|
+
}
|
|
3343
|
+
else if (filename === null ||
|
|
3344
|
+
isPossibleProgramFileName(path.basename(filename), policy)) {
|
|
3345
|
+
recordProjectChange(tracker, changed);
|
|
3346
|
+
}
|
|
3347
|
+
}, () => {
|
|
3213
3348
|
tracker.failed = true;
|
|
3214
|
-
}
|
|
3349
|
+
}, true));
|
|
3350
|
+
}
|
|
3351
|
+
catch {
|
|
3352
|
+
tracker.failed = true;
|
|
3215
3353
|
}
|
|
3216
3354
|
return tracker;
|
|
3217
3355
|
}
|
|
3356
|
+
/** Common ancestor owned by every project directory snapshot. */
|
|
3357
|
+
function commonDirectoryRoot(directories) {
|
|
3358
|
+
if (directories.length === 0)
|
|
3359
|
+
return undefined;
|
|
3360
|
+
let root = path.resolve(directories[0]);
|
|
3361
|
+
for (const directory of directories.slice(1)) {
|
|
3362
|
+
const absolute = path.resolve(directory);
|
|
3363
|
+
while (!pathIsWithin(absolute, root)) {
|
|
3364
|
+
const parent = path.dirname(root);
|
|
3365
|
+
if (parent === root)
|
|
3366
|
+
return root;
|
|
3367
|
+
root = parent;
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
return root;
|
|
3371
|
+
}
|
|
3218
3372
|
/** Watch exact universal inputs, or their nearest existing parent if missing. */
|
|
3219
|
-
async function createHostInputMutationTracker(inputs, filesystem, covered, events = "all") {
|
|
3373
|
+
async function createHostInputMutationTracker(inputs, filesystem, covered, events = "all", preferredRoot) {
|
|
3220
3374
|
const identities = createHostPathIdentityContext(filesystem);
|
|
3221
|
-
const
|
|
3375
|
+
const linkedAncestors = new Map();
|
|
3376
|
+
const authoritative = new Set([...covered]
|
|
3377
|
+
.map((input) => path.resolve(input))
|
|
3378
|
+
.filter((input) => {
|
|
3379
|
+
if (pathTraversesSymbolicLink(input, filesystem, linkedAncestors)) {
|
|
3380
|
+
return false;
|
|
3381
|
+
}
|
|
3382
|
+
try {
|
|
3383
|
+
// A lexical symlink can keep its own directory silent while a target
|
|
3384
|
+
// elsewhere changes or appears. Its joined metadata proof must stay
|
|
3385
|
+
// on the validation path, including while the link is broken.
|
|
3386
|
+
const lexical = filesystem.lstat(input);
|
|
3387
|
+
if (lexical.isSymbolicLink() ||
|
|
3388
|
+
(lexical.isFile() && lexical.nlink > 1n))
|
|
3389
|
+
return false;
|
|
3390
|
+
}
|
|
3391
|
+
catch {
|
|
3392
|
+
// A genuinely absent lexical path is covered by its nearest existing
|
|
3393
|
+
// ancestor and remains eligible for notification proof.
|
|
3394
|
+
}
|
|
3395
|
+
const target = hostInputRealpath(input, filesystem);
|
|
3396
|
+
return (target === null ||
|
|
3397
|
+
pathIdentityKey(target, identities) ===
|
|
3398
|
+
pathIdentityKey(input, identities));
|
|
3399
|
+
}));
|
|
3400
|
+
const locationsByDirectory = new Map();
|
|
3401
|
+
const internalRoot = preferredRoot === undefined ? undefined : path.resolve(preferredRoot);
|
|
3222
3402
|
for (const input of inputs) {
|
|
3223
3403
|
const absolute = path.resolve(input);
|
|
3224
3404
|
const probe = filesystem.exists(absolute)
|
|
3225
3405
|
? { directory: path.dirname(absolute), name: path.basename(absolute) }
|
|
3226
3406
|
: missingPathProbe(absolute, filesystem);
|
|
3227
|
-
const
|
|
3228
|
-
|
|
3407
|
+
const internal = internalRoot !== undefined && pathIsWithin(absolute, internalRoot);
|
|
3408
|
+
const directory = internal ? internalRoot : probe.directory;
|
|
3409
|
+
const directoryIdentity = identities.resolve(directory);
|
|
3410
|
+
let location = locationsByDirectory.get(directoryIdentity.key);
|
|
3229
3411
|
if (location === undefined) {
|
|
3230
3412
|
location = {
|
|
3231
3413
|
directory: directoryIdentity.path,
|
|
3232
3414
|
names: new Set(),
|
|
3415
|
+
...(internal ? { paths: new Set(), recursive: true } : {}),
|
|
3233
3416
|
};
|
|
3234
|
-
|
|
3417
|
+
locationsByDirectory.set(directoryIdentity.key, location);
|
|
3418
|
+
}
|
|
3419
|
+
if (location.paths !== undefined) {
|
|
3420
|
+
location.paths.add(pathIdentityKey(path.resolve(probe.directory, probe.name), identities));
|
|
3421
|
+
}
|
|
3422
|
+
else {
|
|
3423
|
+
location.names.add(normalizeHostInputName(probe.name, identities.caseSensitive(directoryIdentity.path)));
|
|
3235
3424
|
}
|
|
3236
|
-
location.names.add(normalizeHostInputName(probe.name, identities.caseSensitive(directoryIdentity.path)));
|
|
3237
3425
|
}
|
|
3238
|
-
const locations = [...
|
|
3239
|
-
directory: location.directory,
|
|
3240
|
-
names: [...location.names],
|
|
3241
|
-
}));
|
|
3426
|
+
const locations = [...locationsByDirectory.values()];
|
|
3242
3427
|
const tracker = {
|
|
3243
3428
|
changes: new Set(),
|
|
3244
3429
|
changesOmitted: false,
|
|
3245
|
-
close: () =>
|
|
3430
|
+
close: () => {
|
|
3431
|
+
tracker.failed = true;
|
|
3432
|
+
},
|
|
3246
3433
|
// Coverage is the caller's claim, and it is required rather than derived
|
|
3247
3434
|
// from the input list: an input is watched by its exact name here, but only
|
|
3248
3435
|
// the caller knows whether the path leading to it is watched as well, which
|
|
3249
3436
|
// is what a later validation needs before it trusts the watcher instead of
|
|
3250
3437
|
// probing the path again. Deriving it here would hand that claim to every
|
|
3251
3438
|
// future caller by default (samchon/ttsc#1261).
|
|
3252
|
-
covered,
|
|
3439
|
+
covered: authoritative,
|
|
3440
|
+
contentAuthoritative: filesystem.watch === undefined,
|
|
3253
3441
|
failed: false,
|
|
3254
3442
|
membershipChanged: false,
|
|
3443
|
+
overlaps: (input, changed) => identities.isWithin(input, changed) ||
|
|
3444
|
+
identities.isWithin(changed, input),
|
|
3445
|
+
};
|
|
3446
|
+
if (locations.length > MAX_HOST_INPUT_WATCH_SCOPES) {
|
|
3447
|
+
// A graph spread over unrelated external roots cannot be folded into one
|
|
3448
|
+
// recursive observer without watching an arbitrarily broad filesystem
|
|
3449
|
+
// ancestor. Decline the notification proof and use the recorded snapshots;
|
|
3450
|
+
// descriptor count must never scale with an adversarial input graph.
|
|
3451
|
+
tracker.failed = true;
|
|
3452
|
+
return tracker;
|
|
3453
|
+
}
|
|
3454
|
+
const matches = (directory, filename) => {
|
|
3455
|
+
const location = locationsByDirectory.get(identities.resolve(directory).key);
|
|
3456
|
+
if (location === undefined)
|
|
3457
|
+
return false;
|
|
3458
|
+
if (location.paths !== undefined) {
|
|
3459
|
+
let changed = path.resolve(directory, filename);
|
|
3460
|
+
for (;;) {
|
|
3461
|
+
if (location.paths.has(pathIdentityKey(changed, identities))) {
|
|
3462
|
+
return true;
|
|
3463
|
+
}
|
|
3464
|
+
const parent = path.dirname(changed);
|
|
3465
|
+
if (parent === changed || !pathIsWithin(parent, location.directory)) {
|
|
3466
|
+
return false;
|
|
3467
|
+
}
|
|
3468
|
+
changed = parent;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
return location.names.has(normalizeHostInputName(filename, identities.caseSensitive(directory)));
|
|
3255
3472
|
};
|
|
3256
3473
|
if (process.platform === "win32" && filesystem.watch === undefined) {
|
|
3257
|
-
await registerWindowsProjectMutationTracker(tracker, locations
|
|
3474
|
+
await registerWindowsProjectMutationTracker(tracker, locations.map((location) => ({
|
|
3475
|
+
directory: location.directory,
|
|
3476
|
+
...(location.recursive === true
|
|
3477
|
+
? { recursive: true }
|
|
3478
|
+
: { names: [...location.names] }),
|
|
3479
|
+
})), events === "all", filesystem, matches, matches);
|
|
3258
3480
|
return tracker;
|
|
3259
3481
|
}
|
|
3260
3482
|
const watchers = [];
|
|
3261
|
-
tracker.close = () =>
|
|
3483
|
+
tracker.close = () => {
|
|
3484
|
+
tracker.failed = true;
|
|
3485
|
+
closeDirectoryWatches(watchers);
|
|
3486
|
+
};
|
|
3262
3487
|
for (const location of locations) {
|
|
3263
3488
|
try {
|
|
3264
|
-
const names = new Set(location.names);
|
|
3265
|
-
const caseSensitive = identities.caseSensitive(location.directory);
|
|
3266
3489
|
watchers.push(openDirectoryWatch(filesystem, location.directory, (eventType, filename) => {
|
|
3267
3490
|
if (events === "rename" && eventType !== "rename") {
|
|
3268
3491
|
return;
|
|
3269
3492
|
}
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
: normalizeHostInputName(filename, caseSensitive);
|
|
3273
|
-
if (reported === null || names.has(reported)) {
|
|
3274
|
-
recordProjectMutation(tracker, filename === null
|
|
3493
|
+
if (filename === null || matches(location.directory, filename)) {
|
|
3494
|
+
const changed = filename === null
|
|
3275
3495
|
? location.directory
|
|
3276
|
-
: path.join(location.directory, filename)
|
|
3496
|
+
: path.join(location.directory, filename);
|
|
3497
|
+
if (eventType === "rename") {
|
|
3498
|
+
recordProjectMutation(tracker, changed);
|
|
3499
|
+
}
|
|
3500
|
+
else {
|
|
3501
|
+
recordProjectChange(tracker, changed);
|
|
3502
|
+
}
|
|
3277
3503
|
}
|
|
3278
3504
|
}, () => {
|
|
3279
3505
|
tracker.failed = true;
|
|
3280
|
-
}));
|
|
3506
|
+
}, location.recursive === true));
|
|
3281
3507
|
}
|
|
3282
3508
|
catch {
|
|
3283
3509
|
tracker.failed = true;
|
|
@@ -3285,6 +3511,45 @@ async function createHostInputMutationTracker(inputs, filesystem, covered, event
|
|
|
3285
3511
|
}
|
|
3286
3512
|
return tracker;
|
|
3287
3513
|
}
|
|
3514
|
+
/**
|
|
3515
|
+
* Whether any lexical component above an input is a symbolic link or junction.
|
|
3516
|
+
*
|
|
3517
|
+
* A recursive observer follows the component to its current physical target.
|
|
3518
|
+
* Retargeting it can therefore move the input without producing an event on the
|
|
3519
|
+
* old target. Keep those spellings on metadata/realpath validation. The memo
|
|
3520
|
+
* makes this one lstat per distinct directory component for the generation,
|
|
3521
|
+
* rather than one walk per graph input.
|
|
3522
|
+
*/
|
|
3523
|
+
function pathTraversesSymbolicLink(input, filesystem, memo) {
|
|
3524
|
+
const visited = [];
|
|
3525
|
+
let current = path.dirname(path.resolve(input));
|
|
3526
|
+
let linked = false;
|
|
3527
|
+
for (;;) {
|
|
3528
|
+
const known = memo.get(current);
|
|
3529
|
+
if (known !== undefined) {
|
|
3530
|
+
linked = known;
|
|
3531
|
+
break;
|
|
3532
|
+
}
|
|
3533
|
+
visited.push(current);
|
|
3534
|
+
try {
|
|
3535
|
+
if (filesystem.lstat(current).isSymbolicLink()) {
|
|
3536
|
+
linked = true;
|
|
3537
|
+
break;
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
catch {
|
|
3541
|
+
// A missing component is not a link. Its nearest existing ancestor will
|
|
3542
|
+
// still be inspected before the walk reaches the volume root.
|
|
3543
|
+
}
|
|
3544
|
+
const parent = path.dirname(current);
|
|
3545
|
+
if (parent === current)
|
|
3546
|
+
break;
|
|
3547
|
+
current = parent;
|
|
3548
|
+
}
|
|
3549
|
+
for (const directory of visited)
|
|
3550
|
+
memo.set(directory, linked);
|
|
3551
|
+
return linked;
|
|
3552
|
+
}
|
|
3288
3553
|
/**
|
|
3289
3554
|
* Whether a path lies inside a directory the configuration excludes.
|
|
3290
3555
|
*
|
|
@@ -3360,6 +3625,10 @@ function reportsProgramMembership(location, filename, policy, filesystem) {
|
|
|
3360
3625
|
/** Record enough exact mutation evidence without retaining an event stream. */
|
|
3361
3626
|
function recordProjectMutation(tracker, changed) {
|
|
3362
3627
|
tracker.membershipChanged = true;
|
|
3628
|
+
recordProjectChange(tracker, changed);
|
|
3629
|
+
}
|
|
3630
|
+
/** Record a content event without classifying it as a membership change. */
|
|
3631
|
+
function recordProjectChange(tracker, changed) {
|
|
3363
3632
|
if (tracker.changes.has(changed))
|
|
3364
3633
|
return;
|
|
3365
3634
|
if (tracker.changes.size < MAX_GENERATION_MUTATION_PATHS) {
|
|
@@ -3383,7 +3652,7 @@ async function registerWindowsProjectMutationTracker(tracker, locations, allEven
|
|
|
3383
3652
|
* narrowed to program membership exactly as the in-process watcher's are. The
|
|
3384
3653
|
* name-watching trackers pass none, since they already watch exact names.
|
|
3385
3654
|
*/
|
|
3386
|
-
membership) {
|
|
3655
|
+
membership, content, changeAddsMembership) {
|
|
3387
3656
|
const broker = getWindowsProjectMutationBroker();
|
|
3388
3657
|
// The child watches canonical directories, and reports its events under that
|
|
3389
3658
|
// spelling. Everything else in the adapter speaks the walk's own spelling,
|
|
@@ -3404,24 +3673,30 @@ membership) {
|
|
|
3404
3673
|
return {
|
|
3405
3674
|
directory,
|
|
3406
3675
|
...(location.names === undefined ? {} : { names: location.names }),
|
|
3676
|
+
...(location.recursive === true ? { recursive: true } : {}),
|
|
3407
3677
|
};
|
|
3408
3678
|
});
|
|
3409
3679
|
broker.pendingRegistrations += 1;
|
|
3410
3680
|
broker.child.ref();
|
|
3411
|
-
|
|
3681
|
+
// Bun's IPC channel omits Node's Control.ref/unref methods. The child itself
|
|
3682
|
+
// still owns the outstanding acknowledgement on that runtime.
|
|
3683
|
+
broker.child.channel?.ref?.();
|
|
3412
3684
|
const id = broker.nextId++;
|
|
3413
3685
|
let resolveReady;
|
|
3414
3686
|
const ready = new Promise((resolve) => {
|
|
3415
3687
|
resolveReady = resolve;
|
|
3416
3688
|
});
|
|
3417
3689
|
broker.trackers.set(id, {
|
|
3418
|
-
|
|
3690
|
+
...(changeAddsMembership === undefined ? {} : { changeAddsMembership }),
|
|
3691
|
+
...(content === undefined ? {} : { content }),
|
|
3692
|
+
...(membership === undefined ? {} : { membership }),
|
|
3419
3693
|
ready: resolveReady,
|
|
3420
3694
|
spellings,
|
|
3421
3695
|
tracker,
|
|
3422
3696
|
});
|
|
3423
3697
|
tracker.drain = () => drainWindowsProjectMutationBroker(broker);
|
|
3424
3698
|
tracker.close = () => {
|
|
3699
|
+
tracker.failed = true;
|
|
3425
3700
|
const active = broker.trackers.get(id);
|
|
3426
3701
|
if (active === undefined)
|
|
3427
3702
|
return;
|
|
@@ -3479,7 +3754,7 @@ membership) {
|
|
|
3479
3754
|
// exit mid-build.
|
|
3480
3755
|
if (broker.pendingRegistrations === 0 && broker.pendingDrains === 0) {
|
|
3481
3756
|
broker.child.unref();
|
|
3482
|
-
broker.child.channel?.unref();
|
|
3757
|
+
broker.child.channel?.unref?.();
|
|
3483
3758
|
}
|
|
3484
3759
|
}
|
|
3485
3760
|
}
|
|
@@ -3543,14 +3818,26 @@ function getWindowsProjectMutationBroker() {
|
|
|
3543
3818
|
// The walk's spelling for this directory, which is what every
|
|
3544
3819
|
// comparison and every recorded witness downstream expects.
|
|
3545
3820
|
const reported = registration.spellings.get(record.directory) ?? record.directory;
|
|
3821
|
+
const changed = typeof record.filename === "string"
|
|
3822
|
+
? path.join(reported, record.filename)
|
|
3823
|
+
: reported;
|
|
3546
3824
|
if (typeof record.filename === "string" &&
|
|
3547
|
-
registration.membership !== undefined
|
|
3548
|
-
|
|
3825
|
+
registration.membership !== undefined) {
|
|
3826
|
+
const membership = registration.membership(reported, record.filename);
|
|
3827
|
+
if (membership &&
|
|
3828
|
+
(record.eventType === "rename" ||
|
|
3829
|
+
registration.changeAddsMembership?.(reported, record.filename) ===
|
|
3830
|
+
true)) {
|
|
3831
|
+
recordProjectMutation(registration.tracker, changed);
|
|
3832
|
+
return;
|
|
3833
|
+
}
|
|
3834
|
+
if (record.eventType !== "rename" &&
|
|
3835
|
+
registration.content?.(reported, record.filename) === true) {
|
|
3836
|
+
recordProjectChange(registration.tracker, changed);
|
|
3837
|
+
}
|
|
3549
3838
|
return;
|
|
3550
3839
|
}
|
|
3551
|
-
recordProjectMutation(registration.tracker,
|
|
3552
|
-
? path.join(reported, record.filename)
|
|
3553
|
-
: reported);
|
|
3840
|
+
recordProjectMutation(registration.tracker, changed);
|
|
3554
3841
|
}
|
|
3555
3842
|
else {
|
|
3556
3843
|
registration.tracker.membershipChanged = true;
|
|
@@ -3595,7 +3882,7 @@ function startWindowsProjectMutationDrain(broker) {
|
|
|
3595
3882
|
broker.pendingDrains -= 1;
|
|
3596
3883
|
if (broker.pendingDrains === 0 && broker.pendingRegistrations === 0) {
|
|
3597
3884
|
broker.child.unref();
|
|
3598
|
-
broker.child.channel?.unref();
|
|
3885
|
+
broker.child.channel?.unref?.();
|
|
3599
3886
|
}
|
|
3600
3887
|
resolve();
|
|
3601
3888
|
};
|
|
@@ -3606,7 +3893,7 @@ function startWindowsProjectMutationDrain(broker) {
|
|
|
3606
3893
|
// process exits mid-build with nothing to report.
|
|
3607
3894
|
broker.pendingDrains += 1;
|
|
3608
3895
|
broker.child.ref();
|
|
3609
|
-
broker.child.channel?.ref();
|
|
3896
|
+
broker.child.channel?.ref?.();
|
|
3610
3897
|
const timer = setTimeout(release, WINDOWS_MUTATION_DRAIN_FALLBACK_MS);
|
|
3611
3898
|
broker.drains.set(id, release);
|
|
3612
3899
|
if (broker.child.send?.({ id, op: "drain" }) !== true) {
|
|
@@ -3636,9 +3923,9 @@ const WINDOWS_WATCH_BROKER_SOURCE = [
|
|
|
3636
3923
|
" for (const location of message.locations) {",
|
|
3637
3924
|
" try {",
|
|
3638
3925
|
" const names = location.names === undefined ? undefined : new Set(location.names.map((name) => name.toLowerCase()));",
|
|
3639
|
-
" const watcher = fs.watch(location.directory, { persistent: false }, (event, filename) => {",
|
|
3926
|
+
" const watcher = fs.watch(location.directory, { persistent: false, recursive: location.recursive === true }, (event, filename) => {",
|
|
3640
3927
|
" const matches = names === undefined || filename === null || names.has(String(filename).toLowerCase());",
|
|
3641
|
-
' if (matches && (message.allEvents || event === "rename")) process.send?.({ directory: location.directory, filename: filename === null ? null : String(filename), id: message.id });',
|
|
3928
|
+
' 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 });',
|
|
3642
3929
|
" });",
|
|
3643
3930
|
' watcher.on("error", () => process.send?.({ failed: true, id: message.id }));',
|
|
3644
3931
|
" watchers.push(watcher);",
|
|
@@ -3659,14 +3946,18 @@ const WINDOWS_WATCH_BROKER_SOURCE = [
|
|
|
3659
3946
|
"}",
|
|
3660
3947
|
].join("\n");
|
|
3661
3948
|
/**
|
|
3662
|
-
* Report whether
|
|
3663
|
-
*
|
|
3664
|
-
*
|
|
3949
|
+
* Report whether the project walk observed a membership event. This is positive
|
|
3950
|
+
* evidence that the program's root set changed, so it outranks the question of
|
|
3951
|
+
* whether the notifications still work.
|
|
3952
|
+
*
|
|
3953
|
+
* Host and resolution-candidate trackers cover the union of every module's
|
|
3954
|
+
* inputs. Their events remain path witnesses: the requested module's narrow
|
|
3955
|
+
* validation decides whether that path is relevant. Promoting one of them to a
|
|
3956
|
+
* project-wide verdict would discard a generation when an unreachable external
|
|
3957
|
+
* input changes, defeating per-file completeness and doing needless compiles.
|
|
3665
3958
|
*/
|
|
3666
3959
|
function reportsMembershipChange(cached) {
|
|
3667
|
-
return
|
|
3668
|
-
cached.hostInputMutationTracker?.membershipChanged === true ||
|
|
3669
|
-
cached.candidateMutationTracker?.membershipChanged === true);
|
|
3960
|
+
return cached.projectMutationTracker?.membershipChanged === true;
|
|
3670
3961
|
}
|
|
3671
3962
|
/**
|
|
3672
3963
|
* Report whether the live notifications can still prove membership. A watcher
|
|
@@ -3708,11 +3999,15 @@ function drainOnNextTurn() {
|
|
|
3708
3999
|
* broker. Concurrent sibling deliveries share the barrier one of them started.
|
|
3709
4000
|
*/
|
|
3710
4001
|
async function settleProjectMutationEvents(cached) {
|
|
3711
|
-
|
|
4002
|
+
await settleMutationTrackers([
|
|
3712
4003
|
cached.projectMutationTracker,
|
|
3713
4004
|
cached.hostInputMutationTracker,
|
|
3714
4005
|
cached.candidateMutationTracker,
|
|
3715
|
-
]
|
|
4006
|
+
]);
|
|
4007
|
+
}
|
|
4008
|
+
/** Settle one set of optional trackers through one shared barrier each. */
|
|
4009
|
+
async function settleMutationTrackers(candidates) {
|
|
4010
|
+
const trackers = candidates.filter((tracker) => tracker !== undefined);
|
|
3716
4011
|
await Promise.all(trackers.map(async (tracker) => {
|
|
3717
4012
|
tracker.settle ??= (tracker.drain ?? drainOnNextTurn)().finally(() => {
|
|
3718
4013
|
tracker.settle = undefined;
|
|
@@ -4018,9 +4313,21 @@ function matchesCachedExternalInputs(cached) {
|
|
|
4018
4313
|
const spelling = path.resolve(file);
|
|
4019
4314
|
const observation = cached.externalInputObservations?.[spelling];
|
|
4020
4315
|
if (observation !== undefined) {
|
|
4021
|
-
|
|
4316
|
+
const before = inputMetadataEvidence(file, filesystem);
|
|
4317
|
+
if (before !== undefined &&
|
|
4318
|
+
before.separable &&
|
|
4319
|
+
recordedSignatures[spelling] === before.signature) {
|
|
4320
|
+
signatures[spelling] = before.signature;
|
|
4321
|
+
continue;
|
|
4322
|
+
}
|
|
4323
|
+
const observed = matchesGraphInputObservation(file, observation, filesystem, state.identityContext);
|
|
4324
|
+
const after = inputMetadataSignature(file, filesystem);
|
|
4325
|
+
if (!observed) {
|
|
4022
4326
|
matches = false;
|
|
4023
4327
|
}
|
|
4328
|
+
else if (before?.separable === true && before.signature === after) {
|
|
4329
|
+
signatures[spelling] = after;
|
|
4330
|
+
}
|
|
4024
4331
|
continue;
|
|
4025
4332
|
}
|
|
4026
4333
|
// Reuse the recorded hash of an out-of-walk input whose signature still
|
|
@@ -4518,6 +4825,7 @@ function declaredProjectInputKeys(state, cached) {
|
|
|
4518
4825
|
if (state.declaredInputKeysBuilt !== true) {
|
|
4519
4826
|
state.declaredInputKeys = selectDeclaredProjectInputKeys({
|
|
4520
4827
|
identities: state.identityContext,
|
|
4828
|
+
projectInputHashes: cached.inputHashes,
|
|
4521
4829
|
projectRoot: cached.projectRoot,
|
|
4522
4830
|
result: cached.result,
|
|
4523
4831
|
scratchDirectory: cached.scratchDirectory,
|
|
@@ -4529,8 +4837,12 @@ function declaredProjectInputKeys(state, cached) {
|
|
|
4529
4837
|
/**
|
|
4530
4838
|
* Project-walk keys of every input the envelope declares: the reference graph's
|
|
4531
4839
|
* edge endpoints, globals, config chain, and resolution candidates, plus the
|
|
4532
|
-
* universal host inputs
|
|
4533
|
-
*
|
|
4840
|
+
* universal host inputs, intersected with the files the project walk actually
|
|
4841
|
+
* hashed. Out-of-walk declarations and candidates carry their own graph proof;
|
|
4842
|
+
* asking the project observer to witness them as well makes unrelated activity
|
|
4843
|
+
* in ignored directories invalidate an otherwise complete generation. Returns
|
|
4844
|
+
* `undefined` for an envelope with no graph, which declares no input set and
|
|
4845
|
+
* therefore keeps whole-walk comparison.
|
|
4534
4846
|
*/
|
|
4535
4847
|
function selectDeclaredProjectInputKeys(props) {
|
|
4536
4848
|
if (props.result.type === "exception" || props.result.graph === undefined) {
|
|
@@ -4544,7 +4856,10 @@ function selectDeclaredProjectInputKeys(props) {
|
|
|
4544
4856
|
const absolute = path.resolve(props.projectRoot, entry);
|
|
4545
4857
|
if (isTransformScratchInput(absolute, props.scratchDirectory))
|
|
4546
4858
|
return;
|
|
4547
|
-
|
|
4859
|
+
const key = toProjectKey(props.projectRoot, absolute, props.identities);
|
|
4860
|
+
if (Object.prototype.hasOwnProperty.call(props.projectInputHashes, key)) {
|
|
4861
|
+
keys.add(key);
|
|
4862
|
+
}
|
|
4548
4863
|
};
|
|
4549
4864
|
for (const [source, targets] of Object.entries(graph.edges ?? {})) {
|
|
4550
4865
|
add(source);
|
|
@@ -4788,8 +5103,7 @@ async function transformProject(props) {
|
|
|
4788
5103
|
for (let attempt = 0; attempt < TRANSFORM_GENERATION_ATTEMPTS; attempt += 1) {
|
|
4789
5104
|
const cached = await captureTransformGeneration(props);
|
|
4790
5105
|
if (cached.configStateComplete !== false &&
|
|
4791
|
-
(
|
|
4792
|
-
cached.result.type !== "success" ||
|
|
5106
|
+
(cached.result.type !== "success" ||
|
|
4793
5107
|
cached.projectSnapshotComplete === true)) {
|
|
4794
5108
|
return cached;
|
|
4795
5109
|
}
|
|
@@ -4821,7 +5135,7 @@ async function captureTransformGeneration(props) {
|
|
|
4821
5135
|
let retainCandidateTracker = false;
|
|
4822
5136
|
let captured;
|
|
4823
5137
|
try {
|
|
4824
|
-
if (props.
|
|
5138
|
+
if (props.retainProjectMembership) {
|
|
4825
5139
|
try {
|
|
4826
5140
|
clockReferenceDirectory = createTransformScratchDirectory(projectRoot, props.filesystem);
|
|
4827
5141
|
}
|
|
@@ -4850,7 +5164,9 @@ async function captureTransformGeneration(props) {
|
|
|
4850
5164
|
const membershipPolicy = tsconfigPaths.mergeMembershipPolicyOverlay(tsconfigState.membershipPolicy, props.compilerOptions, projectRoot);
|
|
4851
5165
|
const before = collectProjectInputSnapshot(projectRoot, identities, props.filesystem, undefined, { policy: membershipPolicy });
|
|
4852
5166
|
tracker = props.trackProjectMembership
|
|
4853
|
-
? await createProjectMutationTracker(before.projectDirectories,
|
|
5167
|
+
? await createProjectMutationTracker(before.projectDirectories, new Set(Object.keys(before.hashes)
|
|
5168
|
+
.filter((key) => !before.notificationUnsafeInputs.has(key))
|
|
5169
|
+
.map((key) => path.resolve(projectRoot, key))), props.filesystem, membershipPolicy)
|
|
4854
5170
|
: undefined;
|
|
4855
5171
|
const result = withTransformScratchEnvironment(scratchDirectory, () => new ttsc.TtscCompiler({
|
|
4856
5172
|
cwd: projectRoot,
|
|
@@ -4881,15 +5197,26 @@ async function captureTransformGeneration(props) {
|
|
|
4881
5197
|
scratchDirectory,
|
|
4882
5198
|
temporaryTsconfig,
|
|
4883
5199
|
});
|
|
5200
|
+
const externalInputPaths = selectExternalInputPaths({
|
|
5201
|
+
filesystem: props.filesystem,
|
|
5202
|
+
membershipPolicy,
|
|
5203
|
+
projectRoot,
|
|
5204
|
+
result,
|
|
5205
|
+
scratchDirectory,
|
|
5206
|
+
temporaryTsconfig,
|
|
5207
|
+
});
|
|
5208
|
+
const persistentValidationInputs = [
|
|
5209
|
+
...new Set([...persistentHostInputs, ...externalInputPaths]),
|
|
5210
|
+
];
|
|
4884
5211
|
// The generation's absent resolution candidates, which get a watcher of
|
|
4885
5212
|
// their own below; watching one is what lets a delivery stop probing it
|
|
4886
5213
|
// (samchon/ttsc#1261). The validation manifest stays built from the
|
|
4887
5214
|
// universal inputs alone, so nothing else about a candidate changes.
|
|
4888
5215
|
//
|
|
4889
|
-
// Derived only where a tracker could carry it: a build-scoped
|
|
4890
|
-
//
|
|
4891
|
-
// whose answer nothing can read.
|
|
4892
|
-
const notifiableAbsence = props.
|
|
5216
|
+
// Derived only where a retained tracker could carry it: a build-scoped
|
|
5217
|
+
// adapter keeps only the compile-time project observer, so probing every
|
|
5218
|
+
// candidate here would be work whose answer nothing can later read.
|
|
5219
|
+
const notifiableAbsence = props.retainProjectMembership
|
|
4893
5220
|
? selectNotifiableAbsentInputs({
|
|
4894
5221
|
filesystem: props.filesystem,
|
|
4895
5222
|
projectRoot,
|
|
@@ -4898,12 +5225,12 @@ async function captureTransformGeneration(props) {
|
|
|
4898
5225
|
temporaryTsconfig,
|
|
4899
5226
|
})
|
|
4900
5227
|
: { candidates: [], watched: [] };
|
|
4901
|
-
hostInputTracker = props.
|
|
4902
|
-
? await createHostInputMutationTracker(
|
|
5228
|
+
hostInputTracker = props.retainProjectMembership
|
|
5229
|
+
? await createHostInputMutationTracker(persistentValidationInputs, props.filesystem,
|
|
4903
5230
|
// A universal input never reaches the per-input loop that consults a
|
|
4904
5231
|
// coverage claim: an absent one is proven by its directory listing
|
|
4905
5232
|
// instead, which re-resolves the spelling every delivery.
|
|
4906
|
-
new Set())
|
|
5233
|
+
new Set(persistentValidationInputs.map((input) => path.resolve(input))), "all", projectRoot)
|
|
4907
5234
|
: undefined;
|
|
4908
5235
|
// The candidates and the directories carrying them get their own tracker,
|
|
4909
5236
|
// listening for renames alone. Every event that can make one of these
|
|
@@ -4914,16 +5241,8 @@ async function captureTransformGeneration(props) {
|
|
|
4914
5241
|
// every time a bundler wrote inside `node_modules`.
|
|
4915
5242
|
candidateTracker =
|
|
4916
5243
|
notifiableAbsence.watched.length !== 0
|
|
4917
|
-
? await createHostInputMutationTracker(notifiableAbsence.watched, props.filesystem, new Set(notifiableAbsence.candidates), "rename")
|
|
5244
|
+
? await createHostInputMutationTracker(notifiableAbsence.watched, props.filesystem, new Set(notifiableAbsence.candidates), "rename", projectRoot)
|
|
4918
5245
|
: undefined;
|
|
4919
|
-
const externalInputPaths = selectExternalInputPaths({
|
|
4920
|
-
filesystem: props.filesystem,
|
|
4921
|
-
membershipPolicy,
|
|
4922
|
-
projectRoot,
|
|
4923
|
-
result,
|
|
4924
|
-
scratchDirectory,
|
|
4925
|
-
temporaryTsconfig,
|
|
4926
|
-
});
|
|
4927
5246
|
const inputSnapshot = collectProjectInputSnapshot(projectRoot, identities, props.filesystem, undefined, { policy: membershipPolicy });
|
|
4928
5247
|
// Whether the recorded snapshot describes one coherent state of the
|
|
4929
5248
|
// project. A membership event during the compile taints it exactly like an
|
|
@@ -4932,16 +5251,23 @@ async function captureTransformGeneration(props) {
|
|
|
4932
5251
|
// provable from its own recorded state.
|
|
4933
5252
|
const declaredInputs = selectDeclaredProjectInputKeys({
|
|
4934
5253
|
identities,
|
|
5254
|
+
projectInputHashes: inputSnapshot.hashes,
|
|
4935
5255
|
projectRoot,
|
|
4936
5256
|
result,
|
|
4937
5257
|
scratchDirectory,
|
|
4938
5258
|
});
|
|
5259
|
+
// The before/after snapshots prove bytes and metadata. Drain the watcher
|
|
5260
|
+
// opened before compilation as the independent A-B-A witness: a producer
|
|
5261
|
+
// can restore both bytes and timestamps before the second walk, but it
|
|
5262
|
+
// cannot withdraw the already queued content event.
|
|
5263
|
+
await settleMutationTrackers([tracker, hostInputTracker, candidateTracker]);
|
|
4939
5264
|
const walkStable = configStable &&
|
|
4940
5265
|
walkSnapshotComplete(before, declaredInputs) &&
|
|
4941
5266
|
walkSnapshotComplete(inputSnapshot, declaredInputs) &&
|
|
4942
5267
|
sameHashes(before.hashes, inputSnapshot.hashes, declaredInputs) &&
|
|
4943
5268
|
sameHashes(before.fileSignatures, inputSnapshot.fileSignatures, declaredInputs) &&
|
|
4944
5269
|
sameProjectDirectories(before.projectDirectories, inputSnapshot.projectDirectories) &&
|
|
5270
|
+
!trackerChangedDeclaredProjectInput(tracker, declaredInputs, projectRoot) &&
|
|
4945
5271
|
tracker?.membershipChanged !== true &&
|
|
4946
5272
|
hostInputTracker?.membershipChanged !== true &&
|
|
4947
5273
|
candidateTracker?.membershipChanged !== true;
|
|
@@ -5023,6 +5349,7 @@ async function captureTransformGeneration(props) {
|
|
|
5023
5349
|
mergeGenerationProofFailures(failures, graphFailures);
|
|
5024
5350
|
mergeGenerationProofFailures(failures, externalInputSnapshot.failures);
|
|
5025
5351
|
const universalInputCapture = captureUniversalHostInputValidation(cached, props.currentFile);
|
|
5352
|
+
restrictNotificationCoverageToProvenInputs(hostInputTracker, cached, universalInputCapture.validation);
|
|
5026
5353
|
mergeGenerationProofFailures(failures, universalInputCapture.failures);
|
|
5027
5354
|
const graphProofs = graphFailures.entries.length === 0 && graphFailures.omitted === 0;
|
|
5028
5355
|
const universalInputs = universalInputCapture.validation !== undefined;
|
|
@@ -5045,7 +5372,9 @@ async function captureTransformGeneration(props) {
|
|
|
5045
5372
|
// Attach notifications only while they can actually prove membership. A
|
|
5046
5373
|
// generation that could not open its watchers keeps its recorded snapshot
|
|
5047
5374
|
// and validates through it, rather than losing the cache entirely.
|
|
5048
|
-
const notifying =
|
|
5375
|
+
const notifying = props.retainProjectMembership &&
|
|
5376
|
+
stableProjectSnapshot &&
|
|
5377
|
+
notificationsAvailable;
|
|
5049
5378
|
if (notifying && tracker !== undefined) {
|
|
5050
5379
|
cached.projectMutationTracker = tracker;
|
|
5051
5380
|
}
|
|
@@ -5137,6 +5466,25 @@ async function captureTransformGeneration(props) {
|
|
|
5137
5466
|
}
|
|
5138
5467
|
return captured;
|
|
5139
5468
|
}
|
|
5469
|
+
/** Whether a compile-time content event overlaps any declared project input. */
|
|
5470
|
+
function trackerChangedDeclaredProjectInput(tracker, declared, projectRoot) {
|
|
5471
|
+
if (tracker === undefined)
|
|
5472
|
+
return false;
|
|
5473
|
+
if (tracker.changesOmitted)
|
|
5474
|
+
return true;
|
|
5475
|
+
if (tracker.changes.size === 0)
|
|
5476
|
+
return false;
|
|
5477
|
+
if (declared === undefined)
|
|
5478
|
+
return true;
|
|
5479
|
+
const inputs = [...declared].map((input) => path.resolve(projectRoot, input));
|
|
5480
|
+
for (const changed of tracker.changes) {
|
|
5481
|
+
if (inputs.some((input) => tracker.overlaps?.(input, changed) ??
|
|
5482
|
+
(pathIsWithin(input, changed) || pathIsWithin(changed, input)))) {
|
|
5483
|
+
return true;
|
|
5484
|
+
}
|
|
5485
|
+
}
|
|
5486
|
+
return false;
|
|
5487
|
+
}
|
|
5140
5488
|
/** Exclude disposed transform scratch from live host-input tracking. */
|
|
5141
5489
|
function selectPersistentHostInputs(props) {
|
|
5142
5490
|
if (props.result.type === "exception")
|
|
@@ -5699,13 +6047,12 @@ function formatUnknownError(error) {
|
|
|
5699
6047
|
/**
|
|
5700
6048
|
* Remove terminal colour and cursor sequences from text the adapter surfaces.
|
|
5701
6049
|
*
|
|
5702
|
-
* An
|
|
5703
|
-
*
|
|
5704
|
-
*
|
|
5705
|
-
*
|
|
5706
|
-
*
|
|
5707
|
-
*
|
|
5708
|
-
* (samchon/ttsc#1312).
|
|
6050
|
+
* An opaque host exception can contain the host's own rendered output, colour
|
|
6051
|
+
* and all, with no structured diagnostics to format instead. What the adapter
|
|
6052
|
+
* hands back is not going to a terminal: it becomes the `Error` a bundler
|
|
6053
|
+
* reports, so it lands in a Vite overlay, a webpack error report or a CI
|
|
6054
|
+
* annotation, where the escapes render as literal noise around the file and
|
|
6055
|
+
* line the reader needs (samchon/ttsc#1312).
|
|
5709
6056
|
*
|
|
5710
6057
|
* The colour originates in the host's rendering rather than in anything this
|
|
5711
6058
|
* adapter configures, so this is the adapter-side repair, applied to every
|