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