@ttsc/lint 0.26.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/lib/index.d.ts +4 -1
- package/lib/index.js +457 -112
- package/lib/index.js.map +1 -1
- package/lib/structures/format/ITtscLintFormat.d.ts +4 -3
- package/linthost/compile.go +19 -3
- package/linthost/config.go +473 -34
- package/linthost/rules_format_semi.go +500 -63
- package/package.json +2 -2
- package/src/index.ts +509 -115
- package/src/structures/format/ITtscLintFormat.ts +4 -3
package/linthost/config.go
CHANGED
|
@@ -1294,10 +1294,12 @@ func loadConfigFile(location string) (any, error) {
|
|
|
1294
1294
|
}
|
|
1295
1295
|
|
|
1296
1296
|
type configDependencyFingerprint struct {
|
|
1297
|
-
Path
|
|
1298
|
-
Digest
|
|
1299
|
-
|
|
1300
|
-
|
|
1297
|
+
Path string `json:"path"`
|
|
1298
|
+
Digest string `json:"digest"`
|
|
1299
|
+
IdentityStable bool `json:"identityStable"`
|
|
1300
|
+
Kind string `json:"kind"`
|
|
1301
|
+
Realpath *string `json:"realpath"`
|
|
1302
|
+
Scope string `json:"scope"`
|
|
1301
1303
|
}
|
|
1302
1304
|
|
|
1303
1305
|
const (
|
|
@@ -1367,10 +1369,10 @@ func loadConfigFileEvaluationWithin(
|
|
|
1367
1369
|
// configCacheVersion namespaces the on-disk config cache. Bump it whenever
|
|
1368
1370
|
// the shape of a cached config object changes so that entries written by an
|
|
1369
1371
|
// older @ttsc/lint binary are treated as a miss rather than silently reused.
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1372
|
-
//
|
|
1373
|
-
const configCacheVersion = "
|
|
1372
|
+
// v8 also rejects content-restoring A-B-A replacement during evaluation. A v7
|
|
1373
|
+
// cache can otherwise pair output from the transient state with the restored
|
|
1374
|
+
// state's equal digest and physical path.
|
|
1375
|
+
const configCacheVersion = "v8"
|
|
1374
1376
|
|
|
1375
1377
|
// configEvalCache memoizes evaluated .ts/.js lint config objects for the
|
|
1376
1378
|
// lifetime of one process; the on-disk cache (configCacheDir) extends the
|
|
@@ -1557,6 +1559,10 @@ func configDependencyDigestsAreCurrent(
|
|
|
1557
1559
|
dependencies []configDependencyFingerprint,
|
|
1558
1560
|
) bool {
|
|
1559
1561
|
for _, dependency := range dependencies {
|
|
1562
|
+
if !dependency.IdentityStable ||
|
|
1563
|
+
!sameConfigDependencyRealpath(dependency.Realpath, configDependencyRealpath(dependency.Path)) {
|
|
1564
|
+
return false
|
|
1565
|
+
}
|
|
1560
1566
|
digest, err := configDependencyDigest(dependency)
|
|
1561
1567
|
if err != nil {
|
|
1562
1568
|
return false
|
|
@@ -1568,6 +1574,19 @@ func configDependencyDigestsAreCurrent(
|
|
|
1568
1574
|
return true
|
|
1569
1575
|
}
|
|
1570
1576
|
|
|
1577
|
+
func configDependencyRealpath(location string) *string {
|
|
1578
|
+
absolute, err := filepath.Abs(location)
|
|
1579
|
+
if err != nil {
|
|
1580
|
+
return nil
|
|
1581
|
+
}
|
|
1582
|
+
resolved := realProjectPath(absolute)
|
|
1583
|
+
if _, statErr := os.Stat(resolved); statErr != nil {
|
|
1584
|
+
return nil
|
|
1585
|
+
}
|
|
1586
|
+
resolved = filepath.Clean(resolved)
|
|
1587
|
+
return &resolved
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1571
1590
|
func configDependencyDigest(
|
|
1572
1591
|
dependency configDependencyFingerprint,
|
|
1573
1592
|
) (string, error) {
|
|
@@ -1848,7 +1867,8 @@ func normalizeConfigDependencyFingerprints(
|
|
|
1848
1867
|
for _, dependency := range input {
|
|
1849
1868
|
if strings.TrimSpace(dependency.Path) == "" ||
|
|
1850
1869
|
!filepath.IsAbs(dependency.Path) ||
|
|
1851
|
-
|
|
1870
|
+
(dependency.Realpath != nil && !filepath.IsAbs(*dependency.Realpath)) ||
|
|
1871
|
+
(dependency.Digest != "" && len(dependency.Digest) != sha256.Size*2) ||
|
|
1852
1872
|
strings.ToLower(dependency.Digest) != dependency.Digest ||
|
|
1853
1873
|
(dependency.Kind != configDependencyFile &&
|
|
1854
1874
|
dependency.Kind != configDependencyDir &&
|
|
@@ -1858,24 +1878,30 @@ func normalizeConfigDependencyFingerprints(
|
|
|
1858
1878
|
dependency.Scope != configDependencyWatch) {
|
|
1859
1879
|
return nil, false
|
|
1860
1880
|
}
|
|
1861
|
-
if
|
|
1862
|
-
|
|
1881
|
+
if dependency.Digest != "" {
|
|
1882
|
+
if _, err := hex.DecodeString(dependency.Digest); err != nil {
|
|
1883
|
+
return nil, false
|
|
1884
|
+
}
|
|
1863
1885
|
}
|
|
1864
1886
|
absolute := filepath.Clean(dependency.Path)
|
|
1865
1887
|
key := dependency.Kind + "\x00" + absolute
|
|
1866
1888
|
if previous, exists := seen[key]; exists {
|
|
1867
1889
|
if previous.Digest != dependency.Digest ||
|
|
1890
|
+
previous.IdentityStable != dependency.IdentityStable ||
|
|
1868
1891
|
previous.Kind != dependency.Kind ||
|
|
1892
|
+
!sameConfigDependencyRealpath(previous.Realpath, dependency.Realpath) ||
|
|
1869
1893
|
previous.Scope != dependency.Scope {
|
|
1870
1894
|
return nil, false
|
|
1871
1895
|
}
|
|
1872
1896
|
continue
|
|
1873
1897
|
}
|
|
1874
1898
|
fingerprint := configDependencyFingerprint{
|
|
1875
|
-
Path:
|
|
1876
|
-
Digest:
|
|
1877
|
-
|
|
1878
|
-
|
|
1899
|
+
Path: absolute,
|
|
1900
|
+
Digest: dependency.Digest,
|
|
1901
|
+
IdentityStable: dependency.IdentityStable,
|
|
1902
|
+
Kind: dependency.Kind,
|
|
1903
|
+
Realpath: cloneConfigDependencyRealpath(dependency.Realpath),
|
|
1904
|
+
Scope: dependency.Scope,
|
|
1879
1905
|
}
|
|
1880
1906
|
seen[key] = fingerprint
|
|
1881
1907
|
normalized = append(normalized, fingerprint)
|
|
@@ -1886,6 +1912,21 @@ func normalizeConfigDependencyFingerprints(
|
|
|
1886
1912
|
return normalized, true
|
|
1887
1913
|
}
|
|
1888
1914
|
|
|
1915
|
+
func cloneConfigDependencyRealpath(value *string) *string {
|
|
1916
|
+
if value == nil {
|
|
1917
|
+
return nil
|
|
1918
|
+
}
|
|
1919
|
+
cloned := filepath.Clean(*value)
|
|
1920
|
+
return &cloned
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
func sameConfigDependencyRealpath(left, right *string) bool {
|
|
1924
|
+
if left == nil || right == nil {
|
|
1925
|
+
return left == nil && right == nil
|
|
1926
|
+
}
|
|
1927
|
+
return filepath.Clean(*left) == filepath.Clean(*right)
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1889
1930
|
// loadScriptConfigFile evaluates a .js/.cjs/.mjs config file by running a
|
|
1890
1931
|
// Node subprocess that dynamic-imports the file, resolves the exported config
|
|
1891
1932
|
// through the same 8-hop default/config normalization used by the TS loader,
|
|
@@ -1919,12 +1960,18 @@ func loadScriptConfigEvaluationWithin(
|
|
|
1919
1960
|
cmd := exec.CommandContext(
|
|
1920
1961
|
ctx,
|
|
1921
1962
|
node,
|
|
1922
|
-
"-
|
|
1923
|
-
|
|
1963
|
+
"--input-type=commonjs",
|
|
1964
|
+
"-",
|
|
1924
1965
|
location,
|
|
1925
1966
|
outputPath,
|
|
1926
1967
|
resolutionRoot,
|
|
1927
1968
|
)
|
|
1969
|
+
// Windows limits the whole process command line to roughly 32 KiB. The
|
|
1970
|
+
// dependency-tracking loader is intentionally larger than that, so keep only
|
|
1971
|
+
// an explicit CommonJS stdin program and remove Node's stdin sentinel before
|
|
1972
|
+
// the loader runs. This preserves the historical process.argv layout seen by
|
|
1973
|
+
// both the loader and the imported user config without using string eval.
|
|
1974
|
+
cmd.Stdin = strings.NewReader("process.argv.splice(1, 1);\n" + script)
|
|
1928
1975
|
return runConfigLoaderCommand(cmd, location, "config file", outputPath)
|
|
1929
1976
|
}
|
|
1930
1977
|
|
|
@@ -2104,19 +2151,78 @@ function isObject(value) {
|
|
|
2104
2151
|
return value !== null && typeof value === "object";
|
|
2105
2152
|
}
|
|
2106
2153
|
|
|
2154
|
+
function missingPathError(error) {
|
|
2155
|
+
return error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
function dependencyMetadataSignature(location) {
|
|
2159
|
+
const requested = path.resolve(location);
|
|
2160
|
+
let current = requested;
|
|
2161
|
+
for (;;) {
|
|
2162
|
+
try {
|
|
2163
|
+
const link = fs.lstatSync(current, { bigint: true });
|
|
2164
|
+
let target = link;
|
|
2165
|
+
if (link.isSymbolicLink()) {
|
|
2166
|
+
try { target = fs.statSync(current, { bigint: true }); }
|
|
2167
|
+
catch { return undefined; }
|
|
2168
|
+
}
|
|
2169
|
+
return [path.relative(current, requested), link.dev, link.ino, link.mode, link.size, link.mtimeNs, link.ctimeNs, target.dev, target.ino, target.mode, target.size, target.mtimeNs, target.ctimeNs].join(":");
|
|
2170
|
+
} catch (error) {
|
|
2171
|
+
if (!missingPathError(error)) return undefined;
|
|
2172
|
+
const parent = path.dirname(current);
|
|
2173
|
+
if (parent === current) return undefined;
|
|
2174
|
+
current = parent;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
function currentDependencyDigest(kind, location) {
|
|
2180
|
+
try {
|
|
2181
|
+
if (kind === "directory") return directoryDigest(location);
|
|
2182
|
+
if (kind === "entry") return entryDigest(location);
|
|
2183
|
+
if (kind === "optional-file") return optionalFileDigest(location);
|
|
2184
|
+
return createHash("sha256").update(fs.readFileSync(location)).digest("hex");
|
|
2185
|
+
} catch {
|
|
2186
|
+
return "";
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2107
2190
|
function recordDependency(kind, location, digest, owners) {
|
|
2108
2191
|
const key = kind + "\0" + location;
|
|
2109
2192
|
const previous = dependencies.get(key);
|
|
2110
2193
|
const mergedOwners = previous ? previous.owners : new Set();
|
|
2111
2194
|
for (const owner of owners) mergedOwners.add(owner);
|
|
2195
|
+
const beforeSignature = dependencyMetadataSignature(location);
|
|
2196
|
+
const observedDigest = currentDependencyDigest(kind, location);
|
|
2197
|
+
const realpath = dependencyRealpath(location);
|
|
2198
|
+
const afterSignature = dependencyMetadataSignature(location);
|
|
2199
|
+
const identityStable =
|
|
2200
|
+
(!previous || previous.identityStable) &&
|
|
2201
|
+
beforeSignature !== undefined &&
|
|
2202
|
+
afterSignature !== undefined &&
|
|
2203
|
+
beforeSignature === afterSignature &&
|
|
2204
|
+
digest === observedDigest &&
|
|
2205
|
+
(!previous || previous.realpath === realpath) &&
|
|
2206
|
+
(!previous || previous.signature === afterSignature);
|
|
2112
2207
|
dependencies.set(key, {
|
|
2113
|
-
digest: previous && previous.digest !== digest ? "" : digest,
|
|
2208
|
+
digest: !identityStable || (previous && previous.digest !== digest) ? "" : digest,
|
|
2209
|
+
identityStable,
|
|
2114
2210
|
kind,
|
|
2115
2211
|
owners: mergedOwners,
|
|
2116
2212
|
path: location,
|
|
2213
|
+
realpath,
|
|
2214
|
+
signature: afterSignature,
|
|
2117
2215
|
});
|
|
2118
2216
|
}
|
|
2119
2217
|
|
|
2218
|
+
function dependencyRealpath(location) {
|
|
2219
|
+
try {
|
|
2220
|
+
return realPath(location);
|
|
2221
|
+
} catch {
|
|
2222
|
+
return null;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2120
2226
|
function isLocalModuleSpecifier(specifier) {
|
|
2121
2227
|
return specifier.startsWith(".") ||
|
|
2122
2228
|
specifier.startsWith("/") ||
|
|
@@ -2874,10 +2980,22 @@ function realPath(location) {
|
|
|
2874
2980
|
}
|
|
2875
2981
|
|
|
2876
2982
|
function finalizeDependencies() {
|
|
2983
|
+
for (const dependency of [...dependencies.values()]) {
|
|
2984
|
+
recordDependency(
|
|
2985
|
+
dependency.kind,
|
|
2986
|
+
dependency.path,
|
|
2987
|
+
currentDependencyDigest(dependency.kind, dependency.path),
|
|
2988
|
+
[...dependency.owners],
|
|
2989
|
+
);
|
|
2990
|
+
}
|
|
2877
2991
|
const watched = graphWatchReachability();
|
|
2878
|
-
return [...dependencies.values()].map((
|
|
2879
|
-
|
|
2880
|
-
|
|
2992
|
+
return [...dependencies.values()].map((dependency) => ({
|
|
2993
|
+
digest: dependency.digest,
|
|
2994
|
+
identityStable: dependency.identityStable,
|
|
2995
|
+
kind: dependency.kind,
|
|
2996
|
+
path: dependency.path,
|
|
2997
|
+
realpath: dependency.realpath,
|
|
2998
|
+
scope: [...dependency.owners].some((owner) => watched.has(owner))
|
|
2881
2999
|
? "watch"
|
|
2882
3000
|
: "cache",
|
|
2883
3001
|
}));
|
|
@@ -3028,14 +3146,15 @@ func loadTypeScriptConfigEvaluationWithin(
|
|
|
3028
3146
|
// ttsx build hermetic.
|
|
3029
3147
|
"--no-plugins",
|
|
3030
3148
|
}
|
|
3031
|
-
|
|
3149
|
+
anchors := configToolAnchors(location, resolutionRoot)
|
|
3150
|
+
if tsgo := resolveConfigTsgo(anchors); tsgo != "" {
|
|
3032
3151
|
args = append(args, "--binary", tsgo)
|
|
3033
3152
|
}
|
|
3034
3153
|
args = append(args, loader)
|
|
3035
3154
|
|
|
3036
3155
|
ctx, cancel := context.WithCancel(context.Background())
|
|
3037
3156
|
defer cancel()
|
|
3038
|
-
cmd := ttsxCommandContext(ctx, args...)
|
|
3157
|
+
cmd := ttsxCommandContext(ctx, anchors, args...)
|
|
3039
3158
|
cmd.Env = nodeConfigLoaderEnv(location)
|
|
3040
3159
|
return runConfigLoaderCommand(cmd, location, "TypeScript config file", outputPath)
|
|
3041
3160
|
}
|
|
@@ -3120,9 +3239,12 @@ const resolutionRoot = path.resolve(%s);
|
|
|
3120
3239
|
const CONFIG_KEYS = new Set<string>([%s]);
|
|
3121
3240
|
const dependencies = new Map<string, {
|
|
3122
3241
|
digest: string;
|
|
3242
|
+
identityStable: boolean;
|
|
3123
3243
|
kind: "directory" | "entry" | "file" | "optional-file";
|
|
3124
3244
|
path: string;
|
|
3125
3245
|
owners: Set<string>;
|
|
3246
|
+
realpath: string | null;
|
|
3247
|
+
signature: string | undefined;
|
|
3126
3248
|
}>();
|
|
3127
3249
|
const graphNodes = new Map<string, string>();
|
|
3128
3250
|
const graphEdges: Array<{
|
|
@@ -3295,6 +3417,48 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
3295
3417
|
return value !== null && typeof value === "object";
|
|
3296
3418
|
}
|
|
3297
3419
|
|
|
3420
|
+
function missingPathError(error: unknown): boolean {
|
|
3421
|
+
const code = (error as { code?: unknown } | undefined)?.code;
|
|
3422
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
function dependencyMetadataSignature(
|
|
3426
|
+
location: string,
|
|
3427
|
+
): string | undefined {
|
|
3428
|
+
const requested = path.resolve(location);
|
|
3429
|
+
let current = requested;
|
|
3430
|
+
for (;;) {
|
|
3431
|
+
try {
|
|
3432
|
+
const link = fs.lstatSync(current, { bigint: true });
|
|
3433
|
+
let target = link;
|
|
3434
|
+
if (link.isSymbolicLink()) {
|
|
3435
|
+
try { target = fs.statSync(current, { bigint: true }); }
|
|
3436
|
+
catch { return undefined; }
|
|
3437
|
+
}
|
|
3438
|
+
return [path.relative(current, requested), link.dev, link.ino, link.mode, link.size, link.mtimeNs, link.ctimeNs, target.dev, target.ino, target.mode, target.size, target.mtimeNs, target.ctimeNs].join(":");
|
|
3439
|
+
} catch (error) {
|
|
3440
|
+
if (!missingPathError(error)) return undefined;
|
|
3441
|
+
const parent = path.dirname(current);
|
|
3442
|
+
if (parent === current) return undefined;
|
|
3443
|
+
current = parent;
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
function currentDependencyDigest(
|
|
3449
|
+
kind: "directory" | "entry" | "file" | "optional-file",
|
|
3450
|
+
location: string,
|
|
3451
|
+
): string {
|
|
3452
|
+
try {
|
|
3453
|
+
if (kind === "directory") return directoryDigest(location);
|
|
3454
|
+
if (kind === "entry") return entryDigest(location);
|
|
3455
|
+
if (kind === "optional-file") return optionalFileDigest(location);
|
|
3456
|
+
return createHash("sha256").update(fs.readFileSync(location)).digest("hex");
|
|
3457
|
+
} catch {
|
|
3458
|
+
return "";
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3298
3462
|
function recordDependency(
|
|
3299
3463
|
kind: "directory" | "entry" | "file" | "optional-file",
|
|
3300
3464
|
location: string,
|
|
@@ -3305,14 +3469,41 @@ function recordDependency(
|
|
|
3305
3469
|
const previous = dependencies.get(key);
|
|
3306
3470
|
const mergedOwners = previous?.owners ?? new Set<string>();
|
|
3307
3471
|
for (const owner of owners) mergedOwners.add(owner);
|
|
3472
|
+
const beforeSignature = dependencyMetadataSignature(location);
|
|
3473
|
+
const observedDigest = currentDependencyDigest(kind, location);
|
|
3474
|
+
const realpath = dependencyRealpath(location);
|
|
3475
|
+
const afterSignature = dependencyMetadataSignature(location);
|
|
3476
|
+
const identityStable =
|
|
3477
|
+
previous?.identityStable !== false &&
|
|
3478
|
+
beforeSignature !== undefined &&
|
|
3479
|
+
afterSignature !== undefined &&
|
|
3480
|
+
beforeSignature === afterSignature &&
|
|
3481
|
+
digest === observedDigest &&
|
|
3482
|
+
(previous === undefined || previous.realpath === realpath) &&
|
|
3483
|
+
(previous === undefined || previous.signature === afterSignature);
|
|
3308
3484
|
dependencies.set(key, {
|
|
3309
|
-
digest:
|
|
3485
|
+
digest:
|
|
3486
|
+
!identityStable ||
|
|
3487
|
+
(previous !== undefined && previous.digest !== digest)
|
|
3488
|
+
? ""
|
|
3489
|
+
: digest,
|
|
3490
|
+
identityStable,
|
|
3310
3491
|
kind,
|
|
3311
3492
|
owners: mergedOwners,
|
|
3312
3493
|
path: location,
|
|
3494
|
+
realpath,
|
|
3495
|
+
signature: afterSignature,
|
|
3313
3496
|
});
|
|
3314
3497
|
}
|
|
3315
3498
|
|
|
3499
|
+
function dependencyRealpath(location: string): string | null {
|
|
3500
|
+
try {
|
|
3501
|
+
return realPath(location);
|
|
3502
|
+
} catch {
|
|
3503
|
+
return null;
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3316
3507
|
function isLocalModuleSpecifier(specifier: string): boolean {
|
|
3317
3508
|
return specifier.startsWith(".") ||
|
|
3318
3509
|
specifier.startsWith("/") ||
|
|
@@ -4114,10 +4305,20 @@ function realPath(location: string): string {
|
|
|
4114
4305
|
|
|
4115
4306
|
function finalizeDependencies(): Array<{
|
|
4116
4307
|
digest: string;
|
|
4308
|
+
identityStable: boolean;
|
|
4117
4309
|
kind: "directory" | "entry" | "file" | "optional-file";
|
|
4118
4310
|
path: string;
|
|
4311
|
+
realpath: string | null;
|
|
4119
4312
|
scope: "cache" | "watch";
|
|
4120
4313
|
}> {
|
|
4314
|
+
for (const dependency of [...dependencies.values()]) {
|
|
4315
|
+
recordDependency(
|
|
4316
|
+
dependency.kind,
|
|
4317
|
+
dependency.path,
|
|
4318
|
+
currentDependencyDigest(dependency.kind, dependency.path),
|
|
4319
|
+
[...dependency.owners],
|
|
4320
|
+
);
|
|
4321
|
+
}
|
|
4121
4322
|
const watched = graphWatchReachability();
|
|
4122
4323
|
// Opt-in diagnostics for a graph that comes back empty. The only channel this
|
|
4123
4324
|
// loader may use is stderr, because the result travels through a private file
|
|
@@ -4135,9 +4336,13 @@ function finalizeDependencies(): Array<{
|
|
|
4135
4336
|
"\n",
|
|
4136
4337
|
);
|
|
4137
4338
|
}
|
|
4138
|
-
return [...dependencies.values()].map((
|
|
4139
|
-
|
|
4140
|
-
|
|
4339
|
+
return [...dependencies.values()].map((dependency) => ({
|
|
4340
|
+
digest: dependency.digest,
|
|
4341
|
+
identityStable: dependency.identityStable,
|
|
4342
|
+
kind: dependency.kind,
|
|
4343
|
+
path: dependency.path,
|
|
4344
|
+
realpath: dependency.realpath,
|
|
4345
|
+
scope: [...dependency.owners].some((owner) => watched.has(owner))
|
|
4141
4346
|
? "watch"
|
|
4142
4347
|
: "cache",
|
|
4143
4348
|
}));
|
|
@@ -4250,6 +4455,7 @@ func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
|
|
|
4250
4455
|
// resolving either way.
|
|
4251
4456
|
"module": configModuleOption(location),
|
|
4252
4457
|
"moduleResolution": "bundler",
|
|
4458
|
+
"jsx": "preserve",
|
|
4253
4459
|
"noImplicitAny": false,
|
|
4254
4460
|
"outDir": filepath.ToSlash(filepath.Join(outDir, "out")),
|
|
4255
4461
|
"rewriteRelativeImportExtensions": true,
|
|
@@ -4276,6 +4482,23 @@ func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
|
|
|
4276
4482
|
return string(body)
|
|
4277
4483
|
}
|
|
4278
4484
|
|
|
4485
|
+
// ttsc:config-loader-shared begin
|
|
4486
|
+
//
|
|
4487
|
+
// One policy in three Go copies: everything between these markers is
|
|
4488
|
+
// duplicated verbatim in packages/lint/linthost/config.go,
|
|
4489
|
+
// packages/banner/driver/banner.go and packages/strip/driver/config.go. #1169
|
|
4490
|
+
// decided against extracting it — the only home the three modules could share
|
|
4491
|
+
// is the public `packages/ttsc/driver` seam, and packages/lint's go.mod
|
|
4492
|
+
// deliberately requires no in-tree ttsc module — and replaced the checklist
|
|
4493
|
+
// with a gate: `scripts/ci/config-loader-copies.cjs` compares every function
|
|
4494
|
+
// between these markers across all three copies on every pull request, so
|
|
4495
|
+
// editing one and not the others fails by name. That file's header carries the
|
|
4496
|
+
// full decision and the rules for changing this block.
|
|
4497
|
+
//
|
|
4498
|
+
// The code between the markers must stay identical. Comments may differ, the
|
|
4499
|
+
// `@ttsc/<pkg>:` error prefix may differ, and @ttsc/strip spells each name with
|
|
4500
|
+
// a `strip` prefix. Anything package-specific belongs outside the markers.
|
|
4501
|
+
|
|
4279
4502
|
// configModuleOption returns the loader tsconfig's "module" for a config file:
|
|
4280
4503
|
// the module kind Node itself would give that file.
|
|
4281
4504
|
//
|
|
@@ -4406,21 +4629,235 @@ func resolveDirLink(dir string) string {
|
|
|
4406
4629
|
return dir
|
|
4407
4630
|
}
|
|
4408
4631
|
|
|
4632
|
+
// Both tools the TypeScript config evaluator needs — the `ttsx` launcher it
|
|
4633
|
+
// spawns and the native compiler it hands that launcher — are resolved from the
|
|
4634
|
+
// project being linted, with an explicit environment variable winning and a
|
|
4635
|
+
// last resort that invents no path.
|
|
4636
|
+
//
|
|
4637
|
+
// The three Go copies are held identical by the gate named at the top of this
|
|
4638
|
+
// block. The JS original — `resolveConfigTsgo` / `resolveTtsxLauncher` in
|
|
4639
|
+
// packages/lint/src/index.ts — is a fourth copy in another language that no Go
|
|
4640
|
+
// gate can reach; the two evaluators must keep one policy, because it was a
|
|
4641
|
+
// divergence between them that made a TypeScript lint config unevaluable
|
|
4642
|
+
// outside a `ttsx`-launched host.
|
|
4643
|
+
//
|
|
4644
|
+
// The environment alone is the wrong place to ask. `ttsx` exports
|
|
4645
|
+
// TTSC_TSGO_BINARY and TTSC_TTSX_BINARY to its own descendants, so a host
|
|
4646
|
+
// launched under `ttsx` inherited both and a host launched any other way
|
|
4647
|
+
// inherited neither. The shipped `ttscserver` binary invoked with its
|
|
4648
|
+
// documented `--tsgo <path>` flag keeps that path in a local and exports
|
|
4649
|
+
// nothing, and an embedder of the driver package exports nothing either. For
|
|
4650
|
+
// those the evaluator spawned a bare `ttsx` that only a global install puts on
|
|
4651
|
+
// PATH, and, past that, a compiler-less child that aborted with
|
|
4652
|
+
// `ttsc: typescript is required` before a line of the config was read.
|
|
4653
|
+
//
|
|
4654
|
+
// configToolAnchors lists the file paths those resolutions walk upward from,
|
|
4655
|
+
// in order: the config file being evaluated, then the resolution root's
|
|
4656
|
+
// manifest. The config comes first because it is the file whose own
|
|
4657
|
+
// installation decides which toolchain the config's imports were written
|
|
4658
|
+
// against; the resolution root answers for a config that lives outside the
|
|
4659
|
+
// project tree (an `extends` target, or a `configFile` pointed at a shared
|
|
4660
|
+
// package).
|
|
4661
|
+
//
|
|
4662
|
+
// The JS evaluator carries a third anchor, the loaded descriptor's own
|
|
4663
|
+
// directory. It has no counterpart here: this host is a compiled binary rather
|
|
4664
|
+
// than a module some `node_modules` copy of `@ttsc/lint` was loaded from, so
|
|
4665
|
+
// there is no third installation to ask.
|
|
4666
|
+
func configToolAnchors(configPath, resolutionRoot string) []string {
|
|
4667
|
+
anchors := make([]string, 0, 2)
|
|
4668
|
+
if strings.TrimSpace(configPath) != "" {
|
|
4669
|
+
anchors = append(anchors, configPath)
|
|
4670
|
+
}
|
|
4671
|
+
if strings.TrimSpace(resolutionRoot) != "" {
|
|
4672
|
+
anchors = append(anchors, filepath.Join(resolutionRoot, "package.json"))
|
|
4673
|
+
}
|
|
4674
|
+
return anchors
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
// resolveConfigTsgo returns the native TypeScript compiler the evaluator hands
|
|
4678
|
+
// its ttsx child through `--binary`, or "" to leave the child resolving for
|
|
4679
|
+
// itself.
|
|
4680
|
+
//
|
|
4681
|
+
// The child runs with `--cwd <ephemeral loader dir>`, so it cannot discover
|
|
4682
|
+
// `typescript` the way an ordinary invocation does: linkNearestNodeModules is
|
|
4683
|
+
// the only thing that puts the project's modules within its reach, and it links
|
|
4684
|
+
// nothing when the config's ancestry carries no node_modules. An explicit
|
|
4685
|
+
// TTSC_TSGO_BINARY still wins, so an embedder that pins a compiler keeps
|
|
4686
|
+
// pinning it. "" is the unchanged last resort: a project that cannot answer
|
|
4687
|
+
// here could not answer inside the child either, and the child's own diagnostic
|
|
4688
|
+
// is the one that names the missing package.
|
|
4689
|
+
func resolveConfigTsgo(anchors []string) string {
|
|
4690
|
+
if explicit := strings.TrimSpace(os.Getenv("TTSC_TSGO_BINARY")); explicit != "" {
|
|
4691
|
+
return explicit
|
|
4692
|
+
}
|
|
4693
|
+
for _, anchor := range anchors {
|
|
4694
|
+
if binary := tsgoBinaryFrom(anchor); binary != "" {
|
|
4695
|
+
return binary
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
return ""
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4701
|
+
// tsgoBinaryFrom returns the platform compiler executable of the `typescript`
|
|
4702
|
+
// install `anchor` can see, or "" when this anchor reaches neither the package
|
|
4703
|
+
// nor its platform dependency.
|
|
4704
|
+
//
|
|
4705
|
+
// Mirrors resolveTsgo.ts so the Go host and the JS launcher name one file: the
|
|
4706
|
+
// `typescript` manifest, then `@typescript/typescript-<platform>-<arch>`
|
|
4707
|
+
// resolved from that manifest, then `lib/tsc` inside it.
|
|
4708
|
+
//
|
|
4709
|
+
// The install is chased to its real directory before the second hop, because
|
|
4710
|
+
// Node resolves a module's own dependencies from its real location. pnpm keeps
|
|
4711
|
+
// the real `typescript` directory in its content-addressed store with the
|
|
4712
|
+
// platform package beside it and leaves a link in the project's node_modules,
|
|
4713
|
+
// so a walk that started at the link would climb straight past the platform
|
|
4714
|
+
// package. NTFS junctions defeat filepath.EvalSymlinks, so the link component
|
|
4715
|
+
// is chased by hand first, the same order loaderTempBase uses.
|
|
4716
|
+
func tsgoBinaryFrom(anchor string) string {
|
|
4717
|
+
manifest := nodePackageManifestFrom(anchor, "typescript")
|
|
4718
|
+
if manifest == "" {
|
|
4719
|
+
return ""
|
|
4720
|
+
}
|
|
4721
|
+
packageDir := realpathIfPossible(resolveDirLink(filepath.Dir(manifest)))
|
|
4722
|
+
platform, arch := nodePlatformPair()
|
|
4723
|
+
platformManifest := nodePackageManifestFrom(
|
|
4724
|
+
filepath.Join(packageDir, "package.json"),
|
|
4725
|
+
"@typescript/typescript-"+platform+"-"+arch,
|
|
4726
|
+
)
|
|
4727
|
+
if platformManifest == "" {
|
|
4728
|
+
return ""
|
|
4729
|
+
}
|
|
4730
|
+
name := "tsc"
|
|
4731
|
+
if runtime.GOOS == "windows" {
|
|
4732
|
+
name = "tsc.exe"
|
|
4733
|
+
}
|
|
4734
|
+
binary := filepath.Join(filepath.Dir(platformManifest), "lib", name)
|
|
4735
|
+
if stat, err := os.Stat(binary); err != nil || stat.IsDir() {
|
|
4736
|
+
return ""
|
|
4737
|
+
}
|
|
4738
|
+
return binary
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4741
|
+
// resolveTtsxLauncher returns the launcher ttsxCommandContext spawns.
|
|
4742
|
+
//
|
|
4743
|
+
// An explicit TTSC_TTSX_BINARY wins. Otherwise the launcher is derived from the
|
|
4744
|
+
// `ttsc` installation one of the anchors can see, because a bare command name
|
|
4745
|
+
// only works when a bin link happens to be on PATH — which it is for a global
|
|
4746
|
+
// install and is not for the ordinary project-local one. The bare `"ttsx"` name
|
|
4747
|
+
// remains the unchanged last resort for an installation no anchor reaches.
|
|
4748
|
+
func resolveTtsxLauncher(anchors []string) string {
|
|
4749
|
+
if explicit := strings.TrimSpace(os.Getenv("TTSC_TTSX_BINARY")); explicit != "" {
|
|
4750
|
+
return explicit
|
|
4751
|
+
}
|
|
4752
|
+
for _, anchor := range anchors {
|
|
4753
|
+
if launcher := ttsxLauncherFrom(anchor); launcher != "" {
|
|
4754
|
+
return launcher
|
|
4755
|
+
}
|
|
4756
|
+
}
|
|
4757
|
+
return "ttsx"
|
|
4758
|
+
}
|
|
4759
|
+
|
|
4760
|
+
// ttsxLauncherFrom returns `lib/launcher/ttsx.js` of the `ttsc` install
|
|
4761
|
+
// `anchor` can see, or "" when this anchor reaches no such install. Only the
|
|
4762
|
+
// manifest is an exported subpath, so the launcher is derived from where the
|
|
4763
|
+
// manifest resolved rather than requested as a subpath of its own.
|
|
4764
|
+
func ttsxLauncherFrom(anchor string) string {
|
|
4765
|
+
manifest := nodePackageManifestFrom(anchor, "ttsc")
|
|
4766
|
+
if manifest == "" {
|
|
4767
|
+
return ""
|
|
4768
|
+
}
|
|
4769
|
+
launcher := filepath.Join(filepath.Dir(manifest), "lib", "launcher", "ttsx.js")
|
|
4770
|
+
if stat, err := os.Stat(launcher); err != nil || stat.IsDir() {
|
|
4771
|
+
return ""
|
|
4772
|
+
}
|
|
4773
|
+
return launcher
|
|
4774
|
+
}
|
|
4775
|
+
|
|
4776
|
+
// nodePackageManifestFrom resolves `<pkg>/package.json` the way Node's
|
|
4777
|
+
// require.resolve does from the FILE `anchor`: walk upward from the anchor's
|
|
4778
|
+
// directory and return the first `<dir>/node_modules/<pkg>/package.json` that
|
|
4779
|
+
// exists. The anchor is treated as a file path, so its own directory is the
|
|
4780
|
+
// first candidate's parent, and it need not exist — Node derives the search
|
|
4781
|
+
// paths from the string alone.
|
|
4782
|
+
//
|
|
4783
|
+
// A directory already named `node_modules` contributes no candidate of its own,
|
|
4784
|
+
// matching Module._nodeModulePaths, so nothing ever resolves through
|
|
4785
|
+
// `node_modules/node_modules`.
|
|
4786
|
+
//
|
|
4787
|
+
// A relative anchor is resolved against the process directory before the walk,
|
|
4788
|
+
// again matching Node. Walking a relative path instead would terminate at "."
|
|
4789
|
+
// after one step and silently answer nothing for a config named relatively.
|
|
4790
|
+
func nodePackageManifestFrom(anchor, pkg string) string {
|
|
4791
|
+
if strings.TrimSpace(anchor) == "" || pkg == "" {
|
|
4792
|
+
return ""
|
|
4793
|
+
}
|
|
4794
|
+
if absolute, err := filepath.Abs(anchor); err == nil {
|
|
4795
|
+
anchor = absolute
|
|
4796
|
+
}
|
|
4797
|
+
dir := filepath.Dir(filepath.Clean(anchor))
|
|
4798
|
+
for {
|
|
4799
|
+
if filepath.Base(dir) != "node_modules" {
|
|
4800
|
+
candidate := filepath.Join(dir, "node_modules", filepath.FromSlash(pkg), "package.json")
|
|
4801
|
+
if stat, err := os.Stat(candidate); err == nil && !stat.IsDir() {
|
|
4802
|
+
return candidate
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4805
|
+
parent := filepath.Dir(dir)
|
|
4806
|
+
if parent == dir {
|
|
4807
|
+
return ""
|
|
4808
|
+
}
|
|
4809
|
+
dir = parent
|
|
4810
|
+
}
|
|
4811
|
+
}
|
|
4812
|
+
|
|
4813
|
+
// nodePlatformPair is nodePlatformPairFor applied to this build's own target.
|
|
4814
|
+
func nodePlatformPair() (string, string) {
|
|
4815
|
+
return nodePlatformPairFor(runtime.GOOS, runtime.GOARCH)
|
|
4816
|
+
}
|
|
4817
|
+
|
|
4818
|
+
// nodePlatformPairFor maps a Go build target onto the `process.platform` and
|
|
4819
|
+
// `process.arch` pair npm spells a platform package with, so the package name
|
|
4820
|
+
// this host resolves is the same one the JS launcher resolves.
|
|
4821
|
+
//
|
|
4822
|
+
// Only the members whose two vocabularies disagree are mapped. Every other
|
|
4823
|
+
// value is identical on both sides and passes through, which keeps a target
|
|
4824
|
+
// neither side publishes yet resolvable rather than silently wrong, and keeps
|
|
4825
|
+
// this from becoming a list that has to grow with every new port.
|
|
4826
|
+
func nodePlatformPairFor(goos, goarch string) (string, string) {
|
|
4827
|
+
platform := goos
|
|
4828
|
+
switch platform {
|
|
4829
|
+
case "windows":
|
|
4830
|
+
platform = "win32"
|
|
4831
|
+
case "solaris":
|
|
4832
|
+
platform = "sunos"
|
|
4833
|
+
}
|
|
4834
|
+
arch := goarch
|
|
4835
|
+
switch arch {
|
|
4836
|
+
case "amd64":
|
|
4837
|
+
arch = "x64"
|
|
4838
|
+
case "386":
|
|
4839
|
+
arch = "ia32"
|
|
4840
|
+
case "ppc64le":
|
|
4841
|
+
arch = "ppc64"
|
|
4842
|
+
}
|
|
4843
|
+
return platform, arch
|
|
4844
|
+
}
|
|
4845
|
+
|
|
4409
4846
|
// ttsxCommand returns a ttsx exec.Cmd bound to a background context. Use
|
|
4410
4847
|
// ttsxCommandContext when the caller owns a cancellable context.
|
|
4411
|
-
func ttsxCommand(args ...string) *exec.Cmd {
|
|
4412
|
-
return ttsxCommandContext(context.Background(), args...)
|
|
4848
|
+
func ttsxCommand(anchors []string, args ...string) *exec.Cmd {
|
|
4849
|
+
return ttsxCommandContext(context.Background(), anchors, args...)
|
|
4413
4850
|
}
|
|
4414
4851
|
|
|
4415
4852
|
// ttsxCommandContext is the cancellable variant, used by the config loaders so
|
|
4416
4853
|
// their subprocess is torn down with the call that started it. It carries no
|
|
4417
4854
|
// deadline: evaluating a user config is the user's own code running, and how
|
|
4418
4855
|
// long that is allowed to take is not this binary's decision.
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4856
|
+
//
|
|
4857
|
+
// `anchors` are the file paths the launcher is resolved from; see
|
|
4858
|
+
// resolveTtsxLauncher.
|
|
4859
|
+
func ttsxCommandContext(ctx context.Context, anchors []string, args ...string) *exec.Cmd {
|
|
4860
|
+
ttsx := resolveTtsxLauncher(anchors)
|
|
4424
4861
|
if shouldRunTtsxThroughNode(ttsx) {
|
|
4425
4862
|
node := os.Getenv("TTSC_NODE_BINARY")
|
|
4426
4863
|
if node == "" {
|
|
@@ -4531,6 +4968,8 @@ func setEnv(env []string, key, value string) []string {
|
|
|
4531
4968
|
return append(env, prefix+value)
|
|
4532
4969
|
}
|
|
4533
4970
|
|
|
4971
|
+
// ttsc:config-loader-shared end
|
|
4972
|
+
|
|
4534
4973
|
// parseExternalRuleEntry delegates to parseRuleEntry. It is kept under this
|
|
4535
4974
|
// name because test files in the same package call it directly.
|
|
4536
4975
|
func parseExternalRuleEntry(v any) (Severity, json.RawMessage, error) {
|