@ttsc/unplugin 0.28.2 → 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/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/transform.d.cts +23 -4
- package/lib/core/transform.d.mts +23 -4
- 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/package.json +3 -3
- package/src/core/index.ts +18 -1
- package/src/core/transform.ts +1014 -139
package/lib/core/transform.js
CHANGED
|
@@ -9,6 +9,27 @@ var ttsc = require('ttsc');
|
|
|
9
9
|
var pathIdentity = require('ttsc/path-identity');
|
|
10
10
|
var tsconfigPaths = require('./tsconfigPaths.js');
|
|
11
11
|
|
|
12
|
+
/** A bounded proof failure that stays authoritative until its inputs change. */
|
|
13
|
+
class TtscUnstableGenerationError extends Error {
|
|
14
|
+
validation;
|
|
15
|
+
constructor(message, validation) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "TtscUnstableGenerationError";
|
|
18
|
+
this.validation = validation;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Proof witnesses retained beside a compiler result without extending its API. */
|
|
22
|
+
const TRANSFORM_GENERATION_FAILURES = new WeakMap();
|
|
23
|
+
/** Retry baselines retained only for attempts that could not be published. */
|
|
24
|
+
const TRANSFORM_FAILED_GENERATION_VALIDATIONS = new WeakMap();
|
|
25
|
+
/** Rejected cache promises whose unchanged terminal verdict may be replayed. */
|
|
26
|
+
const TERMINAL_TRANSFORM_GENERATIONS = new WeakMap();
|
|
27
|
+
/** Maximum witnesses printed and retained for each failed transform attempt. */
|
|
28
|
+
const MAX_GENERATION_PROOF_FAILURES = 8;
|
|
29
|
+
/** Maximum exact mutation paths kept after a tracker already proved a change. */
|
|
30
|
+
const MAX_GENERATION_MUTATION_PATHS = 8;
|
|
31
|
+
/** One retry absorbs a transient watch write without admitting an infinite loop. */
|
|
32
|
+
const TRANSFORM_GENERATION_ATTEMPTS = 2;
|
|
12
33
|
const DEFAULT_FILESYSTEM_OPERATIONS = Object.freeze({
|
|
13
34
|
exists: fs.existsSync,
|
|
14
35
|
lstat: (location) => fs.lstatSync(location, { bigint: true }),
|
|
@@ -142,8 +163,27 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
|
|
|
142
163
|
for (;;) {
|
|
143
164
|
let transformed = cache?.get(key);
|
|
144
165
|
if (transformed !== undefined) {
|
|
145
|
-
|
|
146
|
-
|
|
166
|
+
const terminal = TERMINAL_TRANSFORM_GENERATIONS.get(transformed);
|
|
167
|
+
if (terminal !== undefined) {
|
|
168
|
+
// A proof failure is a verdict about one observed environment, not an
|
|
169
|
+
// invitation for every later module to repeat the whole compile. Keep
|
|
170
|
+
// replaying it until a source/input probe or an explicit cache reset
|
|
171
|
+
// establishes that a new generation could differ.
|
|
172
|
+
if (!failedGenerationEnvironmentChanged(terminal.validation, {
|
|
173
|
+
currentFile: file,
|
|
174
|
+
currentSource: source,
|
|
175
|
+
filesystem,
|
|
176
|
+
})) {
|
|
177
|
+
throw terminal;
|
|
178
|
+
}
|
|
179
|
+
evictGeneration(cache, key, transformed);
|
|
180
|
+
if (cache?.get(key) !== undefined) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
transformed = undefined;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (transformed !== undefined) {
|
|
147
187
|
const cached = await awaitOrEvict(cache, key, transformed);
|
|
148
188
|
TRANSFORM_RESULT_FILESYSTEM.set(cached.result, filesystem);
|
|
149
189
|
// While this caller awaited the old Promise, another caller may have
|
|
@@ -223,20 +263,28 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
|
|
|
223
263
|
}
|
|
224
264
|
}
|
|
225
265
|
/**
|
|
226
|
-
* Await a cached generation,
|
|
266
|
+
* Await a cached generation, retaining only terminal proof failures.
|
|
227
267
|
*
|
|
228
268
|
* The cache stores the in-flight transform Promise before it settles so
|
|
229
|
-
* concurrent callers share one compilation.
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
269
|
+
* concurrent callers share one compilation. Ordinary compiler and host
|
|
270
|
+
* rejections are evicted so a transient failure cannot become permanent. A
|
|
271
|
+
* bounded stabilization failure is different: it already spent its retry and
|
|
272
|
+
* repeating it for every later module recreates the issue this gate prevents.
|
|
273
|
+
* It stays authoritative until its retained input baseline changes or the cache
|
|
274
|
+
* owner starts a new lifecycle.
|
|
233
275
|
*/
|
|
234
276
|
async function awaitOrEvict(cache, key, generation) {
|
|
235
277
|
try {
|
|
236
278
|
return await generation;
|
|
237
279
|
}
|
|
238
280
|
catch (error) {
|
|
239
|
-
|
|
281
|
+
if (error instanceof TtscUnstableGenerationError &&
|
|
282
|
+
cache?.get(key) === generation) {
|
|
283
|
+
TERMINAL_TRANSFORM_GENERATIONS.set(generation, error);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
evictGeneration(cache, key, generation);
|
|
287
|
+
}
|
|
240
288
|
throw error;
|
|
241
289
|
}
|
|
242
290
|
}
|
|
@@ -318,6 +366,7 @@ function envelopeGraphIndexes(state, props) {
|
|
|
318
366
|
members: new Set(),
|
|
319
367
|
speculative: new Set(),
|
|
320
368
|
inputProofs: new Map(),
|
|
369
|
+
inputProofFailures: new Map(),
|
|
321
370
|
inputProofConflicts: new Set(),
|
|
322
371
|
};
|
|
323
372
|
const graph = props.result.type === "exception" ? undefined : props.result.graph;
|
|
@@ -335,7 +384,11 @@ function envelopeGraphIndexes(state, props) {
|
|
|
335
384
|
.filter((target) => typeof target === "string" && target.length !== 0)
|
|
336
385
|
.map((target) => {
|
|
337
386
|
const absoluteTarget = path.resolve(props.projectRoot, target);
|
|
338
|
-
|
|
387
|
+
const targetIdentity = derivationIdentity(state, absoluteTarget);
|
|
388
|
+
built.members.add(targetIdentity);
|
|
389
|
+
if (!built.spellings.has(targetIdentity)) {
|
|
390
|
+
built.spellings.set(targetIdentity, absoluteTarget);
|
|
391
|
+
}
|
|
339
392
|
return absoluteTarget;
|
|
340
393
|
}));
|
|
341
394
|
built.edges.set(identity, entries);
|
|
@@ -343,7 +396,10 @@ function envelopeGraphIndexes(state, props) {
|
|
|
343
396
|
built.globals.push(...selectListedFiles(props.projectRoot, graph.globals));
|
|
344
397
|
built.configs.push(...selectListedFiles(props.projectRoot, graph.configs));
|
|
345
398
|
for (const input of [...built.globals, ...built.configs]) {
|
|
346
|
-
|
|
399
|
+
const identity = derivationIdentity(state, input);
|
|
400
|
+
built.members.add(identity);
|
|
401
|
+
if (!built.spellings.has(identity))
|
|
402
|
+
built.spellings.set(identity, input);
|
|
347
403
|
}
|
|
348
404
|
const candidateEntries = Object.entries(graph.candidates ?? {}).filter((entry) => Array.isArray(entry[1]));
|
|
349
405
|
// Every candidate source is an importing file the compiler read, so fold
|
|
@@ -351,7 +407,12 @@ function envelopeGraphIndexes(state, props) {
|
|
|
351
407
|
// candidate could be classified speculative before a later entry proves
|
|
352
408
|
// the same path is a realized source.
|
|
353
409
|
for (const [source] of candidateEntries) {
|
|
354
|
-
|
|
410
|
+
const absoluteSource = path.resolve(props.projectRoot, source);
|
|
411
|
+
const identity = derivationIdentity(state, absoluteSource);
|
|
412
|
+
built.members.add(identity);
|
|
413
|
+
if (!built.spellings.has(identity)) {
|
|
414
|
+
built.spellings.set(identity, absoluteSource);
|
|
415
|
+
}
|
|
355
416
|
}
|
|
356
417
|
const realized = new Set(built.members);
|
|
357
418
|
for (const [source, candidates] of candidateEntries) {
|
|
@@ -369,6 +430,17 @@ function envelopeGraphIndexes(state, props) {
|
|
|
369
430
|
if (!realized.has(identity))
|
|
370
431
|
built.speculative.add(identity);
|
|
371
432
|
built.members.add(identity);
|
|
433
|
+
if (!built.spellings.has(identity)) {
|
|
434
|
+
built.spellings.set(identity, path.resolve(props.projectRoot, candidate));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const transformSources = new Set();
|
|
439
|
+
if (props.result.type === "success") {
|
|
440
|
+
for (const output of Object.keys(props.result.typescript)) {
|
|
441
|
+
if (!isDeclarationFile(output)) {
|
|
442
|
+
transformSources.add(derivationIdentity(state, path.resolve(props.projectRoot, output)));
|
|
443
|
+
}
|
|
372
444
|
}
|
|
373
445
|
}
|
|
374
446
|
for (const [input, hash] of Object.entries(graph.inputHashes ?? {})) {
|
|
@@ -388,8 +460,9 @@ function envelopeGraphIndexes(state, props) {
|
|
|
388
460
|
}
|
|
389
461
|
const absolute = path.resolve(props.projectRoot, input);
|
|
390
462
|
const identity = derivationIdentity(state, absolute);
|
|
391
|
-
if (!built.members.has(identity))
|
|
463
|
+
if (!built.members.has(identity) && !transformSources.has(identity)) {
|
|
392
464
|
continue;
|
|
465
|
+
}
|
|
393
466
|
const proof = {
|
|
394
467
|
hash,
|
|
395
468
|
path: absolute,
|
|
@@ -406,6 +479,23 @@ function envelopeGraphIndexes(state, props) {
|
|
|
406
479
|
built.inputProofs.set(identity, proof);
|
|
407
480
|
}
|
|
408
481
|
}
|
|
482
|
+
for (const [input, reason] of Object.entries(graph.inputProofFailures ?? {})) {
|
|
483
|
+
if (typeof reason !== "string" || !/^[a-z0-9-]{1,64}$/.test(reason)) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const absolute = path.resolve(props.projectRoot, input);
|
|
487
|
+
const identity = derivationIdentity(state, absolute);
|
|
488
|
+
if (!built.members.has(identity) && !transformSources.has(identity)) {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (built.inputProofs.has(identity)) {
|
|
492
|
+
built.inputProofs.delete(identity);
|
|
493
|
+
built.inputProofConflicts.add(identity);
|
|
494
|
+
}
|
|
495
|
+
if (!built.inputProofFailures.has(identity)) {
|
|
496
|
+
built.inputProofFailures.set(identity, reason);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
409
499
|
}
|
|
410
500
|
state.graph = built;
|
|
411
501
|
return built;
|
|
@@ -452,8 +542,9 @@ function collectDeclaredIdentities(state, projectRoot, listed) {
|
|
|
452
542
|
* Envelope keys mirror the `typescript` keys (project-relative); values may be
|
|
453
543
|
* project-relative or absolute. Every path is absolutized against the project
|
|
454
544
|
* root and deduplicated; the file itself is dropped (the bundler already
|
|
455
|
-
* watches the module it transforms), and so is the disposed
|
|
456
|
-
* (see
|
|
545
|
+
* watches the module it transforms), and so is every path in the disposed
|
|
546
|
+
* transform scratch tree (see
|
|
547
|
+
* {@link TtscCachedProjectTransform.scratchDirectory}).
|
|
457
548
|
*/
|
|
458
549
|
function notifyWatchInputs(hooks, cached, file) {
|
|
459
550
|
const addWatchFile = hooks?.addWatchFile;
|
|
@@ -466,6 +557,7 @@ function notifyWatchInputs(hooks, cached, file) {
|
|
|
466
557
|
file,
|
|
467
558
|
projectRoot: cached.projectRoot,
|
|
468
559
|
result: cached.result,
|
|
560
|
+
scratchDirectory: cached.scratchDirectory,
|
|
469
561
|
temporaryTsconfig: cached.temporaryTsconfig,
|
|
470
562
|
})) {
|
|
471
563
|
// Hand the adapter the identity this generation already resolved and the
|
|
@@ -535,6 +627,7 @@ function deriveWatchInputs(state, props, fileIdentity) {
|
|
|
535
627
|
const spelling = path.resolve(input);
|
|
536
628
|
if (spelling === currentSpelling ||
|
|
537
629
|
spelling === temporarySpelling ||
|
|
630
|
+
isTransformScratchInput(spelling, props.scratchDirectory) ||
|
|
538
631
|
lexicalSeen.has(spelling)) {
|
|
539
632
|
return;
|
|
540
633
|
}
|
|
@@ -543,6 +636,8 @@ function deriveWatchInputs(state, props, fileIdentity) {
|
|
|
543
636
|
output.push(input);
|
|
544
637
|
};
|
|
545
638
|
const appendPhysical = (input) => {
|
|
639
|
+
if (isTransformScratchInput(input, props.scratchDirectory))
|
|
640
|
+
return;
|
|
546
641
|
const identity = derivationIdentity(state, input);
|
|
547
642
|
if (excluded.has(identity) || physicalSeen.has(identity))
|
|
548
643
|
return;
|
|
@@ -786,11 +881,22 @@ function stripQuery(id) {
|
|
|
786
881
|
return query === -1 ? id : id.slice(0, query);
|
|
787
882
|
}
|
|
788
883
|
/**
|
|
789
|
-
* Returns `true` for
|
|
790
|
-
* `.d.cts`
|
|
884
|
+
* Returns `true` for every declaration-file spelling TypeScript-Go accepts.
|
|
885
|
+
* Besides the standard `.d.ts`, `.d.mts`, and `.d.cts` forms, TypeScript-Go
|
|
886
|
+
* treats an arbitrary-extension source such as `styles.d.css.ts` as a
|
|
887
|
+
* declaration file too.
|
|
791
888
|
*/
|
|
792
889
|
function isDeclarationFile(id) {
|
|
793
|
-
|
|
890
|
+
// Module ids can cross process/platform boundaries (for example, a Windows
|
|
891
|
+
// id inspected by a POSIX host). TypeScript-Go normalizes both separators
|
|
892
|
+
// before taking the basename, so a `.d.` directory component must not turn
|
|
893
|
+
// an ordinary source into a declaration file.
|
|
894
|
+
const normalized = id.replaceAll("\\", "/");
|
|
895
|
+
const base = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
896
|
+
return (base.endsWith(".d.ts") ||
|
|
897
|
+
base.endsWith(".d.mts") ||
|
|
898
|
+
base.endsWith(".d.cts") ||
|
|
899
|
+
(base.endsWith(".ts") && base.includes(".d.")));
|
|
794
900
|
}
|
|
795
901
|
/**
|
|
796
902
|
* Returns `true` when the caller has explicitly opted out of all plugins. An
|
|
@@ -830,7 +936,11 @@ function createTransformResult(source, code) {
|
|
|
830
936
|
function matchesCachedSource(cached, file, source, buildScoped) {
|
|
831
937
|
const identities = envelopeDerivation(cached).identityContext;
|
|
832
938
|
const currentKey = toProjectKey(cached.projectRoot, file, identities);
|
|
833
|
-
|
|
939
|
+
const identity = pathIdentityKey(file, identities);
|
|
940
|
+
const expected = cached.sourceHashes?.[identity] ??
|
|
941
|
+
cached.inputHashes[currentKey] ??
|
|
942
|
+
cached.externalInputHashes?.[identity];
|
|
943
|
+
if (expected !== hashText(source)) {
|
|
834
944
|
return false;
|
|
835
945
|
}
|
|
836
946
|
if (buildScoped &&
|
|
@@ -886,6 +996,7 @@ function matchesNarrowPersistentInputs(cached, file) {
|
|
|
886
996
|
file,
|
|
887
997
|
projectRoot: cached.projectRoot,
|
|
888
998
|
result: cached.result,
|
|
999
|
+
scratchDirectory: cached.scratchDirectory,
|
|
889
1000
|
temporaryTsconfig: cached.temporaryTsconfig,
|
|
890
1001
|
});
|
|
891
1002
|
for (const input of inputs) {
|
|
@@ -1104,6 +1215,7 @@ function isMissingPathError(error) {
|
|
|
1104
1215
|
function captureUniversalHostInputValidation(cached, currentFile) {
|
|
1105
1216
|
const filesystem = resultFilesystem(cached.result);
|
|
1106
1217
|
const state = envelopeDerivation(cached);
|
|
1218
|
+
const failures = createGenerationProofFailures();
|
|
1107
1219
|
const validation = {
|
|
1108
1220
|
entries: new Map(),
|
|
1109
1221
|
covered: new Set(),
|
|
@@ -1113,6 +1225,7 @@ function captureUniversalHostInputValidation(cached, currentFile) {
|
|
|
1113
1225
|
filesystem,
|
|
1114
1226
|
projectRoot: cached.projectRoot,
|
|
1115
1227
|
result: cached.result,
|
|
1228
|
+
scratchDirectory: cached.scratchDirectory,
|
|
1116
1229
|
temporaryTsconfig: cached.temporaryTsconfig,
|
|
1117
1230
|
})) {
|
|
1118
1231
|
const generationHashes = cached.result.type === "exception"
|
|
@@ -1128,8 +1241,14 @@ function captureUniversalHostInputValidation(cached, currentFile) {
|
|
|
1128
1241
|
let readable = false;
|
|
1129
1242
|
if (expected === undefined) {
|
|
1130
1243
|
const current = path.resolve(currentFile);
|
|
1131
|
-
if (path.resolve(input) !== current)
|
|
1132
|
-
|
|
1244
|
+
if (path.resolve(input) !== current) {
|
|
1245
|
+
recordGenerationProofFailure(failures, {
|
|
1246
|
+
domain: "host",
|
|
1247
|
+
kind: "content-proof-missing",
|
|
1248
|
+
path: input,
|
|
1249
|
+
});
|
|
1250
|
+
return { failures };
|
|
1251
|
+
}
|
|
1133
1252
|
// The current module may be supplied from an unsaved editor buffer. Its
|
|
1134
1253
|
// generation snapshot is overlaid below from `currentSource`, so a disk
|
|
1135
1254
|
// fingerprint would be both unavailable and the wrong authority. The
|
|
@@ -1139,7 +1258,12 @@ function captureUniversalHostInputValidation(cached, currentFile) {
|
|
|
1139
1258
|
else {
|
|
1140
1259
|
const current = hostInputStateHash(input, filesystem);
|
|
1141
1260
|
if (expected !== current) {
|
|
1142
|
-
|
|
1261
|
+
recordGenerationProofFailure(failures, {
|
|
1262
|
+
domain: "host",
|
|
1263
|
+
kind: "content-changed",
|
|
1264
|
+
path: input,
|
|
1265
|
+
});
|
|
1266
|
+
return { failures };
|
|
1143
1267
|
}
|
|
1144
1268
|
// A path both sides agree they could not read carries no bytes for a
|
|
1145
1269
|
// signature to stand for. It still belongs in the manifest, so the
|
|
@@ -1150,16 +1274,35 @@ function captureUniversalHostInputValidation(cached, currentFile) {
|
|
|
1150
1274
|
if (generationRealpaths !== undefined) {
|
|
1151
1275
|
if (!Object.prototype.hasOwnProperty.call(generationRealpaths, absoluteInput) ||
|
|
1152
1276
|
!sameHostInputRealpath(generationRealpaths[absoluteInput], hostInputRealpath(input, filesystem), state.identityContext)) {
|
|
1153
|
-
|
|
1277
|
+
recordGenerationProofFailure(failures, {
|
|
1278
|
+
domain: "host",
|
|
1279
|
+
kind: Object.prototype.hasOwnProperty.call(generationRealpaths, absoluteInput)
|
|
1280
|
+
? "realpath-changed"
|
|
1281
|
+
: "realpath-proof-missing",
|
|
1282
|
+
path: input,
|
|
1283
|
+
});
|
|
1284
|
+
return { failures };
|
|
1154
1285
|
}
|
|
1155
1286
|
}
|
|
1156
1287
|
validation.covered.add(path.resolve(input));
|
|
1157
1288
|
const before = inputMetadataEvidence(input, filesystem);
|
|
1158
|
-
if (!matchesRecordedInput(cached, input))
|
|
1159
|
-
|
|
1289
|
+
if (!matchesRecordedInput(cached, input)) {
|
|
1290
|
+
recordGenerationProofFailure(failures, {
|
|
1291
|
+
domain: "host",
|
|
1292
|
+
kind: "snapshot-mismatch",
|
|
1293
|
+
path: input,
|
|
1294
|
+
});
|
|
1295
|
+
return { failures };
|
|
1296
|
+
}
|
|
1160
1297
|
const after = inputMetadataSignature(input, filesystem);
|
|
1161
|
-
if (before?.signature !== after)
|
|
1162
|
-
|
|
1298
|
+
if (before?.signature !== after) {
|
|
1299
|
+
recordGenerationProofFailure(failures, {
|
|
1300
|
+
domain: "host",
|
|
1301
|
+
kind: "changed-during-validation",
|
|
1302
|
+
path: input,
|
|
1303
|
+
});
|
|
1304
|
+
return { failures };
|
|
1305
|
+
}
|
|
1163
1306
|
if (before !== undefined) {
|
|
1164
1307
|
// Do not key this manifest by physical identity. A symlink/junction
|
|
1165
1308
|
// spelling and its selected target deliberately share that identity,
|
|
@@ -1179,8 +1322,14 @@ function captureUniversalHostInputValidation(cached, currentFile) {
|
|
|
1179
1322
|
const probe = missingPathProbe(input, filesystem);
|
|
1180
1323
|
if (probe.blocker !== undefined) {
|
|
1181
1324
|
const signature = inputMetadataSignature(probe.blocker, filesystem);
|
|
1182
|
-
if (signature === undefined)
|
|
1183
|
-
|
|
1325
|
+
if (signature === undefined) {
|
|
1326
|
+
recordGenerationProofFailure(failures, {
|
|
1327
|
+
domain: "host",
|
|
1328
|
+
kind: "blocker-metadata-unavailable",
|
|
1329
|
+
path: probe.blocker,
|
|
1330
|
+
});
|
|
1331
|
+
return { failures };
|
|
1332
|
+
}
|
|
1184
1333
|
// A blocker proves a kind and an identity, not content: it is the
|
|
1185
1334
|
// non-directory ancestor that makes everything below it unreachable, and
|
|
1186
1335
|
// it cannot stop being that without its metadata moving. So it keeps a
|
|
@@ -1207,7 +1356,7 @@ function captureUniversalHostInputValidation(cached, currentFile) {
|
|
|
1207
1356
|
names.add(normalizeHostInputName(probe.name, state.identityContext.caseSensitive(probe.directory)));
|
|
1208
1357
|
}
|
|
1209
1358
|
cached.hostInputValidation = validation;
|
|
1210
|
-
return validation;
|
|
1359
|
+
return { failures, validation };
|
|
1211
1360
|
}
|
|
1212
1361
|
/**
|
|
1213
1362
|
* The recorded state of an input the generation read nothing from: absent, or
|
|
@@ -1512,7 +1661,9 @@ function matchesCompleteInputSnapshot(cached, currentKey, source) {
|
|
|
1512
1661
|
if (!sameProjectDirectories(cached.projectDirectories, current.projectDirectories)) {
|
|
1513
1662
|
return false;
|
|
1514
1663
|
}
|
|
1515
|
-
|
|
1664
|
+
if (Object.prototype.hasOwnProperty.call(cached.inputHashes, currentKey)) {
|
|
1665
|
+
current.hashes[currentKey] = hashText(source);
|
|
1666
|
+
}
|
|
1516
1667
|
if (!sameHashes(cached.inputHashes, current.hashes, declaredInputs)) {
|
|
1517
1668
|
return false;
|
|
1518
1669
|
}
|
|
@@ -1580,18 +1731,30 @@ function matchesExternalInputRealpaths(cached) {
|
|
|
1580
1731
|
}
|
|
1581
1732
|
/**
|
|
1582
1733
|
* Capture external-input hashes without attaching post-compile state to an
|
|
1583
|
-
* earlier graph. Graph members
|
|
1584
|
-
* it now; plugin-declared dependency-only
|
|
1585
|
-
* post-compile snapshot because their own protocol
|
|
1586
|
-
* fingerprints.
|
|
1734
|
+
* earlier graph. Graph members and out-of-walk transformed sources must carry
|
|
1735
|
+
* compiler-time proof and still match it now; plugin-declared dependency-only
|
|
1736
|
+
* paths retain the historical post-compile snapshot because their own protocol
|
|
1737
|
+
* does not claim generation fingerprints.
|
|
1587
1738
|
*/
|
|
1588
1739
|
function captureExternalInputSnapshot(cached, paths) {
|
|
1589
1740
|
const state = envelopeDerivation(cached);
|
|
1590
1741
|
const filesystem = resultFilesystem(cached.result);
|
|
1591
1742
|
const graph = envelopeGraphIndexes(state, cached);
|
|
1743
|
+
// A non-declaration transform output is a compiler-realized source even when
|
|
1744
|
+
// a malformed or legacy graph omitted its node. Its output was computed from
|
|
1745
|
+
// compiler-time bytes, so a post-compile host read cannot prove coherence.
|
|
1746
|
+
const transformSources = new Set();
|
|
1747
|
+
if (cached.result.type === "success") {
|
|
1748
|
+
for (const output of Object.keys(cached.result.typescript)) {
|
|
1749
|
+
if (!isDeclarationFile(output)) {
|
|
1750
|
+
transformSources.add(derivationIdentity(state, path.resolve(cached.projectRoot, output)));
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1592
1754
|
const hashes = {};
|
|
1593
1755
|
const realpaths = {};
|
|
1594
1756
|
const signatures = {};
|
|
1757
|
+
const failures = createGenerationProofFailures();
|
|
1595
1758
|
let complete = true;
|
|
1596
1759
|
// Sandwich every read between two metadata signatures. Only a signature that
|
|
1597
1760
|
// survived its own read, and whose stamp's tick the filesystem's clock has
|
|
@@ -1609,21 +1772,47 @@ function captureExternalInputSnapshot(cached, paths) {
|
|
|
1609
1772
|
// through to the recorded-state branch below, the same evidence a
|
|
1610
1773
|
// plugin-declared dependency path carries. Its absence still invalidates
|
|
1611
1774
|
// the generation when it appears, because `missing` is recorded state.
|
|
1612
|
-
const
|
|
1775
|
+
const realizedTransformSource = transformSources.has(identity);
|
|
1776
|
+
const speculativeOnly = !realizedTransformSource &&
|
|
1777
|
+
graph.speculative.has(identity) &&
|
|
1613
1778
|
!graph.inputProofs.has(identity) &&
|
|
1614
1779
|
!graph.inputProofConflicts.has(identity);
|
|
1615
|
-
if (graph.members.has(identity) &&
|
|
1780
|
+
if ((realizedTransformSource || graph.members.has(identity)) &&
|
|
1781
|
+
!speculativeOnly) {
|
|
1616
1782
|
const proof = graph.inputProofs.get(identity);
|
|
1617
1783
|
if (proof === undefined || graph.inputProofConflicts.has(identity)) {
|
|
1618
1784
|
complete = false;
|
|
1785
|
+
recordGenerationProofFailure(failures, {
|
|
1786
|
+
domain: "external",
|
|
1787
|
+
kind: graph.inputProofConflicts.has(identity)
|
|
1788
|
+
? "graph-proof-conflict"
|
|
1789
|
+
: "graph-proof-missing",
|
|
1790
|
+
detail: graph.inputProofFailures.get(identity),
|
|
1791
|
+
path: input,
|
|
1792
|
+
});
|
|
1619
1793
|
continue;
|
|
1620
1794
|
}
|
|
1621
1795
|
const before = inputMetadataEvidence(input, filesystem);
|
|
1622
1796
|
const currentHash = graphInputStateHash(input, filesystem);
|
|
1797
|
+
const currentRealpath = hostInputRealpath(input, filesystem);
|
|
1623
1798
|
const after = inputMetadataSignature(input, filesystem);
|
|
1624
|
-
|
|
1625
|
-
|
|
1799
|
+
const realpathMatches = sameHostInputRealpath(proof.realpath, currentRealpath, state.identityContext);
|
|
1800
|
+
if (currentHash !== proof.hash || !realpathMatches) {
|
|
1626
1801
|
complete = false;
|
|
1802
|
+
if (currentHash !== proof.hash) {
|
|
1803
|
+
recordGenerationProofFailure(failures, {
|
|
1804
|
+
domain: "external",
|
|
1805
|
+
kind: "graph-content-changed",
|
|
1806
|
+
path: input,
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
if (!realpathMatches) {
|
|
1810
|
+
recordGenerationProofFailure(failures, {
|
|
1811
|
+
domain: "external",
|
|
1812
|
+
kind: "graph-realpath-changed",
|
|
1813
|
+
path: input,
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1627
1816
|
}
|
|
1628
1817
|
else if (currentHash !== null) {
|
|
1629
1818
|
// The recorded hash is the compiler's own proof, so a signature may
|
|
@@ -1643,27 +1832,30 @@ function captureExternalInputSnapshot(cached, paths) {
|
|
|
1643
1832
|
if (hash !== null)
|
|
1644
1833
|
record(input, before, after);
|
|
1645
1834
|
}
|
|
1646
|
-
return { complete, hashes, realpaths, signatures };
|
|
1835
|
+
return { complete, failures, hashes, realpaths, signatures };
|
|
1647
1836
|
}
|
|
1648
|
-
/**
|
|
1649
|
-
function
|
|
1837
|
+
/** Explain every graph member that no longer matches the compiler's state. */
|
|
1838
|
+
function compilerGraphInputProofFailures(cached) {
|
|
1839
|
+
const failures = createGenerationProofFailures();
|
|
1650
1840
|
if (cached.result.type === "exception" ||
|
|
1651
1841
|
cached.result.graph === undefined ||
|
|
1652
1842
|
(cached.result.graph.inputHashes === undefined &&
|
|
1653
|
-
cached.result.graph.inputRealpaths === undefined
|
|
1843
|
+
cached.result.graph.inputRealpaths === undefined &&
|
|
1844
|
+
cached.result.graph.inputProofFailures === undefined)) {
|
|
1654
1845
|
// Legacy sidecars remain compatible for ordinary in-project graphs. Their
|
|
1655
1846
|
// out-of-walk members are still rejected by captureExternalInputSnapshot,
|
|
1656
1847
|
// where a post-compile snapshot cannot prove the compiler's generation.
|
|
1657
|
-
return
|
|
1848
|
+
return failures;
|
|
1658
1849
|
}
|
|
1659
1850
|
const state = envelopeDerivation(cached);
|
|
1660
1851
|
const filesystem = resultFilesystem(cached.result);
|
|
1661
1852
|
const graph = envelopeGraphIndexes(state, cached);
|
|
1662
|
-
if (graph.inputProofConflicts.size !== 0) {
|
|
1663
|
-
return false;
|
|
1664
|
-
}
|
|
1665
1853
|
for (const identity of graph.members) {
|
|
1666
1854
|
const proof = graph.inputProofs.get(identity);
|
|
1855
|
+
const spelling = proof?.path ?? graph.spellings.get(identity) ?? cached.projectRoot;
|
|
1856
|
+
if (isTransformScratchInput(spelling, cached.scratchDirectory)) {
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1667
1859
|
// A speculative candidate has no compile-time read to prove. Requiring one
|
|
1668
1860
|
// would void every generation of every project whose resolution passes over
|
|
1669
1861
|
// a higher-priority spelling, which is every project with a dependency
|
|
@@ -1672,13 +1864,41 @@ function matchesCompilerGraphInputProofs(cached) {
|
|
|
1672
1864
|
if (proof === undefined && graph.speculative.has(identity)) {
|
|
1673
1865
|
continue;
|
|
1674
1866
|
}
|
|
1675
|
-
if (
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1867
|
+
if (graph.inputProofConflicts.has(identity)) {
|
|
1868
|
+
recordGenerationProofFailure(failures, {
|
|
1869
|
+
domain: "graph",
|
|
1870
|
+
kind: "proof-conflict",
|
|
1871
|
+
detail: graph.inputProofFailures.get(identity),
|
|
1872
|
+
path: spelling,
|
|
1873
|
+
});
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
if (proof === undefined) {
|
|
1877
|
+
recordGenerationProofFailure(failures, {
|
|
1878
|
+
domain: "graph",
|
|
1879
|
+
kind: "proof-missing",
|
|
1880
|
+
detail: graph.inputProofFailures.get(identity),
|
|
1881
|
+
path: spelling,
|
|
1882
|
+
});
|
|
1883
|
+
continue;
|
|
1884
|
+
}
|
|
1885
|
+
const currentHash = graphInputStateHash(proof.path, filesystem);
|
|
1886
|
+
if (currentHash !== proof.hash) {
|
|
1887
|
+
recordGenerationProofFailure(failures, {
|
|
1888
|
+
domain: "graph",
|
|
1889
|
+
kind: "content-changed",
|
|
1890
|
+
path: proof.path,
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
if (!sameHostInputRealpath(proof.realpath, hostInputRealpath(proof.path, filesystem), state.identityContext)) {
|
|
1894
|
+
recordGenerationProofFailure(failures, {
|
|
1895
|
+
domain: "graph",
|
|
1896
|
+
kind: "realpath-changed",
|
|
1897
|
+
path: proof.path,
|
|
1898
|
+
});
|
|
1679
1899
|
}
|
|
1680
1900
|
}
|
|
1681
|
-
return
|
|
1901
|
+
return failures;
|
|
1682
1902
|
}
|
|
1683
1903
|
/** Compare one derived input with the snapshot that owned it at generation. */
|
|
1684
1904
|
function matchesRecordedInput(cached, input) {
|
|
@@ -1737,6 +1957,7 @@ function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAU
|
|
|
1737
1957
|
const unstableFiles = new Set();
|
|
1738
1958
|
let attributed = true;
|
|
1739
1959
|
const walked = walkProjectInputs(projectRoot, filesystem);
|
|
1960
|
+
const walkFailures = [...walked.failures];
|
|
1740
1961
|
let complete = walked.complete;
|
|
1741
1962
|
for (const file of walked.files) {
|
|
1742
1963
|
try {
|
|
@@ -1764,6 +1985,7 @@ function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAU
|
|
|
1764
1985
|
before.signature !== after) {
|
|
1765
1986
|
complete = false;
|
|
1766
1987
|
unstableFiles.add(key);
|
|
1988
|
+
walkFailures.push({ kind: "file-changed-during-read", path: file });
|
|
1767
1989
|
}
|
|
1768
1990
|
else {
|
|
1769
1991
|
fileSignatures[key] = after;
|
|
@@ -1780,6 +2002,7 @@ function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAU
|
|
|
1780
2002
|
// File watchers may observe a transform while another process is moving
|
|
1781
2003
|
// or deleting files. The missing key invalidates older cache entries.
|
|
1782
2004
|
complete = false;
|
|
2005
|
+
walkFailures.push({ kind: "file-read-failed", path: file });
|
|
1783
2006
|
try {
|
|
1784
2007
|
unstableFiles.add(toProjectKey(projectRoot, file, identities));
|
|
1785
2008
|
}
|
|
@@ -1798,6 +2021,7 @@ function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAU
|
|
|
1798
2021
|
projectDirectories: walked.directories,
|
|
1799
2022
|
provenSignatures,
|
|
1800
2023
|
unstableFiles,
|
|
2024
|
+
walkFailures,
|
|
1801
2025
|
};
|
|
1802
2026
|
}
|
|
1803
2027
|
/**
|
|
@@ -1811,6 +2035,7 @@ function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAU
|
|
|
1811
2035
|
function walkProjectInputs(root, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
1812
2036
|
let complete = true;
|
|
1813
2037
|
const directories = [];
|
|
2038
|
+
const failures = [];
|
|
1814
2039
|
const files = [];
|
|
1815
2040
|
const stack = [root];
|
|
1816
2041
|
while (stack.length !== 0) {
|
|
@@ -1818,6 +2043,10 @@ function walkProjectInputs(root, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
|
1818
2043
|
const before = projectDirectorySignature(current, filesystem);
|
|
1819
2044
|
if (before === undefined) {
|
|
1820
2045
|
complete = false;
|
|
2046
|
+
failures.push({
|
|
2047
|
+
kind: "directory-metadata-unavailable",
|
|
2048
|
+
path: current,
|
|
2049
|
+
});
|
|
1821
2050
|
continue;
|
|
1822
2051
|
}
|
|
1823
2052
|
let entries;
|
|
@@ -1826,11 +2055,18 @@ function walkProjectInputs(root, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
|
1826
2055
|
}
|
|
1827
2056
|
catch {
|
|
1828
2057
|
complete = false;
|
|
2058
|
+
failures.push({ kind: "directory-read-failed", path: current });
|
|
1829
2059
|
continue;
|
|
1830
2060
|
}
|
|
1831
2061
|
const after = projectDirectorySignature(current, filesystem);
|
|
1832
2062
|
if (after === undefined || before !== after) {
|
|
1833
2063
|
complete = false;
|
|
2064
|
+
failures.push({
|
|
2065
|
+
kind: after === undefined
|
|
2066
|
+
? "directory-metadata-unavailable"
|
|
2067
|
+
: "directory-changed-during-walk",
|
|
2068
|
+
path: current,
|
|
2069
|
+
});
|
|
1834
2070
|
}
|
|
1835
2071
|
directories.push({
|
|
1836
2072
|
path: current,
|
|
@@ -1856,7 +2092,7 @@ function walkProjectInputs(root, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
|
1856
2092
|
}
|
|
1857
2093
|
directories.sort((left, right) => left.path.localeCompare(right.path));
|
|
1858
2094
|
files.sort();
|
|
1859
|
-
return { complete, directories, files };
|
|
2095
|
+
return { complete, directories, failures, files };
|
|
1860
2096
|
}
|
|
1861
2097
|
/** Return a cheap identity for one directory's immediate membership. */
|
|
1862
2098
|
function projectDirectorySignature(directory, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
@@ -1904,6 +2140,8 @@ function openDirectoryWatch(filesystem, directory, listener, onError) {
|
|
|
1904
2140
|
/** Watch every walked directory for membership changes after generation. */
|
|
1905
2141
|
async function createProjectMutationTracker(directories, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
1906
2142
|
const tracker = {
|
|
2143
|
+
changes: new Set(),
|
|
2144
|
+
changesOmitted: false,
|
|
1907
2145
|
close: () => undefined,
|
|
1908
2146
|
failed: false,
|
|
1909
2147
|
membershipChanged: false,
|
|
@@ -1920,9 +2158,12 @@ async function createProjectMutationTracker(directories, filesystem = DEFAULT_FI
|
|
|
1920
2158
|
};
|
|
1921
2159
|
for (const directory of directories) {
|
|
1922
2160
|
try {
|
|
1923
|
-
watchers.push(openDirectoryWatch(filesystem, directory.path, (eventType) => {
|
|
1924
|
-
if (eventType === "rename")
|
|
1925
|
-
tracker
|
|
2161
|
+
watchers.push(openDirectoryWatch(filesystem, directory.path, (eventType, filename) => {
|
|
2162
|
+
if (eventType === "rename") {
|
|
2163
|
+
recordProjectMutation(tracker, filename === null
|
|
2164
|
+
? directory.path
|
|
2165
|
+
: path.join(directory.path, filename));
|
|
2166
|
+
}
|
|
1926
2167
|
}, () => {
|
|
1927
2168
|
tracker.failed = true;
|
|
1928
2169
|
}));
|
|
@@ -1958,6 +2199,8 @@ async function createHostInputMutationTracker(inputs, filesystem, covered, event
|
|
|
1958
2199
|
names: [...location.names],
|
|
1959
2200
|
}));
|
|
1960
2201
|
const tracker = {
|
|
2202
|
+
changes: new Set(),
|
|
2203
|
+
changesOmitted: false,
|
|
1961
2204
|
close: () => undefined,
|
|
1962
2205
|
// Coverage is the caller's claim, and it is required rather than derived
|
|
1963
2206
|
// from the input list: an input is watched by its exact name here, but only
|
|
@@ -1991,7 +2234,9 @@ async function createHostInputMutationTracker(inputs, filesystem, covered, event
|
|
|
1991
2234
|
? null
|
|
1992
2235
|
: normalizeHostInputName(filename, caseSensitive);
|
|
1993
2236
|
if (reported === null || names.has(reported)) {
|
|
1994
|
-
tracker
|
|
2237
|
+
recordProjectMutation(tracker, filename === null
|
|
2238
|
+
? location.directory
|
|
2239
|
+
: path.join(location.directory, filename));
|
|
1995
2240
|
}
|
|
1996
2241
|
}, () => {
|
|
1997
2242
|
tracker.failed = true;
|
|
@@ -2003,6 +2248,18 @@ async function createHostInputMutationTracker(inputs, filesystem, covered, event
|
|
|
2003
2248
|
}
|
|
2004
2249
|
return tracker;
|
|
2005
2250
|
}
|
|
2251
|
+
/** Record enough exact mutation evidence without retaining an event stream. */
|
|
2252
|
+
function recordProjectMutation(tracker, changed) {
|
|
2253
|
+
tracker.membershipChanged = true;
|
|
2254
|
+
if (tracker.changes.has(changed))
|
|
2255
|
+
return;
|
|
2256
|
+
if (tracker.changes.size < MAX_GENERATION_MUTATION_PATHS) {
|
|
2257
|
+
tracker.changes.add(changed);
|
|
2258
|
+
}
|
|
2259
|
+
else {
|
|
2260
|
+
tracker.changesOmitted = true;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2006
2263
|
let windowsProjectMutationBroker;
|
|
2007
2264
|
/**
|
|
2008
2265
|
* Register directory watches in an isolated Windows process.
|
|
@@ -2128,7 +2385,14 @@ function getWindowsProjectMutationBroker() {
|
|
|
2128
2385
|
if (record.ready === true)
|
|
2129
2386
|
registration.ready();
|
|
2130
2387
|
if (record.ready !== true && record.failed !== true) {
|
|
2131
|
-
|
|
2388
|
+
if (typeof record.directory === "string") {
|
|
2389
|
+
recordProjectMutation(registration.tracker, typeof record.filename === "string"
|
|
2390
|
+
? path.join(record.directory, record.filename)
|
|
2391
|
+
: record.directory);
|
|
2392
|
+
}
|
|
2393
|
+
else {
|
|
2394
|
+
registration.tracker.membershipChanged = true;
|
|
2395
|
+
}
|
|
2132
2396
|
}
|
|
2133
2397
|
});
|
|
2134
2398
|
windowsProjectMutationBroker = broker;
|
|
@@ -2212,7 +2476,7 @@ const WINDOWS_WATCH_BROKER_SOURCE = [
|
|
|
2212
2476
|
" const names = location.names === undefined ? undefined : new Set(location.names.map((name) => name.toLowerCase()));",
|
|
2213
2477
|
" const watcher = fs.watch(location.directory, { persistent: false }, (event, filename) => {",
|
|
2214
2478
|
" const matches = names === undefined || filename === null || names.has(String(filename).toLowerCase());",
|
|
2215
|
-
' if (matches && (message.allEvents || event === "rename")) process.send?.({ id: message.id });',
|
|
2479
|
+
' if (matches && (message.allEvents || event === "rename")) process.send?.({ directory: location.directory, filename: filename === null ? null : String(filename), id: message.id });',
|
|
2216
2480
|
" });",
|
|
2217
2481
|
' watcher.on("error", () => process.send?.({ failed: true, id: message.id }));',
|
|
2218
2482
|
" watchers.push(watcher);",
|
|
@@ -2419,12 +2683,13 @@ function matchesCachedExternalInputs(cached) {
|
|
|
2419
2683
|
}
|
|
2420
2684
|
/**
|
|
2421
2685
|
* Derive the absolute out-of-walk input set of a whole project transform: the
|
|
2422
|
-
* union of every reference-graph member (edge keys and
|
|
2423
|
-
* config chain) and
|
|
2424
|
-
* project walk already hashes and the disposed
|
|
2425
|
-
* inputs {@link matchesCachedSource}'s walk cannot see.
|
|
2426
|
-
* that are still missing remain in this set even under
|
|
2427
|
-
* first walk cannot hash a file that has not been created
|
|
2686
|
+
* union of every transformed source key, reference-graph member (edge keys and
|
|
2687
|
+
* targets, globals, the config chain), and plugin-reported dependency, minus
|
|
2688
|
+
* everything the project walk already hashes and the disposed transform scratch
|
|
2689
|
+
* tree. These are the inputs {@link matchesCachedSource}'s walk cannot see.
|
|
2690
|
+
* Resolution candidates that are still missing remain in this set even under
|
|
2691
|
+
* the project root: the first walk cannot hash a file that has not been created
|
|
2692
|
+
* yet.
|
|
2428
2693
|
*
|
|
2429
2694
|
* A `dependenciesComplete` declaration deliberately does not narrow the stored
|
|
2430
2695
|
* set: other files in the same whole-project result can still own the omitted
|
|
@@ -2441,6 +2706,10 @@ function selectExternalInputPaths(props) {
|
|
|
2441
2706
|
const identities = createHostPathIdentityContext(filesystem);
|
|
2442
2707
|
const resolutionCandidates = new Set();
|
|
2443
2708
|
const graph = props.result.graph;
|
|
2709
|
+
// Every transform output key names the source file whose transformed text it
|
|
2710
|
+
// carries. Keep an out-of-walk source in the external snapshot instead of
|
|
2711
|
+
// injecting it into the project-walk key universe (samchon/ttsc#252).
|
|
2712
|
+
members.push(...Object.keys(props.result.typescript));
|
|
2444
2713
|
if (graph !== undefined) {
|
|
2445
2714
|
for (const [source, targets] of Object.entries(graph.edges ?? {})) {
|
|
2446
2715
|
members.push(source);
|
|
@@ -2497,6 +2766,7 @@ function selectExternalInputPaths(props) {
|
|
|
2497
2766
|
const identity = pathIdentityKey(absolute, identities);
|
|
2498
2767
|
const missingCandidate = resolutionCandidates.has(identity) && !filesystem.exists(absolute);
|
|
2499
2768
|
if (identity === excluded ||
|
|
2769
|
+
isTransformScratchInput(absolute, props.scratchDirectory) ||
|
|
2500
2770
|
seen.has(spelling) ||
|
|
2501
2771
|
(!missingCandidate &&
|
|
2502
2772
|
isProjectWalkPath(props.projectRoot, absolute, identities, filesystem))) {
|
|
@@ -2560,6 +2830,7 @@ function selectNotifiableAbsentInputs(props) {
|
|
|
2560
2830
|
const absolute = path.resolve(props.projectRoot, candidate);
|
|
2561
2831
|
const spelling = path.resolve(absolute);
|
|
2562
2832
|
if (seen.has(spelling) ||
|
|
2833
|
+
isTransformScratchInput(absolute, props.scratchDirectory) ||
|
|
2563
2834
|
(excluded !== undefined &&
|
|
2564
2835
|
pathIdentityKey(absolute, identities) === excluded) ||
|
|
2565
2836
|
props.filesystem.exists(absolute)) {
|
|
@@ -2732,6 +3003,103 @@ function walkSnapshotComplete(snapshot, declared) {
|
|
|
2732
3003
|
}
|
|
2733
3004
|
return true;
|
|
2734
3005
|
}
|
|
3006
|
+
/** Preserve exact project-walk and mutation witnesses for one failed attempt. */
|
|
3007
|
+
function recordProjectSnapshotFailures(failures, props) {
|
|
3008
|
+
const recordWalk = (snapshot) => {
|
|
3009
|
+
for (const failure of snapshot.walkFailures) {
|
|
3010
|
+
if (failure.kind.startsWith("file-") && props.declared !== undefined) {
|
|
3011
|
+
try {
|
|
3012
|
+
const key = toProjectKey(props.projectRoot, failure.path, props.identities);
|
|
3013
|
+
if (!props.declared.has(key))
|
|
3014
|
+
continue;
|
|
3015
|
+
}
|
|
3016
|
+
catch {
|
|
3017
|
+
// An unidentifiable failed input taints the complete project walk.
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
recordGenerationProofFailure(failures, {
|
|
3021
|
+
domain: "project",
|
|
3022
|
+
kind: failure.kind,
|
|
3023
|
+
path: failure.path,
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
};
|
|
3027
|
+
recordWalk(props.before);
|
|
3028
|
+
recordWalk(props.snapshot);
|
|
3029
|
+
const keys = props.declared ??
|
|
3030
|
+
new Set([
|
|
3031
|
+
...Object.keys(props.before.hashes),
|
|
3032
|
+
...Object.keys(props.snapshot.hashes),
|
|
3033
|
+
]);
|
|
3034
|
+
for (const key of keys) {
|
|
3035
|
+
if (props.before.hashes[key] !== props.snapshot.hashes[key]) {
|
|
3036
|
+
recordGenerationProofFailure(failures, {
|
|
3037
|
+
domain: "project",
|
|
3038
|
+
kind: "input-content-changed",
|
|
3039
|
+
path: path.resolve(props.projectRoot, key),
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
if (props.before.fileSignatures[key] !== props.snapshot.fileSignatures[key]) {
|
|
3043
|
+
recordGenerationProofFailure(failures, {
|
|
3044
|
+
domain: "project",
|
|
3045
|
+
kind: "input-metadata-changed",
|
|
3046
|
+
path: path.resolve(props.projectRoot, key),
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
const leftDirectories = new Map(props.before.projectDirectories.map((entry) => [
|
|
3051
|
+
entry.path,
|
|
3052
|
+
entry.signature,
|
|
3053
|
+
]));
|
|
3054
|
+
const rightDirectories = new Map(props.snapshot.projectDirectories.map((entry) => [
|
|
3055
|
+
entry.path,
|
|
3056
|
+
entry.signature,
|
|
3057
|
+
]));
|
|
3058
|
+
for (const directory of new Set([
|
|
3059
|
+
...leftDirectories.keys(),
|
|
3060
|
+
...rightDirectories.keys(),
|
|
3061
|
+
])) {
|
|
3062
|
+
if (leftDirectories.get(directory) !== rightDirectories.get(directory)) {
|
|
3063
|
+
recordGenerationProofFailure(failures, {
|
|
3064
|
+
domain: "project",
|
|
3065
|
+
kind: "directory-membership-changed",
|
|
3066
|
+
path: directory,
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
const recordTracker = (tracker, kind) => {
|
|
3071
|
+
if (tracker?.membershipChanged !== true)
|
|
3072
|
+
return;
|
|
3073
|
+
if (tracker.changes.size === 0) {
|
|
3074
|
+
recordGenerationProofFailure(failures, {
|
|
3075
|
+
domain: "project",
|
|
3076
|
+
kind,
|
|
3077
|
+
path: props.projectRoot,
|
|
3078
|
+
});
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3081
|
+
for (const changed of tracker.changes) {
|
|
3082
|
+
recordGenerationProofFailure(failures, {
|
|
3083
|
+
domain: "project",
|
|
3084
|
+
kind,
|
|
3085
|
+
path: changed,
|
|
3086
|
+
});
|
|
3087
|
+
}
|
|
3088
|
+
if (tracker.changesOmitted) {
|
|
3089
|
+
failures.omitted = Math.min(Number.MAX_SAFE_INTEGER, failures.omitted + 1);
|
|
3090
|
+
}
|
|
3091
|
+
};
|
|
3092
|
+
recordTracker(props.tracker, "project-membership-event");
|
|
3093
|
+
recordTracker(props.hostInputTracker, "host-input-event");
|
|
3094
|
+
recordTracker(props.candidateTracker, "candidate-event");
|
|
3095
|
+
if (failures.entries.length === 0) {
|
|
3096
|
+
recordGenerationProofFailure(failures, {
|
|
3097
|
+
domain: "project",
|
|
3098
|
+
kind: "snapshot-incomplete",
|
|
3099
|
+
path: props.projectRoot,
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
2735
3103
|
/** {@link selectDeclaredProjectInputKeys} memoized per envelope generation. */
|
|
2736
3104
|
function declaredProjectInputKeys(state, cached) {
|
|
2737
3105
|
if (state.declaredInputKeysBuilt !== true) {
|
|
@@ -2739,6 +3107,7 @@ function declaredProjectInputKeys(state, cached) {
|
|
|
2739
3107
|
identities: state.identityContext,
|
|
2740
3108
|
projectRoot: cached.projectRoot,
|
|
2741
3109
|
result: cached.result,
|
|
3110
|
+
scratchDirectory: cached.scratchDirectory,
|
|
2742
3111
|
});
|
|
2743
3112
|
state.declaredInputKeysBuilt = true;
|
|
2744
3113
|
}
|
|
@@ -2759,7 +3128,10 @@ function selectDeclaredProjectInputKeys(props) {
|
|
|
2759
3128
|
const add = (entry) => {
|
|
2760
3129
|
if (typeof entry !== "string" || entry.length === 0)
|
|
2761
3130
|
return;
|
|
2762
|
-
|
|
3131
|
+
const absolute = path.resolve(props.projectRoot, entry);
|
|
3132
|
+
if (isTransformScratchInput(absolute, props.scratchDirectory))
|
|
3133
|
+
return;
|
|
3134
|
+
keys.add(toProjectKey(props.projectRoot, absolute, props.identities));
|
|
2763
3135
|
};
|
|
2764
3136
|
for (const [source, targets] of Object.entries(graph.edges ?? {})) {
|
|
2765
3137
|
add(source);
|
|
@@ -2791,46 +3163,231 @@ function selectDeclaredProjectInputKeys(props) {
|
|
|
2791
3163
|
}
|
|
2792
3164
|
return keys;
|
|
2793
3165
|
}
|
|
3166
|
+
/** Create an empty bounded witness collection for one transform attempt. */
|
|
3167
|
+
function createGenerationProofFailures() {
|
|
3168
|
+
return { entries: [], omitted: 0, seen: new Set() };
|
|
3169
|
+
}
|
|
3170
|
+
/** Retain one unique proof witness without allowing diagnostics to grow freely. */
|
|
3171
|
+
function recordGenerationProofFailure(failures, failure) {
|
|
3172
|
+
const key = JSON.stringify([
|
|
3173
|
+
failure.domain,
|
|
3174
|
+
failure.kind,
|
|
3175
|
+
failure.path,
|
|
3176
|
+
failure.detail,
|
|
3177
|
+
]);
|
|
3178
|
+
if (failures.seen.has(key))
|
|
3179
|
+
return;
|
|
3180
|
+
if (failures.entries.length < MAX_GENERATION_PROOF_FAILURES) {
|
|
3181
|
+
// `seen` follows the same bound as `entries`: retaining every discarded
|
|
3182
|
+
// identity would make a bounded diagnostic an unbounded memory sink.
|
|
3183
|
+
failures.seen.add(key);
|
|
3184
|
+
failures.entries.push(failure);
|
|
3185
|
+
}
|
|
3186
|
+
else {
|
|
3187
|
+
failures.omitted = Math.min(Number.MAX_SAFE_INTEGER, failures.omitted + 1);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
/** Fold one bounded witness collection into another. */
|
|
3191
|
+
function mergeGenerationProofFailures(target, source) {
|
|
3192
|
+
for (const failure of source.entries) {
|
|
3193
|
+
recordGenerationProofFailure(target, failure);
|
|
3194
|
+
}
|
|
3195
|
+
target.omitted = Math.min(Number.MAX_SAFE_INTEGER, target.omitted + source.omitted);
|
|
3196
|
+
}
|
|
3197
|
+
/** Hash the declared-input-relevant failure shape without retaining it. */
|
|
3198
|
+
function projectWalkFailureFingerprint(snapshot, declared, projectRoot, identities) {
|
|
3199
|
+
const relevantUnstableFiles = declared === undefined
|
|
3200
|
+
? [...snapshot.unstableFiles]
|
|
3201
|
+
: [...snapshot.unstableFiles].filter((key) => declared.has(key));
|
|
3202
|
+
const relevantFailures = snapshot.walkFailures.filter((failure) => {
|
|
3203
|
+
if (!failure.kind.startsWith("file-"))
|
|
3204
|
+
return true;
|
|
3205
|
+
if (declared === undefined)
|
|
3206
|
+
return true;
|
|
3207
|
+
try {
|
|
3208
|
+
return declared.has(toProjectKey(projectRoot, failure.path, identities));
|
|
3209
|
+
}
|
|
3210
|
+
catch {
|
|
3211
|
+
return true;
|
|
3212
|
+
}
|
|
3213
|
+
});
|
|
3214
|
+
return hashText(JSON.stringify({
|
|
3215
|
+
complete: walkSnapshotComplete(snapshot, declared),
|
|
3216
|
+
directoryComplete: snapshot.directoryComplete,
|
|
3217
|
+
failures: relevantFailures
|
|
3218
|
+
.map((failure) => `${failure.kind}\0${path.resolve(failure.path)}`)
|
|
3219
|
+
.sort(),
|
|
3220
|
+
unstableFiles: relevantUnstableFiles.sort(),
|
|
3221
|
+
}));
|
|
3222
|
+
}
|
|
3223
|
+
/** Compact state of one exact out-of-walk input in a failed generation. */
|
|
3224
|
+
function failedGenerationInputState(input, filesystem) {
|
|
3225
|
+
let directory = "not-directory";
|
|
3226
|
+
try {
|
|
3227
|
+
if (filesystem.stat(input).isDirectory()) {
|
|
3228
|
+
directory = hashText(filesystem
|
|
3229
|
+
.readdir(input)
|
|
3230
|
+
.map((entry) => [
|
|
3231
|
+
entry.name,
|
|
3232
|
+
entry.isDirectory(),
|
|
3233
|
+
entry.isFile(),
|
|
3234
|
+
entry.isSymbolicLink(),
|
|
3235
|
+
].join(":"))
|
|
3236
|
+
.sort()
|
|
3237
|
+
.join("\0"));
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
catch {
|
|
3241
|
+
directory = "unavailable";
|
|
3242
|
+
}
|
|
3243
|
+
return hashText(JSON.stringify([
|
|
3244
|
+
inputMetadataSignature(input, filesystem) ?? "missing",
|
|
3245
|
+
hostInputStateHash(input, filesystem) ?? MISSING_INPUT_STATE,
|
|
3246
|
+
hostInputRealpath(input, filesystem),
|
|
3247
|
+
directory,
|
|
3248
|
+
]));
|
|
3249
|
+
}
|
|
3250
|
+
/** Snapshot every input outside the project walk that could change a retry. */
|
|
3251
|
+
function captureFailedGenerationInputStates(cached, failures) {
|
|
3252
|
+
const filesystem = resultFilesystem(cached.result);
|
|
3253
|
+
const inputs = new Set((cached.externalInputPaths ?? []).map((input) => path.resolve(input)));
|
|
3254
|
+
for (const input of selectPersistentHostInputs({
|
|
3255
|
+
filesystem,
|
|
3256
|
+
projectRoot: cached.projectRoot,
|
|
3257
|
+
result: cached.result,
|
|
3258
|
+
scratchDirectory: cached.scratchDirectory,
|
|
3259
|
+
temporaryTsconfig: cached.temporaryTsconfig,
|
|
3260
|
+
})) {
|
|
3261
|
+
inputs.add(path.resolve(input));
|
|
3262
|
+
}
|
|
3263
|
+
for (const failure of failures.entries) {
|
|
3264
|
+
if (failure.path !== undefined)
|
|
3265
|
+
inputs.add(path.resolve(failure.path));
|
|
3266
|
+
}
|
|
3267
|
+
return new Map([...inputs]
|
|
3268
|
+
.sort()
|
|
3269
|
+
.map((input) => [input, failedGenerationInputState(input, filesystem)]));
|
|
3270
|
+
}
|
|
3271
|
+
/** Capture source baselines for project and out-of-walk transform outputs. */
|
|
3272
|
+
function captureTransformSourceHashes(cached, currentFile, currentSourceHash) {
|
|
3273
|
+
const filesystem = resultFilesystem(cached.result);
|
|
3274
|
+
const identities = envelopeDerivation(cached).identityContext;
|
|
3275
|
+
const hashes = {};
|
|
3276
|
+
if (cached.result.type === "success") {
|
|
3277
|
+
for (const output of Object.keys(cached.result.typescript)) {
|
|
3278
|
+
const file = path.resolve(cached.projectRoot, output);
|
|
3279
|
+
const hash = hostInputStateHash(file, filesystem);
|
|
3280
|
+
if (hash !== null)
|
|
3281
|
+
hashes[pathIdentityKey(file, identities)] = hash;
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
hashes[pathIdentityKey(currentFile, identities)] = currentSourceHash;
|
|
3285
|
+
return hashes;
|
|
3286
|
+
}
|
|
2794
3287
|
/**
|
|
2795
|
-
*
|
|
2796
|
-
* the condition once instead of once per module.
|
|
2797
|
-
*/
|
|
2798
|
-
const REPORTED_UNREUSABLE_GENERATIONS = new Set();
|
|
2799
|
-
/**
|
|
2800
|
-
* Report, once per project root, that a generation cannot be reused.
|
|
3288
|
+
* Whether a terminal proof failure's observed environment actually changed.
|
|
2801
3289
|
*
|
|
2802
|
-
*
|
|
2803
|
-
*
|
|
2804
|
-
*
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
3290
|
+
* This is deliberately a confirmation test: inability to re-probe retains the
|
|
3291
|
+
* old verdict instead of turning every module request into another compile.
|
|
3292
|
+
* Cache lifecycle reset remains the unconditional recovery boundary.
|
|
3293
|
+
*/
|
|
3294
|
+
function failedGenerationEnvironmentChanged(validation, props) {
|
|
3295
|
+
try {
|
|
3296
|
+
const identities = envelopeDerivation(validation.cached).identityContext;
|
|
3297
|
+
const currentSourceHash = hashText(props.currentSource);
|
|
3298
|
+
const expectedSourceHash = validation.cached.sourceHashes?.[pathIdentityKey(props.currentFile, identities)];
|
|
3299
|
+
if (expectedSourceHash !== undefined &&
|
|
3300
|
+
expectedSourceHash !== currentSourceHash) {
|
|
3301
|
+
return true;
|
|
3302
|
+
}
|
|
3303
|
+
const current = collectProjectInputSnapshot(validation.cached.projectRoot, identities, props.filesystem);
|
|
3304
|
+
if (validation.projectWalkComplete !==
|
|
3305
|
+
walkSnapshotComplete(current, validation.declaredInputs) ||
|
|
3306
|
+
validation.projectWalkFailures !==
|
|
3307
|
+
projectWalkFailureFingerprint(current, validation.declaredInputs, validation.cached.projectRoot, identities) ||
|
|
3308
|
+
!sameHashes(validation.projectInputHashes, current.hashes, validation.declaredInputs) ||
|
|
3309
|
+
!sameProjectDirectories(validation.cached.projectDirectories ?? [], current.projectDirectories)) {
|
|
3310
|
+
return true;
|
|
3311
|
+
}
|
|
3312
|
+
for (const [input, recorded] of validation.inputStates) {
|
|
3313
|
+
if (failedGenerationInputState(input, props.filesystem) !== recorded) {
|
|
3314
|
+
return true;
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
return false;
|
|
3318
|
+
}
|
|
3319
|
+
catch {
|
|
3320
|
+
return false;
|
|
2821
3321
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
3322
|
+
}
|
|
3323
|
+
/** Render one input without leaking source content or control characters. */
|
|
3324
|
+
function formatGenerationFailurePath(projectRoot, input) {
|
|
3325
|
+
const absolute = path.resolve(input);
|
|
3326
|
+
const relative = path.relative(projectRoot, absolute);
|
|
3327
|
+
const display = relative === ""
|
|
3328
|
+
? "."
|
|
3329
|
+
: relative !== ".." &&
|
|
3330
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
3331
|
+
!path.isAbsolute(relative)
|
|
3332
|
+
? relative
|
|
3333
|
+
: absolute;
|
|
3334
|
+
return JSON.stringify(display.split(path.sep).join("/"));
|
|
3335
|
+
}
|
|
3336
|
+
/** Build the terminal error shared by every waiter of an unstable generation. */
|
|
3337
|
+
function createUnstableGenerationError(projectRoot, attempts, validation) {
|
|
3338
|
+
const lines = [
|
|
3339
|
+
`ttsc: could not capture a reusable transform generation after ${attempts.length} attempts.`,
|
|
3340
|
+
` project: ${projectRoot}`,
|
|
3341
|
+
];
|
|
3342
|
+
attempts.forEach((failures, index) => {
|
|
3343
|
+
lines.push(` attempt ${index + 1}:`);
|
|
3344
|
+
if (failures.entries.length === 0) {
|
|
3345
|
+
lines.push(" - project/generation-proof-incomplete");
|
|
3346
|
+
}
|
|
3347
|
+
for (const failure of failures.entries) {
|
|
3348
|
+
const input = failure.path === undefined
|
|
3349
|
+
? ""
|
|
3350
|
+
: `: ${formatGenerationFailurePath(projectRoot, failure.path)}`;
|
|
3351
|
+
const detail = failure.detail === undefined
|
|
3352
|
+
? ""
|
|
3353
|
+
: ` (producer: ${JSON.stringify(failure.detail)})`;
|
|
3354
|
+
lines.push(` - ${failure.domain}/${failure.kind}${input}${detail}`);
|
|
3355
|
+
}
|
|
3356
|
+
if (failures.omitted !== 0) {
|
|
3357
|
+
lines.push(` - ... ${failures.omitted} additional witness(es) omitted`);
|
|
3358
|
+
}
|
|
3359
|
+
});
|
|
3360
|
+
lines.push(" Stop writes to the listed inputs before compilation, or fix the producer that omitted or contradicted the listed proof.");
|
|
3361
|
+
return new TtscUnstableGenerationError(lines.join("\n"), validation);
|
|
2829
3362
|
}
|
|
2830
3363
|
function hashText(input) {
|
|
2831
3364
|
return crypto.createHash("sha256").update(input).digest("hex");
|
|
2832
3365
|
}
|
|
2833
3366
|
async function transformProject(props) {
|
|
3367
|
+
const attempts = [];
|
|
3368
|
+
for (let attempt = 0; attempt < TRANSFORM_GENERATION_ATTEMPTS; attempt += 1) {
|
|
3369
|
+
const cached = await captureTransformGeneration(props);
|
|
3370
|
+
if (!props.trackProjectMembership ||
|
|
3371
|
+
cached.result.type !== "success" ||
|
|
3372
|
+
cached.projectSnapshotComplete === true) {
|
|
3373
|
+
return cached;
|
|
3374
|
+
}
|
|
3375
|
+
attempts.push(TRANSFORM_GENERATION_FAILURES.get(cached.result) ??
|
|
3376
|
+
createGenerationProofFailures());
|
|
3377
|
+
if (attempt + 1 === TRANSFORM_GENERATION_ATTEMPTS) {
|
|
3378
|
+
const validation = TRANSFORM_FAILED_GENERATION_VALIDATIONS.get(cached.result);
|
|
3379
|
+
if (validation === undefined) {
|
|
3380
|
+
disposeCachedTransform(cached);
|
|
3381
|
+
throw new Error("ttsc: failed transform generation has no retry validation baseline");
|
|
3382
|
+
}
|
|
3383
|
+
throw createUnstableGenerationError(path.dirname(props.tsconfig), attempts, validation);
|
|
3384
|
+
}
|
|
3385
|
+
disposeCachedTransform(cached);
|
|
3386
|
+
}
|
|
3387
|
+
throw new Error("ttsc: transform generation retry loop did not terminate");
|
|
3388
|
+
}
|
|
3389
|
+
/** Capture one whole-project transform attempt and all of its reuse proofs. */
|
|
3390
|
+
async function captureTransformGeneration(props) {
|
|
2834
3391
|
const projectRoot = path.dirname(props.tsconfig);
|
|
2835
3392
|
const scratchDirectory = createTransformScratchDirectory(projectRoot, props.filesystem);
|
|
2836
3393
|
let tracker;
|
|
@@ -2870,6 +3427,7 @@ async function transformProject(props) {
|
|
|
2870
3427
|
filesystem: props.filesystem,
|
|
2871
3428
|
projectRoot,
|
|
2872
3429
|
result,
|
|
3430
|
+
scratchDirectory,
|
|
2873
3431
|
temporaryTsconfig,
|
|
2874
3432
|
});
|
|
2875
3433
|
// The generation's absent resolution candidates, which get a watcher of
|
|
@@ -2885,6 +3443,7 @@ async function transformProject(props) {
|
|
|
2885
3443
|
filesystem: props.filesystem,
|
|
2886
3444
|
projectRoot,
|
|
2887
3445
|
result,
|
|
3446
|
+
scratchDirectory,
|
|
2888
3447
|
temporaryTsconfig,
|
|
2889
3448
|
})
|
|
2890
3449
|
: { candidates: [], watched: [] };
|
|
@@ -2910,6 +3469,7 @@ async function transformProject(props) {
|
|
|
2910
3469
|
filesystem: props.filesystem,
|
|
2911
3470
|
projectRoot,
|
|
2912
3471
|
result,
|
|
3472
|
+
scratchDirectory,
|
|
2913
3473
|
temporaryTsconfig,
|
|
2914
3474
|
});
|
|
2915
3475
|
const inputSnapshot = collectProjectInputSnapshot(projectRoot, identities, props.filesystem);
|
|
@@ -2922,6 +3482,7 @@ async function transformProject(props) {
|
|
|
2922
3482
|
identities,
|
|
2923
3483
|
projectRoot,
|
|
2924
3484
|
result,
|
|
3485
|
+
scratchDirectory,
|
|
2925
3486
|
});
|
|
2926
3487
|
const walkStable = walkSnapshotComplete(before, declaredInputs) &&
|
|
2927
3488
|
walkSnapshotComplete(inputSnapshot, declaredInputs) &&
|
|
@@ -2937,14 +3498,19 @@ async function transformProject(props) {
|
|
|
2937
3498
|
// Overlay the in-memory source only after proving the two on-disk snapshots
|
|
2938
3499
|
// stable; an unsaved editor buffer must not look like a compile-time race.
|
|
2939
3500
|
const currentFileKey = toProjectKey(projectRoot, props.currentFile, identities);
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
3501
|
+
const currentSourceHash = hashText(props.currentSource);
|
|
3502
|
+
const projectInputHashes = { ...inputSnapshot.hashes };
|
|
3503
|
+
if (Object.prototype.hasOwnProperty.call(inputSnapshot.hashes, currentFileKey)) {
|
|
3504
|
+
inputSnapshot.hashes[currentFileKey] = currentSourceHash;
|
|
3505
|
+
// That overlay makes this one key the only recorded hash a disk signature
|
|
3506
|
+
// cannot stand for: the bytes it names came from the bundler, not the file.
|
|
3507
|
+
delete inputSnapshot.provenSignatures[currentFileKey];
|
|
3508
|
+
}
|
|
2944
3509
|
const cached = {
|
|
2945
3510
|
// Capture the out-of-walk input hashes while the generation is fresh so
|
|
2946
3511
|
// cache validation can re-check them; computed before dispose so the
|
|
2947
|
-
//
|
|
3512
|
+
// scratch-tree exclusion is the only reason its disposed artifacts never
|
|
3513
|
+
// key the persistent generation.
|
|
2948
3514
|
externalInputHashes: {},
|
|
2949
3515
|
externalInputRealpaths: {},
|
|
2950
3516
|
externalInputPaths,
|
|
@@ -2954,12 +3520,14 @@ async function transformProject(props) {
|
|
|
2954
3520
|
projectSnapshotComplete: false,
|
|
2955
3521
|
projectRoot,
|
|
2956
3522
|
result,
|
|
3523
|
+
scratchDirectory,
|
|
2957
3524
|
servedFiles: new Set(),
|
|
2958
3525
|
// Remember the generated temp-dir tsconfig (disposed below) so watch
|
|
2959
3526
|
// derivation can drop it from the envelope's config chain; a registered
|
|
2960
3527
|
// but deleted file would invalidate every persistent-cache snapshot.
|
|
2961
3528
|
...(temporaryTsconfig === undefined ? {} : { temporaryTsconfig }),
|
|
2962
3529
|
};
|
|
3530
|
+
cached.sourceHashes = captureTransformSourceHashes(cached, props.currentFile, currentSourceHash);
|
|
2963
3531
|
const externalInputSnapshot = captureExternalInputSnapshot(cached, externalInputPaths);
|
|
2964
3532
|
cached.externalInputHashes = externalInputSnapshot.hashes;
|
|
2965
3533
|
cached.externalInputRealpaths = externalInputSnapshot.realpaths;
|
|
@@ -2968,21 +3536,39 @@ async function transformProject(props) {
|
|
|
2968
3536
|
// cannot be reused can say which evidence it lacked. The extra work runs
|
|
2969
3537
|
// only on the failing path, where the alternative is recompiling the whole
|
|
2970
3538
|
// project for every remaining module.
|
|
2971
|
-
const
|
|
2972
|
-
|
|
2973
|
-
|
|
3539
|
+
const failures = createGenerationProofFailures();
|
|
3540
|
+
if (!walkStable) {
|
|
3541
|
+
recordProjectSnapshotFailures(failures, {
|
|
3542
|
+
before,
|
|
3543
|
+
candidateTracker,
|
|
3544
|
+
declared: declaredInputs,
|
|
3545
|
+
hostInputTracker,
|
|
3546
|
+
identities,
|
|
3547
|
+
projectRoot,
|
|
3548
|
+
snapshot: inputSnapshot,
|
|
3549
|
+
tracker,
|
|
3550
|
+
});
|
|
3551
|
+
}
|
|
3552
|
+
const graphFailures = compilerGraphInputProofFailures(cached);
|
|
3553
|
+
mergeGenerationProofFailures(failures, graphFailures);
|
|
3554
|
+
mergeGenerationProofFailures(failures, externalInputSnapshot.failures);
|
|
3555
|
+
const universalInputCapture = captureUniversalHostInputValidation(cached, props.currentFile);
|
|
3556
|
+
mergeGenerationProofFailures(failures, universalInputCapture.failures);
|
|
3557
|
+
const graphProofs = graphFailures.entries.length === 0 && graphFailures.omitted === 0;
|
|
3558
|
+
const universalInputs = universalInputCapture.validation !== undefined;
|
|
2974
3559
|
const stableProjectSnapshot = walkStable &&
|
|
2975
3560
|
graphProofs &&
|
|
2976
3561
|
externalInputSnapshot.complete &&
|
|
2977
3562
|
universalInputs;
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3563
|
+
if (!stableProjectSnapshot) {
|
|
3564
|
+
TRANSFORM_GENERATION_FAILURES.set(result, failures);
|
|
3565
|
+
TRANSFORM_FAILED_GENERATION_VALIDATIONS.set(result, {
|
|
3566
|
+
cached,
|
|
3567
|
+
declaredInputs,
|
|
3568
|
+
inputStates: captureFailedGenerationInputStates(cached, failures),
|
|
3569
|
+
projectInputHashes,
|
|
3570
|
+
projectWalkComplete: walkSnapshotComplete(inputSnapshot, declaredInputs),
|
|
3571
|
+
projectWalkFailures: projectWalkFailureFingerprint(inputSnapshot, declaredInputs, projectRoot, identities),
|
|
2986
3572
|
});
|
|
2987
3573
|
}
|
|
2988
3574
|
cached.projectSnapshotComplete = stableProjectSnapshot;
|
|
@@ -3033,16 +3619,23 @@ async function transformProject(props) {
|
|
|
3033
3619
|
}
|
|
3034
3620
|
}
|
|
3035
3621
|
}
|
|
3036
|
-
/** Exclude
|
|
3622
|
+
/** Exclude disposed transform scratch from live host-input tracking. */
|
|
3037
3623
|
function selectPersistentHostInputs(props) {
|
|
3038
3624
|
if (props.result.type === "exception")
|
|
3039
3625
|
return [];
|
|
3040
3626
|
const inputs = selectListedFiles(props.projectRoot, props.result.hostInputs);
|
|
3041
|
-
if (props.
|
|
3627
|
+
if (props.scratchDirectory === undefined &&
|
|
3628
|
+
props.temporaryTsconfig === undefined)
|
|
3042
3629
|
return inputs;
|
|
3043
3630
|
const identities = createHostPathIdentityContext(props.filesystem);
|
|
3044
|
-
const temporary =
|
|
3045
|
-
|
|
3631
|
+
const temporary = props.temporaryTsconfig === undefined
|
|
3632
|
+
? undefined
|
|
3633
|
+
: pathIdentityKey(props.temporaryTsconfig, identities);
|
|
3634
|
+
return inputs.filter((input) => {
|
|
3635
|
+
if (isTransformScratchInput(input, props.scratchDirectory))
|
|
3636
|
+
return false;
|
|
3637
|
+
return pathIdentityKey(input, identities) !== temporary;
|
|
3638
|
+
});
|
|
3046
3639
|
}
|
|
3047
3640
|
function createTransformTsconfig(props, scratchDirectory) {
|
|
3048
3641
|
const compilerOptions = normalizeCompilerOptionsForGeneratedTsconfig({
|
|
@@ -3133,6 +3726,11 @@ function pathIsWithin(child, parent) {
|
|
|
3133
3726
|
!relative.startsWith(`..${path.sep}`) &&
|
|
3134
3727
|
!path.isAbsolute(relative)));
|
|
3135
3728
|
}
|
|
3729
|
+
/** Whether an input is owned by the disposable transform scratch tree. */
|
|
3730
|
+
function isTransformScratchInput(input, scratchDirectory) {
|
|
3731
|
+
return (scratchDirectory !== undefined &&
|
|
3732
|
+
pathIsWithin(path.resolve(input), path.resolve(scratchDirectory)));
|
|
3733
|
+
}
|
|
3136
3734
|
/** Route all compiler/plugin scratch to one owned directory outside project. */
|
|
3137
3735
|
function transformScratchEnvironment(directory) {
|
|
3138
3736
|
return {
|