@ttsc/unplugin 0.28.1 → 0.28.3
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/README.md +3 -1
- package/lib/api.d.cts +8 -0
- package/lib/api.d.mts +8 -0
- package/lib/bun-register.d.cts +25 -0
- package/lib/bun-register.d.mts +25 -0
- package/lib/bun.d.cts +95 -0
- package/lib/bun.d.mts +95 -0
- package/lib/core/index.d.cts +23 -0
- package/lib/core/index.d.mts +23 -0
- package/lib/core/index.js +18 -1
- package/lib/core/index.js.map +1 -1
- package/lib/core/index.mjs +19 -2
- package/lib/core/index.mjs.map +1 -1
- package/lib/core/options.d.cts +54 -0
- package/lib/core/options.d.mts +54 -0
- package/lib/core/transform.d.cts +433 -0
- package/lib/core/transform.d.mts +433 -0
- package/lib/core/transform.d.ts +23 -4
- package/lib/core/transform.js +715 -117
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +715 -117
- package/lib/core/transform.mjs.map +1 -1
- package/lib/core/tsconfigPaths.d.cts +29 -0
- package/lib/core/tsconfigPaths.d.mts +29 -0
- package/lib/core/viteServe.d.cts +81 -0
- package/lib/core/viteServe.d.mts +81 -0
- package/lib/esbuild.d.cts +3 -0
- package/lib/esbuild.d.mts +3 -0
- package/lib/farm.d.cts +3 -0
- package/lib/farm.d.mts +3 -0
- package/lib/index.d.cts +12 -0
- package/lib/index.d.mts +12 -0
- package/lib/next.d.cts +37 -0
- package/lib/next.d.mts +37 -0
- package/lib/rolldown.d.cts +3 -0
- package/lib/rolldown.d.mts +3 -0
- package/lib/rollup.d.cts +3 -0
- package/lib/rollup.d.mts +3 -0
- package/lib/rspack.d.cts +3 -0
- package/lib/rspack.d.mts +3 -0
- package/lib/turbopack.d.cts +58 -0
- package/lib/turbopack.d.mts +58 -0
- package/lib/vite.d.cts +3 -0
- package/lib/vite.d.mts +3 -0
- package/lib/webpack.d.cts +3 -0
- package/lib/webpack.d.mts +3 -0
- package/package.json +122 -17
- package/src/core/index.ts +18 -1
- package/src/core/transform.ts +1014 -139
package/src/core/transform.ts
CHANGED
|
@@ -53,8 +53,24 @@ interface TtscProjectDirectorySnapshot {
|
|
|
53
53
|
signature: string;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** One project-walk observation that could not prove a coherent snapshot. */
|
|
57
|
+
interface TtscProjectWalkFailure {
|
|
58
|
+
kind:
|
|
59
|
+
| "directory-changed-during-walk"
|
|
60
|
+
| "directory-metadata-unavailable"
|
|
61
|
+
| "directory-read-failed"
|
|
62
|
+
| "file-changed-during-read"
|
|
63
|
+
| "file-read-failed";
|
|
64
|
+
/** Absolute lexical spelling observed by the walk. */
|
|
65
|
+
path: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
/** Generation-scoped directory watchers used to detect membership changes. */
|
|
57
69
|
interface TtscProjectMutationTracker {
|
|
70
|
+
/** Absolute paths named by generation-time mutation events. */
|
|
71
|
+
changes: Set<string>;
|
|
72
|
+
/** Whether additional event paths were discarded after the witness bound. */
|
|
73
|
+
changesOmitted: boolean;
|
|
58
74
|
close: () => void;
|
|
59
75
|
/**
|
|
60
76
|
* Absolute spellings whose creation, change or removal this tracker would
|
|
@@ -82,6 +98,80 @@ interface TtscProjectMutationTracker {
|
|
|
82
98
|
settle?: Promise<void>;
|
|
83
99
|
}
|
|
84
100
|
|
|
101
|
+
/** One reason a whole-project transform cannot become a reusable generation. */
|
|
102
|
+
interface TtscGenerationProofFailure {
|
|
103
|
+
domain: "external" | "graph" | "host" | "project";
|
|
104
|
+
/** Machine-readable failure class printed verbatim in terminal diagnostics. */
|
|
105
|
+
kind: string;
|
|
106
|
+
/** Optional producer detail, such as the native compiler observation failure. */
|
|
107
|
+
detail?: string;
|
|
108
|
+
/** Absolute lexical spelling of the input or directory that failed proof. */
|
|
109
|
+
path?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Bounded proof witnesses for one transform attempt. */
|
|
113
|
+
interface TtscGenerationProofFailures {
|
|
114
|
+
entries: TtscGenerationProofFailure[];
|
|
115
|
+
omitted: number;
|
|
116
|
+
seen: Set<string>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Filesystem state that may authorize replacing one terminal failed generation. */
|
|
120
|
+
interface TtscFailedGenerationValidation {
|
|
121
|
+
/** Last attempted generation, retained only as a comparison baseline. */
|
|
122
|
+
cached: TtscCachedProjectTransform;
|
|
123
|
+
/** Input keys whose content can affect the generation, or the whole walk. */
|
|
124
|
+
declaredInputs: ReadonlySet<string> | undefined;
|
|
125
|
+
/** Fingerprints of every out-of-walk and exact host input. */
|
|
126
|
+
inputStates: ReadonlyMap<string, string>;
|
|
127
|
+
/** Unmodified on-disk project hashes before the in-memory source overlay. */
|
|
128
|
+
projectInputHashes: Readonly<Record<string, string>>;
|
|
129
|
+
/** Coherence and exact failure state of the final project walk. */
|
|
130
|
+
projectWalkComplete: boolean;
|
|
131
|
+
projectWalkFailures: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A bounded proof failure that stays authoritative until its inputs change. */
|
|
135
|
+
class TtscUnstableGenerationError extends Error {
|
|
136
|
+
public readonly validation: TtscFailedGenerationValidation;
|
|
137
|
+
|
|
138
|
+
public constructor(
|
|
139
|
+
message: string,
|
|
140
|
+
validation: TtscFailedGenerationValidation,
|
|
141
|
+
) {
|
|
142
|
+
super(message);
|
|
143
|
+
this.name = "TtscUnstableGenerationError";
|
|
144
|
+
this.validation = validation;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Proof witnesses retained beside a compiler result without extending its API. */
|
|
149
|
+
const TRANSFORM_GENERATION_FAILURES = new WeakMap<
|
|
150
|
+
ITtscCompilerTransformation,
|
|
151
|
+
TtscGenerationProofFailures
|
|
152
|
+
>();
|
|
153
|
+
|
|
154
|
+
/** Retry baselines retained only for attempts that could not be published. */
|
|
155
|
+
const TRANSFORM_FAILED_GENERATION_VALIDATIONS = new WeakMap<
|
|
156
|
+
ITtscCompilerTransformation,
|
|
157
|
+
TtscFailedGenerationValidation
|
|
158
|
+
>();
|
|
159
|
+
|
|
160
|
+
/** Rejected cache promises whose unchanged terminal verdict may be replayed. */
|
|
161
|
+
const TERMINAL_TRANSFORM_GENERATIONS = new WeakMap<
|
|
162
|
+
Promise<TtscCachedProjectTransform>,
|
|
163
|
+
TtscUnstableGenerationError
|
|
164
|
+
>();
|
|
165
|
+
|
|
166
|
+
/** Maximum witnesses printed and retained for each failed transform attempt. */
|
|
167
|
+
const MAX_GENERATION_PROOF_FAILURES = 8;
|
|
168
|
+
|
|
169
|
+
/** Maximum exact mutation paths kept after a tracker already proved a change. */
|
|
170
|
+
const MAX_GENERATION_MUTATION_PATHS = 8;
|
|
171
|
+
|
|
172
|
+
/** One retry absorbs a transient watch write without admitting an infinite loop. */
|
|
173
|
+
const TRANSFORM_GENERATION_ATTEMPTS = 2;
|
|
174
|
+
|
|
85
175
|
/**
|
|
86
176
|
* A single entry in the project transform cache.
|
|
87
177
|
*
|
|
@@ -149,6 +239,13 @@ export interface TtscCachedProjectTransform {
|
|
|
149
239
|
* disk bytes against the recorded hash, and may record a signature then.
|
|
150
240
|
*/
|
|
151
241
|
inputSignatures?: Record<string, string>;
|
|
242
|
+
/**
|
|
243
|
+
* Raw source hash of every readable key in the transform output, keyed by
|
|
244
|
+
* filesystem identity. Unlike {@link inputHashes}, this includes source
|
|
245
|
+
* outputs outside the project walk without adding arbitrary output keys to
|
|
246
|
+
* the complete project snapshot.
|
|
247
|
+
*/
|
|
248
|
+
sourceHashes?: Record<string, string>;
|
|
152
249
|
/** Metadata snapshot of every directory in the stable generation walk. */
|
|
153
250
|
projectDirectories?: TtscProjectDirectorySnapshot[];
|
|
154
251
|
/** Live notification state for universal host-input changes. */
|
|
@@ -199,13 +296,19 @@ export interface TtscCachedProjectTransform {
|
|
|
199
296
|
* module's first delivery inside the current build.
|
|
200
297
|
*/
|
|
201
298
|
servedFiles?: Set<string>;
|
|
299
|
+
/**
|
|
300
|
+
* Absolute path of the adapter-owned scratch directory used for this
|
|
301
|
+
* generation. It is disposed after compilation, so none of its compiler,
|
|
302
|
+
* resolver, or plugin artifacts can be a persistent cache or watch input.
|
|
303
|
+
*/
|
|
304
|
+
scratchDirectory?: string;
|
|
202
305
|
/**
|
|
203
306
|
* Absolute path of the generated temp-dir tsconfig this compile ran against,
|
|
204
307
|
* when an alias/compiler-options overlay required one. The compiler reports
|
|
205
308
|
* it in the envelope's `graph.configs` chain, but it is disposed right after
|
|
206
309
|
* the compile, so registering it as a watch input would invalidate every
|
|
207
|
-
* bundler cache snapshot on the next build; watch derivation must skip
|
|
208
|
-
*
|
|
310
|
+
* bundler cache snapshot on the next build; watch derivation must skip this
|
|
311
|
+
* path. {@link scratchDirectory} owns the wider disposable-input bound.
|
|
209
312
|
*/
|
|
210
313
|
temporaryTsconfig?: string;
|
|
211
314
|
}
|
|
@@ -488,8 +591,29 @@ export async function transformTtsc(
|
|
|
488
591
|
for (;;) {
|
|
489
592
|
let transformed = cache?.get(key);
|
|
490
593
|
if (transformed !== undefined) {
|
|
491
|
-
|
|
492
|
-
|
|
594
|
+
const terminal = TERMINAL_TRANSFORM_GENERATIONS.get(transformed);
|
|
595
|
+
if (terminal !== undefined) {
|
|
596
|
+
// A proof failure is a verdict about one observed environment, not an
|
|
597
|
+
// invitation for every later module to repeat the whole compile. Keep
|
|
598
|
+
// replaying it until a source/input probe or an explicit cache reset
|
|
599
|
+
// establishes that a new generation could differ.
|
|
600
|
+
if (
|
|
601
|
+
!failedGenerationEnvironmentChanged(terminal.validation, {
|
|
602
|
+
currentFile: file,
|
|
603
|
+
currentSource: source,
|
|
604
|
+
filesystem,
|
|
605
|
+
})
|
|
606
|
+
) {
|
|
607
|
+
throw terminal;
|
|
608
|
+
}
|
|
609
|
+
evictGeneration(cache, key, transformed);
|
|
610
|
+
if (cache?.get(key) !== undefined) {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
transformed = undefined;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (transformed !== undefined) {
|
|
493
617
|
const cached = await awaitOrEvict(cache, key, transformed);
|
|
494
618
|
TRANSFORM_RESULT_FILESYSTEM.set(cached.result, filesystem);
|
|
495
619
|
// While this caller awaited the old Promise, another caller may have
|
|
@@ -575,13 +699,15 @@ export async function transformTtsc(
|
|
|
575
699
|
}
|
|
576
700
|
|
|
577
701
|
/**
|
|
578
|
-
* Await a cached generation,
|
|
702
|
+
* Await a cached generation, retaining only terminal proof failures.
|
|
579
703
|
*
|
|
580
704
|
* The cache stores the in-flight transform Promise before it settles so
|
|
581
|
-
* concurrent callers share one compilation.
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
705
|
+
* concurrent callers share one compilation. Ordinary compiler and host
|
|
706
|
+
* rejections are evicted so a transient failure cannot become permanent. A
|
|
707
|
+
* bounded stabilization failure is different: it already spent its retry and
|
|
708
|
+
* repeating it for every later module recreates the issue this gate prevents.
|
|
709
|
+
* It stays authoritative until its retained input baseline changes or the cache
|
|
710
|
+
* owner starts a new lifecycle.
|
|
585
711
|
*/
|
|
586
712
|
async function awaitOrEvict(
|
|
587
713
|
cache: TtscTransformCache | undefined,
|
|
@@ -591,7 +717,14 @@ async function awaitOrEvict(
|
|
|
591
717
|
try {
|
|
592
718
|
return await generation;
|
|
593
719
|
} catch (error) {
|
|
594
|
-
|
|
720
|
+
if (
|
|
721
|
+
error instanceof TtscUnstableGenerationError &&
|
|
722
|
+
cache?.get(key) === generation
|
|
723
|
+
) {
|
|
724
|
+
TERMINAL_TRANSFORM_GENERATIONS.set(generation, error);
|
|
725
|
+
} else {
|
|
726
|
+
evictGeneration(cache, key, generation);
|
|
727
|
+
}
|
|
595
728
|
throw error;
|
|
596
729
|
}
|
|
597
730
|
}
|
|
@@ -791,6 +924,8 @@ interface TtscEnvelopeGraphIndexes {
|
|
|
791
924
|
string,
|
|
792
925
|
{ hash: string | null; path: string; realpath: string | null }
|
|
793
926
|
>;
|
|
927
|
+
/** Native compiler-observation failure for an unproven member identity. */
|
|
928
|
+
readonly inputProofFailures: Map<string, string>;
|
|
794
929
|
/** Aliased graph proof keys that reported contradictory generation states. */
|
|
795
930
|
readonly inputProofConflicts: Set<string>;
|
|
796
931
|
}
|
|
@@ -849,6 +984,7 @@ function envelopeGraphIndexes(
|
|
|
849
984
|
members: new Set(),
|
|
850
985
|
speculative: new Set(),
|
|
851
986
|
inputProofs: new Map(),
|
|
987
|
+
inputProofFailures: new Map(),
|
|
852
988
|
inputProofConflicts: new Set(),
|
|
853
989
|
};
|
|
854
990
|
const graph =
|
|
@@ -871,7 +1007,11 @@ function envelopeGraphIndexes(
|
|
|
871
1007
|
)
|
|
872
1008
|
.map((target) => {
|
|
873
1009
|
const absoluteTarget = path.resolve(props.projectRoot, target);
|
|
874
|
-
|
|
1010
|
+
const targetIdentity = derivationIdentity(state, absoluteTarget);
|
|
1011
|
+
built.members.add(targetIdentity);
|
|
1012
|
+
if (!built.spellings.has(targetIdentity)) {
|
|
1013
|
+
built.spellings.set(targetIdentity, absoluteTarget);
|
|
1014
|
+
}
|
|
875
1015
|
return absoluteTarget;
|
|
876
1016
|
}),
|
|
877
1017
|
);
|
|
@@ -880,7 +1020,9 @@ function envelopeGraphIndexes(
|
|
|
880
1020
|
built.globals.push(...selectListedFiles(props.projectRoot, graph.globals));
|
|
881
1021
|
built.configs.push(...selectListedFiles(props.projectRoot, graph.configs));
|
|
882
1022
|
for (const input of [...built.globals, ...built.configs]) {
|
|
883
|
-
|
|
1023
|
+
const identity = derivationIdentity(state, input);
|
|
1024
|
+
built.members.add(identity);
|
|
1025
|
+
if (!built.spellings.has(identity)) built.spellings.set(identity, input);
|
|
884
1026
|
}
|
|
885
1027
|
const candidateEntries = Object.entries(graph.candidates ?? {}).filter(
|
|
886
1028
|
(entry) => Array.isArray(entry[1]),
|
|
@@ -890,9 +1032,12 @@ function envelopeGraphIndexes(
|
|
|
890
1032
|
// candidate could be classified speculative before a later entry proves
|
|
891
1033
|
// the same path is a realized source.
|
|
892
1034
|
for (const [source] of candidateEntries) {
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
);
|
|
1035
|
+
const absoluteSource = path.resolve(props.projectRoot, source);
|
|
1036
|
+
const identity = derivationIdentity(state, absoluteSource);
|
|
1037
|
+
built.members.add(identity);
|
|
1038
|
+
if (!built.spellings.has(identity)) {
|
|
1039
|
+
built.spellings.set(identity, absoluteSource);
|
|
1040
|
+
}
|
|
896
1041
|
}
|
|
897
1042
|
const realized = new Set(built.members);
|
|
898
1043
|
for (const [source, candidates] of candidateEntries) {
|
|
@@ -914,6 +1059,22 @@ function envelopeGraphIndexes(
|
|
|
914
1059
|
// only as a candidate.
|
|
915
1060
|
if (!realized.has(identity)) built.speculative.add(identity);
|
|
916
1061
|
built.members.add(identity);
|
|
1062
|
+
if (!built.spellings.has(identity)) {
|
|
1063
|
+
built.spellings.set(
|
|
1064
|
+
identity,
|
|
1065
|
+
path.resolve(props.projectRoot, candidate),
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const transformSources = new Set<string>();
|
|
1071
|
+
if (props.result.type === "success") {
|
|
1072
|
+
for (const output of Object.keys(props.result.typescript)) {
|
|
1073
|
+
if (!isDeclarationFile(output)) {
|
|
1074
|
+
transformSources.add(
|
|
1075
|
+
derivationIdentity(state, path.resolve(props.projectRoot, output)),
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
917
1078
|
}
|
|
918
1079
|
}
|
|
919
1080
|
for (const [input, hash] of Object.entries(graph.inputHashes ?? {})) {
|
|
@@ -939,7 +1100,9 @@ function envelopeGraphIndexes(
|
|
|
939
1100
|
}
|
|
940
1101
|
const absolute = path.resolve(props.projectRoot, input);
|
|
941
1102
|
const identity = derivationIdentity(state, absolute);
|
|
942
|
-
if (!built.members.has(identity))
|
|
1103
|
+
if (!built.members.has(identity) && !transformSources.has(identity)) {
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
943
1106
|
const proof = {
|
|
944
1107
|
hash,
|
|
945
1108
|
path: absolute,
|
|
@@ -962,6 +1125,25 @@ function envelopeGraphIndexes(
|
|
|
962
1125
|
built.inputProofs.set(identity, proof);
|
|
963
1126
|
}
|
|
964
1127
|
}
|
|
1128
|
+
for (const [input, reason] of Object.entries(
|
|
1129
|
+
graph.inputProofFailures ?? {},
|
|
1130
|
+
)) {
|
|
1131
|
+
if (typeof reason !== "string" || !/^[a-z0-9-]{1,64}$/.test(reason)) {
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
const absolute = path.resolve(props.projectRoot, input);
|
|
1135
|
+
const identity = derivationIdentity(state, absolute);
|
|
1136
|
+
if (!built.members.has(identity) && !transformSources.has(identity)) {
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
if (built.inputProofs.has(identity)) {
|
|
1140
|
+
built.inputProofs.delete(identity);
|
|
1141
|
+
built.inputProofConflicts.add(identity);
|
|
1142
|
+
}
|
|
1143
|
+
if (!built.inputProofFailures.has(identity)) {
|
|
1144
|
+
built.inputProofFailures.set(identity, reason);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
965
1147
|
}
|
|
966
1148
|
state.graph = built;
|
|
967
1149
|
return built;
|
|
@@ -1018,8 +1200,9 @@ function collectDeclaredIdentities(
|
|
|
1018
1200
|
* Envelope keys mirror the `typescript` keys (project-relative); values may be
|
|
1019
1201
|
* project-relative or absolute. Every path is absolutized against the project
|
|
1020
1202
|
* root and deduplicated; the file itself is dropped (the bundler already
|
|
1021
|
-
* watches the module it transforms), and so is the disposed
|
|
1022
|
-
* (see
|
|
1203
|
+
* watches the module it transforms), and so is every path in the disposed
|
|
1204
|
+
* transform scratch tree (see
|
|
1205
|
+
* {@link TtscCachedProjectTransform.scratchDirectory}).
|
|
1023
1206
|
*/
|
|
1024
1207
|
function notifyWatchInputs(
|
|
1025
1208
|
hooks: TtscTransformHooks | undefined,
|
|
@@ -1036,6 +1219,7 @@ function notifyWatchInputs(
|
|
|
1036
1219
|
file,
|
|
1037
1220
|
projectRoot: cached.projectRoot,
|
|
1038
1221
|
result: cached.result,
|
|
1222
|
+
scratchDirectory: cached.scratchDirectory,
|
|
1039
1223
|
temporaryTsconfig: cached.temporaryTsconfig,
|
|
1040
1224
|
})) {
|
|
1041
1225
|
// Hand the adapter the identity this generation already resolved and the
|
|
@@ -1078,6 +1262,7 @@ function selectWatchInputs(props: {
|
|
|
1078
1262
|
file: string;
|
|
1079
1263
|
projectRoot: string;
|
|
1080
1264
|
result: ITtscCompilerTransformation;
|
|
1265
|
+
scratchDirectory?: string;
|
|
1081
1266
|
temporaryTsconfig?: string;
|
|
1082
1267
|
}): string[] {
|
|
1083
1268
|
if (props.result.type === "exception") {
|
|
@@ -1101,6 +1286,7 @@ function deriveWatchInputs(
|
|
|
1101
1286
|
file: string;
|
|
1102
1287
|
projectRoot: string;
|
|
1103
1288
|
result: ITtscCompilerTransformation;
|
|
1289
|
+
scratchDirectory?: string;
|
|
1104
1290
|
temporaryTsconfig?: string;
|
|
1105
1291
|
},
|
|
1106
1292
|
fileIdentity: string,
|
|
@@ -1123,6 +1309,7 @@ function deriveWatchInputs(
|
|
|
1123
1309
|
if (
|
|
1124
1310
|
spelling === currentSpelling ||
|
|
1125
1311
|
spelling === temporarySpelling ||
|
|
1312
|
+
isTransformScratchInput(spelling, props.scratchDirectory) ||
|
|
1126
1313
|
lexicalSeen.has(spelling)
|
|
1127
1314
|
) {
|
|
1128
1315
|
return;
|
|
@@ -1132,6 +1319,7 @@ function deriveWatchInputs(
|
|
|
1132
1319
|
output.push(input);
|
|
1133
1320
|
};
|
|
1134
1321
|
const appendPhysical = (input: string): void => {
|
|
1322
|
+
if (isTransformScratchInput(input, props.scratchDirectory)) return;
|
|
1135
1323
|
const identity = derivationIdentity(state, input);
|
|
1136
1324
|
if (excluded.has(identity) || physicalSeen.has(identity)) return;
|
|
1137
1325
|
physicalSeen.add(identity);
|
|
@@ -1462,11 +1650,24 @@ export function stripQuery(id: string): string {
|
|
|
1462
1650
|
}
|
|
1463
1651
|
|
|
1464
1652
|
/**
|
|
1465
|
-
* Returns `true` for
|
|
1466
|
-
* `.d.cts`
|
|
1653
|
+
* Returns `true` for every declaration-file spelling TypeScript-Go accepts.
|
|
1654
|
+
* Besides the standard `.d.ts`, `.d.mts`, and `.d.cts` forms, TypeScript-Go
|
|
1655
|
+
* treats an arbitrary-extension source such as `styles.d.css.ts` as a
|
|
1656
|
+
* declaration file too.
|
|
1467
1657
|
*/
|
|
1468
1658
|
export function isDeclarationFile(id: string): boolean {
|
|
1469
|
-
|
|
1659
|
+
// Module ids can cross process/platform boundaries (for example, a Windows
|
|
1660
|
+
// id inspected by a POSIX host). TypeScript-Go normalizes both separators
|
|
1661
|
+
// before taking the basename, so a `.d.` directory component must not turn
|
|
1662
|
+
// an ordinary source into a declaration file.
|
|
1663
|
+
const normalized = id.replaceAll("\\", "/");
|
|
1664
|
+
const base = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
1665
|
+
return (
|
|
1666
|
+
base.endsWith(".d.ts") ||
|
|
1667
|
+
base.endsWith(".d.mts") ||
|
|
1668
|
+
base.endsWith(".d.cts") ||
|
|
1669
|
+
(base.endsWith(".ts") && base.includes(".d."))
|
|
1670
|
+
);
|
|
1470
1671
|
}
|
|
1471
1672
|
|
|
1472
1673
|
/**
|
|
@@ -1519,7 +1720,12 @@ function matchesCachedSource(
|
|
|
1519
1720
|
): boolean {
|
|
1520
1721
|
const identities = envelopeDerivation(cached).identityContext;
|
|
1521
1722
|
const currentKey = toProjectKey(cached.projectRoot, file, identities);
|
|
1522
|
-
|
|
1723
|
+
const identity = pathIdentityKey(file, identities);
|
|
1724
|
+
const expected =
|
|
1725
|
+
cached.sourceHashes?.[identity] ??
|
|
1726
|
+
cached.inputHashes[currentKey] ??
|
|
1727
|
+
cached.externalInputHashes?.[identity];
|
|
1728
|
+
if (expected !== hashText(source)) {
|
|
1523
1729
|
return false;
|
|
1524
1730
|
}
|
|
1525
1731
|
if (
|
|
@@ -1583,6 +1789,7 @@ function matchesNarrowPersistentInputs(
|
|
|
1583
1789
|
file,
|
|
1584
1790
|
projectRoot: cached.projectRoot,
|
|
1585
1791
|
result: cached.result,
|
|
1792
|
+
scratchDirectory: cached.scratchDirectory,
|
|
1586
1793
|
temporaryTsconfig: cached.temporaryTsconfig,
|
|
1587
1794
|
});
|
|
1588
1795
|
for (const input of inputs) {
|
|
@@ -1839,9 +2046,13 @@ function isMissingPathError(error: unknown): boolean {
|
|
|
1839
2046
|
function captureUniversalHostInputValidation(
|
|
1840
2047
|
cached: TtscCachedProjectTransform,
|
|
1841
2048
|
currentFile: string,
|
|
1842
|
-
):
|
|
2049
|
+
): {
|
|
2050
|
+
failures: TtscGenerationProofFailures;
|
|
2051
|
+
validation?: TtscHostInputValidation;
|
|
2052
|
+
} {
|
|
1843
2053
|
const filesystem = resultFilesystem(cached.result);
|
|
1844
2054
|
const state = envelopeDerivation(cached);
|
|
2055
|
+
const failures = createGenerationProofFailures();
|
|
1845
2056
|
const validation: TtscHostInputValidation = {
|
|
1846
2057
|
entries: new Map(),
|
|
1847
2058
|
covered: new Set(),
|
|
@@ -1851,6 +2062,7 @@ function captureUniversalHostInputValidation(
|
|
|
1851
2062
|
filesystem,
|
|
1852
2063
|
projectRoot: cached.projectRoot,
|
|
1853
2064
|
result: cached.result,
|
|
2065
|
+
scratchDirectory: cached.scratchDirectory,
|
|
1854
2066
|
temporaryTsconfig: cached.temporaryTsconfig,
|
|
1855
2067
|
})) {
|
|
1856
2068
|
const generationHashes =
|
|
@@ -1868,7 +2080,14 @@ function captureUniversalHostInputValidation(
|
|
|
1868
2080
|
let readable = false;
|
|
1869
2081
|
if (expected === undefined) {
|
|
1870
2082
|
const current = path.resolve(currentFile);
|
|
1871
|
-
if (path.resolve(input) !== current)
|
|
2083
|
+
if (path.resolve(input) !== current) {
|
|
2084
|
+
recordGenerationProofFailure(failures, {
|
|
2085
|
+
domain: "host",
|
|
2086
|
+
kind: "content-proof-missing",
|
|
2087
|
+
path: input,
|
|
2088
|
+
});
|
|
2089
|
+
return { failures };
|
|
2090
|
+
}
|
|
1872
2091
|
// The current module may be supplied from an unsaved editor buffer. Its
|
|
1873
2092
|
// generation snapshot is overlaid below from `currentSource`, so a disk
|
|
1874
2093
|
// fingerprint would be both unavailable and the wrong authority. The
|
|
@@ -1877,7 +2096,12 @@ function captureUniversalHostInputValidation(
|
|
|
1877
2096
|
} else {
|
|
1878
2097
|
const current = hostInputStateHash(input, filesystem);
|
|
1879
2098
|
if (expected !== current) {
|
|
1880
|
-
|
|
2099
|
+
recordGenerationProofFailure(failures, {
|
|
2100
|
+
domain: "host",
|
|
2101
|
+
kind: "content-changed",
|
|
2102
|
+
path: input,
|
|
2103
|
+
});
|
|
2104
|
+
return { failures };
|
|
1881
2105
|
}
|
|
1882
2106
|
// A path both sides agree they could not read carries no bytes for a
|
|
1883
2107
|
// signature to stand for. It still belongs in the manifest, so the
|
|
@@ -1897,14 +2121,38 @@ function captureUniversalHostInputValidation(
|
|
|
1897
2121
|
state.identityContext,
|
|
1898
2122
|
)
|
|
1899
2123
|
) {
|
|
1900
|
-
|
|
2124
|
+
recordGenerationProofFailure(failures, {
|
|
2125
|
+
domain: "host",
|
|
2126
|
+
kind: Object.prototype.hasOwnProperty.call(
|
|
2127
|
+
generationRealpaths,
|
|
2128
|
+
absoluteInput,
|
|
2129
|
+
)
|
|
2130
|
+
? "realpath-changed"
|
|
2131
|
+
: "realpath-proof-missing",
|
|
2132
|
+
path: input,
|
|
2133
|
+
});
|
|
2134
|
+
return { failures };
|
|
1901
2135
|
}
|
|
1902
2136
|
}
|
|
1903
2137
|
validation.covered.add(path.resolve(input));
|
|
1904
2138
|
const before = inputMetadataEvidence(input, filesystem);
|
|
1905
|
-
if (!matchesRecordedInput(cached, input))
|
|
2139
|
+
if (!matchesRecordedInput(cached, input)) {
|
|
2140
|
+
recordGenerationProofFailure(failures, {
|
|
2141
|
+
domain: "host",
|
|
2142
|
+
kind: "snapshot-mismatch",
|
|
2143
|
+
path: input,
|
|
2144
|
+
});
|
|
2145
|
+
return { failures };
|
|
2146
|
+
}
|
|
1906
2147
|
const after = inputMetadataSignature(input, filesystem);
|
|
1907
|
-
if (before?.signature !== after)
|
|
2148
|
+
if (before?.signature !== after) {
|
|
2149
|
+
recordGenerationProofFailure(failures, {
|
|
2150
|
+
domain: "host",
|
|
2151
|
+
kind: "changed-during-validation",
|
|
2152
|
+
path: input,
|
|
2153
|
+
});
|
|
2154
|
+
return { failures };
|
|
2155
|
+
}
|
|
1908
2156
|
if (before !== undefined) {
|
|
1909
2157
|
// Do not key this manifest by physical identity. A symlink/junction
|
|
1910
2158
|
// spelling and its selected target deliberately share that identity,
|
|
@@ -1924,7 +2172,14 @@ function captureUniversalHostInputValidation(
|
|
|
1924
2172
|
const probe = missingPathProbe(input, filesystem);
|
|
1925
2173
|
if (probe.blocker !== undefined) {
|
|
1926
2174
|
const signature = inputMetadataSignature(probe.blocker, filesystem);
|
|
1927
|
-
if (signature === undefined)
|
|
2175
|
+
if (signature === undefined) {
|
|
2176
|
+
recordGenerationProofFailure(failures, {
|
|
2177
|
+
domain: "host",
|
|
2178
|
+
kind: "blocker-metadata-unavailable",
|
|
2179
|
+
path: probe.blocker,
|
|
2180
|
+
});
|
|
2181
|
+
return { failures };
|
|
2182
|
+
}
|
|
1928
2183
|
// A blocker proves a kind and an identity, not content: it is the
|
|
1929
2184
|
// non-directory ancestor that makes everything below it unreachable, and
|
|
1930
2185
|
// it cannot stop being that without its metadata moving. So it keeps a
|
|
@@ -1956,7 +2211,7 @@ function captureUniversalHostInputValidation(
|
|
|
1956
2211
|
);
|
|
1957
2212
|
}
|
|
1958
2213
|
cached.hostInputValidation = validation;
|
|
1959
|
-
return validation;
|
|
2214
|
+
return { failures, validation };
|
|
1960
2215
|
}
|
|
1961
2216
|
|
|
1962
2217
|
/**
|
|
@@ -2347,7 +2602,9 @@ function matchesCompleteInputSnapshot(
|
|
|
2347
2602
|
) {
|
|
2348
2603
|
return false;
|
|
2349
2604
|
}
|
|
2350
|
-
|
|
2605
|
+
if (Object.prototype.hasOwnProperty.call(cached.inputHashes, currentKey)) {
|
|
2606
|
+
current.hashes[currentKey] = hashText(source);
|
|
2607
|
+
}
|
|
2351
2608
|
if (!sameHashes(cached.inputHashes, current.hashes, declaredInputs)) {
|
|
2352
2609
|
return false;
|
|
2353
2610
|
}
|
|
@@ -2430,16 +2687,17 @@ function matchesExternalInputRealpaths(
|
|
|
2430
2687
|
|
|
2431
2688
|
/**
|
|
2432
2689
|
* Capture external-input hashes without attaching post-compile state to an
|
|
2433
|
-
* earlier graph. Graph members
|
|
2434
|
-
* it now; plugin-declared dependency-only
|
|
2435
|
-
* post-compile snapshot because their own protocol
|
|
2436
|
-
* fingerprints.
|
|
2690
|
+
* earlier graph. Graph members and out-of-walk transformed sources must carry
|
|
2691
|
+
* compiler-time proof and still match it now; plugin-declared dependency-only
|
|
2692
|
+
* paths retain the historical post-compile snapshot because their own protocol
|
|
2693
|
+
* does not claim generation fingerprints.
|
|
2437
2694
|
*/
|
|
2438
2695
|
function captureExternalInputSnapshot(
|
|
2439
2696
|
cached: TtscCachedProjectTransform,
|
|
2440
2697
|
paths: readonly string[],
|
|
2441
2698
|
): {
|
|
2442
2699
|
complete: boolean;
|
|
2700
|
+
failures: TtscGenerationProofFailures;
|
|
2443
2701
|
hashes: Record<string, string>;
|
|
2444
2702
|
realpaths: Record<string, string | null>;
|
|
2445
2703
|
signatures: Record<string, string>;
|
|
@@ -2447,9 +2705,23 @@ function captureExternalInputSnapshot(
|
|
|
2447
2705
|
const state = envelopeDerivation(cached);
|
|
2448
2706
|
const filesystem = resultFilesystem(cached.result);
|
|
2449
2707
|
const graph = envelopeGraphIndexes(state, cached);
|
|
2708
|
+
// A non-declaration transform output is a compiler-realized source even when
|
|
2709
|
+
// a malformed or legacy graph omitted its node. Its output was computed from
|
|
2710
|
+
// compiler-time bytes, so a post-compile host read cannot prove coherence.
|
|
2711
|
+
const transformSources = new Set<string>();
|
|
2712
|
+
if (cached.result.type === "success") {
|
|
2713
|
+
for (const output of Object.keys(cached.result.typescript)) {
|
|
2714
|
+
if (!isDeclarationFile(output)) {
|
|
2715
|
+
transformSources.add(
|
|
2716
|
+
derivationIdentity(state, path.resolve(cached.projectRoot, output)),
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2450
2721
|
const hashes: Record<string, string> = {};
|
|
2451
2722
|
const realpaths: Record<string, string | null> = {};
|
|
2452
2723
|
const signatures: Record<string, string> = {};
|
|
2724
|
+
const failures = createGenerationProofFailures();
|
|
2453
2725
|
let complete = true;
|
|
2454
2726
|
// Sandwich every read between two metadata signatures. Only a signature that
|
|
2455
2727
|
// survived its own read, and whose stamp's tick the filesystem's clock has
|
|
@@ -2471,28 +2743,54 @@ function captureExternalInputSnapshot(
|
|
|
2471
2743
|
// through to the recorded-state branch below, the same evidence a
|
|
2472
2744
|
// plugin-declared dependency path carries. Its absence still invalidates
|
|
2473
2745
|
// the generation when it appears, because `missing` is recorded state.
|
|
2746
|
+
const realizedTransformSource = transformSources.has(identity);
|
|
2474
2747
|
const speculativeOnly =
|
|
2748
|
+
!realizedTransformSource &&
|
|
2475
2749
|
graph.speculative.has(identity) &&
|
|
2476
2750
|
!graph.inputProofs.has(identity) &&
|
|
2477
2751
|
!graph.inputProofConflicts.has(identity);
|
|
2478
|
-
if (
|
|
2752
|
+
if (
|
|
2753
|
+
(realizedTransformSource || graph.members.has(identity)) &&
|
|
2754
|
+
!speculativeOnly
|
|
2755
|
+
) {
|
|
2479
2756
|
const proof = graph.inputProofs.get(identity);
|
|
2480
2757
|
if (proof === undefined || graph.inputProofConflicts.has(identity)) {
|
|
2481
2758
|
complete = false;
|
|
2759
|
+
recordGenerationProofFailure(failures, {
|
|
2760
|
+
domain: "external",
|
|
2761
|
+
kind: graph.inputProofConflicts.has(identity)
|
|
2762
|
+
? "graph-proof-conflict"
|
|
2763
|
+
: "graph-proof-missing",
|
|
2764
|
+
detail: graph.inputProofFailures.get(identity),
|
|
2765
|
+
path: input,
|
|
2766
|
+
});
|
|
2482
2767
|
continue;
|
|
2483
2768
|
}
|
|
2484
2769
|
const before = inputMetadataEvidence(input, filesystem);
|
|
2485
2770
|
const currentHash = graphInputStateHash(input, filesystem);
|
|
2771
|
+
const currentRealpath = hostInputRealpath(input, filesystem);
|
|
2486
2772
|
const after = inputMetadataSignature(input, filesystem);
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
)
|
|
2494
|
-
) {
|
|
2773
|
+
const realpathMatches = sameHostInputRealpath(
|
|
2774
|
+
proof.realpath,
|
|
2775
|
+
currentRealpath,
|
|
2776
|
+
state.identityContext,
|
|
2777
|
+
);
|
|
2778
|
+
if (currentHash !== proof.hash || !realpathMatches) {
|
|
2495
2779
|
complete = false;
|
|
2780
|
+
if (currentHash !== proof.hash) {
|
|
2781
|
+
recordGenerationProofFailure(failures, {
|
|
2782
|
+
domain: "external",
|
|
2783
|
+
kind: "graph-content-changed",
|
|
2784
|
+
path: input,
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2787
|
+
if (!realpathMatches) {
|
|
2788
|
+
recordGenerationProofFailure(failures, {
|
|
2789
|
+
domain: "external",
|
|
2790
|
+
kind: "graph-realpath-changed",
|
|
2791
|
+
path: input,
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2496
2794
|
} else if (currentHash !== null) {
|
|
2497
2795
|
// The recorded hash is the compiler's own proof, so a signature may
|
|
2498
2796
|
// only stand for it once the current bytes were shown to match it.
|
|
@@ -2510,32 +2808,36 @@ function captureExternalInputSnapshot(
|
|
|
2510
2808
|
hashes[identity] = hash ?? MISSING_INPUT_STATE;
|
|
2511
2809
|
if (hash !== null) record(input, before, after);
|
|
2512
2810
|
}
|
|
2513
|
-
return { complete, hashes, realpaths, signatures };
|
|
2811
|
+
return { complete, failures, hashes, realpaths, signatures };
|
|
2514
2812
|
}
|
|
2515
2813
|
|
|
2516
|
-
/**
|
|
2517
|
-
function
|
|
2814
|
+
/** Explain every graph member that no longer matches the compiler's state. */
|
|
2815
|
+
function compilerGraphInputProofFailures(
|
|
2518
2816
|
cached: TtscCachedProjectTransform,
|
|
2519
|
-
):
|
|
2817
|
+
): TtscGenerationProofFailures {
|
|
2818
|
+
const failures = createGenerationProofFailures();
|
|
2520
2819
|
if (
|
|
2521
2820
|
cached.result.type === "exception" ||
|
|
2522
2821
|
cached.result.graph === undefined ||
|
|
2523
2822
|
(cached.result.graph.inputHashes === undefined &&
|
|
2524
|
-
cached.result.graph.inputRealpaths === undefined
|
|
2823
|
+
cached.result.graph.inputRealpaths === undefined &&
|
|
2824
|
+
cached.result.graph.inputProofFailures === undefined)
|
|
2525
2825
|
) {
|
|
2526
2826
|
// Legacy sidecars remain compatible for ordinary in-project graphs. Their
|
|
2527
2827
|
// out-of-walk members are still rejected by captureExternalInputSnapshot,
|
|
2528
2828
|
// where a post-compile snapshot cannot prove the compiler's generation.
|
|
2529
|
-
return
|
|
2829
|
+
return failures;
|
|
2530
2830
|
}
|
|
2531
2831
|
const state = envelopeDerivation(cached);
|
|
2532
2832
|
const filesystem = resultFilesystem(cached.result);
|
|
2533
2833
|
const graph = envelopeGraphIndexes(state, cached);
|
|
2534
|
-
if (graph.inputProofConflicts.size !== 0) {
|
|
2535
|
-
return false;
|
|
2536
|
-
}
|
|
2537
2834
|
for (const identity of graph.members) {
|
|
2538
2835
|
const proof = graph.inputProofs.get(identity);
|
|
2836
|
+
const spelling =
|
|
2837
|
+
proof?.path ?? graph.spellings.get(identity) ?? cached.projectRoot;
|
|
2838
|
+
if (isTransformScratchInput(spelling, cached.scratchDirectory)) {
|
|
2839
|
+
continue;
|
|
2840
|
+
}
|
|
2539
2841
|
// A speculative candidate has no compile-time read to prove. Requiring one
|
|
2540
2842
|
// would void every generation of every project whose resolution passes over
|
|
2541
2843
|
// a higher-priority spelling, which is every project with a dependency
|
|
@@ -2544,19 +2846,47 @@ function matchesCompilerGraphInputProofs(
|
|
|
2544
2846
|
if (proof === undefined && graph.speculative.has(identity)) {
|
|
2545
2847
|
continue;
|
|
2546
2848
|
}
|
|
2849
|
+
if (graph.inputProofConflicts.has(identity)) {
|
|
2850
|
+
recordGenerationProofFailure(failures, {
|
|
2851
|
+
domain: "graph",
|
|
2852
|
+
kind: "proof-conflict",
|
|
2853
|
+
detail: graph.inputProofFailures.get(identity),
|
|
2854
|
+
path: spelling,
|
|
2855
|
+
});
|
|
2856
|
+
continue;
|
|
2857
|
+
}
|
|
2858
|
+
if (proof === undefined) {
|
|
2859
|
+
recordGenerationProofFailure(failures, {
|
|
2860
|
+
domain: "graph",
|
|
2861
|
+
kind: "proof-missing",
|
|
2862
|
+
detail: graph.inputProofFailures.get(identity),
|
|
2863
|
+
path: spelling,
|
|
2864
|
+
});
|
|
2865
|
+
continue;
|
|
2866
|
+
}
|
|
2867
|
+
const currentHash = graphInputStateHash(proof.path, filesystem);
|
|
2868
|
+
if (currentHash !== proof.hash) {
|
|
2869
|
+
recordGenerationProofFailure(failures, {
|
|
2870
|
+
domain: "graph",
|
|
2871
|
+
kind: "content-changed",
|
|
2872
|
+
path: proof.path,
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2547
2875
|
if (
|
|
2548
|
-
proof === undefined ||
|
|
2549
|
-
graphInputStateHash(proof.path, filesystem) !== proof.hash ||
|
|
2550
2876
|
!sameHostInputRealpath(
|
|
2551
2877
|
proof.realpath,
|
|
2552
2878
|
hostInputRealpath(proof.path, filesystem),
|
|
2553
2879
|
state.identityContext,
|
|
2554
2880
|
)
|
|
2555
2881
|
) {
|
|
2556
|
-
|
|
2882
|
+
recordGenerationProofFailure(failures, {
|
|
2883
|
+
domain: "graph",
|
|
2884
|
+
kind: "realpath-changed",
|
|
2885
|
+
path: proof.path,
|
|
2886
|
+
});
|
|
2557
2887
|
}
|
|
2558
2888
|
}
|
|
2559
|
-
return
|
|
2889
|
+
return failures;
|
|
2560
2890
|
}
|
|
2561
2891
|
|
|
2562
2892
|
/** Compare one derived input with the snapshot that owned it at generation. */
|
|
@@ -2653,6 +2983,7 @@ function collectProjectInputSnapshot(
|
|
|
2653
2983
|
projectDirectories: TtscProjectDirectorySnapshot[];
|
|
2654
2984
|
provenSignatures: Record<string, string>;
|
|
2655
2985
|
unstableFiles: Set<string>;
|
|
2986
|
+
walkFailures: TtscProjectWalkFailure[];
|
|
2656
2987
|
} {
|
|
2657
2988
|
const hashes: Record<string, string> = {};
|
|
2658
2989
|
const fileSignatures: Record<string, string> = {};
|
|
@@ -2660,6 +2991,7 @@ function collectProjectInputSnapshot(
|
|
|
2660
2991
|
const unstableFiles = new Set<string>();
|
|
2661
2992
|
let attributed = true;
|
|
2662
2993
|
const walked = walkProjectInputs(projectRoot, filesystem);
|
|
2994
|
+
const walkFailures = [...walked.failures];
|
|
2663
2995
|
let complete = walked.complete;
|
|
2664
2996
|
for (const file of walked.files) {
|
|
2665
2997
|
try {
|
|
@@ -2691,6 +3023,7 @@ function collectProjectInputSnapshot(
|
|
|
2691
3023
|
) {
|
|
2692
3024
|
complete = false;
|
|
2693
3025
|
unstableFiles.add(key);
|
|
3026
|
+
walkFailures.push({ kind: "file-changed-during-read", path: file });
|
|
2694
3027
|
} else {
|
|
2695
3028
|
fileSignatures[key] = after;
|
|
2696
3029
|
// Only a signature whose stamp's tick the filesystem's clock provably
|
|
@@ -2705,6 +3038,7 @@ function collectProjectInputSnapshot(
|
|
|
2705
3038
|
// File watchers may observe a transform while another process is moving
|
|
2706
3039
|
// or deleting files. The missing key invalidates older cache entries.
|
|
2707
3040
|
complete = false;
|
|
3041
|
+
walkFailures.push({ kind: "file-read-failed", path: file });
|
|
2708
3042
|
try {
|
|
2709
3043
|
unstableFiles.add(toProjectKey(projectRoot, file, identities));
|
|
2710
3044
|
} catch {
|
|
@@ -2722,6 +3056,7 @@ function collectProjectInputSnapshot(
|
|
|
2722
3056
|
projectDirectories: walked.directories,
|
|
2723
3057
|
provenSignatures,
|
|
2724
3058
|
unstableFiles,
|
|
3059
|
+
walkFailures,
|
|
2725
3060
|
};
|
|
2726
3061
|
}
|
|
2727
3062
|
|
|
@@ -2739,10 +3074,12 @@ function walkProjectInputs(
|
|
|
2739
3074
|
): {
|
|
2740
3075
|
complete: boolean;
|
|
2741
3076
|
directories: TtscProjectDirectorySnapshot[];
|
|
3077
|
+
failures: TtscProjectWalkFailure[];
|
|
2742
3078
|
files: string[];
|
|
2743
3079
|
} {
|
|
2744
3080
|
let complete = true;
|
|
2745
3081
|
const directories: TtscProjectDirectorySnapshot[] = [];
|
|
3082
|
+
const failures: TtscProjectWalkFailure[] = [];
|
|
2746
3083
|
const files: string[] = [];
|
|
2747
3084
|
const stack = [root];
|
|
2748
3085
|
while (stack.length !== 0) {
|
|
@@ -2750,6 +3087,10 @@ function walkProjectInputs(
|
|
|
2750
3087
|
const before = projectDirectorySignature(current, filesystem);
|
|
2751
3088
|
if (before === undefined) {
|
|
2752
3089
|
complete = false;
|
|
3090
|
+
failures.push({
|
|
3091
|
+
kind: "directory-metadata-unavailable",
|
|
3092
|
+
path: current,
|
|
3093
|
+
});
|
|
2753
3094
|
continue;
|
|
2754
3095
|
}
|
|
2755
3096
|
let entries: fs.Dirent[];
|
|
@@ -2757,11 +3098,19 @@ function walkProjectInputs(
|
|
|
2757
3098
|
entries = filesystem.readdir(current);
|
|
2758
3099
|
} catch {
|
|
2759
3100
|
complete = false;
|
|
3101
|
+
failures.push({ kind: "directory-read-failed", path: current });
|
|
2760
3102
|
continue;
|
|
2761
3103
|
}
|
|
2762
3104
|
const after = projectDirectorySignature(current, filesystem);
|
|
2763
3105
|
if (after === undefined || before !== after) {
|
|
2764
3106
|
complete = false;
|
|
3107
|
+
failures.push({
|
|
3108
|
+
kind:
|
|
3109
|
+
after === undefined
|
|
3110
|
+
? "directory-metadata-unavailable"
|
|
3111
|
+
: "directory-changed-during-walk",
|
|
3112
|
+
path: current,
|
|
3113
|
+
});
|
|
2765
3114
|
}
|
|
2766
3115
|
directories.push({
|
|
2767
3116
|
path: current,
|
|
@@ -2787,7 +3136,7 @@ function walkProjectInputs(
|
|
|
2787
3136
|
}
|
|
2788
3137
|
directories.sort((left, right) => left.path.localeCompare(right.path));
|
|
2789
3138
|
files.sort();
|
|
2790
|
-
return { complete, directories, files };
|
|
3139
|
+
return { complete, directories, failures, files };
|
|
2791
3140
|
}
|
|
2792
3141
|
|
|
2793
3142
|
/** Return a cheap identity for one directory's immediate membership. */
|
|
@@ -2862,6 +3211,8 @@ async function createProjectMutationTracker(
|
|
|
2862
3211
|
filesystem: TtscTransformFilesystemOperations = DEFAULT_FILESYSTEM_OPERATIONS,
|
|
2863
3212
|
): Promise<TtscProjectMutationTracker> {
|
|
2864
3213
|
const tracker: TtscProjectMutationTracker = {
|
|
3214
|
+
changes: new Set(),
|
|
3215
|
+
changesOmitted: false,
|
|
2865
3216
|
close: () => undefined,
|
|
2866
3217
|
failed: false,
|
|
2867
3218
|
membershipChanged: false,
|
|
@@ -2886,8 +3237,15 @@ async function createProjectMutationTracker(
|
|
|
2886
3237
|
openDirectoryWatch(
|
|
2887
3238
|
filesystem,
|
|
2888
3239
|
directory.path,
|
|
2889
|
-
(eventType) => {
|
|
2890
|
-
if (eventType === "rename")
|
|
3240
|
+
(eventType, filename) => {
|
|
3241
|
+
if (eventType === "rename") {
|
|
3242
|
+
recordProjectMutation(
|
|
3243
|
+
tracker,
|
|
3244
|
+
filename === null
|
|
3245
|
+
? directory.path
|
|
3246
|
+
: path.join(directory.path, filename),
|
|
3247
|
+
);
|
|
3248
|
+
}
|
|
2891
3249
|
},
|
|
2892
3250
|
() => {
|
|
2893
3251
|
tracker.failed = true;
|
|
@@ -2939,6 +3297,8 @@ async function createHostInputMutationTracker(
|
|
|
2939
3297
|
names: [...location.names],
|
|
2940
3298
|
}));
|
|
2941
3299
|
const tracker: TtscProjectMutationTracker = {
|
|
3300
|
+
changes: new Set(),
|
|
3301
|
+
changesOmitted: false,
|
|
2942
3302
|
close: () => undefined,
|
|
2943
3303
|
// Coverage is the caller's claim, and it is required rather than derived
|
|
2944
3304
|
// from the input list: an input is watched by its exact name here, but only
|
|
@@ -2981,7 +3341,12 @@ async function createHostInputMutationTracker(
|
|
|
2981
3341
|
? null
|
|
2982
3342
|
: normalizeHostInputName(filename, caseSensitive);
|
|
2983
3343
|
if (reported === null || names.has(reported)) {
|
|
2984
|
-
|
|
3344
|
+
recordProjectMutation(
|
|
3345
|
+
tracker,
|
|
3346
|
+
filename === null
|
|
3347
|
+
? location.directory
|
|
3348
|
+
: path.join(location.directory, filename),
|
|
3349
|
+
);
|
|
2985
3350
|
}
|
|
2986
3351
|
},
|
|
2987
3352
|
() => {
|
|
@@ -2996,6 +3361,20 @@ async function createHostInputMutationTracker(
|
|
|
2996
3361
|
return tracker;
|
|
2997
3362
|
}
|
|
2998
3363
|
|
|
3364
|
+
/** Record enough exact mutation evidence without retaining an event stream. */
|
|
3365
|
+
function recordProjectMutation(
|
|
3366
|
+
tracker: TtscProjectMutationTracker,
|
|
3367
|
+
changed: string,
|
|
3368
|
+
): void {
|
|
3369
|
+
tracker.membershipChanged = true;
|
|
3370
|
+
if (tracker.changes.has(changed)) return;
|
|
3371
|
+
if (tracker.changes.size < MAX_GENERATION_MUTATION_PATHS) {
|
|
3372
|
+
tracker.changes.add(changed);
|
|
3373
|
+
} else {
|
|
3374
|
+
tracker.changesOmitted = true;
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
|
|
2999
3378
|
interface WindowsProjectMutationBroker {
|
|
3000
3379
|
child: ChildProcess;
|
|
3001
3380
|
/** Round-trips awaiting the child's reply, by request id. */
|
|
@@ -3128,8 +3507,10 @@ function getWindowsProjectMutationBroker(): WindowsProjectMutationBroker {
|
|
|
3128
3507
|
child.on("message", (message: unknown) => {
|
|
3129
3508
|
if (message === null || typeof message !== "object") return;
|
|
3130
3509
|
const record = message as {
|
|
3510
|
+
directory?: string;
|
|
3131
3511
|
drained?: boolean;
|
|
3132
3512
|
failed?: boolean;
|
|
3513
|
+
filename?: string | null;
|
|
3133
3514
|
id?: number;
|
|
3134
3515
|
ready?: boolean;
|
|
3135
3516
|
};
|
|
@@ -3147,7 +3528,16 @@ function getWindowsProjectMutationBroker(): WindowsProjectMutationBroker {
|
|
|
3147
3528
|
if (record.failed === true) registration.tracker.failed = true;
|
|
3148
3529
|
if (record.ready === true) registration.ready();
|
|
3149
3530
|
if (record.ready !== true && record.failed !== true) {
|
|
3150
|
-
|
|
3531
|
+
if (typeof record.directory === "string") {
|
|
3532
|
+
recordProjectMutation(
|
|
3533
|
+
registration.tracker,
|
|
3534
|
+
typeof record.filename === "string"
|
|
3535
|
+
? path.join(record.directory, record.filename)
|
|
3536
|
+
: record.directory,
|
|
3537
|
+
);
|
|
3538
|
+
} else {
|
|
3539
|
+
registration.tracker.membershipChanged = true;
|
|
3540
|
+
}
|
|
3151
3541
|
}
|
|
3152
3542
|
});
|
|
3153
3543
|
windowsProjectMutationBroker = broker;
|
|
@@ -3238,7 +3628,7 @@ const WINDOWS_WATCH_BROKER_SOURCE = [
|
|
|
3238
3628
|
" const names = location.names === undefined ? undefined : new Set(location.names.map((name) => name.toLowerCase()));",
|
|
3239
3629
|
" const watcher = fs.watch(location.directory, { persistent: false }, (event, filename) => {",
|
|
3240
3630
|
" const matches = names === undefined || filename === null || names.has(String(filename).toLowerCase());",
|
|
3241
|
-
' if (matches && (message.allEvents || event === "rename")) process.send?.({ id: message.id });',
|
|
3631
|
+
' if (matches && (message.allEvents || event === "rename")) process.send?.({ directory: location.directory, filename: filename === null ? null : String(filename), id: message.id });',
|
|
3242
3632
|
" });",
|
|
3243
3633
|
' watcher.on("error", () => process.send?.({ failed: true, id: message.id }));',
|
|
3244
3634
|
" watchers.push(watcher);",
|
|
@@ -3483,12 +3873,13 @@ function matchesCachedExternalInputs(cached: TtscCachedProjectTransform): {
|
|
|
3483
3873
|
|
|
3484
3874
|
/**
|
|
3485
3875
|
* Derive the absolute out-of-walk input set of a whole project transform: the
|
|
3486
|
-
* union of every reference-graph member (edge keys and
|
|
3487
|
-
* config chain) and
|
|
3488
|
-
* project walk already hashes and the disposed
|
|
3489
|
-
* inputs {@link matchesCachedSource}'s walk cannot see.
|
|
3490
|
-
* that are still missing remain in this set even under
|
|
3491
|
-
* first walk cannot hash a file that has not been created
|
|
3876
|
+
* union of every transformed source key, reference-graph member (edge keys and
|
|
3877
|
+
* targets, globals, the config chain), and plugin-reported dependency, minus
|
|
3878
|
+
* everything the project walk already hashes and the disposed transform scratch
|
|
3879
|
+
* tree. These are the inputs {@link matchesCachedSource}'s walk cannot see.
|
|
3880
|
+
* Resolution candidates that are still missing remain in this set even under
|
|
3881
|
+
* the project root: the first walk cannot hash a file that has not been created
|
|
3882
|
+
* yet.
|
|
3492
3883
|
*
|
|
3493
3884
|
* A `dependenciesComplete` declaration deliberately does not narrow the stored
|
|
3494
3885
|
* set: other files in the same whole-project result can still own the omitted
|
|
@@ -3500,6 +3891,7 @@ function selectExternalInputPaths(props: {
|
|
|
3500
3891
|
filesystem?: TtscTransformFilesystemOperations;
|
|
3501
3892
|
projectRoot: string;
|
|
3502
3893
|
result: ITtscCompilerTransformation;
|
|
3894
|
+
scratchDirectory?: string;
|
|
3503
3895
|
temporaryTsconfig?: string;
|
|
3504
3896
|
}): string[] {
|
|
3505
3897
|
if (props.result.type === "exception") {
|
|
@@ -3510,6 +3902,10 @@ function selectExternalInputPaths(props: {
|
|
|
3510
3902
|
const identities = createHostPathIdentityContext(filesystem);
|
|
3511
3903
|
const resolutionCandidates = new Set<string>();
|
|
3512
3904
|
const graph = props.result.graph;
|
|
3905
|
+
// Every transform output key names the source file whose transformed text it
|
|
3906
|
+
// carries. Keep an out-of-walk source in the external snapshot instead of
|
|
3907
|
+
// injecting it into the project-walk key universe (samchon/ttsc#252).
|
|
3908
|
+
members.push(...Object.keys(props.result.typescript));
|
|
3513
3909
|
if (graph !== undefined) {
|
|
3514
3910
|
for (const [source, targets] of Object.entries(graph.edges ?? {})) {
|
|
3515
3911
|
members.push(source);
|
|
@@ -3571,6 +3967,7 @@ function selectExternalInputPaths(props: {
|
|
|
3571
3967
|
resolutionCandidates.has(identity) && !filesystem.exists(absolute);
|
|
3572
3968
|
if (
|
|
3573
3969
|
identity === excluded ||
|
|
3970
|
+
isTransformScratchInput(absolute, props.scratchDirectory) ||
|
|
3574
3971
|
seen.has(spelling) ||
|
|
3575
3972
|
(!missingCandidate &&
|
|
3576
3973
|
isProjectWalkPath(props.projectRoot, absolute, identities, filesystem))
|
|
@@ -3607,6 +4004,7 @@ function selectNotifiableAbsentInputs(props: {
|
|
|
3607
4004
|
filesystem: TtscTransformFilesystemOperations;
|
|
3608
4005
|
projectRoot: string;
|
|
3609
4006
|
result: ITtscCompilerTransformation;
|
|
4007
|
+
scratchDirectory?: string;
|
|
3610
4008
|
temporaryTsconfig?: string;
|
|
3611
4009
|
}): { candidates: string[]; watched: string[] } {
|
|
3612
4010
|
const empty = { candidates: [], watched: [] };
|
|
@@ -3643,6 +4041,7 @@ function selectNotifiableAbsentInputs(props: {
|
|
|
3643
4041
|
const spelling = path.resolve(absolute);
|
|
3644
4042
|
if (
|
|
3645
4043
|
seen.has(spelling) ||
|
|
4044
|
+
isTransformScratchInput(absolute, props.scratchDirectory) ||
|
|
3646
4045
|
(excluded !== undefined &&
|
|
3647
4046
|
pathIdentityKey(absolute, identities) === excluded) ||
|
|
3648
4047
|
props.filesystem.exists(absolute)
|
|
@@ -3825,7 +4224,7 @@ function walkSnapshotComplete(
|
|
|
3825
4224
|
snapshot: {
|
|
3826
4225
|
complete: boolean;
|
|
3827
4226
|
directoryComplete: boolean;
|
|
3828
|
-
unstableFiles:
|
|
4227
|
+
unstableFiles: ReadonlySet<string>;
|
|
3829
4228
|
},
|
|
3830
4229
|
declared: ReadonlySet<string> | undefined,
|
|
3831
4230
|
): boolean {
|
|
@@ -3841,6 +4240,136 @@ function walkSnapshotComplete(
|
|
|
3841
4240
|
return true;
|
|
3842
4241
|
}
|
|
3843
4242
|
|
|
4243
|
+
/** Preserve exact project-walk and mutation witnesses for one failed attempt. */
|
|
4244
|
+
function recordProjectSnapshotFailures(
|
|
4245
|
+
failures: TtscGenerationProofFailures,
|
|
4246
|
+
props: {
|
|
4247
|
+
before: ReturnType<typeof collectProjectInputSnapshot>;
|
|
4248
|
+
candidateTracker?: TtscProjectMutationTracker;
|
|
4249
|
+
declared: ReadonlySet<string> | undefined;
|
|
4250
|
+
hostInputTracker?: TtscProjectMutationTracker;
|
|
4251
|
+
identities: FilesystemPathIdentityContext;
|
|
4252
|
+
projectRoot: string;
|
|
4253
|
+
snapshot: ReturnType<typeof collectProjectInputSnapshot>;
|
|
4254
|
+
tracker?: TtscProjectMutationTracker;
|
|
4255
|
+
},
|
|
4256
|
+
): void {
|
|
4257
|
+
const recordWalk = (
|
|
4258
|
+
snapshot: ReturnType<typeof collectProjectInputSnapshot>,
|
|
4259
|
+
): void => {
|
|
4260
|
+
for (const failure of snapshot.walkFailures) {
|
|
4261
|
+
if (failure.kind.startsWith("file-") && props.declared !== undefined) {
|
|
4262
|
+
try {
|
|
4263
|
+
const key = toProjectKey(
|
|
4264
|
+
props.projectRoot,
|
|
4265
|
+
failure.path,
|
|
4266
|
+
props.identities,
|
|
4267
|
+
);
|
|
4268
|
+
if (!props.declared.has(key)) continue;
|
|
4269
|
+
} catch {
|
|
4270
|
+
// An unidentifiable failed input taints the complete project walk.
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
recordGenerationProofFailure(failures, {
|
|
4274
|
+
domain: "project",
|
|
4275
|
+
kind: failure.kind,
|
|
4276
|
+
path: failure.path,
|
|
4277
|
+
});
|
|
4278
|
+
}
|
|
4279
|
+
};
|
|
4280
|
+
recordWalk(props.before);
|
|
4281
|
+
recordWalk(props.snapshot);
|
|
4282
|
+
|
|
4283
|
+
const keys =
|
|
4284
|
+
props.declared ??
|
|
4285
|
+
new Set([
|
|
4286
|
+
...Object.keys(props.before.hashes),
|
|
4287
|
+
...Object.keys(props.snapshot.hashes),
|
|
4288
|
+
]);
|
|
4289
|
+
for (const key of keys) {
|
|
4290
|
+
if (props.before.hashes[key] !== props.snapshot.hashes[key]) {
|
|
4291
|
+
recordGenerationProofFailure(failures, {
|
|
4292
|
+
domain: "project",
|
|
4293
|
+
kind: "input-content-changed",
|
|
4294
|
+
path: path.resolve(props.projectRoot, key),
|
|
4295
|
+
});
|
|
4296
|
+
}
|
|
4297
|
+
if (
|
|
4298
|
+
props.before.fileSignatures[key] !== props.snapshot.fileSignatures[key]
|
|
4299
|
+
) {
|
|
4300
|
+
recordGenerationProofFailure(failures, {
|
|
4301
|
+
domain: "project",
|
|
4302
|
+
kind: "input-metadata-changed",
|
|
4303
|
+
path: path.resolve(props.projectRoot, key),
|
|
4304
|
+
});
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
const leftDirectories = new Map(
|
|
4309
|
+
props.before.projectDirectories.map((entry) => [
|
|
4310
|
+
entry.path,
|
|
4311
|
+
entry.signature,
|
|
4312
|
+
]),
|
|
4313
|
+
);
|
|
4314
|
+
const rightDirectories = new Map(
|
|
4315
|
+
props.snapshot.projectDirectories.map((entry) => [
|
|
4316
|
+
entry.path,
|
|
4317
|
+
entry.signature,
|
|
4318
|
+
]),
|
|
4319
|
+
);
|
|
4320
|
+
for (const directory of new Set([
|
|
4321
|
+
...leftDirectories.keys(),
|
|
4322
|
+
...rightDirectories.keys(),
|
|
4323
|
+
])) {
|
|
4324
|
+
if (leftDirectories.get(directory) !== rightDirectories.get(directory)) {
|
|
4325
|
+
recordGenerationProofFailure(failures, {
|
|
4326
|
+
domain: "project",
|
|
4327
|
+
kind: "directory-membership-changed",
|
|
4328
|
+
path: directory,
|
|
4329
|
+
});
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
|
|
4333
|
+
const recordTracker = (
|
|
4334
|
+
tracker: TtscProjectMutationTracker | undefined,
|
|
4335
|
+
kind: string,
|
|
4336
|
+
): void => {
|
|
4337
|
+
if (tracker?.membershipChanged !== true) return;
|
|
4338
|
+
if (tracker.changes.size === 0) {
|
|
4339
|
+
recordGenerationProofFailure(failures, {
|
|
4340
|
+
domain: "project",
|
|
4341
|
+
kind,
|
|
4342
|
+
path: props.projectRoot,
|
|
4343
|
+
});
|
|
4344
|
+
return;
|
|
4345
|
+
}
|
|
4346
|
+
for (const changed of tracker.changes) {
|
|
4347
|
+
recordGenerationProofFailure(failures, {
|
|
4348
|
+
domain: "project",
|
|
4349
|
+
kind,
|
|
4350
|
+
path: changed,
|
|
4351
|
+
});
|
|
4352
|
+
}
|
|
4353
|
+
if (tracker.changesOmitted) {
|
|
4354
|
+
failures.omitted = Math.min(
|
|
4355
|
+
Number.MAX_SAFE_INTEGER,
|
|
4356
|
+
failures.omitted + 1,
|
|
4357
|
+
);
|
|
4358
|
+
}
|
|
4359
|
+
};
|
|
4360
|
+
recordTracker(props.tracker, "project-membership-event");
|
|
4361
|
+
recordTracker(props.hostInputTracker, "host-input-event");
|
|
4362
|
+
recordTracker(props.candidateTracker, "candidate-event");
|
|
4363
|
+
|
|
4364
|
+
if (failures.entries.length === 0) {
|
|
4365
|
+
recordGenerationProofFailure(failures, {
|
|
4366
|
+
domain: "project",
|
|
4367
|
+
kind: "snapshot-incomplete",
|
|
4368
|
+
path: props.projectRoot,
|
|
4369
|
+
});
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
|
|
3844
4373
|
/** {@link selectDeclaredProjectInputKeys} memoized per envelope generation. */
|
|
3845
4374
|
function declaredProjectInputKeys(
|
|
3846
4375
|
state: TtscEnvelopeDerivation,
|
|
@@ -3851,6 +4380,7 @@ function declaredProjectInputKeys(
|
|
|
3851
4380
|
identities: state.identityContext,
|
|
3852
4381
|
projectRoot: cached.projectRoot,
|
|
3853
4382
|
result: cached.result,
|
|
4383
|
+
scratchDirectory: cached.scratchDirectory,
|
|
3854
4384
|
});
|
|
3855
4385
|
state.declaredInputKeysBuilt = true;
|
|
3856
4386
|
}
|
|
@@ -3867,6 +4397,7 @@ function selectDeclaredProjectInputKeys(props: {
|
|
|
3867
4397
|
identities: FilesystemPathIdentityContext;
|
|
3868
4398
|
projectRoot: string;
|
|
3869
4399
|
result: ITtscCompilerTransformation;
|
|
4400
|
+
scratchDirectory?: string;
|
|
3870
4401
|
}): Set<string> | undefined {
|
|
3871
4402
|
if (props.result.type === "exception" || props.result.graph === undefined) {
|
|
3872
4403
|
return undefined;
|
|
@@ -3875,13 +4406,9 @@ function selectDeclaredProjectInputKeys(props: {
|
|
|
3875
4406
|
const keys = new Set<string>();
|
|
3876
4407
|
const add = (entry: unknown): void => {
|
|
3877
4408
|
if (typeof entry !== "string" || entry.length === 0) return;
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
path.resolve(props.projectRoot, entry),
|
|
3882
|
-
props.identities,
|
|
3883
|
-
),
|
|
3884
|
-
);
|
|
4409
|
+
const absolute = path.resolve(props.projectRoot, entry);
|
|
4410
|
+
if (isTransformScratchInput(absolute, props.scratchDirectory)) return;
|
|
4411
|
+
keys.add(toProjectKey(props.projectRoot, absolute, props.identities));
|
|
3885
4412
|
};
|
|
3886
4413
|
for (const [source, targets] of Object.entries(graph.edges ?? {})) {
|
|
3887
4414
|
add(source);
|
|
@@ -3905,52 +4432,288 @@ function selectDeclaredProjectInputKeys(props: {
|
|
|
3905
4432
|
return keys;
|
|
3906
4433
|
}
|
|
3907
4434
|
|
|
3908
|
-
/**
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
4435
|
+
/** Create an empty bounded witness collection for one transform attempt. */
|
|
4436
|
+
function createGenerationProofFailures(): TtscGenerationProofFailures {
|
|
4437
|
+
return { entries: [], omitted: 0, seen: new Set() };
|
|
4438
|
+
}
|
|
4439
|
+
|
|
4440
|
+
/** Retain one unique proof witness without allowing diagnostics to grow freely. */
|
|
4441
|
+
function recordGenerationProofFailure(
|
|
4442
|
+
failures: TtscGenerationProofFailures,
|
|
4443
|
+
failure: TtscGenerationProofFailure,
|
|
4444
|
+
): void {
|
|
4445
|
+
const key = JSON.stringify([
|
|
4446
|
+
failure.domain,
|
|
4447
|
+
failure.kind,
|
|
4448
|
+
failure.path,
|
|
4449
|
+
failure.detail,
|
|
4450
|
+
]);
|
|
4451
|
+
if (failures.seen.has(key)) return;
|
|
4452
|
+
if (failures.entries.length < MAX_GENERATION_PROOF_FAILURES) {
|
|
4453
|
+
// `seen` follows the same bound as `entries`: retaining every discarded
|
|
4454
|
+
// identity would make a bounded diagnostic an unbounded memory sink.
|
|
4455
|
+
failures.seen.add(key);
|
|
4456
|
+
failures.entries.push(failure);
|
|
4457
|
+
} else {
|
|
4458
|
+
failures.omitted = Math.min(Number.MAX_SAFE_INTEGER, failures.omitted + 1);
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
|
|
4462
|
+
/** Fold one bounded witness collection into another. */
|
|
4463
|
+
function mergeGenerationProofFailures(
|
|
4464
|
+
target: TtscGenerationProofFailures,
|
|
4465
|
+
source: TtscGenerationProofFailures,
|
|
4466
|
+
): void {
|
|
4467
|
+
for (const failure of source.entries) {
|
|
4468
|
+
recordGenerationProofFailure(target, failure);
|
|
4469
|
+
}
|
|
4470
|
+
target.omitted = Math.min(
|
|
4471
|
+
Number.MAX_SAFE_INTEGER,
|
|
4472
|
+
target.omitted + source.omitted,
|
|
4473
|
+
);
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4476
|
+
/** Hash the declared-input-relevant failure shape without retaining it. */
|
|
4477
|
+
function projectWalkFailureFingerprint(
|
|
4478
|
+
snapshot: {
|
|
4479
|
+
complete: boolean;
|
|
4480
|
+
directoryComplete: boolean;
|
|
4481
|
+
unstableFiles: ReadonlySet<string>;
|
|
4482
|
+
walkFailures: readonly TtscProjectWalkFailure[];
|
|
4483
|
+
},
|
|
4484
|
+
declared: ReadonlySet<string> | undefined,
|
|
4485
|
+
projectRoot: string,
|
|
4486
|
+
identities: FilesystemPathIdentityContext,
|
|
4487
|
+
): string {
|
|
4488
|
+
const relevantUnstableFiles =
|
|
4489
|
+
declared === undefined
|
|
4490
|
+
? [...snapshot.unstableFiles]
|
|
4491
|
+
: [...snapshot.unstableFiles].filter((key) => declared.has(key));
|
|
4492
|
+
const relevantFailures = snapshot.walkFailures.filter((failure) => {
|
|
4493
|
+
if (!failure.kind.startsWith("file-")) return true;
|
|
4494
|
+
if (declared === undefined) return true;
|
|
4495
|
+
try {
|
|
4496
|
+
return declared.has(toProjectKey(projectRoot, failure.path, identities));
|
|
4497
|
+
} catch {
|
|
4498
|
+
return true;
|
|
4499
|
+
}
|
|
4500
|
+
});
|
|
4501
|
+
return hashText(
|
|
4502
|
+
JSON.stringify({
|
|
4503
|
+
complete: walkSnapshotComplete(snapshot, declared),
|
|
4504
|
+
directoryComplete: snapshot.directoryComplete,
|
|
4505
|
+
failures: relevantFailures
|
|
4506
|
+
.map((failure) => `${failure.kind}\0${path.resolve(failure.path)}`)
|
|
4507
|
+
.sort(),
|
|
4508
|
+
unstableFiles: relevantUnstableFiles.sort(),
|
|
4509
|
+
}),
|
|
4510
|
+
);
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
/** Compact state of one exact out-of-walk input in a failed generation. */
|
|
4514
|
+
function failedGenerationInputState(
|
|
4515
|
+
input: string,
|
|
4516
|
+
filesystem: TtscTransformFilesystemOperations,
|
|
4517
|
+
): string {
|
|
4518
|
+
let directory = "not-directory";
|
|
4519
|
+
try {
|
|
4520
|
+
if (filesystem.stat(input).isDirectory()) {
|
|
4521
|
+
directory = hashText(
|
|
4522
|
+
filesystem
|
|
4523
|
+
.readdir(input)
|
|
4524
|
+
.map((entry) =>
|
|
4525
|
+
[
|
|
4526
|
+
entry.name,
|
|
4527
|
+
entry.isDirectory(),
|
|
4528
|
+
entry.isFile(),
|
|
4529
|
+
entry.isSymbolicLink(),
|
|
4530
|
+
].join(":"),
|
|
4531
|
+
)
|
|
4532
|
+
.sort()
|
|
4533
|
+
.join("\0"),
|
|
4534
|
+
);
|
|
4535
|
+
}
|
|
4536
|
+
} catch {
|
|
4537
|
+
directory = "unavailable";
|
|
4538
|
+
}
|
|
4539
|
+
return hashText(
|
|
4540
|
+
JSON.stringify([
|
|
4541
|
+
inputMetadataSignature(input, filesystem) ?? "missing",
|
|
4542
|
+
hostInputStateHash(input, filesystem) ?? MISSING_INPUT_STATE,
|
|
4543
|
+
hostInputRealpath(input, filesystem),
|
|
4544
|
+
directory,
|
|
4545
|
+
]),
|
|
4546
|
+
);
|
|
4547
|
+
}
|
|
4548
|
+
|
|
4549
|
+
/** Snapshot every input outside the project walk that could change a retry. */
|
|
4550
|
+
function captureFailedGenerationInputStates(
|
|
4551
|
+
cached: TtscCachedProjectTransform,
|
|
4552
|
+
failures: TtscGenerationProofFailures,
|
|
4553
|
+
): ReadonlyMap<string, string> {
|
|
4554
|
+
const filesystem = resultFilesystem(cached.result);
|
|
4555
|
+
const inputs = new Set(
|
|
4556
|
+
(cached.externalInputPaths ?? []).map((input) => path.resolve(input)),
|
|
4557
|
+
);
|
|
4558
|
+
for (const input of selectPersistentHostInputs({
|
|
4559
|
+
filesystem,
|
|
4560
|
+
projectRoot: cached.projectRoot,
|
|
4561
|
+
result: cached.result,
|
|
4562
|
+
scratchDirectory: cached.scratchDirectory,
|
|
4563
|
+
temporaryTsconfig: cached.temporaryTsconfig,
|
|
4564
|
+
})) {
|
|
4565
|
+
inputs.add(path.resolve(input));
|
|
4566
|
+
}
|
|
4567
|
+
for (const failure of failures.entries) {
|
|
4568
|
+
if (failure.path !== undefined) inputs.add(path.resolve(failure.path));
|
|
4569
|
+
}
|
|
4570
|
+
return new Map(
|
|
4571
|
+
[...inputs]
|
|
4572
|
+
.sort()
|
|
4573
|
+
.map((input) => [input, failedGenerationInputState(input, filesystem)]),
|
|
4574
|
+
);
|
|
4575
|
+
}
|
|
4576
|
+
|
|
4577
|
+
/** Capture source baselines for project and out-of-walk transform outputs. */
|
|
4578
|
+
function captureTransformSourceHashes(
|
|
4579
|
+
cached: TtscCachedProjectTransform,
|
|
4580
|
+
currentFile: string,
|
|
4581
|
+
currentSourceHash: string,
|
|
4582
|
+
): Record<string, string> {
|
|
4583
|
+
const filesystem = resultFilesystem(cached.result);
|
|
4584
|
+
const identities = envelopeDerivation(cached).identityContext;
|
|
4585
|
+
const hashes: Record<string, string> = {};
|
|
4586
|
+
if (cached.result.type === "success") {
|
|
4587
|
+
for (const output of Object.keys(cached.result.typescript)) {
|
|
4588
|
+
const file = path.resolve(cached.projectRoot, output);
|
|
4589
|
+
const hash = hostInputStateHash(file, filesystem);
|
|
4590
|
+
if (hash !== null) hashes[pathIdentityKey(file, identities)] = hash;
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
hashes[pathIdentityKey(currentFile, identities)] = currentSourceHash;
|
|
4594
|
+
return hashes;
|
|
4595
|
+
}
|
|
3913
4596
|
|
|
3914
4597
|
/**
|
|
3915
|
-
*
|
|
4598
|
+
* Whether a terminal proof failure's observed environment actually changed.
|
|
3916
4599
|
*
|
|
3917
|
-
*
|
|
3918
|
-
*
|
|
3919
|
-
*
|
|
3920
|
-
* that never finished, and each investigation had to rediscover the cause from
|
|
3921
|
-
* outside. A named reason turns the next occurrence into a bug report instead
|
|
3922
|
-
* of an archaeology session.
|
|
4600
|
+
* This is deliberately a confirmation test: inability to re-probe retains the
|
|
4601
|
+
* old verdict instead of turning every module request into another compile.
|
|
4602
|
+
* Cache lifecycle reset remains the unconditional recovery boundary.
|
|
3923
4603
|
*/
|
|
3924
|
-
function
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
walkStable: boolean;
|
|
4604
|
+
function failedGenerationEnvironmentChanged(
|
|
4605
|
+
validation: TtscFailedGenerationValidation,
|
|
4606
|
+
props: {
|
|
4607
|
+
currentFile: string;
|
|
4608
|
+
currentSource: string;
|
|
4609
|
+
filesystem: TtscTransformFilesystemOperations;
|
|
3931
4610
|
},
|
|
3932
|
-
):
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
4611
|
+
): boolean {
|
|
4612
|
+
try {
|
|
4613
|
+
const identities = envelopeDerivation(validation.cached).identityContext;
|
|
4614
|
+
const currentSourceHash = hashText(props.currentSource);
|
|
4615
|
+
const expectedSourceHash =
|
|
4616
|
+
validation.cached.sourceHashes?.[
|
|
4617
|
+
pathIdentityKey(props.currentFile, identities)
|
|
4618
|
+
];
|
|
4619
|
+
if (
|
|
4620
|
+
expectedSourceHash !== undefined &&
|
|
4621
|
+
expectedSourceHash !== currentSourceHash
|
|
4622
|
+
) {
|
|
4623
|
+
return true;
|
|
4624
|
+
}
|
|
4625
|
+
const current = collectProjectInputSnapshot(
|
|
4626
|
+
validation.cached.projectRoot,
|
|
4627
|
+
identities,
|
|
4628
|
+
props.filesystem,
|
|
4629
|
+
);
|
|
4630
|
+
if (
|
|
4631
|
+
validation.projectWalkComplete !==
|
|
4632
|
+
walkSnapshotComplete(current, validation.declaredInputs) ||
|
|
4633
|
+
validation.projectWalkFailures !==
|
|
4634
|
+
projectWalkFailureFingerprint(
|
|
4635
|
+
current,
|
|
4636
|
+
validation.declaredInputs,
|
|
4637
|
+
validation.cached.projectRoot,
|
|
4638
|
+
identities,
|
|
4639
|
+
) ||
|
|
4640
|
+
!sameHashes(
|
|
4641
|
+
validation.projectInputHashes,
|
|
4642
|
+
current.hashes,
|
|
4643
|
+
validation.declaredInputs,
|
|
4644
|
+
) ||
|
|
4645
|
+
!sameProjectDirectories(
|
|
4646
|
+
validation.cached.projectDirectories ?? [],
|
|
4647
|
+
current.projectDirectories,
|
|
4648
|
+
)
|
|
4649
|
+
) {
|
|
4650
|
+
return true;
|
|
4651
|
+
}
|
|
4652
|
+
for (const [input, recorded] of validation.inputStates) {
|
|
4653
|
+
if (failedGenerationInputState(input, props.filesystem) !== recorded) {
|
|
4654
|
+
return true;
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
return false;
|
|
4658
|
+
} catch {
|
|
4659
|
+
return false;
|
|
3944
4660
|
}
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
4661
|
+
}
|
|
4662
|
+
|
|
4663
|
+
/** Render one input without leaking source content or control characters. */
|
|
4664
|
+
function formatGenerationFailurePath(
|
|
4665
|
+
projectRoot: string,
|
|
4666
|
+
input: string,
|
|
4667
|
+
): string {
|
|
4668
|
+
const absolute = path.resolve(input);
|
|
4669
|
+
const relative = path.relative(projectRoot, absolute);
|
|
4670
|
+
const display =
|
|
4671
|
+
relative === ""
|
|
4672
|
+
? "."
|
|
4673
|
+
: relative !== ".." &&
|
|
4674
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
4675
|
+
!path.isAbsolute(relative)
|
|
4676
|
+
? relative
|
|
4677
|
+
: absolute;
|
|
4678
|
+
return JSON.stringify(display.split(path.sep).join("/"));
|
|
4679
|
+
}
|
|
4680
|
+
|
|
4681
|
+
/** Build the terminal error shared by every waiter of an unstable generation. */
|
|
4682
|
+
function createUnstableGenerationError(
|
|
4683
|
+
projectRoot: string,
|
|
4684
|
+
attempts: readonly TtscGenerationProofFailures[],
|
|
4685
|
+
validation: TtscFailedGenerationValidation,
|
|
4686
|
+
): TtscUnstableGenerationError {
|
|
4687
|
+
const lines = [
|
|
4688
|
+
`ttsc: could not capture a reusable transform generation after ${attempts.length} attempts.`,
|
|
4689
|
+
` project: ${projectRoot}`,
|
|
4690
|
+
];
|
|
4691
|
+
attempts.forEach((failures, index) => {
|
|
4692
|
+
lines.push(` attempt ${index + 1}:`);
|
|
4693
|
+
if (failures.entries.length === 0) {
|
|
4694
|
+
lines.push(" - project/generation-proof-incomplete");
|
|
4695
|
+
}
|
|
4696
|
+
for (const failure of failures.entries) {
|
|
4697
|
+
const input =
|
|
4698
|
+
failure.path === undefined
|
|
4699
|
+
? ""
|
|
4700
|
+
: `: ${formatGenerationFailurePath(projectRoot, failure.path)}`;
|
|
4701
|
+
const detail =
|
|
4702
|
+
failure.detail === undefined
|
|
4703
|
+
? ""
|
|
4704
|
+
: ` (producer: ${JSON.stringify(failure.detail)})`;
|
|
4705
|
+
lines.push(` - ${failure.domain}/${failure.kind}${input}${detail}`);
|
|
4706
|
+
}
|
|
4707
|
+
if (failures.omitted !== 0) {
|
|
4708
|
+
lines.push(
|
|
4709
|
+
` - ... ${failures.omitted} additional witness(es) omitted`,
|
|
4710
|
+
);
|
|
4711
|
+
}
|
|
4712
|
+
});
|
|
4713
|
+
lines.push(
|
|
4714
|
+
" Stop writes to the listed inputs before compilation, or fix the producer that omitted or contradicted the listed proof.",
|
|
3953
4715
|
);
|
|
4716
|
+
return new TtscUnstableGenerationError(lines.join("\n"), validation);
|
|
3954
4717
|
}
|
|
3955
4718
|
|
|
3956
4719
|
function hashText(input: string | Buffer): string {
|
|
@@ -3966,6 +4729,52 @@ async function transformProject(props: {
|
|
|
3966
4729
|
plugins?: ResolvedTtscUnpluginOptions["plugins"];
|
|
3967
4730
|
trackProjectMembership: boolean;
|
|
3968
4731
|
tsconfig: string;
|
|
4732
|
+
}): Promise<TtscCachedProjectTransform> {
|
|
4733
|
+
const attempts: TtscGenerationProofFailures[] = [];
|
|
4734
|
+
for (let attempt = 0; attempt < TRANSFORM_GENERATION_ATTEMPTS; attempt += 1) {
|
|
4735
|
+
const cached = await captureTransformGeneration(props);
|
|
4736
|
+
if (
|
|
4737
|
+
!props.trackProjectMembership ||
|
|
4738
|
+
cached.result.type !== "success" ||
|
|
4739
|
+
cached.projectSnapshotComplete === true
|
|
4740
|
+
) {
|
|
4741
|
+
return cached;
|
|
4742
|
+
}
|
|
4743
|
+
attempts.push(
|
|
4744
|
+
TRANSFORM_GENERATION_FAILURES.get(cached.result) ??
|
|
4745
|
+
createGenerationProofFailures(),
|
|
4746
|
+
);
|
|
4747
|
+
if (attempt + 1 === TRANSFORM_GENERATION_ATTEMPTS) {
|
|
4748
|
+
const validation = TRANSFORM_FAILED_GENERATION_VALIDATIONS.get(
|
|
4749
|
+
cached.result,
|
|
4750
|
+
);
|
|
4751
|
+
if (validation === undefined) {
|
|
4752
|
+
disposeCachedTransform(cached);
|
|
4753
|
+
throw new Error(
|
|
4754
|
+
"ttsc: failed transform generation has no retry validation baseline",
|
|
4755
|
+
);
|
|
4756
|
+
}
|
|
4757
|
+
throw createUnstableGenerationError(
|
|
4758
|
+
path.dirname(props.tsconfig),
|
|
4759
|
+
attempts,
|
|
4760
|
+
validation,
|
|
4761
|
+
);
|
|
4762
|
+
}
|
|
4763
|
+
disposeCachedTransform(cached);
|
|
4764
|
+
}
|
|
4765
|
+
throw new Error("ttsc: transform generation retry loop did not terminate");
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
/** Capture one whole-project transform attempt and all of its reuse proofs. */
|
|
4769
|
+
async function captureTransformGeneration(props: {
|
|
4770
|
+
aliasPaths: Record<string, string[]>;
|
|
4771
|
+
compilerOptions: Record<string, unknown>;
|
|
4772
|
+
currentFile: string;
|
|
4773
|
+
currentSource: string;
|
|
4774
|
+
filesystem: TtscTransformFilesystemOperations;
|
|
4775
|
+
plugins?: ResolvedTtscUnpluginOptions["plugins"];
|
|
4776
|
+
trackProjectMembership: boolean;
|
|
4777
|
+
tsconfig: string;
|
|
3969
4778
|
}): Promise<TtscCachedProjectTransform> {
|
|
3970
4779
|
const projectRoot = path.dirname(props.tsconfig);
|
|
3971
4780
|
const scratchDirectory = createTransformScratchDirectory(
|
|
@@ -4019,6 +4828,7 @@ async function transformProject(props: {
|
|
|
4019
4828
|
filesystem: props.filesystem,
|
|
4020
4829
|
projectRoot,
|
|
4021
4830
|
result,
|
|
4831
|
+
scratchDirectory,
|
|
4022
4832
|
temporaryTsconfig,
|
|
4023
4833
|
});
|
|
4024
4834
|
// The generation's absent resolution candidates, which get a watcher of
|
|
@@ -4034,6 +4844,7 @@ async function transformProject(props: {
|
|
|
4034
4844
|
filesystem: props.filesystem,
|
|
4035
4845
|
projectRoot,
|
|
4036
4846
|
result,
|
|
4847
|
+
scratchDirectory,
|
|
4037
4848
|
temporaryTsconfig,
|
|
4038
4849
|
})
|
|
4039
4850
|
: { candidates: [], watched: [] };
|
|
@@ -4067,6 +4878,7 @@ async function transformProject(props: {
|
|
|
4067
4878
|
filesystem: props.filesystem,
|
|
4068
4879
|
projectRoot,
|
|
4069
4880
|
result,
|
|
4881
|
+
scratchDirectory,
|
|
4070
4882
|
temporaryTsconfig,
|
|
4071
4883
|
});
|
|
4072
4884
|
const inputSnapshot = collectProjectInputSnapshot(
|
|
@@ -4083,6 +4895,7 @@ async function transformProject(props: {
|
|
|
4083
4895
|
identities,
|
|
4084
4896
|
projectRoot,
|
|
4085
4897
|
result,
|
|
4898
|
+
scratchDirectory,
|
|
4086
4899
|
});
|
|
4087
4900
|
const walkStable =
|
|
4088
4901
|
walkSnapshotComplete(before, declaredInputs) &&
|
|
@@ -4111,14 +4924,21 @@ async function transformProject(props: {
|
|
|
4111
4924
|
props.currentFile,
|
|
4112
4925
|
identities,
|
|
4113
4926
|
);
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4927
|
+
const currentSourceHash = hashText(props.currentSource);
|
|
4928
|
+
const projectInputHashes = { ...inputSnapshot.hashes };
|
|
4929
|
+
if (
|
|
4930
|
+
Object.prototype.hasOwnProperty.call(inputSnapshot.hashes, currentFileKey)
|
|
4931
|
+
) {
|
|
4932
|
+
inputSnapshot.hashes[currentFileKey] = currentSourceHash;
|
|
4933
|
+
// That overlay makes this one key the only recorded hash a disk signature
|
|
4934
|
+
// cannot stand for: the bytes it names came from the bundler, not the file.
|
|
4935
|
+
delete inputSnapshot.provenSignatures[currentFileKey];
|
|
4936
|
+
}
|
|
4118
4937
|
const cached: TtscCachedProjectTransform = {
|
|
4119
4938
|
// Capture the out-of-walk input hashes while the generation is fresh so
|
|
4120
4939
|
// cache validation can re-check them; computed before dispose so the
|
|
4121
|
-
//
|
|
4940
|
+
// scratch-tree exclusion is the only reason its disposed artifacts never
|
|
4941
|
+
// key the persistent generation.
|
|
4122
4942
|
externalInputHashes: {},
|
|
4123
4943
|
externalInputRealpaths: {},
|
|
4124
4944
|
externalInputPaths,
|
|
@@ -4128,12 +4948,18 @@ async function transformProject(props: {
|
|
|
4128
4948
|
projectSnapshotComplete: false,
|
|
4129
4949
|
projectRoot,
|
|
4130
4950
|
result,
|
|
4951
|
+
scratchDirectory,
|
|
4131
4952
|
servedFiles: new Set(),
|
|
4132
4953
|
// Remember the generated temp-dir tsconfig (disposed below) so watch
|
|
4133
4954
|
// derivation can drop it from the envelope's config chain; a registered
|
|
4134
4955
|
// but deleted file would invalidate every persistent-cache snapshot.
|
|
4135
4956
|
...(temporaryTsconfig === undefined ? {} : { temporaryTsconfig }),
|
|
4136
4957
|
};
|
|
4958
|
+
cached.sourceHashes = captureTransformSourceHashes(
|
|
4959
|
+
cached,
|
|
4960
|
+
props.currentFile,
|
|
4961
|
+
currentSourceHash,
|
|
4962
|
+
);
|
|
4137
4963
|
const externalInputSnapshot = captureExternalInputSnapshot(
|
|
4138
4964
|
cached,
|
|
4139
4965
|
externalInputPaths,
|
|
@@ -4145,23 +4971,52 @@ async function transformProject(props: {
|
|
|
4145
4971
|
// cannot be reused can say which evidence it lacked. The extra work runs
|
|
4146
4972
|
// only on the failing path, where the alternative is recompiling the whole
|
|
4147
4973
|
// project for every remaining module.
|
|
4148
|
-
const
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4974
|
+
const failures = createGenerationProofFailures();
|
|
4975
|
+
if (!walkStable) {
|
|
4976
|
+
recordProjectSnapshotFailures(failures, {
|
|
4977
|
+
before,
|
|
4978
|
+
candidateTracker,
|
|
4979
|
+
declared: declaredInputs,
|
|
4980
|
+
hostInputTracker,
|
|
4981
|
+
identities,
|
|
4982
|
+
projectRoot,
|
|
4983
|
+
snapshot: inputSnapshot,
|
|
4984
|
+
tracker,
|
|
4985
|
+
});
|
|
4986
|
+
}
|
|
4987
|
+
const graphFailures = compilerGraphInputProofFailures(cached);
|
|
4988
|
+
mergeGenerationProofFailures(failures, graphFailures);
|
|
4989
|
+
mergeGenerationProofFailures(failures, externalInputSnapshot.failures);
|
|
4990
|
+
const universalInputCapture = captureUniversalHostInputValidation(
|
|
4991
|
+
cached,
|
|
4992
|
+
props.currentFile,
|
|
4993
|
+
);
|
|
4994
|
+
mergeGenerationProofFailures(failures, universalInputCapture.failures);
|
|
4995
|
+
const graphProofs =
|
|
4996
|
+
graphFailures.entries.length === 0 && graphFailures.omitted === 0;
|
|
4997
|
+
const universalInputs = universalInputCapture.validation !== undefined;
|
|
4152
4998
|
const stableProjectSnapshot =
|
|
4153
4999
|
walkStable &&
|
|
4154
5000
|
graphProofs &&
|
|
4155
5001
|
externalInputSnapshot.complete &&
|
|
4156
5002
|
universalInputs;
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
5003
|
+
if (!stableProjectSnapshot) {
|
|
5004
|
+
TRANSFORM_GENERATION_FAILURES.set(result, failures);
|
|
5005
|
+
TRANSFORM_FAILED_GENERATION_VALIDATIONS.set(result, {
|
|
5006
|
+
cached,
|
|
5007
|
+
declaredInputs,
|
|
5008
|
+
inputStates: captureFailedGenerationInputStates(cached, failures),
|
|
5009
|
+
projectInputHashes,
|
|
5010
|
+
projectWalkComplete: walkSnapshotComplete(
|
|
5011
|
+
inputSnapshot,
|
|
5012
|
+
declaredInputs,
|
|
5013
|
+
),
|
|
5014
|
+
projectWalkFailures: projectWalkFailureFingerprint(
|
|
5015
|
+
inputSnapshot,
|
|
5016
|
+
declaredInputs,
|
|
5017
|
+
projectRoot,
|
|
5018
|
+
identities,
|
|
5019
|
+
),
|
|
4165
5020
|
});
|
|
4166
5021
|
}
|
|
4167
5022
|
cached.projectSnapshotComplete = stableProjectSnapshot;
|
|
@@ -4209,21 +5064,30 @@ async function transformProject(props: {
|
|
|
4209
5064
|
}
|
|
4210
5065
|
}
|
|
4211
5066
|
|
|
4212
|
-
/** Exclude
|
|
5067
|
+
/** Exclude disposed transform scratch from live host-input tracking. */
|
|
4213
5068
|
function selectPersistentHostInputs(props: {
|
|
4214
5069
|
filesystem: TtscTransformFilesystemOperations;
|
|
4215
5070
|
projectRoot: string;
|
|
4216
5071
|
result: ITtscCompilerTransformation;
|
|
5072
|
+
scratchDirectory?: string;
|
|
4217
5073
|
temporaryTsconfig?: string;
|
|
4218
5074
|
}): string[] {
|
|
4219
5075
|
if (props.result.type === "exception") return [];
|
|
4220
5076
|
const inputs = selectListedFiles(props.projectRoot, props.result.hostInputs);
|
|
4221
|
-
if (
|
|
5077
|
+
if (
|
|
5078
|
+
props.scratchDirectory === undefined &&
|
|
5079
|
+
props.temporaryTsconfig === undefined
|
|
5080
|
+
)
|
|
5081
|
+
return inputs;
|
|
4222
5082
|
const identities = createHostPathIdentityContext(props.filesystem);
|
|
4223
|
-
const temporary =
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
5083
|
+
const temporary =
|
|
5084
|
+
props.temporaryTsconfig === undefined
|
|
5085
|
+
? undefined
|
|
5086
|
+
: pathIdentityKey(props.temporaryTsconfig, identities);
|
|
5087
|
+
return inputs.filter((input) => {
|
|
5088
|
+
if (isTransformScratchInput(input, props.scratchDirectory)) return false;
|
|
5089
|
+
return pathIdentityKey(input, identities) !== temporary;
|
|
5090
|
+
});
|
|
4227
5091
|
}
|
|
4228
5092
|
|
|
4229
5093
|
function createTransformTsconfig(
|
|
@@ -4344,6 +5208,17 @@ function pathIsWithin(child: string, parent: string): boolean {
|
|
|
4344
5208
|
);
|
|
4345
5209
|
}
|
|
4346
5210
|
|
|
5211
|
+
/** Whether an input is owned by the disposable transform scratch tree. */
|
|
5212
|
+
function isTransformScratchInput(
|
|
5213
|
+
input: string,
|
|
5214
|
+
scratchDirectory: string | undefined,
|
|
5215
|
+
): boolean {
|
|
5216
|
+
return (
|
|
5217
|
+
scratchDirectory !== undefined &&
|
|
5218
|
+
pathIsWithin(path.resolve(input), path.resolve(scratchDirectory))
|
|
5219
|
+
);
|
|
5220
|
+
}
|
|
5221
|
+
|
|
4347
5222
|
/** Route all compiler/plugin scratch to one owned directory outside project. */
|
|
4348
5223
|
function transformScratchEnvironment(directory: string): NodeJS.ProcessEnv {
|
|
4349
5224
|
return {
|