@ttsc/lint 0.26.2 → 0.28.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 +5 -1
- package/lib/index.js +458 -112
- package/lib/index.js.map +1 -1
- package/linthost/compile.go +147 -3
- package/linthost/config.go +230 -25
- package/linthost/dispatch.go +3 -1
- package/linthost/graph_nodes.go +205 -0
- package/linthost/hints.go +1 -1
- package/linthost/project_inputs.go +15 -4
- package/linthost/serve.go +19 -0
- package/package.json +2 -2
- package/rule/graph.go +150 -0
- package/src/index.ts +511 -115
package/lib/index.js
CHANGED
|
@@ -48,20 +48,20 @@ function goSubpackageName(namespace) {
|
|
|
48
48
|
return namespace.replace(/-/g, "_");
|
|
49
49
|
}
|
|
50
50
|
const LINT_CONFIG_FILENAMES = [
|
|
51
|
+
"lint.config.json",
|
|
52
|
+
"lint.config.js",
|
|
53
|
+
"lint.config.mjs",
|
|
54
|
+
"lint.config.cjs",
|
|
51
55
|
"lint.config.ts",
|
|
52
56
|
"lint.config.mts",
|
|
53
57
|
"lint.config.cts",
|
|
54
|
-
"lint.config.
|
|
55
|
-
"lint.config.
|
|
56
|
-
"lint.config.
|
|
57
|
-
"lint.config.
|
|
58
|
+
"ttsc-lint.config.json",
|
|
59
|
+
"ttsc-lint.config.js",
|
|
60
|
+
"ttsc-lint.config.mjs",
|
|
61
|
+
"ttsc-lint.config.cjs",
|
|
58
62
|
"ttsc-lint.config.ts",
|
|
59
63
|
"ttsc-lint.config.mts",
|
|
60
64
|
"ttsc-lint.config.cts",
|
|
61
|
-
"ttsc-lint.config.mjs",
|
|
62
|
-
"ttsc-lint.config.cjs",
|
|
63
|
-
"ttsc-lint.config.js",
|
|
64
|
-
"ttsc-lint.config.json",
|
|
65
65
|
];
|
|
66
66
|
/**
|
|
67
67
|
* Tsconfig plugin-entry keys owned by the ttsc host framework. They are
|
|
@@ -95,13 +95,14 @@ const FRAMEWORK_KEYS = new Set([
|
|
|
95
95
|
*/
|
|
96
96
|
function createTtscPlugin(context) {
|
|
97
97
|
rejectUnsupportedEntryKeys(context.plugin);
|
|
98
|
-
const
|
|
98
|
+
const resolvedConfig = resolveConfigFileContributors(context);
|
|
99
99
|
// Build the descriptor without a `contributors` key when none were
|
|
100
100
|
// declared, so consumers (and the existing key-shape regression
|
|
101
101
|
// tests) see the same surface as before this feature shipped.
|
|
102
102
|
const descriptor = {
|
|
103
103
|
capabilities: {
|
|
104
104
|
diagnosticsTiming: true,
|
|
105
|
+
graphNodes: true,
|
|
105
106
|
lsp: true,
|
|
106
107
|
projectContextArgs: true,
|
|
107
108
|
projectDiagnostics: true,
|
|
@@ -109,6 +110,9 @@ function createTtscPlugin(context) {
|
|
|
109
110
|
residentCheck: true,
|
|
110
111
|
threadingArgs: true,
|
|
111
112
|
},
|
|
113
|
+
hostInputHashes: resolvedConfig.hostInputHashes,
|
|
114
|
+
hostInputRealpaths: resolvedConfig.hostInputRealpaths,
|
|
115
|
+
hostInputs: resolvedConfig.hostInputs,
|
|
112
116
|
name: "@ttsc/lint",
|
|
113
117
|
reportsTypeScriptDiagnostics: true,
|
|
114
118
|
// `context.dirname` is this descriptor's own directory in every load mode —
|
|
@@ -117,8 +121,8 @@ function createTtscPlugin(context) {
|
|
|
117
121
|
source: node_path_1.default.resolve(context.dirname, "..", "plugin"),
|
|
118
122
|
stage: "check",
|
|
119
123
|
};
|
|
120
|
-
if (contributors.length > 0) {
|
|
121
|
-
descriptor.contributors = contributors;
|
|
124
|
+
if (resolvedConfig.contributors.length > 0) {
|
|
125
|
+
descriptor.contributors = resolvedConfig.contributors;
|
|
122
126
|
}
|
|
123
127
|
return descriptor;
|
|
124
128
|
}
|
|
@@ -163,12 +167,28 @@ function loadContributorPluginViaRequire(specifier, context, namespace, anchorFi
|
|
|
163
167
|
*/
|
|
164
168
|
function resolveConfigFileContributors(context) {
|
|
165
169
|
const configFile = readConfigFileOption(context);
|
|
166
|
-
const
|
|
167
|
-
?
|
|
168
|
-
:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
const explicitConfigPath = configFile === undefined
|
|
171
|
+
? undefined
|
|
172
|
+
: node_path_1.default.resolve(pluginConfigBaseDir(context), configFile);
|
|
173
|
+
const discovery = explicitConfigPath === undefined
|
|
174
|
+
? discoverLintConfigFile(context)
|
|
175
|
+
: {
|
|
176
|
+
configPath: explicitConfigPath,
|
|
177
|
+
hostInputHashes: hashHostInputPaths([explicitConfigPath]),
|
|
178
|
+
hostInputRealpaths: realpathHostInputPaths([explicitConfigPath]),
|
|
179
|
+
hostInputs: [explicitConfigPath],
|
|
180
|
+
};
|
|
181
|
+
const { configPath } = discovery;
|
|
182
|
+
if (!configPath || !node_fs_1.default.existsSync(configPath)) {
|
|
183
|
+
return {
|
|
184
|
+
contributors: [],
|
|
185
|
+
hostInputHashes: discovery.hostInputHashes,
|
|
186
|
+
hostInputRealpaths: discovery.hostInputRealpaths,
|
|
187
|
+
hostInputs: discovery.hostInputs,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const evaluation = readConfigPluginEntries(configPath, context);
|
|
191
|
+
const entries = evaluation.entries;
|
|
172
192
|
assertContributorNamespacesDoNotCollide(entries, configPath);
|
|
173
193
|
// Dedup exact repeated namespaces on the Go-subpackage form. Config-array
|
|
174
194
|
// folding can surface the same namespace more than once; that existing
|
|
@@ -182,7 +202,157 @@ function resolveConfigFileContributors(context) {
|
|
|
182
202
|
occupied.add(goName);
|
|
183
203
|
out.push({ name: goName, source: entry.source });
|
|
184
204
|
}
|
|
185
|
-
|
|
205
|
+
const dependencyInputs = evaluation.dependencies
|
|
206
|
+
.filter((dependency) => dependency.scope === "watch" && dependency.kind !== "directory")
|
|
207
|
+
.map((dependency) => dependency.path);
|
|
208
|
+
const hostInputHashes = { ...discovery.hostInputHashes };
|
|
209
|
+
const hostInputRealpaths = { ...discovery.hostInputRealpaths };
|
|
210
|
+
const unstableRealpaths = new Set();
|
|
211
|
+
const unprovenInputs = new Set();
|
|
212
|
+
const missingOptionalDigest = (0, node_crypto_1.createHash)("sha256")
|
|
213
|
+
.update("missing\0")
|
|
214
|
+
.digest("hex");
|
|
215
|
+
const directoryCandidateDigest = (0, node_crypto_1.createHash)("sha256")
|
|
216
|
+
.update("ttsc:host-input:directory\0")
|
|
217
|
+
.digest("hex");
|
|
218
|
+
for (const dependency of evaluation.dependencies) {
|
|
219
|
+
if (dependency.scope !== "watch" || dependency.kind === "directory") {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const input = node_path_1.default.resolve(dependency.path);
|
|
223
|
+
const realpath = dependency.realpath;
|
|
224
|
+
if (!dependency.identityStable) {
|
|
225
|
+
delete hostInputRealpaths[input];
|
|
226
|
+
delete hostInputHashes[input];
|
|
227
|
+
unstableRealpaths.add(input);
|
|
228
|
+
unprovenInputs.add(input);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (Object.prototype.hasOwnProperty.call(hostInputRealpaths, input) &&
|
|
232
|
+
hostInputRealpaths[input] !== realpath) {
|
|
233
|
+
delete hostInputRealpaths[input];
|
|
234
|
+
delete hostInputHashes[input];
|
|
235
|
+
unstableRealpaths.add(input);
|
|
236
|
+
}
|
|
237
|
+
else if (!unstableRealpaths.has(input)) {
|
|
238
|
+
hostInputRealpaths[input] = realpath;
|
|
239
|
+
}
|
|
240
|
+
let hash;
|
|
241
|
+
if (dependency.kind === "file" &&
|
|
242
|
+
/^[0-9a-f]{64}$/.test(dependency.digest)) {
|
|
243
|
+
hash = dependency.digest;
|
|
244
|
+
}
|
|
245
|
+
else if (dependency.digest === missingOptionalDigest) {
|
|
246
|
+
// The evaluator's optional-file digest includes a state prefix. The
|
|
247
|
+
// public host-input contract uses null for the observed missing state.
|
|
248
|
+
hash = null;
|
|
249
|
+
}
|
|
250
|
+
else if (dependency.digest === directoryCandidateDigest) {
|
|
251
|
+
// A path that is currently a directory is still an exact file candidate:
|
|
252
|
+
// replacing it with a file changes module/config selection.
|
|
253
|
+
hash = directoryCandidateDigest;
|
|
254
|
+
}
|
|
255
|
+
if (hash === undefined) {
|
|
256
|
+
delete hostInputHashes[input];
|
|
257
|
+
unprovenInputs.add(input);
|
|
258
|
+
}
|
|
259
|
+
else if (unprovenInputs.has(input)) {
|
|
260
|
+
// A later observation cannot revive proof another evaluation stage
|
|
261
|
+
// could not provide for the same combined descriptor result.
|
|
262
|
+
}
|
|
263
|
+
else if (Object.prototype.hasOwnProperty.call(hostInputHashes, input) &&
|
|
264
|
+
hostInputHashes[input] !== hash) {
|
|
265
|
+
delete hostInputHashes[input];
|
|
266
|
+
unprovenInputs.add(input);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
hostInputHashes[input] = hash;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
contributors: out,
|
|
274
|
+
hostInputHashes,
|
|
275
|
+
hostInputRealpaths,
|
|
276
|
+
hostInputs: [...discovery.hostInputs, ...dependencyInputs],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/** Snapshot config candidates before any discovery/evaluation side effect. */
|
|
280
|
+
function hashHostInputPaths(inputs) {
|
|
281
|
+
return Object.fromEntries(inputs.map((input) => {
|
|
282
|
+
const file = node_path_1.default.resolve(input);
|
|
283
|
+
try {
|
|
284
|
+
if (node_fs_1.default.statSync(file).isDirectory()) {
|
|
285
|
+
return [
|
|
286
|
+
file,
|
|
287
|
+
(0, node_crypto_1.createHash)("sha256")
|
|
288
|
+
.update("ttsc:host-input:directory\0")
|
|
289
|
+
.digest("hex"),
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
return [
|
|
293
|
+
file,
|
|
294
|
+
(0, node_crypto_1.createHash)("sha256").update(node_fs_1.default.readFileSync(file)).digest("hex"),
|
|
295
|
+
];
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return [file, null];
|
|
299
|
+
}
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
function hostInputRealpath(file) {
|
|
303
|
+
try {
|
|
304
|
+
return node_fs_1.default.realpathSync.native(file);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function realpathHostInputPaths(inputs) {
|
|
311
|
+
return Object.fromEntries(inputs.map((input) => {
|
|
312
|
+
const file = node_path_1.default.resolve(input);
|
|
313
|
+
return [file, hostInputRealpath(file)];
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
/** Mirror native discovery and fingerprint every candidate before selecting. */
|
|
317
|
+
function discoverLintConfigFile(context) {
|
|
318
|
+
const hostInputHashes = {};
|
|
319
|
+
const hostInputRealpaths = {};
|
|
320
|
+
const hostInputs = [];
|
|
321
|
+
const recordCandidates = (candidates) => {
|
|
322
|
+
for (const candidate of candidates) {
|
|
323
|
+
const absolute = node_path_1.default.resolve(candidate);
|
|
324
|
+
if (Object.prototype.hasOwnProperty.call(hostInputHashes, absolute)) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
hostInputs.push(absolute);
|
|
328
|
+
Object.assign(hostInputHashes, hashHostInputPaths([absolute]));
|
|
329
|
+
Object.assign(hostInputRealpaths, realpathHostInputPaths([absolute]));
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
for (const origin of discoveryConfigBaseDirs(context)) {
|
|
333
|
+
for (let directory = origin;; directory = node_path_1.default.dirname(directory)) {
|
|
334
|
+
const candidates = LINT_CONFIG_FILENAMES.map((name) => node_path_1.default.join(directory, name));
|
|
335
|
+
recordCandidates(candidates);
|
|
336
|
+
const matches = lintConfigMatchesIn(directory);
|
|
337
|
+
if (matches.length === 1) {
|
|
338
|
+
return {
|
|
339
|
+
configPath: matches[0],
|
|
340
|
+
hostInputHashes,
|
|
341
|
+
hostInputRealpaths,
|
|
342
|
+
hostInputs,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (matches.length > 1) {
|
|
346
|
+
throw new Error(`@ttsc/lint: multiple lint config files found in ${directory} (${matches
|
|
347
|
+
.map((file) => node_path_1.default.basename(file))
|
|
348
|
+
.join(", ")}); set "configFile" explicitly`);
|
|
349
|
+
}
|
|
350
|
+
const parent = node_path_1.default.dirname(directory);
|
|
351
|
+
if (parent === directory)
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return { hostInputHashes, hostInputRealpaths, hostInputs };
|
|
186
356
|
}
|
|
187
357
|
function assertContributorNamespacesDoNotCollide(entries, configPath) {
|
|
188
358
|
const namespacesByGoName = new Map();
|
|
@@ -237,23 +407,6 @@ function readConfigFileOption(context) {
|
|
|
237
407
|
}
|
|
238
408
|
return value;
|
|
239
409
|
}
|
|
240
|
-
function findLintConfigFile(context) {
|
|
241
|
-
// Mirror the Go side (driver.PluginConfigBaseDir): the caller-declared
|
|
242
|
-
// pluginConfigDir is the single walk origin when present — it names the
|
|
243
|
-
// real project when the resolved tsconfig is a generated wrapper in a temp
|
|
244
|
-
// dir (@ttsc/unplugin's alias overlay), and it keeps the wrapper's temp
|
|
245
|
-
// ancestry out of the walk so a stray config planted there is never
|
|
246
|
-
// honored. Otherwise walk upward from the tsconfig directory first, then
|
|
247
|
-
// fall back to the working directory: that covers callers that point at an
|
|
248
|
-
// out-of-tree tsconfig without declaring an anchor.
|
|
249
|
-
for (const origin of discoveryConfigBaseDirs(context)) {
|
|
250
|
-
const discovered = findLintConfigFileFrom(origin);
|
|
251
|
-
if (discovered !== undefined) {
|
|
252
|
-
return discovered;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return undefined;
|
|
256
|
-
}
|
|
257
410
|
function discoveryConfigBaseDirs(context) {
|
|
258
411
|
if (context.pluginConfigDir) {
|
|
259
412
|
return [node_path_1.default.resolve(context.cwd ?? ".", context.pluginConfigDir)];
|
|
@@ -262,45 +415,25 @@ function discoveryConfigBaseDirs(context) {
|
|
|
262
415
|
const cwd = node_path_1.default.resolve(context.cwd ?? context.projectRoot);
|
|
263
416
|
return tsconfigDir === cwd ? [tsconfigDir] : [tsconfigDir, cwd];
|
|
264
417
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
// the binary's own discovery to surface the issue once with one canonical
|
|
271
|
-
// message).
|
|
272
|
-
const candidateSet = new Set(LINT_CONFIG_FILENAMES);
|
|
273
|
-
let dir = origin;
|
|
274
|
-
while (true) {
|
|
275
|
-
// One `readdirSync` per directory level beats 14 `existsSync`+
|
|
276
|
-
// `statSync` pairs (= 28 stat syscalls) per level; intersect the
|
|
277
|
-
// listing with the candidate set instead.
|
|
278
|
-
let entries;
|
|
418
|
+
/** Return the non-directory candidates native discovery recognizes. */
|
|
419
|
+
function lintConfigMatchesIn(directory) {
|
|
420
|
+
const matches = [];
|
|
421
|
+
for (const name of LINT_CONFIG_FILENAMES) {
|
|
422
|
+
const candidate = node_path_1.default.join(directory, name);
|
|
279
423
|
try {
|
|
280
|
-
|
|
424
|
+
// Go's os.Stat follows symlinks and junctions. Follow them here too so a
|
|
425
|
+
// directory or dangling link cannot stop descriptor discovery before
|
|
426
|
+
// the native host reaches a valid ancestor config. Probing the canonical
|
|
427
|
+
// candidate spelling also preserves Go's behavior on case-insensitive
|
|
428
|
+
// filesystems when the directory entry uses different casing.
|
|
429
|
+
if (!node_fs_1.default.statSync(candidate).isDirectory())
|
|
430
|
+
matches.push(candidate);
|
|
281
431
|
}
|
|
282
432
|
catch {
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
const matches = [];
|
|
286
|
-
for (const entry of entries) {
|
|
287
|
-
if (!candidateSet.has(entry.name))
|
|
288
|
-
continue;
|
|
289
|
-
if (!entry.isFile() && !entry.isSymbolicLink())
|
|
290
|
-
continue;
|
|
291
|
-
matches.push(node_path_1.default.join(dir, entry.name));
|
|
292
|
-
}
|
|
293
|
-
if (matches.length === 1) {
|
|
294
|
-
return matches[0];
|
|
433
|
+
// Missing, dangling, and unreadable candidates are not native matches.
|
|
295
434
|
}
|
|
296
|
-
if (matches.length > 1) {
|
|
297
|
-
return undefined; // ambiguous — defer to the Go side's error
|
|
298
|
-
}
|
|
299
|
-
const parent = node_path_1.default.dirname(dir);
|
|
300
|
-
if (parent === dir)
|
|
301
|
-
return undefined;
|
|
302
|
-
dir = parent;
|
|
303
435
|
}
|
|
436
|
+
return matches;
|
|
304
437
|
}
|
|
305
438
|
/**
|
|
306
439
|
* Base directory for resolving a relative `configFile` from the tsconfig plugin
|
|
@@ -332,8 +465,9 @@ function readConfigPluginEntries(configPath, context) {
|
|
|
332
465
|
// is a real subprocess, and a host that only wanted to know whether there
|
|
333
466
|
// were contributors would otherwise depend on a launcher being resolvable and
|
|
334
467
|
// on a compiler accepting one more invocation.
|
|
335
|
-
if (jsonConfigDeclaresNoContributor(configPath))
|
|
336
|
-
return [];
|
|
468
|
+
if (jsonConfigDeclaresNoContributor(configPath)) {
|
|
469
|
+
return { dependencies: [], entries: [] };
|
|
470
|
+
}
|
|
337
471
|
// Every other config uses the same isolated evaluator. Executable config can
|
|
338
472
|
// name contributor packages whose top-level code writes to stdout, so loading
|
|
339
473
|
// it in this host process would corrupt CLI JSON or preface the first LSP
|
|
@@ -417,9 +551,11 @@ const CONFIG_KEYS = new Set<string>([
|
|
|
417
551
|
]);
|
|
418
552
|
const dependencies = new Map<string, {
|
|
419
553
|
digest: string;
|
|
554
|
+
identityStable: boolean;
|
|
420
555
|
kind: "directory" | "file" | "optional-file";
|
|
421
556
|
path: string;
|
|
422
557
|
owners: Set<string>;
|
|
558
|
+
realpath: string | null;
|
|
423
559
|
}>();
|
|
424
560
|
const graphNodes = new Map<string, string>();
|
|
425
561
|
const graphEdges: Array<{
|
|
@@ -450,6 +586,23 @@ const configUrlSpellings = [
|
|
|
450
586
|
pathToFileURL(realConfigLocation()).href,
|
|
451
587
|
]),
|
|
452
588
|
];
|
|
589
|
+
const moduleProbeExtensions = [
|
|
590
|
+
".ts",
|
|
591
|
+
".tsx",
|
|
592
|
+
".mts",
|
|
593
|
+
".cts",
|
|
594
|
+
".js",
|
|
595
|
+
".mjs",
|
|
596
|
+
".cjs",
|
|
597
|
+
".json",
|
|
598
|
+
".node",
|
|
599
|
+
] as const;
|
|
600
|
+
const jsToTsProbeExtensions = new Map<string, readonly string[]>([
|
|
601
|
+
[".js", [".ts", ".tsx"]],
|
|
602
|
+
[".jsx", [".tsx"]],
|
|
603
|
+
[".mjs", [".mts"]],
|
|
604
|
+
[".cjs", [".cts"]],
|
|
605
|
+
]);
|
|
453
606
|
for (const spelling of configUrlSpellings) {
|
|
454
607
|
graphNodes.set(spelling, configLocation);
|
|
455
608
|
}
|
|
@@ -471,6 +624,11 @@ declare const process: {
|
|
|
471
624
|
|
|
472
625
|
const hooks = registerHooks({
|
|
473
626
|
resolve(specifier, context, nextResolve) {
|
|
627
|
+
const requestedParent =
|
|
628
|
+
context.parentURL && new URL(context.parentURL).href;
|
|
629
|
+
if (requestedParent !== undefined && graphNodes.has(requestedParent)) {
|
|
630
|
+
recordLocalResolutionCandidates(specifier, requestedParent);
|
|
631
|
+
}
|
|
474
632
|
const resolved = nextResolve(specifier, context);
|
|
475
633
|
if (typeof resolved.url !== "string" || !resolved.url.startsWith("file:")) {
|
|
476
634
|
return resolved;
|
|
@@ -603,14 +761,32 @@ function recordDependency(
|
|
|
603
761
|
const previous = dependencies.get(key);
|
|
604
762
|
const mergedOwners = previous?.owners ?? new Set<string>();
|
|
605
763
|
for (const owner of owners) mergedOwners.add(owner);
|
|
764
|
+
const realpath = dependencyRealpath(location);
|
|
765
|
+
const identityStable =
|
|
766
|
+
previous?.identityStable !== false &&
|
|
767
|
+
(previous === undefined || previous.realpath === realpath);
|
|
606
768
|
dependencies.set(key, {
|
|
607
|
-
digest:
|
|
769
|
+
digest:
|
|
770
|
+
!identityStable ||
|
|
771
|
+
(previous !== undefined && previous.digest !== digest)
|
|
772
|
+
? ""
|
|
773
|
+
: digest,
|
|
774
|
+
identityStable,
|
|
608
775
|
kind,
|
|
609
776
|
owners: mergedOwners,
|
|
610
777
|
path: location,
|
|
778
|
+
realpath,
|
|
611
779
|
});
|
|
612
780
|
}
|
|
613
781
|
|
|
782
|
+
function dependencyRealpath(location: string): string | null {
|
|
783
|
+
try {
|
|
784
|
+
return realPath(location);
|
|
785
|
+
} catch {
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
614
790
|
function isLocalModuleSpecifier(specifier: string): boolean {
|
|
615
791
|
return specifier.startsWith(".") ||
|
|
616
792
|
specifier.startsWith("/") ||
|
|
@@ -728,13 +904,19 @@ function directoryDigestRecord(
|
|
|
728
904
|
|
|
729
905
|
function optionalFileDigest(location: string): string {
|
|
730
906
|
try {
|
|
731
|
-
|
|
907
|
+
const entry = fs.statSync(location);
|
|
908
|
+
if (entry.isFile()) {
|
|
732
909
|
return createHash("sha256")
|
|
733
910
|
.update(Buffer.concat([Buffer.from("file\\0"), fs.readFileSync(location)]))
|
|
734
911
|
.digest("hex");
|
|
735
912
|
}
|
|
913
|
+
if (entry.isDirectory()) {
|
|
914
|
+
return createHash("sha256")
|
|
915
|
+
.update("ttsc:host-input:directory\\0")
|
|
916
|
+
.digest("hex");
|
|
917
|
+
}
|
|
736
918
|
} catch {
|
|
737
|
-
// Missing
|
|
919
|
+
// Missing and unreadable candidates share the absent state.
|
|
738
920
|
}
|
|
739
921
|
return createHash("sha256").update("missing\\0").digest("hex");
|
|
740
922
|
}
|
|
@@ -760,6 +942,80 @@ function recordOptionalFileDependency(
|
|
|
760
942
|
return false;
|
|
761
943
|
}
|
|
762
944
|
|
|
945
|
+
function moduleResolutionCandidates(base: string): string[] {
|
|
946
|
+
const extension = path.extname(base).toLowerCase();
|
|
947
|
+
const substitutions = jsToTsProbeExtensions.get(extension) ?? [];
|
|
948
|
+
const stem = base.slice(0, base.length - extension.length);
|
|
949
|
+
return [
|
|
950
|
+
base,
|
|
951
|
+
...substitutions.map((candidate) => stem + candidate),
|
|
952
|
+
...moduleProbeExtensions.map((candidate) => base + candidate),
|
|
953
|
+
path.join(base, "package.json"),
|
|
954
|
+
...moduleProbeExtensions.map((candidate) =>
|
|
955
|
+
path.join(base, "index" + candidate),
|
|
956
|
+
),
|
|
957
|
+
];
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/** Record exact local probes before the runtime resolver chooses one. */
|
|
961
|
+
function recordLocalResolutionCandidates(
|
|
962
|
+
specifier: string,
|
|
963
|
+
parentUrl: string,
|
|
964
|
+
): void {
|
|
965
|
+
if (!isLocalModuleSpecifier(specifier)) return;
|
|
966
|
+
let bases: string[];
|
|
967
|
+
try {
|
|
968
|
+
if (specifier.startsWith("file:")) {
|
|
969
|
+
bases = [fileURLToPath(specifier)];
|
|
970
|
+
} else {
|
|
971
|
+
const directory = path.dirname(fileURLToPath(parentUrl));
|
|
972
|
+
const raw = path.resolve(directory, specifier);
|
|
973
|
+
const suffixStart = specifier.search(/[?#]/);
|
|
974
|
+
const pathname =
|
|
975
|
+
suffixStart === -1 ? specifier : specifier.slice(0, suffixStart);
|
|
976
|
+
bases = pathname === ""
|
|
977
|
+
? [raw]
|
|
978
|
+
: [...new Set([raw, path.resolve(directory, pathname)])];
|
|
979
|
+
}
|
|
980
|
+
} catch {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const owners = [parentUrl];
|
|
984
|
+
for (const base of bases) {
|
|
985
|
+
try {
|
|
986
|
+
if (fs.statSync(base).isFile()) {
|
|
987
|
+
recordOptionalFileDependency(base, owners);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
} catch {
|
|
991
|
+
// A missing exact spelling falls through to source/extension/directory
|
|
992
|
+
// probes, all of which can redirect a later evaluation.
|
|
993
|
+
}
|
|
994
|
+
for (const candidate of moduleResolutionCandidates(base)) {
|
|
995
|
+
recordOptionalFileDependency(candidate, owners);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/** CommonJS LOAD_AS_FILE / LOAD_AS_DIRECTORY candidates for one legacy path. */
|
|
1001
|
+
function recordLegacyPackagePathCandidates(
|
|
1002
|
+
candidate: string,
|
|
1003
|
+
owners: readonly string[],
|
|
1004
|
+
): void {
|
|
1005
|
+
for (const file of [
|
|
1006
|
+
candidate,
|
|
1007
|
+
candidate + ".js",
|
|
1008
|
+
candidate + ".json",
|
|
1009
|
+
candidate + ".node",
|
|
1010
|
+
path.join(candidate, "package.json"),
|
|
1011
|
+
path.join(candidate, "index.js"),
|
|
1012
|
+
path.join(candidate, "index.json"),
|
|
1013
|
+
path.join(candidate, "index.node"),
|
|
1014
|
+
]) {
|
|
1015
|
+
recordOptionalFileDependency(file, owners);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
763
1019
|
function recordPackageManifests(
|
|
764
1020
|
location: string,
|
|
765
1021
|
owners: readonly string[],
|
|
@@ -805,26 +1061,26 @@ function recordNodeModulesSearchDirectories(
|
|
|
805
1061
|
// The directory digest of node_modules records a missing scope.
|
|
806
1062
|
}
|
|
807
1063
|
}
|
|
808
|
-
if (packageName !== undefined) {
|
|
809
|
-
const selected = recordPackageCandidateTopology(
|
|
810
|
-
modules,
|
|
811
|
-
packageName,
|
|
812
|
-
specifier,
|
|
813
|
-
childLocation,
|
|
814
|
-
owners,
|
|
815
|
-
conditions,
|
|
816
|
-
);
|
|
817
|
-
if (
|
|
818
|
-
selected ||
|
|
819
|
-
resolvedPackageContains(modules, packageName, childLocation)
|
|
820
|
-
) {
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
1064
|
}
|
|
825
1065
|
} catch {
|
|
826
1066
|
// Missing search levels do not participate in the current resolution.
|
|
827
1067
|
}
|
|
1068
|
+
if (packageName !== undefined) {
|
|
1069
|
+
const selected = recordPackageCandidateTopology(
|
|
1070
|
+
modules,
|
|
1071
|
+
packageName,
|
|
1072
|
+
specifier,
|
|
1073
|
+
childLocation,
|
|
1074
|
+
owners,
|
|
1075
|
+
conditions,
|
|
1076
|
+
);
|
|
1077
|
+
if (
|
|
1078
|
+
selected ||
|
|
1079
|
+
resolvedPackageContains(modules, packageName, childLocation)
|
|
1080
|
+
) {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
828
1084
|
if (
|
|
829
1085
|
packageName === undefined &&
|
|
830
1086
|
samePhysicalPath(current, resolutionRoot)
|
|
@@ -846,14 +1102,32 @@ function recordPackageCandidateTopology(
|
|
|
846
1102
|
conditions: readonly string[],
|
|
847
1103
|
): boolean {
|
|
848
1104
|
const packageRoot = path.join(modules, packageName);
|
|
1105
|
+
const subpath = specifier
|
|
1106
|
+
.slice(packageName.length)
|
|
1107
|
+
.replace(/^[/\\\\]+/, "");
|
|
849
1108
|
try {
|
|
850
|
-
if (!fs.statSync(packageRoot).isDirectory())
|
|
1109
|
+
if (!fs.statSync(packageRoot).isDirectory()) {
|
|
1110
|
+
recordOptionalFileDependency(
|
|
1111
|
+
path.join(packageRoot, "package.json"),
|
|
1112
|
+
owners,
|
|
1113
|
+
);
|
|
1114
|
+
recordLegacyPackagePathCandidates(
|
|
1115
|
+
subpath === "" ? packageRoot : path.join(packageRoot, subpath),
|
|
1116
|
+
owners,
|
|
1117
|
+
);
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
851
1120
|
} catch {
|
|
1121
|
+
recordOptionalFileDependency(
|
|
1122
|
+
path.join(packageRoot, "package.json"),
|
|
1123
|
+
owners,
|
|
1124
|
+
);
|
|
1125
|
+
recordLegacyPackagePathCandidates(
|
|
1126
|
+
subpath === "" ? packageRoot : path.join(packageRoot, subpath),
|
|
1127
|
+
owners,
|
|
1128
|
+
);
|
|
852
1129
|
return false;
|
|
853
1130
|
}
|
|
854
|
-
const subpath = specifier
|
|
855
|
-
.slice(packageName.length)
|
|
856
|
-
.replace(/^[/\\\\]+/, "");
|
|
857
1131
|
const rootTopology = recordPackageRootTopology(
|
|
858
1132
|
packageRoot,
|
|
859
1133
|
owners,
|
|
@@ -888,6 +1162,7 @@ function recordPackageRootTopology(
|
|
|
888
1162
|
const legacySelected = (): boolean =>
|
|
889
1163
|
useMain &&
|
|
890
1164
|
packagePathCandidateMatchesChild(normalizedRoot, childLocation, true);
|
|
1165
|
+
if (useMain) recordLegacyPackagePathCandidates(normalizedRoot, owners);
|
|
891
1166
|
if (!recordOptionalFileDependency(manifest, owners)) {
|
|
892
1167
|
const selected = legacySelected();
|
|
893
1168
|
if (!selected) {
|
|
@@ -934,6 +1209,7 @@ function recordPackageRootTopology(
|
|
|
934
1209
|
// it literally and permits absolute paths and paths outside the package.
|
|
935
1210
|
const main = path.resolve(normalizedRoot, metadata.main);
|
|
936
1211
|
recordPackagePathCandidate(main, owners);
|
|
1212
|
+
recordLegacyPackagePathCandidates(main, owners);
|
|
937
1213
|
selected =
|
|
938
1214
|
packagePathCandidateMatchesChild(main, childLocation, true) ||
|
|
939
1215
|
selected;
|
|
@@ -1181,6 +1457,7 @@ function recordPackageSubpathTopology(
|
|
|
1181
1457
|
const candidate = boundedPackageTarget(packageRoot, subpath);
|
|
1182
1458
|
if (candidate === undefined) return false;
|
|
1183
1459
|
recordPackagePathCandidate(candidate, owners);
|
|
1460
|
+
recordLegacyPackagePathCandidates(candidate, owners);
|
|
1184
1461
|
let selected = packagePathCandidateMatchesChild(
|
|
1185
1462
|
candidate,
|
|
1186
1463
|
childLocation,
|
|
@@ -1200,6 +1477,7 @@ function recordPackageSubpathTopology(
|
|
|
1200
1477
|
if (typeof metadata.main === "string") {
|
|
1201
1478
|
const main = path.resolve(candidate, metadata.main);
|
|
1202
1479
|
recordPackagePathCandidate(main, owners);
|
|
1480
|
+
recordLegacyPackagePathCandidates(main, owners);
|
|
1203
1481
|
selected =
|
|
1204
1482
|
packagePathCandidateMatchesChild(main, childLocation, true) ||
|
|
1205
1483
|
selected;
|
|
@@ -1370,10 +1648,25 @@ function realPath(location: string): string {
|
|
|
1370
1648
|
|
|
1371
1649
|
function finalizeDependencies(): Array<{
|
|
1372
1650
|
digest: string;
|
|
1651
|
+
identityStable: boolean;
|
|
1373
1652
|
kind: "directory" | "file" | "optional-file";
|
|
1374
1653
|
path: string;
|
|
1654
|
+
realpath: string | null;
|
|
1375
1655
|
scope: "cache" | "watch";
|
|
1376
1656
|
}> {
|
|
1657
|
+
// The evaluator may have observed a dependency, run arbitrary config code,
|
|
1658
|
+
// and then serialize after that path changed again. Re-read every recorded
|
|
1659
|
+
// dependency under its original ownership set so an A -> B -> A transition
|
|
1660
|
+
// is marked identity-unstable instead of pairing transient output with the
|
|
1661
|
+
// restored fingerprint.
|
|
1662
|
+
for (const dependency of [...dependencies.values()]) {
|
|
1663
|
+
recordDependency(
|
|
1664
|
+
dependency.kind,
|
|
1665
|
+
dependency.path,
|
|
1666
|
+
currentDependencyDigest(dependency.kind, dependency.path),
|
|
1667
|
+
[...dependency.owners],
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1377
1670
|
const watched = graphWatchReachability();
|
|
1378
1671
|
return [...dependencies.values()].map(({ owners, ...dependency }) => ({
|
|
1379
1672
|
...dependency,
|
|
@@ -1383,6 +1676,21 @@ function finalizeDependencies(): Array<{
|
|
|
1383
1676
|
}));
|
|
1384
1677
|
}
|
|
1385
1678
|
|
|
1679
|
+
function currentDependencyDigest(
|
|
1680
|
+
kind: "directory" | "file" | "optional-file",
|
|
1681
|
+
location: string,
|
|
1682
|
+
): string {
|
|
1683
|
+
try {
|
|
1684
|
+
if (kind === "directory") return directoryDigest(location);
|
|
1685
|
+
if (kind === "optional-file") return optionalFileDigest(location);
|
|
1686
|
+
return createHash("sha256")
|
|
1687
|
+
.update(fs.readFileSync(location))
|
|
1688
|
+
.digest("hex");
|
|
1689
|
+
} catch {
|
|
1690
|
+
return "";
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1386
1694
|
function graphWatchReachability(): Set<string> {
|
|
1387
1695
|
const adjacency = new Map<string, typeof graphEdges>();
|
|
1388
1696
|
for (const edge of graphEdges) {
|
|
@@ -1547,7 +1855,7 @@ function readTtsxConfigPlugins(configPath, context) {
|
|
|
1547
1855
|
if (cached &&
|
|
1548
1856
|
cached.entries.every(isValidConfigPluginEntry) &&
|
|
1549
1857
|
configDependenciesAreCurrent(cached.dependencies)) {
|
|
1550
|
-
return cached
|
|
1858
|
+
return cached;
|
|
1551
1859
|
}
|
|
1552
1860
|
}
|
|
1553
1861
|
// A config can be saved while it is being evaluated. Retry a bounded number
|
|
@@ -1560,10 +1868,10 @@ function readTtsxConfigPlugins(configPath, context) {
|
|
|
1560
1868
|
if (configDependenciesAreCurrent(evaluation.dependencies)) {
|
|
1561
1869
|
if (cacheKey)
|
|
1562
1870
|
writeConfigPluginCache(cacheKey, evaluation);
|
|
1563
|
-
return evaluation
|
|
1871
|
+
return evaluation;
|
|
1564
1872
|
}
|
|
1565
1873
|
}
|
|
1566
|
-
return evaluation
|
|
1874
|
+
return evaluation;
|
|
1567
1875
|
}
|
|
1568
1876
|
/**
|
|
1569
1877
|
* Reports whether a cached plugin entry is still usable: a well-formed
|
|
@@ -1590,7 +1898,7 @@ function isValidConfigPluginEntry(entry) {
|
|
|
1590
1898
|
}
|
|
1591
1899
|
}
|
|
1592
1900
|
function evaluateTtsxConfigPlugins(configPath, context) {
|
|
1593
|
-
const tempDir =
|
|
1901
|
+
const tempDir = createCanonicalTempDirectory("ttsc-lint-cfg-", loaderTempBase(configPath));
|
|
1594
1902
|
try {
|
|
1595
1903
|
linkNearestNodeModules(tempDir, node_path_1.default.dirname(configPath));
|
|
1596
1904
|
const loaderPath = node_path_1.default.join(tempDir, "loader.mts");
|
|
@@ -1735,11 +2043,27 @@ function evaluateTtsxConfigPlugins(configPath, context) {
|
|
|
1735
2043
|
// ────────────────────────────────────────────────────────────────────────────
|
|
1736
2044
|
// Config cache (shared with the Go sidecar — packages/lint/linthost/config.go)
|
|
1737
2045
|
// ────────────────────────────────────────────────────────────────────────────
|
|
2046
|
+
/** Create evaluator storage beneath a frozen physical parent. */
|
|
2047
|
+
function createCanonicalTempDirectory(prefix, parent) {
|
|
2048
|
+
const physicalParent = node_fs_1.default.realpathSync.native(parent);
|
|
2049
|
+
if (!node_fs_1.default.lstatSync(physicalParent).isDirectory()) {
|
|
2050
|
+
throw new Error(`@ttsc/lint: temporary directory parent is not a directory: ${physicalParent}`);
|
|
2051
|
+
}
|
|
2052
|
+
const directory = node_fs_1.default.mkdtempSync(node_path_1.default.join(physicalParent, prefix));
|
|
2053
|
+
if (!node_fs_1.default.lstatSync(directory).isDirectory()) {
|
|
2054
|
+
throw new Error(`@ttsc/lint: temporary directory postflight is not a directory: ${directory}`);
|
|
2055
|
+
}
|
|
2056
|
+
const physicalDirectory = node_fs_1.default.realpathSync.native(directory);
|
|
2057
|
+
if (node_path_1.default.dirname(physicalDirectory) !== physicalParent) {
|
|
2058
|
+
throw new Error(`@ttsc/lint: temporary directory escaped its physical parent: ${physicalDirectory}`);
|
|
2059
|
+
}
|
|
2060
|
+
return physicalDirectory;
|
|
2061
|
+
}
|
|
1738
2062
|
/**
|
|
1739
2063
|
* Namespaces the on-disk config cache. Kept in lockstep with the Go sidecar's
|
|
1740
2064
|
* `configCacheVersion`; bump both when the cached shape changes.
|
|
1741
2065
|
*/
|
|
1742
|
-
const CONFIG_CACHE_VERSION = "
|
|
2066
|
+
const CONFIG_CACHE_VERSION = "v7";
|
|
1743
2067
|
/**
|
|
1744
2068
|
* Directory shared by this factory and the Go sidecar for cached lint configs.
|
|
1745
2069
|
* The two write different files (the `kind` segment of the cache key keeps
|
|
@@ -1848,6 +2172,12 @@ function normalizeConfigDependencyFingerprints(value) {
|
|
|
1848
2172
|
typeof candidate !== "object" ||
|
|
1849
2173
|
typeof candidate.path !== "string" ||
|
|
1850
2174
|
typeof candidate.digest !== "string" ||
|
|
2175
|
+
typeof candidate.identityStable !==
|
|
2176
|
+
"boolean" ||
|
|
2177
|
+
(candidate.realpath !== null &&
|
|
2178
|
+
(typeof candidate.realpath !==
|
|
2179
|
+
"string" ||
|
|
2180
|
+
!node_path_1.default.isAbsolute(candidate.realpath))) ||
|
|
1851
2181
|
!["directory", "file", "optional-file"].includes(candidate.kind) ||
|
|
1852
2182
|
!["cache", "watch"].includes(candidate.scope)) {
|
|
1853
2183
|
return undefined;
|
|
@@ -1855,22 +2185,36 @@ function normalizeConfigDependencyFingerprints(value) {
|
|
|
1855
2185
|
const candidatePath = candidate.path;
|
|
1856
2186
|
const digest = candidate.digest;
|
|
1857
2187
|
const kind = candidate.kind;
|
|
2188
|
+
const identityStable = candidate
|
|
2189
|
+
.identityStable;
|
|
2190
|
+
const realpath = candidate.realpath;
|
|
1858
2191
|
const scope = candidate.scope;
|
|
1859
|
-
if (!node_path_1.default.isAbsolute(candidatePath) ||
|
|
2192
|
+
if (!node_path_1.default.isAbsolute(candidatePath) ||
|
|
2193
|
+
(digest !== "" && !/^[0-9a-f]{64}$/.test(digest))) {
|
|
1860
2194
|
return undefined;
|
|
1861
2195
|
}
|
|
1862
2196
|
const location = node_path_1.default.resolve(candidatePath);
|
|
1863
|
-
|
|
2197
|
+
// The same lexical path can be both a traversed directory and an exact
|
|
2198
|
+
// file candidate. Those observations have different freshness contracts:
|
|
2199
|
+
// one fingerprints the listing, while the other fingerprints its entry
|
|
2200
|
+
// type. Preserve both, but still reject contradictory duplicates of one
|
|
2201
|
+
// kind.
|
|
2202
|
+
const key = kind + "\0" + location;
|
|
2203
|
+
const previous = dependencies.get(key);
|
|
1864
2204
|
if (previous !== undefined &&
|
|
1865
2205
|
(previous.digest !== digest ||
|
|
2206
|
+
previous.identityStable !== identityStable ||
|
|
1866
2207
|
previous.kind !== kind ||
|
|
2208
|
+
previous.realpath !== realpath ||
|
|
1867
2209
|
previous.scope !== scope)) {
|
|
1868
2210
|
return undefined;
|
|
1869
2211
|
}
|
|
1870
|
-
dependencies.set(
|
|
2212
|
+
dependencies.set(key, {
|
|
1871
2213
|
digest,
|
|
2214
|
+
identityStable,
|
|
1872
2215
|
kind,
|
|
1873
2216
|
path: location,
|
|
2217
|
+
realpath,
|
|
1874
2218
|
scope,
|
|
1875
2219
|
});
|
|
1876
2220
|
}
|
|
@@ -1880,6 +2224,10 @@ function configDependenciesAreCurrent(dependencies) {
|
|
|
1880
2224
|
if (dependencies.length === 0)
|
|
1881
2225
|
return false;
|
|
1882
2226
|
return dependencies.every((dependency) => {
|
|
2227
|
+
if (!dependency.identityStable ||
|
|
2228
|
+
dependency.realpath !== hostInputRealpath(dependency.path)) {
|
|
2229
|
+
return false;
|
|
2230
|
+
}
|
|
1883
2231
|
if (!/^[0-9a-f]{64}$/.test(dependency.digest))
|
|
1884
2232
|
return false;
|
|
1885
2233
|
try {
|
|
@@ -1950,14 +2298,20 @@ function configDirectoryDigestRecord(name, entry, target) {
|
|
|
1950
2298
|
}
|
|
1951
2299
|
function configOptionalFileDigest(location) {
|
|
1952
2300
|
try {
|
|
1953
|
-
|
|
2301
|
+
const entry = node_fs_1.default.statSync(location);
|
|
2302
|
+
if (entry.isFile()) {
|
|
1954
2303
|
return (0, node_crypto_1.createHash)("sha256")
|
|
1955
2304
|
.update(node_buffer_1.Buffer.concat([node_buffer_1.Buffer.from("file\0"), node_fs_1.default.readFileSync(location)]))
|
|
1956
2305
|
.digest("hex");
|
|
1957
2306
|
}
|
|
2307
|
+
if (entry.isDirectory()) {
|
|
2308
|
+
return (0, node_crypto_1.createHash)("sha256")
|
|
2309
|
+
.update("ttsc:host-input:directory\0")
|
|
2310
|
+
.digest("hex");
|
|
2311
|
+
}
|
|
1958
2312
|
}
|
|
1959
2313
|
catch {
|
|
1960
|
-
// Missing
|
|
2314
|
+
// Missing and unreadable candidates share the absent state.
|
|
1961
2315
|
}
|
|
1962
2316
|
return (0, node_crypto_1.createHash)("sha256").update("missing\0").digest("hex");
|
|
1963
2317
|
}
|
|
@@ -2177,14 +2531,6 @@ function loaderTempBase(configPath) {
|
|
|
2177
2531
|
}
|
|
2178
2532
|
return node_path_1.default.dirname(configPath);
|
|
2179
2533
|
}
|
|
2180
|
-
function realpathIfPossible(location) {
|
|
2181
|
-
try {
|
|
2182
|
-
return node_fs_1.default.realpathSync(location);
|
|
2183
|
-
}
|
|
2184
|
-
catch {
|
|
2185
|
-
return location;
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
2534
|
function nodeConfigLoaderEnv(configPath) {
|
|
2189
2535
|
const env = { ...process.env };
|
|
2190
2536
|
const parts = [];
|