@ttsc/unplugin 0.15.2 → 0.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/lib/core/index.d.ts +1 -0
- package/lib/core/index.js +7 -1
- package/lib/core/index.js.map +1 -1
- package/lib/core/index.mjs +7 -1
- package/lib/core/index.mjs.map +1 -1
- package/lib/core/transform.d.ts +18 -1
- package/lib/core/transform.js +108 -32
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +108 -32
- package/lib/core/transform.mjs.map +1 -1
- package/lib/core/tsconfigPaths.d.ts +29 -0
- package/lib/core/tsconfigPaths.js +309 -0
- package/lib/core/tsconfigPaths.js.map +1 -0
- package/lib/core/tsconfigPaths.mjs +306 -0
- package/lib/core/tsconfigPaths.mjs.map +1 -0
- package/lib/turbopack.d.ts +43 -0
- package/lib/turbopack.js +55 -0
- package/lib/turbopack.js.map +1 -0
- package/lib/turbopack.mjs +51 -0
- package/lib/turbopack.mjs.map +1 -0
- package/package.json +8 -3
- package/src/core/index.ts +8 -1
- package/src/core/transform.ts +145 -49
- package/src/core/tsconfigPaths.ts +338 -0
- package/src/turbopack.ts +84 -0
package/src/core/transform.ts
CHANGED
|
@@ -10,6 +10,10 @@ import { TtscCompiler } from "ttsc";
|
|
|
10
10
|
import type { TransformResult } from "unplugin";
|
|
11
11
|
|
|
12
12
|
import type { ResolvedTtscUnpluginOptions } from "./options";
|
|
13
|
+
import {
|
|
14
|
+
absolutizePathsTarget,
|
|
15
|
+
readEffectiveTsconfigPaths,
|
|
16
|
+
} from "./tsconfigPaths";
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* The normalised transform result type that this module produces.
|
|
@@ -71,6 +75,21 @@ export function createTtscTransformCache(): TtscTransformCache {
|
|
|
71
75
|
return new Map();
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Hooks the bundler adapter passes into {@link transformTtsc} so transform
|
|
80
|
+
* side-channels (currently the plugin-reported dependency list) reach the
|
|
81
|
+
* bundler without leaking extra fields on the returned `TransformResult`.
|
|
82
|
+
*/
|
|
83
|
+
export interface TtscTransformHooks {
|
|
84
|
+
/**
|
|
85
|
+
* Invoked once per absolute dependency path the plugin reported for the
|
|
86
|
+
* transformed file (`dependencies` in the transform envelope). Adapters
|
|
87
|
+
* forward this to the bundler's `addWatchFile` so type-only inputs
|
|
88
|
+
* participate in HMR invalidation.
|
|
89
|
+
*/
|
|
90
|
+
addWatchFile?: (file: string) => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
74
93
|
/**
|
|
75
94
|
* Apply the ttsc plugin transform to a single source file.
|
|
76
95
|
*
|
|
@@ -90,6 +109,9 @@ export function createTtscTransformCache(): TtscTransformCache {
|
|
|
90
109
|
* object).
|
|
91
110
|
* @param cache - Optional per-build cache; cleared by the caller on
|
|
92
111
|
* `buildStart`.
|
|
112
|
+
* @param hooks - Optional adapter callbacks; see {@link TtscTransformHooks}.
|
|
113
|
+
* Dependency notifications fire on cache hits too — watch registrations are
|
|
114
|
+
* per build, not per compilation.
|
|
93
115
|
*/
|
|
94
116
|
export async function transformTtsc(
|
|
95
117
|
id: string,
|
|
@@ -97,6 +119,7 @@ export async function transformTtsc(
|
|
|
97
119
|
options: ResolvedTtscUnpluginOptions,
|
|
98
120
|
aliases?: unknown,
|
|
99
121
|
cache?: TtscTransformCache,
|
|
122
|
+
hooks?: TtscTransformHooks,
|
|
100
123
|
): Promise<TtscTransformResult | undefined> {
|
|
101
124
|
const clean = stripQuery(id);
|
|
102
125
|
if (clean.includes("\0")) {
|
|
@@ -111,9 +134,7 @@ export async function transformTtsc(
|
|
|
111
134
|
}
|
|
112
135
|
|
|
113
136
|
const tsconfig = resolveTsconfig(file, options.project);
|
|
114
|
-
const
|
|
115
|
-
const baseUrl = resolveBaseUrl(tsconfigDir, options.compilerOptions);
|
|
116
|
-
const aliasPaths = createAliasPaths(baseUrl, aliases);
|
|
137
|
+
const aliasPaths = createAliasPaths(aliases);
|
|
117
138
|
const key = createTransformCacheKey({
|
|
118
139
|
aliasPaths,
|
|
119
140
|
compilerOptions: options.compilerOptions,
|
|
@@ -131,6 +152,11 @@ export async function transformTtsc(
|
|
|
131
152
|
projectRoot: cached.projectRoot,
|
|
132
153
|
result: cached.result,
|
|
133
154
|
});
|
|
155
|
+
notifyFileDependencies(hooks, {
|
|
156
|
+
file,
|
|
157
|
+
projectRoot: cached.projectRoot,
|
|
158
|
+
result: cached.result,
|
|
159
|
+
});
|
|
134
160
|
return createTransformResult(source, code);
|
|
135
161
|
}
|
|
136
162
|
cache?.delete(key);
|
|
@@ -140,7 +166,6 @@ export async function transformTtsc(
|
|
|
140
166
|
if (transformed === undefined) {
|
|
141
167
|
transformed = transformProject({
|
|
142
168
|
aliasPaths,
|
|
143
|
-
baseUrl,
|
|
144
169
|
compilerOptions: options.compilerOptions,
|
|
145
170
|
currentFile: file,
|
|
146
171
|
currentSource: source,
|
|
@@ -152,9 +177,83 @@ export async function transformTtsc(
|
|
|
152
177
|
const { projectRoot, result } = await transformed;
|
|
153
178
|
reportSuccessDiagnostics(result);
|
|
154
179
|
const code = selectTransformedSource({ file, projectRoot, result });
|
|
180
|
+
notifyFileDependencies(hooks, { file, projectRoot, result });
|
|
155
181
|
return createTransformResult(source, code);
|
|
156
182
|
}
|
|
157
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Forward the plugin-reported dependency list for `file` to the adapter's
|
|
186
|
+
* `addWatchFile` hook.
|
|
187
|
+
*
|
|
188
|
+
* The transform envelope's `dependencies` keys mirror the `typescript` keys
|
|
189
|
+
* (project-relative); values may be project-relative or absolute. Every path is
|
|
190
|
+
* absolutized against the project root and deduplicated, and the file itself is
|
|
191
|
+
* dropped — the bundler already watches the module it transforms.
|
|
192
|
+
*/
|
|
193
|
+
function notifyFileDependencies(
|
|
194
|
+
hooks: TtscTransformHooks | undefined,
|
|
195
|
+
props: {
|
|
196
|
+
file: string;
|
|
197
|
+
projectRoot: string;
|
|
198
|
+
result: ITtscCompilerTransformation;
|
|
199
|
+
},
|
|
200
|
+
): void {
|
|
201
|
+
const addWatchFile = hooks?.addWatchFile;
|
|
202
|
+
if (addWatchFile === undefined) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
for (const dependency of selectFileDependencies(props)) {
|
|
206
|
+
addWatchFile(dependency);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Extract the absolute, deduplicated dependency list for a single file from the
|
|
212
|
+
* compiler result. Mirrors {@link selectTransformedSource}'s key lookup: fast
|
|
213
|
+
* project-relative match first, then a resolve-based scan. Returns an empty
|
|
214
|
+
* list on exceptions or when the plugin reported nothing.
|
|
215
|
+
*/
|
|
216
|
+
function selectFileDependencies(props: {
|
|
217
|
+
file: string;
|
|
218
|
+
projectRoot: string;
|
|
219
|
+
result: ITtscCompilerTransformation;
|
|
220
|
+
}): string[] {
|
|
221
|
+
if (props.result.type === "exception") {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
const dependencies = props.result.dependencies;
|
|
225
|
+
if (dependencies === undefined) {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
const key = toProjectKey(props.projectRoot, props.file);
|
|
229
|
+
let entries = dependencies[key];
|
|
230
|
+
if (entries === undefined) {
|
|
231
|
+
for (const [candidate, candidateEntries] of Object.entries(dependencies)) {
|
|
232
|
+
if (path.resolve(props.projectRoot, candidate) === props.file) {
|
|
233
|
+
entries = candidateEntries;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (!Array.isArray(entries)) {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
const output: string[] = [];
|
|
242
|
+
const seen = new Set<string>();
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const absolute = path.resolve(props.projectRoot, entry);
|
|
248
|
+
if (absolute === props.file || seen.has(absolute)) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
seen.add(absolute);
|
|
252
|
+
output.push(absolute);
|
|
253
|
+
}
|
|
254
|
+
return output;
|
|
255
|
+
}
|
|
256
|
+
|
|
158
257
|
/**
|
|
159
258
|
* Strip a query string or hash fragment from a bundler module id.
|
|
160
259
|
*
|
|
@@ -337,7 +436,6 @@ function hashText(input: string | Buffer): string {
|
|
|
337
436
|
|
|
338
437
|
async function transformProject(props: {
|
|
339
438
|
aliasPaths: Record<string, string[]>;
|
|
340
|
-
baseUrl: string;
|
|
341
439
|
compilerOptions: Record<string, unknown>;
|
|
342
440
|
currentFile: string;
|
|
343
441
|
currentSource: string;
|
|
@@ -370,7 +468,6 @@ async function transformProject(props: {
|
|
|
370
468
|
|
|
371
469
|
function createTransformTsconfig(props: {
|
|
372
470
|
aliasPaths: Record<string, string[]>;
|
|
373
|
-
baseUrl: string;
|
|
374
471
|
compilerOptions: Record<string, unknown>;
|
|
375
472
|
tsconfig: string;
|
|
376
473
|
}): { path: string; dispose: () => void } {
|
|
@@ -416,9 +513,10 @@ function createTransformTsconfig(props: {
|
|
|
416
513
|
* tsconfig must be converted to an absolute path before writing the generated
|
|
417
514
|
* file. Otherwise TypeScript-Go resolves it against the temp dir.
|
|
418
515
|
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
* `
|
|
516
|
+
* `paths` targets are absolutized for the same reason, with the extra twist
|
|
517
|
+
* that TypeScript-Go rejects bare non-relative targets outright (TS5090) and
|
|
518
|
+
* has removed `baseUrl` (TS5102), so anchoring them as absolute paths is the
|
|
519
|
+
* only temp-dir-safe encoding. No synthetic `baseUrl` is ever written.
|
|
422
520
|
*/
|
|
423
521
|
function normalizeCompilerOptionsForGeneratedTsconfig(
|
|
424
522
|
compilerOptions: Record<string, unknown>,
|
|
@@ -426,7 +524,7 @@ function normalizeCompilerOptionsForGeneratedTsconfig(
|
|
|
426
524
|
): Record<string, unknown> {
|
|
427
525
|
const output = { ...compilerOptions };
|
|
428
526
|
// Scalar path fields: resolve each against the original tsconfig directory.
|
|
429
|
-
for (const key of ["
|
|
527
|
+
for (const key of ["declarationDir", "outDir", "rootDir"]) {
|
|
430
528
|
if (typeof output[key] === "string") {
|
|
431
529
|
output[key] = path.resolve(tsconfigDir, output[key]);
|
|
432
530
|
}
|
|
@@ -439,8 +537,14 @@ function normalizeCompilerOptionsForGeneratedTsconfig(
|
|
|
439
537
|
);
|
|
440
538
|
}
|
|
441
539
|
}
|
|
442
|
-
|
|
443
|
-
|
|
540
|
+
const paths = readPaths(output.paths);
|
|
541
|
+
if (Object.keys(paths).length !== 0) {
|
|
542
|
+
output.paths = Object.fromEntries(
|
|
543
|
+
Object.entries(paths).map(([key, targets]) => [
|
|
544
|
+
key,
|
|
545
|
+
targets.map((target) => absolutizePathsTarget(tsconfigDir, target)),
|
|
546
|
+
]),
|
|
547
|
+
);
|
|
444
548
|
}
|
|
445
549
|
if (Array.isArray(output.plugins)) {
|
|
446
550
|
output.plugins = output.plugins.map((entry) =>
|
|
@@ -467,32 +571,37 @@ function normalizePluginConfigForGeneratedTsconfig(
|
|
|
467
571
|
return output;
|
|
468
572
|
}
|
|
469
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Build the `paths` overlay that forwards bundler aliases to the compiler.
|
|
576
|
+
*
|
|
577
|
+
* Because the generated tsconfig `extends` the project one and TypeScript
|
|
578
|
+
* merges `compilerOptions` per option key, declaring `paths` here replaces the
|
|
579
|
+
* project's own `paths` wholesale. The overlay therefore re-states the
|
|
580
|
+
* project's effective mappings first, so tsconfig-only aliases keep resolving;
|
|
581
|
+
* inline `compilerOptions.paths` from the plugin options ride on top, and the
|
|
582
|
+
* bundler aliases win last — they mirror what the bundler will actually do at
|
|
583
|
+
* resolve time.
|
|
584
|
+
*
|
|
585
|
+
* No `baseUrl` is emitted: TypeScript-Go removed the option (TS5102), and all
|
|
586
|
+
* targets are absolute so none is needed.
|
|
587
|
+
*/
|
|
470
588
|
function createAliasCompilerOptions(props: {
|
|
471
589
|
aliasPaths: Record<string, string[]>;
|
|
472
|
-
baseUrl: string;
|
|
473
590
|
compilerOptions: Record<string, unknown>;
|
|
591
|
+
tsconfig: string;
|
|
474
592
|
}): Record<string, unknown> {
|
|
475
593
|
if (Object.keys(props.aliasPaths).length === 0) {
|
|
476
594
|
return {};
|
|
477
595
|
}
|
|
478
596
|
return {
|
|
479
|
-
baseUrl: toCompilerPath(props.baseUrl, props.compilerOptions),
|
|
480
597
|
paths: {
|
|
598
|
+
...readEffectiveTsconfigPaths(props.tsconfig),
|
|
481
599
|
...readPaths(props.compilerOptions.paths),
|
|
482
600
|
...props.aliasPaths,
|
|
483
601
|
},
|
|
484
602
|
};
|
|
485
603
|
}
|
|
486
604
|
|
|
487
|
-
function hasPaths(value: unknown): boolean {
|
|
488
|
-
return (
|
|
489
|
-
typeof value === "object" &&
|
|
490
|
-
value !== null &&
|
|
491
|
-
!Array.isArray(value) &&
|
|
492
|
-
Object.keys(value).length !== 0
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
605
|
function readPaths(value: unknown): Record<string, string[]> {
|
|
497
606
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
498
607
|
return {};
|
|
@@ -512,28 +621,14 @@ function readPaths(value: unknown): Record<string, string[]> {
|
|
|
512
621
|
return output;
|
|
513
622
|
}
|
|
514
623
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
function toCompilerPath(
|
|
525
|
-
absoluteBaseUrl: string,
|
|
526
|
-
compilerOptions: Record<string, unknown>,
|
|
527
|
-
): string {
|
|
528
|
-
return typeof compilerOptions.baseUrl === "string"
|
|
529
|
-
? compilerOptions.baseUrl
|
|
530
|
-
: absoluteBaseUrl;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function createAliasPaths(
|
|
534
|
-
baseUrl: string,
|
|
535
|
-
aliases: unknown,
|
|
536
|
-
): Record<string, string[]> {
|
|
624
|
+
/**
|
|
625
|
+
* Convert bundler aliases into absolute `paths` mappings.
|
|
626
|
+
*
|
|
627
|
+
* Targets are written as absolute paths on purpose: the generated tsconfig
|
|
628
|
+
* lives in a system temp directory, where TypeScript-Go would reject bare
|
|
629
|
+
* relative targets (TS5090) and anchor `./`-style ones at the wrong directory.
|
|
630
|
+
*/
|
|
631
|
+
function createAliasPaths(aliases: unknown): Record<string, string[]> {
|
|
537
632
|
const paths: Record<string, string[]> = {};
|
|
538
633
|
for (const alias of normalizeAliases(aliases)) {
|
|
539
634
|
if (typeof alias.find !== "string" || alias.find.length === 0) {
|
|
@@ -546,10 +641,11 @@ function createAliasPaths(
|
|
|
546
641
|
if (key.length === 0) {
|
|
547
642
|
continue;
|
|
548
643
|
}
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
644
|
+
const target = normalizePath(
|
|
645
|
+
path.isAbsolute(alias.replacement)
|
|
646
|
+
? alias.replacement
|
|
647
|
+
: path.resolve(process.cwd(), alias.replacement),
|
|
648
|
+
);
|
|
553
649
|
paths[key] = [target];
|
|
554
650
|
paths[`${key}/*`] = [`${target}/*`];
|
|
555
651
|
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read the effective `compilerOptions.paths` of a tsconfig, following its
|
|
7
|
+
* `extends` chain, and absolutize every mapping target.
|
|
8
|
+
*
|
|
9
|
+
* TypeScript merges `compilerOptions` per option key, so the effective `paths`
|
|
10
|
+
* is the whole object from the nearest config in the chain that declares one
|
|
11
|
+
* (own config first, then `extends` entries in reverse priority order).
|
|
12
|
+
* Relative targets are anchored at the directory of the config that declares
|
|
13
|
+
* them — TypeScript-Go resolves inherited relative `paths` against the
|
|
14
|
+
* declaring file, not the extending one.
|
|
15
|
+
*
|
|
16
|
+
* The generated transform tsconfig replaces `paths` wholesale (standard
|
|
17
|
+
* `extends` semantics), so the alias overlay must re-state these base mappings
|
|
18
|
+
* or every tsconfig-only alias silently stops resolving. Absolutizing is
|
|
19
|
+
* required because the generated config lives in a system temp directory and
|
|
20
|
+
* TypeScript-Go rejects non-relative targets (TS5090) while accepting absolute
|
|
21
|
+
* ones.
|
|
22
|
+
*
|
|
23
|
+
* Best-effort by design: a missing or unparsable config in the chain yields
|
|
24
|
+
* `{}` here and a real config error from the compiler, which owns config
|
|
25
|
+
* diagnostics.
|
|
26
|
+
*/
|
|
27
|
+
export function readEffectiveTsconfigPaths(
|
|
28
|
+
tsconfig: string,
|
|
29
|
+
): Record<string, string[]> {
|
|
30
|
+
const declared = findDeclaredPaths(path.resolve(tsconfig), new Set());
|
|
31
|
+
if (declared === null) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
const output: Record<string, string[]> = {};
|
|
35
|
+
for (const [key, targets] of Object.entries(declared.paths)) {
|
|
36
|
+
if (!Array.isArray(targets)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const absolute = targets
|
|
40
|
+
.filter((target): target is string => typeof target === "string")
|
|
41
|
+
.map((target) => absolutizePathsTarget(declared.baseDir, target));
|
|
42
|
+
if (absolute.length !== 0) {
|
|
43
|
+
output[key] = absolute;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return output;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Anchor a single `paths` target at `baseDir` unless it is already absolute,
|
|
51
|
+
* normalizing to forward slashes. The `*` wildcard survives `path.resolve` as a
|
|
52
|
+
* literal segment, so patterns like `./src/*` stay patterns.
|
|
53
|
+
*/
|
|
54
|
+
export function absolutizePathsTarget(baseDir: string, target: string): string {
|
|
55
|
+
const resolved = path.isAbsolute(target)
|
|
56
|
+
? target
|
|
57
|
+
: path.resolve(baseDir, target);
|
|
58
|
+
return resolved.replace(/\\/g, "/");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The `paths` object found while walking one tsconfig's `extends` chain,
|
|
63
|
+
* together with the directory of the config that declared it (the anchor for
|
|
64
|
+
* relative targets).
|
|
65
|
+
*/
|
|
66
|
+
interface IDeclaredPaths {
|
|
67
|
+
baseDir: string;
|
|
68
|
+
paths: Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Locate the nearest `compilerOptions.paths` declaration in the `extends` chain
|
|
73
|
+
* rooted at `tsconfig`. The own config wins over its bases; within an `extends`
|
|
74
|
+
* array, later entries win over earlier ones. `seen` breaks circular chains —
|
|
75
|
+
* the compiler reports the actual config error.
|
|
76
|
+
*/
|
|
77
|
+
function findDeclaredPaths(
|
|
78
|
+
tsconfig: string,
|
|
79
|
+
seen: Set<string>,
|
|
80
|
+
): IDeclaredPaths | null {
|
|
81
|
+
const canonical = resolveRealPath(tsconfig);
|
|
82
|
+
if (seen.has(canonical)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
seen.add(canonical);
|
|
86
|
+
|
|
87
|
+
let parsed: { extends?: unknown; compilerOptions?: { paths?: unknown } };
|
|
88
|
+
try {
|
|
89
|
+
parsed = parseJsonc(fs.readFileSync(canonical, "utf8")) as typeof parsed;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const own = parsed.compilerOptions?.paths;
|
|
98
|
+
if (typeof own === "object" && own !== null && !Array.isArray(own)) {
|
|
99
|
+
return {
|
|
100
|
+
baseDir: path.dirname(canonical),
|
|
101
|
+
paths: own as Record<string, unknown>,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const specifier of extendsSpecifiers(parsed.extends).reverse()) {
|
|
106
|
+
const base = resolveExtendsConfig(canonical, specifier);
|
|
107
|
+
if (base === null) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const declared = findDeclaredPaths(base, seen);
|
|
111
|
+
if (declared !== null) {
|
|
112
|
+
return declared;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Normalize the `extends` field into a list of string specifiers. */
|
|
119
|
+
function extendsSpecifiers(extended: unknown): string[] {
|
|
120
|
+
if (typeof extended === "string") {
|
|
121
|
+
return [extended];
|
|
122
|
+
}
|
|
123
|
+
if (Array.isArray(extended)) {
|
|
124
|
+
return extended.filter(
|
|
125
|
+
(entry): entry is string => typeof entry === "string",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resolve an `extends` specifier to an absolute config path using TypeScript's
|
|
133
|
+
* rules: absolute paths and relative specifiers get `.json` /
|
|
134
|
+
* `tsconfig.json`-directory fallbacks; bare specifiers go through Node's module
|
|
135
|
+
* resolver scoped to the declaring config. Returns `null` instead of throwing —
|
|
136
|
+
* the compiler reports unresolvable `extends` itself.
|
|
137
|
+
*/
|
|
138
|
+
function resolveExtendsConfig(
|
|
139
|
+
tsconfig: string,
|
|
140
|
+
specifier: string,
|
|
141
|
+
): string | null {
|
|
142
|
+
if (path.isAbsolute(specifier)) {
|
|
143
|
+
return resolveExistingExtendsPath(specifier);
|
|
144
|
+
}
|
|
145
|
+
if (isRelativeSpecifier(specifier)) {
|
|
146
|
+
return resolveExistingExtendsPath(
|
|
147
|
+
path.resolve(path.dirname(tsconfig), specifier),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const resolver = createRequire(tsconfig);
|
|
151
|
+
try {
|
|
152
|
+
return resolveRealPath(resolver.resolve(specifier));
|
|
153
|
+
} catch {
|
|
154
|
+
try {
|
|
155
|
+
return resolveRealPath(resolver.resolve(`${specifier}.json`));
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Try an on-disk `extends` location as-is, with `.json` appended, and as a
|
|
164
|
+
* directory containing `tsconfig.json`. Returns the first existing match.
|
|
165
|
+
*/
|
|
166
|
+
function resolveExistingExtendsPath(location: string): string | null {
|
|
167
|
+
for (const candidate of new Set([
|
|
168
|
+
location,
|
|
169
|
+
`${location}.json`,
|
|
170
|
+
path.join(location, "tsconfig.json"),
|
|
171
|
+
])) {
|
|
172
|
+
if (fs.existsSync(candidate)) {
|
|
173
|
+
return resolveRealPath(candidate);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Return true when `specifier` is a relative path reference: `.`, `..`, or a
|
|
181
|
+
* string starting with `./`, `../`, `.\\`, or `..\\`.
|
|
182
|
+
*/
|
|
183
|
+
function isRelativeSpecifier(specifier: string): boolean {
|
|
184
|
+
return (
|
|
185
|
+
specifier === "." ||
|
|
186
|
+
specifier === ".." ||
|
|
187
|
+
specifier.startsWith("./") ||
|
|
188
|
+
specifier.startsWith("../") ||
|
|
189
|
+
specifier.startsWith(".\\") ||
|
|
190
|
+
specifier.startsWith("..\\")
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Resolve symlinks on `location`, returning the original path when
|
|
196
|
+
* `realpathSync` fails (e.g. when the file does not exist).
|
|
197
|
+
*/
|
|
198
|
+
function resolveRealPath(location: string): string {
|
|
199
|
+
try {
|
|
200
|
+
return fs.realpathSync(location);
|
|
201
|
+
} catch {
|
|
202
|
+
return location;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Parse a JSONC (JSON with Comments) string by stripping comments and trailing
|
|
208
|
+
* commas before handing off to `JSON.parse`. A leading UTF-8 BOM is dropped —
|
|
209
|
+
* `JSON.parse` rejects it, and this reader must not lose `paths` for a config
|
|
210
|
+
* the compiler accepts.
|
|
211
|
+
*/
|
|
212
|
+
function parseJsonc(input: string): unknown {
|
|
213
|
+
const text = input.charCodeAt(0) === 0xfeff ? input.slice(1) : input;
|
|
214
|
+
return JSON.parse(stripTrailingCommas(stripComments(text)));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Remove `//` line comments and `/* block comments *\/` from a JSONC string.
|
|
219
|
+
* Correctly handles strings that contain comment-like character sequences by
|
|
220
|
+
* tracking string boundaries and escape characters.
|
|
221
|
+
*/
|
|
222
|
+
function stripComments(input: string): string {
|
|
223
|
+
let output = "";
|
|
224
|
+
let inBlockComment = false;
|
|
225
|
+
let inLineComment = false;
|
|
226
|
+
let inString = false;
|
|
227
|
+
let quote = "";
|
|
228
|
+
let escape = false;
|
|
229
|
+
|
|
230
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
231
|
+
const current = input[i]!;
|
|
232
|
+
const next = input[i + 1];
|
|
233
|
+
|
|
234
|
+
if (inBlockComment) {
|
|
235
|
+
if (current === "*" && next === "/") {
|
|
236
|
+
inBlockComment = false;
|
|
237
|
+
i += 1;
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (inLineComment) {
|
|
242
|
+
if (current === "\n") {
|
|
243
|
+
inLineComment = false;
|
|
244
|
+
output += current;
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (inString) {
|
|
249
|
+
output += current;
|
|
250
|
+
if (escape) {
|
|
251
|
+
escape = false;
|
|
252
|
+
} else if (current === "\\") {
|
|
253
|
+
escape = true;
|
|
254
|
+
} else if (current === quote) {
|
|
255
|
+
inString = false;
|
|
256
|
+
quote = "";
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (current === '"' || current === "'") {
|
|
262
|
+
inString = true;
|
|
263
|
+
quote = current;
|
|
264
|
+
output += current;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (current === "/" && next === "/") {
|
|
268
|
+
inLineComment = true;
|
|
269
|
+
i += 1;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (current === "/" && next === "*") {
|
|
273
|
+
inBlockComment = true;
|
|
274
|
+
i += 1;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
output += current;
|
|
278
|
+
}
|
|
279
|
+
return output;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Remove trailing commas before `}` or `]` from a JSON string (after comments
|
|
284
|
+
* have already been stripped). Handles string boundaries and escape characters
|
|
285
|
+
* to avoid removing commas inside string values.
|
|
286
|
+
*/
|
|
287
|
+
function stripTrailingCommas(input: string): string {
|
|
288
|
+
let output = "";
|
|
289
|
+
let inString = false;
|
|
290
|
+
let quote = "";
|
|
291
|
+
let escape = false;
|
|
292
|
+
|
|
293
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
294
|
+
const current = input[i]!;
|
|
295
|
+
if (inString) {
|
|
296
|
+
output += current;
|
|
297
|
+
if (escape) {
|
|
298
|
+
escape = false;
|
|
299
|
+
} else if (current === "\\") {
|
|
300
|
+
escape = true;
|
|
301
|
+
} else if (current === quote) {
|
|
302
|
+
inString = false;
|
|
303
|
+
quote = "";
|
|
304
|
+
}
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (current === '"' || current === "'") {
|
|
309
|
+
inString = true;
|
|
310
|
+
quote = current;
|
|
311
|
+
output += current;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (current === ",") {
|
|
315
|
+
const next = nextNonWhitespace(input, i + 1);
|
|
316
|
+
if (next === "}" || next === "]") {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
output += current;
|
|
321
|
+
}
|
|
322
|
+
return output;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Return the first non-whitespace character at or after position `from` in
|
|
327
|
+
* `input`, or `undefined` when only whitespace remains. Used by
|
|
328
|
+
* `stripTrailingCommas` to detect whether a comma is trailing.
|
|
329
|
+
*/
|
|
330
|
+
function nextNonWhitespace(input: string, from: number): string | undefined {
|
|
331
|
+
for (let i = from; i < input.length; i += 1) {
|
|
332
|
+
const current = input[i]!;
|
|
333
|
+
if (/\s/.test(current) === false) {
|
|
334
|
+
return current;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|