@ttsc/unplugin 0.28.5 → 0.29.0
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 +15 -11
- package/lib/api.js +10 -0
- package/lib/api.js.map +1 -1
- package/lib/api.mjs +3 -2
- package/lib/api.mjs.map +1 -1
- package/lib/bun-register.d.cts +10 -7
- package/lib/bun-register.d.mts +10 -7
- package/lib/bun-register.d.ts +10 -7
- package/lib/bun-register.js +90 -24
- package/lib/bun-register.js.map +1 -1
- package/lib/bun-register.mjs +90 -24
- package/lib/bun-register.mjs.map +1 -1
- package/lib/bun.d.cts +10 -8
- package/lib/bun.d.mts +10 -8
- package/lib/bun.d.ts +10 -8
- package/lib/bun.js +19 -17
- package/lib/bun.js.map +1 -1
- package/lib/bun.mjs +20 -18
- package/lib/bun.mjs.map +1 -1
- package/lib/core/index.d.cts +10 -8
- package/lib/core/index.d.mts +10 -8
- package/lib/core/index.d.ts +10 -8
- package/lib/core/index.js +88 -16
- package/lib/core/index.js.map +1 -1
- package/lib/core/index.mjs +81 -18
- package/lib/core/index.mjs.map +1 -1
- package/lib/core/projectDiscovery.d.cts +55 -0
- package/lib/core/projectDiscovery.d.mts +55 -0
- package/lib/core/projectDiscovery.d.ts +55 -0
- package/lib/core/projectDiscovery.js +187 -0
- package/lib/core/projectDiscovery.js.map +1 -0
- package/lib/core/projectDiscovery.mjs +182 -0
- package/lib/core/projectDiscovery.mjs.map +1 -0
- package/lib/core/sourceExtensions.d.cts +10 -0
- package/lib/core/sourceExtensions.d.mts +10 -0
- package/lib/core/sourceExtensions.d.ts +10 -0
- package/lib/core/sourceExtensions.js +38 -0
- package/lib/core/sourceExtensions.js.map +1 -0
- package/lib/core/sourceExtensions.mjs +32 -0
- package/lib/core/sourceExtensions.mjs.map +1 -0
- package/lib/core/transform.d.cts +96 -16
- package/lib/core/transform.d.mts +96 -16
- package/lib/core/transform.d.ts +96 -16
- package/lib/core/transform.js +1295 -309
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +1290 -310
- package/lib/core/transform.mjs.map +1 -1
- package/lib/core/tsconfigPaths.d.cts +55 -3
- package/lib/core/tsconfigPaths.d.mts +55 -3
- package/lib/core/tsconfigPaths.d.ts +55 -3
- package/lib/core/tsconfigPaths.js +260 -46
- package/lib/core/tsconfigPaths.js.map +1 -1
- package/lib/core/tsconfigPaths.mjs +255 -47
- package/lib/core/tsconfigPaths.mjs.map +1 -1
- package/lib/core/viteServe.d.cts +18 -21
- package/lib/core/viteServe.d.mts +18 -21
- package/lib/core/viteServe.d.ts +18 -21
- package/lib/core/viteServe.js +88 -47
- package/lib/core/viteServe.js.map +1 -1
- package/lib/core/viteServe.mjs +89 -49
- package/lib/core/viteServe.mjs.map +1 -1
- package/lib/next.d.cts +7 -0
- package/lib/next.d.mts +7 -0
- package/lib/next.d.ts +7 -0
- package/lib/next.js +186 -54
- package/lib/next.js.map +1 -1
- package/lib/next.mjs +186 -55
- package/lib/next.mjs.map +1 -1
- package/lib/turbopack.d.cts +6 -4
- package/lib/turbopack.d.mts +6 -4
- package/lib/turbopack.d.ts +6 -4
- package/lib/turbopack.js +11 -10
- package/lib/turbopack.js.map +1 -1
- package/lib/turbopack.mjs +11 -10
- package/lib/turbopack.mjs.map +1 -1
- package/package.json +7 -4
- package/src/bun-register.ts +112 -25
- package/src/bun.ts +60 -50
- package/src/core/index.ts +114 -19
- package/src/core/projectDiscovery.ts +256 -0
- package/src/core/sourceExtensions.ts +47 -0
- package/src/core/transform.ts +1671 -361
- package/src/core/tsconfigPaths.ts +406 -46
- package/src/core/viteServe.ts +125 -74
- package/src/next.ts +230 -54
- package/src/turbopack.ts +11 -10
package/src/core/transform.ts
CHANGED
|
@@ -12,19 +12,32 @@ import {
|
|
|
12
12
|
type FilesystemPathIdentityContext,
|
|
13
13
|
type FilesystemPathIdentityOperations,
|
|
14
14
|
createFilesystemPathIdentityContext,
|
|
15
|
+
resolveFilesystemPath,
|
|
15
16
|
} from "ttsc/path-identity";
|
|
16
17
|
import type { TransformResult } from "unplugin";
|
|
17
18
|
|
|
18
19
|
import type { ResolvedTtscUnpluginOptions } from "./options";
|
|
19
20
|
import {
|
|
21
|
+
findNearestProjectTsconfig,
|
|
22
|
+
isIgnoredProjectDirectory,
|
|
23
|
+
} from "./projectDiscovery";
|
|
24
|
+
import {
|
|
25
|
+
CONFIG_DIR_TEMPLATE_LIST_OPTIONS,
|
|
26
|
+
CONFIG_DIR_TEMPLATE_SCALAR_OPTIONS,
|
|
20
27
|
type ITtscProjectMembershipPolicy,
|
|
21
28
|
PERMISSIVE_PROJECT_MEMBERSHIP_POLICY,
|
|
22
29
|
absolutizePathsTarget,
|
|
23
30
|
mergeMembershipPolicyOverlay,
|
|
24
31
|
readEffectiveTsconfigPaths,
|
|
32
|
+
readEffectiveTsconfigTemplateCompilerOptions,
|
|
33
|
+
readEffectiveTsconfigTemplateFileSpecs,
|
|
25
34
|
readProjectMembershipPolicy,
|
|
35
|
+
readTsconfigSourceSnapshot,
|
|
36
|
+
resolveConfigDirTemplatePath,
|
|
26
37
|
} from "./tsconfigPaths";
|
|
27
38
|
|
|
39
|
+
const TTSC_SEMANTIC_CONFIG_PATH = "TTSC_SEMANTIC_CONFIG_PATH";
|
|
40
|
+
|
|
28
41
|
/**
|
|
29
42
|
* The normalised transform result type that this module produces.
|
|
30
43
|
*
|
|
@@ -284,6 +297,12 @@ const TRANSFORM_FAILED_GENERATION_VALIDATIONS = new WeakMap<
|
|
|
284
297
|
TtscFailedGenerationValidation
|
|
285
298
|
>();
|
|
286
299
|
|
|
300
|
+
/** Internal retained clock-probe ownership for published generations. */
|
|
301
|
+
const TRANSFORM_CLOCK_REFERENCE_DIRECTORIES = new WeakMap<
|
|
302
|
+
TtscCachedProjectTransform,
|
|
303
|
+
string
|
|
304
|
+
>();
|
|
305
|
+
|
|
287
306
|
/** Cache promises whose unchanged terminal verdict may be replayed. */
|
|
288
307
|
const TERMINAL_TRANSFORM_GENERATIONS = new WeakMap<
|
|
289
308
|
Promise<TtscCachedProjectTransform>,
|
|
@@ -310,6 +329,11 @@ const TRANSFORM_GENERATION_ATTEMPTS = 2;
|
|
|
310
329
|
* graph-free envelopes retain complete-snapshot validation.
|
|
311
330
|
*/
|
|
312
331
|
export interface TtscCachedProjectTransform {
|
|
332
|
+
/** Predicate-preserving compiler proofs for external candidate spellings. */
|
|
333
|
+
externalInputObservations?: Record<
|
|
334
|
+
string,
|
|
335
|
+
ITtscCompilerTransformation.IInputObservation
|
|
336
|
+
>;
|
|
313
337
|
/**
|
|
314
338
|
* SHA-256 hash of every input the compiler reported outside the project walk
|
|
315
339
|
* (keyed by filesystem identity), captured at the time of the transform.
|
|
@@ -429,6 +453,8 @@ export interface TtscCachedProjectTransform {
|
|
|
429
453
|
hostInputValidation?: TtscHostInputValidation;
|
|
430
454
|
/** Live notification state for file/directory creation, deletion, and rename. */
|
|
431
455
|
projectMutationTracker?: TtscProjectMutationTracker;
|
|
456
|
+
/** Whether a generated wrapper and its source config graph stayed coherent. */
|
|
457
|
+
configStateComplete?: boolean;
|
|
432
458
|
/**
|
|
433
459
|
* Whether the generation-time project walk observed every directory and file
|
|
434
460
|
* it attempted to snapshot. An incomplete walk may never authorize narrow
|
|
@@ -697,40 +723,97 @@ function clearTtscTransformCache(cache: TtscTransformCache): void {
|
|
|
697
723
|
* What the generation already knows about one derived watch input, handed to
|
|
698
724
|
* the adapter so it does not rederive it per input per delivery.
|
|
699
725
|
*
|
|
700
|
-
*
|
|
701
|
-
* {@link pathIdentityKey} of the input,
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
* `
|
|
705
|
-
* inputs) for one
|
|
726
|
+
* All facts are generation state: the identity is the memoized
|
|
727
|
+
* {@link pathIdentityKey} of the input, `missing` preserves the original public
|
|
728
|
+
* existence contract, and `unavailable` distinguishes a failed file predicate
|
|
729
|
+
* from ordinary absence. An adapter that computes them itself pays a
|
|
730
|
+
* `realpath`, a case-sensitivity directory listing, and an `existsSync` for
|
|
731
|
+
* every input of every delivered module, which is O(modules x inputs) for one
|
|
732
|
+
* build (samchon/ttsc#1246).
|
|
706
733
|
*/
|
|
707
734
|
export interface TtscWatchInputEvidence {
|
|
708
735
|
/** Memoized filesystem identity of the input. */
|
|
709
736
|
identity: string;
|
|
710
|
-
/** Whether the generation recorded this input as
|
|
737
|
+
/** Whether the generation recorded this input as unavailable as a file. */
|
|
711
738
|
missing: boolean;
|
|
739
|
+
/** The generation state Metro can compare with its main-process baseline. */
|
|
740
|
+
state?: TtscWatchInputState;
|
|
741
|
+
/** Which unavailable predicate must become true before invalidation. */
|
|
742
|
+
unavailable?: "missing" | "not-file";
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** Exact generation state behind one derived watch input. */
|
|
746
|
+
export type TtscWatchInputState =
|
|
747
|
+
| {
|
|
748
|
+
/** A project-walk or dependency-only input read as ordinary host bytes. */
|
|
749
|
+
codec: "host";
|
|
750
|
+
hash: string;
|
|
751
|
+
}
|
|
752
|
+
| {
|
|
753
|
+
/** A realized compiler-graph input, including its physical target. */
|
|
754
|
+
codec: "graph";
|
|
755
|
+
hash: string;
|
|
756
|
+
realpath: string | null;
|
|
757
|
+
}
|
|
758
|
+
| {
|
|
759
|
+
/** The exact compiler predicates observed for a resolver input. */
|
|
760
|
+
codec: "predicates";
|
|
761
|
+
observation: ITtscCompilerTransformation.IInputObservation;
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
/** Main-process file predicate used only by project discovery. */
|
|
765
|
+
export interface TtscWatchInputFileBaseline {
|
|
766
|
+
fileExists: boolean;
|
|
767
|
+
identity: string;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** Main-process state broad enough to compare every watch-input codec. */
|
|
771
|
+
export interface TtscWatchInputBaseline extends TtscWatchInputFileBaseline {
|
|
772
|
+
directoryExists: boolean;
|
|
773
|
+
graphHash: string;
|
|
774
|
+
graphReadHash: string | null;
|
|
775
|
+
hostHash: string;
|
|
776
|
+
realpath: { ok: false; path?: never } | { ok: true; path: string };
|
|
777
|
+
stat: "directory" | "file" | "missing";
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/** Baseline shape stored for either a discovery predicate or a full input. */
|
|
781
|
+
export type TtscWatchInputKeyBaseline =
|
|
782
|
+
| TtscWatchInputFileBaseline
|
|
783
|
+
| TtscWatchInputBaseline;
|
|
784
|
+
|
|
785
|
+
/** One derived input and its optional generation proof. */
|
|
786
|
+
export interface TtscWatchInput {
|
|
787
|
+
evidence?: TtscWatchInputEvidence;
|
|
788
|
+
file: string;
|
|
712
789
|
}
|
|
713
790
|
|
|
714
791
|
/**
|
|
715
792
|
* Hooks the bundler adapter passes into {@link transformTtsc} so transform
|
|
716
|
-
* side-channels (plugin-reported dependencies and host
|
|
717
|
-
*
|
|
718
|
-
* `TransformResult`.
|
|
793
|
+
* side-channels (plugin-reported dependencies and host resolver inputs) reach
|
|
794
|
+
* the bundler without leaking extra fields on the returned `TransformResult`.
|
|
719
795
|
*/
|
|
720
796
|
export interface TtscTransformHooks {
|
|
721
797
|
/**
|
|
722
798
|
* Invoked once per absolute watch-input path derived for the transformed file
|
|
723
799
|
* `F`: the plugin-reported `dependencies[F]` list unioned with the host-owned
|
|
724
800
|
* reference graph's contribution — the reachability closure of `graph.edges`
|
|
725
|
-
* from `F`, the `graph.globals` files, the `graph.configs` chain,
|
|
726
|
-
*
|
|
727
|
-
* `dependenciesComplete`, only `dependencies[F]`,
|
|
728
|
-
*
|
|
801
|
+
* from `F`, the `graph.globals` files, the `graph.configs` chain, importer
|
|
802
|
+
* `graph.candidates`, and universal `graph.resolutionInputs`. For a file the
|
|
803
|
+
* envelope declared `dependenciesComplete`, only `dependencies[F]`,
|
|
804
|
+
* `graph.candidates`, `graph.resolutionInputs`, and the universal
|
|
805
|
+
* `graph.configs` chain remain. Adapters forward this to the bundler's
|
|
729
806
|
* `addWatchFile` so type-only inputs participate in watch-mode and
|
|
730
807
|
* persistent-cache invalidation. See {@link selectWatchInputs} for the exact
|
|
731
808
|
* derivation.
|
|
732
809
|
*/
|
|
733
810
|
addWatchFile?: (file: string, evidence?: TtscWatchInputEvidence) => void;
|
|
811
|
+
/**
|
|
812
|
+
* Batched form of {@link addWatchFile}. When supplied, the transform calls it
|
|
813
|
+
* once per delivered module and does not call `addWatchFile` for that
|
|
814
|
+
* module.
|
|
815
|
+
*/
|
|
816
|
+
addWatchFiles?: (inputs: readonly TtscWatchInput[]) => void;
|
|
734
817
|
/**
|
|
735
818
|
* Invoked when the plugin declared the transformed file volatile (the
|
|
736
819
|
* envelope's `volatile` list): its output depends on non-file inputs that no
|
|
@@ -756,8 +839,7 @@ export interface TtscTransformHooks {
|
|
|
756
839
|
* @param id - Bundler module id (may carry a query string or virtual prefix).
|
|
757
840
|
* @param source - Current file content supplied by the bundler.
|
|
758
841
|
* @param options - Resolved plugin options.
|
|
759
|
-
* @param aliases - Raw
|
|
760
|
-
* object).
|
|
842
|
+
* @param aliases - Raw Vite alias configuration (object or array).
|
|
761
843
|
* @param cache - Optional project cache. Callers with a real `buildStart`
|
|
762
844
|
* boundary declare it through {@link beginTtscTransformBuild}; other hosts
|
|
763
845
|
* retain persistent validation.
|
|
@@ -868,9 +950,11 @@ export async function transformTtsc(
|
|
|
868
950
|
}
|
|
869
951
|
// The compile is fine and simply has nothing for this module, so the
|
|
870
952
|
// module goes back to the host untransformed rather than failing the
|
|
871
|
-
// build (samchon/ttsc#1308).
|
|
872
|
-
//
|
|
953
|
+
// build (samchon/ttsc#1308). Its project config and selection inputs
|
|
954
|
+
// still decide whether a later generation will contain this module,
|
|
955
|
+
// so hosts must receive the same universal watch-input batch.
|
|
873
956
|
reportMissingProgramOutput(cached, error, epoch);
|
|
957
|
+
notifyWatchInputs(hooks, cached, file);
|
|
874
958
|
markCachedSourceServed(cached, file);
|
|
875
959
|
return undefined;
|
|
876
960
|
}
|
|
@@ -926,6 +1010,7 @@ export async function transformTtsc(
|
|
|
926
1010
|
throw error;
|
|
927
1011
|
}
|
|
928
1012
|
reportMissingProgramOutput(cached, error, epoch);
|
|
1013
|
+
notifyWatchInputs(hooks, cached, file);
|
|
929
1014
|
markCachedSourceServed(cached, file);
|
|
930
1015
|
return undefined;
|
|
931
1016
|
}
|
|
@@ -1129,7 +1214,7 @@ function evictGeneration(
|
|
|
1129
1214
|
}
|
|
1130
1215
|
}
|
|
1131
1216
|
|
|
1132
|
-
/**
|
|
1217
|
+
/** Release one generation's watchers and retained clock probe exactly once. */
|
|
1133
1218
|
function disposeCachedTransform(cached: TtscCachedProjectTransform): void {
|
|
1134
1219
|
const trackers = [
|
|
1135
1220
|
cached.projectMutationTracker,
|
|
@@ -1139,7 +1224,21 @@ function disposeCachedTransform(cached: TtscCachedProjectTransform): void {
|
|
|
1139
1224
|
cached.projectMutationTracker = undefined;
|
|
1140
1225
|
cached.hostInputMutationTracker = undefined;
|
|
1141
1226
|
cached.candidateMutationTracker = undefined;
|
|
1142
|
-
|
|
1227
|
+
const clockReferenceDirectory =
|
|
1228
|
+
TRANSFORM_CLOCK_REFERENCE_DIRECTORIES.get(cached);
|
|
1229
|
+
TRANSFORM_CLOCK_REFERENCE_DIRECTORIES.delete(cached);
|
|
1230
|
+
for (const tracker of trackers) {
|
|
1231
|
+
try {
|
|
1232
|
+
tracker?.close();
|
|
1233
|
+
} catch {
|
|
1234
|
+
// Disposal is scheduled behind fulfilled generation Promises, so it has
|
|
1235
|
+
// no caller that can recover from a close failure. Keep releasing every
|
|
1236
|
+
// independent resource and leave no rejected cleanup Promise behind.
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (clockReferenceDirectory !== undefined) {
|
|
1240
|
+
disposeFilesystemClockReference(clockReferenceDirectory);
|
|
1241
|
+
}
|
|
1143
1242
|
}
|
|
1144
1243
|
|
|
1145
1244
|
/**
|
|
@@ -1198,7 +1297,7 @@ interface TtscEnvelopeDerivation {
|
|
|
1198
1297
|
* mirroring the historical scan). `undefined` until the first key miss.
|
|
1199
1298
|
*/
|
|
1200
1299
|
dependencyIndex?: Map<string, unknown>;
|
|
1201
|
-
/** Per-
|
|
1300
|
+
/** Per lexical delivered-module spelling memo of its final watch-input list. */
|
|
1202
1301
|
readonly watchInputs: Map<string, string[]>;
|
|
1203
1302
|
/**
|
|
1204
1303
|
* Lazily built project-walk keys of the envelope's declared inputs, and
|
|
@@ -1255,38 +1354,39 @@ interface TtscEnvelopeGraphIndexes {
|
|
|
1255
1354
|
readonly edges: Map<string, string[]>;
|
|
1256
1355
|
/** Identity of each direct-edge source -> its absolute spelling. */
|
|
1257
1356
|
readonly spellings: Map<string, string>;
|
|
1258
|
-
/**
|
|
1357
|
+
/** Importer-owned resolver-input entries, sources pre-identified. */
|
|
1259
1358
|
readonly candidates: { source: string; files: string[] }[];
|
|
1260
1359
|
/** Resolved absolute `graph.globals` and `graph.configs` members. */
|
|
1261
1360
|
readonly globals: string[];
|
|
1262
1361
|
readonly configs: string[];
|
|
1263
|
-
/**
|
|
1264
|
-
readonly
|
|
1362
|
+
/** Resolver inputs whose state affects every source file. */
|
|
1363
|
+
readonly resolutionInputs: string[];
|
|
1364
|
+
/** Every realized or resolver-input path, keyed by lexical spelling. */
|
|
1365
|
+
readonly memberSpellings: Set<string>;
|
|
1265
1366
|
/**
|
|
1266
|
-
*
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
* target that won, and the host enumerates the higher-priority spellings so
|
|
1272
|
-
* that one appearing later can invalidate the generation. Such a path has no
|
|
1273
|
-
* compile-time read to prove, so it carries the evidence a plugin-declared
|
|
1274
|
-
* dependency path carries (the state recorded when the envelope was produced)
|
|
1275
|
-
* instead of a compiler proof it can never have.
|
|
1276
|
-
*
|
|
1277
|
-
* A candidate that is also a realized input (an edge endpoint, a global, or a
|
|
1278
|
-
* config) is absent from this set and keeps the realized standard.
|
|
1367
|
+
* Predicate-only resolver inputs, keyed by absolute lexical spelling. These
|
|
1368
|
+
* are exact compiler calls but need not carry file content, for example a
|
|
1369
|
+
* failed file predicate or automatic type-root directory enumeration. A path
|
|
1370
|
+
* that is also a realized edge, global, config, or source keeps the stronger
|
|
1371
|
+
* realized-file standard.
|
|
1279
1372
|
*/
|
|
1280
1373
|
readonly speculative: Set<string>;
|
|
1281
|
-
/** Compiler-time proof
|
|
1374
|
+
/** Compiler-time legacy proof keyed by absolute lexical spelling. */
|
|
1282
1375
|
readonly inputProofs: Map<
|
|
1283
1376
|
string,
|
|
1284
1377
|
{ hash: string | null; path: string; realpath: string | null }
|
|
1285
1378
|
>;
|
|
1286
|
-
/** Native compiler-observation failure
|
|
1379
|
+
/** Native compiler-observation failure keyed by absolute lexical spelling. */
|
|
1287
1380
|
readonly inputProofFailures: Map<string, string>;
|
|
1288
|
-
/**
|
|
1381
|
+
/** Graph proof spellings that reported contradictory generation states. */
|
|
1289
1382
|
readonly inputProofConflicts: Set<string>;
|
|
1383
|
+
/** Predicate-preserving proofs keyed by absolute lexical spelling. */
|
|
1384
|
+
readonly inputObservations: Map<
|
|
1385
|
+
string,
|
|
1386
|
+
ITtscCompilerTransformation.IInputObservation
|
|
1387
|
+
>;
|
|
1388
|
+
/** Absolute spellings whose predicate proof is malformed or contradictory. */
|
|
1389
|
+
readonly inputObservationConflicts: Set<string>;
|
|
1290
1390
|
}
|
|
1291
1391
|
|
|
1292
1392
|
/**
|
|
@@ -1334,17 +1434,22 @@ function envelopeGraphIndexes(
|
|
|
1334
1434
|
if (state.graph !== undefined) {
|
|
1335
1435
|
return state.graph;
|
|
1336
1436
|
}
|
|
1437
|
+
const platform = resultFilesystem(props.result).platform ?? process.platform;
|
|
1438
|
+
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
1337
1439
|
const built: TtscEnvelopeGraphIndexes = {
|
|
1338
1440
|
edges: new Map(),
|
|
1339
1441
|
spellings: new Map(),
|
|
1340
1442
|
candidates: [],
|
|
1341
1443
|
globals: [],
|
|
1342
1444
|
configs: [],
|
|
1343
|
-
|
|
1445
|
+
resolutionInputs: [],
|
|
1446
|
+
memberSpellings: new Set(),
|
|
1344
1447
|
speculative: new Set(),
|
|
1345
1448
|
inputProofs: new Map(),
|
|
1346
1449
|
inputProofFailures: new Map(),
|
|
1347
1450
|
inputProofConflicts: new Set(),
|
|
1451
|
+
inputObservations: new Map(),
|
|
1452
|
+
inputObservationConflicts: new Set(),
|
|
1348
1453
|
};
|
|
1349
1454
|
const graph =
|
|
1350
1455
|
props.result.type === "exception" ? undefined : props.result.graph;
|
|
@@ -1355,7 +1460,7 @@ function envelopeGraphIndexes(
|
|
|
1355
1460
|
}
|
|
1356
1461
|
const absolute = path.resolve(props.projectRoot, source);
|
|
1357
1462
|
const identity = derivationIdentity(state, absolute);
|
|
1358
|
-
built.
|
|
1463
|
+
built.memberSpellings.add(absolute);
|
|
1359
1464
|
built.spellings.set(identity, absolute);
|
|
1360
1465
|
const entries = built.edges.get(identity) ?? [];
|
|
1361
1466
|
entries.push(
|
|
@@ -1367,7 +1472,7 @@ function envelopeGraphIndexes(
|
|
|
1367
1472
|
.map((target) => {
|
|
1368
1473
|
const absoluteTarget = path.resolve(props.projectRoot, target);
|
|
1369
1474
|
const targetIdentity = derivationIdentity(state, absoluteTarget);
|
|
1370
|
-
built.
|
|
1475
|
+
built.memberSpellings.add(absoluteTarget);
|
|
1371
1476
|
if (!built.spellings.has(targetIdentity)) {
|
|
1372
1477
|
built.spellings.set(targetIdentity, absoluteTarget);
|
|
1373
1478
|
}
|
|
@@ -1380,7 +1485,7 @@ function envelopeGraphIndexes(
|
|
|
1380
1485
|
built.configs.push(...selectListedFiles(props.projectRoot, graph.configs));
|
|
1381
1486
|
for (const input of [...built.globals, ...built.configs]) {
|
|
1382
1487
|
const identity = derivationIdentity(state, input);
|
|
1383
|
-
built.
|
|
1488
|
+
built.memberSpellings.add(path.resolve(input));
|
|
1384
1489
|
if (!built.spellings.has(identity)) built.spellings.set(identity, input);
|
|
1385
1490
|
}
|
|
1386
1491
|
const candidateEntries = Object.entries(graph.candidates ?? {}).filter(
|
|
@@ -1393,12 +1498,22 @@ function envelopeGraphIndexes(
|
|
|
1393
1498
|
for (const [source] of candidateEntries) {
|
|
1394
1499
|
const absoluteSource = path.resolve(props.projectRoot, source);
|
|
1395
1500
|
const identity = derivationIdentity(state, absoluteSource);
|
|
1396
|
-
built.
|
|
1501
|
+
built.memberSpellings.add(absoluteSource);
|
|
1397
1502
|
if (!built.spellings.has(identity)) {
|
|
1398
1503
|
built.spellings.set(identity, absoluteSource);
|
|
1399
1504
|
}
|
|
1400
1505
|
}
|
|
1401
|
-
const realized = new Set(built.
|
|
1506
|
+
const realized = new Set(built.memberSpellings);
|
|
1507
|
+
built.resolutionInputs.push(
|
|
1508
|
+
...selectListedFiles(props.projectRoot, graph.resolutionInputs),
|
|
1509
|
+
);
|
|
1510
|
+
for (const input of built.resolutionInputs) {
|
|
1511
|
+
const spelling = path.resolve(input);
|
|
1512
|
+
const identity = derivationIdentity(state, input);
|
|
1513
|
+
if (!realized.has(spelling)) built.speculative.add(spelling);
|
|
1514
|
+
built.memberSpellings.add(spelling);
|
|
1515
|
+
if (!built.spellings.has(identity)) built.spellings.set(identity, input);
|
|
1516
|
+
}
|
|
1402
1517
|
for (const [source, candidates] of candidateEntries) {
|
|
1403
1518
|
built.candidates.push({
|
|
1404
1519
|
source: derivationIdentity(
|
|
@@ -1409,33 +1524,64 @@ function envelopeGraphIndexes(
|
|
|
1409
1524
|
});
|
|
1410
1525
|
for (const candidate of candidates) {
|
|
1411
1526
|
if (typeof candidate !== "string" || candidate.length === 0) continue;
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1414
|
-
path.resolve(props.projectRoot, candidate),
|
|
1415
|
-
);
|
|
1527
|
+
const absoluteCandidate = path.resolve(props.projectRoot, candidate);
|
|
1528
|
+
const identity = derivationIdentity(state, absoluteCandidate);
|
|
1416
1529
|
// Edges, globals, configs, and every candidate source are folded in
|
|
1417
1530
|
// above, so a path absent from that set is one the envelope reported
|
|
1418
1531
|
// only as a candidate.
|
|
1419
|
-
if (!realized.has(
|
|
1420
|
-
|
|
1532
|
+
if (!realized.has(absoluteCandidate)) {
|
|
1533
|
+
built.speculative.add(absoluteCandidate);
|
|
1534
|
+
}
|
|
1535
|
+
built.memberSpellings.add(absoluteCandidate);
|
|
1421
1536
|
if (!built.spellings.has(identity)) {
|
|
1422
|
-
built.spellings.set(
|
|
1423
|
-
identity,
|
|
1424
|
-
path.resolve(props.projectRoot, candidate),
|
|
1425
|
-
);
|
|
1537
|
+
built.spellings.set(identity, absoluteCandidate);
|
|
1426
1538
|
}
|
|
1427
1539
|
}
|
|
1428
1540
|
}
|
|
1429
|
-
const
|
|
1541
|
+
const transformSourceSpellings = new Set<string>();
|
|
1430
1542
|
if (props.result.type === "success") {
|
|
1431
1543
|
for (const output of Object.keys(props.result.typescript)) {
|
|
1432
1544
|
if (!isDeclarationFile(output)) {
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
);
|
|
1545
|
+
const absoluteOutput = path.resolve(props.projectRoot, output);
|
|
1546
|
+
transformSourceSpellings.add(absoluteOutput);
|
|
1436
1547
|
}
|
|
1437
1548
|
}
|
|
1438
1549
|
}
|
|
1550
|
+
for (const [input, reported] of Object.entries(
|
|
1551
|
+
graph.inputObservations ?? {},
|
|
1552
|
+
)) {
|
|
1553
|
+
if (input.length === 0) continue;
|
|
1554
|
+
const absolute = path.resolve(props.projectRoot, input);
|
|
1555
|
+
const spelling = path.resolve(absolute);
|
|
1556
|
+
if (
|
|
1557
|
+
!built.memberSpellings.has(spelling) &&
|
|
1558
|
+
!transformSourceSpellings.has(spelling)
|
|
1559
|
+
) {
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
const normalized = normalizeGraphInputObservation(reported, platform);
|
|
1563
|
+
if (normalized === undefined) {
|
|
1564
|
+
built.inputObservationConflicts.add(spelling);
|
|
1565
|
+
if (!built.inputProofFailures.has(spelling)) {
|
|
1566
|
+
built.inputProofFailures.set(spelling, "malformed-observation");
|
|
1567
|
+
}
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
const previous = built.inputObservations.get(spelling);
|
|
1571
|
+
const merged =
|
|
1572
|
+
previous === undefined
|
|
1573
|
+
? normalized
|
|
1574
|
+
: mergeGraphInputObservations(previous, normalized);
|
|
1575
|
+
if (merged === undefined) {
|
|
1576
|
+
built.inputObservations.delete(spelling);
|
|
1577
|
+
built.inputObservationConflicts.add(spelling);
|
|
1578
|
+
if (!built.inputProofFailures.has(spelling)) {
|
|
1579
|
+
built.inputProofFailures.set(spelling, "conflicting-observation");
|
|
1580
|
+
}
|
|
1581
|
+
} else if (!built.inputObservationConflicts.has(spelling)) {
|
|
1582
|
+
built.inputObservations.set(spelling, merged);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1439
1585
|
for (const [input, hash] of Object.entries(graph.inputHashes ?? {})) {
|
|
1440
1586
|
if (
|
|
1441
1587
|
hash !== null &&
|
|
@@ -1453,22 +1599,27 @@ function envelopeGraphIndexes(
|
|
|
1453
1599
|
if (
|
|
1454
1600
|
reportedRealpath !== null &&
|
|
1455
1601
|
(typeof reportedRealpath !== "string" ||
|
|
1456
|
-
!
|
|
1602
|
+
!pathApi.isAbsolute(reportedRealpath))
|
|
1457
1603
|
) {
|
|
1458
1604
|
continue;
|
|
1459
1605
|
}
|
|
1460
1606
|
const absolute = path.resolve(props.projectRoot, input);
|
|
1461
|
-
const
|
|
1462
|
-
if (
|
|
1607
|
+
const spelling = path.resolve(absolute);
|
|
1608
|
+
if (
|
|
1609
|
+
!built.memberSpellings.has(spelling) &&
|
|
1610
|
+
!transformSourceSpellings.has(spelling)
|
|
1611
|
+
) {
|
|
1463
1612
|
continue;
|
|
1464
1613
|
}
|
|
1465
1614
|
const proof = {
|
|
1466
1615
|
hash,
|
|
1467
1616
|
path: absolute,
|
|
1468
1617
|
realpath:
|
|
1469
|
-
reportedRealpath === null
|
|
1618
|
+
reportedRealpath === null
|
|
1619
|
+
? null
|
|
1620
|
+
: resolveFilesystemPath(reportedRealpath, platform),
|
|
1470
1621
|
};
|
|
1471
|
-
const previous = built.inputProofs.get(
|
|
1622
|
+
const previous = built.inputProofs.get(spelling);
|
|
1472
1623
|
if (
|
|
1473
1624
|
previous !== undefined &&
|
|
1474
1625
|
(previous.hash !== proof.hash ||
|
|
@@ -1478,10 +1629,10 @@ function envelopeGraphIndexes(
|
|
|
1478
1629
|
state.identityContext,
|
|
1479
1630
|
))
|
|
1480
1631
|
) {
|
|
1481
|
-
built.inputProofs.delete(
|
|
1482
|
-
built.inputProofConflicts.add(
|
|
1483
|
-
} else if (!built.inputProofConflicts.has(
|
|
1484
|
-
built.inputProofs.set(
|
|
1632
|
+
built.inputProofs.delete(spelling);
|
|
1633
|
+
built.inputProofConflicts.add(spelling);
|
|
1634
|
+
} else if (!built.inputProofConflicts.has(spelling)) {
|
|
1635
|
+
built.inputProofs.set(spelling, proof);
|
|
1485
1636
|
}
|
|
1486
1637
|
}
|
|
1487
1638
|
for (const [input, reason] of Object.entries(
|
|
@@ -1491,16 +1642,35 @@ function envelopeGraphIndexes(
|
|
|
1491
1642
|
continue;
|
|
1492
1643
|
}
|
|
1493
1644
|
const absolute = path.resolve(props.projectRoot, input);
|
|
1494
|
-
const
|
|
1495
|
-
if (
|
|
1645
|
+
const spelling = path.resolve(absolute);
|
|
1646
|
+
if (
|
|
1647
|
+
!built.memberSpellings.has(spelling) &&
|
|
1648
|
+
!transformSourceSpellings.has(spelling)
|
|
1649
|
+
) {
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
const observation = built.inputObservations.get(spelling);
|
|
1653
|
+
const legacyProjection =
|
|
1654
|
+
observation === undefined
|
|
1655
|
+
? undefined
|
|
1656
|
+
: legacyProjectionOfGraphInputObservation(observation);
|
|
1657
|
+
if (
|
|
1658
|
+
built.speculative.has(spelling) &&
|
|
1659
|
+
!built.inputProofs.has(spelling) &&
|
|
1660
|
+
legacyProjection?.failure === reason
|
|
1661
|
+
) {
|
|
1496
1662
|
continue;
|
|
1497
1663
|
}
|
|
1498
|
-
if (built.
|
|
1499
|
-
built.
|
|
1500
|
-
built.
|
|
1664
|
+
if (built.inputObservations.has(spelling)) {
|
|
1665
|
+
built.inputObservations.delete(spelling);
|
|
1666
|
+
built.inputObservationConflicts.add(spelling);
|
|
1501
1667
|
}
|
|
1502
|
-
if (
|
|
1503
|
-
built.
|
|
1668
|
+
if (built.inputProofs.has(spelling)) {
|
|
1669
|
+
built.inputProofs.delete(spelling);
|
|
1670
|
+
built.inputProofConflicts.add(spelling);
|
|
1671
|
+
}
|
|
1672
|
+
if (!built.inputProofFailures.has(spelling)) {
|
|
1673
|
+
built.inputProofFailures.set(spelling, reason);
|
|
1504
1674
|
}
|
|
1505
1675
|
}
|
|
1506
1676
|
}
|
|
@@ -1508,6 +1678,218 @@ function envelopeGraphIndexes(
|
|
|
1508
1678
|
return built;
|
|
1509
1679
|
}
|
|
1510
1680
|
|
|
1681
|
+
/** Normalize one untrusted predicate proof without filling missing predicates. */
|
|
1682
|
+
function normalizeGraphInputObservation(
|
|
1683
|
+
value: unknown,
|
|
1684
|
+
platform: NodeJS.Platform = process.platform,
|
|
1685
|
+
): ITtscCompilerTransformation.IInputObservation | undefined {
|
|
1686
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1687
|
+
return undefined;
|
|
1688
|
+
}
|
|
1689
|
+
const entry = value as Record<string, unknown>;
|
|
1690
|
+
const observation: ITtscCompilerTransformation.IInputObservation = {};
|
|
1691
|
+
if (Object.prototype.hasOwnProperty.call(entry, "accessibleEntries")) {
|
|
1692
|
+
const accessible = entry.accessibleEntries;
|
|
1693
|
+
if (
|
|
1694
|
+
typeof accessible !== "object" ||
|
|
1695
|
+
accessible === null ||
|
|
1696
|
+
Array.isArray(accessible)
|
|
1697
|
+
) {
|
|
1698
|
+
return undefined;
|
|
1699
|
+
}
|
|
1700
|
+
const lists = accessible as Record<string, unknown>;
|
|
1701
|
+
if (
|
|
1702
|
+
!Array.isArray(lists.directories) ||
|
|
1703
|
+
!lists.directories.every(
|
|
1704
|
+
(name): name is string => typeof name === "string",
|
|
1705
|
+
) ||
|
|
1706
|
+
!Array.isArray(lists.files) ||
|
|
1707
|
+
!lists.files.every((name): name is string => typeof name === "string")
|
|
1708
|
+
) {
|
|
1709
|
+
return undefined;
|
|
1710
|
+
}
|
|
1711
|
+
observation.accessibleEntries = {
|
|
1712
|
+
directories: [...lists.directories],
|
|
1713
|
+
files: [...lists.files],
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
if (Object.prototype.hasOwnProperty.call(entry, "fileExists")) {
|
|
1717
|
+
if (typeof entry.fileExists !== "boolean") return undefined;
|
|
1718
|
+
observation.fileExists = entry.fileExists;
|
|
1719
|
+
}
|
|
1720
|
+
if (Object.prototype.hasOwnProperty.call(entry, "directoryExists")) {
|
|
1721
|
+
if (typeof entry.directoryExists !== "boolean") return undefined;
|
|
1722
|
+
observation.directoryExists = entry.directoryExists;
|
|
1723
|
+
}
|
|
1724
|
+
if (Object.prototype.hasOwnProperty.call(entry, "stat")) {
|
|
1725
|
+
if (
|
|
1726
|
+
entry.stat !== "missing" &&
|
|
1727
|
+
entry.stat !== "file" &&
|
|
1728
|
+
entry.stat !== "directory"
|
|
1729
|
+
) {
|
|
1730
|
+
return undefined;
|
|
1731
|
+
}
|
|
1732
|
+
observation.stat = entry.stat;
|
|
1733
|
+
}
|
|
1734
|
+
if (Object.prototype.hasOwnProperty.call(entry, "readFile")) {
|
|
1735
|
+
const read = entry.readFile;
|
|
1736
|
+
if (typeof read !== "object" || read === null || Array.isArray(read)) {
|
|
1737
|
+
return undefined;
|
|
1738
|
+
}
|
|
1739
|
+
const result = read as Record<string, unknown>;
|
|
1740
|
+
if (result.ok === false && result.hash === undefined) {
|
|
1741
|
+
observation.readFile = { ok: false };
|
|
1742
|
+
} else if (
|
|
1743
|
+
result.ok === true &&
|
|
1744
|
+
typeof result.hash === "string" &&
|
|
1745
|
+
/^[0-9a-f]{64}$/.test(result.hash)
|
|
1746
|
+
) {
|
|
1747
|
+
observation.readFile = { hash: result.hash, ok: true };
|
|
1748
|
+
} else {
|
|
1749
|
+
return undefined;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (Object.prototype.hasOwnProperty.call(entry, "realpath")) {
|
|
1753
|
+
const realpath = entry.realpath;
|
|
1754
|
+
if (
|
|
1755
|
+
typeof realpath !== "object" ||
|
|
1756
|
+
realpath === null ||
|
|
1757
|
+
Array.isArray(realpath)
|
|
1758
|
+
) {
|
|
1759
|
+
return undefined;
|
|
1760
|
+
}
|
|
1761
|
+
const result = realpath as Record<string, unknown>;
|
|
1762
|
+
if (result.ok === false && result.path === undefined) {
|
|
1763
|
+
observation.realpath = { ok: false };
|
|
1764
|
+
} else if (
|
|
1765
|
+
result.ok === true &&
|
|
1766
|
+
typeof result.path === "string" &&
|
|
1767
|
+
(platform === "win32" ? path.win32 : path.posix).isAbsolute(result.path)
|
|
1768
|
+
) {
|
|
1769
|
+
observation.realpath = {
|
|
1770
|
+
ok: true,
|
|
1771
|
+
path: resolveFilesystemPath(result.path, platform),
|
|
1772
|
+
};
|
|
1773
|
+
} else {
|
|
1774
|
+
return undefined;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return Object.keys(observation).length !== 0 &&
|
|
1778
|
+
graphInputObservationCompatible(observation)
|
|
1779
|
+
? observation
|
|
1780
|
+
: undefined;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/** Join duplicate lexical keys only when every repeated predicate agrees. */
|
|
1784
|
+
function mergeGraphInputObservations(
|
|
1785
|
+
left: ITtscCompilerTransformation.IInputObservation,
|
|
1786
|
+
right: ITtscCompilerTransformation.IInputObservation,
|
|
1787
|
+
): ITtscCompilerTransformation.IInputObservation | undefined {
|
|
1788
|
+
for (const property of [
|
|
1789
|
+
"accessibleEntries",
|
|
1790
|
+
"directoryExists",
|
|
1791
|
+
"fileExists",
|
|
1792
|
+
"readFile",
|
|
1793
|
+
"realpath",
|
|
1794
|
+
"stat",
|
|
1795
|
+
] as const) {
|
|
1796
|
+
if (
|
|
1797
|
+
left[property] !== undefined &&
|
|
1798
|
+
right[property] !== undefined &&
|
|
1799
|
+
JSON.stringify(left[property]) !== JSON.stringify(right[property])
|
|
1800
|
+
) {
|
|
1801
|
+
return undefined;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
const merged = { ...left, ...right };
|
|
1805
|
+
return graphInputObservationCompatible(merged) ? merged : undefined;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/** Whether one predicate set can describe a single stable filesystem state. */
|
|
1809
|
+
function graphInputObservationCompatible(
|
|
1810
|
+
observation: ITtscCompilerTransformation.IInputObservation,
|
|
1811
|
+
): boolean {
|
|
1812
|
+
const { accessibleEntries, directoryExists, fileExists, readFile, stat } =
|
|
1813
|
+
observation;
|
|
1814
|
+
const hasAccessibleEntries =
|
|
1815
|
+
accessibleEntries !== undefined &&
|
|
1816
|
+
(accessibleEntries.directories.length !== 0 ||
|
|
1817
|
+
accessibleEntries.files.length !== 0);
|
|
1818
|
+
if (
|
|
1819
|
+
hasAccessibleEntries &&
|
|
1820
|
+
(fileExists === true ||
|
|
1821
|
+
directoryExists === false ||
|
|
1822
|
+
(stat !== undefined && stat !== "directory") ||
|
|
1823
|
+
readFile?.ok === true)
|
|
1824
|
+
) {
|
|
1825
|
+
return false;
|
|
1826
|
+
}
|
|
1827
|
+
if (fileExists === true && directoryExists === true) return false;
|
|
1828
|
+
if (
|
|
1829
|
+
stat === "directory" &&
|
|
1830
|
+
(fileExists === true || directoryExists === false)
|
|
1831
|
+
) {
|
|
1832
|
+
return false;
|
|
1833
|
+
}
|
|
1834
|
+
if (stat === "file" && (fileExists === false || directoryExists === true)) {
|
|
1835
|
+
return false;
|
|
1836
|
+
}
|
|
1837
|
+
if (stat === "missing" && (fileExists === true || directoryExists === true)) {
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1840
|
+
if (
|
|
1841
|
+
readFile?.ok === true &&
|
|
1842
|
+
(fileExists === false ||
|
|
1843
|
+
directoryExists === true ||
|
|
1844
|
+
(stat !== undefined && stat !== "file"))
|
|
1845
|
+
) {
|
|
1846
|
+
return false;
|
|
1847
|
+
}
|
|
1848
|
+
return true;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
/**
|
|
1852
|
+
* Reconstruct a legacy hash/realpath pair only when the rich predicates prove
|
|
1853
|
+
* that projection without collapsing an unreadable file or failed file probe
|
|
1854
|
+
* into generic absence.
|
|
1855
|
+
*/
|
|
1856
|
+
function legacyProjectionOfGraphInputObservation(
|
|
1857
|
+
observation: ITtscCompilerTransformation.IInputObservation,
|
|
1858
|
+
):
|
|
1859
|
+
| { failure?: undefined; hash: string | null; realpath: string | null }
|
|
1860
|
+
| { failure: string } {
|
|
1861
|
+
if (observation.readFile?.ok === true) {
|
|
1862
|
+
return observation.realpath?.ok === true
|
|
1863
|
+
? {
|
|
1864
|
+
hash: observation.readFile.hash,
|
|
1865
|
+
realpath: observation.realpath.path,
|
|
1866
|
+
}
|
|
1867
|
+
: { failure: "realpath-unavailable" };
|
|
1868
|
+
}
|
|
1869
|
+
const directory =
|
|
1870
|
+
observation.stat === "directory" || observation.directoryExists === true;
|
|
1871
|
+
if (directory) {
|
|
1872
|
+
return observation.realpath?.ok === true
|
|
1873
|
+
? {
|
|
1874
|
+
hash: hashText("ttsc:host-input:directory\0"),
|
|
1875
|
+
realpath: observation.realpath.path,
|
|
1876
|
+
}
|
|
1877
|
+
: { failure: "realpath-unavailable" };
|
|
1878
|
+
}
|
|
1879
|
+
const file = observation.stat === "file" || observation.fileExists === true;
|
|
1880
|
+
if (file) {
|
|
1881
|
+
return { failure: "content-unavailable" };
|
|
1882
|
+
}
|
|
1883
|
+
const missing =
|
|
1884
|
+
observation.stat === "missing" ||
|
|
1885
|
+
observation.fileExists === false ||
|
|
1886
|
+
observation.directoryExists === false ||
|
|
1887
|
+
observation.readFile?.ok === false;
|
|
1888
|
+
return missing
|
|
1889
|
+
? { hash: null, realpath: null }
|
|
1890
|
+
: { failure: "unsupported-input-kind" };
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1511
1893
|
/**
|
|
1512
1894
|
* {@link pathIdentityKey} memoized inside one envelope's derivation state.
|
|
1513
1895
|
* Callers always pass already-resolved absolute paths, so the input string is a
|
|
@@ -1551,21 +1933,8 @@ function collectDeclaredIdentities(
|
|
|
1551
1933
|
}
|
|
1552
1934
|
|
|
1553
1935
|
/**
|
|
1554
|
-
*
|
|
1555
|
-
*
|
|
1556
|
-
* host-owned reference graph's contribution (`reach(edges, file)`, `globals`,
|
|
1557
|
-
* `configs`, and resolution candidates).
|
|
1558
|
-
*
|
|
1559
|
-
* Envelope keys mirror the `typescript` keys (project-relative); values may be
|
|
1560
|
-
* project-relative or absolute. Every path is absolutized against the project
|
|
1561
|
-
* root and deduplicated; the file itself is dropped (the bundler already
|
|
1562
|
-
* watches the module it transforms), and so is every path in the disposed
|
|
1563
|
-
* transform scratch tree (see
|
|
1564
|
-
* {@link TtscCachedProjectTransform.scratchDirectory}).
|
|
1565
|
-
*/
|
|
1566
|
-
/**
|
|
1567
|
-
* Register the failed generation's own project inputs so the host can observe
|
|
1568
|
-
* the fix.
|
|
1936
|
+
* Register the failed generation's project and external inputs so the host can
|
|
1937
|
+
* observe the fix.
|
|
1569
1938
|
*
|
|
1570
1939
|
* A successful delivery registers the derived watch inputs, which is how a
|
|
1571
1940
|
* type-only file that no bundler graph contains still invalidates the modules
|
|
@@ -1576,76 +1945,200 @@ function collectDeclaredIdentities(
|
|
|
1576
1945
|
* arrive: the user repairs a file the bundler does not track, nothing is
|
|
1577
1946
|
* invalidated, and the error stays on screen (samchon/ttsc#1312).
|
|
1578
1947
|
*
|
|
1579
|
-
*
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
1582
|
-
*
|
|
1948
|
+
* A failure envelope can retain exact external input spellings from its graph
|
|
1949
|
+
* and host metadata. A pre-transform typecheck failure may have no graph yet;
|
|
1950
|
+
* its structured diagnostics, or the host's standard diagnostic lines when it
|
|
1951
|
+
* could return only an exception, still name the external files that need a
|
|
1952
|
+
* repair. The cost is paid only on a failure, and only until the next compile
|
|
1953
|
+
* succeeds and narrows the set back to the derived inputs.
|
|
1583
1954
|
*/
|
|
1584
1955
|
function notifyFailedGenerationInputs(
|
|
1585
1956
|
hooks: TtscTransformHooks | undefined,
|
|
1586
1957
|
cached: TtscCachedProjectTransform,
|
|
1587
1958
|
): void {
|
|
1588
1959
|
const addWatchFile = hooks?.addWatchFile;
|
|
1589
|
-
|
|
1960
|
+
const addWatchFiles = hooks?.addWatchFiles;
|
|
1961
|
+
if (addWatchFile === undefined && addWatchFiles === undefined) {
|
|
1590
1962
|
return;
|
|
1591
1963
|
}
|
|
1964
|
+
const inputs: TtscWatchInput[] = [];
|
|
1965
|
+
const seen = new Set<string>();
|
|
1966
|
+
const append = (input: string): void => {
|
|
1967
|
+
const spelling = path.resolve(input);
|
|
1968
|
+
if (
|
|
1969
|
+
seen.has(spelling) ||
|
|
1970
|
+
isTransformScratchInput(spelling, cached.scratchDirectory)
|
|
1971
|
+
) {
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
seen.add(spelling);
|
|
1975
|
+
// No evidence, deliberately. A failed generation is replayed for the rest
|
|
1976
|
+
// of its pass without re-proving its inputs, so the adapter must observe
|
|
1977
|
+
// the current availability itself.
|
|
1978
|
+
inputs.push({ file: spelling });
|
|
1979
|
+
};
|
|
1592
1980
|
for (const key of Object.keys(cached.inputHashes)) {
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1981
|
+
append(path.resolve(cached.projectRoot, key));
|
|
1982
|
+
}
|
|
1983
|
+
// The project walk deliberately excludes node_modules and cannot reach
|
|
1984
|
+
// sibling-project or out-of-root inputs. The generation already retained
|
|
1985
|
+
// their exact lexical spellings, so keep that ownership on the failure path
|
|
1986
|
+
// instead of deriving a narrower second answer.
|
|
1987
|
+
for (const input of cached.externalInputPaths ?? []) {
|
|
1988
|
+
append(input);
|
|
1989
|
+
}
|
|
1990
|
+
if (cached.result.type === "failure") {
|
|
1991
|
+
for (const diagnostic of cached.result.diagnostics) {
|
|
1992
|
+
if (typeof diagnostic.file === "string" && diagnostic.file.length !== 0) {
|
|
1993
|
+
append(path.resolve(cached.projectRoot, diagnostic.file));
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
} else if (cached.result.type === "exception") {
|
|
1997
|
+
for (const diagnostic of selectExceptionDiagnosticFiles(
|
|
1998
|
+
cached.result.error,
|
|
1999
|
+
)) {
|
|
2000
|
+
append(path.resolve(cached.projectRoot, diagnostic));
|
|
1596
2001
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
// watch on a path that does not exist registers nothing, and no module
|
|
1605
|
-
// graph carries a type-only input. It costs one `existsSync` per input,
|
|
1606
|
-
// and only where the adapter reads evidence at all, which is Vite serve.
|
|
1607
|
-
addWatchFile(input);
|
|
2002
|
+
}
|
|
2003
|
+
if (addWatchFiles !== undefined) {
|
|
2004
|
+
addWatchFiles(inputs);
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
for (const input of inputs) {
|
|
2008
|
+
addWatchFile!(input.file);
|
|
1608
2009
|
}
|
|
1609
2010
|
}
|
|
1610
2011
|
|
|
2012
|
+
/**
|
|
2013
|
+
* Extract file spellings only from the two standard TypeScript diagnostic
|
|
2014
|
+
* forms.
|
|
2015
|
+
*/
|
|
2016
|
+
function selectExceptionDiagnosticFiles(error: unknown): string[] {
|
|
2017
|
+
const files: string[] = [];
|
|
2018
|
+
for (const line of formatUnknownError(error).split(/\r?\n/)) {
|
|
2019
|
+
const colon =
|
|
2020
|
+
/^(.+):\d+:\d+\s+-\s+(?:error|warning|suggestion|message)\s+TS\d+:\s+.+$/i.exec(
|
|
2021
|
+
line,
|
|
2022
|
+
);
|
|
2023
|
+
const parenthesized =
|
|
2024
|
+
/^(.+?)\(\d+,\d+\):\s+(?:error|warning|suggestion|message)\s+TS\d+:\s+.+$/i.exec(
|
|
2025
|
+
line,
|
|
2026
|
+
);
|
|
2027
|
+
const file = colon?.[1] ?? parenthesized?.[1];
|
|
2028
|
+
if (file !== undefined && file.trim().length !== 0) {
|
|
2029
|
+
files.push(file.trim());
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
return files;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
/**
|
|
2036
|
+
* Forward every derived watch input for `file` to the adapter's `addWatchFile`
|
|
2037
|
+
* hook: the plugin-reported `dependencies[file]` list unioned with the
|
|
2038
|
+
* host-owned reference graph's contribution (`reach(edges, file)`, `globals`,
|
|
2039
|
+
* `configs`, and resolution candidates).
|
|
2040
|
+
*
|
|
2041
|
+
* Envelope keys mirror the `typescript` keys (project-relative); values may be
|
|
2042
|
+
* project-relative or absolute. Every path is absolutized against the project
|
|
2043
|
+
* root and deduplicated; the file itself is dropped (the bundler already
|
|
2044
|
+
* watches the module it transforms), and so is every path in the disposed
|
|
2045
|
+
* transform scratch tree (see
|
|
2046
|
+
* {@link TtscCachedProjectTransform.scratchDirectory}).
|
|
2047
|
+
*/
|
|
1611
2048
|
function notifyWatchInputs(
|
|
1612
2049
|
hooks: TtscTransformHooks | undefined,
|
|
1613
2050
|
cached: TtscCachedProjectTransform,
|
|
1614
2051
|
file: string,
|
|
1615
2052
|
): void {
|
|
1616
2053
|
const addWatchFile = hooks?.addWatchFile;
|
|
1617
|
-
|
|
2054
|
+
const addWatchFiles = hooks?.addWatchFiles;
|
|
2055
|
+
if (addWatchFile === undefined && addWatchFiles === undefined) {
|
|
1618
2056
|
return;
|
|
1619
2057
|
}
|
|
1620
2058
|
const state = envelopeDerivation(cached);
|
|
1621
2059
|
const external = cached.externalInputHashes ?? {};
|
|
1622
|
-
|
|
2060
|
+
const inputs = selectWatchInputs({
|
|
1623
2061
|
file,
|
|
1624
2062
|
projectRoot: cached.projectRoot,
|
|
1625
2063
|
result: cached.result,
|
|
1626
2064
|
scratchDirectory: cached.scratchDirectory,
|
|
1627
2065
|
temporaryTsconfig: cached.temporaryTsconfig,
|
|
1628
|
-
})) {
|
|
2066
|
+
}).map((input): TtscWatchInput => {
|
|
1629
2067
|
// Hand the adapter the identity this generation already resolved and the
|
|
1630
|
-
//
|
|
1631
|
-
//
|
|
1632
|
-
//
|
|
1633
|
-
// (samchon/ttsc#1246).
|
|
2068
|
+
// exact state it already recorded. Both are memoized per generation, while
|
|
2069
|
+
// an adapter deriving them itself pays repeated filesystem reads and can
|
|
2070
|
+
// accidentally attach a later state to an earlier transform.
|
|
1634
2071
|
const identity = derivationIdentity(state, input);
|
|
1635
|
-
|
|
2072
|
+
const spelling = path.resolve(input);
|
|
2073
|
+
const observation = cached.externalInputObservations?.[spelling];
|
|
2074
|
+
const missing =
|
|
2075
|
+
observation?.fileExists === false ||
|
|
2076
|
+
state.graph?.inputProofs.get(spelling)?.hash === null ||
|
|
2077
|
+
external[identity] === MISSING_INPUT_STATE;
|
|
2078
|
+
const projectKey = toProjectKey(
|
|
2079
|
+
cached.projectRoot,
|
|
2080
|
+
input,
|
|
2081
|
+
state.identityContext,
|
|
2082
|
+
);
|
|
2083
|
+
const externalHash = Object.prototype.hasOwnProperty.call(
|
|
2084
|
+
external,
|
|
1636
2085
|
identity,
|
|
1637
|
-
|
|
1638
|
-
|
|
2086
|
+
)
|
|
2087
|
+
? external[identity]
|
|
2088
|
+
: undefined;
|
|
2089
|
+
const projectHash = Object.prototype.hasOwnProperty.call(
|
|
2090
|
+
cached.inputHashes,
|
|
2091
|
+
projectKey,
|
|
2092
|
+
)
|
|
2093
|
+
? cached.inputHashes[projectKey]
|
|
2094
|
+
: undefined;
|
|
2095
|
+
const graphRealpaths = cached.externalInputRealpaths ?? {};
|
|
2096
|
+
const stateEvidence: TtscWatchInputState | undefined =
|
|
2097
|
+
observation !== undefined
|
|
2098
|
+
? { codec: "predicates", observation }
|
|
2099
|
+
: externalHash !== undefined &&
|
|
2100
|
+
Object.prototype.hasOwnProperty.call(graphRealpaths, identity)
|
|
2101
|
+
? {
|
|
2102
|
+
codec: "graph",
|
|
2103
|
+
hash: externalHash,
|
|
2104
|
+
realpath: graphRealpaths[identity] ?? null,
|
|
2105
|
+
}
|
|
2106
|
+
: externalHash !== undefined
|
|
2107
|
+
? { codec: "host", hash: externalHash }
|
|
2108
|
+
: projectHash !== undefined
|
|
2109
|
+
? { codec: "host", hash: projectHash }
|
|
2110
|
+
: undefined;
|
|
2111
|
+
return {
|
|
2112
|
+
file: input,
|
|
2113
|
+
evidence: {
|
|
2114
|
+
identity,
|
|
2115
|
+
missing,
|
|
2116
|
+
...(stateEvidence === undefined ? {} : { state: stateEvidence }),
|
|
2117
|
+
unavailable:
|
|
2118
|
+
observation?.fileExists === false
|
|
2119
|
+
? "not-file"
|
|
2120
|
+
: missing
|
|
2121
|
+
? "missing"
|
|
2122
|
+
: undefined,
|
|
2123
|
+
},
|
|
2124
|
+
};
|
|
2125
|
+
});
|
|
2126
|
+
if (addWatchFiles !== undefined) {
|
|
2127
|
+
addWatchFiles(inputs);
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
for (const input of inputs) {
|
|
2131
|
+
addWatchFile!(input.file, input.evidence);
|
|
1639
2132
|
}
|
|
1640
2133
|
}
|
|
1641
2134
|
|
|
1642
2135
|
/**
|
|
1643
2136
|
* Derive the absolute, deduplicated watch-input list for a single file.
|
|
1644
2137
|
*
|
|
1645
|
-
* By default the derivation
|
|
1646
|
-
*
|
|
1647
|
-
*
|
|
1648
|
-
* remain
|
|
2138
|
+
* By default the derivation unions plugin dependencies, the reachable graph,
|
|
2139
|
+
* globals, configs, importer resolver inputs, and universal resolver inputs.
|
|
2140
|
+
* The plugin-reported list can only widen the host-owned language-semantic
|
|
2141
|
+
* bound, never narrow it. Resolver inputs remain in that bound in both modes.
|
|
1649
2142
|
*
|
|
1650
2143
|
* An envelope that lists `file` in `dependenciesComplete` narrows it to
|
|
1651
2144
|
* `dependencies[file] ∪ configs`: the plugin declared its reported list the
|
|
@@ -1657,10 +2150,12 @@ function notifyWatchInputs(
|
|
|
1657
2150
|
* volatile keeps the baseline: the two declarations contradict, so the
|
|
1658
2151
|
* conservative one wins.
|
|
1659
2152
|
*
|
|
1660
|
-
* The derived list is a pure function of the envelope and the file's
|
|
1661
|
-
*
|
|
1662
|
-
*
|
|
1663
|
-
*
|
|
2153
|
+
* The derived list is a pure function of the envelope and the delivered file's
|
|
2154
|
+
* normalized lexical spelling. Graph traversal uses its filesystem identity,
|
|
2155
|
+
* but lexical inputs exclude only that exact spelling, so two aliases of one
|
|
2156
|
+
* source require distinct lists. Repeated deliveries of one spelling replay the
|
|
2157
|
+
* per-envelope memo ({@link envelopeDerivation}) instead of re-walking the
|
|
2158
|
+
* graph. Returns an empty list on exceptions.
|
|
1664
2159
|
*/
|
|
1665
2160
|
function selectWatchInputs(props: {
|
|
1666
2161
|
file: string;
|
|
@@ -1674,12 +2169,13 @@ function selectWatchInputs(props: {
|
|
|
1674
2169
|
}
|
|
1675
2170
|
const state = envelopeDerivation(props);
|
|
1676
2171
|
const fileIdentity = derivationIdentity(state, props.file);
|
|
1677
|
-
const
|
|
2172
|
+
const fileSpelling = path.resolve(props.file);
|
|
2173
|
+
const memoized = state.watchInputs.get(fileSpelling);
|
|
1678
2174
|
if (memoized !== undefined) {
|
|
1679
2175
|
return memoized;
|
|
1680
2176
|
}
|
|
1681
2177
|
const derived = deriveWatchInputs(state, props, fileIdentity);
|
|
1682
|
-
state.watchInputs.set(
|
|
2178
|
+
state.watchInputs.set(fileSpelling, derived);
|
|
1683
2179
|
return derived;
|
|
1684
2180
|
}
|
|
1685
2181
|
|
|
@@ -1738,7 +2234,7 @@ function deriveWatchInputs(
|
|
|
1738
2234
|
!isVolatileFile(state, props),
|
|
1739
2235
|
}))
|
|
1740
2236
|
appendPhysical(input);
|
|
1741
|
-
//
|
|
2237
|
+
// Resolver inputs, plugin dependencies, and universal host inputs
|
|
1742
2238
|
// preserve lexical aliases. Physical deduplication would collapse
|
|
1743
2239
|
// `alias/selection.cjs` into the selected target path, so a bundler would
|
|
1744
2240
|
// watch only the target and miss a symlink/junction retarget.
|
|
@@ -1759,14 +2255,14 @@ function selectHostInputs(props: {
|
|
|
1759
2255
|
}
|
|
1760
2256
|
|
|
1761
2257
|
/**
|
|
1762
|
-
* Return
|
|
1763
|
-
*
|
|
1764
|
-
*
|
|
1765
|
-
*
|
|
2258
|
+
* Return exact importer-owned and universal resolver inputs for `file`. They
|
|
2259
|
+
* remain host-owned even when a plugin declares `dependenciesComplete`: plugin
|
|
2260
|
+
* code cannot vouch for a compiler resolution or automatic type change that
|
|
2261
|
+
* occurs without any plugin input changing.
|
|
1766
2262
|
*
|
|
1767
|
-
*
|
|
1768
|
-
* per-envelope state, so one delivery scans only the
|
|
1769
|
-
*
|
|
2263
|
+
* Importer entries and their source identities come from the shared
|
|
2264
|
+
* per-envelope state, so one delivery scans only the recorded inputs instead of
|
|
2265
|
+
* re-resolving every source.
|
|
1770
2266
|
*/
|
|
1771
2267
|
function selectResolutionCandidateInputs(
|
|
1772
2268
|
graph: TtscEnvelopeGraphIndexes,
|
|
@@ -1777,10 +2273,7 @@ function selectResolutionCandidateInputs(
|
|
|
1777
2273
|
result: ITtscCompilerTransformation;
|
|
1778
2274
|
},
|
|
1779
2275
|
): string[] {
|
|
1780
|
-
if (
|
|
1781
|
-
props.result.type === "exception" ||
|
|
1782
|
-
props.result.graph?.candidates === undefined
|
|
1783
|
-
) {
|
|
2276
|
+
if (props.result.type === "exception" || props.result.graph === undefined) {
|
|
1784
2277
|
return [];
|
|
1785
2278
|
}
|
|
1786
2279
|
const reachable = new Set(
|
|
@@ -1788,7 +2281,8 @@ function selectResolutionCandidateInputs(
|
|
|
1788
2281
|
derivationIdentity(state, source),
|
|
1789
2282
|
),
|
|
1790
2283
|
);
|
|
1791
|
-
const output: string[] = [];
|
|
2284
|
+
const output: string[] = [...graph.resolutionInputs];
|
|
2285
|
+
if (props.result.graph.candidates === undefined) return output;
|
|
1792
2286
|
for (const entry of graph.candidates) {
|
|
1793
2287
|
if (!reachable.has(entry.source)) {
|
|
1794
2288
|
continue;
|
|
@@ -1963,9 +2457,11 @@ function declaresCompleteDependencies(
|
|
|
1963
2457
|
}
|
|
1964
2458
|
|
|
1965
2459
|
/**
|
|
1966
|
-
* Extract the absolute, deduplicated dependency list for a
|
|
1967
|
-
* compiler result. Mirrors
|
|
1968
|
-
*
|
|
2460
|
+
* Extract the absolute, lexical-spelling-deduplicated dependency list for a
|
|
2461
|
+
* single file from the compiler result. Mirrors
|
|
2462
|
+
* {@link selectTransformedSource}'s key lookup: fast project-relative match
|
|
2463
|
+
* first, then a per-envelope identity index. Distinct lexical aliases must
|
|
2464
|
+
* survive so bundlers observe a later symlink or junction retarget. Returns an
|
|
1969
2465
|
* empty list on exceptions or when the plugin reported nothing.
|
|
1970
2466
|
*/
|
|
1971
2467
|
function selectFileDependencies(props: {
|
|
@@ -2002,17 +2498,15 @@ function selectFileDependencies(props: {
|
|
|
2002
2498
|
}
|
|
2003
2499
|
const output: string[] = [];
|
|
2004
2500
|
const seen = new Set<string>();
|
|
2005
|
-
const fileIdentity = derivationIdentity(state, props.file);
|
|
2006
2501
|
for (const entry of entries) {
|
|
2007
2502
|
if (typeof entry !== "string" || entry.length === 0) {
|
|
2008
2503
|
continue;
|
|
2009
2504
|
}
|
|
2010
2505
|
const absolute = path.resolve(props.projectRoot, entry);
|
|
2011
|
-
|
|
2012
|
-
if (identity === fileIdentity || seen.has(identity)) {
|
|
2506
|
+
if (seen.has(absolute)) {
|
|
2013
2507
|
continue;
|
|
2014
2508
|
}
|
|
2015
|
-
seen.add(
|
|
2509
|
+
seen.add(absolute);
|
|
2016
2510
|
output.push(absolute);
|
|
2017
2511
|
}
|
|
2018
2512
|
return output;
|
|
@@ -2141,6 +2635,10 @@ function matchesCachedSource(
|
|
|
2141
2635
|
// out-of-walk snapshot — before any of this pass's deliveries may be
|
|
2142
2636
|
// settled against it. That proof is what a per-pass recompile used to buy
|
|
2143
2637
|
// (samchon/ttsc#1300), at a walk instead of a compile.
|
|
2638
|
+
refreshFilesystemClockReference(
|
|
2639
|
+
TRANSFORM_CLOCK_REFERENCE_DIRECTORIES.get(cached),
|
|
2640
|
+
resultFilesystem(cached.result),
|
|
2641
|
+
);
|
|
2144
2642
|
if (!matchesCompleteInputSnapshot(cached, currentKey, source)) {
|
|
2145
2643
|
return false;
|
|
2146
2644
|
}
|
|
@@ -2152,6 +2650,10 @@ function matchesCachedSource(
|
|
|
2152
2650
|
return true;
|
|
2153
2651
|
}
|
|
2154
2652
|
}
|
|
2653
|
+
refreshFilesystemClockReference(
|
|
2654
|
+
TRANSFORM_CLOCK_REFERENCE_DIRECTORIES.get(cached),
|
|
2655
|
+
resultFilesystem(cached.result),
|
|
2656
|
+
);
|
|
2155
2657
|
if (
|
|
2156
2658
|
cached.result.type !== "exception" &&
|
|
2157
2659
|
cached.result.graph !== undefined &&
|
|
@@ -2225,20 +2727,21 @@ function matchesNarrowPersistentInputs(
|
|
|
2225
2727
|
|
|
2226
2728
|
/**
|
|
2227
2729
|
* Validate one derived input against the generation, skipping the content read
|
|
2228
|
-
* while the recorded metadata signature still holds
|
|
2730
|
+
* while the recorded metadata signature still holds and its freshly minted
|
|
2731
|
+
* clock reference remains safe.
|
|
2229
2732
|
*
|
|
2230
2733
|
* Sibling deliveries of one generation share most of their derived inputs, and
|
|
2231
2734
|
* `graph.globals` is shared by every one of them, so re-reading and re-hashing
|
|
2232
2735
|
* the whole derived set per delivery multiplies one generation's proven bytes
|
|
2233
2736
|
* by the module count. The derived set is proven the same way the universal
|
|
2234
2737
|
* descriptor inputs are ({@link matchesUniversalHostInputs}), under the same
|
|
2235
|
-
* rules:
|
|
2236
|
-
* signature change falls back to the full
|
|
2237
|
-
* only around a read nothing raced, only
|
|
2238
|
-
*
|
|
2239
|
-
* filesystem's own clock has provably
|
|
2240
|
-
* ({@link stampSeparable}), so a same-length rewrite
|
|
2241
|
-
* hide behind an unchanged signature.
|
|
2738
|
+
* rules: a currently separable unchanged signature stands in for the content
|
|
2739
|
+
* comparison, and any signature or clock-ordering change falls back to the full
|
|
2740
|
+
* comparison. A signature is recorded only around a read nothing raced, only
|
|
2741
|
+
* for a recorded state that came from reading the input rather than from
|
|
2742
|
+
* failing to, and only while the observed filesystem's own clock has provably
|
|
2743
|
+
* left the stamp's tick ({@link stampSeparable}), so a same-length rewrite
|
|
2744
|
+
* inside that tick cannot hide behind an unchanged signature.
|
|
2242
2745
|
*
|
|
2243
2746
|
* The signature carries the physical identity of both the lexical path and its
|
|
2244
2747
|
* link target ({@link inputMetadataSignature}), so retargeting a symlink or
|
|
@@ -2249,6 +2752,17 @@ function matchesProvenInput(
|
|
|
2249
2752
|
state: TtscEnvelopeDerivation,
|
|
2250
2753
|
input: string,
|
|
2251
2754
|
): boolean {
|
|
2755
|
+
const observation = cached.externalInputObservations?.[path.resolve(input)];
|
|
2756
|
+
if (observation !== undefined) {
|
|
2757
|
+
if (
|
|
2758
|
+
observation.fileExists === false &&
|
|
2759
|
+
Object.keys(observation).length === 1 &&
|
|
2760
|
+
notifiesAbsence(cached, input)
|
|
2761
|
+
) {
|
|
2762
|
+
return true;
|
|
2763
|
+
}
|
|
2764
|
+
return matchesRecordedInput(cached, input);
|
|
2765
|
+
}
|
|
2252
2766
|
const slot = inputSignatureSlot(cached, state, input);
|
|
2253
2767
|
if (slot === undefined) {
|
|
2254
2768
|
return matchesRecordedInput(cached, input);
|
|
@@ -2263,7 +2777,11 @@ function matchesProvenInput(
|
|
|
2263
2777
|
}
|
|
2264
2778
|
const filesystem = resultFilesystem(cached.result);
|
|
2265
2779
|
const before = inputMetadataEvidence(input, filesystem);
|
|
2266
|
-
if (
|
|
2780
|
+
if (
|
|
2781
|
+
before !== undefined &&
|
|
2782
|
+
before.separable &&
|
|
2783
|
+
slot.signatures[slot.key] === before.signature
|
|
2784
|
+
) {
|
|
2267
2785
|
return true;
|
|
2268
2786
|
}
|
|
2269
2787
|
if (!matchesRecordedInput(cached, input)) {
|
|
@@ -2325,6 +2843,9 @@ function inputSignatureSlot(
|
|
|
2325
2843
|
| { key: string; recorded: string; signatures: Record<string, string> }
|
|
2326
2844
|
| undefined {
|
|
2327
2845
|
const identity = derivationIdentity(state, input);
|
|
2846
|
+
if (cached.externalInputObservations?.[path.resolve(input)] !== undefined) {
|
|
2847
|
+
return undefined;
|
|
2848
|
+
}
|
|
2328
2849
|
const external = cached.externalInputHashes ?? {};
|
|
2329
2850
|
if (Object.prototype.hasOwnProperty.call(external, identity)) {
|
|
2330
2851
|
// The recorded hash is identity-keyed because aliases of one physical file
|
|
@@ -2384,7 +2905,8 @@ function matchesUniversalHostInputEntries(
|
|
|
2384
2905
|
const evidence = inputMetadataEvidence(entry.path, filesystem);
|
|
2385
2906
|
if (
|
|
2386
2907
|
entry.signature !== undefined &&
|
|
2387
|
-
evidence?.signature === entry.signature
|
|
2908
|
+
evidence?.signature === entry.signature &&
|
|
2909
|
+
(entry.strict === true || evidence.separable)
|
|
2388
2910
|
)
|
|
2389
2911
|
continue;
|
|
2390
2912
|
if (entry.strict === true) return false;
|
|
@@ -2644,8 +3166,8 @@ function captureUniversalHostInputValidation(
|
|
|
2644
3166
|
const MISSING_INPUT_STATE = "missing";
|
|
2645
3167
|
|
|
2646
3168
|
/**
|
|
2647
|
-
* The
|
|
2648
|
-
*
|
|
3169
|
+
* The current stamp an adapter-owned probe has minted, keyed by the operations
|
|
3170
|
+
* object that observes it and, inside, by reporting device.
|
|
2649
3171
|
*
|
|
2650
3172
|
* A filesystem stamps a write once per clock tick, so two same-length writes
|
|
2651
3173
|
* inside one tick are indistinguishable by metadata alone. A signature may
|
|
@@ -2653,72 +3175,46 @@ const MISSING_INPUT_STATE = "missing";
|
|
|
2653
3175
|
* it, and that guarantee needs a reference instant the observed filesystem
|
|
2654
3176
|
* itself produced: once some stamp on the same device is strictly newer than an
|
|
2655
3177
|
* input's modification stamp, that input's tick is provably over, so any later
|
|
2656
|
-
* write must mint a newer stamp and move the signature.
|
|
2657
|
-
* racily-clean index rule
|
|
2658
|
-
*
|
|
2659
|
-
* stamp the cache-owned operations report, seeded per generation by
|
|
2660
|
-
* {@link mintFilesystemClockReference}.
|
|
3178
|
+
* write must mint a newer stamp and move the signature. This adapts git's
|
|
3179
|
+
* racily-clean index rule: both sides of the comparison come from the same
|
|
3180
|
+
* reporting device, and the newer side is a write the adapter itself made.
|
|
2661
3181
|
*
|
|
2662
|
-
*
|
|
2663
|
-
*
|
|
2664
|
-
*
|
|
2665
|
-
*
|
|
3182
|
+
* A timestamp merely observed on another input cannot advance the reference.
|
|
3183
|
+
* Tools may assign modification times, and a filesystem clock can move
|
|
3184
|
+
* backwards independently of the process clock. Neither a passive historical
|
|
3185
|
+
* maximum nor `Date.now()` proves what stamp a write would mint now. The probe
|
|
3186
|
+
* is therefore rewritten immediately before every validation that may reuse a
|
|
3187
|
+
* content signature, and its previous reference is cleared before the write. A
|
|
3188
|
+
* failed refresh or a different reporting device declines the optimization and
|
|
3189
|
+
* retains the content comparison (samchon/ttsc#1344).
|
|
2666
3190
|
*
|
|
2667
|
-
*
|
|
2668
|
-
*
|
|
2669
|
-
*
|
|
2670
|
-
*
|
|
2671
|
-
*
|
|
2672
|
-
* copy from a machine whose clock ran ahead — pushes its device's floor past
|
|
2673
|
-
* the present and reopens the same-tick window for every other input on that
|
|
2674
|
-
* device until the clock catches up. A clock that jumps backwards strands the
|
|
2675
|
-
* floor above the present the same way, a different hazard from the constant
|
|
2676
|
-
* offset the paragraph above is about: an offset moves both operands together
|
|
2677
|
-
* and changes nothing, a jump moves only the present.
|
|
2678
|
-
*
|
|
2679
|
-
* The minted probe is not enough on its own to replace observed stamps: it
|
|
2680
|
-
* lands on the scratch volume, which is frequently not the inputs' volume (a
|
|
2681
|
-
* project on `D:` with `TEMP` on `C:`), and a probe-only floor would then
|
|
2682
|
-
* decline every _content_ signature, so every input carrying bytes would be
|
|
2683
|
-
* re-read on every delivery. A strict blocker keeps its signature either way,
|
|
2684
|
-
* because it proves a kind rather than content. Observed stamps keep the common
|
|
2685
|
-
* case working; the probe covers the case they cannot, a tree whose files were
|
|
2686
|
-
* all written inside one tick.
|
|
3191
|
+
* The probe lives in an adapter-owned temporary directory outside the project.
|
|
3192
|
+
* That directory may be on another volume, such as `C:` when a project lives on
|
|
3193
|
+
* `D:`. Such a split-volume generation safely keeps reading content because no
|
|
3194
|
+
* same-device reference exists; it never substitutes a process-clock guess or
|
|
3195
|
+
* writes a probe into the user's project.
|
|
2687
3196
|
*/
|
|
2688
|
-
const
|
|
3197
|
+
const FILESYSTEM_CLOCK_REFERENCES = new WeakMap<
|
|
2689
3198
|
TtscTransformFilesystemOperations,
|
|
2690
3199
|
Map<bigint, bigint>
|
|
2691
3200
|
>();
|
|
2692
3201
|
|
|
2693
|
-
/** Return one observed filesystem's per-device
|
|
2694
|
-
function
|
|
3202
|
+
/** Return one observed filesystem's current per-device references. */
|
|
3203
|
+
function filesystemClockReferences(
|
|
2695
3204
|
filesystem: TtscTransformFilesystemOperations,
|
|
2696
3205
|
): Map<bigint, bigint> {
|
|
2697
|
-
let
|
|
2698
|
-
if (
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
}
|
|
2702
|
-
return floors;
|
|
2703
|
-
}
|
|
2704
|
-
|
|
2705
|
-
/** Raise a device's clock floor with the stamps one observation reported. */
|
|
2706
|
-
function observeFilesystemClock(
|
|
2707
|
-
filesystem: TtscTransformFilesystemOperations,
|
|
2708
|
-
stats: fs.BigIntStats,
|
|
2709
|
-
): void {
|
|
2710
|
-
const floors = filesystemClockFloors(filesystem);
|
|
2711
|
-
const stamp = stats.mtimeNs > stats.ctimeNs ? stats.mtimeNs : stats.ctimeNs;
|
|
2712
|
-
const current = floors.get(stats.dev);
|
|
2713
|
-
if (current === undefined || stamp > current) {
|
|
2714
|
-
floors.set(stats.dev, stamp);
|
|
3206
|
+
let references = FILESYSTEM_CLOCK_REFERENCES.get(filesystem);
|
|
3207
|
+
if (references === undefined) {
|
|
3208
|
+
references = new Map();
|
|
3209
|
+
FILESYSTEM_CLOCK_REFERENCES.set(filesystem, references);
|
|
2715
3210
|
}
|
|
3211
|
+
return references;
|
|
2716
3212
|
}
|
|
2717
3213
|
|
|
2718
3214
|
/**
|
|
2719
3215
|
* Report whether a later write to the observed path is guaranteed to move its
|
|
2720
|
-
* modification stamp: the device's
|
|
2721
|
-
* the tick that minted the stamp is provably over. The
|
|
3216
|
+
* modification stamp: the device's current probe holds a stamp strictly newer,
|
|
3217
|
+
* so the tick that minted the stamp is provably over. The probe was written
|
|
2722
3218
|
* before the caller's content read began, which is the ordering the guarantee
|
|
2723
3219
|
* needs — a stamp minted before the read proves every post-read write lands in
|
|
2724
3220
|
* a newer tick.
|
|
@@ -2727,42 +3223,62 @@ function stampSeparable(
|
|
|
2727
3223
|
filesystem: TtscTransformFilesystemOperations,
|
|
2728
3224
|
stats: fs.BigIntStats,
|
|
2729
3225
|
): boolean {
|
|
2730
|
-
const
|
|
2731
|
-
return
|
|
3226
|
+
const reference = filesystemClockReferences(filesystem).get(stats.dev);
|
|
3227
|
+
return reference !== undefined && stats.mtimeNs < reference;
|
|
2732
3228
|
}
|
|
2733
3229
|
|
|
2734
3230
|
/**
|
|
2735
|
-
*
|
|
2736
|
-
* filesystem's clock floor.
|
|
3231
|
+
* Replace every prior clock proof with one reference minted by a fresh write.
|
|
2737
3232
|
*
|
|
2738
|
-
* The
|
|
3233
|
+
* The reference directory is storage the adapter already owns, deliberately
|
|
2739
3234
|
* outside the project root, so stamping a probe file there produces a
|
|
2740
3235
|
* freshly-minted "now" without touching the user's project — the analogue of
|
|
2741
3236
|
* git writing its index. The probe is observed through the cache-owned
|
|
2742
3237
|
* operations and keyed by the device those operations report, so it only ever
|
|
2743
|
-
* separates stamps on the filesystem that actually minted it
|
|
2744
|
-
*
|
|
2745
|
-
*
|
|
2746
|
-
* declined until passively observed stamps separate an input on their own.
|
|
3238
|
+
* separates stamps on the filesystem that actually minted it. When its volume
|
|
3239
|
+
* differs from the inputs' volume, or the observed filesystem cannot see the
|
|
3240
|
+
* probe at all, nothing is proven and content comparison continues.
|
|
2747
3241
|
*
|
|
2748
|
-
*
|
|
2749
|
-
*
|
|
2750
|
-
*
|
|
2751
|
-
*
|
|
2752
|
-
* property of this rule, so the cross-volume case degrades to more reads here
|
|
2753
|
-
* rather than being bought with it.
|
|
3242
|
+
* Forcing the reference onto the inputs' volume would require writing into the
|
|
3243
|
+
* user's project or an otherwise unowned neighboring directory. That is not a
|
|
3244
|
+
* valid price for this optimization, so the cross-volume case degrades to more
|
|
3245
|
+
* reads instead.
|
|
2754
3246
|
*/
|
|
2755
|
-
function
|
|
2756
|
-
|
|
3247
|
+
function refreshFilesystemClockReference(
|
|
3248
|
+
referenceDirectory: string | undefined,
|
|
2757
3249
|
filesystem: TtscTransformFilesystemOperations,
|
|
2758
3250
|
): void {
|
|
3251
|
+
const references = filesystemClockReferences(filesystem);
|
|
3252
|
+
references.clear();
|
|
3253
|
+
if (referenceDirectory === undefined) return;
|
|
3254
|
+
try {
|
|
3255
|
+
const probe = path.join(referenceDirectory, "clock-reference");
|
|
3256
|
+
fs.writeFileSync(probe, `${process.hrtime.bigint()}\n`);
|
|
3257
|
+
const stats = filesystem.lstat(probe);
|
|
3258
|
+
if (stats.isFile()) {
|
|
3259
|
+
references.set(stats.dev, stats.mtimeNs);
|
|
3260
|
+
}
|
|
3261
|
+
} catch {
|
|
3262
|
+
// A failed refresh leaves no reference, so content comparison continues.
|
|
3263
|
+
references.clear();
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
/** Remove only the known probe and its now-empty owned directory. */
|
|
3268
|
+
function disposeFilesystemClockReference(referenceDirectory: string): void {
|
|
3269
|
+
try {
|
|
3270
|
+
fs.rmSync(path.join(referenceDirectory, "clock-reference"), {
|
|
3271
|
+
force: true,
|
|
3272
|
+
});
|
|
3273
|
+
} catch {
|
|
3274
|
+
// The probe may already have disappeared; the directory removal below is
|
|
3275
|
+
// still safe because it is deliberately non-recursive.
|
|
3276
|
+
}
|
|
2759
3277
|
try {
|
|
2760
|
-
|
|
2761
|
-
fs.writeFileSync(probe, "");
|
|
2762
|
-
observeFilesystemClock(filesystem, filesystem.lstat(probe));
|
|
3278
|
+
fs.rmdirSync(referenceDirectory);
|
|
2763
3279
|
} catch {
|
|
2764
|
-
//
|
|
2765
|
-
//
|
|
3280
|
+
// Eviction schedules cleanup without awaiting its Promise. A foreign entry
|
|
3281
|
+
// or a concurrent removal leaves, at worst, an empty temporary directory.
|
|
2766
3282
|
}
|
|
2767
3283
|
}
|
|
2768
3284
|
|
|
@@ -2797,12 +3313,10 @@ function inputMetadataEvidence(
|
|
|
2797
3313
|
): TtscInputMetadataEvidence | undefined {
|
|
2798
3314
|
try {
|
|
2799
3315
|
const link = filesystem.lstat(file);
|
|
2800
|
-
observeFilesystemClock(filesystem, link);
|
|
2801
3316
|
let target = link;
|
|
2802
3317
|
if (link.isSymbolicLink()) {
|
|
2803
3318
|
try {
|
|
2804
3319
|
target = filesystem.statBigInt(file);
|
|
2805
|
-
observeFilesystemClock(filesystem, target);
|
|
2806
3320
|
} catch {
|
|
2807
3321
|
// Keep a broken link in the existing-input manifest. Its own metadata
|
|
2808
3322
|
// stays stable while the target is missing, and the first successful
|
|
@@ -2873,7 +3387,21 @@ function graphInputStateHash(
|
|
|
2873
3387
|
file: string,
|
|
2874
3388
|
filesystem: TtscTransformFilesystemOperations = DEFAULT_FILESYSTEM_OPERATIONS,
|
|
2875
3389
|
): string | null {
|
|
3390
|
+
const kind = compilerStatKind(file, filesystem);
|
|
3391
|
+
const readHash = graphInputReadHash(file, filesystem, kind);
|
|
3392
|
+
if (readHash !== null) return readHash;
|
|
3393
|
+
return kind === "directory" ? hashText("ttsc:host-input:directory\0") : null;
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
/** Hash only a successful compiler-style ReadFile result, without kind fallback. */
|
|
3397
|
+
function graphInputReadHash(
|
|
3398
|
+
file: string,
|
|
3399
|
+
filesystem: TtscTransformFilesystemOperations = DEFAULT_FILESYSTEM_OPERATIONS,
|
|
3400
|
+
observedKind?: "directory" | "file" | "missing",
|
|
3401
|
+
): string | null {
|
|
3402
|
+
const kind = observedKind ?? compilerStatKind(file, filesystem);
|
|
2876
3403
|
try {
|
|
3404
|
+
if (kind === "directory") return null;
|
|
2877
3405
|
const bytes = filesystem.readFile(file);
|
|
2878
3406
|
if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
|
|
2879
3407
|
const even = bytes.subarray(
|
|
@@ -2898,14 +3426,179 @@ function graphInputStateHash(
|
|
|
2898
3426
|
: bytes;
|
|
2899
3427
|
return hashText(content);
|
|
2900
3428
|
} catch {
|
|
3429
|
+
return null;
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
/** Current result of TypeScript-Go's Stat predicate. */
|
|
3434
|
+
function compilerStatKind(
|
|
3435
|
+
file: string,
|
|
3436
|
+
filesystem: TtscTransformFilesystemOperations,
|
|
3437
|
+
): "directory" | "file" | "missing" {
|
|
3438
|
+
try {
|
|
3439
|
+
return filesystem.stat(file).isDirectory() ? "directory" : "file";
|
|
3440
|
+
} catch {
|
|
3441
|
+
return "missing";
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
/** Return the exact recorded predicates that no longer hold for one spelling. */
|
|
3446
|
+
function graphInputObservationFailures(
|
|
3447
|
+
file: string,
|
|
3448
|
+
observation: ITtscCompilerTransformation.IInputObservation,
|
|
3449
|
+
filesystem: TtscTransformFilesystemOperations,
|
|
3450
|
+
identities: FilesystemPathIdentityContext,
|
|
3451
|
+
): string[] {
|
|
3452
|
+
const failures: string[] = [];
|
|
3453
|
+
if (observation.accessibleEntries !== undefined) {
|
|
3454
|
+
const current = compilerAccessibleEntries(file, filesystem);
|
|
3455
|
+
if (
|
|
3456
|
+
JSON.stringify(current) !== JSON.stringify(observation.accessibleEntries)
|
|
3457
|
+
) {
|
|
3458
|
+
failures.push("accessible-entries-changed");
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
const kind =
|
|
3462
|
+
observation.fileExists !== undefined ||
|
|
3463
|
+
observation.directoryExists !== undefined ||
|
|
3464
|
+
observation.stat !== undefined ||
|
|
3465
|
+
observation.readFile !== undefined
|
|
3466
|
+
? compilerStatKind(file, filesystem)
|
|
3467
|
+
: undefined;
|
|
3468
|
+
if (
|
|
3469
|
+
observation.fileExists !== undefined &&
|
|
3470
|
+
(kind === "file") !== observation.fileExists
|
|
3471
|
+
) {
|
|
3472
|
+
failures.push("file-exists-changed");
|
|
3473
|
+
}
|
|
3474
|
+
if (
|
|
3475
|
+
observation.directoryExists !== undefined &&
|
|
3476
|
+
(kind === "directory") !== observation.directoryExists
|
|
3477
|
+
) {
|
|
3478
|
+
failures.push("directory-exists-changed");
|
|
3479
|
+
}
|
|
3480
|
+
if (observation.stat !== undefined && kind !== observation.stat) {
|
|
3481
|
+
failures.push("stat-changed");
|
|
3482
|
+
}
|
|
3483
|
+
if (observation.readFile !== undefined) {
|
|
3484
|
+
const currentHash = graphInputReadHash(file, filesystem, kind);
|
|
3485
|
+
if (
|
|
3486
|
+
(observation.readFile.ok && currentHash !== observation.readFile.hash) ||
|
|
3487
|
+
(!observation.readFile.ok && currentHash !== null)
|
|
3488
|
+
) {
|
|
3489
|
+
failures.push("read-file-changed");
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
if (observation.realpath !== undefined) {
|
|
3493
|
+
const currentRealpath = compilerInputRealpathObservation(file, filesystem);
|
|
3494
|
+
if (
|
|
3495
|
+
observation.realpath.ok !== currentRealpath.ok ||
|
|
3496
|
+
(observation.realpath.ok &&
|
|
3497
|
+
currentRealpath.ok &&
|
|
3498
|
+
!sameHostInputRealpath(
|
|
3499
|
+
observation.realpath.path,
|
|
3500
|
+
currentRealpath.path,
|
|
3501
|
+
identities,
|
|
3502
|
+
))
|
|
3503
|
+
) {
|
|
3504
|
+
failures.push("realpath-changed");
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
return failures;
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
/** Replay TypeScript-Go's accessible-entry classification and sorted order. */
|
|
3511
|
+
function compilerAccessibleEntries(
|
|
3512
|
+
directory: string,
|
|
3513
|
+
filesystem: TtscTransformFilesystemOperations,
|
|
3514
|
+
): NonNullable<
|
|
3515
|
+
ITtscCompilerTransformation.IInputObservation["accessibleEntries"]
|
|
3516
|
+
> {
|
|
3517
|
+
const directories: string[] = [];
|
|
3518
|
+
const files: string[] = [];
|
|
3519
|
+
const pathApi = filesystem.platform === "win32" ? path.win32 : path.posix;
|
|
3520
|
+
let entries: fs.Dirent[];
|
|
3521
|
+
try {
|
|
3522
|
+
entries = filesystem.readdir(directory);
|
|
3523
|
+
} catch {
|
|
3524
|
+
return { directories, files };
|
|
3525
|
+
}
|
|
3526
|
+
for (const entry of entries) {
|
|
3527
|
+
if (entry.isDirectory()) {
|
|
3528
|
+
directories.push(entry.name);
|
|
3529
|
+
continue;
|
|
3530
|
+
}
|
|
3531
|
+
if (entry.isFile()) {
|
|
3532
|
+
files.push(entry.name);
|
|
3533
|
+
continue;
|
|
3534
|
+
}
|
|
3535
|
+
if (!entry.isSymbolicLink()) continue;
|
|
2901
3536
|
try {
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
3537
|
+
const target = filesystem.stat(pathApi.join(directory, entry.name));
|
|
3538
|
+
if (target.isDirectory()) directories.push(entry.name);
|
|
3539
|
+
else if (target.isFile()) files.push(entry.name);
|
|
2905
3540
|
} catch {
|
|
2906
|
-
|
|
3541
|
+
// TypeScript-Go omits inaccessible and broken linked entries.
|
|
2907
3542
|
}
|
|
2908
3543
|
}
|
|
3544
|
+
directories.sort();
|
|
3545
|
+
files.sort();
|
|
3546
|
+
return { directories, files };
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
/** Validate one predicate-preserving graph proof against a filesystem view. */
|
|
3550
|
+
export function validateGraphInputObservation(
|
|
3551
|
+
file: string,
|
|
3552
|
+
observation: ITtscCompilerTransformation.IInputObservation,
|
|
3553
|
+
filesystem: TtscTransformFilesystemOperations = DEFAULT_FILESYSTEM_OPERATIONS,
|
|
3554
|
+
): string[] {
|
|
3555
|
+
const normalized = normalizeGraphInputObservation(
|
|
3556
|
+
observation,
|
|
3557
|
+
filesystem.platform,
|
|
3558
|
+
);
|
|
3559
|
+
if (normalized === undefined) return ["proof-conflict"];
|
|
3560
|
+
return graphInputObservationFailures(
|
|
3561
|
+
file,
|
|
3562
|
+
normalized,
|
|
3563
|
+
filesystem,
|
|
3564
|
+
createHostPathIdentityContext(filesystem),
|
|
3565
|
+
);
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
/** Whether every compiler-time predicate still returns its recorded value. */
|
|
3569
|
+
function matchesGraphInputObservation(
|
|
3570
|
+
file: string,
|
|
3571
|
+
observation: ITtscCompilerTransformation.IInputObservation,
|
|
3572
|
+
filesystem: TtscTransformFilesystemOperations,
|
|
3573
|
+
identities: FilesystemPathIdentityContext,
|
|
3574
|
+
): boolean {
|
|
3575
|
+
return (
|
|
3576
|
+
graphInputObservationFailures(file, observation, filesystem, identities)
|
|
3577
|
+
.length === 0
|
|
3578
|
+
);
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
/** Replay TypeScript-Go's Realpath result, including its lexical fallback. */
|
|
3582
|
+
function compilerInputRealpathObservation(
|
|
3583
|
+
file: string,
|
|
3584
|
+
filesystem: TtscTransformFilesystemOperations,
|
|
3585
|
+
): NonNullable<ITtscCompilerTransformation.IInputObservation["realpath"]> {
|
|
3586
|
+
try {
|
|
3587
|
+
const realpath = filesystem.realpath(file);
|
|
3588
|
+
return realpath.length === 0
|
|
3589
|
+
? { ok: false }
|
|
3590
|
+
: {
|
|
3591
|
+
ok: true,
|
|
3592
|
+
path: resolveFilesystemPath(realpath, filesystem.platform),
|
|
3593
|
+
};
|
|
3594
|
+
} catch {
|
|
3595
|
+
// TypeScript-Go's OS and io filesystems return the cleaned input spelling
|
|
3596
|
+
// when native realpath resolution fails; they do not expose the failure.
|
|
3597
|
+
return {
|
|
3598
|
+
ok: true,
|
|
3599
|
+
path: resolveFilesystemPath(file, filesystem.platform),
|
|
3600
|
+
};
|
|
3601
|
+
}
|
|
2909
3602
|
}
|
|
2910
3603
|
|
|
2911
3604
|
/** Physical target selected by a lexical host-input path. */
|
|
@@ -3093,6 +3786,9 @@ function matchesExternalInputRealpaths(
|
|
|
3093
3786
|
const state = envelopeDerivation(cached);
|
|
3094
3787
|
const filesystem = resultFilesystem(cached.result);
|
|
3095
3788
|
for (const input of cached.externalInputPaths ?? []) {
|
|
3789
|
+
if (cached.externalInputObservations?.[path.resolve(input)] !== undefined) {
|
|
3790
|
+
continue;
|
|
3791
|
+
}
|
|
3096
3792
|
const identity = derivationIdentity(state, input);
|
|
3097
3793
|
if (!Object.prototype.hasOwnProperty.call(expected, identity)) continue;
|
|
3098
3794
|
if (
|
|
@@ -3122,6 +3818,7 @@ function captureExternalInputSnapshot(
|
|
|
3122
3818
|
complete: boolean;
|
|
3123
3819
|
failures: TtscGenerationProofFailures;
|
|
3124
3820
|
hashes: Record<string, string>;
|
|
3821
|
+
observations: Record<string, ITtscCompilerTransformation.IInputObservation>;
|
|
3125
3822
|
realpaths: Record<string, string | null>;
|
|
3126
3823
|
signatures: Record<string, string>;
|
|
3127
3824
|
} {
|
|
@@ -3131,17 +3828,19 @@ function captureExternalInputSnapshot(
|
|
|
3131
3828
|
// A non-declaration transform output is a compiler-realized source even when
|
|
3132
3829
|
// a malformed or legacy graph omitted its node. Its output was computed from
|
|
3133
3830
|
// compiler-time bytes, so a post-compile host read cannot prove coherence.
|
|
3134
|
-
const
|
|
3831
|
+
const transformSourceSpellings = new Set<string>();
|
|
3135
3832
|
if (cached.result.type === "success") {
|
|
3136
3833
|
for (const output of Object.keys(cached.result.typescript)) {
|
|
3137
3834
|
if (!isDeclarationFile(output)) {
|
|
3138
|
-
|
|
3139
|
-
derivationIdentity(state, path.resolve(cached.projectRoot, output)),
|
|
3140
|
-
);
|
|
3835
|
+
transformSourceSpellings.add(path.resolve(cached.projectRoot, output));
|
|
3141
3836
|
}
|
|
3142
3837
|
}
|
|
3143
3838
|
}
|
|
3144
3839
|
const hashes: Record<string, string> = {};
|
|
3840
|
+
const observations: Record<
|
|
3841
|
+
string,
|
|
3842
|
+
ITtscCompilerTransformation.IInputObservation
|
|
3843
|
+
> = {};
|
|
3145
3844
|
const realpaths: Record<string, string | null> = {};
|
|
3146
3845
|
const signatures: Record<string, string> = {};
|
|
3147
3846
|
const failures = createGenerationProofFailures();
|
|
@@ -3162,29 +3861,64 @@ function captureExternalInputSnapshot(
|
|
|
3162
3861
|
};
|
|
3163
3862
|
for (const input of paths) {
|
|
3164
3863
|
const identity = derivationIdentity(state, input);
|
|
3165
|
-
|
|
3864
|
+
const spelling = path.resolve(input);
|
|
3865
|
+
const predicateObservation = graph.inputObservations.get(spelling);
|
|
3866
|
+
const predicateConflict = graph.inputObservationConflicts.has(spelling);
|
|
3867
|
+
if (
|
|
3868
|
+
graph.speculative.has(spelling) &&
|
|
3869
|
+
(predicateObservation !== undefined || predicateConflict)
|
|
3870
|
+
) {
|
|
3871
|
+
if (predicateConflict || predicateObservation === undefined) {
|
|
3872
|
+
complete = false;
|
|
3873
|
+
recordGenerationProofFailure(failures, {
|
|
3874
|
+
domain: "external",
|
|
3875
|
+
kind: "graph-proof-conflict",
|
|
3876
|
+
detail: graph.inputProofFailures.get(spelling),
|
|
3877
|
+
path: input,
|
|
3878
|
+
});
|
|
3879
|
+
continue;
|
|
3880
|
+
}
|
|
3881
|
+
const mismatches = graphInputObservationFailures(
|
|
3882
|
+
input,
|
|
3883
|
+
predicateObservation,
|
|
3884
|
+
filesystem,
|
|
3885
|
+
state.identityContext,
|
|
3886
|
+
);
|
|
3887
|
+
if (mismatches.length !== 0) complete = false;
|
|
3888
|
+
for (const kind of mismatches) {
|
|
3889
|
+
recordGenerationProofFailure(failures, {
|
|
3890
|
+
domain: "external",
|
|
3891
|
+
kind: `graph-${kind}`,
|
|
3892
|
+
path: input,
|
|
3893
|
+
});
|
|
3894
|
+
}
|
|
3895
|
+
observations[spelling] = predicateObservation;
|
|
3896
|
+
continue;
|
|
3897
|
+
}
|
|
3898
|
+
// A member the envelope reported only as a resolver input falls
|
|
3166
3899
|
// through to the recorded-state branch below, the same evidence a
|
|
3167
3900
|
// plugin-declared dependency path carries. Its absence still invalidates
|
|
3168
3901
|
// the generation when it appears, because `missing` is recorded state.
|
|
3169
|
-
const realizedTransformSource =
|
|
3902
|
+
const realizedTransformSource = transformSourceSpellings.has(spelling);
|
|
3170
3903
|
const speculativeOnly =
|
|
3171
3904
|
!realizedTransformSource &&
|
|
3172
|
-
graph.speculative.has(
|
|
3173
|
-
!graph.inputProofs.has(
|
|
3174
|
-
!graph.inputProofConflicts.has(
|
|
3905
|
+
graph.speculative.has(spelling) &&
|
|
3906
|
+
!graph.inputProofs.has(spelling) &&
|
|
3907
|
+
!graph.inputProofConflicts.has(spelling) &&
|
|
3908
|
+
!graph.inputProofFailures.has(spelling);
|
|
3175
3909
|
if (
|
|
3176
|
-
(realizedTransformSource || graph.
|
|
3910
|
+
(realizedTransformSource || graph.memberSpellings.has(spelling)) &&
|
|
3177
3911
|
!speculativeOnly
|
|
3178
3912
|
) {
|
|
3179
|
-
const proof = graph.inputProofs.get(
|
|
3180
|
-
if (proof === undefined || graph.inputProofConflicts.has(
|
|
3913
|
+
const proof = graph.inputProofs.get(spelling);
|
|
3914
|
+
if (proof === undefined || graph.inputProofConflicts.has(spelling)) {
|
|
3181
3915
|
complete = false;
|
|
3182
3916
|
recordGenerationProofFailure(failures, {
|
|
3183
3917
|
domain: "external",
|
|
3184
|
-
kind: graph.inputProofConflicts.has(
|
|
3918
|
+
kind: graph.inputProofConflicts.has(spelling)
|
|
3185
3919
|
? "graph-proof-conflict"
|
|
3186
3920
|
: "graph-proof-missing",
|
|
3187
|
-
detail: graph.inputProofFailures.get(
|
|
3921
|
+
detail: graph.inputProofFailures.get(spelling),
|
|
3188
3922
|
path: input,
|
|
3189
3923
|
});
|
|
3190
3924
|
continue;
|
|
@@ -3231,7 +3965,14 @@ function captureExternalInputSnapshot(
|
|
|
3231
3965
|
hashes[identity] = hash ?? MISSING_INPUT_STATE;
|
|
3232
3966
|
if (hash !== null) record(input, before, after);
|
|
3233
3967
|
}
|
|
3234
|
-
return {
|
|
3968
|
+
return {
|
|
3969
|
+
complete,
|
|
3970
|
+
failures,
|
|
3971
|
+
hashes,
|
|
3972
|
+
observations,
|
|
3973
|
+
realpaths,
|
|
3974
|
+
signatures,
|
|
3975
|
+
};
|
|
3235
3976
|
}
|
|
3236
3977
|
|
|
3237
3978
|
/** Explain every graph member that no longer matches the compiler's state. */
|
|
@@ -3243,6 +3984,7 @@ function compilerGraphInputProofFailures(
|
|
|
3243
3984
|
cached.result.type === "exception" ||
|
|
3244
3985
|
cached.result.graph === undefined ||
|
|
3245
3986
|
(cached.result.graph.inputHashes === undefined &&
|
|
3987
|
+
cached.result.graph.inputObservations === undefined &&
|
|
3246
3988
|
cached.result.graph.inputRealpaths === undefined &&
|
|
3247
3989
|
cached.result.graph.inputProofFailures === undefined)
|
|
3248
3990
|
) {
|
|
@@ -3251,38 +3993,105 @@ function compilerGraphInputProofFailures(
|
|
|
3251
3993
|
// where a post-compile snapshot cannot prove the compiler's generation.
|
|
3252
3994
|
return failures;
|
|
3253
3995
|
}
|
|
3254
|
-
const state = envelopeDerivation(cached);
|
|
3255
|
-
const filesystem = resultFilesystem(cached.result);
|
|
3256
|
-
const graph = envelopeGraphIndexes(state, cached);
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3996
|
+
const state = envelopeDerivation(cached);
|
|
3997
|
+
const filesystem = resultFilesystem(cached.result);
|
|
3998
|
+
const graph = envelopeGraphIndexes(state, cached);
|
|
3999
|
+
const predicateSpellings = new Set<string>();
|
|
4000
|
+
const predicateConflictSpellings = new Set<string>();
|
|
4001
|
+
for (const [spelling, observation] of graph.inputObservations) {
|
|
4002
|
+
predicateSpellings.add(spelling);
|
|
4003
|
+
for (const kind of graphInputObservationFailures(
|
|
4004
|
+
spelling,
|
|
4005
|
+
observation,
|
|
4006
|
+
filesystem,
|
|
4007
|
+
state.identityContext,
|
|
4008
|
+
)) {
|
|
4009
|
+
recordGenerationProofFailure(failures, {
|
|
4010
|
+
domain: "graph",
|
|
4011
|
+
kind,
|
|
4012
|
+
path: spelling,
|
|
4013
|
+
});
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
for (const spelling of graph.inputObservationConflicts) {
|
|
4017
|
+
predicateSpellings.add(spelling);
|
|
4018
|
+
predicateConflictSpellings.add(spelling);
|
|
4019
|
+
recordGenerationProofFailure(failures, {
|
|
4020
|
+
domain: "graph",
|
|
4021
|
+
kind: "proof-conflict",
|
|
4022
|
+
detail: graph.inputProofFailures.get(spelling),
|
|
4023
|
+
path: spelling,
|
|
4024
|
+
});
|
|
4025
|
+
}
|
|
4026
|
+
for (const spelling of graph.memberSpellings) {
|
|
4027
|
+
const proof = graph.inputProofs.get(spelling);
|
|
3261
4028
|
if (isTransformScratchInput(spelling, cached.scratchDirectory)) {
|
|
3262
4029
|
continue;
|
|
3263
4030
|
}
|
|
3264
|
-
|
|
3265
|
-
// would void every generation of every project whose resolution passes over
|
|
3266
|
-
// a higher-priority spelling, which is every project with a dependency
|
|
3267
|
-
// typed by a declaration file (samchon/ttsc#1245). It is validated instead
|
|
3268
|
-
// against the state {@link captureExternalInputSnapshot} recorded for it.
|
|
3269
|
-
if (proof === undefined && graph.speculative.has(identity)) {
|
|
4031
|
+
if (predicateConflictSpellings.has(spelling)) {
|
|
3270
4032
|
continue;
|
|
3271
4033
|
}
|
|
3272
|
-
if (graph.inputProofConflicts.has(
|
|
4034
|
+
if (graph.inputProofConflicts.has(spelling)) {
|
|
3273
4035
|
recordGenerationProofFailure(failures, {
|
|
3274
4036
|
domain: "graph",
|
|
3275
4037
|
kind: "proof-conflict",
|
|
3276
|
-
detail: graph.inputProofFailures.get(
|
|
4038
|
+
detail: graph.inputProofFailures.get(spelling),
|
|
4039
|
+
path: spelling,
|
|
4040
|
+
});
|
|
4041
|
+
continue;
|
|
4042
|
+
}
|
|
4043
|
+
if (graph.inputProofFailures.has(spelling)) {
|
|
4044
|
+
recordGenerationProofFailure(failures, {
|
|
4045
|
+
domain: "graph",
|
|
4046
|
+
kind: "proof-missing",
|
|
4047
|
+
detail: graph.inputProofFailures.get(spelling),
|
|
3277
4048
|
path: spelling,
|
|
3278
4049
|
});
|
|
3279
4050
|
continue;
|
|
3280
4051
|
}
|
|
4052
|
+
const observation = graph.inputObservations.get(spelling);
|
|
4053
|
+
if (observation !== undefined && proof !== undefined) {
|
|
4054
|
+
const projection = legacyProjectionOfGraphInputObservation(observation);
|
|
4055
|
+
if (
|
|
4056
|
+
projection.failure !== undefined ||
|
|
4057
|
+
projection.hash !== proof.hash ||
|
|
4058
|
+
!sameHostInputRealpath(
|
|
4059
|
+
projection.realpath,
|
|
4060
|
+
proof.realpath,
|
|
4061
|
+
state.identityContext,
|
|
4062
|
+
)
|
|
4063
|
+
) {
|
|
4064
|
+
recordGenerationProofFailure(failures, {
|
|
4065
|
+
domain: "graph",
|
|
4066
|
+
kind: "proof-conflict",
|
|
4067
|
+
detail: projection.failure,
|
|
4068
|
+
path: spelling,
|
|
4069
|
+
});
|
|
4070
|
+
}
|
|
4071
|
+
// The rich predicates were already replayed above. Their legacy
|
|
4072
|
+
// projection is an internal producer-consistency check, never a reason
|
|
4073
|
+
// to read the same filesystem input again. A projection that cannot
|
|
4074
|
+
// represent the observation is itself inconsistent with a supplied
|
|
4075
|
+
// legacy proof, rather than permission to trust either representation.
|
|
4076
|
+
continue;
|
|
4077
|
+
}
|
|
4078
|
+
if (
|
|
4079
|
+
graph.speculative.has(spelling) &&
|
|
4080
|
+
predicateSpellings.has(spelling) &&
|
|
4081
|
+
!graph.inputProofFailures.has(spelling)
|
|
4082
|
+
) {
|
|
4083
|
+
continue;
|
|
4084
|
+
}
|
|
4085
|
+
// A legacy sidecar can report a resolver candidate without a compiler
|
|
4086
|
+
// predicate. Validate it against the generation snapshot instead.
|
|
4087
|
+
if (proof === undefined && graph.speculative.has(spelling)) {
|
|
4088
|
+
continue;
|
|
4089
|
+
}
|
|
3281
4090
|
if (proof === undefined) {
|
|
3282
4091
|
recordGenerationProofFailure(failures, {
|
|
3283
4092
|
domain: "graph",
|
|
3284
4093
|
kind: "proof-missing",
|
|
3285
|
-
detail: graph.inputProofFailures.get(
|
|
4094
|
+
detail: graph.inputProofFailures.get(spelling),
|
|
3286
4095
|
path: spelling,
|
|
3287
4096
|
});
|
|
3288
4097
|
continue;
|
|
@@ -3331,6 +4140,15 @@ function matchesRecordedInput(
|
|
|
3331
4140
|
? cached.inputHashes[projectKey]
|
|
3332
4141
|
: undefined;
|
|
3333
4142
|
const identity = derivationIdentity(state, input);
|
|
4143
|
+
const observation = cached.externalInputObservations?.[path.resolve(input)];
|
|
4144
|
+
if (observation !== undefined) {
|
|
4145
|
+
return matchesGraphInputObservation(
|
|
4146
|
+
input,
|
|
4147
|
+
observation,
|
|
4148
|
+
filesystem,
|
|
4149
|
+
state.identityContext,
|
|
4150
|
+
);
|
|
4151
|
+
}
|
|
3334
4152
|
const externalHash = (cached.externalInputHashes ?? {})[identity];
|
|
3335
4153
|
const externalRealpaths = cached.externalInputRealpaths;
|
|
3336
4154
|
const graphInput =
|
|
@@ -3386,7 +4204,31 @@ export function collectProjectInputHashes(
|
|
|
3386
4204
|
filesystem: TtscTransformFilesystemOperations = DEFAULT_FILESYSTEM_OPERATIONS,
|
|
3387
4205
|
policy?: ITtscProjectMembershipPolicy,
|
|
3388
4206
|
): Record<string, string> {
|
|
3389
|
-
return
|
|
4207
|
+
return collectProjectInputHashSnapshot(
|
|
4208
|
+
projectRoot,
|
|
4209
|
+
identities,
|
|
4210
|
+
filesystem,
|
|
4211
|
+
policy,
|
|
4212
|
+
).hashes;
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
/** One project-walk hash set together with its completeness proof. */
|
|
4216
|
+
export interface TtscProjectInputHashSnapshot {
|
|
4217
|
+
complete: boolean;
|
|
4218
|
+
hashes: Record<string, string>;
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
/**
|
|
4222
|
+
* Hash the project walk and retain whether every attempted directory and file
|
|
4223
|
+
* was observed coherently. Cache-key hosts must reject an incomplete set.
|
|
4224
|
+
*/
|
|
4225
|
+
export function collectProjectInputHashSnapshot(
|
|
4226
|
+
projectRoot: string,
|
|
4227
|
+
identities: FilesystemPathIdentityContext = createHostPathIdentityContext(),
|
|
4228
|
+
filesystem: TtscTransformFilesystemOperations = DEFAULT_FILESYSTEM_OPERATIONS,
|
|
4229
|
+
policy?: ITtscProjectMembershipPolicy,
|
|
4230
|
+
): TtscProjectInputHashSnapshot {
|
|
4231
|
+
const snapshot = collectProjectInputSnapshot(
|
|
3390
4232
|
projectRoot,
|
|
3391
4233
|
identities,
|
|
3392
4234
|
filesystem,
|
|
@@ -3394,7 +4236,11 @@ export function collectProjectInputHashes(
|
|
|
3394
4236
|
{
|
|
3395
4237
|
policy,
|
|
3396
4238
|
},
|
|
3397
|
-
)
|
|
4239
|
+
);
|
|
4240
|
+
return {
|
|
4241
|
+
complete: snapshot.complete && snapshot.directoryComplete,
|
|
4242
|
+
hashes: snapshot.hashes,
|
|
4243
|
+
};
|
|
3398
4244
|
}
|
|
3399
4245
|
|
|
3400
4246
|
/** Hash project files and snapshot the directory topology in one walk. */
|
|
@@ -3453,11 +4299,12 @@ function collectProjectInputSnapshot(
|
|
|
3453
4299
|
const before = inputMetadataEvidence(file, filesystem);
|
|
3454
4300
|
// A file whose signature still equals the one captured around the read
|
|
3455
4301
|
// that produced the recorded hash carries that content, so the whole
|
|
3456
|
-
// project does not have to be re-read to prove one delivery.
|
|
3457
|
-
//
|
|
3458
|
-
//
|
|
4302
|
+
// project does not have to be re-read to prove one delivery. Recheck the
|
|
4303
|
+
// clock ordering as well: rollback can make a formerly safe floor unable
|
|
4304
|
+
// to answer for a new write (samchon/ttsc#1344).
|
|
3459
4305
|
if (
|
|
3460
4306
|
before !== undefined &&
|
|
4307
|
+
before.separable &&
|
|
3461
4308
|
proven !== undefined &&
|
|
3462
4309
|
proven.signatures[key] === before.signature &&
|
|
3463
4310
|
Object.prototype.hasOwnProperty.call(proven.hashes, key)
|
|
@@ -3673,7 +4520,7 @@ function walkProjectInputs(
|
|
|
3673
4520
|
|
|
3674
4521
|
/**
|
|
3675
4522
|
* Return a directory's metadata stamp, used to detect that its membership moved
|
|
3676
|
-
* _while_ the walk was enumerating it
|
|
4523
|
+
* _while_ the walk was enumerating it.
|
|
3677
4524
|
*
|
|
3678
4525
|
* This is the right instrument for that job and the wrong one for comparing two
|
|
3679
4526
|
* generations: it moves for ignored entries too. {@link walkProjectInputs}
|
|
@@ -3685,9 +4532,6 @@ function projectDirectorySignature(
|
|
|
3685
4532
|
): string | undefined {
|
|
3686
4533
|
try {
|
|
3687
4534
|
const stats = filesystem.statBigInt(directory);
|
|
3688
|
-
// Directory stamps are minted by the same clock as file stamps, so every
|
|
3689
|
-
// walk observation also raises the clock floor that separates them.
|
|
3690
|
-
observeFilesystemClock(filesystem, stats);
|
|
3691
4535
|
if (!stats.isDirectory()) {
|
|
3692
4536
|
return undefined;
|
|
3693
4537
|
}
|
|
@@ -3761,6 +4605,24 @@ function openDirectoryWatch(
|
|
|
3761
4605
|
return { close: () => watcher.close() };
|
|
3762
4606
|
}
|
|
3763
4607
|
|
|
4608
|
+
/** Detach and close every watcher, then preserve the first cleanup failure. */
|
|
4609
|
+
function closeDirectoryWatches(watchers: { close: () => void }[]): void {
|
|
4610
|
+
const owned = watchers.splice(0);
|
|
4611
|
+
let failed = false;
|
|
4612
|
+
let failure: unknown;
|
|
4613
|
+
for (const watcher of owned) {
|
|
4614
|
+
try {
|
|
4615
|
+
watcher.close();
|
|
4616
|
+
} catch (error) {
|
|
4617
|
+
if (!failed) {
|
|
4618
|
+
failed = true;
|
|
4619
|
+
failure = error;
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
if (failed) throw failure;
|
|
4624
|
+
}
|
|
4625
|
+
|
|
3764
4626
|
/** Watch every walked directory for membership changes after generation. */
|
|
3765
4627
|
async function createProjectMutationTracker(
|
|
3766
4628
|
directories: readonly TtscProjectDirectorySnapshot[],
|
|
@@ -3791,10 +4653,7 @@ async function createProjectMutationTracker(
|
|
|
3791
4653
|
return tracker;
|
|
3792
4654
|
}
|
|
3793
4655
|
const watchers: { close: () => void }[] = [];
|
|
3794
|
-
tracker.close = () =>
|
|
3795
|
-
for (const watcher of watchers) watcher.close();
|
|
3796
|
-
watchers.length = 0;
|
|
3797
|
-
};
|
|
4656
|
+
tracker.close = () => closeDirectoryWatches(watchers);
|
|
3798
4657
|
for (const directory of directories) {
|
|
3799
4658
|
try {
|
|
3800
4659
|
watchers.push(
|
|
@@ -3896,10 +4755,7 @@ async function createHostInputMutationTracker(
|
|
|
3896
4755
|
return tracker;
|
|
3897
4756
|
}
|
|
3898
4757
|
const watchers: { close: () => void }[] = [];
|
|
3899
|
-
tracker.close = () =>
|
|
3900
|
-
for (const watcher of watchers) watcher.close();
|
|
3901
|
-
watchers.length = 0;
|
|
3902
|
-
};
|
|
4758
|
+
tracker.close = () => closeDirectoryWatches(watchers);
|
|
3903
4759
|
for (const location of locations) {
|
|
3904
4760
|
try {
|
|
3905
4761
|
const names = new Set(location.names);
|
|
@@ -4137,14 +4993,36 @@ async function registerWindowsProjectMutationTracker(
|
|
|
4137
4993
|
if (active === undefined) return;
|
|
4138
4994
|
broker.trackers.delete(id);
|
|
4139
4995
|
active.ready();
|
|
4140
|
-
|
|
4996
|
+
let failed = false;
|
|
4997
|
+
let failure: unknown;
|
|
4998
|
+
try {
|
|
4999
|
+
broker.child.send?.({ id, op: "remove" });
|
|
5000
|
+
} catch (error) {
|
|
5001
|
+
failed = true;
|
|
5002
|
+
failure = error;
|
|
5003
|
+
}
|
|
4141
5004
|
if (broker.trackers.size === 0) {
|
|
4142
|
-
broker.child.disconnect?.();
|
|
4143
|
-
broker.child.kill();
|
|
4144
5005
|
if (windowsProjectMutationBroker === broker) {
|
|
4145
5006
|
windowsProjectMutationBroker = undefined;
|
|
4146
5007
|
}
|
|
5008
|
+
try {
|
|
5009
|
+
broker.child.disconnect?.();
|
|
5010
|
+
} catch (error) {
|
|
5011
|
+
if (!failed) {
|
|
5012
|
+
failed = true;
|
|
5013
|
+
failure = error;
|
|
5014
|
+
}
|
|
5015
|
+
}
|
|
5016
|
+
try {
|
|
5017
|
+
broker.child.kill();
|
|
5018
|
+
} catch (error) {
|
|
5019
|
+
if (!failed) {
|
|
5020
|
+
failed = true;
|
|
5021
|
+
failure = error;
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
4147
5024
|
}
|
|
5025
|
+
if (failed) throw failure;
|
|
4148
5026
|
};
|
|
4149
5027
|
broker.child.send?.({
|
|
4150
5028
|
allEvents,
|
|
@@ -4528,10 +5406,251 @@ export function collectExternalInputHashes(
|
|
|
4528
5406
|
return hashes;
|
|
4529
5407
|
}
|
|
4530
5408
|
|
|
5409
|
+
/** Capture the stable file predicate used by implicit project discovery. */
|
|
5410
|
+
export function captureWatchInputFileBaseline(
|
|
5411
|
+
file: string,
|
|
5412
|
+
filesystem: TtscTransformFilesystemOperations = DEFAULT_FILESYSTEM_OPERATIONS,
|
|
5413
|
+
): TtscWatchInputFileBaseline | undefined {
|
|
5414
|
+
const capture = (): TtscWatchInputFileBaseline => {
|
|
5415
|
+
const identities = createHostPathIdentityContext(filesystem);
|
|
5416
|
+
let fileExists = false;
|
|
5417
|
+
try {
|
|
5418
|
+
fileExists = filesystem.stat(file).isFile();
|
|
5419
|
+
} catch {
|
|
5420
|
+
// Project discovery rejects every candidate not proven to be a file.
|
|
5421
|
+
}
|
|
5422
|
+
return {
|
|
5423
|
+
fileExists,
|
|
5424
|
+
identity: pathIdentityKey(file, identities),
|
|
5425
|
+
};
|
|
5426
|
+
};
|
|
5427
|
+
try {
|
|
5428
|
+
const before = capture();
|
|
5429
|
+
const after = capture();
|
|
5430
|
+
return stableStringify(before) === stableStringify(after)
|
|
5431
|
+
? after
|
|
5432
|
+
: undefined;
|
|
5433
|
+
} catch {
|
|
5434
|
+
return undefined;
|
|
5435
|
+
}
|
|
5436
|
+
}
|
|
5437
|
+
|
|
5438
|
+
/**
|
|
5439
|
+
* Capture one stable main-process baseline that can be compared with any
|
|
5440
|
+
* generation-owned watch-input evidence. Two equal broad observations are
|
|
5441
|
+
* required so a cache key never publishes a torn path state.
|
|
5442
|
+
*/
|
|
5443
|
+
export function captureWatchInputBaseline(
|
|
5444
|
+
file: string,
|
|
5445
|
+
filesystem: TtscTransformFilesystemOperations = DEFAULT_FILESYSTEM_OPERATIONS,
|
|
5446
|
+
): TtscWatchInputBaseline | undefined {
|
|
5447
|
+
const capture = (): TtscWatchInputBaseline => {
|
|
5448
|
+
const identities = createHostPathIdentityContext(filesystem);
|
|
5449
|
+
const stat = compilerStatKind(file, filesystem);
|
|
5450
|
+
return {
|
|
5451
|
+
directoryExists: stat === "directory",
|
|
5452
|
+
fileExists: stat === "file",
|
|
5453
|
+
graphHash: graphInputStateHash(file, filesystem) ?? MISSING_INPUT_STATE,
|
|
5454
|
+
graphReadHash: graphInputReadHash(file, filesystem),
|
|
5455
|
+
hostHash: hostInputStateHash(file, filesystem) ?? MISSING_INPUT_STATE,
|
|
5456
|
+
identity: pathIdentityKey(file, identities),
|
|
5457
|
+
realpath: compilerInputRealpathObservation(file, filesystem),
|
|
5458
|
+
stat,
|
|
5459
|
+
};
|
|
5460
|
+
};
|
|
5461
|
+
try {
|
|
5462
|
+
const before = capture();
|
|
5463
|
+
const after = capture();
|
|
5464
|
+
return stableStringify(before) === stableStringify(after)
|
|
5465
|
+
? after
|
|
5466
|
+
: undefined;
|
|
5467
|
+
} catch {
|
|
5468
|
+
return undefined;
|
|
5469
|
+
}
|
|
5470
|
+
}
|
|
5471
|
+
|
|
5472
|
+
/** Compare generation evidence with the main process's exact key baseline. */
|
|
5473
|
+
export function watchInputEvidenceMatchesBaseline(
|
|
5474
|
+
evidence: TtscWatchInputEvidence,
|
|
5475
|
+
baseline: TtscWatchInputKeyBaseline,
|
|
5476
|
+
): boolean {
|
|
5477
|
+
if (
|
|
5478
|
+
!isWatchInputKeyBaseline(baseline) ||
|
|
5479
|
+
evidence.identity !== baseline.identity ||
|
|
5480
|
+
evidence.state === undefined
|
|
5481
|
+
) {
|
|
5482
|
+
return false;
|
|
5483
|
+
}
|
|
5484
|
+
const broad = broadWatchInputBaseline(baseline);
|
|
5485
|
+
if (evidence.state.codec === "host") {
|
|
5486
|
+
return broad !== undefined && evidence.state.hash === broad.hostHash;
|
|
5487
|
+
}
|
|
5488
|
+
const identities = createHostPathIdentityContext();
|
|
5489
|
+
if (evidence.state.codec === "graph") {
|
|
5490
|
+
return (
|
|
5491
|
+
broad !== undefined &&
|
|
5492
|
+
evidence.state.hash === broad.graphHash &&
|
|
5493
|
+
sameHostInputRealpath(
|
|
5494
|
+
evidence.state.realpath,
|
|
5495
|
+
broad.realpath.ok ? broad.realpath.path : null,
|
|
5496
|
+
identities,
|
|
5497
|
+
)
|
|
5498
|
+
);
|
|
5499
|
+
}
|
|
5500
|
+
const observation = evidence.state.observation;
|
|
5501
|
+
if (
|
|
5502
|
+
observation.fileExists !== undefined &&
|
|
5503
|
+
observation.fileExists !== baseline.fileExists
|
|
5504
|
+
) {
|
|
5505
|
+
return false;
|
|
5506
|
+
}
|
|
5507
|
+
if (
|
|
5508
|
+
observation.directoryExists !== undefined &&
|
|
5509
|
+
(broad === undefined ||
|
|
5510
|
+
observation.directoryExists !== broad.directoryExists)
|
|
5511
|
+
) {
|
|
5512
|
+
return false;
|
|
5513
|
+
}
|
|
5514
|
+
if (
|
|
5515
|
+
observation.stat !== undefined &&
|
|
5516
|
+
(broad === undefined || observation.stat !== broad.stat)
|
|
5517
|
+
) {
|
|
5518
|
+
return false;
|
|
5519
|
+
}
|
|
5520
|
+
if (
|
|
5521
|
+
observation.readFile !== undefined &&
|
|
5522
|
+
(broad === undefined ||
|
|
5523
|
+
(observation.readFile.ok &&
|
|
5524
|
+
observation.readFile.hash !== broad.graphReadHash) ||
|
|
5525
|
+
(!observation.readFile.ok && broad.graphReadHash !== null))
|
|
5526
|
+
) {
|
|
5527
|
+
return false;
|
|
5528
|
+
}
|
|
5529
|
+
if (observation.realpath !== undefined) {
|
|
5530
|
+
if (broad === undefined) {
|
|
5531
|
+
return false;
|
|
5532
|
+
}
|
|
5533
|
+
if (observation.realpath.ok !== broad.realpath.ok) {
|
|
5534
|
+
return false;
|
|
5535
|
+
}
|
|
5536
|
+
if (
|
|
5537
|
+
observation.realpath.ok &&
|
|
5538
|
+
broad.realpath.ok &&
|
|
5539
|
+
!sameHostInputRealpath(
|
|
5540
|
+
observation.realpath.path,
|
|
5541
|
+
broad.realpath.path,
|
|
5542
|
+
identities,
|
|
5543
|
+
)
|
|
5544
|
+
) {
|
|
5545
|
+
return false;
|
|
5546
|
+
}
|
|
5547
|
+
}
|
|
5548
|
+
return true;
|
|
5549
|
+
}
|
|
5550
|
+
|
|
5551
|
+
/** Validate the complete narrow or broad shape stored in a key baseline. */
|
|
5552
|
+
export function isWatchInputKeyBaseline(
|
|
5553
|
+
baseline: unknown,
|
|
5554
|
+
): baseline is TtscWatchInputKeyBaseline {
|
|
5555
|
+
if (!isPlainRecord(baseline)) return false;
|
|
5556
|
+
const keys = Object.keys(baseline).sort();
|
|
5557
|
+
if (
|
|
5558
|
+
typeof baseline.fileExists !== "boolean" ||
|
|
5559
|
+
typeof baseline.identity !== "string" ||
|
|
5560
|
+
!isAbsoluteFilesystemPath(baseline.identity)
|
|
5561
|
+
) {
|
|
5562
|
+
return false;
|
|
5563
|
+
}
|
|
5564
|
+
if (stableStringify(keys) === stableStringify(["fileExists", "identity"])) {
|
|
5565
|
+
return true;
|
|
5566
|
+
}
|
|
5567
|
+
if (
|
|
5568
|
+
stableStringify(keys) !==
|
|
5569
|
+
stableStringify([
|
|
5570
|
+
"directoryExists",
|
|
5571
|
+
"fileExists",
|
|
5572
|
+
"graphHash",
|
|
5573
|
+
"graphReadHash",
|
|
5574
|
+
"hostHash",
|
|
5575
|
+
"identity",
|
|
5576
|
+
"realpath",
|
|
5577
|
+
"stat",
|
|
5578
|
+
])
|
|
5579
|
+
) {
|
|
5580
|
+
return false;
|
|
5581
|
+
}
|
|
5582
|
+
if (
|
|
5583
|
+
typeof baseline.directoryExists !== "boolean" ||
|
|
5584
|
+
!isWatchInputStateHash(baseline.graphHash) ||
|
|
5585
|
+
!(
|
|
5586
|
+
baseline.graphReadHash === null || isContentHash(baseline.graphReadHash)
|
|
5587
|
+
) ||
|
|
5588
|
+
!isWatchInputStateHash(baseline.hostHash) ||
|
|
5589
|
+
!isWatchInputRealpathBaseline(baseline.realpath) ||
|
|
5590
|
+
(baseline.stat !== "directory" &&
|
|
5591
|
+
baseline.stat !== "file" &&
|
|
5592
|
+
baseline.stat !== "missing")
|
|
5593
|
+
) {
|
|
5594
|
+
return false;
|
|
5595
|
+
}
|
|
5596
|
+
return (
|
|
5597
|
+
baseline.fileExists === (baseline.stat === "file") &&
|
|
5598
|
+
baseline.directoryExists === (baseline.stat === "directory")
|
|
5599
|
+
);
|
|
5600
|
+
}
|
|
5601
|
+
|
|
5602
|
+
/** Whether an unknown value is one ordinary JSON object. */
|
|
5603
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
5604
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
5605
|
+
return false;
|
|
5606
|
+
}
|
|
5607
|
+
const prototype = Object.getPrototypeOf(value);
|
|
5608
|
+
return prototype === Object.prototype || prototype === null;
|
|
5609
|
+
}
|
|
5610
|
+
|
|
5611
|
+
/** Whether one identity can name an absolute path on either supported syntax. */
|
|
5612
|
+
function isAbsoluteFilesystemPath(value: string): boolean {
|
|
5613
|
+
return path.posix.isAbsolute(value) || path.win32.isAbsolute(value);
|
|
5614
|
+
}
|
|
5615
|
+
|
|
5616
|
+
/** Whether a serialized hash is a content digest. */
|
|
5617
|
+
function isContentHash(value: unknown): value is string {
|
|
5618
|
+
return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
/** Whether a baseline state is either a digest or the missing marker. */
|
|
5622
|
+
function isWatchInputStateHash(value: unknown): value is string {
|
|
5623
|
+
return value === MISSING_INPUT_STATE || isContentHash(value);
|
|
5624
|
+
}
|
|
5625
|
+
|
|
5626
|
+
/** Validate the serialized Realpath predicate as an exact discriminated union. */
|
|
5627
|
+
function isWatchInputRealpathBaseline(
|
|
5628
|
+
value: unknown,
|
|
5629
|
+
): value is TtscWatchInputBaseline["realpath"] {
|
|
5630
|
+
if (!isPlainRecord(value) || typeof value.ok !== "boolean") return false;
|
|
5631
|
+
const keys = Object.keys(value).sort();
|
|
5632
|
+
if (value.ok === false) {
|
|
5633
|
+
return stableStringify(keys) === stableStringify(["ok"]);
|
|
5634
|
+
}
|
|
5635
|
+
return (
|
|
5636
|
+
stableStringify(keys) === stableStringify(["ok", "path"]) &&
|
|
5637
|
+
typeof value.path === "string" &&
|
|
5638
|
+
isAbsoluteFilesystemPath(value.path)
|
|
5639
|
+
);
|
|
5640
|
+
}
|
|
5641
|
+
|
|
5642
|
+
/** Return a broad baseline after the complete entry has been validated. */
|
|
5643
|
+
function broadWatchInputBaseline(
|
|
5644
|
+
baseline: TtscWatchInputKeyBaseline,
|
|
5645
|
+
): TtscWatchInputBaseline | undefined {
|
|
5646
|
+
return "directoryExists" in baseline ? baseline : undefined;
|
|
5647
|
+
}
|
|
5648
|
+
|
|
4531
5649
|
/**
|
|
4532
5650
|
* Re-check a cached mixed graph/dependency input set with its owning codec,
|
|
4533
5651
|
* reusing the recorded hash of any input whose metadata signature still holds
|
|
4534
|
-
* and reporting the signatures
|
|
5652
|
+
* under a freshly minted same-device reference and reporting the signatures
|
|
5653
|
+
* this pass captured.
|
|
4535
5654
|
*
|
|
4536
5655
|
* The caller adopts those signatures only once every input is proven unchanged,
|
|
4537
5656
|
* so a signature never outlives the content comparison that justified it.
|
|
@@ -4557,6 +5676,20 @@ function matchesCachedExternalInputs(cached: TtscCachedProjectTransform): {
|
|
|
4557
5676
|
Object.keys(cached.externalInputHashes ?? {})) {
|
|
4558
5677
|
const identity = derivationIdentity(state, file);
|
|
4559
5678
|
const spelling = path.resolve(file);
|
|
5679
|
+
const observation = cached.externalInputObservations?.[spelling];
|
|
5680
|
+
if (observation !== undefined) {
|
|
5681
|
+
if (
|
|
5682
|
+
!matchesGraphInputObservation(
|
|
5683
|
+
file,
|
|
5684
|
+
observation,
|
|
5685
|
+
filesystem,
|
|
5686
|
+
state.identityContext,
|
|
5687
|
+
)
|
|
5688
|
+
) {
|
|
5689
|
+
matches = false;
|
|
5690
|
+
}
|
|
5691
|
+
continue;
|
|
5692
|
+
}
|
|
4560
5693
|
// Reuse the recorded hash of an out-of-walk input whose signature still
|
|
4561
5694
|
// equals the one captured around the read that proved it. The signature is
|
|
4562
5695
|
// keyed by this exact spelling, so an alias of the same physical file
|
|
@@ -4566,6 +5699,7 @@ function matchesCachedExternalInputs(cached: TtscCachedProjectTransform): {
|
|
|
4566
5699
|
before !== undefined &&
|
|
4567
5700
|
Object.prototype.hasOwnProperty.call(recordedSignatures, spelling) &&
|
|
4568
5701
|
Object.prototype.hasOwnProperty.call(recordedHashes, identity) &&
|
|
5702
|
+
before.separable &&
|
|
4569
5703
|
before.signature === recordedSignatures[spelling]
|
|
4570
5704
|
) {
|
|
4571
5705
|
continue;
|
|
@@ -4624,6 +5758,14 @@ function selectExternalInputPaths(props: {
|
|
|
4624
5758
|
const identities = createHostPathIdentityContext(filesystem);
|
|
4625
5759
|
const resolutionCandidates = new Set<string>();
|
|
4626
5760
|
const graph = props.result.graph;
|
|
5761
|
+
const graphState = envelopeDerivation({
|
|
5762
|
+
projectRoot: props.projectRoot,
|
|
5763
|
+
result: props.result,
|
|
5764
|
+
});
|
|
5765
|
+
const graphIndexes = envelopeGraphIndexes(graphState, {
|
|
5766
|
+
projectRoot: props.projectRoot,
|
|
5767
|
+
result: props.result,
|
|
5768
|
+
});
|
|
4627
5769
|
// Every transform output key names the source file whose transformed text it
|
|
4628
5770
|
// carries. Keep an out-of-walk source in the external snapshot instead of
|
|
4629
5771
|
// injecting it into the project-walk key universe (samchon/ttsc#252).
|
|
@@ -4635,7 +5777,11 @@ function selectExternalInputPaths(props: {
|
|
|
4635
5777
|
members.push(...targets);
|
|
4636
5778
|
}
|
|
4637
5779
|
}
|
|
4638
|
-
for (const listed of [
|
|
5780
|
+
for (const listed of [
|
|
5781
|
+
graph.globals,
|
|
5782
|
+
graph.configs,
|
|
5783
|
+
graph.resolutionInputs,
|
|
5784
|
+
]) {
|
|
4639
5785
|
if (Array.isArray(listed)) {
|
|
4640
5786
|
members.push(...listed);
|
|
4641
5787
|
}
|
|
@@ -4650,7 +5796,12 @@ function selectExternalInputPaths(props: {
|
|
|
4650
5796
|
}
|
|
4651
5797
|
const absolute = path.resolve(props.projectRoot, candidate);
|
|
4652
5798
|
members.push(candidate);
|
|
4653
|
-
resolutionCandidates.add(
|
|
5799
|
+
resolutionCandidates.add(path.resolve(absolute));
|
|
5800
|
+
}
|
|
5801
|
+
}
|
|
5802
|
+
for (const input of graph.resolutionInputs ?? []) {
|
|
5803
|
+
if (typeof input === "string" && input.length !== 0) {
|
|
5804
|
+
resolutionCandidates.add(path.resolve(props.projectRoot, input));
|
|
4654
5805
|
}
|
|
4655
5806
|
}
|
|
4656
5807
|
}
|
|
@@ -4666,9 +5817,7 @@ function selectExternalInputPaths(props: {
|
|
|
4666
5817
|
// Plugin discovery inputs deliberately include absent config and
|
|
4667
5818
|
// resolution probes. A project walk cannot snapshot a path that does
|
|
4668
5819
|
// not exist yet, even when its spelling lies below projectRoot.
|
|
4669
|
-
resolutionCandidates.add(
|
|
4670
|
-
pathIdentityKey(path.resolve(props.projectRoot, input), identities),
|
|
4671
|
-
);
|
|
5820
|
+
resolutionCandidates.add(path.resolve(props.projectRoot, input));
|
|
4672
5821
|
}
|
|
4673
5822
|
}
|
|
4674
5823
|
}
|
|
@@ -4685,8 +5834,11 @@ function selectExternalInputPaths(props: {
|
|
|
4685
5834
|
const absolute = path.resolve(props.projectRoot, member);
|
|
4686
5835
|
const spelling = path.resolve(absolute);
|
|
4687
5836
|
const identity = pathIdentityKey(absolute, identities);
|
|
5837
|
+
const observation = graphIndexes.inputObservations.get(spelling);
|
|
4688
5838
|
const missingCandidate =
|
|
4689
|
-
resolutionCandidates.has(
|
|
5839
|
+
resolutionCandidates.has(spelling) &&
|
|
5840
|
+
(observation?.fileExists === false ||
|
|
5841
|
+
(observation === undefined && !filesystem.exists(absolute)));
|
|
4690
5842
|
if (
|
|
4691
5843
|
identity === excluded ||
|
|
4692
5844
|
isTransformScratchInput(absolute, props.scratchDirectory) ||
|
|
@@ -4744,6 +5896,14 @@ function selectNotifiableAbsentInputs(props: {
|
|
|
4744
5896
|
return empty;
|
|
4745
5897
|
}
|
|
4746
5898
|
const identities = createHostPathIdentityContext(props.filesystem);
|
|
5899
|
+
const graphState = envelopeDerivation({
|
|
5900
|
+
projectRoot: props.projectRoot,
|
|
5901
|
+
result: props.result,
|
|
5902
|
+
});
|
|
5903
|
+
const graphIndexes = envelopeGraphIndexes(graphState, {
|
|
5904
|
+
projectRoot: props.projectRoot,
|
|
5905
|
+
result: props.result,
|
|
5906
|
+
});
|
|
4747
5907
|
const excluded =
|
|
4748
5908
|
props.temporaryTsconfig === undefined
|
|
4749
5909
|
? undefined
|
|
@@ -4757,7 +5917,10 @@ function selectNotifiableAbsentInputs(props: {
|
|
|
4757
5917
|
// them. Sharing a set would let one silently answer for the other.
|
|
4758
5918
|
const seen = new Set<string>();
|
|
4759
5919
|
const chain = new Set<string>();
|
|
4760
|
-
for (const candidates of
|
|
5920
|
+
for (const candidates of [
|
|
5921
|
+
...Object.values(graph.candidates ?? {}),
|
|
5922
|
+
graph.resolutionInputs ?? [],
|
|
5923
|
+
]) {
|
|
4761
5924
|
if (!Array.isArray(candidates)) {
|
|
4762
5925
|
continue;
|
|
4763
5926
|
}
|
|
@@ -4767,12 +5930,16 @@ function selectNotifiableAbsentInputs(props: {
|
|
|
4767
5930
|
}
|
|
4768
5931
|
const absolute = path.resolve(props.projectRoot, candidate);
|
|
4769
5932
|
const spelling = path.resolve(absolute);
|
|
5933
|
+
const observation = graphIndexes.inputObservations.get(spelling);
|
|
5934
|
+
const absentAsFile =
|
|
5935
|
+
observation?.fileExists === false ||
|
|
5936
|
+
(observation === undefined && !props.filesystem.exists(absolute));
|
|
4770
5937
|
if (
|
|
4771
5938
|
seen.has(spelling) ||
|
|
4772
5939
|
isTransformScratchInput(absolute, props.scratchDirectory) ||
|
|
4773
5940
|
(excluded !== undefined &&
|
|
4774
5941
|
pathIdentityKey(absolute, identities) === excluded) ||
|
|
4775
|
-
|
|
5942
|
+
!absentAsFile
|
|
4776
5943
|
) {
|
|
4777
5944
|
continue;
|
|
4778
5945
|
}
|
|
@@ -4882,20 +6049,6 @@ function insideProject(directory: string, projectRoot: string): boolean {
|
|
|
4882
6049
|
*/
|
|
4883
6050
|
const NOTIFIABLE_ABSENCE_DIRECTORY_LIMIT = 512;
|
|
4884
6051
|
|
|
4885
|
-
function isIgnoredProjectDirectory(name: string): boolean {
|
|
4886
|
-
// The residue of what used to be a fifteen-name list, kept to the three
|
|
4887
|
-
// directories no tsconfig can name and no program can contain: the VCS
|
|
4888
|
-
// store, the package manager's tree (TypeScript's own default `exclude`
|
|
4889
|
-
// carries it too), and ttsc's own plugin cache. Everything else the old list
|
|
4890
|
-
// guessed at, and guessing was wrong in both directions: a bundler writing
|
|
4891
|
-
// to an unnamed directory changed project membership with its own output,
|
|
4892
|
-
// while a real source directory named `build` or `temp` was dropped from the
|
|
4893
|
-
// walk and its new files were never seen (samchon/ttsc#1307). Those are now
|
|
4894
|
-
// decided by `ITtscProjectMembershipPolicy`, which reads the configuration
|
|
4895
|
-
// that actually knows.
|
|
4896
|
-
return name === ".git" || name === ".ttsc" || name === "node_modules";
|
|
4897
|
-
}
|
|
4898
|
-
|
|
4899
6052
|
/**
|
|
4900
6053
|
* Whether the resolved configuration keeps this directory out of the program.
|
|
4901
6054
|
*
|
|
@@ -5181,6 +6334,8 @@ function selectDeclaredProjectInputKeys(props: {
|
|
|
5181
6334
|
for (const input of graph.globals) add(input);
|
|
5182
6335
|
if (Array.isArray(graph.configs))
|
|
5183
6336
|
for (const input of graph.configs) add(input);
|
|
6337
|
+
if (Array.isArray(graph.resolutionInputs))
|
|
6338
|
+
for (const input of graph.resolutionInputs) add(input);
|
|
5184
6339
|
for (const [source, candidates] of Object.entries(graph.candidates ?? {})) {
|
|
5185
6340
|
add(source);
|
|
5186
6341
|
if (Array.isArray(candidates)) for (const entry of candidates) add(entry);
|
|
@@ -5478,6 +6633,10 @@ function createUnstableGenerationError(
|
|
|
5478
6633
|
lines.push(
|
|
5479
6634
|
" Stop writes to the listed inputs before compilation, or fix the producer that omitted or contradicted the listed proof.",
|
|
5480
6635
|
);
|
|
6636
|
+
// The failed snapshot remains useful as immutable retry evidence, but it can
|
|
6637
|
+
// never serve a module. Release every live resource before its terminal
|
|
6638
|
+
// verdict is retained in the cache.
|
|
6639
|
+
disposeCachedTransform(validation.cached);
|
|
5481
6640
|
return new TtscUnstableGenerationError(lines.join("\n"), validation);
|
|
5482
6641
|
}
|
|
5483
6642
|
|
|
@@ -5504,9 +6663,10 @@ async function transformProject(props: {
|
|
|
5504
6663
|
for (let attempt = 0; attempt < TRANSFORM_GENERATION_ATTEMPTS; attempt += 1) {
|
|
5505
6664
|
const cached = await captureTransformGeneration(props);
|
|
5506
6665
|
if (
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
6666
|
+
cached.configStateComplete !== false &&
|
|
6667
|
+
(!props.trackProjectMembership ||
|
|
6668
|
+
cached.result.type !== "success" ||
|
|
6669
|
+
cached.projectSnapshotComplete === true)
|
|
5510
6670
|
) {
|
|
5511
6671
|
return cached;
|
|
5512
6672
|
}
|
|
@@ -5552,23 +6712,54 @@ async function captureTransformGeneration(props: {
|
|
|
5552
6712
|
projectRoot,
|
|
5553
6713
|
props.filesystem,
|
|
5554
6714
|
);
|
|
6715
|
+
let clockReferenceDirectory: string | undefined;
|
|
6716
|
+
let retainClockReferenceDirectory = false;
|
|
5555
6717
|
let tracker: TtscProjectMutationTracker | undefined;
|
|
5556
6718
|
let retainTracker = false;
|
|
5557
6719
|
let hostInputTracker: TtscProjectMutationTracker | undefined;
|
|
5558
6720
|
let candidateTracker: TtscProjectMutationTracker | undefined;
|
|
5559
6721
|
let retainHostInputTracker = false;
|
|
5560
6722
|
let retainCandidateTracker = false;
|
|
6723
|
+
let captured: TtscCachedProjectTransform | undefined;
|
|
5561
6724
|
try {
|
|
5562
|
-
|
|
6725
|
+
if (props.trackProjectMembership) {
|
|
6726
|
+
try {
|
|
6727
|
+
clockReferenceDirectory = createTransformScratchDirectory(
|
|
6728
|
+
projectRoot,
|
|
6729
|
+
props.filesystem,
|
|
6730
|
+
);
|
|
6731
|
+
} catch {
|
|
6732
|
+
// A retained probe is an optimization. The live compiler scratch can
|
|
6733
|
+
// still authorize capture, and later validations will compare bytes.
|
|
6734
|
+
}
|
|
6735
|
+
}
|
|
6736
|
+
const materializesConfig =
|
|
6737
|
+
Object.keys(props.compilerOptions).length !== 0 ||
|
|
6738
|
+
Object.keys(props.aliasPaths).length !== 0;
|
|
6739
|
+
const tsconfigState = readTransformTsconfigState(
|
|
6740
|
+
props.tsconfig,
|
|
6741
|
+
materializesConfig,
|
|
6742
|
+
);
|
|
6743
|
+
const configured = createTransformTsconfig(
|
|
6744
|
+
props,
|
|
6745
|
+
scratchDirectory,
|
|
6746
|
+
tsconfigState,
|
|
6747
|
+
);
|
|
5563
6748
|
const temporaryTsconfig =
|
|
5564
6749
|
configured.path === props.tsconfig ? undefined : configured.path;
|
|
6750
|
+
const compilerEnvironment = transformScratchEnvironment(scratchDirectory);
|
|
6751
|
+
if (temporaryTsconfig === undefined) {
|
|
6752
|
+
delete compilerEnvironment[TTSC_SEMANTIC_CONFIG_PATH];
|
|
6753
|
+
} else {
|
|
6754
|
+
compilerEnvironment[TTSC_SEMANTIC_CONFIG_PATH] = props.tsconfig;
|
|
6755
|
+
}
|
|
5565
6756
|
const identities = createHostPathIdentityContext(props.filesystem);
|
|
5566
6757
|
// Read from the project's own tsconfig rather than the generated one: a
|
|
5567
6758
|
// relative `outDir` is anchored at the config that declares it, and the
|
|
5568
6759
|
// generated config lives in a system temp directory. The caller's
|
|
5569
6760
|
// compiler-options overlay still wins, since it wins for the compile too.
|
|
5570
6761
|
const membershipPolicy = mergeMembershipPolicyOverlay(
|
|
5571
|
-
|
|
6762
|
+
tsconfigState.membershipPolicy,
|
|
5572
6763
|
props.compilerOptions,
|
|
5573
6764
|
projectRoot,
|
|
5574
6765
|
);
|
|
@@ -5599,14 +6790,21 @@ async function captureTransformGeneration(props: {
|
|
|
5599
6790
|
plugins: props.plugins,
|
|
5600
6791
|
projectRoot,
|
|
5601
6792
|
tsconfig: configured.path,
|
|
5602
|
-
env:
|
|
6793
|
+
env: compilerEnvironment,
|
|
5603
6794
|
}).transform(),
|
|
5604
6795
|
);
|
|
5605
6796
|
TRANSFORM_RESULT_FILESYSTEM.set(result, props.filesystem);
|
|
6797
|
+
const configStable =
|
|
6798
|
+
tsconfigState.signature === undefined ||
|
|
6799
|
+
tsconfigState.signature ===
|
|
6800
|
+
readTransformTsconfigState(props.tsconfig, true).signature;
|
|
5606
6801
|
// Mint the generation's clock reference after the compile and before any
|
|
5607
6802
|
// signature-recording read below, so every input written before the
|
|
5608
6803
|
// compile sits in a provably finished tick when its signature is captured.
|
|
5609
|
-
|
|
6804
|
+
refreshFilesystemClockReference(
|
|
6805
|
+
clockReferenceDirectory ?? scratchDirectory,
|
|
6806
|
+
props.filesystem,
|
|
6807
|
+
);
|
|
5610
6808
|
const persistentHostInputs = selectPersistentHostInputs({
|
|
5611
6809
|
filesystem: props.filesystem,
|
|
5612
6810
|
projectRoot,
|
|
@@ -5684,6 +6882,7 @@ async function captureTransformGeneration(props: {
|
|
|
5684
6882
|
scratchDirectory,
|
|
5685
6883
|
});
|
|
5686
6884
|
const walkStable =
|
|
6885
|
+
configStable &&
|
|
5687
6886
|
walkSnapshotComplete(before, declaredInputs) &&
|
|
5688
6887
|
walkSnapshotComplete(inputSnapshot, declaredInputs) &&
|
|
5689
6888
|
sameHashes(before.hashes, inputSnapshot.hashes, declaredInputs) &&
|
|
@@ -5734,6 +6933,7 @@ async function captureTransformGeneration(props: {
|
|
|
5734
6933
|
externalInputHashes: {},
|
|
5735
6934
|
externalInputRealpaths: {},
|
|
5736
6935
|
externalInputPaths,
|
|
6936
|
+
configStateComplete: configStable,
|
|
5737
6937
|
inputHashes: inputSnapshot.hashes,
|
|
5738
6938
|
inputSignatures: inputSnapshot.provenSignatures,
|
|
5739
6939
|
membershipPolicy,
|
|
@@ -5759,6 +6959,7 @@ async function captureTransformGeneration(props: {
|
|
|
5759
6959
|
externalInputPaths,
|
|
5760
6960
|
);
|
|
5761
6961
|
cached.externalInputHashes = externalInputSnapshot.hashes;
|
|
6962
|
+
cached.externalInputObservations = externalInputSnapshot.observations;
|
|
5762
6963
|
cached.externalInputRealpaths = externalInputSnapshot.realpaths;
|
|
5763
6964
|
cached.externalInputSignatures = externalInputSnapshot.signatures;
|
|
5764
6965
|
// Evaluate every half, rather than short-circuiting, so a generation that
|
|
@@ -5766,6 +6967,13 @@ async function captureTransformGeneration(props: {
|
|
|
5766
6967
|
// only on the failing path, where the alternative is recompiling the whole
|
|
5767
6968
|
// project for every remaining module.
|
|
5768
6969
|
const failures = createGenerationProofFailures();
|
|
6970
|
+
if (!configStable) {
|
|
6971
|
+
recordGenerationProofFailure(failures, {
|
|
6972
|
+
domain: "project",
|
|
6973
|
+
kind: "config-state-changed",
|
|
6974
|
+
path: props.tsconfig,
|
|
6975
|
+
});
|
|
6976
|
+
}
|
|
5769
6977
|
if (!walkStable) {
|
|
5770
6978
|
recordProjectSnapshotFailures(failures, {
|
|
5771
6979
|
before,
|
|
@@ -5834,28 +7042,80 @@ async function captureTransformGeneration(props: {
|
|
|
5834
7042
|
retainTracker = notifying && tracker !== undefined;
|
|
5835
7043
|
retainHostInputTracker = notifying && hostInputTracker !== undefined;
|
|
5836
7044
|
retainCandidateTracker = notifying && candidateTracker !== undefined;
|
|
5837
|
-
|
|
7045
|
+
if (clockReferenceDirectory !== undefined) {
|
|
7046
|
+
retainClockReferenceDirectory = true;
|
|
7047
|
+
}
|
|
7048
|
+
captured = cached;
|
|
5838
7049
|
} finally {
|
|
7050
|
+
let cleanupFailed = false;
|
|
7051
|
+
let cleanupFailure: unknown;
|
|
5839
7052
|
try {
|
|
5840
|
-
if (!retainTracker && tracker !== undefined) {
|
|
5841
|
-
tracker.close();
|
|
5842
|
-
}
|
|
5843
|
-
} finally {
|
|
5844
7053
|
try {
|
|
5845
|
-
if (!
|
|
5846
|
-
|
|
7054
|
+
if (!retainTracker && tracker !== undefined) {
|
|
7055
|
+
tracker.close();
|
|
5847
7056
|
}
|
|
5848
7057
|
} finally {
|
|
5849
7058
|
try {
|
|
5850
|
-
if (!
|
|
5851
|
-
|
|
7059
|
+
if (!retainHostInputTracker && hostInputTracker !== undefined) {
|
|
7060
|
+
hostInputTracker.close();
|
|
5852
7061
|
}
|
|
5853
7062
|
} finally {
|
|
5854
|
-
|
|
7063
|
+
try {
|
|
7064
|
+
if (!retainCandidateTracker && candidateTracker !== undefined) {
|
|
7065
|
+
candidateTracker.close();
|
|
7066
|
+
}
|
|
7067
|
+
} finally {
|
|
7068
|
+
try {
|
|
7069
|
+
fs.rmSync(scratchDirectory, { force: true, recursive: true });
|
|
7070
|
+
} finally {
|
|
7071
|
+
if (
|
|
7072
|
+
!retainClockReferenceDirectory &&
|
|
7073
|
+
clockReferenceDirectory !== undefined
|
|
7074
|
+
) {
|
|
7075
|
+
disposeFilesystemClockReference(clockReferenceDirectory);
|
|
7076
|
+
}
|
|
7077
|
+
}
|
|
7078
|
+
}
|
|
7079
|
+
}
|
|
7080
|
+
}
|
|
7081
|
+
} catch (error) {
|
|
7082
|
+
cleanupFailed = true;
|
|
7083
|
+
cleanupFailure = error;
|
|
7084
|
+
}
|
|
7085
|
+
if (cleanupFailed) {
|
|
7086
|
+
// The generation never leaves this function when local cleanup fails.
|
|
7087
|
+
// Close everything that was waiting to transfer, without letting a
|
|
7088
|
+
// secondary teardown error replace the first failure.
|
|
7089
|
+
for (const retained of [
|
|
7090
|
+
retainTracker ? tracker : undefined,
|
|
7091
|
+
retainHostInputTracker ? hostInputTracker : undefined,
|
|
7092
|
+
retainCandidateTracker ? candidateTracker : undefined,
|
|
7093
|
+
]) {
|
|
7094
|
+
try {
|
|
7095
|
+
retained?.close();
|
|
7096
|
+
} catch {
|
|
7097
|
+
// Continue releasing the other generation-owned resources.
|
|
5855
7098
|
}
|
|
5856
7099
|
}
|
|
7100
|
+
if (
|
|
7101
|
+
retainClockReferenceDirectory &&
|
|
7102
|
+
clockReferenceDirectory !== undefined
|
|
7103
|
+
) {
|
|
7104
|
+
disposeFilesystemClockReference(clockReferenceDirectory);
|
|
7105
|
+
}
|
|
7106
|
+
throw cleanupFailure;
|
|
5857
7107
|
}
|
|
5858
7108
|
}
|
|
7109
|
+
if (captured === undefined) {
|
|
7110
|
+
throw new Error("ttsc: transform generation capture produced no result");
|
|
7111
|
+
}
|
|
7112
|
+
if (clockReferenceDirectory !== undefined) {
|
|
7113
|
+
TRANSFORM_CLOCK_REFERENCE_DIRECTORIES.set(
|
|
7114
|
+
captured,
|
|
7115
|
+
clockReferenceDirectory,
|
|
7116
|
+
);
|
|
7117
|
+
}
|
|
7118
|
+
return captured;
|
|
5859
7119
|
}
|
|
5860
7120
|
|
|
5861
7121
|
/** Exclude disposed transform scratch from live host-input tracking. */
|
|
@@ -5884,6 +7144,50 @@ function selectPersistentHostInputs(props: {
|
|
|
5884
7144
|
});
|
|
5885
7145
|
}
|
|
5886
7146
|
|
|
7147
|
+
interface ITransformTsconfigState {
|
|
7148
|
+
effectivePaths: Record<string, string[]>;
|
|
7149
|
+
membershipPolicy: ITtscProjectMembershipPolicy;
|
|
7150
|
+
signature?: string;
|
|
7151
|
+
templateCompilerOptions: Record<string, unknown>;
|
|
7152
|
+
templateFileSpecs: Record<string, unknown>;
|
|
7153
|
+
}
|
|
7154
|
+
|
|
7155
|
+
/** Read every wrapper-dependent view and bind it to one config-chain state. */
|
|
7156
|
+
function readTransformTsconfigState(
|
|
7157
|
+
tsconfig: string,
|
|
7158
|
+
materializesConfig: boolean,
|
|
7159
|
+
): ITransformTsconfigState {
|
|
7160
|
+
const membershipPolicy = readProjectMembershipPolicy(tsconfig);
|
|
7161
|
+
if (!materializesConfig) {
|
|
7162
|
+
return {
|
|
7163
|
+
effectivePaths: {},
|
|
7164
|
+
membershipPolicy,
|
|
7165
|
+
templateCompilerOptions: {},
|
|
7166
|
+
templateFileSpecs: {},
|
|
7167
|
+
};
|
|
7168
|
+
}
|
|
7169
|
+
const effectivePaths = readEffectiveTsconfigPaths(tsconfig);
|
|
7170
|
+
const templateCompilerOptions =
|
|
7171
|
+
readEffectiveTsconfigTemplateCompilerOptions(tsconfig);
|
|
7172
|
+
const templateFileSpecs = readEffectiveTsconfigTemplateFileSpecs(tsconfig);
|
|
7173
|
+
const sources = readTsconfigSourceSnapshot(tsconfig);
|
|
7174
|
+
return {
|
|
7175
|
+
effectivePaths,
|
|
7176
|
+
membershipPolicy,
|
|
7177
|
+
signature: hashText(
|
|
7178
|
+
JSON.stringify({
|
|
7179
|
+
effectivePaths,
|
|
7180
|
+
membershipPolicy,
|
|
7181
|
+
sources,
|
|
7182
|
+
templateCompilerOptions,
|
|
7183
|
+
templateFileSpecs,
|
|
7184
|
+
}),
|
|
7185
|
+
),
|
|
7186
|
+
templateCompilerOptions,
|
|
7187
|
+
templateFileSpecs,
|
|
7188
|
+
};
|
|
7189
|
+
}
|
|
7190
|
+
|
|
5887
7191
|
function createTransformTsconfig(
|
|
5888
7192
|
props: {
|
|
5889
7193
|
aliasPaths: Record<string, string[]>;
|
|
@@ -5891,24 +7195,34 @@ function createTransformTsconfig(
|
|
|
5891
7195
|
tsconfig: string;
|
|
5892
7196
|
},
|
|
5893
7197
|
scratchDirectory: string,
|
|
7198
|
+
state: ITransformTsconfigState,
|
|
5894
7199
|
): { path: string } {
|
|
5895
|
-
const
|
|
7200
|
+
const overlay = normalizeCompilerOptionsForGeneratedTsconfig(
|
|
5896
7201
|
{
|
|
5897
7202
|
...props.compilerOptions,
|
|
5898
|
-
...createAliasCompilerOptions(props),
|
|
7203
|
+
...createAliasCompilerOptions(props, state.effectivePaths),
|
|
5899
7204
|
},
|
|
5900
7205
|
path.dirname(props.tsconfig),
|
|
5901
7206
|
);
|
|
5902
|
-
if (Object.keys(
|
|
7207
|
+
if (Object.keys(overlay).length === 0) {
|
|
5903
7208
|
return { path: props.tsconfig };
|
|
5904
7209
|
}
|
|
5905
7210
|
|
|
7211
|
+
// A `${configDir}` value survives every `extends` hop and binds only at the
|
|
7212
|
+
// final consumer. The scratch wrapper would therefore move it out of the
|
|
7213
|
+
// project even for an unrelated overlay. Re-state only those inherited
|
|
7214
|
+
// values as absolute paths so the wrapper remains semantically transparent.
|
|
7215
|
+
const compilerOptions = {
|
|
7216
|
+
...state.templateCompilerOptions,
|
|
7217
|
+
...overlay,
|
|
7218
|
+
};
|
|
5906
7219
|
const file = path.join(scratchDirectory, "tsconfig.json");
|
|
5907
7220
|
fs.writeFileSync(
|
|
5908
7221
|
file,
|
|
5909
7222
|
JSON.stringify(
|
|
5910
7223
|
{
|
|
5911
7224
|
extends: normalizePath(props.tsconfig),
|
|
7225
|
+
...state.templateFileSpecs,
|
|
5912
7226
|
compilerOptions,
|
|
5913
7227
|
},
|
|
5914
7228
|
null,
|
|
@@ -6067,16 +7381,18 @@ function normalizeCompilerOptionsForGeneratedTsconfig(
|
|
|
6067
7381
|
): Record<string, unknown> {
|
|
6068
7382
|
const output = { ...compilerOptions };
|
|
6069
7383
|
// Scalar path fields: resolve each against the original tsconfig directory.
|
|
6070
|
-
for (const key of
|
|
7384
|
+
for (const key of CONFIG_DIR_TEMPLATE_SCALAR_OPTIONS) {
|
|
6071
7385
|
if (typeof output[key] === "string") {
|
|
6072
|
-
output[key] =
|
|
7386
|
+
output[key] = resolveConfigDirTemplatePath(tsconfigDir, output[key]);
|
|
6073
7387
|
}
|
|
6074
7388
|
}
|
|
6075
7389
|
// Array path fields: resolve each element individually.
|
|
6076
|
-
for (const key of
|
|
7390
|
+
for (const key of CONFIG_DIR_TEMPLATE_LIST_OPTIONS) {
|
|
6077
7391
|
if (Array.isArray(output[key])) {
|
|
6078
7392
|
output[key] = output[key].map((entry) =>
|
|
6079
|
-
typeof entry === "string"
|
|
7393
|
+
typeof entry === "string"
|
|
7394
|
+
? resolveConfigDirTemplatePath(tsconfigDir, entry)
|
|
7395
|
+
: entry,
|
|
6080
7396
|
);
|
|
6081
7397
|
}
|
|
6082
7398
|
}
|
|
@@ -6136,17 +7452,20 @@ function normalizePluginConfigForGeneratedTsconfig(
|
|
|
6136
7452
|
* No `baseUrl` is emitted: TypeScript-Go removed the option (TS5102), and all
|
|
6137
7453
|
* targets are absolute so none is needed.
|
|
6138
7454
|
*/
|
|
6139
|
-
function createAliasCompilerOptions(
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
7455
|
+
function createAliasCompilerOptions(
|
|
7456
|
+
props: {
|
|
7457
|
+
aliasPaths: Record<string, string[]>;
|
|
7458
|
+
compilerOptions: Record<string, unknown>;
|
|
7459
|
+
tsconfig: string;
|
|
7460
|
+
},
|
|
7461
|
+
effectivePaths: Record<string, string[]>,
|
|
7462
|
+
): Record<string, unknown> {
|
|
6144
7463
|
if (Object.keys(props.aliasPaths).length === 0) {
|
|
6145
7464
|
return {};
|
|
6146
7465
|
}
|
|
6147
7466
|
return {
|
|
6148
7467
|
paths: {
|
|
6149
|
-
...
|
|
7468
|
+
...effectivePaths,
|
|
6150
7469
|
...readPaths(props.compilerOptions.paths),
|
|
6151
7470
|
...props.aliasPaths,
|
|
6152
7471
|
},
|
|
@@ -6531,9 +7850,9 @@ function stripTerminalEscapes(text: string): string {
|
|
|
6531
7850
|
*
|
|
6532
7851
|
* If `tsconfig` is supplied it is returned as-is (absolute) or resolved from
|
|
6533
7852
|
* `process.cwd()` (relative). Otherwise the function walks ancestor directories
|
|
6534
|
-
* starting at `file`'s directory, returning the first `tsconfig.json`
|
|
6535
|
-
* Falls back to `<cwd>/tsconfig.json` when no ancestor contains one;
|
|
6536
|
-
* compiler will error if that file does not exist, which is the correct
|
|
7853
|
+
* starting at `file`'s directory, returning the first `tsconfig.json` proven to
|
|
7854
|
+
* be a file. Falls back to `<cwd>/tsconfig.json` when no ancestor contains one;
|
|
7855
|
+
* the compiler will error if that file does not exist, which is the correct
|
|
6537
7856
|
* behavior for a mis-configured project.
|
|
6538
7857
|
*/
|
|
6539
7858
|
function resolveTsconfig(
|
|
@@ -6547,18 +7866,9 @@ function resolveTsconfig(
|
|
|
6547
7866
|
: path.resolve(process.cwd(), tsconfig);
|
|
6548
7867
|
}
|
|
6549
7868
|
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
if (filesystem.exists(candidate)) {
|
|
6554
|
-
return candidate;
|
|
6555
|
-
}
|
|
6556
|
-
const parent = path.dirname(current);
|
|
6557
|
-
// Reached filesystem root, stop walking.
|
|
6558
|
-
if (parent === current) {
|
|
6559
|
-
break;
|
|
6560
|
-
}
|
|
6561
|
-
current = parent;
|
|
7869
|
+
const discovered = findNearestProjectTsconfig(path.dirname(file), filesystem);
|
|
7870
|
+
if (discovered !== undefined) {
|
|
7871
|
+
return discovered;
|
|
6562
7872
|
}
|
|
6563
7873
|
return path.resolve(process.cwd(), "tsconfig.json");
|
|
6564
7874
|
}
|