@ttsc/unplugin 0.19.3 → 0.20.1
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 +2 -0
- package/lib/core/index.js +39 -2
- package/lib/core/index.js.map +1 -1
- package/lib/core/index.mjs +39 -2
- package/lib/core/index.mjs.map +1 -1
- package/lib/core/transform.d.ts +29 -13
- package/lib/core/transform.js +194 -33
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +194 -34
- package/lib/core/transform.mjs.map +1 -1
- package/lib/core/tsconfigPaths.js +20 -12
- package/lib/core/tsconfigPaths.js.map +1 -1
- package/lib/core/tsconfigPaths.mjs +20 -12
- package/lib/core/tsconfigPaths.mjs.map +1 -1
- package/lib/core/viteServe.d.ts +74 -0
- package/lib/core/viteServe.js +180 -0
- package/lib/core/viteServe.js.map +1 -0
- package/lib/core/viteServe.mjs +178 -0
- package/lib/core/viteServe.mjs.map +1 -0
- package/package.json +3 -3
- package/src/core/index.ts +41 -2
- package/src/core/transform.ts +246 -46
- package/src/core/tsconfigPaths.ts +20 -12
- package/src/core/viteServe.ts +271 -0
package/src/core/transform.ts
CHANGED
|
@@ -53,7 +53,7 @@ export interface TtscTransformAlias {
|
|
|
53
53
|
export interface TtscCachedProjectTransform {
|
|
54
54
|
/**
|
|
55
55
|
* SHA-256 hash of every input the compiler reported outside the project walk
|
|
56
|
-
* (keyed by
|
|
56
|
+
* (keyed by filesystem identity), captured at the time of the transform.
|
|
57
57
|
*
|
|
58
58
|
* The project walk cannot see files outside the project root or under ignored
|
|
59
59
|
* directories (`node_modules` declarations, monorepo sibling sources,
|
|
@@ -65,6 +65,12 @@ export interface TtscCachedProjectTransform {
|
|
|
65
65
|
* `buildStart` and never replay across edits.
|
|
66
66
|
*/
|
|
67
67
|
externalInputHashes?: Record<string, string>;
|
|
68
|
+
/**
|
|
69
|
+
* Original absolute spellings of {@link externalInputHashes} inputs. These
|
|
70
|
+
* stay separate from their identity keys so validation reads the paths the
|
|
71
|
+
* compiler reported rather than a normalized replacement spelling.
|
|
72
|
+
*/
|
|
73
|
+
externalInputPaths?: string[];
|
|
68
74
|
/**
|
|
69
75
|
* SHA-256 hash of each project-relative input path at the time of the
|
|
70
76
|
* transform.
|
|
@@ -101,22 +107,27 @@ export function createTtscTransformCache(): TtscTransformCache {
|
|
|
101
107
|
return new Map();
|
|
102
108
|
}
|
|
103
109
|
|
|
110
|
+
/** Cached case-insensitivity probes for existing macOS filesystem locations. */
|
|
111
|
+
const CASE_INSENSITIVE_FILESYSTEMS = new Map<string, boolean>();
|
|
112
|
+
|
|
104
113
|
/**
|
|
105
114
|
* Hooks the bundler adapter passes into {@link transformTtsc} so transform
|
|
106
|
-
* side-channels (
|
|
107
|
-
* bundler without leaking extra fields on the returned
|
|
115
|
+
* side-channels (plugin-reported dependencies and host resolution candidates)
|
|
116
|
+
* reach the bundler without leaking extra fields on the returned
|
|
117
|
+
* `TransformResult`.
|
|
108
118
|
*/
|
|
109
119
|
export interface TtscTransformHooks {
|
|
110
120
|
/**
|
|
111
121
|
* Invoked once per absolute watch-input path derived for the transformed file
|
|
112
122
|
* `F`: the plugin-reported `dependencies[F]` list unioned with the host-owned
|
|
113
123
|
* reference graph's contribution — the reachability closure of `graph.edges`
|
|
114
|
-
* from `F`, the `graph.globals` files,
|
|
115
|
-
* for a file the envelope declared
|
|
116
|
-
* `dependencies[F]
|
|
117
|
-
* this to the bundler's
|
|
118
|
-
*
|
|
119
|
-
* for the exact
|
|
124
|
+
* from `F`, the `graph.globals` files, the `graph.configs` chain, and missing
|
|
125
|
+
* higher-priority `graph.candidates` — or, for a file the envelope declared
|
|
126
|
+
* `dependenciesComplete`, only `dependencies[F]`, `graph.candidates`, and the
|
|
127
|
+
* universal `graph.configs` chain. Adapters forward this to the bundler's
|
|
128
|
+
* `addWatchFile` so type-only inputs participate in watch-mode and
|
|
129
|
+
* persistent-cache invalidation. See {@link selectWatchInputs} for the exact
|
|
130
|
+
* derivation.
|
|
120
131
|
*/
|
|
121
132
|
addWatchFile?: (file: string) => void;
|
|
122
133
|
/**
|
|
@@ -313,7 +324,7 @@ function evictGeneration(
|
|
|
313
324
|
* Forward every derived watch input for `file` to the adapter's `addWatchFile`
|
|
314
325
|
* hook: the plugin-reported `dependencies[file]` list unioned with the
|
|
315
326
|
* host-owned reference graph's contribution (`reach(edges, file)`, `globals`,
|
|
316
|
-
* `configs
|
|
327
|
+
* `configs`, and resolution candidates).
|
|
317
328
|
*
|
|
318
329
|
* Envelope keys mirror the `typescript` keys (project-relative); values may be
|
|
319
330
|
* project-relative or absolute. Every path is absolutized against the project
|
|
@@ -344,7 +355,8 @@ function notifyWatchInputs(
|
|
|
344
355
|
*
|
|
345
356
|
* By default the derivation is a union: `dependencies[file] ∪ reach(edges,
|
|
346
357
|
* file) ∪ globals ∪ configs`. The plugin-reported list can only widen the
|
|
347
|
-
* host-owned language-semantic bound, never narrow it.
|
|
358
|
+
* host-owned language-semantic bound, never narrow it. Resolution candidates
|
|
359
|
+
* remain part of that host-owned bound in both modes.
|
|
348
360
|
*
|
|
349
361
|
* An envelope that lists `file` in `dependenciesComplete` narrows it to
|
|
350
362
|
* `dependencies[file] ∪ configs`: the plugin declared its reported list the
|
|
@@ -368,8 +380,8 @@ function selectWatchInputs(props: {
|
|
|
368
380
|
const seen = new Set<string>();
|
|
369
381
|
const excluded = new Set(
|
|
370
382
|
props.temporaryTsconfig === undefined
|
|
371
|
-
? [props.file]
|
|
372
|
-
: [props.file,
|
|
383
|
+
? [pathIdentityKey(props.file)]
|
|
384
|
+
: [pathIdentityKey(props.file), pathIdentityKey(props.temporaryTsconfig)],
|
|
373
385
|
);
|
|
374
386
|
for (const absolute of [
|
|
375
387
|
...selectFileDependencies(props),
|
|
@@ -377,16 +389,56 @@ function selectWatchInputs(props: {
|
|
|
377
389
|
...props,
|
|
378
390
|
complete: declaresCompleteDependencies(props) && !isVolatileFile(props),
|
|
379
391
|
}),
|
|
392
|
+
...selectResolutionCandidateInputs(props),
|
|
380
393
|
]) {
|
|
381
|
-
|
|
394
|
+
const identity = pathIdentityKey(absolute);
|
|
395
|
+
if (excluded.has(identity) || seen.has(identity)) {
|
|
382
396
|
continue;
|
|
383
397
|
}
|
|
384
|
-
seen.add(
|
|
398
|
+
seen.add(identity);
|
|
385
399
|
output.push(absolute);
|
|
386
400
|
}
|
|
387
401
|
return output;
|
|
388
402
|
}
|
|
389
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Return the module-resolution paths that can supersede a currently resolved
|
|
406
|
+
* module reachable from `file`. They remain host-owned even when a plugin
|
|
407
|
+
* declares `dependenciesComplete`: plugin code cannot vouch for a compiler
|
|
408
|
+
* resolution change that occurs without any plugin input changing.
|
|
409
|
+
*/
|
|
410
|
+
function selectResolutionCandidateInputs(props: {
|
|
411
|
+
file: string;
|
|
412
|
+
projectRoot: string;
|
|
413
|
+
result: ITtscCompilerTransformation;
|
|
414
|
+
}): string[] {
|
|
415
|
+
if (props.result.type === "exception") {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
const graph = props.result.graph;
|
|
419
|
+
if (graph === undefined || graph.candidates === undefined) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
const reachable = new Set(
|
|
423
|
+
selectReachableSources(props.projectRoot, props.file, graph).map(
|
|
424
|
+
pathIdentityKey,
|
|
425
|
+
),
|
|
426
|
+
);
|
|
427
|
+
const output: string[] = [];
|
|
428
|
+
for (const [source, candidates] of Object.entries(graph.candidates)) {
|
|
429
|
+
if (
|
|
430
|
+
!reachable.has(
|
|
431
|
+
pathIdentityKey(path.resolve(props.projectRoot, source)),
|
|
432
|
+
) ||
|
|
433
|
+
!Array.isArray(candidates)
|
|
434
|
+
) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
output.push(...selectListedFiles(props.projectRoot, candidates));
|
|
438
|
+
}
|
|
439
|
+
return output;
|
|
440
|
+
}
|
|
441
|
+
|
|
390
442
|
/**
|
|
391
443
|
* Flatten the host-owned reference graph for one file into absolute paths.
|
|
392
444
|
*
|
|
@@ -438,26 +490,29 @@ function selectReachableEdges(
|
|
|
438
490
|
if (!Array.isArray(targets)) {
|
|
439
491
|
continue;
|
|
440
492
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
493
|
+
const identity = pathIdentityKey(path.resolve(projectRoot, source));
|
|
494
|
+
const entries = edges.get(identity) ?? [];
|
|
495
|
+
entries.push(
|
|
496
|
+
...targets
|
|
444
497
|
.filter(
|
|
445
498
|
(target): target is string =>
|
|
446
499
|
typeof target === "string" && target.length !== 0,
|
|
447
500
|
)
|
|
448
501
|
.map((target) => path.resolve(projectRoot, target)),
|
|
449
502
|
);
|
|
503
|
+
edges.set(identity, entries);
|
|
450
504
|
}
|
|
451
505
|
const output: string[] = [];
|
|
452
|
-
const visited = new Set<string>([file]);
|
|
506
|
+
const visited = new Set<string>([pathIdentityKey(file)]);
|
|
453
507
|
const queue = [file];
|
|
454
508
|
while (queue.length !== 0) {
|
|
455
509
|
const current = queue.pop()!;
|
|
456
|
-
for (const target of edges.get(current) ?? []) {
|
|
457
|
-
|
|
510
|
+
for (const target of edges.get(pathIdentityKey(current)) ?? []) {
|
|
511
|
+
const identity = pathIdentityKey(target);
|
|
512
|
+
if (visited.has(identity)) {
|
|
458
513
|
continue;
|
|
459
514
|
}
|
|
460
|
-
visited.add(
|
|
515
|
+
visited.add(identity);
|
|
461
516
|
queue.push(target);
|
|
462
517
|
output.push(target);
|
|
463
518
|
}
|
|
@@ -465,6 +520,54 @@ function selectReachableEdges(
|
|
|
465
520
|
return output;
|
|
466
521
|
}
|
|
467
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Return the source files whose direct graph edges are reachable from `file`,
|
|
525
|
+
* including `file` itself. Resolution candidates belong to importers rather
|
|
526
|
+
* than targets, so this is intentionally distinct from selectReachableEdges.
|
|
527
|
+
*/
|
|
528
|
+
function selectReachableSources(
|
|
529
|
+
projectRoot: string,
|
|
530
|
+
file: string,
|
|
531
|
+
graph: ITtscCompilerTransformation.IReferenceGraph,
|
|
532
|
+
): string[] {
|
|
533
|
+
const edges = new Map<string, string[]>();
|
|
534
|
+
const spellings = new Map<string, string>();
|
|
535
|
+
for (const [source, targets] of Object.entries(graph.edges ?? {})) {
|
|
536
|
+
if (!Array.isArray(targets)) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const absolute = path.resolve(projectRoot, source);
|
|
540
|
+
const identity = pathIdentityKey(absolute);
|
|
541
|
+
spellings.set(identity, absolute);
|
|
542
|
+
const entries = edges.get(identity) ?? [];
|
|
543
|
+
entries.push(
|
|
544
|
+
...targets
|
|
545
|
+
.filter(
|
|
546
|
+
(target): target is string =>
|
|
547
|
+
typeof target === "string" && target.length !== 0,
|
|
548
|
+
)
|
|
549
|
+
.map((target) => path.resolve(projectRoot, target)),
|
|
550
|
+
);
|
|
551
|
+
edges.set(identity, entries);
|
|
552
|
+
}
|
|
553
|
+
const output = [file];
|
|
554
|
+
const visited = new Set<string>([pathIdentityKey(file)]);
|
|
555
|
+
const queue = [file];
|
|
556
|
+
while (queue.length !== 0) {
|
|
557
|
+
const current = queue.pop()!;
|
|
558
|
+
for (const target of edges.get(pathIdentityKey(current)) ?? []) {
|
|
559
|
+
const identity = pathIdentityKey(target);
|
|
560
|
+
if (visited.has(identity)) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
visited.add(identity);
|
|
564
|
+
queue.push(target);
|
|
565
|
+
output.push(spellings.get(identity) ?? target);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return output;
|
|
569
|
+
}
|
|
570
|
+
|
|
468
571
|
/**
|
|
469
572
|
* Absolutize one graph string list (`globals`, `configs`), skipping members a
|
|
470
573
|
* malformed envelope section may carry. Duplicates survive; the caller
|
|
@@ -534,7 +637,8 @@ function declaresFile(
|
|
|
534
637
|
(entry) =>
|
|
535
638
|
typeof entry === "string" &&
|
|
536
639
|
entry.length !== 0 &&
|
|
537
|
-
path.resolve(props.projectRoot, entry) ===
|
|
640
|
+
pathIdentityKey(path.resolve(props.projectRoot, entry)) ===
|
|
641
|
+
pathIdentityKey(props.file),
|
|
538
642
|
);
|
|
539
643
|
}
|
|
540
644
|
|
|
@@ -560,7 +664,10 @@ function selectFileDependencies(props: {
|
|
|
560
664
|
let entries = dependencies[key];
|
|
561
665
|
if (entries === undefined) {
|
|
562
666
|
for (const [candidate, candidateEntries] of Object.entries(dependencies)) {
|
|
563
|
-
if (
|
|
667
|
+
if (
|
|
668
|
+
pathIdentityKey(path.resolve(props.projectRoot, candidate)) ===
|
|
669
|
+
pathIdentityKey(props.file)
|
|
670
|
+
) {
|
|
564
671
|
entries = candidateEntries;
|
|
565
672
|
break;
|
|
566
673
|
}
|
|
@@ -576,10 +683,11 @@ function selectFileDependencies(props: {
|
|
|
576
683
|
continue;
|
|
577
684
|
}
|
|
578
685
|
const absolute = path.resolve(props.projectRoot, entry);
|
|
579
|
-
|
|
686
|
+
const identity = pathIdentityKey(absolute);
|
|
687
|
+
if (identity === pathIdentityKey(props.file) || seen.has(identity)) {
|
|
580
688
|
continue;
|
|
581
689
|
}
|
|
582
|
-
seen.add(
|
|
690
|
+
seen.add(identity);
|
|
583
691
|
output.push(absolute);
|
|
584
692
|
}
|
|
585
693
|
return output;
|
|
@@ -678,7 +786,9 @@ function matchesCachedSource(
|
|
|
678
786
|
const externalHashes = cached.externalInputHashes ?? {};
|
|
679
787
|
return sameHashes(
|
|
680
788
|
externalHashes,
|
|
681
|
-
collectExternalInputHashes(
|
|
789
|
+
collectExternalInputHashes(
|
|
790
|
+
cached.externalInputPaths ?? Object.keys(externalHashes),
|
|
791
|
+
),
|
|
682
792
|
);
|
|
683
793
|
}
|
|
684
794
|
|
|
@@ -773,7 +883,7 @@ function listProjectInputFiles(root: string): string[] {
|
|
|
773
883
|
* only the reference graph can prove relevant.
|
|
774
884
|
*/
|
|
775
885
|
export function isProjectWalkPath(root: string, file: string): boolean {
|
|
776
|
-
const relative = path.relative(
|
|
886
|
+
const relative = path.relative(pathIdentityKey(root), pathIdentityKey(file));
|
|
777
887
|
if (
|
|
778
888
|
relative.length === 0 ||
|
|
779
889
|
relative === ".." ||
|
|
@@ -789,20 +899,26 @@ export function isProjectWalkPath(root: string, file: string): boolean {
|
|
|
789
899
|
|
|
790
900
|
/**
|
|
791
901
|
* Hash a list of absolute out-of-walk input paths: content SHA-256 for a
|
|
792
|
-
* readable file, a stable `missing` marker otherwise.
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
*
|
|
902
|
+
* readable file, a stable `missing` marker otherwise. Keys use filesystem
|
|
903
|
+
* identity so case-only spellings share one snapshot entry, while reads retain
|
|
904
|
+
* the original path supplied by the compiler. The marker is state, not an error
|
|
905
|
+
* — a recorded input disappearing (or reappearing) must change the comparison
|
|
906
|
+
* exactly like a content edit. Exported so `@ttsc/metro` can re-hash its
|
|
907
|
+
* recorded snapshot with identical semantics at cache-key time.
|
|
796
908
|
*/
|
|
797
909
|
export function collectExternalInputHashes(
|
|
798
910
|
paths: readonly string[],
|
|
799
911
|
): Record<string, string> {
|
|
800
912
|
const hashes: Record<string, string> = {};
|
|
801
913
|
for (const file of paths) {
|
|
914
|
+
const identity = pathIdentityKey(file);
|
|
915
|
+
if (identity in hashes) {
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
802
918
|
try {
|
|
803
|
-
hashes[
|
|
919
|
+
hashes[identity] = hashText(fs.readFileSync(file));
|
|
804
920
|
} catch {
|
|
805
|
-
hashes[
|
|
921
|
+
hashes[identity] = "missing";
|
|
806
922
|
}
|
|
807
923
|
}
|
|
808
924
|
return hashes;
|
|
@@ -813,7 +929,9 @@ export function collectExternalInputHashes(
|
|
|
813
929
|
* union of every reference-graph member (edge keys and targets, globals, the
|
|
814
930
|
* config chain) and every plugin-reported dependency, minus everything the
|
|
815
931
|
* project walk already hashes and the disposed temp-dir tsconfig. These are the
|
|
816
|
-
* inputs {@link matchesCachedSource}'s walk cannot see.
|
|
932
|
+
* inputs {@link matchesCachedSource}'s walk cannot see. Resolution candidates
|
|
933
|
+
* that are still missing remain in this set even under the project root: the
|
|
934
|
+
* first walk cannot hash a file that has not been created yet.
|
|
817
935
|
*
|
|
818
936
|
* A `dependenciesComplete` declaration deliberately does not narrow this set,
|
|
819
937
|
* unlike the per-file watch derivation. This cache replays one whole envelope,
|
|
@@ -833,6 +951,7 @@ function selectExternalInputPaths(props: {
|
|
|
833
951
|
return [];
|
|
834
952
|
}
|
|
835
953
|
const members: string[] = [];
|
|
954
|
+
const resolutionCandidates = new Set<string>();
|
|
836
955
|
const graph = props.result.graph;
|
|
837
956
|
if (graph !== undefined) {
|
|
838
957
|
for (const [source, targets] of Object.entries(graph.edges ?? {})) {
|
|
@@ -846,6 +965,19 @@ function selectExternalInputPaths(props: {
|
|
|
846
965
|
members.push(...listed);
|
|
847
966
|
}
|
|
848
967
|
}
|
|
968
|
+
for (const candidates of Object.values(graph.candidates ?? {})) {
|
|
969
|
+
if (!Array.isArray(candidates)) {
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
for (const candidate of candidates) {
|
|
973
|
+
if (typeof candidate !== "string" || candidate.length === 0) {
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const absolute = path.resolve(props.projectRoot, candidate);
|
|
977
|
+
members.push(candidate);
|
|
978
|
+
resolutionCandidates.add(pathIdentityKey(absolute));
|
|
979
|
+
}
|
|
980
|
+
}
|
|
849
981
|
}
|
|
850
982
|
for (const entries of Object.values(props.result.dependencies ?? {})) {
|
|
851
983
|
if (Array.isArray(entries)) {
|
|
@@ -855,7 +987,7 @@ function selectExternalInputPaths(props: {
|
|
|
855
987
|
const excluded =
|
|
856
988
|
props.temporaryTsconfig === undefined
|
|
857
989
|
? undefined
|
|
858
|
-
:
|
|
990
|
+
: pathIdentityKey(props.temporaryTsconfig);
|
|
859
991
|
const output: string[] = [];
|
|
860
992
|
const seen = new Set<string>();
|
|
861
993
|
for (const member of members) {
|
|
@@ -863,14 +995,17 @@ function selectExternalInputPaths(props: {
|
|
|
863
995
|
continue;
|
|
864
996
|
}
|
|
865
997
|
const absolute = path.resolve(props.projectRoot, member);
|
|
998
|
+
const identity = pathIdentityKey(absolute);
|
|
999
|
+
const missingCandidate =
|
|
1000
|
+
resolutionCandidates.has(identity) && !fs.existsSync(absolute);
|
|
866
1001
|
if (
|
|
867
|
-
|
|
868
|
-
seen.has(
|
|
869
|
-
isProjectWalkPath(props.projectRoot, absolute)
|
|
1002
|
+
identity === excluded ||
|
|
1003
|
+
seen.has(identity) ||
|
|
1004
|
+
(!missingCandidate && isProjectWalkPath(props.projectRoot, absolute))
|
|
870
1005
|
) {
|
|
871
1006
|
continue;
|
|
872
1007
|
}
|
|
873
|
-
seen.add(
|
|
1008
|
+
seen.add(identity);
|
|
874
1009
|
output.push(absolute);
|
|
875
1010
|
}
|
|
876
1011
|
output.sort();
|
|
@@ -939,13 +1074,17 @@ async function transformProject(props: {
|
|
|
939
1074
|
}).transform();
|
|
940
1075
|
const temporaryTsconfig =
|
|
941
1076
|
configured.path === props.tsconfig ? undefined : configured.path;
|
|
1077
|
+
const externalInputPaths = selectExternalInputPaths({
|
|
1078
|
+
projectRoot,
|
|
1079
|
+
result,
|
|
1080
|
+
temporaryTsconfig,
|
|
1081
|
+
});
|
|
942
1082
|
return {
|
|
943
1083
|
// Capture the out-of-walk input hashes while the generation is fresh so
|
|
944
1084
|
// cache validation can re-check them; computed before dispose so the
|
|
945
1085
|
// exclusion of the temp-dir tsconfig is the only reason it never keys.
|
|
946
|
-
externalInputHashes: collectExternalInputHashes(
|
|
947
|
-
|
|
948
|
-
),
|
|
1086
|
+
externalInputHashes: collectExternalInputHashes(externalInputPaths),
|
|
1087
|
+
externalInputPaths,
|
|
949
1088
|
inputHashes: collectInputHashes({
|
|
950
1089
|
currentFile: props.currentFile,
|
|
951
1090
|
currentSource: props.currentSource,
|
|
@@ -1181,7 +1320,7 @@ function createTransformCacheKey(props: {
|
|
|
1181
1320
|
aliasPaths: props.aliasPaths,
|
|
1182
1321
|
compilerOptions: props.compilerOptions,
|
|
1183
1322
|
plugins: props.plugins,
|
|
1184
|
-
tsconfig:
|
|
1323
|
+
tsconfig: pathIdentityKey(props.tsconfig),
|
|
1185
1324
|
});
|
|
1186
1325
|
}
|
|
1187
1326
|
|
|
@@ -1257,7 +1396,10 @@ function selectTransformedSource(props: {
|
|
|
1257
1396
|
}
|
|
1258
1397
|
// Slow path: resolve each candidate to an absolute path for comparison.
|
|
1259
1398
|
for (const [candidate, source] of Object.entries(props.result.typescript)) {
|
|
1260
|
-
if (
|
|
1399
|
+
if (
|
|
1400
|
+
pathIdentityKey(path.resolve(props.projectRoot, candidate)) ===
|
|
1401
|
+
pathIdentityKey(props.file)
|
|
1402
|
+
) {
|
|
1261
1403
|
return source;
|
|
1262
1404
|
}
|
|
1263
1405
|
}
|
|
@@ -1357,7 +1499,65 @@ function resolveTsconfig(file: string, tsconfig?: string): string {
|
|
|
1357
1499
|
}
|
|
1358
1500
|
|
|
1359
1501
|
function toProjectKey(root: string, file: string): string {
|
|
1360
|
-
return normalizePath(
|
|
1502
|
+
return normalizePath(
|
|
1503
|
+
path.relative(pathIdentityKey(root), pathIdentityKey(file)),
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* Build a comparison key for a path without changing the spelling handed to a
|
|
1509
|
+
* filesystem or bundler. Windows is case-insensitive; macOS is probed per
|
|
1510
|
+
* existing filesystem location so case-sensitive volumes keep distinct paths.
|
|
1511
|
+
*/
|
|
1512
|
+
export function pathIdentityKey(file: string): string {
|
|
1513
|
+
const absolute = path.resolve(file);
|
|
1514
|
+
return filesystemIsCaseInsensitive(absolute)
|
|
1515
|
+
? absolute.toLowerCase()
|
|
1516
|
+
: absolute;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function filesystemIsCaseInsensitive(file: string): boolean {
|
|
1520
|
+
if (process.platform === "win32") {
|
|
1521
|
+
return true;
|
|
1522
|
+
}
|
|
1523
|
+
if (process.platform !== "darwin") {
|
|
1524
|
+
return false;
|
|
1525
|
+
}
|
|
1526
|
+
let existing = file;
|
|
1527
|
+
while (!fs.existsSync(existing)) {
|
|
1528
|
+
const parent = path.dirname(existing);
|
|
1529
|
+
if (parent === existing) {
|
|
1530
|
+
return false;
|
|
1531
|
+
}
|
|
1532
|
+
existing = parent;
|
|
1533
|
+
}
|
|
1534
|
+
let resolved: string;
|
|
1535
|
+
try {
|
|
1536
|
+
resolved = fs.realpathSync.native(existing);
|
|
1537
|
+
} catch {
|
|
1538
|
+
return false;
|
|
1539
|
+
}
|
|
1540
|
+
const cached = CASE_INSENSITIVE_FILESYSTEMS.get(resolved);
|
|
1541
|
+
if (cached !== undefined) {
|
|
1542
|
+
return cached;
|
|
1543
|
+
}
|
|
1544
|
+
const alternate = togglePathCase(resolved);
|
|
1545
|
+
const insensitive = alternate !== undefined && fs.existsSync(alternate);
|
|
1546
|
+
CASE_INSENSITIVE_FILESYSTEMS.set(resolved, insensitive);
|
|
1547
|
+
return insensitive;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function togglePathCase(file: string): string | undefined {
|
|
1551
|
+
for (let index = file.length - 1; index >= 0; --index) {
|
|
1552
|
+
const character = file[index]!;
|
|
1553
|
+
if (character >= "a" && character <= "z") {
|
|
1554
|
+
return `${file.slice(0, index)}${character.toUpperCase()}${file.slice(index + 1)}`;
|
|
1555
|
+
}
|
|
1556
|
+
if (character >= "A" && character <= "Z") {
|
|
1557
|
+
return `${file.slice(0, index)}${character.toLowerCase()}${file.slice(index + 1)}`;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return undefined;
|
|
1361
1561
|
}
|
|
1362
1562
|
|
|
1363
1563
|
function normalizePath(file: string): string {
|
|
@@ -130,10 +130,10 @@ function extendsSpecifiers(extended: unknown): string[] {
|
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
132
|
* Resolve an `extends` specifier to an absolute config path using TypeScript's
|
|
133
|
-
* rules: absolute paths and relative specifiers get `.json`
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
133
|
+
* rules: absolute paths and relative specifiers get an exact-file / `.json`
|
|
134
|
+
* fallback; bare specifiers go through Node's module resolver scoped to the
|
|
135
|
+
* declaring config. Returns `null` instead of throwing; the compiler reports
|
|
136
|
+
* unresolvable `extends` itself.
|
|
137
137
|
*/
|
|
138
138
|
function resolveExtendsConfig(
|
|
139
139
|
tsconfig: string,
|
|
@@ -221,22 +221,30 @@ function isBarePackageRoot(specifier: string): boolean {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
|
-
* Try an on-disk `extends` location as-is,
|
|
225
|
-
*
|
|
224
|
+
* Try an on-disk `extends` location as-is and, unless it already ends in
|
|
225
|
+
* `.json`, with that extension appended. A directory is never a config file and
|
|
226
|
+
* cannot be expanded to `tsconfig.json` or a double `.json` suffix.
|
|
226
227
|
*/
|
|
227
228
|
function resolveExistingExtendsPath(location: string): string | null {
|
|
228
|
-
|
|
229
|
-
location
|
|
230
|
-
`${location}.json
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (fs.existsSync(candidate)) {
|
|
229
|
+
const candidates = location.endsWith(".json")
|
|
230
|
+
? [location]
|
|
231
|
+
: [location, `${location}.json`];
|
|
232
|
+
for (const candidate of candidates) {
|
|
233
|
+
if (isFile(candidate)) {
|
|
234
234
|
return resolveRealPath(candidate);
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
return null;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
function isFile(location: string): boolean {
|
|
241
|
+
try {
|
|
242
|
+
return fs.statSync(location).isFile();
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
240
248
|
/**
|
|
241
249
|
* Return true when `specifier` is a relative path reference: `.`, `..`, or a
|
|
242
250
|
* string starting with `./`, `../`, `.\\`, or `..\\`.
|