@ttsc/lint 0.26.2 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +4 -1
- package/lib/index.js +457 -112
- package/lib/index.js.map +1 -1
- package/linthost/compile.go +19 -3
- package/linthost/config.go +230 -25
- package/package.json +2 -2
- package/src/index.ts +509 -115
package/src/index.ts
CHANGED
|
@@ -34,6 +34,9 @@ type TtscPluginDescriptor = {
|
|
|
34
34
|
threadingArgs?: boolean;
|
|
35
35
|
};
|
|
36
36
|
contributors?: TtscPluginContributor[];
|
|
37
|
+
hostInputHashes?: Record<string, string | null>;
|
|
38
|
+
hostInputRealpaths?: Record<string, string | null>;
|
|
39
|
+
hostInputs?: string[];
|
|
37
40
|
name: string;
|
|
38
41
|
reportsTypeScriptDiagnostics?: boolean;
|
|
39
42
|
source: string;
|
|
@@ -82,20 +85,20 @@ function goSubpackageName(namespace: string): string {
|
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
const LINT_CONFIG_FILENAMES = [
|
|
88
|
+
"lint.config.json",
|
|
89
|
+
"lint.config.js",
|
|
90
|
+
"lint.config.mjs",
|
|
91
|
+
"lint.config.cjs",
|
|
85
92
|
"lint.config.ts",
|
|
86
93
|
"lint.config.mts",
|
|
87
94
|
"lint.config.cts",
|
|
88
|
-
"lint.config.
|
|
89
|
-
"lint.config.
|
|
90
|
-
"lint.config.
|
|
91
|
-
"lint.config.
|
|
95
|
+
"ttsc-lint.config.json",
|
|
96
|
+
"ttsc-lint.config.js",
|
|
97
|
+
"ttsc-lint.config.mjs",
|
|
98
|
+
"ttsc-lint.config.cjs",
|
|
92
99
|
"ttsc-lint.config.ts",
|
|
93
100
|
"ttsc-lint.config.mts",
|
|
94
101
|
"ttsc-lint.config.cts",
|
|
95
|
-
"ttsc-lint.config.mjs",
|
|
96
|
-
"ttsc-lint.config.cjs",
|
|
97
|
-
"ttsc-lint.config.js",
|
|
98
|
-
"ttsc-lint.config.json",
|
|
99
102
|
];
|
|
100
103
|
|
|
101
104
|
/**
|
|
@@ -133,7 +136,7 @@ export default function createTtscPlugin(
|
|
|
133
136
|
context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
134
137
|
): TtscPluginDescriptor {
|
|
135
138
|
rejectUnsupportedEntryKeys(context.plugin);
|
|
136
|
-
const
|
|
139
|
+
const resolvedConfig = resolveConfigFileContributors(context);
|
|
137
140
|
// Build the descriptor without a `contributors` key when none were
|
|
138
141
|
// declared, so consumers (and the existing key-shape regression
|
|
139
142
|
// tests) see the same surface as before this feature shipped.
|
|
@@ -147,6 +150,9 @@ export default function createTtscPlugin(
|
|
|
147
150
|
residentCheck: true,
|
|
148
151
|
threadingArgs: true,
|
|
149
152
|
},
|
|
153
|
+
hostInputHashes: resolvedConfig.hostInputHashes,
|
|
154
|
+
hostInputRealpaths: resolvedConfig.hostInputRealpaths,
|
|
155
|
+
hostInputs: resolvedConfig.hostInputs,
|
|
150
156
|
name: "@ttsc/lint",
|
|
151
157
|
reportsTypeScriptDiagnostics: true,
|
|
152
158
|
// `context.dirname` is this descriptor's own directory in every load mode —
|
|
@@ -155,8 +161,8 @@ export default function createTtscPlugin(
|
|
|
155
161
|
source: path.resolve(context.dirname, "..", "plugin"),
|
|
156
162
|
stage: "check",
|
|
157
163
|
};
|
|
158
|
-
if (contributors.length > 0) {
|
|
159
|
-
descriptor.contributors = contributors;
|
|
164
|
+
if (resolvedConfig.contributors.length > 0) {
|
|
165
|
+
descriptor.contributors = resolvedConfig.contributors;
|
|
160
166
|
}
|
|
161
167
|
return descriptor;
|
|
162
168
|
}
|
|
@@ -210,8 +216,10 @@ type ConfigPluginEntry = { namespace: string; source: string };
|
|
|
210
216
|
|
|
211
217
|
type ConfigDependencyFingerprint = {
|
|
212
218
|
digest: string;
|
|
219
|
+
identityStable: boolean;
|
|
213
220
|
kind: "directory" | "file" | "optional-file";
|
|
214
221
|
path: string;
|
|
222
|
+
realpath: string | null;
|
|
215
223
|
scope: "cache" | "watch";
|
|
216
224
|
};
|
|
217
225
|
|
|
@@ -237,15 +245,38 @@ type ConfigPluginEvaluation = {
|
|
|
237
245
|
*/
|
|
238
246
|
function resolveConfigFileContributors(
|
|
239
247
|
context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
240
|
-
):
|
|
248
|
+
): {
|
|
249
|
+
contributors: TtscPluginContributor[];
|
|
250
|
+
hostInputHashes: Record<string, string | null>;
|
|
251
|
+
hostInputRealpaths: Record<string, string | null>;
|
|
252
|
+
hostInputs: string[];
|
|
253
|
+
} {
|
|
241
254
|
const configFile = readConfigFileOption(context);
|
|
242
|
-
const
|
|
243
|
-
configFile
|
|
244
|
-
?
|
|
245
|
-
:
|
|
246
|
-
|
|
255
|
+
const explicitConfigPath =
|
|
256
|
+
configFile === undefined
|
|
257
|
+
? undefined
|
|
258
|
+
: path.resolve(pluginConfigBaseDir(context), configFile);
|
|
259
|
+
const discovery =
|
|
260
|
+
explicitConfigPath === undefined
|
|
261
|
+
? discoverLintConfigFile(context)
|
|
262
|
+
: {
|
|
263
|
+
configPath: explicitConfigPath,
|
|
264
|
+
hostInputHashes: hashHostInputPaths([explicitConfigPath]),
|
|
265
|
+
hostInputRealpaths: realpathHostInputPaths([explicitConfigPath]),
|
|
266
|
+
hostInputs: [explicitConfigPath],
|
|
267
|
+
};
|
|
268
|
+
const { configPath } = discovery;
|
|
269
|
+
if (!configPath || !fs.existsSync(configPath)) {
|
|
270
|
+
return {
|
|
271
|
+
contributors: [],
|
|
272
|
+
hostInputHashes: discovery.hostInputHashes,
|
|
273
|
+
hostInputRealpaths: discovery.hostInputRealpaths,
|
|
274
|
+
hostInputs: discovery.hostInputs,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
247
277
|
|
|
248
|
-
const
|
|
278
|
+
const evaluation = readConfigPluginEntries(configPath, context);
|
|
279
|
+
const entries = evaluation.entries;
|
|
249
280
|
assertContributorNamespacesDoNotCollide(entries, configPath);
|
|
250
281
|
// Dedup exact repeated namespaces on the Go-subpackage form. Config-array
|
|
251
282
|
// folding can surface the same namespace more than once; that existing
|
|
@@ -258,7 +289,180 @@ function resolveConfigFileContributors(
|
|
|
258
289
|
occupied.add(goName);
|
|
259
290
|
out.push({ name: goName, source: entry.source });
|
|
260
291
|
}
|
|
261
|
-
|
|
292
|
+
const dependencyInputs = evaluation.dependencies
|
|
293
|
+
.filter(
|
|
294
|
+
(dependency) =>
|
|
295
|
+
dependency.scope === "watch" && dependency.kind !== "directory",
|
|
296
|
+
)
|
|
297
|
+
.map((dependency) => dependency.path);
|
|
298
|
+
const hostInputHashes = { ...discovery.hostInputHashes };
|
|
299
|
+
const hostInputRealpaths = { ...discovery.hostInputRealpaths };
|
|
300
|
+
const unstableRealpaths = new Set<string>();
|
|
301
|
+
const unprovenInputs = new Set<string>();
|
|
302
|
+
const missingOptionalDigest = createHash("sha256")
|
|
303
|
+
.update("missing\0")
|
|
304
|
+
.digest("hex");
|
|
305
|
+
const directoryCandidateDigest = createHash("sha256")
|
|
306
|
+
.update("ttsc:host-input:directory\0")
|
|
307
|
+
.digest("hex");
|
|
308
|
+
for (const dependency of evaluation.dependencies) {
|
|
309
|
+
if (dependency.scope !== "watch" || dependency.kind === "directory") {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const input = path.resolve(dependency.path);
|
|
313
|
+
const realpath = dependency.realpath;
|
|
314
|
+
if (!dependency.identityStable) {
|
|
315
|
+
delete hostInputRealpaths[input];
|
|
316
|
+
delete hostInputHashes[input];
|
|
317
|
+
unstableRealpaths.add(input);
|
|
318
|
+
unprovenInputs.add(input);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (
|
|
322
|
+
Object.prototype.hasOwnProperty.call(hostInputRealpaths, input) &&
|
|
323
|
+
hostInputRealpaths[input] !== realpath
|
|
324
|
+
) {
|
|
325
|
+
delete hostInputRealpaths[input];
|
|
326
|
+
delete hostInputHashes[input];
|
|
327
|
+
unstableRealpaths.add(input);
|
|
328
|
+
} else if (!unstableRealpaths.has(input)) {
|
|
329
|
+
hostInputRealpaths[input] = realpath;
|
|
330
|
+
}
|
|
331
|
+
let hash: string | null | undefined;
|
|
332
|
+
if (
|
|
333
|
+
dependency.kind === "file" &&
|
|
334
|
+
/^[0-9a-f]{64}$/.test(dependency.digest)
|
|
335
|
+
) {
|
|
336
|
+
hash = dependency.digest;
|
|
337
|
+
} else if (dependency.digest === missingOptionalDigest) {
|
|
338
|
+
// The evaluator's optional-file digest includes a state prefix. The
|
|
339
|
+
// public host-input contract uses null for the observed missing state.
|
|
340
|
+
hash = null;
|
|
341
|
+
} else if (dependency.digest === directoryCandidateDigest) {
|
|
342
|
+
// A path that is currently a directory is still an exact file candidate:
|
|
343
|
+
// replacing it with a file changes module/config selection.
|
|
344
|
+
hash = directoryCandidateDigest;
|
|
345
|
+
}
|
|
346
|
+
if (hash === undefined) {
|
|
347
|
+
delete hostInputHashes[input];
|
|
348
|
+
unprovenInputs.add(input);
|
|
349
|
+
} else if (unprovenInputs.has(input)) {
|
|
350
|
+
// A later observation cannot revive proof another evaluation stage
|
|
351
|
+
// could not provide for the same combined descriptor result.
|
|
352
|
+
} else if (
|
|
353
|
+
Object.prototype.hasOwnProperty.call(hostInputHashes, input) &&
|
|
354
|
+
hostInputHashes[input] !== hash
|
|
355
|
+
) {
|
|
356
|
+
delete hostInputHashes[input];
|
|
357
|
+
unprovenInputs.add(input);
|
|
358
|
+
} else {
|
|
359
|
+
hostInputHashes[input] = hash;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
contributors: out,
|
|
364
|
+
hostInputHashes,
|
|
365
|
+
hostInputRealpaths,
|
|
366
|
+
hostInputs: [...discovery.hostInputs, ...dependencyInputs],
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Snapshot config candidates before any discovery/evaluation side effect. */
|
|
371
|
+
function hashHostInputPaths(
|
|
372
|
+
inputs: readonly string[],
|
|
373
|
+
): Record<string, string | null> {
|
|
374
|
+
return Object.fromEntries(
|
|
375
|
+
inputs.map((input) => {
|
|
376
|
+
const file = path.resolve(input);
|
|
377
|
+
try {
|
|
378
|
+
if (fs.statSync(file).isDirectory()) {
|
|
379
|
+
return [
|
|
380
|
+
file,
|
|
381
|
+
createHash("sha256")
|
|
382
|
+
.update("ttsc:host-input:directory\0")
|
|
383
|
+
.digest("hex"),
|
|
384
|
+
] as const;
|
|
385
|
+
}
|
|
386
|
+
return [
|
|
387
|
+
file,
|
|
388
|
+
createHash("sha256").update(fs.readFileSync(file)).digest("hex"),
|
|
389
|
+
] as const;
|
|
390
|
+
} catch {
|
|
391
|
+
return [file, null] as const;
|
|
392
|
+
}
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function hostInputRealpath(file: string): string | null {
|
|
398
|
+
try {
|
|
399
|
+
return fs.realpathSync.native(file);
|
|
400
|
+
} catch {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function realpathHostInputPaths(
|
|
406
|
+
inputs: readonly string[],
|
|
407
|
+
): Record<string, string | null> {
|
|
408
|
+
return Object.fromEntries(
|
|
409
|
+
inputs.map((input) => {
|
|
410
|
+
const file = path.resolve(input);
|
|
411
|
+
return [file, hostInputRealpath(file)] as const;
|
|
412
|
+
}),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Mirror native discovery and fingerprint every candidate before selecting. */
|
|
417
|
+
function discoverLintConfigFile(
|
|
418
|
+
context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
419
|
+
): {
|
|
420
|
+
configPath?: string;
|
|
421
|
+
hostInputHashes: Record<string, string | null>;
|
|
422
|
+
hostInputRealpaths: Record<string, string | null>;
|
|
423
|
+
hostInputs: string[];
|
|
424
|
+
} {
|
|
425
|
+
const hostInputHashes: Record<string, string | null> = {};
|
|
426
|
+
const hostInputRealpaths: Record<string, string | null> = {};
|
|
427
|
+
const hostInputs: string[] = [];
|
|
428
|
+
const recordCandidates = (candidates: readonly string[]): void => {
|
|
429
|
+
for (const candidate of candidates) {
|
|
430
|
+
const absolute = path.resolve(candidate);
|
|
431
|
+
if (Object.prototype.hasOwnProperty.call(hostInputHashes, absolute)) {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
hostInputs.push(absolute);
|
|
435
|
+
Object.assign(hostInputHashes, hashHostInputPaths([absolute]));
|
|
436
|
+
Object.assign(hostInputRealpaths, realpathHostInputPaths([absolute]));
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
for (const origin of discoveryConfigBaseDirs(context)) {
|
|
440
|
+
for (let directory = origin; ; directory = path.dirname(directory)) {
|
|
441
|
+
const candidates = LINT_CONFIG_FILENAMES.map((name) =>
|
|
442
|
+
path.join(directory, name),
|
|
443
|
+
);
|
|
444
|
+
recordCandidates(candidates);
|
|
445
|
+
const matches = lintConfigMatchesIn(directory);
|
|
446
|
+
if (matches.length === 1) {
|
|
447
|
+
return {
|
|
448
|
+
configPath: matches[0],
|
|
449
|
+
hostInputHashes,
|
|
450
|
+
hostInputRealpaths,
|
|
451
|
+
hostInputs,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
if (matches.length > 1) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
`@ttsc/lint: multiple lint config files found in ${directory} (${matches
|
|
457
|
+
.map((file) => path.basename(file))
|
|
458
|
+
.join(", ")}); set "configFile" explicitly`,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
const parent = path.dirname(directory);
|
|
462
|
+
if (parent === directory) break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return { hostInputHashes, hostInputRealpaths, hostInputs };
|
|
262
466
|
}
|
|
263
467
|
|
|
264
468
|
function assertContributorNamespacesDoNotCollide(
|
|
@@ -328,26 +532,6 @@ function readConfigFileOption(
|
|
|
328
532
|
return value;
|
|
329
533
|
}
|
|
330
534
|
|
|
331
|
-
function findLintConfigFile(
|
|
332
|
-
context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
333
|
-
): string | undefined {
|
|
334
|
-
// Mirror the Go side (driver.PluginConfigBaseDir): the caller-declared
|
|
335
|
-
// pluginConfigDir is the single walk origin when present — it names the
|
|
336
|
-
// real project when the resolved tsconfig is a generated wrapper in a temp
|
|
337
|
-
// dir (@ttsc/unplugin's alias overlay), and it keeps the wrapper's temp
|
|
338
|
-
// ancestry out of the walk so a stray config planted there is never
|
|
339
|
-
// honored. Otherwise walk upward from the tsconfig directory first, then
|
|
340
|
-
// fall back to the working directory: that covers callers that point at an
|
|
341
|
-
// out-of-tree tsconfig without declaring an anchor.
|
|
342
|
-
for (const origin of discoveryConfigBaseDirs(context)) {
|
|
343
|
-
const discovered = findLintConfigFileFrom(origin);
|
|
344
|
-
if (discovered !== undefined) {
|
|
345
|
-
return discovered;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return undefined;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
535
|
function discoveryConfigBaseDirs(
|
|
352
536
|
context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
353
537
|
): string[] {
|
|
@@ -359,41 +543,23 @@ function discoveryConfigBaseDirs(
|
|
|
359
543
|
return tsconfigDir === cwd ? [tsconfigDir] : [tsconfigDir, cwd];
|
|
360
544
|
}
|
|
361
545
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
// the binary's own discovery to surface the issue once with one canonical
|
|
368
|
-
// message).
|
|
369
|
-
const candidateSet = new Set<string>(LINT_CONFIG_FILENAMES);
|
|
370
|
-
let dir = origin;
|
|
371
|
-
while (true) {
|
|
372
|
-
// One `readdirSync` per directory level beats 14 `existsSync`+
|
|
373
|
-
// `statSync` pairs (= 28 stat syscalls) per level; intersect the
|
|
374
|
-
// listing with the candidate set instead.
|
|
375
|
-
let entries: fs.Dirent[];
|
|
546
|
+
/** Return the non-directory candidates native discovery recognizes. */
|
|
547
|
+
function lintConfigMatchesIn(directory: string): string[] {
|
|
548
|
+
const matches: string[] = [];
|
|
549
|
+
for (const name of LINT_CONFIG_FILENAMES) {
|
|
550
|
+
const candidate = path.join(directory, name);
|
|
376
551
|
try {
|
|
377
|
-
|
|
552
|
+
// Go's os.Stat follows symlinks and junctions. Follow them here too so a
|
|
553
|
+
// directory or dangling link cannot stop descriptor discovery before
|
|
554
|
+
// the native host reaches a valid ancestor config. Probing the canonical
|
|
555
|
+
// candidate spelling also preserves Go's behavior on case-insensitive
|
|
556
|
+
// filesystems when the directory entry uses different casing.
|
|
557
|
+
if (!fs.statSync(candidate).isDirectory()) matches.push(candidate);
|
|
378
558
|
} catch {
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
const matches: string[] = [];
|
|
382
|
-
for (const entry of entries) {
|
|
383
|
-
if (!candidateSet.has(entry.name)) continue;
|
|
384
|
-
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
385
|
-
matches.push(path.join(dir, entry.name));
|
|
559
|
+
// Missing, dangling, and unreadable candidates are not native matches.
|
|
386
560
|
}
|
|
387
|
-
if (matches.length === 1) {
|
|
388
|
-
return matches[0];
|
|
389
|
-
}
|
|
390
|
-
if (matches.length > 1) {
|
|
391
|
-
return undefined; // ambiguous — defer to the Go side's error
|
|
392
|
-
}
|
|
393
|
-
const parent = path.dirname(dir);
|
|
394
|
-
if (parent === dir) return undefined;
|
|
395
|
-
dir = parent;
|
|
396
561
|
}
|
|
562
|
+
return matches;
|
|
397
563
|
}
|
|
398
564
|
|
|
399
565
|
/**
|
|
@@ -429,7 +595,7 @@ function tsconfigBaseDir(
|
|
|
429
595
|
function readConfigPluginEntries(
|
|
430
596
|
configPath: string,
|
|
431
597
|
context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
432
|
-
):
|
|
598
|
+
): ConfigPluginEvaluation {
|
|
433
599
|
// A JSON config that can bring no contributor with it — no `plugins` map and
|
|
434
600
|
// no `extends` chain to follow — has nothing to extract, and reading it runs
|
|
435
601
|
// no user code, so the isolated evaluator is not needed to keep its strings
|
|
@@ -437,7 +603,9 @@ function readConfigPluginEntries(
|
|
|
437
603
|
// is a real subprocess, and a host that only wanted to know whether there
|
|
438
604
|
// were contributors would otherwise depend on a launcher being resolvable and
|
|
439
605
|
// on a compiler accepting one more invocation.
|
|
440
|
-
if (jsonConfigDeclaresNoContributor(configPath))
|
|
606
|
+
if (jsonConfigDeclaresNoContributor(configPath)) {
|
|
607
|
+
return { dependencies: [], entries: [] };
|
|
608
|
+
}
|
|
441
609
|
// Every other config uses the same isolated evaluator. Executable config can
|
|
442
610
|
// name contributor packages whose top-level code writes to stdout, so loading
|
|
443
611
|
// it in this host process would corrupt CLI JSON or preface the first LSP
|
|
@@ -520,9 +688,11 @@ const CONFIG_KEYS = new Set<string>([
|
|
|
520
688
|
]);
|
|
521
689
|
const dependencies = new Map<string, {
|
|
522
690
|
digest: string;
|
|
691
|
+
identityStable: boolean;
|
|
523
692
|
kind: "directory" | "file" | "optional-file";
|
|
524
693
|
path: string;
|
|
525
694
|
owners: Set<string>;
|
|
695
|
+
realpath: string | null;
|
|
526
696
|
}>();
|
|
527
697
|
const graphNodes = new Map<string, string>();
|
|
528
698
|
const graphEdges: Array<{
|
|
@@ -553,6 +723,23 @@ const configUrlSpellings = [
|
|
|
553
723
|
pathToFileURL(realConfigLocation()).href,
|
|
554
724
|
]),
|
|
555
725
|
];
|
|
726
|
+
const moduleProbeExtensions = [
|
|
727
|
+
".ts",
|
|
728
|
+
".tsx",
|
|
729
|
+
".mts",
|
|
730
|
+
".cts",
|
|
731
|
+
".js",
|
|
732
|
+
".mjs",
|
|
733
|
+
".cjs",
|
|
734
|
+
".json",
|
|
735
|
+
".node",
|
|
736
|
+
] as const;
|
|
737
|
+
const jsToTsProbeExtensions = new Map<string, readonly string[]>([
|
|
738
|
+
[".js", [".ts", ".tsx"]],
|
|
739
|
+
[".jsx", [".tsx"]],
|
|
740
|
+
[".mjs", [".mts"]],
|
|
741
|
+
[".cjs", [".cts"]],
|
|
742
|
+
]);
|
|
556
743
|
for (const spelling of configUrlSpellings) {
|
|
557
744
|
graphNodes.set(spelling, configLocation);
|
|
558
745
|
}
|
|
@@ -574,6 +761,11 @@ declare const process: {
|
|
|
574
761
|
|
|
575
762
|
const hooks = registerHooks({
|
|
576
763
|
resolve(specifier, context, nextResolve) {
|
|
764
|
+
const requestedParent =
|
|
765
|
+
context.parentURL && new URL(context.parentURL).href;
|
|
766
|
+
if (requestedParent !== undefined && graphNodes.has(requestedParent)) {
|
|
767
|
+
recordLocalResolutionCandidates(specifier, requestedParent);
|
|
768
|
+
}
|
|
577
769
|
const resolved = nextResolve(specifier, context);
|
|
578
770
|
if (typeof resolved.url !== "string" || !resolved.url.startsWith("file:")) {
|
|
579
771
|
return resolved;
|
|
@@ -706,14 +898,32 @@ function recordDependency(
|
|
|
706
898
|
const previous = dependencies.get(key);
|
|
707
899
|
const mergedOwners = previous?.owners ?? new Set<string>();
|
|
708
900
|
for (const owner of owners) mergedOwners.add(owner);
|
|
901
|
+
const realpath = dependencyRealpath(location);
|
|
902
|
+
const identityStable =
|
|
903
|
+
previous?.identityStable !== false &&
|
|
904
|
+
(previous === undefined || previous.realpath === realpath);
|
|
709
905
|
dependencies.set(key, {
|
|
710
|
-
digest:
|
|
906
|
+
digest:
|
|
907
|
+
!identityStable ||
|
|
908
|
+
(previous !== undefined && previous.digest !== digest)
|
|
909
|
+
? ""
|
|
910
|
+
: digest,
|
|
911
|
+
identityStable,
|
|
711
912
|
kind,
|
|
712
913
|
owners: mergedOwners,
|
|
713
914
|
path: location,
|
|
915
|
+
realpath,
|
|
714
916
|
});
|
|
715
917
|
}
|
|
716
918
|
|
|
919
|
+
function dependencyRealpath(location: string): string | null {
|
|
920
|
+
try {
|
|
921
|
+
return realPath(location);
|
|
922
|
+
} catch {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
717
927
|
function isLocalModuleSpecifier(specifier: string): boolean {
|
|
718
928
|
return specifier.startsWith(".") ||
|
|
719
929
|
specifier.startsWith("/") ||
|
|
@@ -831,13 +1041,19 @@ function directoryDigestRecord(
|
|
|
831
1041
|
|
|
832
1042
|
function optionalFileDigest(location: string): string {
|
|
833
1043
|
try {
|
|
834
|
-
|
|
1044
|
+
const entry = fs.statSync(location);
|
|
1045
|
+
if (entry.isFile()) {
|
|
835
1046
|
return createHash("sha256")
|
|
836
1047
|
.update(Buffer.concat([Buffer.from("file\\0"), fs.readFileSync(location)]))
|
|
837
1048
|
.digest("hex");
|
|
838
1049
|
}
|
|
1050
|
+
if (entry.isDirectory()) {
|
|
1051
|
+
return createHash("sha256")
|
|
1052
|
+
.update("ttsc:host-input:directory\\0")
|
|
1053
|
+
.digest("hex");
|
|
1054
|
+
}
|
|
839
1055
|
} catch {
|
|
840
|
-
// Missing
|
|
1056
|
+
// Missing and unreadable candidates share the absent state.
|
|
841
1057
|
}
|
|
842
1058
|
return createHash("sha256").update("missing\\0").digest("hex");
|
|
843
1059
|
}
|
|
@@ -863,6 +1079,80 @@ function recordOptionalFileDependency(
|
|
|
863
1079
|
return false;
|
|
864
1080
|
}
|
|
865
1081
|
|
|
1082
|
+
function moduleResolutionCandidates(base: string): string[] {
|
|
1083
|
+
const extension = path.extname(base).toLowerCase();
|
|
1084
|
+
const substitutions = jsToTsProbeExtensions.get(extension) ?? [];
|
|
1085
|
+
const stem = base.slice(0, base.length - extension.length);
|
|
1086
|
+
return [
|
|
1087
|
+
base,
|
|
1088
|
+
...substitutions.map((candidate) => stem + candidate),
|
|
1089
|
+
...moduleProbeExtensions.map((candidate) => base + candidate),
|
|
1090
|
+
path.join(base, "package.json"),
|
|
1091
|
+
...moduleProbeExtensions.map((candidate) =>
|
|
1092
|
+
path.join(base, "index" + candidate),
|
|
1093
|
+
),
|
|
1094
|
+
];
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/** Record exact local probes before the runtime resolver chooses one. */
|
|
1098
|
+
function recordLocalResolutionCandidates(
|
|
1099
|
+
specifier: string,
|
|
1100
|
+
parentUrl: string,
|
|
1101
|
+
): void {
|
|
1102
|
+
if (!isLocalModuleSpecifier(specifier)) return;
|
|
1103
|
+
let bases: string[];
|
|
1104
|
+
try {
|
|
1105
|
+
if (specifier.startsWith("file:")) {
|
|
1106
|
+
bases = [fileURLToPath(specifier)];
|
|
1107
|
+
} else {
|
|
1108
|
+
const directory = path.dirname(fileURLToPath(parentUrl));
|
|
1109
|
+
const raw = path.resolve(directory, specifier);
|
|
1110
|
+
const suffixStart = specifier.search(/[?#]/);
|
|
1111
|
+
const pathname =
|
|
1112
|
+
suffixStart === -1 ? specifier : specifier.slice(0, suffixStart);
|
|
1113
|
+
bases = pathname === ""
|
|
1114
|
+
? [raw]
|
|
1115
|
+
: [...new Set([raw, path.resolve(directory, pathname)])];
|
|
1116
|
+
}
|
|
1117
|
+
} catch {
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const owners = [parentUrl];
|
|
1121
|
+
for (const base of bases) {
|
|
1122
|
+
try {
|
|
1123
|
+
if (fs.statSync(base).isFile()) {
|
|
1124
|
+
recordOptionalFileDependency(base, owners);
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
} catch {
|
|
1128
|
+
// A missing exact spelling falls through to source/extension/directory
|
|
1129
|
+
// probes, all of which can redirect a later evaluation.
|
|
1130
|
+
}
|
|
1131
|
+
for (const candidate of moduleResolutionCandidates(base)) {
|
|
1132
|
+
recordOptionalFileDependency(candidate, owners);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/** CommonJS LOAD_AS_FILE / LOAD_AS_DIRECTORY candidates for one legacy path. */
|
|
1138
|
+
function recordLegacyPackagePathCandidates(
|
|
1139
|
+
candidate: string,
|
|
1140
|
+
owners: readonly string[],
|
|
1141
|
+
): void {
|
|
1142
|
+
for (const file of [
|
|
1143
|
+
candidate,
|
|
1144
|
+
candidate + ".js",
|
|
1145
|
+
candidate + ".json",
|
|
1146
|
+
candidate + ".node",
|
|
1147
|
+
path.join(candidate, "package.json"),
|
|
1148
|
+
path.join(candidate, "index.js"),
|
|
1149
|
+
path.join(candidate, "index.json"),
|
|
1150
|
+
path.join(candidate, "index.node"),
|
|
1151
|
+
]) {
|
|
1152
|
+
recordOptionalFileDependency(file, owners);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
866
1156
|
function recordPackageManifests(
|
|
867
1157
|
location: string,
|
|
868
1158
|
owners: readonly string[],
|
|
@@ -908,26 +1198,26 @@ function recordNodeModulesSearchDirectories(
|
|
|
908
1198
|
// The directory digest of node_modules records a missing scope.
|
|
909
1199
|
}
|
|
910
1200
|
}
|
|
911
|
-
if (packageName !== undefined) {
|
|
912
|
-
const selected = recordPackageCandidateTopology(
|
|
913
|
-
modules,
|
|
914
|
-
packageName,
|
|
915
|
-
specifier,
|
|
916
|
-
childLocation,
|
|
917
|
-
owners,
|
|
918
|
-
conditions,
|
|
919
|
-
);
|
|
920
|
-
if (
|
|
921
|
-
selected ||
|
|
922
|
-
resolvedPackageContains(modules, packageName, childLocation)
|
|
923
|
-
) {
|
|
924
|
-
return;
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
1201
|
}
|
|
928
1202
|
} catch {
|
|
929
1203
|
// Missing search levels do not participate in the current resolution.
|
|
930
1204
|
}
|
|
1205
|
+
if (packageName !== undefined) {
|
|
1206
|
+
const selected = recordPackageCandidateTopology(
|
|
1207
|
+
modules,
|
|
1208
|
+
packageName,
|
|
1209
|
+
specifier,
|
|
1210
|
+
childLocation,
|
|
1211
|
+
owners,
|
|
1212
|
+
conditions,
|
|
1213
|
+
);
|
|
1214
|
+
if (
|
|
1215
|
+
selected ||
|
|
1216
|
+
resolvedPackageContains(modules, packageName, childLocation)
|
|
1217
|
+
) {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
931
1221
|
if (
|
|
932
1222
|
packageName === undefined &&
|
|
933
1223
|
samePhysicalPath(current, resolutionRoot)
|
|
@@ -949,14 +1239,32 @@ function recordPackageCandidateTopology(
|
|
|
949
1239
|
conditions: readonly string[],
|
|
950
1240
|
): boolean {
|
|
951
1241
|
const packageRoot = path.join(modules, packageName);
|
|
1242
|
+
const subpath = specifier
|
|
1243
|
+
.slice(packageName.length)
|
|
1244
|
+
.replace(/^[/\\\\]+/, "");
|
|
952
1245
|
try {
|
|
953
|
-
if (!fs.statSync(packageRoot).isDirectory())
|
|
1246
|
+
if (!fs.statSync(packageRoot).isDirectory()) {
|
|
1247
|
+
recordOptionalFileDependency(
|
|
1248
|
+
path.join(packageRoot, "package.json"),
|
|
1249
|
+
owners,
|
|
1250
|
+
);
|
|
1251
|
+
recordLegacyPackagePathCandidates(
|
|
1252
|
+
subpath === "" ? packageRoot : path.join(packageRoot, subpath),
|
|
1253
|
+
owners,
|
|
1254
|
+
);
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
954
1257
|
} catch {
|
|
1258
|
+
recordOptionalFileDependency(
|
|
1259
|
+
path.join(packageRoot, "package.json"),
|
|
1260
|
+
owners,
|
|
1261
|
+
);
|
|
1262
|
+
recordLegacyPackagePathCandidates(
|
|
1263
|
+
subpath === "" ? packageRoot : path.join(packageRoot, subpath),
|
|
1264
|
+
owners,
|
|
1265
|
+
);
|
|
955
1266
|
return false;
|
|
956
1267
|
}
|
|
957
|
-
const subpath = specifier
|
|
958
|
-
.slice(packageName.length)
|
|
959
|
-
.replace(/^[/\\\\]+/, "");
|
|
960
1268
|
const rootTopology = recordPackageRootTopology(
|
|
961
1269
|
packageRoot,
|
|
962
1270
|
owners,
|
|
@@ -991,6 +1299,7 @@ function recordPackageRootTopology(
|
|
|
991
1299
|
const legacySelected = (): boolean =>
|
|
992
1300
|
useMain &&
|
|
993
1301
|
packagePathCandidateMatchesChild(normalizedRoot, childLocation, true);
|
|
1302
|
+
if (useMain) recordLegacyPackagePathCandidates(normalizedRoot, owners);
|
|
994
1303
|
if (!recordOptionalFileDependency(manifest, owners)) {
|
|
995
1304
|
const selected = legacySelected();
|
|
996
1305
|
if (!selected) {
|
|
@@ -1037,6 +1346,7 @@ function recordPackageRootTopology(
|
|
|
1037
1346
|
// it literally and permits absolute paths and paths outside the package.
|
|
1038
1347
|
const main = path.resolve(normalizedRoot, metadata.main);
|
|
1039
1348
|
recordPackagePathCandidate(main, owners);
|
|
1349
|
+
recordLegacyPackagePathCandidates(main, owners);
|
|
1040
1350
|
selected =
|
|
1041
1351
|
packagePathCandidateMatchesChild(main, childLocation, true) ||
|
|
1042
1352
|
selected;
|
|
@@ -1284,6 +1594,7 @@ function recordPackageSubpathTopology(
|
|
|
1284
1594
|
const candidate = boundedPackageTarget(packageRoot, subpath);
|
|
1285
1595
|
if (candidate === undefined) return false;
|
|
1286
1596
|
recordPackagePathCandidate(candidate, owners);
|
|
1597
|
+
recordLegacyPackagePathCandidates(candidate, owners);
|
|
1287
1598
|
let selected = packagePathCandidateMatchesChild(
|
|
1288
1599
|
candidate,
|
|
1289
1600
|
childLocation,
|
|
@@ -1303,6 +1614,7 @@ function recordPackageSubpathTopology(
|
|
|
1303
1614
|
if (typeof metadata.main === "string") {
|
|
1304
1615
|
const main = path.resolve(candidate, metadata.main);
|
|
1305
1616
|
recordPackagePathCandidate(main, owners);
|
|
1617
|
+
recordLegacyPackagePathCandidates(main, owners);
|
|
1306
1618
|
selected =
|
|
1307
1619
|
packagePathCandidateMatchesChild(main, childLocation, true) ||
|
|
1308
1620
|
selected;
|
|
@@ -1473,10 +1785,25 @@ function realPath(location: string): string {
|
|
|
1473
1785
|
|
|
1474
1786
|
function finalizeDependencies(): Array<{
|
|
1475
1787
|
digest: string;
|
|
1788
|
+
identityStable: boolean;
|
|
1476
1789
|
kind: "directory" | "file" | "optional-file";
|
|
1477
1790
|
path: string;
|
|
1791
|
+
realpath: string | null;
|
|
1478
1792
|
scope: "cache" | "watch";
|
|
1479
1793
|
}> {
|
|
1794
|
+
// The evaluator may have observed a dependency, run arbitrary config code,
|
|
1795
|
+
// and then serialize after that path changed again. Re-read every recorded
|
|
1796
|
+
// dependency under its original ownership set so an A -> B -> A transition
|
|
1797
|
+
// is marked identity-unstable instead of pairing transient output with the
|
|
1798
|
+
// restored fingerprint.
|
|
1799
|
+
for (const dependency of [...dependencies.values()]) {
|
|
1800
|
+
recordDependency(
|
|
1801
|
+
dependency.kind,
|
|
1802
|
+
dependency.path,
|
|
1803
|
+
currentDependencyDigest(dependency.kind, dependency.path),
|
|
1804
|
+
[...dependency.owners],
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1480
1807
|
const watched = graphWatchReachability();
|
|
1481
1808
|
return [...dependencies.values()].map(({ owners, ...dependency }) => ({
|
|
1482
1809
|
...dependency,
|
|
@@ -1486,6 +1813,21 @@ function finalizeDependencies(): Array<{
|
|
|
1486
1813
|
}));
|
|
1487
1814
|
}
|
|
1488
1815
|
|
|
1816
|
+
function currentDependencyDigest(
|
|
1817
|
+
kind: "directory" | "file" | "optional-file",
|
|
1818
|
+
location: string,
|
|
1819
|
+
): string {
|
|
1820
|
+
try {
|
|
1821
|
+
if (kind === "directory") return directoryDigest(location);
|
|
1822
|
+
if (kind === "optional-file") return optionalFileDigest(location);
|
|
1823
|
+
return createHash("sha256")
|
|
1824
|
+
.update(fs.readFileSync(location))
|
|
1825
|
+
.digest("hex");
|
|
1826
|
+
} catch {
|
|
1827
|
+
return "";
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1489
1831
|
function graphWatchReachability(): Set<string> {
|
|
1490
1832
|
const adjacency = new Map<string, typeof graphEdges>();
|
|
1491
1833
|
for (const edge of graphEdges) {
|
|
@@ -1642,7 +1984,7 @@ function extractPluginSource(value: unknown): string | undefined {
|
|
|
1642
1984
|
function readTtsxConfigPlugins(
|
|
1643
1985
|
configPath: string,
|
|
1644
1986
|
context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
1645
|
-
):
|
|
1987
|
+
): ConfigPluginEvaluation {
|
|
1646
1988
|
const resolutionRoot = path.resolve(pluginConfigBaseDir(context));
|
|
1647
1989
|
const cacheKey = configCacheKey(`plugins\0${resolutionRoot}`, configPath);
|
|
1648
1990
|
if (cacheKey) {
|
|
@@ -1656,7 +1998,7 @@ function readTtsxConfigPlugins(
|
|
|
1656
1998
|
cached.entries.every(isValidConfigPluginEntry) &&
|
|
1657
1999
|
configDependenciesAreCurrent(cached.dependencies)
|
|
1658
2000
|
) {
|
|
1659
|
-
return cached
|
|
2001
|
+
return cached;
|
|
1660
2002
|
}
|
|
1661
2003
|
}
|
|
1662
2004
|
// A config can be saved while it is being evaluated. Retry a bounded number
|
|
@@ -1668,10 +2010,10 @@ function readTtsxConfigPlugins(
|
|
|
1668
2010
|
evaluation = evaluateTtsxConfigPlugins(configPath, context);
|
|
1669
2011
|
if (configDependenciesAreCurrent(evaluation.dependencies)) {
|
|
1670
2012
|
if (cacheKey) writeConfigPluginCache(cacheKey, evaluation);
|
|
1671
|
-
return evaluation
|
|
2013
|
+
return evaluation;
|
|
1672
2014
|
}
|
|
1673
2015
|
}
|
|
1674
|
-
return evaluation
|
|
2016
|
+
return evaluation!;
|
|
1675
2017
|
}
|
|
1676
2018
|
|
|
1677
2019
|
/**
|
|
@@ -1704,8 +2046,9 @@ function evaluateTtsxConfigPlugins(
|
|
|
1704
2046
|
configPath: string,
|
|
1705
2047
|
context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
1706
2048
|
): ConfigPluginEvaluation {
|
|
1707
|
-
const tempDir =
|
|
1708
|
-
|
|
2049
|
+
const tempDir = createCanonicalTempDirectory(
|
|
2050
|
+
"ttsc-lint-cfg-",
|
|
2051
|
+
loaderTempBase(configPath),
|
|
1709
2052
|
);
|
|
1710
2053
|
try {
|
|
1711
2054
|
linkNearestNodeModules(tempDir, path.dirname(configPath));
|
|
@@ -1896,11 +2239,34 @@ function evaluateTtsxConfigPlugins(
|
|
|
1896
2239
|
// Config cache (shared with the Go sidecar — packages/lint/linthost/config.go)
|
|
1897
2240
|
// ────────────────────────────────────────────────────────────────────────────
|
|
1898
2241
|
|
|
2242
|
+
/** Create evaluator storage beneath a frozen physical parent. */
|
|
2243
|
+
function createCanonicalTempDirectory(prefix: string, parent: string): string {
|
|
2244
|
+
const physicalParent = fs.realpathSync.native(parent);
|
|
2245
|
+
if (!fs.lstatSync(physicalParent).isDirectory()) {
|
|
2246
|
+
throw new Error(
|
|
2247
|
+
`@ttsc/lint: temporary directory parent is not a directory: ${physicalParent}`,
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
const directory = fs.mkdtempSync(path.join(physicalParent, prefix));
|
|
2251
|
+
if (!fs.lstatSync(directory).isDirectory()) {
|
|
2252
|
+
throw new Error(
|
|
2253
|
+
`@ttsc/lint: temporary directory postflight is not a directory: ${directory}`,
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
const physicalDirectory = fs.realpathSync.native(directory);
|
|
2257
|
+
if (path.dirname(physicalDirectory) !== physicalParent) {
|
|
2258
|
+
throw new Error(
|
|
2259
|
+
`@ttsc/lint: temporary directory escaped its physical parent: ${physicalDirectory}`,
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
return physicalDirectory;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
1899
2265
|
/**
|
|
1900
2266
|
* Namespaces the on-disk config cache. Kept in lockstep with the Go sidecar's
|
|
1901
2267
|
* `configCacheVersion`; bump both when the cached shape changes.
|
|
1902
2268
|
*/
|
|
1903
|
-
const CONFIG_CACHE_VERSION = "
|
|
2269
|
+
const CONFIG_CACHE_VERSION = "v7";
|
|
1904
2270
|
|
|
1905
2271
|
/**
|
|
1906
2272
|
* Directory shared by this factory and the Go sidecar for cached lint configs.
|
|
@@ -2024,6 +2390,14 @@ function normalizeConfigDependencyFingerprints(
|
|
|
2024
2390
|
typeof candidate !== "object" ||
|
|
2025
2391
|
typeof (candidate as ConfigDependencyFingerprint).path !== "string" ||
|
|
2026
2392
|
typeof (candidate as ConfigDependencyFingerprint).digest !== "string" ||
|
|
2393
|
+
typeof (candidate as ConfigDependencyFingerprint).identityStable !==
|
|
2394
|
+
"boolean" ||
|
|
2395
|
+
((candidate as ConfigDependencyFingerprint).realpath !== null &&
|
|
2396
|
+
(typeof (candidate as ConfigDependencyFingerprint).realpath !==
|
|
2397
|
+
"string" ||
|
|
2398
|
+
!path.isAbsolute(
|
|
2399
|
+
(candidate as ConfigDependencyFingerprint).realpath as string,
|
|
2400
|
+
))) ||
|
|
2027
2401
|
!["directory", "file", "optional-file"].includes(
|
|
2028
2402
|
(candidate as ConfigDependencyFingerprint).kind,
|
|
2029
2403
|
) ||
|
|
@@ -2036,24 +2410,40 @@ function normalizeConfigDependencyFingerprints(
|
|
|
2036
2410
|
const candidatePath = (candidate as ConfigDependencyFingerprint).path;
|
|
2037
2411
|
const digest = (candidate as ConfigDependencyFingerprint).digest;
|
|
2038
2412
|
const kind = (candidate as ConfigDependencyFingerprint).kind;
|
|
2413
|
+
const identityStable = (candidate as ConfigDependencyFingerprint)
|
|
2414
|
+
.identityStable;
|
|
2415
|
+
const realpath = (candidate as ConfigDependencyFingerprint).realpath;
|
|
2039
2416
|
const scope = (candidate as ConfigDependencyFingerprint).scope;
|
|
2040
|
-
if (
|
|
2417
|
+
if (
|
|
2418
|
+
!path.isAbsolute(candidatePath) ||
|
|
2419
|
+
(digest !== "" && !/^[0-9a-f]{64}$/.test(digest))
|
|
2420
|
+
) {
|
|
2041
2421
|
return undefined;
|
|
2042
2422
|
}
|
|
2043
2423
|
const location = path.resolve(candidatePath);
|
|
2044
|
-
|
|
2424
|
+
// The same lexical path can be both a traversed directory and an exact
|
|
2425
|
+
// file candidate. Those observations have different freshness contracts:
|
|
2426
|
+
// one fingerprints the listing, while the other fingerprints its entry
|
|
2427
|
+
// type. Preserve both, but still reject contradictory duplicates of one
|
|
2428
|
+
// kind.
|
|
2429
|
+
const key = kind + "\0" + location;
|
|
2430
|
+
const previous = dependencies.get(key);
|
|
2045
2431
|
if (
|
|
2046
2432
|
previous !== undefined &&
|
|
2047
2433
|
(previous.digest !== digest ||
|
|
2434
|
+
previous.identityStable !== identityStable ||
|
|
2048
2435
|
previous.kind !== kind ||
|
|
2436
|
+
previous.realpath !== realpath ||
|
|
2049
2437
|
previous.scope !== scope)
|
|
2050
2438
|
) {
|
|
2051
2439
|
return undefined;
|
|
2052
2440
|
}
|
|
2053
|
-
dependencies.set(
|
|
2441
|
+
dependencies.set(key, {
|
|
2054
2442
|
digest,
|
|
2443
|
+
identityStable,
|
|
2055
2444
|
kind,
|
|
2056
2445
|
path: location,
|
|
2446
|
+
realpath,
|
|
2057
2447
|
scope,
|
|
2058
2448
|
});
|
|
2059
2449
|
}
|
|
@@ -2067,6 +2457,12 @@ function configDependenciesAreCurrent(
|
|
|
2067
2457
|
): boolean {
|
|
2068
2458
|
if (dependencies.length === 0) return false;
|
|
2069
2459
|
return dependencies.every((dependency) => {
|
|
2460
|
+
if (
|
|
2461
|
+
!dependency.identityStable ||
|
|
2462
|
+
dependency.realpath !== hostInputRealpath(dependency.path)
|
|
2463
|
+
) {
|
|
2464
|
+
return false;
|
|
2465
|
+
}
|
|
2070
2466
|
if (!/^[0-9a-f]{64}$/.test(dependency.digest)) return false;
|
|
2071
2467
|
try {
|
|
2072
2468
|
const digest =
|
|
@@ -2156,15 +2552,21 @@ function configDirectoryDigestRecord(
|
|
|
2156
2552
|
|
|
2157
2553
|
function configOptionalFileDigest(location: string): string {
|
|
2158
2554
|
try {
|
|
2159
|
-
|
|
2555
|
+
const entry = fs.statSync(location);
|
|
2556
|
+
if (entry.isFile()) {
|
|
2160
2557
|
return createHash("sha256")
|
|
2161
2558
|
.update(
|
|
2162
2559
|
Buffer.concat([Buffer.from("file\0"), fs.readFileSync(location)]),
|
|
2163
2560
|
)
|
|
2164
2561
|
.digest("hex");
|
|
2165
2562
|
}
|
|
2563
|
+
if (entry.isDirectory()) {
|
|
2564
|
+
return createHash("sha256")
|
|
2565
|
+
.update("ttsc:host-input:directory\0")
|
|
2566
|
+
.digest("hex");
|
|
2567
|
+
}
|
|
2166
2568
|
} catch {
|
|
2167
|
-
// Missing
|
|
2569
|
+
// Missing and unreadable candidates share the absent state.
|
|
2168
2570
|
}
|
|
2169
2571
|
return createHash("sha256").update("missing\0").digest("hex");
|
|
2170
2572
|
}
|
|
@@ -2425,14 +2827,6 @@ function loaderTempBase(configPath: string): string {
|
|
|
2425
2827
|
return path.dirname(configPath);
|
|
2426
2828
|
}
|
|
2427
2829
|
|
|
2428
|
-
function realpathIfPossible(location: string): string {
|
|
2429
|
-
try {
|
|
2430
|
-
return fs.realpathSync(location);
|
|
2431
|
-
} catch {
|
|
2432
|
-
return location;
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
2830
|
function nodeConfigLoaderEnv(configPath: string): NodeJS.ProcessEnv {
|
|
2437
2831
|
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
2438
2832
|
const parts: string[] = [];
|