@ttsc/lint 0.20.1 → 0.22.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 +27 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +1283 -71
- package/lib/index.js.map +1 -1
- package/linthost/check_serve.go +305 -0
- package/linthost/config.go +2410 -98
- package/linthost/dispatch.go +7 -1
- package/linthost/host.go +6 -4
- package/linthost/lsp.go +55 -0
- package/linthost/project_inputs.go +241 -0
- package/linthost/serve.go +24 -3
- package/package.json +2 -2
- package/rule/project.go +58 -0
- package/src/index.ts +1363 -95
package/linthost/config.go
CHANGED
|
@@ -7,6 +7,7 @@ import (
|
|
|
7
7
|
"encoding/hex"
|
|
8
8
|
"encoding/json"
|
|
9
9
|
"fmt"
|
|
10
|
+
"io"
|
|
10
11
|
"net/url"
|
|
11
12
|
"os"
|
|
12
13
|
"os/exec"
|
|
@@ -219,6 +220,22 @@ func (r boundProjectRuleResolver) RuleOptionsVariants(name string) []json.RawMes
|
|
|
219
220
|
return resolvedRuleOptionsVariants(r.RuleResolver, name)
|
|
220
221
|
}
|
|
221
222
|
|
|
223
|
+
func (r boundProjectRuleResolver) ConfigPaths() []string {
|
|
224
|
+
resolver, ok := r.RuleResolver.(interface{ ConfigPaths() []string })
|
|
225
|
+
if !ok {
|
|
226
|
+
return nil
|
|
227
|
+
}
|
|
228
|
+
return resolver.ConfigPaths()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
func (r boundProjectRuleResolver) ConfigDirectories() []string {
|
|
232
|
+
resolver, ok := r.RuleResolver.(interface{ ConfigDirectories() []string })
|
|
233
|
+
if !ok {
|
|
234
|
+
return nil
|
|
235
|
+
}
|
|
236
|
+
return resolver.ConfigDirectories()
|
|
237
|
+
}
|
|
238
|
+
|
|
222
239
|
// ResolveRules implements RuleResolver. A flat RuleConfig has no glob scoping,
|
|
223
240
|
// so every file receives the full map unchanged.
|
|
224
241
|
func (c RuleConfig) ResolveRules(string) ResolvedRuleConfig {
|
|
@@ -354,7 +371,30 @@ func (r InlineRuleResolver) ResolveProjectRules(names []string) (map[string]Proj
|
|
|
354
371
|
// ConfigEntry per file, the extends-target entries declared before the
|
|
355
372
|
// extending file's own entry so local rules win on collision.
|
|
356
373
|
type ConfigStore struct {
|
|
357
|
-
|
|
374
|
+
directories []string
|
|
375
|
+
entries []ConfigEntry
|
|
376
|
+
paths []string
|
|
377
|
+
resolutionRoot string
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ConfigPaths returns the config and extends files that produced this store.
|
|
381
|
+
// The paths are retained as exact dependencies even when no rule declares
|
|
382
|
+
// additional project inputs.
|
|
383
|
+
func (s *ConfigStore) ConfigPaths() []string {
|
|
384
|
+
if s == nil {
|
|
385
|
+
return nil
|
|
386
|
+
}
|
|
387
|
+
return append([]string(nil), s.paths...)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ConfigDirectories returns resolution-topology directories whose immediate
|
|
391
|
+
// entries can change which executable-config module Node selects. Consumers
|
|
392
|
+
// watch these as cold configuration inputs rather than ordinary rule data.
|
|
393
|
+
func (s *ConfigStore) ConfigDirectories() []string {
|
|
394
|
+
if s == nil {
|
|
395
|
+
return nil
|
|
396
|
+
}
|
|
397
|
+
return append([]string(nil), s.directories...)
|
|
358
398
|
}
|
|
359
399
|
|
|
360
400
|
// RuleOptions implements the file-agnostic RuleResolver compatibility method.
|
|
@@ -700,10 +740,21 @@ func parseExternalConfigStore(raw any, configDir string) (*ConfigStore, error) {
|
|
|
700
740
|
// guard so a config that `extends` itself (directly or transitively) is
|
|
701
741
|
// rejected.
|
|
702
742
|
func collectConfigStore(raw any, configDir, rootPath string) (*ConfigStore, error) {
|
|
703
|
-
|
|
743
|
+
return collectConfigStoreWithin(raw, configDir, rootPath, configDir)
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
func collectConfigStoreWithin(
|
|
747
|
+
raw any,
|
|
748
|
+
configDir string,
|
|
749
|
+
rootPath string,
|
|
750
|
+
resolutionRoot string,
|
|
751
|
+
) (*ConfigStore, error) {
|
|
752
|
+
store := &ConfigStore{resolutionRoot: filepath.Clean(resolutionRoot)}
|
|
704
753
|
var chain []string
|
|
705
754
|
if rootPath != "" {
|
|
706
|
-
|
|
755
|
+
rootPath = filepath.Clean(rootPath)
|
|
756
|
+
chain = []string{rootPath}
|
|
757
|
+
store.paths = append(store.paths, rootPath)
|
|
707
758
|
}
|
|
708
759
|
if err := collectConfigObject(store, raw, configDir, "config", chain); err != nil {
|
|
709
760
|
return nil, err
|
|
@@ -787,11 +838,19 @@ func collectConfigObject(store *ConfigStore, raw any, baseDir, path string, chai
|
|
|
787
838
|
if err != nil {
|
|
788
839
|
return err
|
|
789
840
|
}
|
|
790
|
-
|
|
841
|
+
if !containsPath(store.paths, location) {
|
|
842
|
+
store.paths = append(store.paths, location)
|
|
843
|
+
}
|
|
844
|
+
evaluated, err := loadConfigFileEvaluationWithin(
|
|
845
|
+
location,
|
|
846
|
+
store.resolutionRoot,
|
|
847
|
+
)
|
|
791
848
|
if err != nil {
|
|
792
849
|
return err
|
|
793
850
|
}
|
|
794
|
-
|
|
851
|
+
appendConfigPaths(store, evaluated.dependencies)
|
|
852
|
+
appendConfigDirectories(store, evaluated.dependencyDirectories)
|
|
853
|
+
if err := collectConfigObject(store, evaluated.value, filepath.Dir(location), path+".extends", extendedChain); err != nil {
|
|
795
854
|
return err
|
|
796
855
|
}
|
|
797
856
|
}
|
|
@@ -1004,6 +1063,7 @@ func LoadConfigResolver(entry *PluginEntry, cwd, tsconfigPath string) (RuleResol
|
|
|
1004
1063
|
if inline == nil {
|
|
1005
1064
|
inline = map[string]any{}
|
|
1006
1065
|
}
|
|
1066
|
+
resolutionRoot := tsconfigBaseDir(cwd, tsconfigPath)
|
|
1007
1067
|
|
|
1008
1068
|
if configFileValue, ok := inline["configFile"]; ok {
|
|
1009
1069
|
configFile, ok := configFileValue.(string)
|
|
@@ -1014,7 +1074,7 @@ func LoadConfigResolver(entry *PluginEntry, cwd, tsconfigPath string) (RuleResol
|
|
|
1014
1074
|
return nil, fmt.Errorf("@ttsc/lint: \"configFile\" must not be empty")
|
|
1015
1075
|
}
|
|
1016
1076
|
location := resolveConfigFilePath(configFile, cwd, tsconfigPath)
|
|
1017
|
-
return loadConfigResolver(location)
|
|
1077
|
+
return loadConfigResolver(location, resolutionRoot)
|
|
1018
1078
|
}
|
|
1019
1079
|
|
|
1020
1080
|
discovered, err := findLintConfigFile(cwd, tsconfigPath)
|
|
@@ -1028,23 +1088,53 @@ func LoadConfigResolver(entry *PluginEntry, cwd, tsconfigPath string) (RuleResol
|
|
|
1028
1088
|
strings.Join(discoveryConfigBaseDirs(cwd, tsconfigPath), ", then from "),
|
|
1029
1089
|
)
|
|
1030
1090
|
}
|
|
1031
|
-
return loadConfigResolver(discovered)
|
|
1091
|
+
return loadConfigResolver(discovered, resolutionRoot)
|
|
1032
1092
|
}
|
|
1033
1093
|
|
|
1034
1094
|
// loadConfigResolver loads and parses the lint config file at `location` into
|
|
1035
1095
|
// a *ConfigStore and returns it as a RuleResolver.
|
|
1036
|
-
func loadConfigResolver(
|
|
1037
|
-
|
|
1096
|
+
func loadConfigResolver(
|
|
1097
|
+
location string,
|
|
1098
|
+
resolutionRoot string,
|
|
1099
|
+
) (RuleResolver, error) {
|
|
1100
|
+
evaluated, err := loadConfigFileEvaluationWithin(location, resolutionRoot)
|
|
1038
1101
|
if err != nil {
|
|
1039
1102
|
return nil, err
|
|
1040
1103
|
}
|
|
1041
|
-
store, err :=
|
|
1104
|
+
store, err := collectConfigStoreWithin(
|
|
1105
|
+
evaluated.value,
|
|
1106
|
+
filepath.Dir(location),
|
|
1107
|
+
location,
|
|
1108
|
+
resolutionRoot,
|
|
1109
|
+
)
|
|
1042
1110
|
if err != nil {
|
|
1043
1111
|
return nil, err
|
|
1044
1112
|
}
|
|
1113
|
+
appendConfigPaths(store, evaluated.dependencies)
|
|
1114
|
+
appendConfigDirectories(store, evaluated.dependencyDirectories)
|
|
1045
1115
|
return store, nil
|
|
1046
1116
|
}
|
|
1047
1117
|
|
|
1118
|
+
func appendConfigPaths(store *ConfigStore, paths []string) {
|
|
1119
|
+
for _, location := range paths {
|
|
1120
|
+
location = filepath.Clean(location)
|
|
1121
|
+
if !containsPath(store.paths, location) {
|
|
1122
|
+
store.paths = append(store.paths, location)
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
sort.Strings(store.paths)
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
func appendConfigDirectories(store *ConfigStore, directories []string) {
|
|
1129
|
+
for _, location := range directories {
|
|
1130
|
+
location = filepath.Clean(location)
|
|
1131
|
+
if !containsPath(store.directories, location) {
|
|
1132
|
+
store.directories = append(store.directories, location)
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
sort.Strings(store.directories)
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1048
1138
|
func findLintConfigFile(cwd, tsconfigPath string) (string, error) {
|
|
1049
1139
|
for _, origin := range discoveryConfigBaseDirs(cwd, tsconfigPath) {
|
|
1050
1140
|
discovered, err := findLintConfigFileFrom(origin)
|
|
@@ -1209,23 +1299,84 @@ func tsconfigBaseDir(cwd, tsconfigPath string) string {
|
|
|
1209
1299
|
// monorepo build — which spawns one `ttsc` process per package — evaluates a
|
|
1210
1300
|
// shared lint config once instead of once per package.
|
|
1211
1301
|
func loadConfigFile(location string) (any, error) {
|
|
1302
|
+
evaluated, err := loadConfigFileEvaluation(location)
|
|
1303
|
+
return evaluated.value, err
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
type configDependencyFingerprint struct {
|
|
1307
|
+
Path string `json:"path"`
|
|
1308
|
+
Digest string `json:"digest"`
|
|
1309
|
+
Kind string `json:"kind"`
|
|
1310
|
+
Scope string `json:"scope"`
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const (
|
|
1314
|
+
configDependencyCache = "cache"
|
|
1315
|
+
configDependencyWatch = "watch"
|
|
1316
|
+
configDependencyFile = "file"
|
|
1317
|
+
configDependencyDir = "directory"
|
|
1318
|
+
configDependencyOptionalFile = "optional-file"
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
type evaluatedConfigFile struct {
|
|
1322
|
+
value any
|
|
1323
|
+
dependencies []string
|
|
1324
|
+
dependencyDirectories []string
|
|
1325
|
+
dependencyDigests []configDependencyFingerprint
|
|
1326
|
+
dependenciesTracked bool
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
type cachedConfigEvaluation struct {
|
|
1330
|
+
Value any `json:"value"`
|
|
1331
|
+
Dependencies []configDependencyFingerprint `json:"dependencies"`
|
|
1332
|
+
DependenciesTracked bool `json:"dependenciesTracked"`
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
func loadConfigFileEvaluation(location string) (evaluatedConfigFile, error) {
|
|
1336
|
+
return loadConfigFileEvaluationWithin(location, filepath.Dir(location))
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
func loadConfigFileEvaluationWithin(
|
|
1340
|
+
location string,
|
|
1341
|
+
resolutionRoot string,
|
|
1342
|
+
) (evaluatedConfigFile, error) {
|
|
1343
|
+
if strings.TrimSpace(resolutionRoot) == "" {
|
|
1344
|
+
resolutionRoot = filepath.Dir(location)
|
|
1345
|
+
}
|
|
1346
|
+
if absolute, err := filepath.Abs(resolutionRoot); err == nil {
|
|
1347
|
+
resolutionRoot = absolute
|
|
1348
|
+
}
|
|
1349
|
+
resolutionRoot = filepath.Clean(resolutionRoot)
|
|
1212
1350
|
ext := strings.ToLower(filepath.Ext(location))
|
|
1213
1351
|
switch ext {
|
|
1214
1352
|
case ".json":
|
|
1215
|
-
|
|
1353
|
+
value, err := loadJSONConfigFile(location)
|
|
1354
|
+
return evaluatedConfigFile{value: value}, err
|
|
1216
1355
|
case ".js", ".cjs", ".mjs":
|
|
1217
|
-
return
|
|
1356
|
+
return loadCachedConfigEvaluationForRoot(
|
|
1357
|
+
location,
|
|
1358
|
+
resolutionRoot,
|
|
1359
|
+
func(location string) (evaluatedConfigFile, error) {
|
|
1360
|
+
return loadScriptConfigEvaluationWithin(location, resolutionRoot)
|
|
1361
|
+
},
|
|
1362
|
+
)
|
|
1218
1363
|
case ".ts", ".cts", ".mts":
|
|
1219
|
-
return
|
|
1364
|
+
return loadCachedConfigEvaluationForRoot(
|
|
1365
|
+
location,
|
|
1366
|
+
resolutionRoot,
|
|
1367
|
+
func(location string) (evaluatedConfigFile, error) {
|
|
1368
|
+
return loadTypeScriptConfigEvaluationWithin(location, resolutionRoot)
|
|
1369
|
+
},
|
|
1370
|
+
)
|
|
1220
1371
|
default:
|
|
1221
|
-
return
|
|
1372
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: unsupported config file extension %q for %s", ext, location)
|
|
1222
1373
|
}
|
|
1223
1374
|
}
|
|
1224
1375
|
|
|
1225
1376
|
// configCacheVersion namespaces the on-disk config cache. Bump it whenever
|
|
1226
1377
|
// the shape of a cached config object changes so that entries written by an
|
|
1227
1378
|
// older @ttsc/lint binary are treated as a miss rather than silently reused.
|
|
1228
|
-
const configCacheVersion = "
|
|
1379
|
+
const configCacheVersion = "v5"
|
|
1229
1380
|
|
|
1230
1381
|
// configEvalCache memoizes evaluated .ts/.js lint config objects for the
|
|
1231
1382
|
// lifetime of one process; the on-disk cache (configCacheDir) extends the
|
|
@@ -1233,7 +1384,7 @@ const configCacheVersion = "v2"
|
|
|
1233
1384
|
// spawns. Guarded by configEvalCacheMu.
|
|
1234
1385
|
var (
|
|
1235
1386
|
configEvalCacheMu sync.Mutex
|
|
1236
|
-
configEvalCache = map[string]
|
|
1387
|
+
configEvalCache = map[string]cachedConfigEvaluation{}
|
|
1237
1388
|
)
|
|
1238
1389
|
|
|
1239
1390
|
// configCacheDir is the directory shared by this Go sidecar and the JS
|
|
@@ -1245,8 +1396,9 @@ func configCacheDir() string {
|
|
|
1245
1396
|
}
|
|
1246
1397
|
|
|
1247
1398
|
// configCacheDisabled reports whether the env opt-out is set — an escape
|
|
1248
|
-
// hatch for
|
|
1249
|
-
//
|
|
1399
|
+
// hatch for configs whose behavior depends on state outside their local module
|
|
1400
|
+
// graph, such as environment variables, network responses, or arbitrary file
|
|
1401
|
+
// reads.
|
|
1250
1402
|
func configCacheDisabled() bool {
|
|
1251
1403
|
return os.Getenv("TTSC_LINT_DISABLE_CONFIG_CACHE") != ""
|
|
1252
1404
|
}
|
|
@@ -1269,67 +1421,254 @@ func configCacheKey(kind, absPath string, content []byte) string {
|
|
|
1269
1421
|
return hex.EncodeToString(h.Sum(nil))
|
|
1270
1422
|
}
|
|
1271
1423
|
|
|
1272
|
-
// loadCachedConfigFile
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1277
|
-
// Errors are never cached: a failed evaluation re-runs next time.
|
|
1424
|
+
// loadCachedConfigFile preserves the historical value-only test seam around
|
|
1425
|
+
// the two-tier (in-process + on-disk) cache. Production executable-config
|
|
1426
|
+
// loaders use loadCachedConfigEvaluation so their complete local module graph
|
|
1427
|
+
// participates in validation. Errors are never cached: a failed evaluation
|
|
1428
|
+
// re-runs next time.
|
|
1278
1429
|
func loadCachedConfigFile(location string, eval func(string) (any, error)) (any, error) {
|
|
1430
|
+
evaluated, err := loadCachedConfigEvaluationWithPolicy(
|
|
1431
|
+
location,
|
|
1432
|
+
func(location string) (evaluatedConfigFile, error) {
|
|
1433
|
+
value, err := eval(location)
|
|
1434
|
+
return evaluatedConfigFile{value: value}, err
|
|
1435
|
+
},
|
|
1436
|
+
false,
|
|
1437
|
+
"",
|
|
1438
|
+
)
|
|
1439
|
+
return evaluated.value, err
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
func loadCachedConfigEvaluation(
|
|
1443
|
+
location string,
|
|
1444
|
+
eval func(string) (evaluatedConfigFile, error),
|
|
1445
|
+
) (evaluatedConfigFile, error) {
|
|
1446
|
+
return loadCachedConfigEvaluationWithPolicy(location, eval, true, "")
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
func loadCachedConfigEvaluationForRoot(
|
|
1450
|
+
location string,
|
|
1451
|
+
resolutionRoot string,
|
|
1452
|
+
eval func(string) (evaluatedConfigFile, error),
|
|
1453
|
+
) (evaluatedConfigFile, error) {
|
|
1454
|
+
return loadCachedConfigEvaluationWithPolicy(
|
|
1455
|
+
location,
|
|
1456
|
+
eval,
|
|
1457
|
+
true,
|
|
1458
|
+
filepath.Clean(resolutionRoot),
|
|
1459
|
+
)
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
func loadCachedConfigEvaluationWithPolicy(
|
|
1463
|
+
location string,
|
|
1464
|
+
eval func(string) (evaluatedConfigFile, error),
|
|
1465
|
+
dependenciesRequired bool,
|
|
1466
|
+
cacheNamespace string,
|
|
1467
|
+
) (evaluatedConfigFile, error) {
|
|
1279
1468
|
if configCacheDisabled() {
|
|
1280
1469
|
return eval(location)
|
|
1281
1470
|
}
|
|
1282
1471
|
content, err := os.ReadFile(location)
|
|
1283
1472
|
if err != nil {
|
|
1284
|
-
return
|
|
1473
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: read config file %s: %w", location, err)
|
|
1285
1474
|
}
|
|
1286
1475
|
abs := location
|
|
1287
1476
|
if resolved, absErr := filepath.Abs(location); absErr == nil {
|
|
1288
1477
|
abs = resolved
|
|
1289
1478
|
}
|
|
1290
|
-
|
|
1479
|
+
kind := "config-value"
|
|
1480
|
+
if dependenciesRequired {
|
|
1481
|
+
kind = "config-graph"
|
|
1482
|
+
}
|
|
1483
|
+
if cacheNamespace != "" {
|
|
1484
|
+
kind += "\x00" + cacheNamespace
|
|
1485
|
+
}
|
|
1486
|
+
key := configCacheKey(kind, abs, content)
|
|
1291
1487
|
|
|
1292
1488
|
configEvalCacheMu.Lock()
|
|
1293
1489
|
cached, ok := configEvalCache[key]
|
|
1294
1490
|
configEvalCacheMu.Unlock()
|
|
1295
|
-
if ok
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1491
|
+
if ok &&
|
|
1492
|
+
cached.DependenciesTracked == dependenciesRequired &&
|
|
1493
|
+
cachedConfigEvaluationIsCurrent(cached) {
|
|
1494
|
+
return evaluatedConfigFileFromCache(cached), nil
|
|
1495
|
+
}
|
|
1496
|
+
if disk, hit := readConfigDiskCache(key); hit &&
|
|
1497
|
+
disk.DependenciesTracked == dependenciesRequired &&
|
|
1498
|
+
cachedConfigEvaluationIsCurrent(disk) {
|
|
1299
1499
|
configEvalCacheMu.Lock()
|
|
1300
|
-
configEvalCache[key] =
|
|
1500
|
+
configEvalCache[key] = disk
|
|
1301
1501
|
configEvalCacheMu.Unlock()
|
|
1302
|
-
return
|
|
1502
|
+
return evaluatedConfigFileFromCache(disk), nil
|
|
1303
1503
|
}
|
|
1304
1504
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1505
|
+
var evaluated evaluatedConfigFile
|
|
1506
|
+
for attempt := 0; attempt < 3; attempt++ {
|
|
1507
|
+
evaluated, err = eval(location)
|
|
1508
|
+
if err != nil {
|
|
1509
|
+
return evaluatedConfigFile{}, err
|
|
1510
|
+
}
|
|
1511
|
+
if evaluated.dependenciesTracked != dependenciesRequired {
|
|
1512
|
+
return evaluatedConfigFile{}, fmt.Errorf(
|
|
1513
|
+
"@ttsc/lint: config evaluator for %s returned dependenciesTracked=%t, want %t",
|
|
1514
|
+
location,
|
|
1515
|
+
evaluated.dependenciesTracked,
|
|
1516
|
+
dependenciesRequired,
|
|
1517
|
+
)
|
|
1518
|
+
}
|
|
1519
|
+
if (!evaluated.dependenciesTracked ||
|
|
1520
|
+
len(evaluated.dependencyDigests) != 0) &&
|
|
1521
|
+
configDependencyDigestsAreCurrent(evaluated.dependencyDigests) {
|
|
1522
|
+
cached = cachedConfigEvaluation{
|
|
1523
|
+
Value: evaluated.value,
|
|
1524
|
+
Dependencies: append([]configDependencyFingerprint(nil), evaluated.dependencyDigests...),
|
|
1525
|
+
DependenciesTracked: evaluated.dependenciesTracked,
|
|
1526
|
+
}
|
|
1527
|
+
configEvalCacheMu.Lock()
|
|
1528
|
+
configEvalCache[key] = cached
|
|
1529
|
+
configEvalCacheMu.Unlock()
|
|
1530
|
+
writeConfigDiskCache(key, cached)
|
|
1531
|
+
return evaluated, nil
|
|
1532
|
+
}
|
|
1308
1533
|
}
|
|
1309
|
-
|
|
1310
|
-
configEvalCache[key] = value
|
|
1311
|
-
configEvalCacheMu.Unlock()
|
|
1312
|
-
writeConfigDiskCache(key, value)
|
|
1313
|
-
return value, nil
|
|
1534
|
+
return evaluated, nil
|
|
1314
1535
|
}
|
|
1315
1536
|
|
|
1316
1537
|
// readConfigDiskCache returns the cached config object for `key`, or
|
|
1317
1538
|
// (nil, false) on any miss — a missing file, an unreadable file, or
|
|
1318
1539
|
// content that no longer parses as a config object. Every failure is a
|
|
1319
1540
|
// soft miss: the caller re-evaluates rather than surfacing a cache fault.
|
|
1320
|
-
func
|
|
1541
|
+
func evaluatedConfigFileFromCache(cached cachedConfigEvaluation) evaluatedConfigFile {
|
|
1542
|
+
dependencies := make([]string, 0, len(cached.Dependencies))
|
|
1543
|
+
directories := make([]string, 0, len(cached.Dependencies))
|
|
1544
|
+
for _, dependency := range cached.Dependencies {
|
|
1545
|
+
if dependency.Scope == configDependencyWatch {
|
|
1546
|
+
if dependency.Kind == configDependencyDir {
|
|
1547
|
+
directories = append(directories, dependency.Path)
|
|
1548
|
+
} else {
|
|
1549
|
+
dependencies = append(dependencies, dependency.Path)
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
return evaluatedConfigFile{
|
|
1554
|
+
value: cached.Value,
|
|
1555
|
+
dependencies: dependencies,
|
|
1556
|
+
dependencyDirectories: directories,
|
|
1557
|
+
dependencyDigests: append([]configDependencyFingerprint(nil), cached.Dependencies...),
|
|
1558
|
+
dependenciesTracked: cached.DependenciesTracked,
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
func configDependencyDigestsAreCurrent(
|
|
1563
|
+
dependencies []configDependencyFingerprint,
|
|
1564
|
+
) bool {
|
|
1565
|
+
for _, dependency := range dependencies {
|
|
1566
|
+
digest, err := configDependencyDigest(dependency)
|
|
1567
|
+
if err != nil {
|
|
1568
|
+
return false
|
|
1569
|
+
}
|
|
1570
|
+
if digest != dependency.Digest {
|
|
1571
|
+
return false
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return true
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
func configDependencyDigest(
|
|
1578
|
+
dependency configDependencyFingerprint,
|
|
1579
|
+
) (string, error) {
|
|
1580
|
+
if dependency.Kind == configDependencyDir {
|
|
1581
|
+
entries, err := os.ReadDir(dependency.Path)
|
|
1582
|
+
if err != nil {
|
|
1583
|
+
return "", err
|
|
1584
|
+
}
|
|
1585
|
+
h := sha256.New()
|
|
1586
|
+
for index, entry := range entries {
|
|
1587
|
+
kind := "other"
|
|
1588
|
+
info, err := entry.Info()
|
|
1589
|
+
if err != nil {
|
|
1590
|
+
return "", err
|
|
1591
|
+
}
|
|
1592
|
+
switch {
|
|
1593
|
+
case info.Mode()&os.ModeSymlink != 0:
|
|
1594
|
+
kind = "symlink"
|
|
1595
|
+
case info.IsDir():
|
|
1596
|
+
kind = "directory"
|
|
1597
|
+
case info.Mode().IsRegular():
|
|
1598
|
+
kind = "file"
|
|
1599
|
+
}
|
|
1600
|
+
target := ""
|
|
1601
|
+
if kind == "symlink" {
|
|
1602
|
+
target, err = os.Readlink(filepath.Join(dependency.Path, entry.Name()))
|
|
1603
|
+
if err != nil {
|
|
1604
|
+
target = "<unreadable>"
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
h.Write([]byte(entry.Name()))
|
|
1608
|
+
h.Write([]byte{0})
|
|
1609
|
+
h.Write([]byte(kind))
|
|
1610
|
+
h.Write([]byte{0})
|
|
1611
|
+
h.Write([]byte(target))
|
|
1612
|
+
if index+1 != len(entries) {
|
|
1613
|
+
h.Write([]byte{0})
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
return hex.EncodeToString(h.Sum(nil)), nil
|
|
1617
|
+
}
|
|
1618
|
+
if dependency.Kind == configDependencyOptionalFile {
|
|
1619
|
+
info, err := os.Stat(dependency.Path)
|
|
1620
|
+
if err != nil || !info.Mode().IsRegular() {
|
|
1621
|
+
digest := sha256.Sum256([]byte("missing\x00"))
|
|
1622
|
+
return hex.EncodeToString(digest[:]), nil
|
|
1623
|
+
}
|
|
1624
|
+
body, err := os.ReadFile(dependency.Path)
|
|
1625
|
+
if err != nil {
|
|
1626
|
+
digest := sha256.Sum256([]byte("missing\x00"))
|
|
1627
|
+
return hex.EncodeToString(digest[:]), nil
|
|
1628
|
+
}
|
|
1629
|
+
h := sha256.New()
|
|
1630
|
+
h.Write([]byte("file\x00"))
|
|
1631
|
+
h.Write(body)
|
|
1632
|
+
return hex.EncodeToString(h.Sum(nil)), nil
|
|
1633
|
+
}
|
|
1634
|
+
body, err := os.ReadFile(dependency.Path)
|
|
1635
|
+
if err != nil {
|
|
1636
|
+
return "", err
|
|
1637
|
+
}
|
|
1638
|
+
digest := sha256.Sum256(body)
|
|
1639
|
+
return hex.EncodeToString(digest[:]), nil
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
func cachedConfigEvaluationIsCurrent(cached cachedConfigEvaluation) bool {
|
|
1643
|
+
if !cached.DependenciesTracked {
|
|
1644
|
+
return len(cached.Dependencies) == 0
|
|
1645
|
+
}
|
|
1646
|
+
normalized, ok := normalizeConfigDependencyFingerprints(cached.Dependencies)
|
|
1647
|
+
return ok && configDependencyDigestsAreCurrent(normalized)
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
func readConfigDiskCache(key string) (cachedConfigEvaluation, bool) {
|
|
1321
1651
|
body, err := os.ReadFile(filepath.Join(configCacheDir(), key+".json"))
|
|
1322
1652
|
if err != nil {
|
|
1323
|
-
return
|
|
1653
|
+
return cachedConfigEvaluation{}, false
|
|
1324
1654
|
}
|
|
1325
|
-
var
|
|
1326
|
-
if err := json.Unmarshal(body, &
|
|
1327
|
-
return
|
|
1655
|
+
var cached cachedConfigEvaluation
|
|
1656
|
+
if err := json.Unmarshal(body, &cached); err != nil {
|
|
1657
|
+
return cachedConfigEvaluation{}, false
|
|
1328
1658
|
}
|
|
1329
|
-
if !isConfigObject(
|
|
1330
|
-
return
|
|
1659
|
+
if !isConfigObject(cached.Value) {
|
|
1660
|
+
return cachedConfigEvaluation{}, false
|
|
1661
|
+
}
|
|
1662
|
+
if cached.DependenciesTracked {
|
|
1663
|
+
normalized, ok := normalizeConfigDependencyFingerprints(cached.Dependencies)
|
|
1664
|
+
if !ok {
|
|
1665
|
+
return cachedConfigEvaluation{}, false
|
|
1666
|
+
}
|
|
1667
|
+
cached.Dependencies = normalized
|
|
1668
|
+
} else if len(cached.Dependencies) != 0 {
|
|
1669
|
+
return cachedConfigEvaluation{}, false
|
|
1331
1670
|
}
|
|
1332
|
-
return
|
|
1671
|
+
return cached, true
|
|
1333
1672
|
}
|
|
1334
1673
|
|
|
1335
1674
|
// writeConfigDiskCache stores `value` under `key`. It is best-effort: a
|
|
@@ -1337,8 +1676,8 @@ func readConfigDiskCache(key string) (any, bool) {
|
|
|
1337
1676
|
// (the next run re-evaluates) rather than failing the lint run. The write
|
|
1338
1677
|
// goes through a temp file + rename so a concurrent reader in a sibling
|
|
1339
1678
|
// `ttsc` process never observes a half-written entry.
|
|
1340
|
-
func writeConfigDiskCache(key string,
|
|
1341
|
-
body, err := json.Marshal(
|
|
1679
|
+
func writeConfigDiskCache(key string, cached cachedConfigEvaluation) {
|
|
1680
|
+
body, err := json.Marshal(cached)
|
|
1342
1681
|
if err != nil {
|
|
1343
1682
|
return
|
|
1344
1683
|
}
|
|
@@ -1408,56 +1747,294 @@ func serializableConfigKeysLiteral() string {
|
|
|
1408
1747
|
|
|
1409
1748
|
// runConfigLoaderCommand runs a prepared config-loader subprocess (`cmd`),
|
|
1410
1749
|
// then turns its result into a parsed config object. It owns the shared tail
|
|
1411
|
-
// of both subprocess-backed loaders:
|
|
1412
|
-
// timeout from a process error
|
|
1413
|
-
//
|
|
1750
|
+
// of both subprocess-backed loaders: discarding user stdout, distinguishing a
|
|
1751
|
+
// timeout from a process error, reading the private result file, JSON-parsing
|
|
1752
|
+
// its envelope, and rejecting a non-object result. `ctx` is the
|
|
1414
1753
|
// timeout context the caller bound `cmd` to; `location` is the config file path
|
|
1415
1754
|
// for error messages; `label` is the human-readable subject (e.g. "config
|
|
1416
1755
|
// file" or "TypeScript config file") spliced into the load/parse error
|
|
1417
1756
|
// prefixes so each loader keeps its own wording.
|
|
1418
|
-
func runConfigLoaderCommand(
|
|
1419
|
-
|
|
1757
|
+
func runConfigLoaderCommand(
|
|
1758
|
+
ctx context.Context,
|
|
1759
|
+
cmd *exec.Cmd,
|
|
1760
|
+
location string,
|
|
1761
|
+
label string,
|
|
1762
|
+
outputPath string,
|
|
1763
|
+
) (evaluatedConfigFile, error) {
|
|
1764
|
+
var stderr bytes.Buffer
|
|
1765
|
+
cmd.Stdout = io.Discard
|
|
1766
|
+
cmd.Stderr = &stderr
|
|
1767
|
+
err := cmd.Run()
|
|
1768
|
+
// A loader diagnostic is only useful when the load succeeds, because a
|
|
1769
|
+
// failure already carries the same text in its message. Forward it so an
|
|
1770
|
+
// assertion about what the loader recorded can name what it resolved.
|
|
1771
|
+
if err == nil && os.Getenv("TTSC_LINT_DEBUG_CONFIG_GRAPH") != "" {
|
|
1772
|
+
if text := strings.TrimSpace(stderr.String()); text != "" {
|
|
1773
|
+
fmt.Fprintln(os.Stderr, text)
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1420
1776
|
if err != nil {
|
|
1421
1777
|
if ctx.Err() == context.DeadlineExceeded {
|
|
1422
|
-
return
|
|
1778
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: load %s %s: timed out after %s", label, location, configLoaderTimeout)
|
|
1423
1779
|
}
|
|
1424
|
-
|
|
1425
|
-
if
|
|
1426
|
-
|
|
1780
|
+
stderrText := strings.TrimSpace(stderr.String())
|
|
1781
|
+
if stderrText != "" {
|
|
1782
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: load %s %s: %s", label, location, stderrText)
|
|
1427
1783
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1784
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: load %s %s: %w", label, location, err)
|
|
1785
|
+
}
|
|
1786
|
+
output, err := os.ReadFile(outputPath)
|
|
1787
|
+
if err != nil {
|
|
1788
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: read %s %s result: %w", label, location, err)
|
|
1789
|
+
}
|
|
1790
|
+
var envelope struct {
|
|
1791
|
+
Dependencies []configDependencyFingerprint `json:"dependencies"`
|
|
1792
|
+
Value any `json:"value"`
|
|
1793
|
+
}
|
|
1794
|
+
if err := json.Unmarshal(output, &envelope); err != nil {
|
|
1795
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: parse %s %s output: %w", label, location, err)
|
|
1796
|
+
}
|
|
1797
|
+
if !isConfigObject(envelope.Value) {
|
|
1798
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: config file %s must export an ITtscLintConfig object", location)
|
|
1799
|
+
}
|
|
1800
|
+
normalized, ok := normalizeConfigDependencyFingerprints(envelope.Dependencies)
|
|
1801
|
+
if !ok {
|
|
1802
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: %s %s returned malformed dependency fingerprints", label, location)
|
|
1803
|
+
}
|
|
1804
|
+
dependencies := make([]string, 0, len(normalized))
|
|
1805
|
+
directories := make([]string, 0, len(normalized))
|
|
1806
|
+
for _, dependency := range normalized {
|
|
1807
|
+
if dependency.Scope == configDependencyWatch {
|
|
1808
|
+
if dependency.Kind == configDependencyDir {
|
|
1809
|
+
directories = append(directories, dependency.Path)
|
|
1810
|
+
} else {
|
|
1811
|
+
dependencies = append(dependencies, dependency.Path)
|
|
1812
|
+
}
|
|
1430
1813
|
}
|
|
1431
|
-
return nil, fmt.Errorf("@ttsc/lint: load %s %s: %w", label, location, err)
|
|
1432
1814
|
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1815
|
+
return evaluatedConfigFile{
|
|
1816
|
+
value: envelope.Value,
|
|
1817
|
+
dependencies: dependencies,
|
|
1818
|
+
dependencyDirectories: directories,
|
|
1819
|
+
dependencyDigests: normalized,
|
|
1820
|
+
dependenciesTracked: true,
|
|
1821
|
+
}, nil
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
func normalizeConfigDependencyFingerprints(
|
|
1825
|
+
input []configDependencyFingerprint,
|
|
1826
|
+
) ([]configDependencyFingerprint, bool) {
|
|
1827
|
+
if len(input) == 0 {
|
|
1828
|
+
return nil, false
|
|
1436
1829
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1830
|
+
seen := make(map[string]configDependencyFingerprint, len(input))
|
|
1831
|
+
normalized := make([]configDependencyFingerprint, 0, len(input))
|
|
1832
|
+
for _, dependency := range input {
|
|
1833
|
+
if strings.TrimSpace(dependency.Path) == "" ||
|
|
1834
|
+
!filepath.IsAbs(dependency.Path) ||
|
|
1835
|
+
len(dependency.Digest) != sha256.Size*2 ||
|
|
1836
|
+
strings.ToLower(dependency.Digest) != dependency.Digest ||
|
|
1837
|
+
(dependency.Kind != configDependencyFile &&
|
|
1838
|
+
dependency.Kind != configDependencyDir &&
|
|
1839
|
+
dependency.Kind != configDependencyOptionalFile) ||
|
|
1840
|
+
(dependency.Scope != configDependencyCache &&
|
|
1841
|
+
dependency.Scope != configDependencyWatch) {
|
|
1842
|
+
return nil, false
|
|
1843
|
+
}
|
|
1844
|
+
if _, err := hex.DecodeString(dependency.Digest); err != nil {
|
|
1845
|
+
return nil, false
|
|
1846
|
+
}
|
|
1847
|
+
absolute := filepath.Clean(dependency.Path)
|
|
1848
|
+
key := dependency.Kind + "\x00" + absolute
|
|
1849
|
+
if previous, exists := seen[key]; exists {
|
|
1850
|
+
if previous.Digest != dependency.Digest ||
|
|
1851
|
+
previous.Kind != dependency.Kind ||
|
|
1852
|
+
previous.Scope != dependency.Scope {
|
|
1853
|
+
return nil, false
|
|
1854
|
+
}
|
|
1855
|
+
continue
|
|
1856
|
+
}
|
|
1857
|
+
fingerprint := configDependencyFingerprint{
|
|
1858
|
+
Path: absolute,
|
|
1859
|
+
Digest: dependency.Digest,
|
|
1860
|
+
Kind: dependency.Kind,
|
|
1861
|
+
Scope: dependency.Scope,
|
|
1862
|
+
}
|
|
1863
|
+
seen[key] = fingerprint
|
|
1864
|
+
normalized = append(normalized, fingerprint)
|
|
1439
1865
|
}
|
|
1440
|
-
|
|
1866
|
+
sort.Slice(normalized, func(left, right int) bool {
|
|
1867
|
+
return normalized[left].Path < normalized[right].Path
|
|
1868
|
+
})
|
|
1869
|
+
return normalized, true
|
|
1441
1870
|
}
|
|
1442
1871
|
|
|
1443
1872
|
// loadScriptConfigFile evaluates a .js/.cjs/.mjs config file by running a
|
|
1444
1873
|
// Node subprocess that dynamic-imports the file, resolves the exported config
|
|
1445
|
-
// through the same 8-hop default/config normalization used by the TS loader,
|
|
1446
|
-
// serializes the result
|
|
1874
|
+
// through the same 8-hop default/config normalization used by the TS loader,
|
|
1875
|
+
// and serializes the result into a private result file. The subprocess has a
|
|
1447
1876
|
// configLoaderTimeout deadline to prevent user code from hanging indefinitely.
|
|
1448
1877
|
func loadScriptConfigFile(location string) (any, error) {
|
|
1449
|
-
|
|
1450
|
-
|
|
1878
|
+
evaluated, err := loadScriptConfigEvaluation(location)
|
|
1879
|
+
return evaluated.value, err
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
func loadScriptConfigEvaluation(location string) (evaluatedConfigFile, error) {
|
|
1883
|
+
return loadScriptConfigEvaluationWithin(location, filepath.Dir(location))
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
func loadScriptConfigEvaluationWithin(
|
|
1887
|
+
location string,
|
|
1888
|
+
resolutionRoot string,
|
|
1889
|
+
) (evaluatedConfigFile, error) {
|
|
1890
|
+
tempDir, err := os.MkdirTemp("", "ttsc-lint-script-config-")
|
|
1891
|
+
if err != nil {
|
|
1892
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: create script config result directory: %w", err)
|
|
1893
|
+
}
|
|
1894
|
+
defer os.RemoveAll(tempDir)
|
|
1895
|
+
outputPath := filepath.Join(tempDir, "result.json")
|
|
1896
|
+
script := scriptConfigLoaderSource()
|
|
1897
|
+
node := os.Getenv("TTSC_NODE_BINARY")
|
|
1898
|
+
if node == "" {
|
|
1899
|
+
node = "node"
|
|
1900
|
+
}
|
|
1901
|
+
ctx, cancel := context.WithTimeout(context.Background(), configLoaderTimeout)
|
|
1902
|
+
defer cancel()
|
|
1903
|
+
cmd := exec.CommandContext(
|
|
1904
|
+
ctx,
|
|
1905
|
+
node,
|
|
1906
|
+
"-e",
|
|
1907
|
+
script,
|
|
1908
|
+
location,
|
|
1909
|
+
outputPath,
|
|
1910
|
+
resolutionRoot,
|
|
1911
|
+
)
|
|
1912
|
+
return runConfigLoaderCommand(ctx, cmd, location, "config file", outputPath)
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// scriptConfigLoaderSource returns the CommonJS source of the loader script
|
|
1916
|
+
// Node executes to evaluate a .js/.cjs/.mjs lint config file. It is a named
|
|
1917
|
+
// function for the same reason as typeScriptConfigLoaderSource: the source is
|
|
1918
|
+
// a fmt.Sprintf format string, so every literal percent sign inside it must be
|
|
1919
|
+
// doubled, and only a callable generator lets a regression prove the emitted
|
|
1920
|
+
// script carries no formatting artifact.
|
|
1921
|
+
func scriptConfigLoaderSource() string {
|
|
1922
|
+
return fmt.Sprintf(`
|
|
1923
|
+
const { Buffer } = require("node:buffer");
|
|
1924
|
+
const fs = require("node:fs");
|
|
1925
|
+
const { createHash } = require("node:crypto");
|
|
1926
|
+
const { registerHooks } = require("node:module");
|
|
1927
|
+
const path = require("node:path");
|
|
1928
|
+
const { fileURLToPath, pathToFileURL } = require("node:url");
|
|
1451
1929
|
|
|
1452
1930
|
const CONFIG_KEYS = new Set([%s]);
|
|
1931
|
+
const configUrl = pathToFileURL(process.argv[1]).href;
|
|
1932
|
+
const outputPath = process.argv[2];
|
|
1933
|
+
const resolutionRoot = path.resolve(process.argv[3]);
|
|
1934
|
+
const dependencies = new Map();
|
|
1935
|
+
const graphNodes = new Map();
|
|
1936
|
+
const graphEdges = [];
|
|
1937
|
+
const configLocation = fileURLToPath(configUrl);
|
|
1938
|
+
// Every spelling of this config the module system might key an edge under.
|
|
1939
|
+
//
|
|
1940
|
+
// Which one it uses is not knowable from here, and guessing has failed in both
|
|
1941
|
+
// directions. A path handed in by another producer can be escaped by a rule
|
|
1942
|
+
// Node does not share. Node respells a resolved file module through its real
|
|
1943
|
+
// path unless "--preserve-symlinks" is set, so a config reached through a
|
|
1944
|
+
// symlinked directory is keyed by its target. And a Windows 8.3 short name is
|
|
1945
|
+
// not a symlink: fs.realpathSync expands it, the module resolver does not, so
|
|
1946
|
+
// asking the volume there produces a spelling no edge carries.
|
|
1947
|
+
//
|
|
1948
|
+
// A seed that names a URL no edge was keyed under sits on a node with no
|
|
1949
|
+
// outgoing edges, the walk ends immediately, and every dependency recorded
|
|
1950
|
+
// after the first import is demoted from watch to cache. That failure is
|
|
1951
|
+
// silent: the build still succeeds and simply stops reacting. Seeding every
|
|
1952
|
+
// spelling costs one extra queue entry and cannot be wrong.
|
|
1953
|
+
const configUrlSpellings = [
|
|
1954
|
+
...new Set([
|
|
1955
|
+
configUrl,
|
|
1956
|
+
pathToFileURL(configLocation).href,
|
|
1957
|
+
pathToFileURL(realConfigLocation()).href,
|
|
1958
|
+
]),
|
|
1959
|
+
];
|
|
1960
|
+
for (const spelling of configUrlSpellings) {
|
|
1961
|
+
graphNodes.set(spelling, configLocation);
|
|
1962
|
+
}
|
|
1963
|
+
recordDependency(
|
|
1964
|
+
"file",
|
|
1965
|
+
configLocation,
|
|
1966
|
+
createHash("sha256").update(fs.readFileSync(configLocation)).digest("hex"),
|
|
1967
|
+
configUrlSpellings,
|
|
1968
|
+
);
|
|
1969
|
+
recordPackageManifests(configLocation, configUrlSpellings);
|
|
1970
|
+
const hooks = registerHooks({
|
|
1971
|
+
resolve(specifier, context, nextResolve) {
|
|
1972
|
+
const resolved = nextResolve(specifier, context);
|
|
1973
|
+
if (typeof resolved.url !== "string" || !resolved.url.startsWith("file:")) {
|
|
1974
|
+
return resolved;
|
|
1975
|
+
}
|
|
1976
|
+
const url = new URL(resolved.url).href;
|
|
1977
|
+
const parent = context.parentURL && new URL(context.parentURL).href;
|
|
1978
|
+
const location = fileURLToPath(url);
|
|
1979
|
+
// The entry is recognized by what was asked for, not only by what came
|
|
1980
|
+
// back. A module URL is assigned by whoever loaded it: a compiling loader
|
|
1981
|
+
// can serve the config from its emitted output, and a platform can hand
|
|
1982
|
+
// back a different spelling of the same file. Either way the URL bears no
|
|
1983
|
+
// resemblance to the one this process was given, so the config's own
|
|
1984
|
+
// imports would be rejected here — their parent is a URL no node was
|
|
1985
|
+
// recorded under — and the graph would collapse to the records made before
|
|
1986
|
+
// the first import. The request itself is unambiguous, so it decides.
|
|
1987
|
+
const entry =
|
|
1988
|
+
specifier === configUrl ||
|
|
1989
|
+
url === configUrl ||
|
|
1990
|
+
samePhysicalPath(location, configLocation);
|
|
1991
|
+
if (!entry && (parent === undefined || !graphNodes.has(parent))) {
|
|
1992
|
+
return resolved;
|
|
1993
|
+
}
|
|
1994
|
+
graphNodes.set(url, location);
|
|
1995
|
+
if (parent !== undefined) {
|
|
1996
|
+
graphEdges.push({
|
|
1997
|
+
child: url,
|
|
1998
|
+
packageBoundary:
|
|
1999
|
+
pathHasNodeModules(location) && !isLocalModuleSpecifier(specifier),
|
|
2000
|
+
parent,
|
|
2001
|
+
});
|
|
2002
|
+
recordResolutionTopology(
|
|
2003
|
+
specifier,
|
|
2004
|
+
parent,
|
|
2005
|
+
url,
|
|
2006
|
+
location,
|
|
2007
|
+
context.conditions,
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
try {
|
|
2011
|
+
recordDependency(
|
|
2012
|
+
"file",
|
|
2013
|
+
location,
|
|
2014
|
+
createHash("sha256").update(fs.readFileSync(location)).digest("hex"),
|
|
2015
|
+
[url],
|
|
2016
|
+
);
|
|
2017
|
+
} catch {
|
|
2018
|
+
recordDependency("file", location, "", [url]);
|
|
2019
|
+
}
|
|
2020
|
+
return resolved;
|
|
2021
|
+
},
|
|
2022
|
+
});
|
|
1453
2023
|
|
|
1454
2024
|
(async () => {
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
2025
|
+
try {
|
|
2026
|
+
const mod = await import(configUrl);
|
|
2027
|
+
const value = await resolveConfig(mod, true);
|
|
2028
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
2029
|
+
throw new Error("config file must export an ITtscLintConfig object");
|
|
2030
|
+
}
|
|
2031
|
+
fs.writeFileSync(outputPath, JSON.stringify({
|
|
2032
|
+
dependencies: finalizeDependencies(),
|
|
2033
|
+
value: toSerializableConfig(value),
|
|
2034
|
+
}), "utf8");
|
|
2035
|
+
} finally {
|
|
2036
|
+
hooks.deregister();
|
|
1459
2037
|
}
|
|
1460
|
-
process.stdout.write(JSON.stringify(toSerializableConfig(value)));
|
|
1461
2038
|
})().catch((error) => {
|
|
1462
2039
|
process.stderr.write(error && error.stack ? error.stack : String(error));
|
|
1463
2040
|
process.exit(1);
|
|
@@ -1501,6 +2078,768 @@ function isModuleNamespace(value) {
|
|
|
1501
2078
|
return Object.prototype.toString.call(value) === "[object Module]";
|
|
1502
2079
|
}
|
|
1503
2080
|
|
|
2081
|
+
function isObject(value) {
|
|
2082
|
+
return value !== null && typeof value === "object";
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
function recordDependency(kind, location, digest, owners) {
|
|
2086
|
+
const key = kind + "\0" + location;
|
|
2087
|
+
const previous = dependencies.get(key);
|
|
2088
|
+
const mergedOwners = previous ? previous.owners : new Set();
|
|
2089
|
+
for (const owner of owners) mergedOwners.add(owner);
|
|
2090
|
+
dependencies.set(key, {
|
|
2091
|
+
digest: previous && previous.digest !== digest ? "" : digest,
|
|
2092
|
+
kind,
|
|
2093
|
+
owners: mergedOwners,
|
|
2094
|
+
path: location,
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
function isLocalModuleSpecifier(specifier) {
|
|
2099
|
+
return specifier.startsWith(".") ||
|
|
2100
|
+
specifier.startsWith("/") ||
|
|
2101
|
+
specifier.startsWith("file:") ||
|
|
2102
|
+
/^[A-Za-z]:[\\/]/.test(specifier);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
function pathHasNodeModules(location) {
|
|
2106
|
+
return location.replaceAll("\\", "/").split("/").includes("node_modules");
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function recordResolutionTopology(
|
|
2110
|
+
specifier,
|
|
2111
|
+
parentUrl,
|
|
2112
|
+
childUrl,
|
|
2113
|
+
childLocation,
|
|
2114
|
+
conditions,
|
|
2115
|
+
) {
|
|
2116
|
+
const owners = [parentUrl, childUrl];
|
|
2117
|
+
const parentLocation = graphNodes.get(parentUrl);
|
|
2118
|
+
if (parentLocation !== undefined && isLocalModuleSpecifier(specifier)) {
|
|
2119
|
+
recordDirectoryDependency(path.dirname(parentLocation), owners);
|
|
2120
|
+
}
|
|
2121
|
+
recordDirectoryDependency(path.dirname(childLocation), owners);
|
|
2122
|
+
recordPackageManifests(childLocation, owners);
|
|
2123
|
+
if (parentLocation !== undefined && !isLocalModuleSpecifier(specifier)) {
|
|
2124
|
+
recordNodeModulesSearchDirectories(
|
|
2125
|
+
parentLocation,
|
|
2126
|
+
specifier,
|
|
2127
|
+
childLocation,
|
|
2128
|
+
owners,
|
|
2129
|
+
conditions,
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
function recordDirectoryDependency(location, owners) {
|
|
2135
|
+
try {
|
|
2136
|
+
recordDependency("directory", location, directoryDigest(location), owners);
|
|
2137
|
+
} catch {
|
|
2138
|
+
recordDependency("directory", location, "", owners);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
function directoryDigest(location) {
|
|
2143
|
+
const entries = [];
|
|
2144
|
+
if (process.platform === "win32") {
|
|
2145
|
+
for (const entry of fs.readdirSync(location, { withFileTypes: true })) {
|
|
2146
|
+
let target = Buffer.alloc(0);
|
|
2147
|
+
if (entry.isSymbolicLink()) {
|
|
2148
|
+
try {
|
|
2149
|
+
target = Buffer.from(
|
|
2150
|
+
fs.readlinkSync(path.join(location, entry.name)),
|
|
2151
|
+
"utf8",
|
|
2152
|
+
);
|
|
2153
|
+
} catch {
|
|
2154
|
+
target = Buffer.from("<unreadable>");
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
entries.push(directoryDigestRecord(Buffer.from(entry.name), entry, target));
|
|
2158
|
+
}
|
|
2159
|
+
} else {
|
|
2160
|
+
for (const entry of fs.readdirSync(
|
|
2161
|
+
location,
|
|
2162
|
+
{ encoding: "buffer", withFileTypes: true },
|
|
2163
|
+
)) {
|
|
2164
|
+
let target = Buffer.alloc(0);
|
|
2165
|
+
if (entry.isSymbolicLink()) {
|
|
2166
|
+
try {
|
|
2167
|
+
target = fs.readlinkSync(
|
|
2168
|
+
Buffer.concat([
|
|
2169
|
+
Buffer.from(location),
|
|
2170
|
+
Buffer.from(path.sep),
|
|
2171
|
+
entry.name,
|
|
2172
|
+
]),
|
|
2173
|
+
{ encoding: "buffer" },
|
|
2174
|
+
);
|
|
2175
|
+
} catch {
|
|
2176
|
+
target = Buffer.from("<unreadable>");
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
entries.push(directoryDigestRecord(entry.name, entry, target));
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
entries.sort(Buffer.compare);
|
|
2183
|
+
const serialized = Buffer.concat(
|
|
2184
|
+
entries.flatMap((entry, index) =>
|
|
2185
|
+
index === 0 ? [entry] : [Buffer.from([0]), entry],
|
|
2186
|
+
),
|
|
2187
|
+
);
|
|
2188
|
+
return createHash("sha256").update(serialized).digest("hex");
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function directoryDigestRecord(name, entry, target) {
|
|
2192
|
+
const kind = entry.isDirectory()
|
|
2193
|
+
? "directory"
|
|
2194
|
+
: entry.isFile()
|
|
2195
|
+
? "file"
|
|
2196
|
+
: entry.isSymbolicLink()
|
|
2197
|
+
? "symlink"
|
|
2198
|
+
: "other";
|
|
2199
|
+
return Buffer.concat([name, Buffer.from("\0" + kind + "\0"), target]);
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
function optionalFileDigest(location) {
|
|
2203
|
+
try {
|
|
2204
|
+
if (fs.statSync(location).isFile()) {
|
|
2205
|
+
return createHash("sha256")
|
|
2206
|
+
.update(Buffer.concat([Buffer.from("file\0"), fs.readFileSync(location)]))
|
|
2207
|
+
.digest("hex");
|
|
2208
|
+
}
|
|
2209
|
+
} catch {
|
|
2210
|
+
}
|
|
2211
|
+
return createHash("sha256").update("missing\0").digest("hex");
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
function recordOptionalFileDependency(location, owners) {
|
|
2215
|
+
try {
|
|
2216
|
+
if (fs.statSync(location).isFile()) {
|
|
2217
|
+
recordDependency(
|
|
2218
|
+
"file",
|
|
2219
|
+
location,
|
|
2220
|
+
createHash("sha256").update(fs.readFileSync(location)).digest("hex"),
|
|
2221
|
+
owners,
|
|
2222
|
+
);
|
|
2223
|
+
return true;
|
|
2224
|
+
}
|
|
2225
|
+
} catch {
|
|
2226
|
+
}
|
|
2227
|
+
recordDependency("optional-file", location, optionalFileDigest(location), owners);
|
|
2228
|
+
return false;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
function recordPackageManifests(location, owners) {
|
|
2232
|
+
let current = path.dirname(location);
|
|
2233
|
+
while (true) {
|
|
2234
|
+
const manifest = path.join(current, "package.json");
|
|
2235
|
+
if (recordOptionalFileDependency(manifest, owners)) return;
|
|
2236
|
+
const parent = path.dirname(current);
|
|
2237
|
+
if (parent === current || path.basename(current) === "node_modules") return;
|
|
2238
|
+
current = parent;
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
function recordNodeModulesSearchDirectories(
|
|
2243
|
+
parentLocation,
|
|
2244
|
+
specifier,
|
|
2245
|
+
childLocation,
|
|
2246
|
+
owners,
|
|
2247
|
+
conditions,
|
|
2248
|
+
) {
|
|
2249
|
+
const packageName = modulePackageName(specifier);
|
|
2250
|
+
const scope =
|
|
2251
|
+
specifier.startsWith("@") && specifier.includes("/")
|
|
2252
|
+
? specifier.slice(0, specifier.indexOf("/"))
|
|
2253
|
+
: undefined;
|
|
2254
|
+
let current = path.dirname(parentLocation);
|
|
2255
|
+
while (true) {
|
|
2256
|
+
recordDirectoryDependency(current, owners);
|
|
2257
|
+
const modules = path.join(current, "node_modules");
|
|
2258
|
+
try {
|
|
2259
|
+
if (fs.statSync(modules).isDirectory()) {
|
|
2260
|
+
recordDirectoryDependency(modules, owners);
|
|
2261
|
+
if (scope !== undefined) {
|
|
2262
|
+
const scoped = path.join(modules, scope);
|
|
2263
|
+
try {
|
|
2264
|
+
if (fs.statSync(scoped).isDirectory()) {
|
|
2265
|
+
recordDirectoryDependency(scoped, owners);
|
|
2266
|
+
}
|
|
2267
|
+
} catch {
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
if (packageName !== undefined) {
|
|
2271
|
+
const selected = recordPackageCandidateTopology(
|
|
2272
|
+
modules,
|
|
2273
|
+
packageName,
|
|
2274
|
+
specifier,
|
|
2275
|
+
childLocation,
|
|
2276
|
+
owners,
|
|
2277
|
+
conditions,
|
|
2278
|
+
);
|
|
2279
|
+
if (
|
|
2280
|
+
selected ||
|
|
2281
|
+
resolvedPackageContains(modules, packageName, childLocation)
|
|
2282
|
+
) {
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
} catch {
|
|
2288
|
+
}
|
|
2289
|
+
if (
|
|
2290
|
+
packageName === undefined &&
|
|
2291
|
+
samePhysicalPath(current, resolutionRoot)
|
|
2292
|
+
) {
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
const parent = path.dirname(current);
|
|
2296
|
+
if (parent === current) return;
|
|
2297
|
+
current = parent;
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
function recordPackageCandidateTopology(
|
|
2302
|
+
modules,
|
|
2303
|
+
packageName,
|
|
2304
|
+
specifier,
|
|
2305
|
+
childLocation,
|
|
2306
|
+
owners,
|
|
2307
|
+
conditions,
|
|
2308
|
+
) {
|
|
2309
|
+
const packageRoot = path.join(modules, packageName);
|
|
2310
|
+
try {
|
|
2311
|
+
if (!fs.statSync(packageRoot).isDirectory()) return false;
|
|
2312
|
+
} catch {
|
|
2313
|
+
return false;
|
|
2314
|
+
}
|
|
2315
|
+
const subpath = specifier
|
|
2316
|
+
.slice(packageName.length)
|
|
2317
|
+
.replace(/^[/\\]+/, "");
|
|
2318
|
+
const rootTopology = recordPackageRootTopology(
|
|
2319
|
+
packageRoot,
|
|
2320
|
+
owners,
|
|
2321
|
+
subpath === "",
|
|
2322
|
+
subpath === "" ? "." : "./" + subpath.replaceAll("\\", "/"),
|
|
2323
|
+
childLocation,
|
|
2324
|
+
conditions,
|
|
2325
|
+
);
|
|
2326
|
+
if (subpath !== "" && !rootTopology.hasExports) {
|
|
2327
|
+
return (
|
|
2328
|
+
recordPackageSubpathTopology(
|
|
2329
|
+
packageRoot,
|
|
2330
|
+
subpath,
|
|
2331
|
+
childLocation,
|
|
2332
|
+
owners,
|
|
2333
|
+
) || rootTopology.selected
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
return rootTopology.selected;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
function recordPackageRootTopology(
|
|
2340
|
+
packageRoot,
|
|
2341
|
+
owners,
|
|
2342
|
+
useMain,
|
|
2343
|
+
packageSubpath,
|
|
2344
|
+
childLocation,
|
|
2345
|
+
conditions,
|
|
2346
|
+
) {
|
|
2347
|
+
const normalizedRoot = path.resolve(packageRoot);
|
|
2348
|
+
const manifest = path.join(normalizedRoot, "package.json");
|
|
2349
|
+
const legacySelected = () =>
|
|
2350
|
+
useMain &&
|
|
2351
|
+
packagePathCandidateMatchesChild(normalizedRoot, childLocation, true);
|
|
2352
|
+
if (!recordOptionalFileDependency(manifest, owners)) {
|
|
2353
|
+
const selected = legacySelected();
|
|
2354
|
+
if (!selected) {
|
|
2355
|
+
recordPackageIndexCandidates(normalizedRoot, useMain, owners);
|
|
2356
|
+
}
|
|
2357
|
+
return { hasExports: false, selected };
|
|
2358
|
+
}
|
|
2359
|
+
try {
|
|
2360
|
+
const value = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
2361
|
+
if (value !== null && typeof value === "object") {
|
|
2362
|
+
const hasExports =
|
|
2363
|
+
value.exports !== undefined && value.exports !== null;
|
|
2364
|
+
if (hasExports) {
|
|
2365
|
+
const target = selectPackageExportsTarget(
|
|
2366
|
+
value.exports,
|
|
2367
|
+
packageSubpath,
|
|
2368
|
+
new Set(conditions),
|
|
2369
|
+
);
|
|
2370
|
+
const candidate =
|
|
2371
|
+
typeof target === "string"
|
|
2372
|
+
? packageExportsTarget(normalizedRoot, target)
|
|
2373
|
+
: undefined;
|
|
2374
|
+
const selected =
|
|
2375
|
+
candidate !== undefined &&
|
|
2376
|
+
packagePathCandidateMatchesChild(
|
|
2377
|
+
candidate,
|
|
2378
|
+
childLocation,
|
|
2379
|
+
false,
|
|
2380
|
+
);
|
|
2381
|
+
if (selected) {
|
|
2382
|
+
recordPackagePathCandidate(candidate, owners);
|
|
2383
|
+
} else if (candidate !== undefined) {
|
|
2384
|
+
// A nearer package the search skipped starts winning the moment its
|
|
2385
|
+
// own active target appears, and neither the parent node_modules
|
|
2386
|
+
// listing nor the manifest changes when only that file is created.
|
|
2387
|
+
recordOptionalFileDependency(candidate, owners);
|
|
2388
|
+
}
|
|
2389
|
+
return { hasExports: true, selected };
|
|
2390
|
+
}
|
|
2391
|
+
let selected = legacySelected();
|
|
2392
|
+
if (useMain && typeof value.main === "string") {
|
|
2393
|
+
const main = path.resolve(normalizedRoot, value.main);
|
|
2394
|
+
recordPackagePathCandidate(main, owners);
|
|
2395
|
+
selected =
|
|
2396
|
+
packagePathCandidateMatchesChild(main, childLocation, true) ||
|
|
2397
|
+
selected;
|
|
2398
|
+
}
|
|
2399
|
+
if (!selected) {
|
|
2400
|
+
recordPackageIndexCandidates(normalizedRoot, useMain, owners);
|
|
2401
|
+
}
|
|
2402
|
+
return { hasExports: false, selected };
|
|
2403
|
+
}
|
|
2404
|
+
} catch {
|
|
2405
|
+
}
|
|
2406
|
+
const rootSelected = legacySelected();
|
|
2407
|
+
if (!rootSelected) {
|
|
2408
|
+
recordPackageIndexCandidates(normalizedRoot, useMain, owners);
|
|
2409
|
+
}
|
|
2410
|
+
return { hasExports: false, selected: rootSelected };
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// recordPackageIndexCandidates pins the LOAD_INDEX fallbacks of a package root
|
|
2414
|
+
// this resolution walked past without selecting. An empty package directory, or
|
|
2415
|
+
// one whose manifest declares no usable entry, becomes resolvable as soon as one
|
|
2416
|
+
// of these files exists, and that creation changes neither the parent directory
|
|
2417
|
+
// listing nor the manifest digest already recorded for the candidate.
|
|
2418
|
+
function recordPackageIndexCandidates(packageRoot, useMain, owners) {
|
|
2419
|
+
if (!useMain) return;
|
|
2420
|
+
for (const name of ["index.js", "index.json", "index.node"]) {
|
|
2421
|
+
recordOptionalFileDependency(path.join(packageRoot, name), owners);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
function selectPackageExportsTarget(
|
|
2426
|
+
exportsValue,
|
|
2427
|
+
packageSubpath,
|
|
2428
|
+
conditions,
|
|
2429
|
+
) {
|
|
2430
|
+
let mappings = exportsValue;
|
|
2431
|
+
if (
|
|
2432
|
+
typeof mappings === "string" ||
|
|
2433
|
+
Array.isArray(mappings) ||
|
|
2434
|
+
(isObject(mappings) &&
|
|
2435
|
+
Object.keys(mappings).every((key) => !key.startsWith(".")))
|
|
2436
|
+
) {
|
|
2437
|
+
if (packageSubpath !== ".") return undefined;
|
|
2438
|
+
return selectPackageTarget(mappings, "", false, conditions);
|
|
2439
|
+
}
|
|
2440
|
+
if (!isObject(mappings)) return undefined;
|
|
2441
|
+
if (
|
|
2442
|
+
Object.prototype.hasOwnProperty.call(mappings, packageSubpath) &&
|
|
2443
|
+
!packageSubpath.includes("*") &&
|
|
2444
|
+
!packageSubpath.endsWith("/")
|
|
2445
|
+
) {
|
|
2446
|
+
return selectPackageTarget(
|
|
2447
|
+
mappings[packageSubpath],
|
|
2448
|
+
"",
|
|
2449
|
+
false,
|
|
2450
|
+
conditions,
|
|
2451
|
+
);
|
|
2452
|
+
}
|
|
2453
|
+
let bestMatch = "";
|
|
2454
|
+
let bestSubpath = "";
|
|
2455
|
+
for (const key of Object.keys(mappings)) {
|
|
2456
|
+
const wildcard = key.indexOf("*");
|
|
2457
|
+
if (
|
|
2458
|
+
wildcard === -1 ||
|
|
2459
|
+
key.lastIndexOf("*") !== wildcard ||
|
|
2460
|
+
!packageSubpath.startsWith(key.slice(0, wildcard))
|
|
2461
|
+
) {
|
|
2462
|
+
continue;
|
|
2463
|
+
}
|
|
2464
|
+
const trailer = key.slice(wildcard + 1);
|
|
2465
|
+
if (
|
|
2466
|
+
packageSubpath.length < key.length ||
|
|
2467
|
+
!packageSubpath.endsWith(trailer) ||
|
|
2468
|
+
packagePatternKeyCompare(bestMatch, key) !== 1
|
|
2469
|
+
) {
|
|
2470
|
+
continue;
|
|
2471
|
+
}
|
|
2472
|
+
bestMatch = key;
|
|
2473
|
+
bestSubpath = packageSubpath.slice(
|
|
2474
|
+
wildcard,
|
|
2475
|
+
packageSubpath.length - trailer.length,
|
|
2476
|
+
);
|
|
2477
|
+
}
|
|
2478
|
+
return bestMatch === ""
|
|
2479
|
+
? undefined
|
|
2480
|
+
: selectPackageTarget(
|
|
2481
|
+
mappings[bestMatch],
|
|
2482
|
+
bestSubpath,
|
|
2483
|
+
true,
|
|
2484
|
+
conditions,
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
function selectPackageTarget(target, subpath, pattern, conditions) {
|
|
2489
|
+
if (typeof target === "string") {
|
|
2490
|
+
const selected = pattern ? target.replaceAll("*", subpath) : target;
|
|
2491
|
+
return validPackageExportsTarget(selected) ? selected : undefined;
|
|
2492
|
+
}
|
|
2493
|
+
if (Array.isArray(target)) {
|
|
2494
|
+
for (const item of target) {
|
|
2495
|
+
const selected = selectPackageTarget(
|
|
2496
|
+
item,
|
|
2497
|
+
subpath,
|
|
2498
|
+
pattern,
|
|
2499
|
+
conditions,
|
|
2500
|
+
);
|
|
2501
|
+
if (selected !== undefined && selected !== null) return selected;
|
|
2502
|
+
}
|
|
2503
|
+
return null;
|
|
2504
|
+
}
|
|
2505
|
+
if (isObject(target)) {
|
|
2506
|
+
for (const [condition, value] of Object.entries(target)) {
|
|
2507
|
+
if (condition !== "default" && !conditions.has(condition)) continue;
|
|
2508
|
+
const selected = selectPackageTarget(
|
|
2509
|
+
value,
|
|
2510
|
+
subpath,
|
|
2511
|
+
pattern,
|
|
2512
|
+
conditions,
|
|
2513
|
+
);
|
|
2514
|
+
if (selected !== undefined) return selected;
|
|
2515
|
+
}
|
|
2516
|
+
return undefined;
|
|
2517
|
+
}
|
|
2518
|
+
return target === null ? null : undefined;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
function packagePatternKeyCompare(left, right) {
|
|
2522
|
+
const leftWildcard = left.indexOf("*");
|
|
2523
|
+
const rightWildcard = right.indexOf("*");
|
|
2524
|
+
const leftBase =
|
|
2525
|
+
leftWildcard === -1 ? left.length : leftWildcard + 1;
|
|
2526
|
+
const rightBase =
|
|
2527
|
+
rightWildcard === -1 ? right.length : rightWildcard + 1;
|
|
2528
|
+
if (leftBase > rightBase) return -1;
|
|
2529
|
+
if (rightBase > leftBase) return 1;
|
|
2530
|
+
if (leftWildcard === -1) return 1;
|
|
2531
|
+
if (rightWildcard === -1) return -1;
|
|
2532
|
+
if (left.length > right.length) return -1;
|
|
2533
|
+
if (right.length > left.length) return 1;
|
|
2534
|
+
return 0;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
function packageExportsTarget(packageRoot, target) {
|
|
2538
|
+
if (!validPackageExportsTarget(target)) return undefined;
|
|
2539
|
+
try {
|
|
2540
|
+
// Node resolves an exports target as a URL against the package manifest,
|
|
2541
|
+
// so percent escapes, query strings, and fragments all take part in the
|
|
2542
|
+
// path it finally loads. Joining the raw target by hand diverges from that
|
|
2543
|
+
// whenever the target is anything but a plain relative path, and a target
|
|
2544
|
+
// Node resolves while this model rejects loses the selected file's
|
|
2545
|
+
// fingerprint, leaving a retargeted symlink cached as fresh.
|
|
2546
|
+
const packageUrl = pathToFileURL(path.join(packageRoot, "package.json"));
|
|
2547
|
+
const resolved = new URL(target, packageUrl);
|
|
2548
|
+
const packagePath = new URL(".", packageUrl).pathname;
|
|
2549
|
+
if (!resolved.pathname.startsWith(packagePath)) return undefined;
|
|
2550
|
+
return fileURLToPath(resolved);
|
|
2551
|
+
} catch {
|
|
2552
|
+
return undefined;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
function validPackageExportsTarget(target) {
|
|
2557
|
+
if (!target.startsWith("./") || /%%2f|%%5c/i.test(target)) return false;
|
|
2558
|
+
const components = target
|
|
2559
|
+
.slice(2)
|
|
2560
|
+
.replaceAll("\\", "/")
|
|
2561
|
+
.split("/");
|
|
2562
|
+
if (
|
|
2563
|
+
components.some(
|
|
2564
|
+
(component) => {
|
|
2565
|
+
try {
|
|
2566
|
+
const decoded = decodeURIComponent(component);
|
|
2567
|
+
return (
|
|
2568
|
+
decoded === "." ||
|
|
2569
|
+
decoded === ".." ||
|
|
2570
|
+
decoded.includes("/") ||
|
|
2571
|
+
decoded.includes("\\") ||
|
|
2572
|
+
decoded.toLowerCase() === "node_modules"
|
|
2573
|
+
);
|
|
2574
|
+
} catch {
|
|
2575
|
+
return true;
|
|
2576
|
+
}
|
|
2577
|
+
},
|
|
2578
|
+
)
|
|
2579
|
+
) {
|
|
2580
|
+
return false;
|
|
2581
|
+
}
|
|
2582
|
+
return true;
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
function packagePathCandidateMatchesChild(
|
|
2586
|
+
candidate,
|
|
2587
|
+
childLocation,
|
|
2588
|
+
legacy,
|
|
2589
|
+
) {
|
|
2590
|
+
let child;
|
|
2591
|
+
try {
|
|
2592
|
+
child = fs.realpathSync.native(childLocation);
|
|
2593
|
+
} catch {
|
|
2594
|
+
child = path.resolve(childLocation);
|
|
2595
|
+
}
|
|
2596
|
+
const candidates = legacy
|
|
2597
|
+
? [
|
|
2598
|
+
candidate,
|
|
2599
|
+
candidate + ".js",
|
|
2600
|
+
candidate + ".json",
|
|
2601
|
+
candidate + ".node",
|
|
2602
|
+
path.join(candidate, "index.js"),
|
|
2603
|
+
path.join(candidate, "index.json"),
|
|
2604
|
+
path.join(candidate, "index.node"),
|
|
2605
|
+
]
|
|
2606
|
+
: [candidate];
|
|
2607
|
+
return candidates.some((location) => {
|
|
2608
|
+
try {
|
|
2609
|
+
return sameResolutionPath(fs.realpathSync.native(location), child);
|
|
2610
|
+
} catch {
|
|
2611
|
+
return false;
|
|
2612
|
+
}
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
function recordPackageSubpathTopology(
|
|
2617
|
+
packageRoot,
|
|
2618
|
+
subpath,
|
|
2619
|
+
childLocation,
|
|
2620
|
+
owners,
|
|
2621
|
+
) {
|
|
2622
|
+
const candidate = boundedPackageTarget(packageRoot, subpath);
|
|
2623
|
+
if (candidate === undefined) return false;
|
|
2624
|
+
recordPackagePathCandidate(candidate, owners);
|
|
2625
|
+
let selected = packagePathCandidateMatchesChild(
|
|
2626
|
+
candidate,
|
|
2627
|
+
childLocation,
|
|
2628
|
+
true,
|
|
2629
|
+
);
|
|
2630
|
+
try {
|
|
2631
|
+
if (!fs.statSync(candidate).isDirectory()) return selected;
|
|
2632
|
+
} catch {
|
|
2633
|
+
return selected;
|
|
2634
|
+
}
|
|
2635
|
+
const manifest = path.join(candidate, "package.json");
|
|
2636
|
+
if (!recordOptionalFileDependency(manifest, owners)) return selected;
|
|
2637
|
+
try {
|
|
2638
|
+
const value = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
2639
|
+
if (value !== null && typeof value === "object") {
|
|
2640
|
+
if (typeof value.main === "string") {
|
|
2641
|
+
const main = path.resolve(candidate, value.main);
|
|
2642
|
+
recordPackagePathCandidate(main, owners);
|
|
2643
|
+
selected =
|
|
2644
|
+
packagePathCandidateMatchesChild(main, childLocation, true) ||
|
|
2645
|
+
selected;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
} catch {
|
|
2649
|
+
}
|
|
2650
|
+
return selected;
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
function boundedPackageTarget(
|
|
2654
|
+
packageRoot,
|
|
2655
|
+
target,
|
|
2656
|
+
) {
|
|
2657
|
+
const candidate = path.resolve(packageRoot, target);
|
|
2658
|
+
const relative = path.relative(packageRoot, candidate);
|
|
2659
|
+
if (
|
|
2660
|
+
relative === ".." ||
|
|
2661
|
+
relative.startsWith(".." + path.sep) ||
|
|
2662
|
+
path.isAbsolute(relative)
|
|
2663
|
+
) {
|
|
2664
|
+
return undefined;
|
|
2665
|
+
}
|
|
2666
|
+
return candidate;
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
function recordPackagePathCandidate(
|
|
2670
|
+
candidate,
|
|
2671
|
+
owners,
|
|
2672
|
+
visited = new Set(),
|
|
2673
|
+
depth = 0,
|
|
2674
|
+
) {
|
|
2675
|
+
const normalized = path.resolve(candidate);
|
|
2676
|
+
// The depth bound owns termination. A platform-wide case fold would merge
|
|
2677
|
+
// paths that differ only by case, which a per-directory case-sensitive
|
|
2678
|
+
// Windows tree keeps distinct, and would truncate a valid symlink chain.
|
|
2679
|
+
if (depth >= 64 || visited.has(normalized)) return;
|
|
2680
|
+
visited.add(normalized);
|
|
2681
|
+
const parsed = path.parse(normalized);
|
|
2682
|
+
const components = normalized
|
|
2683
|
+
.slice(parsed.root.length)
|
|
2684
|
+
.split(path.sep)
|
|
2685
|
+
.filter(Boolean);
|
|
2686
|
+
let current = parsed.root;
|
|
2687
|
+
for (let index = 0; index < components.length; index++) {
|
|
2688
|
+
const component = components[index];
|
|
2689
|
+
const next = path.join(current, component);
|
|
2690
|
+
let entry;
|
|
2691
|
+
try {
|
|
2692
|
+
entry = fs.lstatSync(next);
|
|
2693
|
+
} catch {
|
|
2694
|
+
recordDirectoryDependency(current, owners);
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
if (entry.isSymbolicLink()) {
|
|
2698
|
+
recordDirectoryDependency(current, owners);
|
|
2699
|
+
try {
|
|
2700
|
+
const target = fs.readlinkSync(next);
|
|
2701
|
+
const remainder = components.slice(index + 1);
|
|
2702
|
+
recordPackagePathCandidate(
|
|
2703
|
+
path.join(path.resolve(current, target), ...remainder),
|
|
2704
|
+
owners,
|
|
2705
|
+
visited,
|
|
2706
|
+
depth + 1,
|
|
2707
|
+
);
|
|
2708
|
+
} catch {
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
let isDirectory = entry.isDirectory();
|
|
2712
|
+
if (entry.isSymbolicLink()) {
|
|
2713
|
+
try {
|
|
2714
|
+
isDirectory = fs.statSync(next).isDirectory();
|
|
2715
|
+
} catch {
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
if (index === components.length - 1) {
|
|
2720
|
+
recordDirectoryDependency(isDirectory ? next : current, owners);
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2723
|
+
if (!isDirectory) {
|
|
2724
|
+
recordDirectoryDependency(current, owners);
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
current = next;
|
|
2728
|
+
}
|
|
2729
|
+
recordDirectoryDependency(current, owners);
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
function modulePackageName(specifier) {
|
|
2733
|
+
if (specifier.startsWith("@")) {
|
|
2734
|
+
const components = specifier.split("/");
|
|
2735
|
+
return components.length >= 2
|
|
2736
|
+
? components[0] + "/" + components[1]
|
|
2737
|
+
: undefined;
|
|
2738
|
+
}
|
|
2739
|
+
const [name] = specifier.split("/");
|
|
2740
|
+
return name && !name.startsWith("#") ? name : undefined;
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
function resolvedPackageContains(modules, packageName, childLocation) {
|
|
2744
|
+
try {
|
|
2745
|
+
const packageRoot = fs.realpathSync(path.join(modules, packageName));
|
|
2746
|
+
const relative = path.relative(
|
|
2747
|
+
packageRoot,
|
|
2748
|
+
fs.realpathSync(childLocation),
|
|
2749
|
+
);
|
|
2750
|
+
return (
|
|
2751
|
+
relative === "" ||
|
|
2752
|
+
(relative !== ".." &&
|
|
2753
|
+
!relative.startsWith(".." + path.sep) &&
|
|
2754
|
+
!path.isAbsolute(relative))
|
|
2755
|
+
);
|
|
2756
|
+
} catch {
|
|
2757
|
+
return false;
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
function sameResolutionPath(left, right) {
|
|
2762
|
+
return path.relative(left, right) === "";
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
function samePhysicalPath(left, right) {
|
|
2766
|
+
try {
|
|
2767
|
+
return sameResolutionPath(realPath(left), realPath(right));
|
|
2768
|
+
} catch {
|
|
2769
|
+
// Fall back to the spellings themselves, folding case the way the platform
|
|
2770
|
+
// does. On the entry gate a false negative is catastrophic — the config
|
|
2771
|
+
// stops being recognized and its whole graph collapses — while a false
|
|
2772
|
+
// positive only over-includes, so the degradation has to lean toward "same
|
|
2773
|
+
// file". A drive-letter or component case difference is the ordinary
|
|
2774
|
+
// Windows situation; a per-directory case-sensitive tree is the rare one.
|
|
2775
|
+
return sameResolutionPath(left, right);
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
/**
|
|
2780
|
+
* The config's real path, or its declared one when the volume will not say.
|
|
2781
|
+
*
|
|
2782
|
+
* A config can disappear between the host reading it and this loader starting,
|
|
2783
|
+
* and a throw here would replace a precise report from the import below with a
|
|
2784
|
+
* crash in bookkeeping. Seeding lexically instead only risks the demotion this
|
|
2785
|
+
* value exists to prevent, on a file that is already gone.
|
|
2786
|
+
*/
|
|
2787
|
+
function realConfigLocation() {
|
|
2788
|
+
try {
|
|
2789
|
+
return realPath(configLocation);
|
|
2790
|
+
} catch {
|
|
2791
|
+
return configLocation;
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
function realPath(location) {
|
|
2796
|
+
return fs.realpathSync.native
|
|
2797
|
+
? fs.realpathSync.native(location)
|
|
2798
|
+
: fs.realpathSync(location);
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
function finalizeDependencies() {
|
|
2802
|
+
const watched = graphWatchReachability();
|
|
2803
|
+
return [...dependencies.values()].map(({ owners, ...dependency }) => ({
|
|
2804
|
+
...dependency,
|
|
2805
|
+
scope: [...owners].some((owner) => watched.has(owner))
|
|
2806
|
+
? "watch"
|
|
2807
|
+
: "cache",
|
|
2808
|
+
}));
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
function graphWatchReachability() {
|
|
2812
|
+
const adjacency = new Map();
|
|
2813
|
+
for (const edge of graphEdges) {
|
|
2814
|
+
const outgoing = adjacency.get(edge.parent) || [];
|
|
2815
|
+
outgoing.push(edge);
|
|
2816
|
+
adjacency.set(edge.parent, outgoing);
|
|
2817
|
+
}
|
|
2818
|
+
const queue = configUrlSpellings.map((url) => ({
|
|
2819
|
+
url,
|
|
2820
|
+
watched: true,
|
|
2821
|
+
}));
|
|
2822
|
+
const visited = new Set();
|
|
2823
|
+
const watched = new Set();
|
|
2824
|
+
while (queue.length !== 0) {
|
|
2825
|
+
const state = queue.shift();
|
|
2826
|
+
const key = state.url + "\0" + (state.watched ? "1" : "0");
|
|
2827
|
+
if (visited.has(key)) continue;
|
|
2828
|
+
visited.add(key);
|
|
2829
|
+
if (state.watched) watched.add(state.url);
|
|
2830
|
+
for (const edge of adjacency.get(state.url) || []) {
|
|
2831
|
+
const childLocation = graphNodes.get(edge.child);
|
|
2832
|
+
const childWatched = edge.packageBoundary
|
|
2833
|
+
? false
|
|
2834
|
+
: childLocation !== undefined && !pathHasNodeModules(childLocation)
|
|
2835
|
+
? true
|
|
2836
|
+
: state.watched;
|
|
2837
|
+
queue.push({ url: edge.child, watched: childWatched });
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
return watched;
|
|
2841
|
+
}
|
|
2842
|
+
|
|
1504
2843
|
function hasConfigKey(value) {
|
|
1505
2844
|
for (const key of CONFIG_KEYS) {
|
|
1506
2845
|
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
@@ -1534,44 +2873,70 @@ function toSerializableConfig(value) {
|
|
|
1534
2873
|
return out;
|
|
1535
2874
|
}
|
|
1536
2875
|
`, serializableConfigKeysLiteral())
|
|
1537
|
-
node := os.Getenv("TTSC_NODE_BINARY")
|
|
1538
|
-
if node == "" {
|
|
1539
|
-
node = "node"
|
|
1540
|
-
}
|
|
1541
|
-
ctx, cancel := context.WithTimeout(context.Background(), configLoaderTimeout)
|
|
1542
|
-
defer cancel()
|
|
1543
|
-
cmd := exec.CommandContext(ctx, node, "-e", script, location)
|
|
1544
|
-
return runConfigLoaderCommand(ctx, cmd, location, "config file")
|
|
1545
2876
|
}
|
|
1546
2877
|
|
|
1547
2878
|
// loadTypeScriptConfigFile evaluates a .ts/.cts/.mts config file by writing
|
|
1548
2879
|
// an ephemeral loader script and tsconfig into a temp directory, symlinking the
|
|
1549
2880
|
// nearest node_modules, then running `ttsx` with a configLoaderTimeout deadline.
|
|
1550
2881
|
// The loader script imports the config file, resolves it through the same
|
|
1551
|
-
// normalization chain used by loadScriptConfigFile, and writes
|
|
2882
|
+
// normalization chain used by loadScriptConfigFile, and writes a private JSON
|
|
2883
|
+
// result file so user stdout cannot corrupt the protocol.
|
|
1552
2884
|
func loadTypeScriptConfigFile(location string) (any, error) {
|
|
2885
|
+
evaluated, err := loadTypeScriptConfigEvaluation(location)
|
|
2886
|
+
return evaluated.value, err
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
func loadTypeScriptConfigEvaluation(location string) (evaluatedConfigFile, error) {
|
|
2890
|
+
return loadTypeScriptConfigEvaluationWithin(location, filepath.Dir(location))
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
func loadTypeScriptConfigEvaluationWithin(
|
|
2894
|
+
location string,
|
|
2895
|
+
resolutionRoot string,
|
|
2896
|
+
) (evaluatedConfigFile, error) {
|
|
1553
2897
|
tempDir, err := os.MkdirTemp(loaderTempBase(location, os.TempDir()), "ttsc-lint-config-")
|
|
1554
2898
|
if err != nil {
|
|
1555
|
-
return
|
|
2899
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: create config loader tempdir: %w", err)
|
|
1556
2900
|
}
|
|
1557
2901
|
tempDir = realpathIfPossible(tempDir)
|
|
1558
2902
|
defer os.RemoveAll(tempDir)
|
|
1559
2903
|
|
|
1560
2904
|
if err := linkNearestNodeModules(tempDir, filepath.Dir(location)); err != nil {
|
|
1561
|
-
return
|
|
2905
|
+
return evaluatedConfigFile{}, err
|
|
1562
2906
|
}
|
|
1563
2907
|
|
|
1564
2908
|
loader := filepath.Join(tempDir, "loader.mts")
|
|
2909
|
+
outputPath := filepath.Join(tempDir, "result.json")
|
|
1565
2910
|
tsconfig := filepath.Join(tempDir, "tsconfig.json")
|
|
1566
2911
|
importLiteral, err := json.Marshal(fileURL(location))
|
|
1567
2912
|
if err != nil {
|
|
1568
|
-
return
|
|
2913
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: encode config import %s: %w", location, err)
|
|
2914
|
+
}
|
|
2915
|
+
outputLiteral, err := json.Marshal(outputPath)
|
|
2916
|
+
if err != nil {
|
|
2917
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: encode config result path %s: %w", outputPath, err)
|
|
2918
|
+
}
|
|
2919
|
+
resolutionRootLiteral, err := json.Marshal(filepath.Clean(resolutionRoot))
|
|
2920
|
+
if err != nil {
|
|
2921
|
+
return evaluatedConfigFile{}, fmt.Errorf(
|
|
2922
|
+
"@ttsc/lint: encode config resolution root %s: %w",
|
|
2923
|
+
resolutionRoot,
|
|
2924
|
+
err,
|
|
2925
|
+
)
|
|
1569
2926
|
}
|
|
1570
|
-
if err := os.WriteFile(
|
|
1571
|
-
|
|
2927
|
+
if err := os.WriteFile(
|
|
2928
|
+
loader,
|
|
2929
|
+
[]byte(typeScriptConfigLoaderSource(
|
|
2930
|
+
string(importLiteral),
|
|
2931
|
+
string(outputLiteral),
|
|
2932
|
+
string(resolutionRootLiteral),
|
|
2933
|
+
)),
|
|
2934
|
+
0o644,
|
|
2935
|
+
); err != nil {
|
|
2936
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: write config loader: %w", err)
|
|
1572
2937
|
}
|
|
1573
2938
|
if err := os.WriteFile(tsconfig, []byte(typeScriptConfigLoaderTsconfig(loader, location, tempDir)), 0o644); err != nil {
|
|
1574
|
-
return
|
|
2939
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: write config loader tsconfig: %w", err)
|
|
1575
2940
|
}
|
|
1576
2941
|
|
|
1577
2942
|
args := []string{
|
|
@@ -1597,7 +2962,7 @@ func loadTypeScriptConfigFile(location string) (any, error) {
|
|
|
1597
2962
|
defer cancel()
|
|
1598
2963
|
cmd := ttsxCommandContext(ctx, args...)
|
|
1599
2964
|
cmd.Env = nodeConfigLoaderEnv(location)
|
|
1600
|
-
return runConfigLoaderCommand(ctx, cmd, location, "TypeScript config file")
|
|
2965
|
+
return runConfigLoaderCommand(ctx, cmd, location, "TypeScript config file", outputPath)
|
|
1601
2966
|
}
|
|
1602
2967
|
|
|
1603
2968
|
// isConfigObject reports whether `value` is a top-level config object. A lint
|
|
@@ -1656,26 +3021,151 @@ func uncFileURL(pathname string) string {
|
|
|
1656
3021
|
// `importLiteral` is a JSON-encoded file URL (produced by json.Marshal). It is
|
|
1657
3022
|
// assigned to a variable before `import(configUrl)` so tsgo does not try to
|
|
1658
3023
|
// statically resolve the file URL during the loader build.
|
|
1659
|
-
func typeScriptConfigLoaderSource(
|
|
1660
|
-
|
|
3024
|
+
func typeScriptConfigLoaderSource(
|
|
3025
|
+
importLiteral string,
|
|
3026
|
+
outputLiteral string,
|
|
3027
|
+
resolutionRootLiteral string,
|
|
3028
|
+
) string {
|
|
3029
|
+
return fmt.Sprintf(`// @ts-ignore -- internal loader must not require user-installed Node typings.
|
|
3030
|
+
import * as fs from "node:fs";
|
|
3031
|
+
// @ts-ignore -- internal loader must not require user-installed Node typings.
|
|
3032
|
+
import { Buffer } from "node:buffer";
|
|
3033
|
+
// @ts-ignore -- internal loader must not require user-installed Node typings.
|
|
3034
|
+
import { createHash } from "node:crypto";
|
|
3035
|
+
// @ts-ignore -- internal loader must not require user-installed Node typings.
|
|
3036
|
+
import { registerHooks } from "node:module";
|
|
3037
|
+
// @ts-ignore -- internal loader must not require user-installed Node typings.
|
|
3038
|
+
import * as path from "node:path";
|
|
3039
|
+
// @ts-ignore -- internal loader must not require user-installed Node typings.
|
|
3040
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3041
|
+
|
|
3042
|
+
const configUrl = %s;
|
|
3043
|
+
const outputPath = %s;
|
|
3044
|
+
const resolutionRoot = path.resolve(%s);
|
|
1661
3045
|
const CONFIG_KEYS = new Set<string>([%s]);
|
|
1662
|
-
const
|
|
3046
|
+
const dependencies = new Map<string, {
|
|
3047
|
+
digest: string;
|
|
3048
|
+
kind: "directory" | "file" | "optional-file";
|
|
3049
|
+
path: string;
|
|
3050
|
+
owners: Set<string>;
|
|
3051
|
+
}>();
|
|
3052
|
+
const graphNodes = new Map<string, string>();
|
|
3053
|
+
const graphEdges: Array<{
|
|
3054
|
+
child: string;
|
|
3055
|
+
packageBoundary: boolean;
|
|
3056
|
+
parent: string;
|
|
3057
|
+
}> = [];
|
|
3058
|
+
const configLocation = fileURLToPath(configUrl);
|
|
3059
|
+
// Every spelling of this config the module system might key an edge under.
|
|
3060
|
+
//
|
|
3061
|
+
// Which one it uses is not knowable from here, and guessing has failed in both
|
|
3062
|
+
// directions. A path handed in by another producer can be escaped by a rule
|
|
3063
|
+
// Node does not share. Node respells a resolved file module through its real
|
|
3064
|
+
// path unless "--preserve-symlinks" is set, so a config reached through a
|
|
3065
|
+
// symlinked directory is keyed by its target. And a Windows 8.3 short name is
|
|
3066
|
+
// not a symlink: fs.realpathSync expands it, the module resolver does not, so
|
|
3067
|
+
// asking the volume there produces a spelling no edge carries.
|
|
3068
|
+
//
|
|
3069
|
+
// A seed that names a URL no edge was keyed under sits on a node with no
|
|
3070
|
+
// outgoing edges, the walk ends immediately, and every dependency recorded
|
|
3071
|
+
// after the first import is demoted from watch to cache. That failure is
|
|
3072
|
+
// silent: the build still succeeds and simply stops reacting. Seeding every
|
|
3073
|
+
// spelling costs one extra queue entry and cannot be wrong.
|
|
3074
|
+
const configUrlSpellings = [
|
|
3075
|
+
...new Set([
|
|
3076
|
+
configUrl,
|
|
3077
|
+
pathToFileURL(configLocation).href,
|
|
3078
|
+
pathToFileURL(realConfigLocation()).href,
|
|
3079
|
+
]),
|
|
3080
|
+
];
|
|
3081
|
+
for (const spelling of configUrlSpellings) {
|
|
3082
|
+
graphNodes.set(spelling, configLocation);
|
|
3083
|
+
}
|
|
3084
|
+
recordDependency(
|
|
3085
|
+
"file",
|
|
3086
|
+
configLocation,
|
|
3087
|
+
createHash("sha256").update(fs.readFileSync(configLocation)).digest("hex"),
|
|
3088
|
+
configUrlSpellings,
|
|
3089
|
+
);
|
|
3090
|
+
recordPackageManifests(configLocation, configUrlSpellings);
|
|
1663
3091
|
|
|
1664
3092
|
declare const process: {
|
|
3093
|
+
env: Record<string, string | undefined>;
|
|
3094
|
+
platform: string;
|
|
1665
3095
|
stdout: { write(value: string): void };
|
|
1666
3096
|
stderr: { write(value: string): void };
|
|
1667
3097
|
exit(code?: number): never;
|
|
1668
3098
|
};
|
|
1669
3099
|
|
|
3100
|
+
const hooks = registerHooks({
|
|
3101
|
+
resolve(specifier, context, nextResolve) {
|
|
3102
|
+
const resolved = nextResolve(specifier, context);
|
|
3103
|
+
if (typeof resolved.url !== "string" || !resolved.url.startsWith("file:")) {
|
|
3104
|
+
return resolved;
|
|
3105
|
+
}
|
|
3106
|
+
const url = new URL(resolved.url).href;
|
|
3107
|
+
const parent = context.parentURL && new URL(context.parentURL).href;
|
|
3108
|
+
const location = fileURLToPath(url);
|
|
3109
|
+
// The entry is recognized by what was asked for, not only by what came
|
|
3110
|
+
// back. A module URL is assigned by whoever loaded it: a compiling loader
|
|
3111
|
+
// can serve the config from its emitted output, and a platform can hand
|
|
3112
|
+
// back a different spelling of the same file. Either way the URL bears no
|
|
3113
|
+
// resemblance to the one this process was given, so the config's own
|
|
3114
|
+
// imports would be rejected here — their parent is a URL no node was
|
|
3115
|
+
// recorded under — and the graph would collapse to the records made before
|
|
3116
|
+
// the first import. The request itself is unambiguous, so it decides.
|
|
3117
|
+
const entry =
|
|
3118
|
+
specifier === configUrl ||
|
|
3119
|
+
url === new URL(configUrl).href ||
|
|
3120
|
+
samePhysicalPath(location, configLocation);
|
|
3121
|
+
if (!entry && (parent === undefined || !graphNodes.has(parent))) {
|
|
3122
|
+
return resolved;
|
|
3123
|
+
}
|
|
3124
|
+
graphNodes.set(url, location);
|
|
3125
|
+
if (parent !== undefined) {
|
|
3126
|
+
graphEdges.push({
|
|
3127
|
+
child: url,
|
|
3128
|
+
packageBoundary:
|
|
3129
|
+
pathHasNodeModules(location) && !isLocalModuleSpecifier(specifier),
|
|
3130
|
+
parent,
|
|
3131
|
+
});
|
|
3132
|
+
recordResolutionTopology(
|
|
3133
|
+
specifier,
|
|
3134
|
+
parent,
|
|
3135
|
+
url,
|
|
3136
|
+
location,
|
|
3137
|
+
context.conditions,
|
|
3138
|
+
);
|
|
3139
|
+
}
|
|
3140
|
+
try {
|
|
3141
|
+
recordDependency(
|
|
3142
|
+
"file",
|
|
3143
|
+
location,
|
|
3144
|
+
createHash("sha256").update(fs.readFileSync(location)).digest("hex"),
|
|
3145
|
+
[url],
|
|
3146
|
+
);
|
|
3147
|
+
} catch {
|
|
3148
|
+
recordDependency("file", location, "", [url]);
|
|
3149
|
+
}
|
|
3150
|
+
return resolved;
|
|
3151
|
+
},
|
|
3152
|
+
});
|
|
3153
|
+
|
|
1670
3154
|
try {
|
|
3155
|
+
const importedConfig = await import(configUrl);
|
|
1671
3156
|
const value = await resolveConfig(importedConfig, true);
|
|
1672
3157
|
if (!isObject(value) || Array.isArray(value)) {
|
|
1673
3158
|
throw new Error("config file must export an ITtscLintConfig object");
|
|
1674
3159
|
}
|
|
1675
|
-
|
|
3160
|
+
fs.writeFileSync(outputPath, JSON.stringify({
|
|
3161
|
+
dependencies: finalizeDependencies(),
|
|
3162
|
+
value: toSerializableConfig(value),
|
|
3163
|
+
}), "utf8");
|
|
1676
3164
|
} catch (error) {
|
|
1677
3165
|
process.stderr.write(error instanceof Error && error.stack ? error.stack : String(error));
|
|
1678
3166
|
process.exit(1);
|
|
3167
|
+
} finally {
|
|
3168
|
+
hooks.deregister();
|
|
1679
3169
|
}
|
|
1680
3170
|
|
|
1681
3171
|
async function resolveConfig(value: unknown, allowNamedConfig: boolean): Promise<unknown> {
|
|
@@ -1716,6 +3206,823 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
1716
3206
|
return value !== null && typeof value === "object";
|
|
1717
3207
|
}
|
|
1718
3208
|
|
|
3209
|
+
function recordDependency(
|
|
3210
|
+
kind: "directory" | "file" | "optional-file",
|
|
3211
|
+
location: string,
|
|
3212
|
+
digest: string,
|
|
3213
|
+
owners: readonly string[],
|
|
3214
|
+
): void {
|
|
3215
|
+
const key = kind + "\0" + location;
|
|
3216
|
+
const previous = dependencies.get(key);
|
|
3217
|
+
const mergedOwners = previous?.owners ?? new Set<string>();
|
|
3218
|
+
for (const owner of owners) mergedOwners.add(owner);
|
|
3219
|
+
dependencies.set(key, {
|
|
3220
|
+
digest: previous !== undefined && previous.digest !== digest ? "" : digest,
|
|
3221
|
+
kind,
|
|
3222
|
+
owners: mergedOwners,
|
|
3223
|
+
path: location,
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
function isLocalModuleSpecifier(specifier: string): boolean {
|
|
3228
|
+
return specifier.startsWith(".") ||
|
|
3229
|
+
specifier.startsWith("/") ||
|
|
3230
|
+
specifier.startsWith("file:") ||
|
|
3231
|
+
/^[A-Za-z]:[\\/]/.test(specifier);
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
function pathHasNodeModules(location: string): boolean {
|
|
3235
|
+
return location.replaceAll("\\", "/").split("/").includes("node_modules");
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
function recordResolutionTopology(
|
|
3239
|
+
specifier: string,
|
|
3240
|
+
parentUrl: string,
|
|
3241
|
+
childUrl: string,
|
|
3242
|
+
childLocation: string,
|
|
3243
|
+
conditions: readonly string[],
|
|
3244
|
+
): void {
|
|
3245
|
+
const owners = [parentUrl, childUrl];
|
|
3246
|
+
const parentLocation = graphNodes.get(parentUrl);
|
|
3247
|
+
if (parentLocation !== undefined && isLocalModuleSpecifier(specifier)) {
|
|
3248
|
+
recordDirectoryDependency(path.dirname(parentLocation), owners);
|
|
3249
|
+
}
|
|
3250
|
+
recordDirectoryDependency(path.dirname(childLocation), owners);
|
|
3251
|
+
recordPackageManifests(childLocation, owners);
|
|
3252
|
+
if (parentLocation !== undefined && !isLocalModuleSpecifier(specifier)) {
|
|
3253
|
+
recordNodeModulesSearchDirectories(
|
|
3254
|
+
parentLocation,
|
|
3255
|
+
specifier,
|
|
3256
|
+
childLocation,
|
|
3257
|
+
owners,
|
|
3258
|
+
conditions,
|
|
3259
|
+
);
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
function recordDirectoryDependency(
|
|
3264
|
+
location: string,
|
|
3265
|
+
owners: readonly string[],
|
|
3266
|
+
): void {
|
|
3267
|
+
try {
|
|
3268
|
+
recordDependency("directory", location, directoryDigest(location), owners);
|
|
3269
|
+
} catch {
|
|
3270
|
+
recordDependency("directory", location, "", owners);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
function directoryDigest(location: string): string {
|
|
3275
|
+
const entries: Buffer[] = [];
|
|
3276
|
+
if (process.platform === "win32") {
|
|
3277
|
+
for (const entry of fs.readdirSync(location, { withFileTypes: true })) {
|
|
3278
|
+
let target = Buffer.alloc(0);
|
|
3279
|
+
if (entry.isSymbolicLink()) {
|
|
3280
|
+
try {
|
|
3281
|
+
target = Buffer.from(
|
|
3282
|
+
fs.readlinkSync(path.join(location, entry.name)),
|
|
3283
|
+
"utf8",
|
|
3284
|
+
);
|
|
3285
|
+
} catch {
|
|
3286
|
+
target = Buffer.from("<unreadable>");
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
entries.push(directoryDigestRecord(Buffer.from(entry.name), entry, target));
|
|
3290
|
+
}
|
|
3291
|
+
} else {
|
|
3292
|
+
for (const entry of fs.readdirSync(location, {
|
|
3293
|
+
encoding: "buffer",
|
|
3294
|
+
withFileTypes: true,
|
|
3295
|
+
})) {
|
|
3296
|
+
let target = Buffer.alloc(0);
|
|
3297
|
+
if (entry.isSymbolicLink()) {
|
|
3298
|
+
try {
|
|
3299
|
+
target = fs.readlinkSync(
|
|
3300
|
+
Buffer.concat([
|
|
3301
|
+
Buffer.from(location),
|
|
3302
|
+
Buffer.from(path.sep),
|
|
3303
|
+
entry.name,
|
|
3304
|
+
]),
|
|
3305
|
+
{ encoding: "buffer" },
|
|
3306
|
+
);
|
|
3307
|
+
} catch {
|
|
3308
|
+
target = Buffer.from("<unreadable>");
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
entries.push(directoryDigestRecord(entry.name, entry, target));
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
entries.sort(Buffer.compare);
|
|
3315
|
+
const serialized = Buffer.concat(
|
|
3316
|
+
entries.flatMap((entry, index) =>
|
|
3317
|
+
index === 0 ? [entry] : [Buffer.from([0]), entry],
|
|
3318
|
+
),
|
|
3319
|
+
);
|
|
3320
|
+
return createHash("sha256").update(serialized).digest("hex");
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
function directoryDigestRecord(
|
|
3324
|
+
name: Buffer,
|
|
3325
|
+
entry: {
|
|
3326
|
+
isDirectory(): boolean;
|
|
3327
|
+
isFile(): boolean;
|
|
3328
|
+
isSymbolicLink(): boolean;
|
|
3329
|
+
},
|
|
3330
|
+
target: Buffer,
|
|
3331
|
+
): Buffer {
|
|
3332
|
+
const kind = entry.isDirectory()
|
|
3333
|
+
? "directory"
|
|
3334
|
+
: entry.isFile()
|
|
3335
|
+
? "file"
|
|
3336
|
+
: entry.isSymbolicLink()
|
|
3337
|
+
? "symlink"
|
|
3338
|
+
: "other";
|
|
3339
|
+
return Buffer.concat([name, Buffer.from("\0" + kind + "\0"), target]);
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
function optionalFileDigest(location: string): string {
|
|
3343
|
+
try {
|
|
3344
|
+
if (fs.statSync(location).isFile()) {
|
|
3345
|
+
return createHash("sha256")
|
|
3346
|
+
.update(Buffer.concat([Buffer.from("file\0"), fs.readFileSync(location)]))
|
|
3347
|
+
.digest("hex");
|
|
3348
|
+
}
|
|
3349
|
+
} catch {
|
|
3350
|
+
}
|
|
3351
|
+
return createHash("sha256").update("missing\0").digest("hex");
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
function recordOptionalFileDependency(
|
|
3355
|
+
location: string,
|
|
3356
|
+
owners: readonly string[],
|
|
3357
|
+
): boolean {
|
|
3358
|
+
try {
|
|
3359
|
+
if (fs.statSync(location).isFile()) {
|
|
3360
|
+
recordDependency(
|
|
3361
|
+
"file",
|
|
3362
|
+
location,
|
|
3363
|
+
createHash("sha256").update(fs.readFileSync(location)).digest("hex"),
|
|
3364
|
+
owners,
|
|
3365
|
+
);
|
|
3366
|
+
return true;
|
|
3367
|
+
}
|
|
3368
|
+
} catch {
|
|
3369
|
+
}
|
|
3370
|
+
recordDependency("optional-file", location, optionalFileDigest(location), owners);
|
|
3371
|
+
return false;
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
function recordPackageManifests(
|
|
3375
|
+
location: string,
|
|
3376
|
+
owners: readonly string[],
|
|
3377
|
+
): void {
|
|
3378
|
+
let current = path.dirname(location);
|
|
3379
|
+
while (true) {
|
|
3380
|
+
const manifest = path.join(current, "package.json");
|
|
3381
|
+
if (recordOptionalFileDependency(manifest, owners)) return;
|
|
3382
|
+
const parent = path.dirname(current);
|
|
3383
|
+
if (parent === current || path.basename(current) === "node_modules") return;
|
|
3384
|
+
current = parent;
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
function recordNodeModulesSearchDirectories(
|
|
3389
|
+
parentLocation: string,
|
|
3390
|
+
specifier: string,
|
|
3391
|
+
childLocation: string,
|
|
3392
|
+
owners: readonly string[],
|
|
3393
|
+
conditions: readonly string[],
|
|
3394
|
+
): void {
|
|
3395
|
+
const packageName = modulePackageName(specifier);
|
|
3396
|
+
const scope =
|
|
3397
|
+
specifier.startsWith("@") && specifier.includes("/")
|
|
3398
|
+
? specifier.slice(0, specifier.indexOf("/"))
|
|
3399
|
+
: undefined;
|
|
3400
|
+
let current = path.dirname(parentLocation);
|
|
3401
|
+
while (true) {
|
|
3402
|
+
recordDirectoryDependency(current, owners);
|
|
3403
|
+
const modules = path.join(current, "node_modules");
|
|
3404
|
+
try {
|
|
3405
|
+
if (fs.statSync(modules).isDirectory()) {
|
|
3406
|
+
recordDirectoryDependency(modules, owners);
|
|
3407
|
+
if (scope !== undefined) {
|
|
3408
|
+
const scoped = path.join(modules, scope);
|
|
3409
|
+
try {
|
|
3410
|
+
if (fs.statSync(scoped).isDirectory()) {
|
|
3411
|
+
recordDirectoryDependency(scoped, owners);
|
|
3412
|
+
}
|
|
3413
|
+
} catch {
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
if (packageName !== undefined) {
|
|
3417
|
+
const selected = recordPackageCandidateTopology(
|
|
3418
|
+
modules,
|
|
3419
|
+
packageName,
|
|
3420
|
+
specifier,
|
|
3421
|
+
childLocation,
|
|
3422
|
+
owners,
|
|
3423
|
+
conditions,
|
|
3424
|
+
);
|
|
3425
|
+
if (
|
|
3426
|
+
selected ||
|
|
3427
|
+
resolvedPackageContains(modules, packageName, childLocation)
|
|
3428
|
+
) {
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
} catch {
|
|
3434
|
+
}
|
|
3435
|
+
if (
|
|
3436
|
+
packageName === undefined &&
|
|
3437
|
+
samePhysicalPath(current, resolutionRoot)
|
|
3438
|
+
) {
|
|
3439
|
+
return;
|
|
3440
|
+
}
|
|
3441
|
+
const parent = path.dirname(current);
|
|
3442
|
+
if (parent === current) return;
|
|
3443
|
+
current = parent;
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
function recordPackageCandidateTopology(
|
|
3448
|
+
modules: string,
|
|
3449
|
+
packageName: string,
|
|
3450
|
+
specifier: string,
|
|
3451
|
+
childLocation: string,
|
|
3452
|
+
owners: readonly string[],
|
|
3453
|
+
conditions: readonly string[],
|
|
3454
|
+
): boolean {
|
|
3455
|
+
const packageRoot = path.join(modules, packageName);
|
|
3456
|
+
try {
|
|
3457
|
+
if (!fs.statSync(packageRoot).isDirectory()) return false;
|
|
3458
|
+
} catch {
|
|
3459
|
+
return false;
|
|
3460
|
+
}
|
|
3461
|
+
const subpath = specifier
|
|
3462
|
+
.slice(packageName.length)
|
|
3463
|
+
.replace(/^[/\\]+/, "");
|
|
3464
|
+
const rootTopology = recordPackageRootTopology(
|
|
3465
|
+
packageRoot,
|
|
3466
|
+
owners,
|
|
3467
|
+
subpath === "",
|
|
3468
|
+
subpath === "" ? "." : "./" + subpath.replaceAll("\\", "/"),
|
|
3469
|
+
childLocation,
|
|
3470
|
+
conditions,
|
|
3471
|
+
);
|
|
3472
|
+
if (subpath !== "" && !rootTopology.hasExports) {
|
|
3473
|
+
return (
|
|
3474
|
+
recordPackageSubpathTopology(
|
|
3475
|
+
packageRoot,
|
|
3476
|
+
subpath,
|
|
3477
|
+
childLocation,
|
|
3478
|
+
owners,
|
|
3479
|
+
) || rootTopology.selected
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
return rootTopology.selected;
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
function recordPackageRootTopology(
|
|
3486
|
+
packageRoot: string,
|
|
3487
|
+
owners: readonly string[],
|
|
3488
|
+
useMain: boolean,
|
|
3489
|
+
packageSubpath: string,
|
|
3490
|
+
childLocation: string,
|
|
3491
|
+
conditions: readonly string[],
|
|
3492
|
+
): { hasExports: boolean; selected: boolean } {
|
|
3493
|
+
const normalizedRoot = path.resolve(packageRoot);
|
|
3494
|
+
const manifest = path.join(normalizedRoot, "package.json");
|
|
3495
|
+
const legacySelected = (): boolean =>
|
|
3496
|
+
useMain &&
|
|
3497
|
+
packagePathCandidateMatchesChild(normalizedRoot, childLocation, true);
|
|
3498
|
+
if (!recordOptionalFileDependency(manifest, owners)) {
|
|
3499
|
+
const selected = legacySelected();
|
|
3500
|
+
if (!selected) {
|
|
3501
|
+
recordPackageIndexCandidates(normalizedRoot, useMain, owners);
|
|
3502
|
+
}
|
|
3503
|
+
return { hasExports: false, selected };
|
|
3504
|
+
}
|
|
3505
|
+
try {
|
|
3506
|
+
const value = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
3507
|
+
if (value !== null && typeof value === "object") {
|
|
3508
|
+
const metadata = value as Record<string, unknown>;
|
|
3509
|
+
const hasExports =
|
|
3510
|
+
metadata.exports !== undefined && metadata.exports !== null;
|
|
3511
|
+
if (hasExports) {
|
|
3512
|
+
const target = selectPackageExportsTarget(
|
|
3513
|
+
metadata.exports,
|
|
3514
|
+
packageSubpath,
|
|
3515
|
+
new Set(conditions),
|
|
3516
|
+
);
|
|
3517
|
+
const candidate =
|
|
3518
|
+
typeof target === "string"
|
|
3519
|
+
? packageExportsTarget(normalizedRoot, target)
|
|
3520
|
+
: undefined;
|
|
3521
|
+
const selected =
|
|
3522
|
+
candidate !== undefined &&
|
|
3523
|
+
packagePathCandidateMatchesChild(
|
|
3524
|
+
candidate,
|
|
3525
|
+
childLocation,
|
|
3526
|
+
false,
|
|
3527
|
+
);
|
|
3528
|
+
if (selected) {
|
|
3529
|
+
recordPackagePathCandidate(candidate, owners);
|
|
3530
|
+
} else if (candidate !== undefined) {
|
|
3531
|
+
// A nearer package the search skipped starts winning the moment its
|
|
3532
|
+
// own active target appears, and neither the parent node_modules
|
|
3533
|
+
// listing nor the manifest changes when only that file is created.
|
|
3534
|
+
recordOptionalFileDependency(candidate, owners);
|
|
3535
|
+
}
|
|
3536
|
+
return { hasExports: true, selected };
|
|
3537
|
+
}
|
|
3538
|
+
let selected = legacySelected();
|
|
3539
|
+
if (useMain && typeof metadata.main === "string") {
|
|
3540
|
+
const main = path.resolve(normalizedRoot, metadata.main);
|
|
3541
|
+
recordPackagePathCandidate(main, owners);
|
|
3542
|
+
selected =
|
|
3543
|
+
packagePathCandidateMatchesChild(main, childLocation, true) ||
|
|
3544
|
+
selected;
|
|
3545
|
+
}
|
|
3546
|
+
if (!selected) {
|
|
3547
|
+
recordPackageIndexCandidates(normalizedRoot, useMain, owners);
|
|
3548
|
+
}
|
|
3549
|
+
return { hasExports: false, selected };
|
|
3550
|
+
}
|
|
3551
|
+
} catch {
|
|
3552
|
+
}
|
|
3553
|
+
const rootSelected = legacySelected();
|
|
3554
|
+
if (!rootSelected) {
|
|
3555
|
+
recordPackageIndexCandidates(normalizedRoot, useMain, owners);
|
|
3556
|
+
}
|
|
3557
|
+
return { hasExports: false, selected: rootSelected };
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
// recordPackageIndexCandidates pins the LOAD_INDEX fallbacks of a package root
|
|
3561
|
+
// this resolution walked past without selecting. An empty package directory, or
|
|
3562
|
+
// one whose manifest declares no usable entry, becomes resolvable as soon as one
|
|
3563
|
+
// of these files exists, and that creation changes neither the parent directory
|
|
3564
|
+
// listing nor the manifest digest already recorded for the candidate.
|
|
3565
|
+
function recordPackageIndexCandidates(
|
|
3566
|
+
packageRoot: string,
|
|
3567
|
+
useMain: boolean,
|
|
3568
|
+
owners: readonly string[],
|
|
3569
|
+
): void {
|
|
3570
|
+
if (!useMain) return;
|
|
3571
|
+
for (const name of ["index.js", "index.json", "index.node"]) {
|
|
3572
|
+
recordOptionalFileDependency(path.join(packageRoot, name), owners);
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
function selectPackageExportsTarget(
|
|
3577
|
+
exportsValue: unknown,
|
|
3578
|
+
packageSubpath: string,
|
|
3579
|
+
conditions: ReadonlySet<string>,
|
|
3580
|
+
): string | null | undefined {
|
|
3581
|
+
let mappings: unknown = exportsValue;
|
|
3582
|
+
if (
|
|
3583
|
+
typeof mappings === "string" ||
|
|
3584
|
+
Array.isArray(mappings) ||
|
|
3585
|
+
(isObject(mappings) &&
|
|
3586
|
+
Object.keys(mappings).every((key) => !key.startsWith(".")))
|
|
3587
|
+
) {
|
|
3588
|
+
if (packageSubpath !== ".") return undefined;
|
|
3589
|
+
return selectPackageTarget(mappings, "", false, conditions);
|
|
3590
|
+
}
|
|
3591
|
+
if (!isObject(mappings)) return undefined;
|
|
3592
|
+
if (
|
|
3593
|
+
Object.prototype.hasOwnProperty.call(mappings, packageSubpath) &&
|
|
3594
|
+
!packageSubpath.includes("*") &&
|
|
3595
|
+
!packageSubpath.endsWith("/")
|
|
3596
|
+
) {
|
|
3597
|
+
return selectPackageTarget(
|
|
3598
|
+
mappings[packageSubpath],
|
|
3599
|
+
"",
|
|
3600
|
+
false,
|
|
3601
|
+
conditions,
|
|
3602
|
+
);
|
|
3603
|
+
}
|
|
3604
|
+
let bestMatch = "";
|
|
3605
|
+
let bestSubpath = "";
|
|
3606
|
+
for (const key of Object.keys(mappings)) {
|
|
3607
|
+
const wildcard = key.indexOf("*");
|
|
3608
|
+
if (
|
|
3609
|
+
wildcard === -1 ||
|
|
3610
|
+
key.lastIndexOf("*") !== wildcard ||
|
|
3611
|
+
!packageSubpath.startsWith(key.slice(0, wildcard))
|
|
3612
|
+
) {
|
|
3613
|
+
continue;
|
|
3614
|
+
}
|
|
3615
|
+
const trailer = key.slice(wildcard + 1);
|
|
3616
|
+
if (
|
|
3617
|
+
packageSubpath.length < key.length ||
|
|
3618
|
+
!packageSubpath.endsWith(trailer) ||
|
|
3619
|
+
packagePatternKeyCompare(bestMatch, key) !== 1
|
|
3620
|
+
) {
|
|
3621
|
+
continue;
|
|
3622
|
+
}
|
|
3623
|
+
bestMatch = key;
|
|
3624
|
+
bestSubpath = packageSubpath.slice(
|
|
3625
|
+
wildcard,
|
|
3626
|
+
packageSubpath.length - trailer.length,
|
|
3627
|
+
);
|
|
3628
|
+
}
|
|
3629
|
+
return bestMatch === ""
|
|
3630
|
+
? undefined
|
|
3631
|
+
: selectPackageTarget(
|
|
3632
|
+
mappings[bestMatch],
|
|
3633
|
+
bestSubpath,
|
|
3634
|
+
true,
|
|
3635
|
+
conditions,
|
|
3636
|
+
);
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
function selectPackageTarget(
|
|
3640
|
+
target: unknown,
|
|
3641
|
+
subpath: string,
|
|
3642
|
+
pattern: boolean,
|
|
3643
|
+
conditions: ReadonlySet<string>,
|
|
3644
|
+
): string | null | undefined {
|
|
3645
|
+
if (typeof target === "string") {
|
|
3646
|
+
const selected = pattern ? target.replaceAll("*", subpath) : target;
|
|
3647
|
+
return validPackageExportsTarget(selected) ? selected : undefined;
|
|
3648
|
+
}
|
|
3649
|
+
if (Array.isArray(target)) {
|
|
3650
|
+
for (const item of target) {
|
|
3651
|
+
const selected = selectPackageTarget(
|
|
3652
|
+
item,
|
|
3653
|
+
subpath,
|
|
3654
|
+
pattern,
|
|
3655
|
+
conditions,
|
|
3656
|
+
);
|
|
3657
|
+
if (selected !== undefined && selected !== null) return selected;
|
|
3658
|
+
}
|
|
3659
|
+
return null;
|
|
3660
|
+
}
|
|
3661
|
+
if (isObject(target)) {
|
|
3662
|
+
for (const [condition, value] of Object.entries(target)) {
|
|
3663
|
+
if (condition !== "default" && !conditions.has(condition)) continue;
|
|
3664
|
+
const selected = selectPackageTarget(
|
|
3665
|
+
value,
|
|
3666
|
+
subpath,
|
|
3667
|
+
pattern,
|
|
3668
|
+
conditions,
|
|
3669
|
+
);
|
|
3670
|
+
if (selected !== undefined) return selected;
|
|
3671
|
+
}
|
|
3672
|
+
return undefined;
|
|
3673
|
+
}
|
|
3674
|
+
return target === null ? null : undefined;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
function packagePatternKeyCompare(left: string, right: string): number {
|
|
3678
|
+
const leftWildcard = left.indexOf("*");
|
|
3679
|
+
const rightWildcard = right.indexOf("*");
|
|
3680
|
+
const leftBase =
|
|
3681
|
+
leftWildcard === -1 ? left.length : leftWildcard + 1;
|
|
3682
|
+
const rightBase =
|
|
3683
|
+
rightWildcard === -1 ? right.length : rightWildcard + 1;
|
|
3684
|
+
if (leftBase > rightBase) return -1;
|
|
3685
|
+
if (rightBase > leftBase) return 1;
|
|
3686
|
+
if (leftWildcard === -1) return 1;
|
|
3687
|
+
if (rightWildcard === -1) return -1;
|
|
3688
|
+
if (left.length > right.length) return -1;
|
|
3689
|
+
if (right.length > left.length) return 1;
|
|
3690
|
+
return 0;
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
function packageExportsTarget(
|
|
3694
|
+
packageRoot: string,
|
|
3695
|
+
target: string,
|
|
3696
|
+
): string | undefined {
|
|
3697
|
+
if (!validPackageExportsTarget(target)) return undefined;
|
|
3698
|
+
try {
|
|
3699
|
+
// Node resolves an exports target as a URL against the package manifest,
|
|
3700
|
+
// so percent escapes, query strings, and fragments all take part in the
|
|
3701
|
+
// path it finally loads. Joining the raw target by hand diverges from that
|
|
3702
|
+
// whenever the target is anything but a plain relative path, and a target
|
|
3703
|
+
// Node resolves while this model rejects loses the selected file's
|
|
3704
|
+
// fingerprint, leaving a retargeted symlink cached as fresh.
|
|
3705
|
+
const packageUrl = pathToFileURL(path.join(packageRoot, "package.json"));
|
|
3706
|
+
const resolved = new URL(target, packageUrl);
|
|
3707
|
+
const packagePath = new URL(".", packageUrl).pathname;
|
|
3708
|
+
if (!resolved.pathname.startsWith(packagePath)) return undefined;
|
|
3709
|
+
return fileURLToPath(resolved);
|
|
3710
|
+
} catch {
|
|
3711
|
+
return undefined;
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
function validPackageExportsTarget(target: string): boolean {
|
|
3716
|
+
if (!target.startsWith("./") || /%%2f|%%5c/i.test(target)) return false;
|
|
3717
|
+
const components = target
|
|
3718
|
+
.slice(2)
|
|
3719
|
+
.replaceAll("\\", "/")
|
|
3720
|
+
.split("/");
|
|
3721
|
+
if (
|
|
3722
|
+
components.some(
|
|
3723
|
+
(component) => {
|
|
3724
|
+
try {
|
|
3725
|
+
const decoded = decodeURIComponent(component);
|
|
3726
|
+
return (
|
|
3727
|
+
decoded === "." ||
|
|
3728
|
+
decoded === ".." ||
|
|
3729
|
+
decoded.includes("/") ||
|
|
3730
|
+
decoded.includes("\\") ||
|
|
3731
|
+
decoded.toLowerCase() === "node_modules"
|
|
3732
|
+
);
|
|
3733
|
+
} catch {
|
|
3734
|
+
return true;
|
|
3735
|
+
}
|
|
3736
|
+
},
|
|
3737
|
+
)
|
|
3738
|
+
) {
|
|
3739
|
+
return false;
|
|
3740
|
+
}
|
|
3741
|
+
return true;
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
function packagePathCandidateMatchesChild(
|
|
3745
|
+
candidate: string,
|
|
3746
|
+
childLocation: string,
|
|
3747
|
+
legacy: boolean,
|
|
3748
|
+
): boolean {
|
|
3749
|
+
let child: string;
|
|
3750
|
+
try {
|
|
3751
|
+
child = fs.realpathSync.native(childLocation);
|
|
3752
|
+
} catch {
|
|
3753
|
+
child = path.resolve(childLocation);
|
|
3754
|
+
}
|
|
3755
|
+
const candidates = legacy
|
|
3756
|
+
? [
|
|
3757
|
+
candidate,
|
|
3758
|
+
candidate + ".js",
|
|
3759
|
+
candidate + ".json",
|
|
3760
|
+
candidate + ".node",
|
|
3761
|
+
path.join(candidate, "index.js"),
|
|
3762
|
+
path.join(candidate, "index.json"),
|
|
3763
|
+
path.join(candidate, "index.node"),
|
|
3764
|
+
]
|
|
3765
|
+
: [candidate];
|
|
3766
|
+
return candidates.some((location) => {
|
|
3767
|
+
try {
|
|
3768
|
+
return sameResolutionPath(fs.realpathSync.native(location), child);
|
|
3769
|
+
} catch {
|
|
3770
|
+
return false;
|
|
3771
|
+
}
|
|
3772
|
+
});
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
function recordPackageSubpathTopology(
|
|
3776
|
+
packageRoot: string,
|
|
3777
|
+
subpath: string,
|
|
3778
|
+
childLocation: string,
|
|
3779
|
+
owners: readonly string[],
|
|
3780
|
+
): boolean {
|
|
3781
|
+
const candidate = boundedPackageTarget(packageRoot, subpath);
|
|
3782
|
+
if (candidate === undefined) return false;
|
|
3783
|
+
recordPackagePathCandidate(candidate, owners);
|
|
3784
|
+
let selected = packagePathCandidateMatchesChild(
|
|
3785
|
+
candidate,
|
|
3786
|
+
childLocation,
|
|
3787
|
+
true,
|
|
3788
|
+
);
|
|
3789
|
+
try {
|
|
3790
|
+
if (!fs.statSync(candidate).isDirectory()) return selected;
|
|
3791
|
+
} catch {
|
|
3792
|
+
return selected;
|
|
3793
|
+
}
|
|
3794
|
+
const manifest = path.join(candidate, "package.json");
|
|
3795
|
+
if (!recordOptionalFileDependency(manifest, owners)) return selected;
|
|
3796
|
+
try {
|
|
3797
|
+
const value = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
3798
|
+
if (value !== null && typeof value === "object") {
|
|
3799
|
+
const metadata = value as Record<string, unknown>;
|
|
3800
|
+
if (typeof metadata.main === "string") {
|
|
3801
|
+
const main = path.resolve(candidate, metadata.main);
|
|
3802
|
+
recordPackagePathCandidate(main, owners);
|
|
3803
|
+
selected =
|
|
3804
|
+
packagePathCandidateMatchesChild(main, childLocation, true) ||
|
|
3805
|
+
selected;
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
} catch {
|
|
3809
|
+
}
|
|
3810
|
+
return selected;
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
function boundedPackageTarget(
|
|
3814
|
+
packageRoot: string,
|
|
3815
|
+
target: string,
|
|
3816
|
+
): string | undefined {
|
|
3817
|
+
const candidate = path.resolve(packageRoot, target);
|
|
3818
|
+
const relative = path.relative(packageRoot, candidate);
|
|
3819
|
+
if (
|
|
3820
|
+
relative === ".." ||
|
|
3821
|
+
relative.startsWith(".." + path.sep) ||
|
|
3822
|
+
path.isAbsolute(relative)
|
|
3823
|
+
) {
|
|
3824
|
+
return undefined;
|
|
3825
|
+
}
|
|
3826
|
+
return candidate;
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
function recordPackagePathCandidate(
|
|
3830
|
+
candidate: string,
|
|
3831
|
+
owners: readonly string[],
|
|
3832
|
+
visited: Set<string> = new Set(),
|
|
3833
|
+
depth = 0,
|
|
3834
|
+
): void {
|
|
3835
|
+
const normalized = path.resolve(candidate);
|
|
3836
|
+
// The depth bound owns termination. A platform-wide case fold would merge
|
|
3837
|
+
// paths that differ only by case, which a per-directory case-sensitive
|
|
3838
|
+
// Windows tree keeps distinct, and would truncate a valid symlink chain.
|
|
3839
|
+
if (depth >= 64 || visited.has(normalized)) return;
|
|
3840
|
+
visited.add(normalized);
|
|
3841
|
+
const parsed = path.parse(normalized);
|
|
3842
|
+
const components = normalized
|
|
3843
|
+
.slice(parsed.root.length)
|
|
3844
|
+
.split(path.sep)
|
|
3845
|
+
.filter(Boolean);
|
|
3846
|
+
let current = parsed.root;
|
|
3847
|
+
for (let index = 0; index < components.length; index++) {
|
|
3848
|
+
const component = components[index];
|
|
3849
|
+
const next = path.join(current, component);
|
|
3850
|
+
let entry: ReturnType<typeof fs.lstatSync>;
|
|
3851
|
+
try {
|
|
3852
|
+
entry = fs.lstatSync(next);
|
|
3853
|
+
} catch {
|
|
3854
|
+
recordDirectoryDependency(current, owners);
|
|
3855
|
+
return;
|
|
3856
|
+
}
|
|
3857
|
+
if (entry.isSymbolicLink()) {
|
|
3858
|
+
recordDirectoryDependency(current, owners);
|
|
3859
|
+
try {
|
|
3860
|
+
const target = fs.readlinkSync(next);
|
|
3861
|
+
const remainder = components.slice(index + 1);
|
|
3862
|
+
recordPackagePathCandidate(
|
|
3863
|
+
path.join(path.resolve(current, target), ...remainder),
|
|
3864
|
+
owners,
|
|
3865
|
+
visited,
|
|
3866
|
+
depth + 1,
|
|
3867
|
+
);
|
|
3868
|
+
} catch {
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
let isDirectory = entry.isDirectory();
|
|
3872
|
+
if (entry.isSymbolicLink()) {
|
|
3873
|
+
try {
|
|
3874
|
+
isDirectory = fs.statSync(next).isDirectory();
|
|
3875
|
+
} catch {
|
|
3876
|
+
return;
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
if (index === components.length - 1) {
|
|
3880
|
+
recordDirectoryDependency(isDirectory ? next : current, owners);
|
|
3881
|
+
return;
|
|
3882
|
+
}
|
|
3883
|
+
if (!isDirectory) {
|
|
3884
|
+
recordDirectoryDependency(current, owners);
|
|
3885
|
+
return;
|
|
3886
|
+
}
|
|
3887
|
+
current = next;
|
|
3888
|
+
}
|
|
3889
|
+
recordDirectoryDependency(current, owners);
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
function modulePackageName(specifier: string): string | undefined {
|
|
3893
|
+
if (specifier.startsWith("@")) {
|
|
3894
|
+
const components = specifier.split("/");
|
|
3895
|
+
return components.length >= 2
|
|
3896
|
+
? components[0] + "/" + components[1]
|
|
3897
|
+
: undefined;
|
|
3898
|
+
}
|
|
3899
|
+
const [name] = specifier.split("/");
|
|
3900
|
+
return name && !name.startsWith("#") ? name : undefined;
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
function resolvedPackageContains(
|
|
3904
|
+
modules: string,
|
|
3905
|
+
packageName: string,
|
|
3906
|
+
childLocation: string,
|
|
3907
|
+
): boolean {
|
|
3908
|
+
try {
|
|
3909
|
+
const packageRoot = fs.realpathSync(path.join(modules, packageName));
|
|
3910
|
+
const relative = path.relative(
|
|
3911
|
+
packageRoot,
|
|
3912
|
+
fs.realpathSync(childLocation),
|
|
3913
|
+
);
|
|
3914
|
+
return (
|
|
3915
|
+
relative === "" ||
|
|
3916
|
+
(relative !== ".." &&
|
|
3917
|
+
!relative.startsWith(".." + path.sep) &&
|
|
3918
|
+
!path.isAbsolute(relative))
|
|
3919
|
+
);
|
|
3920
|
+
} catch {
|
|
3921
|
+
return false;
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
function sameResolutionPath(left: string, right: string): boolean {
|
|
3926
|
+
return path.relative(left, right) === "";
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
function samePhysicalPath(left: string, right: string): boolean {
|
|
3930
|
+
try {
|
|
3931
|
+
return sameResolutionPath(realPath(left), realPath(right));
|
|
3932
|
+
} catch {
|
|
3933
|
+
// Fall back to the spellings themselves, folding case the way the platform
|
|
3934
|
+
// does. On the entry gate a false negative is catastrophic — the config
|
|
3935
|
+
// stops being recognized and its whole graph collapses — while a false
|
|
3936
|
+
// positive only over-includes, so the degradation has to lean toward "same
|
|
3937
|
+
// file". A drive-letter or component case difference is the ordinary
|
|
3938
|
+
// Windows situation; a per-directory case-sensitive tree is the rare one.
|
|
3939
|
+
return sameResolutionPath(left, right);
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
/**
|
|
3944
|
+
* The config's real path, or its declared one when the volume will not say.
|
|
3945
|
+
*
|
|
3946
|
+
* A config can disappear between the host reading it and this loader starting,
|
|
3947
|
+
* and a throw here would replace a precise report from the import below with a
|
|
3948
|
+
* crash in bookkeeping. Seeding lexically instead only risks the demotion this
|
|
3949
|
+
* value exists to prevent, on a file that is already gone.
|
|
3950
|
+
*/
|
|
3951
|
+
function realConfigLocation(): string {
|
|
3952
|
+
try {
|
|
3953
|
+
return realPath(configLocation);
|
|
3954
|
+
} catch {
|
|
3955
|
+
return configLocation;
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
function realPath(location: string): string {
|
|
3960
|
+
return fs.realpathSync.native
|
|
3961
|
+
? fs.realpathSync.native(location)
|
|
3962
|
+
: fs.realpathSync(location);
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
function finalizeDependencies(): Array<{
|
|
3966
|
+
digest: string;
|
|
3967
|
+
kind: "directory" | "file" | "optional-file";
|
|
3968
|
+
path: string;
|
|
3969
|
+
scope: "cache" | "watch";
|
|
3970
|
+
}> {
|
|
3971
|
+
const watched = graphWatchReachability();
|
|
3972
|
+
// Opt-in diagnostics for a graph that comes back empty. The only channel this
|
|
3973
|
+
// loader may use is stderr, because the result travels through a private file
|
|
3974
|
+
// that user output must not corrupt; it stays silent unless a caller asks.
|
|
3975
|
+
if (process.env.TTSC_LINT_DEBUG_CONFIG_GRAPH) {
|
|
3976
|
+
process.stderr.write(
|
|
3977
|
+
"@ttsc/lint: config graph " +
|
|
3978
|
+
JSON.stringify({
|
|
3979
|
+
configUrl,
|
|
3980
|
+
seeds: configUrlSpellings,
|
|
3981
|
+
nodes: [...graphNodes.keys()],
|
|
3982
|
+
edges: graphEdges.map((edge) => edge.parent + " -> " + edge.child),
|
|
3983
|
+
watched: [...watched],
|
|
3984
|
+
}) +
|
|
3985
|
+
"\n",
|
|
3986
|
+
);
|
|
3987
|
+
}
|
|
3988
|
+
return [...dependencies.values()].map(({ owners, ...dependency }) => ({
|
|
3989
|
+
...dependency,
|
|
3990
|
+
scope: [...owners].some((owner) => watched.has(owner))
|
|
3991
|
+
? "watch"
|
|
3992
|
+
: "cache",
|
|
3993
|
+
}));
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
function graphWatchReachability(): Set<string> {
|
|
3997
|
+
const adjacency = new Map<string, typeof graphEdges>();
|
|
3998
|
+
for (const edge of graphEdges) {
|
|
3999
|
+
const outgoing = adjacency.get(edge.parent) ?? [];
|
|
4000
|
+
outgoing.push(edge);
|
|
4001
|
+
adjacency.set(edge.parent, outgoing);
|
|
4002
|
+
}
|
|
4003
|
+
const queue: Array<{ url: string; watched: boolean }> =
|
|
4004
|
+
configUrlSpellings.map((url) => ({ url, watched: true }));
|
|
4005
|
+
const visited = new Set<string>();
|
|
4006
|
+
const watched = new Set<string>();
|
|
4007
|
+
while (queue.length !== 0) {
|
|
4008
|
+
const state = queue.shift()!;
|
|
4009
|
+
const key = state.url + "\0" + (state.watched ? "1" : "0");
|
|
4010
|
+
if (visited.has(key)) continue;
|
|
4011
|
+
visited.add(key);
|
|
4012
|
+
if (state.watched) watched.add(state.url);
|
|
4013
|
+
for (const edge of adjacency.get(state.url) ?? []) {
|
|
4014
|
+
const childLocation = graphNodes.get(edge.child);
|
|
4015
|
+
const childWatched = edge.packageBoundary
|
|
4016
|
+
? false
|
|
4017
|
+
: childLocation !== undefined && !pathHasNodeModules(childLocation)
|
|
4018
|
+
? true
|
|
4019
|
+
: state.watched;
|
|
4020
|
+
queue.push({ url: edge.child, watched: childWatched });
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
return watched;
|
|
4024
|
+
}
|
|
4025
|
+
|
|
1719
4026
|
function hasOwn(value: Record<string, unknown>, key: string): boolean {
|
|
1720
4027
|
return Object.prototype.hasOwnProperty.call(value, key);
|
|
1721
4028
|
}
|
|
@@ -1759,7 +4066,12 @@ function toSerializableConfig(value: Record<string, unknown>): Record<string, un
|
|
|
1759
4066
|
}
|
|
1760
4067
|
return out;
|
|
1761
4068
|
}
|
|
1762
|
-
`,
|
|
4069
|
+
`,
|
|
4070
|
+
importLiteral,
|
|
4071
|
+
outputLiteral,
|
|
4072
|
+
resolutionRootLiteral,
|
|
4073
|
+
serializableConfigKeysLiteral(),
|
|
4074
|
+
)
|
|
1763
4075
|
}
|
|
1764
4076
|
|
|
1765
4077
|
// typeScriptConfigLoaderTsconfig generates the JSON content of the ephemeral
|