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