@ttsc/unplugin 0.22.0 → 0.24.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/lib/core/transform.d.ts +4 -3
- package/lib/core/transform.js +241 -173
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +241 -173
- package/lib/core/transform.mjs.map +1 -1
- package/package.json +4 -4
- package/src/core/transform.ts +454 -225
package/src/core/transform.ts
CHANGED
|
@@ -7,6 +7,10 @@ import type {
|
|
|
7
7
|
ITtscCompilerTransformation,
|
|
8
8
|
} from "ttsc";
|
|
9
9
|
import { TtscCompiler } from "ttsc";
|
|
10
|
+
import {
|
|
11
|
+
type FilesystemPathIdentityContext,
|
|
12
|
+
createFilesystemPathIdentityContext,
|
|
13
|
+
} from "ttsc/path-identity";
|
|
10
14
|
import type { TransformResult } from "unplugin";
|
|
11
15
|
|
|
12
16
|
import type { ResolvedTtscUnpluginOptions } from "./options";
|
|
@@ -113,6 +117,12 @@ export type TtscTransformCache = Map<
|
|
|
113
117
|
*/
|
|
114
118
|
const BUILD_SCOPED_TRANSFORM_CACHES = new WeakSet<TtscTransformCache>();
|
|
115
119
|
|
|
120
|
+
function createHostPathIdentityContext(): FilesystemPathIdentityContext {
|
|
121
|
+
return createFilesystemPathIdentityContext({
|
|
122
|
+
throwOnRealpathError: false,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
116
126
|
/** Create an empty persistent transform cache. */
|
|
117
127
|
export function createTtscTransformCache(): TtscTransformCache {
|
|
118
128
|
return new Map();
|
|
@@ -143,9 +153,6 @@ export function resetTtscTransformCache(cache: TtscTransformCache): void {
|
|
|
143
153
|
BUILD_SCOPED_TRANSFORM_CACHES.delete(cache);
|
|
144
154
|
}
|
|
145
155
|
|
|
146
|
-
/** Cached case-insensitivity probes for existing macOS filesystem locations. */
|
|
147
|
-
const CASE_INSENSITIVE_FILESYSTEMS = new Map<string, boolean>();
|
|
148
|
-
|
|
149
156
|
/**
|
|
150
157
|
* Hooks the bundler adapter passes into {@link transformTtsc} so transform
|
|
151
158
|
* side-channels (plugin-reported dependencies and host resolution candidates)
|
|
@@ -244,7 +251,7 @@ export async function transformTtsc(
|
|
|
244
251
|
// A file the plugin declared volatile must never be served from the
|
|
245
252
|
// cache: its output depends on non-file inputs, so the input-hash
|
|
246
253
|
// snapshot cannot prove freshness. Fall through to a fresh transform.
|
|
247
|
-
!isVolatileFile({
|
|
254
|
+
!isVolatileFile(envelopeDerivation(cached), {
|
|
248
255
|
file,
|
|
249
256
|
projectRoot: cached.projectRoot,
|
|
250
257
|
result: cached.result,
|
|
@@ -308,7 +315,9 @@ export async function transformTtsc(
|
|
|
308
315
|
});
|
|
309
316
|
notifyWatchInputs(hooks, { file, projectRoot, result, temporaryTsconfig });
|
|
310
317
|
markCachedSourceServed(cached, file);
|
|
311
|
-
if (
|
|
318
|
+
if (
|
|
319
|
+
isVolatileFile(envelopeDerivation(cached), { file, projectRoot, result })
|
|
320
|
+
) {
|
|
312
321
|
hooks?.markVolatile?.();
|
|
313
322
|
}
|
|
314
323
|
return createTransformResult(source, code);
|
|
@@ -377,6 +386,211 @@ function evictGeneration(
|
|
|
377
386
|
}
|
|
378
387
|
}
|
|
379
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Per-envelope derivation state: every index the per-delivery paths need, built
|
|
391
|
+
* at most once and shared by all deliveries of one compiler result.
|
|
392
|
+
*
|
|
393
|
+
* Building the direct-edge index, the candidate entries, the declared-file
|
|
394
|
+
* identity sets, or the output/dependency key indexes per delivery costs
|
|
395
|
+
* O(envelope) `pathIdentityKey` computations per module — and each of those
|
|
396
|
+
* costs real path-resolution and case-semantics probes
|
|
397
|
+
* ({@link pathIdentityKey}). A graph-bearing envelope (typia >= 13.1.19) turned
|
|
398
|
+
* that into O(modules x edges) filesystem work per build, which is the
|
|
399
|
+
* samchon/ttsc#1007 stall. All deliveries of one generation share this state,
|
|
400
|
+
* so a delivery pays only its own reachability closure with memoized
|
|
401
|
+
* identities.
|
|
402
|
+
*
|
|
403
|
+
* Every index is lazy: a host that never wires `addWatchFile` and never misses
|
|
404
|
+
* a project-relative key pays nothing beyond the volatile/completeness
|
|
405
|
+
* membership sets, which are themselves built on first predicate use.
|
|
406
|
+
*
|
|
407
|
+
* Freshness matches the generation contract: every derivable path is a recorded
|
|
408
|
+
* project or external input, so persistent-mode validation already proves those
|
|
409
|
+
* paths unchanged on every non-build-scoped hit, and any change invalidates the
|
|
410
|
+
* generation — and this state with it. The `WeakMap` key is the envelope object
|
|
411
|
+
* itself, so the state dies when the generation does.
|
|
412
|
+
*/
|
|
413
|
+
interface TtscEnvelopeDerivation {
|
|
414
|
+
/** One filesystem snapshot for every identity comparison in this envelope. */
|
|
415
|
+
readonly identityContext: FilesystemPathIdentityContext;
|
|
416
|
+
/** Memoized {@link pathIdentityKey} results, keyed by the exact input. */
|
|
417
|
+
readonly identities: Map<string, string>;
|
|
418
|
+
/**
|
|
419
|
+
* Lazily built reference-graph indexes, `undefined` until the first
|
|
420
|
+
* watch-input derivation. A host without an `addWatchFile` hook never pays
|
|
421
|
+
* the O(edges) build.
|
|
422
|
+
*/
|
|
423
|
+
graph?: TtscEnvelopeGraphIndexes;
|
|
424
|
+
/**
|
|
425
|
+
* Lazily collected identities of the envelope's `volatile` member files,
|
|
426
|
+
* `undefined` until the first volatility predicate.
|
|
427
|
+
*/
|
|
428
|
+
volatileFiles?: Set<string>;
|
|
429
|
+
/**
|
|
430
|
+
* Lazily collected identities of the envelope's `dependenciesComplete` member
|
|
431
|
+
* files, `undefined` until the first completeness predicate.
|
|
432
|
+
*/
|
|
433
|
+
dependenciesComplete?: Set<string>;
|
|
434
|
+
/**
|
|
435
|
+
* Lazily built identity -> output source index of the `typescript` map (first
|
|
436
|
+
* match wins, mirroring the historical scan). `undefined` until the first
|
|
437
|
+
* project-relative key miss.
|
|
438
|
+
*/
|
|
439
|
+
outputIndex?: Map<string, string>;
|
|
440
|
+
/**
|
|
441
|
+
* Lazily built identity -> `dependencies` entries index (first match wins,
|
|
442
|
+
* mirroring the historical scan). `undefined` until the first key miss.
|
|
443
|
+
*/
|
|
444
|
+
dependencyIndex?: Map<string, unknown>;
|
|
445
|
+
/** Per-file memo of the final derived watch-input list. */
|
|
446
|
+
readonly watchInputs: Map<string, string[]>;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Reference-graph indexes shared by every watch-input derivation. */
|
|
450
|
+
interface TtscEnvelopeGraphIndexes {
|
|
451
|
+
/** Identity of each direct-edge source -> its resolved absolute targets. */
|
|
452
|
+
readonly edges: Map<string, string[]>;
|
|
453
|
+
/** Identity of each direct-edge source -> its absolute spelling. */
|
|
454
|
+
readonly spellings: Map<string, string>;
|
|
455
|
+
/** Resolution-candidate entries in envelope order, sources pre-identified. */
|
|
456
|
+
readonly candidates: { source: string; files: string[] }[];
|
|
457
|
+
/** Resolved absolute `graph.globals` and `graph.configs` members. */
|
|
458
|
+
readonly globals: string[];
|
|
459
|
+
readonly configs: string[];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Derivation states keyed by the compiler result object. One result object is
|
|
464
|
+
* produced by one compile against one project root, so the root captured at
|
|
465
|
+
* build time is the only root the state ever sees.
|
|
466
|
+
*/
|
|
467
|
+
const ENVELOPE_DERIVATIONS = new WeakMap<
|
|
468
|
+
ITtscCompilerTransformation,
|
|
469
|
+
TtscEnvelopeDerivation
|
|
470
|
+
>();
|
|
471
|
+
|
|
472
|
+
/** Return the derivation state of `props.result`, building it on first use. */
|
|
473
|
+
function envelopeDerivation(props: {
|
|
474
|
+
projectRoot: string;
|
|
475
|
+
result: ITtscCompilerTransformation;
|
|
476
|
+
}): TtscEnvelopeDerivation {
|
|
477
|
+
const existing = ENVELOPE_DERIVATIONS.get(props.result);
|
|
478
|
+
if (existing !== undefined) {
|
|
479
|
+
return existing;
|
|
480
|
+
}
|
|
481
|
+
const created: TtscEnvelopeDerivation = {
|
|
482
|
+
identityContext: createHostPathIdentityContext(),
|
|
483
|
+
identities: new Map(),
|
|
484
|
+
watchInputs: new Map(),
|
|
485
|
+
};
|
|
486
|
+
ENVELOPE_DERIVATIONS.set(props.result, created);
|
|
487
|
+
return created;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Build the reference-graph indexes of one envelope on first watch-input
|
|
492
|
+
* derivation. Malformed sections are dropped member by member, mirroring the
|
|
493
|
+
* historical per-delivery scan.
|
|
494
|
+
*/
|
|
495
|
+
function envelopeGraphIndexes(
|
|
496
|
+
state: TtscEnvelopeDerivation,
|
|
497
|
+
props: {
|
|
498
|
+
projectRoot: string;
|
|
499
|
+
result: ITtscCompilerTransformation;
|
|
500
|
+
},
|
|
501
|
+
): TtscEnvelopeGraphIndexes {
|
|
502
|
+
if (state.graph !== undefined) {
|
|
503
|
+
return state.graph;
|
|
504
|
+
}
|
|
505
|
+
const built: TtscEnvelopeGraphIndexes = {
|
|
506
|
+
edges: new Map(),
|
|
507
|
+
spellings: new Map(),
|
|
508
|
+
candidates: [],
|
|
509
|
+
globals: [],
|
|
510
|
+
configs: [],
|
|
511
|
+
};
|
|
512
|
+
const graph =
|
|
513
|
+
props.result.type === "exception" ? undefined : props.result.graph;
|
|
514
|
+
if (graph !== undefined) {
|
|
515
|
+
for (const [source, targets] of Object.entries(graph.edges ?? {})) {
|
|
516
|
+
if (!Array.isArray(targets)) {
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const absolute = path.resolve(props.projectRoot, source);
|
|
520
|
+
const identity = derivationIdentity(state, absolute);
|
|
521
|
+
built.spellings.set(identity, absolute);
|
|
522
|
+
const entries = built.edges.get(identity) ?? [];
|
|
523
|
+
entries.push(
|
|
524
|
+
...targets
|
|
525
|
+
.filter(
|
|
526
|
+
(target): target is string =>
|
|
527
|
+
typeof target === "string" && target.length !== 0,
|
|
528
|
+
)
|
|
529
|
+
.map((target) => path.resolve(props.projectRoot, target)),
|
|
530
|
+
);
|
|
531
|
+
built.edges.set(identity, entries);
|
|
532
|
+
}
|
|
533
|
+
built.globals.push(...selectListedFiles(props.projectRoot, graph.globals));
|
|
534
|
+
built.configs.push(...selectListedFiles(props.projectRoot, graph.configs));
|
|
535
|
+
for (const [source, candidates] of Object.entries(graph.candidates ?? {})) {
|
|
536
|
+
if (!Array.isArray(candidates)) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
built.candidates.push({
|
|
540
|
+
source: derivationIdentity(
|
|
541
|
+
state,
|
|
542
|
+
path.resolve(props.projectRoot, source),
|
|
543
|
+
),
|
|
544
|
+
files: selectListedFiles(props.projectRoot, candidates),
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
state.graph = built;
|
|
549
|
+
return built;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* {@link pathIdentityKey} memoized inside one envelope's derivation state.
|
|
554
|
+
* Callers always pass already-resolved absolute paths, so the input string is a
|
|
555
|
+
* stable memo key.
|
|
556
|
+
*/
|
|
557
|
+
function derivationIdentity(
|
|
558
|
+
state: TtscEnvelopeDerivation,
|
|
559
|
+
file: string,
|
|
560
|
+
): string {
|
|
561
|
+
const existing = state.identities.get(file);
|
|
562
|
+
if (existing !== undefined) {
|
|
563
|
+
return existing;
|
|
564
|
+
}
|
|
565
|
+
const identity = pathIdentityKey(file, state.identityContext);
|
|
566
|
+
state.identities.set(file, identity);
|
|
567
|
+
return identity;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Fold one envelope member list (`volatile`, `dependenciesComplete`) into an
|
|
572
|
+
* identity set. Members are keyed like `typescript`, so a project-relative and
|
|
573
|
+
* an absolute spelling of the same file share one identity; a malformed member
|
|
574
|
+
* is ignored rather than fatal.
|
|
575
|
+
*/
|
|
576
|
+
function collectDeclaredIdentities(
|
|
577
|
+
state: TtscEnvelopeDerivation,
|
|
578
|
+
projectRoot: string,
|
|
579
|
+
listed: unknown,
|
|
580
|
+
): Set<string> {
|
|
581
|
+
const output = new Set<string>();
|
|
582
|
+
if (!Array.isArray(listed)) {
|
|
583
|
+
return output;
|
|
584
|
+
}
|
|
585
|
+
for (const entry of listed) {
|
|
586
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
output.add(derivationIdentity(state, path.resolve(projectRoot, entry)));
|
|
590
|
+
}
|
|
591
|
+
return output;
|
|
592
|
+
}
|
|
593
|
+
|
|
380
594
|
/**
|
|
381
595
|
* Forward every derived watch input for `file` to the adapter's `addWatchFile`
|
|
382
596
|
* hook: the plugin-reported `dependencies[file]` list unioned with the
|
|
@@ -425,7 +639,10 @@ function notifyWatchInputs(
|
|
|
425
639
|
* volatile keeps the baseline: the two declarations contradict, so the
|
|
426
640
|
* conservative one wins.
|
|
427
641
|
*
|
|
428
|
-
*
|
|
642
|
+
* The derived list is a pure function of the envelope and the file's filesystem
|
|
643
|
+
* identity, so it is computed at most once per generation per file: sibling and
|
|
644
|
+
* repeated deliveries replay the per-envelope memo ({@link envelopeDerivation})
|
|
645
|
+
* instead of re-walking the graph. Returns an empty list on exceptions.
|
|
429
646
|
*/
|
|
430
647
|
function selectWatchInputs(props: {
|
|
431
648
|
file: string;
|
|
@@ -433,22 +650,49 @@ function selectWatchInputs(props: {
|
|
|
433
650
|
result: ITtscCompilerTransformation;
|
|
434
651
|
temporaryTsconfig?: string;
|
|
435
652
|
}): string[] {
|
|
653
|
+
if (props.result.type === "exception") {
|
|
654
|
+
return [];
|
|
655
|
+
}
|
|
656
|
+
const state = envelopeDerivation(props);
|
|
657
|
+
const fileIdentity = derivationIdentity(state, props.file);
|
|
658
|
+
const memoized = state.watchInputs.get(fileIdentity);
|
|
659
|
+
if (memoized !== undefined) {
|
|
660
|
+
return memoized;
|
|
661
|
+
}
|
|
662
|
+
const derived = deriveWatchInputs(state, props, fileIdentity);
|
|
663
|
+
state.watchInputs.set(fileIdentity, derived);
|
|
664
|
+
return derived;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Compute one file's watch-input list over the shared per-envelope state. */
|
|
668
|
+
function deriveWatchInputs(
|
|
669
|
+
state: TtscEnvelopeDerivation,
|
|
670
|
+
props: {
|
|
671
|
+
file: string;
|
|
672
|
+
projectRoot: string;
|
|
673
|
+
result: ITtscCompilerTransformation;
|
|
674
|
+
temporaryTsconfig?: string;
|
|
675
|
+
},
|
|
676
|
+
fileIdentity: string,
|
|
677
|
+
): string[] {
|
|
678
|
+
const graph = envelopeGraphIndexes(state, props);
|
|
436
679
|
const output: string[] = [];
|
|
437
680
|
const seen = new Set<string>();
|
|
438
|
-
const excluded = new Set(
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
);
|
|
681
|
+
const excluded = new Set([fileIdentity]);
|
|
682
|
+
if (props.temporaryTsconfig !== undefined) {
|
|
683
|
+
excluded.add(derivationIdentity(state, props.temporaryTsconfig));
|
|
684
|
+
}
|
|
443
685
|
for (const absolute of [
|
|
444
686
|
...selectFileDependencies(props),
|
|
445
|
-
...selectGraphInputs({
|
|
687
|
+
...selectGraphInputs(graph, state, {
|
|
446
688
|
...props,
|
|
447
|
-
complete:
|
|
689
|
+
complete:
|
|
690
|
+
declaresCompleteDependencies(state, props) &&
|
|
691
|
+
!isVolatileFile(state, props),
|
|
448
692
|
}),
|
|
449
|
-
...selectResolutionCandidateInputs(props),
|
|
693
|
+
...selectResolutionCandidateInputs(graph, state, props),
|
|
450
694
|
]) {
|
|
451
|
-
const identity =
|
|
695
|
+
const identity = derivationIdentity(state, absolute);
|
|
452
696
|
if (excluded.has(identity) || seen.has(identity)) {
|
|
453
697
|
continue;
|
|
454
698
|
}
|
|
@@ -463,35 +707,37 @@ function selectWatchInputs(props: {
|
|
|
463
707
|
* module reachable from `file`. They remain host-owned even when a plugin
|
|
464
708
|
* declares `dependenciesComplete`: plugin code cannot vouch for a compiler
|
|
465
709
|
* resolution change that occurs without any plugin input changing.
|
|
710
|
+
*
|
|
711
|
+
* Candidate entries and their source identities come from the shared
|
|
712
|
+
* per-envelope state, so one delivery scans only the candidates themselves
|
|
713
|
+
* instead of re-resolving every candidate source.
|
|
466
714
|
*/
|
|
467
|
-
function selectResolutionCandidateInputs(
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (
|
|
715
|
+
function selectResolutionCandidateInputs(
|
|
716
|
+
graph: TtscEnvelopeGraphIndexes,
|
|
717
|
+
state: TtscEnvelopeDerivation,
|
|
718
|
+
props: {
|
|
719
|
+
file: string;
|
|
720
|
+
projectRoot: string;
|
|
721
|
+
result: ITtscCompilerTransformation;
|
|
722
|
+
},
|
|
723
|
+
): string[] {
|
|
724
|
+
if (
|
|
725
|
+
props.result.type === "exception" ||
|
|
726
|
+
props.result.graph?.candidates === undefined
|
|
727
|
+
) {
|
|
477
728
|
return [];
|
|
478
729
|
}
|
|
479
730
|
const reachable = new Set(
|
|
480
|
-
selectReachableSources(
|
|
481
|
-
|
|
731
|
+
selectReachableSources(graph, state, props.file).map((source) =>
|
|
732
|
+
derivationIdentity(state, source),
|
|
482
733
|
),
|
|
483
734
|
);
|
|
484
735
|
const output: string[] = [];
|
|
485
|
-
for (const
|
|
486
|
-
if (
|
|
487
|
-
!reachable.has(
|
|
488
|
-
pathIdentityKey(path.resolve(props.projectRoot, source)),
|
|
489
|
-
) ||
|
|
490
|
-
!Array.isArray(candidates)
|
|
491
|
-
) {
|
|
736
|
+
for (const entry of graph.candidates) {
|
|
737
|
+
if (!reachable.has(entry.source)) {
|
|
492
738
|
continue;
|
|
493
739
|
}
|
|
494
|
-
output.push(...
|
|
740
|
+
output.push(...entry.files);
|
|
495
741
|
}
|
|
496
742
|
return output;
|
|
497
743
|
}
|
|
@@ -510,62 +756,47 @@ function selectResolutionCandidateInputs(props: {
|
|
|
510
756
|
* `dependencies[file]` list the complete replacement for them. Returns an empty
|
|
511
757
|
* list on exceptions or without a graph.
|
|
512
758
|
*/
|
|
513
|
-
function selectGraphInputs(
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
if (graph === undefined) {
|
|
759
|
+
function selectGraphInputs(
|
|
760
|
+
graph: TtscEnvelopeGraphIndexes,
|
|
761
|
+
state: TtscEnvelopeDerivation,
|
|
762
|
+
props: {
|
|
763
|
+
complete: boolean;
|
|
764
|
+
file: string;
|
|
765
|
+
projectRoot: string;
|
|
766
|
+
result: ITtscCompilerTransformation;
|
|
767
|
+
},
|
|
768
|
+
): string[] {
|
|
769
|
+
if (props.result.type === "exception" || props.result.graph === undefined) {
|
|
524
770
|
return [];
|
|
525
771
|
}
|
|
526
772
|
const output: string[] = [];
|
|
527
773
|
if (!props.complete) {
|
|
528
|
-
output.push(...selectReachableEdges(
|
|
529
|
-
output.push(...
|
|
774
|
+
output.push(...selectReachableEdges(graph, state, props.file));
|
|
775
|
+
output.push(...graph.globals);
|
|
530
776
|
}
|
|
531
|
-
output.push(...
|
|
777
|
+
output.push(...graph.configs);
|
|
532
778
|
return output;
|
|
533
779
|
}
|
|
534
780
|
|
|
535
781
|
/**
|
|
536
782
|
* Walk the reachability closure of the graph's direct `edges` from `file`,
|
|
537
783
|
* returning the absolute path of every file reached (the starting file itself
|
|
538
|
-
* excluded, even when a cycle points back at it).
|
|
784
|
+
* excluded, even when a cycle points back at it). Reads the shared per-envelope
|
|
785
|
+
* edge index instead of rebuilding it per delivery.
|
|
539
786
|
*/
|
|
540
787
|
function selectReachableEdges(
|
|
541
|
-
|
|
788
|
+
graph: TtscEnvelopeGraphIndexes,
|
|
789
|
+
state: TtscEnvelopeDerivation,
|
|
542
790
|
file: string,
|
|
543
|
-
graph: ITtscCompilerTransformation.IReferenceGraph,
|
|
544
791
|
): string[] {
|
|
545
|
-
const edges = new Map<string, string[]>();
|
|
546
|
-
for (const [source, targets] of Object.entries(graph.edges ?? {})) {
|
|
547
|
-
if (!Array.isArray(targets)) {
|
|
548
|
-
continue;
|
|
549
|
-
}
|
|
550
|
-
const identity = pathIdentityKey(path.resolve(projectRoot, source));
|
|
551
|
-
const entries = edges.get(identity) ?? [];
|
|
552
|
-
entries.push(
|
|
553
|
-
...targets
|
|
554
|
-
.filter(
|
|
555
|
-
(target): target is string =>
|
|
556
|
-
typeof target === "string" && target.length !== 0,
|
|
557
|
-
)
|
|
558
|
-
.map((target) => path.resolve(projectRoot, target)),
|
|
559
|
-
);
|
|
560
|
-
edges.set(identity, entries);
|
|
561
|
-
}
|
|
562
792
|
const output: string[] = [];
|
|
563
|
-
const visited = new Set<string>([
|
|
793
|
+
const visited = new Set<string>([derivationIdentity(state, file)]);
|
|
564
794
|
const queue = [file];
|
|
565
795
|
while (queue.length !== 0) {
|
|
566
796
|
const current = queue.pop()!;
|
|
567
|
-
for (const target of edges.get(
|
|
568
|
-
|
|
797
|
+
for (const target of graph.edges.get(derivationIdentity(state, current)) ??
|
|
798
|
+
[]) {
|
|
799
|
+
const identity = derivationIdentity(state, target);
|
|
569
800
|
if (visited.has(identity)) {
|
|
570
801
|
continue;
|
|
571
802
|
}
|
|
@@ -583,43 +814,24 @@ function selectReachableEdges(
|
|
|
583
814
|
* than targets, so this is intentionally distinct from selectReachableEdges.
|
|
584
815
|
*/
|
|
585
816
|
function selectReachableSources(
|
|
586
|
-
|
|
817
|
+
graph: TtscEnvelopeGraphIndexes,
|
|
818
|
+
state: TtscEnvelopeDerivation,
|
|
587
819
|
file: string,
|
|
588
|
-
graph: ITtscCompilerTransformation.IReferenceGraph,
|
|
589
820
|
): string[] {
|
|
590
|
-
const edges = new Map<string, string[]>();
|
|
591
|
-
const spellings = new Map<string, string>();
|
|
592
|
-
for (const [source, targets] of Object.entries(graph.edges ?? {})) {
|
|
593
|
-
if (!Array.isArray(targets)) {
|
|
594
|
-
continue;
|
|
595
|
-
}
|
|
596
|
-
const absolute = path.resolve(projectRoot, source);
|
|
597
|
-
const identity = pathIdentityKey(absolute);
|
|
598
|
-
spellings.set(identity, absolute);
|
|
599
|
-
const entries = edges.get(identity) ?? [];
|
|
600
|
-
entries.push(
|
|
601
|
-
...targets
|
|
602
|
-
.filter(
|
|
603
|
-
(target): target is string =>
|
|
604
|
-
typeof target === "string" && target.length !== 0,
|
|
605
|
-
)
|
|
606
|
-
.map((target) => path.resolve(projectRoot, target)),
|
|
607
|
-
);
|
|
608
|
-
edges.set(identity, entries);
|
|
609
|
-
}
|
|
610
821
|
const output = [file];
|
|
611
|
-
const visited = new Set<string>([
|
|
822
|
+
const visited = new Set<string>([derivationIdentity(state, file)]);
|
|
612
823
|
const queue = [file];
|
|
613
824
|
while (queue.length !== 0) {
|
|
614
825
|
const current = queue.pop()!;
|
|
615
|
-
for (const target of edges.get(
|
|
616
|
-
|
|
826
|
+
for (const target of graph.edges.get(derivationIdentity(state, current)) ??
|
|
827
|
+
[]) {
|
|
828
|
+
const identity = derivationIdentity(state, target);
|
|
617
829
|
if (visited.has(identity)) {
|
|
618
830
|
continue;
|
|
619
831
|
}
|
|
620
832
|
visited.add(identity);
|
|
621
833
|
queue.push(target);
|
|
622
|
-
output.push(spellings.get(identity) ?? target);
|
|
834
|
+
output.push(graph.spellings.get(identity) ?? target);
|
|
623
835
|
}
|
|
624
836
|
}
|
|
625
837
|
return output;
|
|
@@ -647,17 +859,26 @@ function selectListedFiles(projectRoot: string, listed: unknown): string[] {
|
|
|
647
859
|
/**
|
|
648
860
|
* Report whether the plugin declared `file` volatile: its output depends on
|
|
649
861
|
* non-file inputs (environment, time, network), so neither the project
|
|
650
|
-
* transform cache nor a bundler's persistent cache may replay it.
|
|
862
|
+
* transform cache nor a bundler's persistent cache may replay it. Reads the
|
|
863
|
+
* per-envelope identity set instead of rescanning the member list.
|
|
651
864
|
*/
|
|
652
|
-
function isVolatileFile(
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
865
|
+
function isVolatileFile(
|
|
866
|
+
state: TtscEnvelopeDerivation,
|
|
867
|
+
props: {
|
|
868
|
+
file: string;
|
|
869
|
+
projectRoot: string;
|
|
870
|
+
result: ITtscCompilerTransformation;
|
|
871
|
+
},
|
|
872
|
+
): boolean {
|
|
657
873
|
if (props.result.type === "exception") {
|
|
658
874
|
return false;
|
|
659
875
|
}
|
|
660
|
-
|
|
876
|
+
const declared = (state.volatileFiles ??= collectDeclaredIdentities(
|
|
877
|
+
state,
|
|
878
|
+
props.projectRoot,
|
|
879
|
+
props.result.volatile,
|
|
880
|
+
));
|
|
881
|
+
return declared.has(derivationIdentity(state, props.file));
|
|
661
882
|
}
|
|
662
883
|
|
|
663
884
|
/**
|
|
@@ -666,44 +887,30 @@ function isVolatileFile(props: {
|
|
|
666
887
|
* itself and the universal config chain. Callers must still keep the baseline
|
|
667
888
|
* for a file the same envelope declared volatile.
|
|
668
889
|
*/
|
|
669
|
-
function declaresCompleteDependencies(
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
return declaresFile(props.result.dependenciesComplete, props);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* Report whether one of the envelope's transformed-file lists (`volatile`,
|
|
682
|
-
* `dependenciesComplete`) names `file`. Members are keyed like `typescript`, so
|
|
683
|
-
* a project-relative and an absolute spelling of the same file both match; a
|
|
684
|
-
* malformed member is ignored rather than fatal.
|
|
685
|
-
*/
|
|
686
|
-
function declaresFile(
|
|
687
|
-
listed: unknown,
|
|
688
|
-
props: { file: string; projectRoot: string },
|
|
890
|
+
function declaresCompleteDependencies(
|
|
891
|
+
state: TtscEnvelopeDerivation,
|
|
892
|
+
props: {
|
|
893
|
+
file: string;
|
|
894
|
+
projectRoot: string;
|
|
895
|
+
result: ITtscCompilerTransformation;
|
|
896
|
+
},
|
|
689
897
|
): boolean {
|
|
690
|
-
if (
|
|
898
|
+
if (props.result.type === "exception") {
|
|
691
899
|
return false;
|
|
692
900
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
);
|
|
901
|
+
const declared = (state.dependenciesComplete ??= collectDeclaredIdentities(
|
|
902
|
+
state,
|
|
903
|
+
props.projectRoot,
|
|
904
|
+
props.result.dependenciesComplete,
|
|
905
|
+
));
|
|
906
|
+
return declared.has(derivationIdentity(state, props.file));
|
|
700
907
|
}
|
|
701
908
|
|
|
702
909
|
/**
|
|
703
910
|
* Extract the absolute, deduplicated dependency list for a single file from the
|
|
704
911
|
* compiler result. Mirrors {@link selectTransformedSource}'s key lookup: fast
|
|
705
|
-
* project-relative match first, then a
|
|
706
|
-
* list on exceptions or when the plugin reported nothing.
|
|
912
|
+
* project-relative match first, then a per-envelope identity index. Returns an
|
|
913
|
+
* empty list on exceptions or when the plugin reported nothing.
|
|
707
914
|
*/
|
|
708
915
|
function selectFileDependencies(props: {
|
|
709
916
|
file: string;
|
|
@@ -717,31 +924,36 @@ function selectFileDependencies(props: {
|
|
|
717
924
|
if (dependencies === undefined) {
|
|
718
925
|
return [];
|
|
719
926
|
}
|
|
720
|
-
const
|
|
927
|
+
const state = envelopeDerivation(props);
|
|
928
|
+
const key = toProjectKey(
|
|
929
|
+
props.projectRoot,
|
|
930
|
+
props.file,
|
|
931
|
+
state.identityContext,
|
|
932
|
+
);
|
|
721
933
|
let entries = dependencies[key];
|
|
722
934
|
if (entries === undefined) {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
935
|
+
const index = (state.dependencyIndex ??= createEnvelopeKeyIndex(
|
|
936
|
+
state,
|
|
937
|
+
props.projectRoot,
|
|
938
|
+
dependencies,
|
|
939
|
+
));
|
|
940
|
+
entries = index.get(derivationIdentity(state, props.file)) as
|
|
941
|
+
| string[]
|
|
942
|
+
| undefined;
|
|
732
943
|
}
|
|
733
944
|
if (!Array.isArray(entries)) {
|
|
734
945
|
return [];
|
|
735
946
|
}
|
|
736
947
|
const output: string[] = [];
|
|
737
948
|
const seen = new Set<string>();
|
|
949
|
+
const fileIdentity = derivationIdentity(state, props.file);
|
|
738
950
|
for (const entry of entries) {
|
|
739
951
|
if (typeof entry !== "string" || entry.length === 0) {
|
|
740
952
|
continue;
|
|
741
953
|
}
|
|
742
954
|
const absolute = path.resolve(props.projectRoot, entry);
|
|
743
|
-
const identity =
|
|
744
|
-
if (identity ===
|
|
955
|
+
const identity = derivationIdentity(state, absolute);
|
|
956
|
+
if (identity === fileIdentity || seen.has(identity)) {
|
|
745
957
|
continue;
|
|
746
958
|
}
|
|
747
959
|
seen.add(identity);
|
|
@@ -750,6 +962,29 @@ function selectFileDependencies(props: {
|
|
|
750
962
|
return output;
|
|
751
963
|
}
|
|
752
964
|
|
|
965
|
+
/**
|
|
966
|
+
* Build a first-match identity index over one envelope key map (`typescript`,
|
|
967
|
+
* `dependencies`), mirroring the historical per-delivery scan that returned the
|
|
968
|
+
* first entry whose resolved key matched by filesystem identity.
|
|
969
|
+
*/
|
|
970
|
+
function createEnvelopeKeyIndex<T>(
|
|
971
|
+
state: TtscEnvelopeDerivation,
|
|
972
|
+
projectRoot: string,
|
|
973
|
+
keyed: Record<string, T>,
|
|
974
|
+
): Map<string, T> {
|
|
975
|
+
const index = new Map<string, T>();
|
|
976
|
+
for (const [candidate, value] of Object.entries(keyed)) {
|
|
977
|
+
const identity = derivationIdentity(
|
|
978
|
+
state,
|
|
979
|
+
path.resolve(projectRoot, candidate),
|
|
980
|
+
);
|
|
981
|
+
if (!index.has(identity)) {
|
|
982
|
+
index.set(identity, value);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return index;
|
|
986
|
+
}
|
|
987
|
+
|
|
753
988
|
/**
|
|
754
989
|
* Strip a query string or hash fragment from a bundler module id.
|
|
755
990
|
*
|
|
@@ -819,14 +1054,21 @@ function matchesCachedSource(
|
|
|
819
1054
|
source: string,
|
|
820
1055
|
buildScoped: boolean,
|
|
821
1056
|
): boolean {
|
|
822
|
-
const
|
|
1057
|
+
const identities = envelopeDerivation(cached).identityContext;
|
|
1058
|
+
const currentKey = toProjectKey(cached.projectRoot, file, identities);
|
|
823
1059
|
if (cached.inputHashes[currentKey] !== hashText(source)) {
|
|
824
1060
|
return false;
|
|
825
1061
|
}
|
|
826
|
-
if (
|
|
1062
|
+
if (
|
|
1063
|
+
buildScoped &&
|
|
1064
|
+
!cached.servedFiles?.has(pathIdentityKey(file, identities))
|
|
1065
|
+
) {
|
|
827
1066
|
return true;
|
|
828
1067
|
}
|
|
829
|
-
const currentHashes = collectProjectInputHashes(
|
|
1068
|
+
const currentHashes = collectProjectInputHashes(
|
|
1069
|
+
cached.projectRoot,
|
|
1070
|
+
identities,
|
|
1071
|
+
);
|
|
830
1072
|
currentHashes[currentKey] = hashText(source);
|
|
831
1073
|
if (!sameHashes(cached.inputHashes, currentHashes)) {
|
|
832
1074
|
return false;
|
|
@@ -853,7 +1095,9 @@ function markCachedSourceServed(
|
|
|
853
1095
|
cached: TtscCachedProjectTransform,
|
|
854
1096
|
file: string,
|
|
855
1097
|
): void {
|
|
856
|
-
(cached.servedFiles ??= new Set()).add(
|
|
1098
|
+
(cached.servedFiles ??= new Set()).add(
|
|
1099
|
+
pathIdentityKey(file, envelopeDerivation(cached).identityContext),
|
|
1100
|
+
);
|
|
857
1101
|
}
|
|
858
1102
|
|
|
859
1103
|
/**
|
|
@@ -874,11 +1118,11 @@ function collectInputHashes(props: {
|
|
|
874
1118
|
currentSource: string;
|
|
875
1119
|
projectRoot: string;
|
|
876
1120
|
}): Record<string, string> {
|
|
877
|
-
const
|
|
1121
|
+
const identities = createHostPathIdentityContext();
|
|
1122
|
+
const hashes = collectProjectInputHashes(props.projectRoot, identities);
|
|
878
1123
|
// Overlay the in-memory source so unsaved edits invalidate the cache.
|
|
879
|
-
hashes[toProjectKey(props.projectRoot, props.currentFile)] =
|
|
880
|
-
props.currentSource
|
|
881
|
-
);
|
|
1124
|
+
hashes[toProjectKey(props.projectRoot, props.currentFile, identities)] =
|
|
1125
|
+
hashText(props.currentSource);
|
|
882
1126
|
return hashes;
|
|
883
1127
|
}
|
|
884
1128
|
|
|
@@ -890,11 +1134,14 @@ function collectInputHashes(props: {
|
|
|
890
1134
|
*/
|
|
891
1135
|
export function collectProjectInputHashes(
|
|
892
1136
|
projectRoot: string,
|
|
1137
|
+
identities: FilesystemPathIdentityContext = createHostPathIdentityContext(),
|
|
893
1138
|
): Record<string, string> {
|
|
894
1139
|
const hashes: Record<string, string> = {};
|
|
895
1140
|
for (const file of listProjectInputFiles(projectRoot)) {
|
|
896
1141
|
try {
|
|
897
|
-
hashes[toProjectKey(projectRoot, file)] = hashText(
|
|
1142
|
+
hashes[toProjectKey(projectRoot, file, identities)] = hashText(
|
|
1143
|
+
fs.readFileSync(file),
|
|
1144
|
+
);
|
|
898
1145
|
} catch {
|
|
899
1146
|
// File watchers may observe a transform while another process is moving
|
|
900
1147
|
// or deleting files. The missing key invalidates older cache entries.
|
|
@@ -947,8 +1194,17 @@ function listProjectInputFiles(root: string): string[] {
|
|
|
947
1194
|
* Missing paths and files reached through symlinks or Windows junctions are
|
|
948
1195
|
* out-of-walk inputs that only the reference graph can prove relevant.
|
|
949
1196
|
*/
|
|
950
|
-
export function isProjectWalkPath(
|
|
951
|
-
|
|
1197
|
+
export function isProjectWalkPath(
|
|
1198
|
+
root: string,
|
|
1199
|
+
file: string,
|
|
1200
|
+
identities: FilesystemPathIdentityContext = createHostPathIdentityContext(),
|
|
1201
|
+
): boolean {
|
|
1202
|
+
if (!identities.isWithin(root, file)) {
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
const rootKey = pathIdentityKey(root, identities);
|
|
1206
|
+
const fileKey = pathIdentityKey(file, identities);
|
|
1207
|
+
const relative = fileKey.slice(rootKey.length).replace(/^[/\\]+/, "");
|
|
952
1208
|
if (
|
|
953
1209
|
relative.length === 0 ||
|
|
954
1210
|
relative === ".." ||
|
|
@@ -994,8 +1250,9 @@ export function collectExternalInputHashes(
|
|
|
994
1250
|
paths: readonly string[],
|
|
995
1251
|
): Record<string, string> {
|
|
996
1252
|
const hashes: Record<string, string> = {};
|
|
1253
|
+
const identities = createHostPathIdentityContext();
|
|
997
1254
|
for (const file of paths) {
|
|
998
|
-
const identity = pathIdentityKey(file);
|
|
1255
|
+
const identity = pathIdentityKey(file, identities);
|
|
999
1256
|
if (identity in hashes) {
|
|
1000
1257
|
continue;
|
|
1001
1258
|
}
|
|
@@ -1035,6 +1292,7 @@ function selectExternalInputPaths(props: {
|
|
|
1035
1292
|
return [];
|
|
1036
1293
|
}
|
|
1037
1294
|
const members: string[] = [];
|
|
1295
|
+
const identities = createHostPathIdentityContext();
|
|
1038
1296
|
const resolutionCandidates = new Set<string>();
|
|
1039
1297
|
const graph = props.result.graph;
|
|
1040
1298
|
if (graph !== undefined) {
|
|
@@ -1059,7 +1317,7 @@ function selectExternalInputPaths(props: {
|
|
|
1059
1317
|
}
|
|
1060
1318
|
const absolute = path.resolve(props.projectRoot, candidate);
|
|
1061
1319
|
members.push(candidate);
|
|
1062
|
-
resolutionCandidates.add(pathIdentityKey(absolute));
|
|
1320
|
+
resolutionCandidates.add(pathIdentityKey(absolute, identities));
|
|
1063
1321
|
}
|
|
1064
1322
|
}
|
|
1065
1323
|
}
|
|
@@ -1071,7 +1329,7 @@ function selectExternalInputPaths(props: {
|
|
|
1071
1329
|
const excluded =
|
|
1072
1330
|
props.temporaryTsconfig === undefined
|
|
1073
1331
|
? undefined
|
|
1074
|
-
: pathIdentityKey(props.temporaryTsconfig);
|
|
1332
|
+
: pathIdentityKey(props.temporaryTsconfig, identities);
|
|
1075
1333
|
const output: string[] = [];
|
|
1076
1334
|
const seen = new Set<string>();
|
|
1077
1335
|
for (const member of members) {
|
|
@@ -1079,13 +1337,14 @@ function selectExternalInputPaths(props: {
|
|
|
1079
1337
|
continue;
|
|
1080
1338
|
}
|
|
1081
1339
|
const absolute = path.resolve(props.projectRoot, member);
|
|
1082
|
-
const identity = pathIdentityKey(absolute);
|
|
1340
|
+
const identity = pathIdentityKey(absolute, identities);
|
|
1083
1341
|
const missingCandidate =
|
|
1084
1342
|
resolutionCandidates.has(identity) && !fs.existsSync(absolute);
|
|
1085
1343
|
if (
|
|
1086
1344
|
identity === excluded ||
|
|
1087
1345
|
seen.has(identity) ||
|
|
1088
|
-
(!missingCandidate &&
|
|
1346
|
+
(!missingCandidate &&
|
|
1347
|
+
isProjectWalkPath(props.projectRoot, absolute, identities))
|
|
1089
1348
|
) {
|
|
1090
1349
|
continue;
|
|
1091
1350
|
}
|
|
@@ -1474,19 +1733,26 @@ function selectTransformedSource(props: {
|
|
|
1474
1733
|
}
|
|
1475
1734
|
|
|
1476
1735
|
// Fast path: the compiler key matches the normalised project-relative path.
|
|
1477
|
-
const
|
|
1736
|
+
const state = envelopeDerivation(props);
|
|
1737
|
+
const key = toProjectKey(
|
|
1738
|
+
props.projectRoot,
|
|
1739
|
+
props.file,
|
|
1740
|
+
state.identityContext,
|
|
1741
|
+
);
|
|
1478
1742
|
const direct = props.result.typescript[key];
|
|
1479
1743
|
if (direct !== undefined) {
|
|
1480
1744
|
return direct;
|
|
1481
1745
|
}
|
|
1482
|
-
// Slow path:
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1746
|
+
// Slow path: the first-match identity index of the envelope's `typescript`
|
|
1747
|
+
// keys, built once per generation instead of scanned per delivery.
|
|
1748
|
+
const index = (state.outputIndex ??= createEnvelopeKeyIndex(
|
|
1749
|
+
state,
|
|
1750
|
+
props.projectRoot,
|
|
1751
|
+
props.result.typescript,
|
|
1752
|
+
));
|
|
1753
|
+
const source = index.get(derivationIdentity(state, props.file));
|
|
1754
|
+
if (source !== undefined) {
|
|
1755
|
+
return source;
|
|
1490
1756
|
}
|
|
1491
1757
|
throw new Error(`ttsc transform did not return output for ${props.file}`);
|
|
1492
1758
|
}
|
|
@@ -1583,10 +1849,17 @@ function resolveTsconfig(file: string, tsconfig?: string): string {
|
|
|
1583
1849
|
return path.resolve(process.cwd(), "tsconfig.json");
|
|
1584
1850
|
}
|
|
1585
1851
|
|
|
1586
|
-
function toProjectKey(
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
)
|
|
1852
|
+
function toProjectKey(
|
|
1853
|
+
root: string,
|
|
1854
|
+
file: string,
|
|
1855
|
+
identities: FilesystemPathIdentityContext = createHostPathIdentityContext(),
|
|
1856
|
+
): string {
|
|
1857
|
+
const rootKey = pathIdentityKey(root, identities);
|
|
1858
|
+
const fileKey = pathIdentityKey(file, identities);
|
|
1859
|
+
if (!identities.isWithin(root, file)) {
|
|
1860
|
+
return normalizePath(fileKey);
|
|
1861
|
+
}
|
|
1862
|
+
return normalizePath(fileKey.slice(rootKey.length).replace(/^[/\\]+/, ""));
|
|
1590
1863
|
}
|
|
1591
1864
|
|
|
1592
1865
|
/**
|
|
@@ -1594,55 +1867,11 @@ function toProjectKey(root: string, file: string): string {
|
|
|
1594
1867
|
* filesystem or bundler. Windows is case-insensitive; macOS is probed per
|
|
1595
1868
|
* existing filesystem location so case-sensitive volumes keep distinct paths.
|
|
1596
1869
|
*/
|
|
1597
|
-
export function pathIdentityKey(
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
function filesystemIsCaseInsensitive(file: string): boolean {
|
|
1605
|
-
if (process.platform === "win32") {
|
|
1606
|
-
return true;
|
|
1607
|
-
}
|
|
1608
|
-
if (process.platform !== "darwin") {
|
|
1609
|
-
return false;
|
|
1610
|
-
}
|
|
1611
|
-
let existing = file;
|
|
1612
|
-
while (!fs.existsSync(existing)) {
|
|
1613
|
-
const parent = path.dirname(existing);
|
|
1614
|
-
if (parent === existing) {
|
|
1615
|
-
return false;
|
|
1616
|
-
}
|
|
1617
|
-
existing = parent;
|
|
1618
|
-
}
|
|
1619
|
-
let resolved: string;
|
|
1620
|
-
try {
|
|
1621
|
-
resolved = fs.realpathSync.native(existing);
|
|
1622
|
-
} catch {
|
|
1623
|
-
return false;
|
|
1624
|
-
}
|
|
1625
|
-
const cached = CASE_INSENSITIVE_FILESYSTEMS.get(resolved);
|
|
1626
|
-
if (cached !== undefined) {
|
|
1627
|
-
return cached;
|
|
1628
|
-
}
|
|
1629
|
-
const alternate = togglePathCase(resolved);
|
|
1630
|
-
const insensitive = alternate !== undefined && fs.existsSync(alternate);
|
|
1631
|
-
CASE_INSENSITIVE_FILESYSTEMS.set(resolved, insensitive);
|
|
1632
|
-
return insensitive;
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
function togglePathCase(file: string): string | undefined {
|
|
1636
|
-
for (let index = file.length - 1; index >= 0; --index) {
|
|
1637
|
-
const character = file[index]!;
|
|
1638
|
-
if (character >= "a" && character <= "z") {
|
|
1639
|
-
return `${file.slice(0, index)}${character.toUpperCase()}${file.slice(index + 1)}`;
|
|
1640
|
-
}
|
|
1641
|
-
if (character >= "A" && character <= "Z") {
|
|
1642
|
-
return `${file.slice(0, index)}${character.toLowerCase()}${file.slice(index + 1)}`;
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
return undefined;
|
|
1870
|
+
export function pathIdentityKey(
|
|
1871
|
+
file: string,
|
|
1872
|
+
identities: FilesystemPathIdentityContext = createHostPathIdentityContext(),
|
|
1873
|
+
): string {
|
|
1874
|
+
return identities.resolve(file).key;
|
|
1646
1875
|
}
|
|
1647
1876
|
|
|
1648
1877
|
function normalizePath(file: string): string {
|