@orkestrel/probe 0.0.11 → 0.0.13
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 +22 -11
- package/dist/bin/main.js +1 -1
- package/dist/bin/main.js.map +1 -1
- package/dist/src/core/index.cjs +77 -16
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +245 -147
- package/dist/src/core/index.d.ts +245 -147
- package/dist/src/core/index.js +75 -17
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +829 -375
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +531 -163
- package/dist/src/server/index.d.ts +531 -163
- package/dist/src/server/index.js +821 -375
- package/dist/src/server/index.js.map +1 -1
- package/package.json +20 -21
package/dist/src/server/index.js
CHANGED
|
@@ -4,14 +4,14 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
4
4
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { attempt, compileGuard, compileSchema, isArray, isRecord, schemaToParameters } from "@orkestrel/contract";
|
|
7
|
-
import { CLAIM_SHAPE, LINT_DEADLINE, PROBE_DEADLINE, PROBE_KEYS, ProbeError, computeReceipt, createDestroyedError, formatCheck, formatReceipt, formatSpecification, formatVerdict, isClaim, isDraft, isProbeError, isVerdict, matchesSpecification } from "../core/index.js";
|
|
7
|
+
import { CLAIM_SHAPE, LINT_DEADLINE, PROBE_DEADLINE, PROBE_KEYS, PROBE_SPECIFICATIONS, ProbeError, RUNTIME_PLUGIN, TYPE_MIRROR, computeReceipt, createDestroyedError, formatCheck, formatReceipt, formatSpecification, formatVerdict, isClaim, isDraft, isProbeError, isVerdict, matchesSpecification } from "../core/index.js";
|
|
8
8
|
import { Emitter } from "@orkestrel/emitter";
|
|
9
9
|
import { createQueue } from "@orkestrel/queue";
|
|
10
10
|
import { createTimeout } from "@orkestrel/timeout";
|
|
11
11
|
import { createLSPClient } from "@orkestrel/lsp";
|
|
12
12
|
import { createStdioClientTransport } from "@orkestrel/lsp/server";
|
|
13
13
|
import { PassThrough } from "node:stream";
|
|
14
|
-
import {
|
|
14
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
15
15
|
import { createMCPLegacy, createMCPServer, isBoundedJSON } from "@orkestrel/mcp";
|
|
16
16
|
import { createStdioServer } from "@orkestrel/mcp/server";
|
|
17
17
|
import { createTool, createToolManager } from "@orkestrel/tool";
|
|
@@ -37,6 +37,64 @@ function normalizePath(path) {
|
|
|
37
37
|
return path.replaceAll("\\", "/");
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
+
* Reads the condition code a native fault carries.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* A caller catches `unknown`, so every reading of a caught value's own property calls into code
|
|
44
|
+
* this package does not own: a proxy whose `has` trap throws and a value whose `code` getter throws
|
|
45
|
+
* both arrive here. The read is guarded, so a fault that refuses inspection reports no code rather
|
|
46
|
+
* than raising a second fault over the first. A `code` the host reports as anything other than a
|
|
47
|
+
* string is no code this package compares against, so it reports none either.
|
|
48
|
+
*
|
|
49
|
+
* One reading serves every site that classifies a native fault, so a host condition is admitted the
|
|
50
|
+
* same way wherever it is read.
|
|
51
|
+
*
|
|
52
|
+
* @param error - The caught value to read
|
|
53
|
+
* @returns The fault's `code` when it carries a string one; `undefined` otherwise
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* readFaultCode(Object.assign(new Error('no such file'), { code: 'ENOENT' })) // 'ENOENT'
|
|
58
|
+
* readFaultCode(new Error('the runtime stage was destroyed')) // undefined
|
|
59
|
+
* readFaultCode('ENOENT') // undefined
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
function readFaultCode(error) {
|
|
63
|
+
if (typeof error !== "object" || error === null) return void 0;
|
|
64
|
+
const fault = error;
|
|
65
|
+
const reading = attempt(() => "code" in fault ? fault.code : void 0);
|
|
66
|
+
if (!reading.success) return void 0;
|
|
67
|
+
return typeof reading.value === "string" ? reading.value : void 0;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Reports whether one path resolves outside the root it is read against.
|
|
71
|
+
*
|
|
72
|
+
* @remarks
|
|
73
|
+
* Containment decides which paths a claim may reach and which absolute paths a digest may rewrite,
|
|
74
|
+
* so one reading serves every site that asks the question. A target resolving to the root itself is
|
|
75
|
+
* contained, and each caller decides on its own what that empty relative path means for it.
|
|
76
|
+
*
|
|
77
|
+
* The test reads the relative path rather than a string prefix of the root, so a sibling directory
|
|
78
|
+
* whose name begins with the root's own text is outside rather than beneath it. An absolute
|
|
79
|
+
* relative path is the answer a host returns for two paths on different volumes, which no root
|
|
80
|
+
* contains.
|
|
81
|
+
*
|
|
82
|
+
* @param root - The root the target is read against
|
|
83
|
+
* @param target - The path to test, absolute or root-relative
|
|
84
|
+
* @returns True if the target resolves outside the root; false otherwise
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* escapesRoot('/srv/checkout', 'src/core/greeting.ts') // false
|
|
89
|
+
* escapesRoot('/srv/checkout', '../secrets.env') // true
|
|
90
|
+
* escapesRoot('/srv/checkout', '/srv/checkout-backup/secrets.env') // true
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
function escapesRoot(root, target) {
|
|
94
|
+
const path = relative(resolve(root), resolve(root, target));
|
|
95
|
+
return path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
40
98
|
* Resolves a path inside a target workspace and rejects traversal outside it.
|
|
41
99
|
*
|
|
42
100
|
* @remarks
|
|
@@ -64,7 +122,7 @@ function resolveWorkspaceFile(workspace, target, mutate = false) {
|
|
|
64
122
|
const root = resolve(workspace);
|
|
65
123
|
const file = resolve(root, target);
|
|
66
124
|
const path = relative(root, file);
|
|
67
|
-
if (path === "" ||
|
|
125
|
+
if (path === "" || escapesRoot(root, file)) throw new ProbeError(`Path escapes the workspace: ${target}`, {
|
|
68
126
|
origin: "claimant",
|
|
69
127
|
code: "refused",
|
|
70
128
|
context: { path: target }
|
|
@@ -77,18 +135,16 @@ function resolveWorkspaceFile(workspace, target, mutate = false) {
|
|
|
77
135
|
descendant = resolve(descendant, segment);
|
|
78
136
|
const outcome = attempt(() => lstatSync(descendant));
|
|
79
137
|
if (!outcome.success) {
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
throw error;
|
|
138
|
+
const code = readFaultCode(outcome.error);
|
|
139
|
+
if (code === "ENOENT" || code === "ENOTDIR") break;
|
|
140
|
+
throw outcome.error;
|
|
83
141
|
}
|
|
84
142
|
if (outcome.value.isSymbolicLink()) throw new ProbeError(`Path crosses a symbolic link: ${target}`, {
|
|
85
143
|
origin: "workspace",
|
|
86
144
|
code: "refused",
|
|
87
145
|
context: { path: target }
|
|
88
146
|
});
|
|
89
|
-
|
|
90
|
-
const remainder = relative(canonical, resolved);
|
|
91
|
-
if (remainder === ".." || remainder.startsWith(`..${sep}`) || isAbsolute(remainder)) throw new ProbeError(`Path escapes the workspace: ${target}`, {
|
|
147
|
+
if (escapesRoot(canonical, realpathSync(descendant))) throw new ProbeError(`Path escapes the workspace: ${target}`, {
|
|
92
148
|
origin: "claimant",
|
|
93
149
|
code: "refused",
|
|
94
150
|
context: { path: target }
|
|
@@ -96,7 +152,8 @@ function resolveWorkspaceFile(workspace, target, mutate = false) {
|
|
|
96
152
|
}
|
|
97
153
|
} catch (error) {
|
|
98
154
|
if (error instanceof ProbeError) throw error;
|
|
99
|
-
const
|
|
155
|
+
const code = readFaultCode(error);
|
|
156
|
+
const claimant = code === "ENAMETOOLONG" || code === "ERR_INVALID_ARG_VALUE";
|
|
100
157
|
throw new ProbeError(`The workspace path cannot be inspected: ${target}`, {
|
|
101
158
|
origin: claimant ? "claimant" : "workspace",
|
|
102
159
|
code: claimant ? "refused" : "malformed",
|
|
@@ -176,7 +233,7 @@ function overwriteFile(file, text) {
|
|
|
176
233
|
*
|
|
177
234
|
* @param file - The path whose creation failed
|
|
178
235
|
* @param error - The fault the failed operation raised
|
|
179
|
-
* @returns True
|
|
236
|
+
* @returns True if the fault means the host refuses this name; false otherwise
|
|
180
237
|
*
|
|
181
238
|
* @example
|
|
182
239
|
* ```ts
|
|
@@ -189,11 +246,7 @@ function overwriteFile(file, text) {
|
|
|
189
246
|
* ```
|
|
190
247
|
*/
|
|
191
248
|
function isRefusedName(file, error) {
|
|
192
|
-
|
|
193
|
-
const fault = error;
|
|
194
|
-
const reading = attempt(() => "code" in fault ? fault.code : void 0);
|
|
195
|
-
if (!reading.success) return false;
|
|
196
|
-
const code = reading.value;
|
|
249
|
+
const code = readFaultCode(error);
|
|
197
250
|
if (code === "ENAMETOOLONG") return true;
|
|
198
251
|
if (code === "ERR_INVALID_ARG_VALUE") return file.includes("\0");
|
|
199
252
|
if (code !== "ENOENT") return false;
|
|
@@ -271,6 +324,65 @@ function relativeWorkspaceMessage(workspace, message) {
|
|
|
271
324
|
return text;
|
|
272
325
|
}
|
|
273
326
|
/**
|
|
327
|
+
* Scans the plain-text output of one compiler run into the diagnostics it reported.
|
|
328
|
+
*
|
|
329
|
+
* @remarks
|
|
330
|
+
* Reads the shape `tsc --pretty false` prints: `path(line,column): error TSnnnn: message` for a
|
|
331
|
+
* diagnostic about a file, and `error TSnnnn: message` for one about a project. A line that carries
|
|
332
|
+
* neither prefix and begins with whitespace is an elaboration of the diagnostic above it and joins
|
|
333
|
+
* that message with a newline; any other line is skipped, so a runtime's own failure text yields no
|
|
334
|
+
* diagnostic and a caller reads that as the fault it is. The compiler counts lines and columns from
|
|
335
|
+
* one and columns in UTF-16 code units, so each location is lowered to the zero-based UTF-16 point
|
|
336
|
+
* `Diagnostic.range` fixes and `end` resolves to `start`, which is the extent this output carries.
|
|
337
|
+
* A warning reads as a diagnostic, because the workspace's own gate reports it the same way. The
|
|
338
|
+
* `TS\d+` token that follows `error` or `warning` is what recognizes the line; nothing is captured
|
|
339
|
+
* from it, because `Diagnostic` carries no field for it.
|
|
340
|
+
*
|
|
341
|
+
* @param text - The compiler's output
|
|
342
|
+
* @returns One record per reported diagnostic, in the compiler's own order
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* ```ts
|
|
346
|
+
* scanDiagnostics("src/a.ts(1,14): error TS2322: Type 'string' is not assignable to type 'number'.")
|
|
347
|
+
* // [{ path: 'src/a.ts', range: { start: { line: 0, character: 13 }, end: { line: 0, character: 13 } }, message: "Type 'string' is not assignable to type 'number'." }]
|
|
348
|
+
* scanDiagnostics('error TS5058: The specified path does not exist.')
|
|
349
|
+
* // [{ message: 'The specified path does not exist.' }]
|
|
350
|
+
* ```
|
|
351
|
+
*/
|
|
352
|
+
function scanDiagnostics(text) {
|
|
353
|
+
const diagnostics = [];
|
|
354
|
+
for (const line of text.split(/\r\n|\n/)) {
|
|
355
|
+
const located = /^(.+)\((\d+),(\d+)\): (?:error|warning) TS\d+: (.*)$/u.exec(line);
|
|
356
|
+
if (located !== null) {
|
|
357
|
+
const point = {
|
|
358
|
+
line: Number.parseInt(located[2] ?? "1", 10) - 1,
|
|
359
|
+
character: Number.parseInt(located[3] ?? "1", 10) - 1
|
|
360
|
+
};
|
|
361
|
+
diagnostics.push({
|
|
362
|
+
path: normalizePath(located[1] ?? ""),
|
|
363
|
+
range: {
|
|
364
|
+
start: point,
|
|
365
|
+
end: point
|
|
366
|
+
},
|
|
367
|
+
message: located[4] ?? ""
|
|
368
|
+
});
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const bare = /^(?:error|warning) TS\d+: (.*)$/u.exec(line);
|
|
372
|
+
if (bare !== null) {
|
|
373
|
+
diagnostics.push({ message: bare[1] ?? "" });
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const previous = diagnostics.at(-1);
|
|
377
|
+
if (previous === void 0 || !/^\s+\S/u.test(line)) continue;
|
|
378
|
+
diagnostics[diagnostics.length - 1] = {
|
|
379
|
+
...previous,
|
|
380
|
+
message: `${previous.message}\n${line}`
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
return diagnostics;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
274
386
|
* Resolves one installed module from the target workspace.
|
|
275
387
|
*
|
|
276
388
|
* @param workspace - The target workspace root
|
|
@@ -288,7 +400,7 @@ function resolveWorkspaceModule(workspace, specifier) {
|
|
|
288
400
|
const outcome = attempt(() => createRequire(resolve(workspace, "package.json")).resolve(specifier));
|
|
289
401
|
if (outcome.success) return outcome.value;
|
|
290
402
|
const error = outcome.error;
|
|
291
|
-
const missing =
|
|
403
|
+
const missing = readFaultCode(error) === "MODULE_NOT_FOUND";
|
|
292
404
|
throw new ProbeError(`The workspace cannot resolve ${specifier}`, {
|
|
293
405
|
origin: "workspace",
|
|
294
406
|
code: missing ? "missing" : "malformed",
|
|
@@ -296,18 +408,35 @@ function resolveWorkspaceModule(workspace, specifier) {
|
|
|
296
408
|
cause: error
|
|
297
409
|
});
|
|
298
410
|
}
|
|
299
|
-
|
|
411
|
+
/**
|
|
412
|
+
* Loads the installed `vitest/node` module from a target workspace.
|
|
413
|
+
*
|
|
414
|
+
* @remarks
|
|
415
|
+
* The only tool this package drives in process. The compiler is a process this package spawns
|
|
416
|
+
* rather than a module it loads, so `typescript` is resolved through {@link resolveWorkspaceBinary}
|
|
417
|
+
* instead.
|
|
418
|
+
*
|
|
419
|
+
* @param workspace - The target workspace root
|
|
420
|
+
* @returns The installed `vitest/node` module
|
|
421
|
+
* @throws When the workspace cannot load `vitest/node`
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* ```ts
|
|
425
|
+
* const vitest = loadWorkspaceVitest(process.cwd())
|
|
426
|
+
* typeof vitest.createVitest === 'function' // true
|
|
427
|
+
* ```
|
|
428
|
+
*/
|
|
429
|
+
function loadWorkspaceVitest(workspace) {
|
|
300
430
|
const outcome = attempt(() => {
|
|
301
|
-
|
|
302
|
-
return specifier === "typescript" ? require("typescript") : require("vitest/node");
|
|
431
|
+
return createRequire(resolve(workspace, "package.json"))("vitest/node");
|
|
303
432
|
});
|
|
304
433
|
if (outcome.success) return outcome.value;
|
|
305
434
|
const error = outcome.error;
|
|
306
|
-
const missing =
|
|
307
|
-
throw new ProbeError(
|
|
435
|
+
const missing = readFaultCode(error) === "MODULE_NOT_FOUND";
|
|
436
|
+
throw new ProbeError("The workspace cannot load vitest/node", {
|
|
308
437
|
origin: "workspace",
|
|
309
438
|
code: missing ? "missing" : "malformed",
|
|
310
|
-
context: { name:
|
|
439
|
+
context: { name: "vitest/node" },
|
|
311
440
|
cause: error
|
|
312
441
|
});
|
|
313
442
|
}
|
|
@@ -341,7 +470,8 @@ function readWorkspaceManifest(workspace, name) {
|
|
|
341
470
|
const reading = attempt(() => readFileSync(path, "utf8"));
|
|
342
471
|
if (!reading.success) {
|
|
343
472
|
const error = reading.error;
|
|
344
|
-
const
|
|
473
|
+
const code = readFaultCode(error);
|
|
474
|
+
const missing = code === "ENOENT" || code === "ENOTDIR";
|
|
345
475
|
throw new ProbeError(`${name} does not publish a readable manifest`, {
|
|
346
476
|
origin: "workspace",
|
|
347
477
|
code: missing ? "missing" : "malformed",
|
|
@@ -379,20 +509,26 @@ function readWorkspaceManifest(workspace, name) {
|
|
|
379
509
|
/**
|
|
380
510
|
* Resolves a package's portable JavaScript binary from the target workspace.
|
|
381
511
|
*
|
|
512
|
+
* @remarks
|
|
513
|
+
* A package publishes its binaries under keys of its own choosing, and that key is often not the
|
|
514
|
+
* package name: `typescript` publishes `tsc`. Name the key as `command` wherever the two differ.
|
|
515
|
+
* Default: the package name.
|
|
516
|
+
*
|
|
382
517
|
* @param workspace - The target workspace root
|
|
383
|
-
* @param name - The installed package name
|
|
518
|
+
* @param name - The installed package name
|
|
519
|
+
* @param command - The `bin` key to read. Default: `name`
|
|
384
520
|
* @returns The absolute JavaScript entry named by the package's `bin` field
|
|
385
|
-
* @throws When the package does not publish a binary under the requested
|
|
521
|
+
* @throws When the package does not publish a binary under the requested key
|
|
386
522
|
*
|
|
387
523
|
* @example
|
|
388
524
|
* ```ts
|
|
389
525
|
* relativeWorkspaceFile(process.cwd(), resolveWorkspaceBinary(process.cwd(), 'oxlint'))
|
|
390
526
|
* // 'node_modules/oxlint/bin/oxlint'
|
|
391
|
-
* resolveWorkspaceBinary(process.cwd(), 'typescript')
|
|
392
|
-
* //
|
|
527
|
+
* relativeWorkspaceFile(process.cwd(), resolveWorkspaceBinary(process.cwd(), 'typescript', 'tsc'))
|
|
528
|
+
* // 'node_modules/typescript/bin/tsc'
|
|
393
529
|
* ```
|
|
394
530
|
*/
|
|
395
|
-
function resolveWorkspaceBinary(workspace, name) {
|
|
531
|
+
function resolveWorkspaceBinary(workspace, name, command = name) {
|
|
396
532
|
const manifest = readWorkspaceManifest(workspace, name);
|
|
397
533
|
const bin = manifest.contents.bin;
|
|
398
534
|
if (bin === void 0) throw new ProbeError(`${name} does not publish a bin field`, {
|
|
@@ -401,13 +537,13 @@ function resolveWorkspaceBinary(workspace, name) {
|
|
|
401
537
|
context: { name }
|
|
402
538
|
});
|
|
403
539
|
if (typeof bin === "string") return resolve(manifest.path, "..", bin);
|
|
404
|
-
if (!isRecord(bin) || !(
|
|
540
|
+
if (!isRecord(bin) || !(command in bin)) throw new ProbeError(`${name} does not publish the ${command} binary`, {
|
|
405
541
|
origin: "workspace",
|
|
406
542
|
code: "missing",
|
|
407
543
|
context: { name }
|
|
408
544
|
});
|
|
409
|
-
const entry = bin[
|
|
410
|
-
if (typeof entry !== "string") throw new ProbeError(`${name} publishes an invalid ${
|
|
545
|
+
const entry = bin[command];
|
|
546
|
+
if (typeof entry !== "string") throw new ProbeError(`${name} publishes an invalid ${command} binary`, {
|
|
411
547
|
origin: "workspace",
|
|
412
548
|
code: "malformed",
|
|
413
549
|
context: {
|
|
@@ -485,32 +621,66 @@ function inferDocumentLanguage(path) {
|
|
|
485
621
|
return "typescript";
|
|
486
622
|
}
|
|
487
623
|
/**
|
|
488
|
-
*
|
|
624
|
+
* Builds the fresh sibling path a revision's file is written at, preserving the test's resolution
|
|
625
|
+
* directory.
|
|
489
626
|
*
|
|
490
627
|
* @param workspace - The target workspace root
|
|
491
628
|
* @param path - The workspace-relative test path
|
|
492
629
|
* @param revision - The fresh revision identity
|
|
493
|
-
* @returns
|
|
630
|
+
* @returns The absolute sibling path
|
|
494
631
|
*
|
|
495
632
|
* @example
|
|
496
633
|
* ```ts
|
|
497
634
|
* relativeWorkspaceFile(
|
|
498
635
|
* '/srv/checkout',
|
|
499
|
-
*
|
|
636
|
+
* buildRevisionPath('/srv/checkout', 'tmp/probe/greeting.test.ts', '4821-9f0c'),
|
|
500
637
|
* )
|
|
501
638
|
* // 'tmp/probe/greeting.test.probe-4821-9f0c.ts'
|
|
502
639
|
* ```
|
|
503
640
|
*/
|
|
504
|
-
function
|
|
641
|
+
function buildRevisionPath(workspace, path, revision) {
|
|
505
642
|
const file = resolveWorkspaceFile(workspace, path);
|
|
506
643
|
const extension = extname(file);
|
|
507
644
|
return `${extension === "" ? file : file.slice(0, -extension.length)}.probe-${revision}${extension}`;
|
|
508
645
|
}
|
|
509
646
|
/**
|
|
647
|
+
* Filters one issue list to the distinct issues it carries, in the order they arrived.
|
|
648
|
+
*
|
|
649
|
+
* @remarks
|
|
650
|
+
* A stage that reads one candidate through more than one tool run reports that candidate's
|
|
651
|
+
* diagnostic once per run. Two issues are the same issue when their party, path, message, and range
|
|
652
|
+
* all agree, because those are every value an issue carries.
|
|
653
|
+
*
|
|
654
|
+
* @param issues - The issues to filter
|
|
655
|
+
* @returns The distinct issues, in the order the list carried them
|
|
656
|
+
*
|
|
657
|
+
* @example
|
|
658
|
+
* ```ts
|
|
659
|
+
* const issue: Issue = { origin: 'claimant', path: 'src/core/greeting.ts', message: 'not assignable' }
|
|
660
|
+
* filterUniqueIssues([issue, { ...issue }]).length // 1
|
|
661
|
+
* ```
|
|
662
|
+
*/
|
|
663
|
+
function filterUniqueIssues(issues) {
|
|
664
|
+
const seen = /* @__PURE__ */ new Set();
|
|
665
|
+
const distinct = [];
|
|
666
|
+
for (const issue of issues) {
|
|
667
|
+
const key = JSON.stringify([
|
|
668
|
+
issue.origin,
|
|
669
|
+
issue.path,
|
|
670
|
+
issue.message,
|
|
671
|
+
issue.range
|
|
672
|
+
]);
|
|
673
|
+
if (seen.has(key)) continue;
|
|
674
|
+
seen.add(key);
|
|
675
|
+
distinct.push(issue);
|
|
676
|
+
}
|
|
677
|
+
return distinct;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
510
680
|
* Reports whether a path is a workspace module Vitest can cache.
|
|
511
681
|
*
|
|
512
682
|
* @param path - The candidate file path
|
|
513
|
-
* @returns True
|
|
683
|
+
* @returns True if the path is a script, TypeScript, Vue, or JSON module; false otherwise
|
|
514
684
|
*
|
|
515
685
|
* @example
|
|
516
686
|
* ```ts
|
|
@@ -522,6 +692,75 @@ function matchesWorkspaceModule(path) {
|
|
|
522
692
|
return /\.(?:[cm]?[jt]sx?|vue|json)$/.test(path);
|
|
523
693
|
}
|
|
524
694
|
/**
|
|
695
|
+
* Reports whether the host that wrote one file is still running.
|
|
696
|
+
*
|
|
697
|
+
* @remarks
|
|
698
|
+
* Signal 0 delivers nothing and reports reachability alone. A process this one may not signal
|
|
699
|
+
* reports `EPERM` and reads as running, and a non-positive identity names a process group rather
|
|
700
|
+
* than a process, so both answer true: the safe direction is to keep a file this package cannot
|
|
701
|
+
* account for.
|
|
702
|
+
*
|
|
703
|
+
* @param id - The process id a file's own revision names
|
|
704
|
+
* @returns True if that process is running or cannot be judged; false otherwise
|
|
705
|
+
*
|
|
706
|
+
* @example
|
|
707
|
+
* ```ts
|
|
708
|
+
* matchesLiveProcess(process.pid) // true
|
|
709
|
+
* matchesLiveProcess(0) // true
|
|
710
|
+
* ```
|
|
711
|
+
*/
|
|
712
|
+
function matchesLiveProcess(id) {
|
|
713
|
+
if (!Number.isSafeInteger(id) || id <= 0) return true;
|
|
714
|
+
try {
|
|
715
|
+
process.kill(id, 0);
|
|
716
|
+
return true;
|
|
717
|
+
} catch (error) {
|
|
718
|
+
return readFaultCode(error) === "EPERM";
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Collects every regular file a target workspace holds, skipping the trees no inspection reads.
|
|
723
|
+
*
|
|
724
|
+
* @remarks
|
|
725
|
+
* Skips a directory named `.git`, `dist`, or `node_modules` at any depth, and the directory
|
|
726
|
+
* {@link TYPE_MIRROR} names, which would otherwise carry this package's own mirror into the walk. A
|
|
727
|
+
* directory this host cannot list is skipped rather than raised, so one unreadable directory in a
|
|
728
|
+
* consumer's tree is not a reason to refuse the whole walk. A symbolic link is not carried, whether
|
|
729
|
+
* it names a file or a directory, so a file reached only through one is absent from the result; a
|
|
730
|
+
* `Dirent` that is neither a directory nor a regular file is skipped the same way.
|
|
731
|
+
*
|
|
732
|
+
* @param workspace - The target workspace root
|
|
733
|
+
* @returns The absolute path of every regular file the walk found
|
|
734
|
+
*
|
|
735
|
+
* @example
|
|
736
|
+
* ```ts
|
|
737
|
+
* collectWorkspaceFiles(process.cwd()).some((path) => path.endsWith('package.json')) // true
|
|
738
|
+
* ```
|
|
739
|
+
*/
|
|
740
|
+
function collectWorkspaceFiles(workspace) {
|
|
741
|
+
const excluded = normalizePath(resolveWorkspaceFile(workspace, TYPE_MIRROR));
|
|
742
|
+
const files = [];
|
|
743
|
+
const directories = [resolve(workspace)];
|
|
744
|
+
while (directories.length > 0) {
|
|
745
|
+
const directory = directories.pop();
|
|
746
|
+
if (directory === void 0) break;
|
|
747
|
+
let entries = [];
|
|
748
|
+
try {
|
|
749
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
750
|
+
} catch {}
|
|
751
|
+
for (const entry of entries) {
|
|
752
|
+
if (entry.name === ".git" || entry.name === "dist" || entry.name === "node_modules") continue;
|
|
753
|
+
const path = join(directory, entry.name);
|
|
754
|
+
if (entry.isDirectory()) {
|
|
755
|
+
if (normalizePath(path) !== excluded) directories.push(path);
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (entry.isFile()) files.push(path);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return files;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
525
764
|
* Normalizes a caught or foreign error into readable text.
|
|
526
765
|
*
|
|
527
766
|
* @param value - The value to describe
|
|
@@ -540,9 +779,9 @@ function describeUnknown(value) {
|
|
|
540
779
|
return String(value);
|
|
541
780
|
}
|
|
542
781
|
/**
|
|
543
|
-
* Guards one
|
|
782
|
+
* Guards one stage operation with the stage failure contract.
|
|
544
783
|
*
|
|
545
|
-
* @param stage - The
|
|
784
|
+
* @param stage - The stage serving the operation
|
|
546
785
|
* @param operation - The operation to settle
|
|
547
786
|
* @returns The operation's fulfilled value
|
|
548
787
|
* @throws The original `ProbeError`, or an instrument-owned malformed failure retaining the cause
|
|
@@ -614,12 +853,12 @@ function findRefusedPaths(value) {
|
|
|
614
853
|
* sorts every record's keys.
|
|
615
854
|
*
|
|
616
855
|
* @remarks
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
* sorting removes a difference that means nothing.
|
|
856
|
+
* `tsc --showConfig` spells a path relative to the project file, so a printed record carries an
|
|
857
|
+
* absolute path only where a project itself declares one. Rewriting a contained absolute path to
|
|
858
|
+
* its relative form keeps one commit checked out at two roots reading to one digest. A path that
|
|
859
|
+
* escapes the workspace is left as it stands, because rewriting it would name a file outside the
|
|
860
|
+
* tree by a relative spelling. Key order is the compiler's own and it differs between the supported
|
|
861
|
+
* majors, so sorting removes a difference that means nothing.
|
|
623
862
|
*
|
|
624
863
|
* @param workspace - The target workspace root
|
|
625
864
|
* @param value - The value to rewrite
|
|
@@ -637,7 +876,7 @@ function normalizeValue(workspace, value) {
|
|
|
637
876
|
if (!isAbsolute(value)) return value;
|
|
638
877
|
const path = relative(root, resolve(value));
|
|
639
878
|
if (path === "") return ".";
|
|
640
|
-
if (
|
|
879
|
+
if (escapesRoot(root, value)) return value;
|
|
641
880
|
return normalizePath(path);
|
|
642
881
|
}
|
|
643
882
|
if (isArray(value)) return value.map((entry) => normalizeValue(workspace, entry));
|
|
@@ -720,9 +959,66 @@ function releaseListeners(emitter, capture) {
|
|
|
720
959
|
}
|
|
721
960
|
}
|
|
722
961
|
//#endregion
|
|
962
|
+
//#region src/server/parsers.ts
|
|
963
|
+
/**
|
|
964
|
+
* Parses the configuration one compiler run printed for a TypeScript project.
|
|
965
|
+
*
|
|
966
|
+
* @remarks
|
|
967
|
+
* Reads what `tsc --showConfig` writes: a JSON record carrying the resolved `compilerOptions`
|
|
968
|
+
* beside the project's own file selection. `compilerOptions` is carried unvalidated, because a
|
|
969
|
+
* caller digests it rather than reading a member out of it. `files` and `include` are read only
|
|
970
|
+
* where the printed record carries one whose entries are all strings, so a member the compiler
|
|
971
|
+
* printed in another shape reads as absent rather than as an empty selection. Text that is not a
|
|
972
|
+
* JSON record, including the diagnostic the compiler prints for a project it refuses, yields
|
|
973
|
+
* `undefined`.
|
|
974
|
+
*
|
|
975
|
+
* @param text - The compiler's printed configuration
|
|
976
|
+
* @returns The project's resolved configuration, or `undefined` when the text carries no record
|
|
977
|
+
*
|
|
978
|
+
* @example
|
|
979
|
+
* ```ts
|
|
980
|
+
* parseProjectConfig('{"compilerOptions":{"strict":true}}')?.compilerOptions // { strict: true }
|
|
981
|
+
* parseProjectConfig("error TS5058: The specified path does not exist.") // undefined
|
|
982
|
+
* ```
|
|
983
|
+
*/
|
|
984
|
+
function parseProjectConfig(text) {
|
|
985
|
+
const parsed = attempt(() => JSON.parse(text));
|
|
986
|
+
if (!parsed.success || !isRecord(parsed.value)) return void 0;
|
|
987
|
+
const files = parsed.value.files;
|
|
988
|
+
const include = parsed.value.include;
|
|
989
|
+
return {
|
|
990
|
+
compilerOptions: parsed.value.compilerOptions,
|
|
991
|
+
...isArray(files) && files.every((entry) => typeof entry === "string") ? { files } : {},
|
|
992
|
+
...isArray(include) && include.every((entry) => typeof entry === "string") ? { include } : {}
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Parses the process id one revision identity names.
|
|
997
|
+
*
|
|
998
|
+
* @remarks
|
|
999
|
+
* Every file and directory this package writes into a target carries the writing host's process id
|
|
1000
|
+
* and a fresh UUID, joined by `-`. A sweep reads that id to decide whether the host that wrote the
|
|
1001
|
+
* name is gone. The whole identity must match, so a name carrying anything else yields `undefined`
|
|
1002
|
+
* and the sweep leaves what it found alone.
|
|
1003
|
+
*
|
|
1004
|
+
* @param revision - The revision identity a name carries
|
|
1005
|
+
* @returns The process id it names, or `undefined` when the text is not a revision identity
|
|
1006
|
+
*
|
|
1007
|
+
* @example
|
|
1008
|
+
* ```ts
|
|
1009
|
+
* parseRevisionOwner('4821-1b4e28ba-2fa1-11d2-883f-0016d3cca427') // 4821
|
|
1010
|
+
* parseRevisionOwner('4821-9f0c') // undefined
|
|
1011
|
+
* ```
|
|
1012
|
+
*/
|
|
1013
|
+
function parseRevisionOwner(revision) {
|
|
1014
|
+
const owner = /^(\d+)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u.exec(revision)?.[1];
|
|
1015
|
+
return owner === void 0 ? void 0 : Number.parseInt(owner, 10);
|
|
1016
|
+
}
|
|
1017
|
+
//#endregion
|
|
723
1018
|
//#region src/server/Overlay.ts
|
|
724
1019
|
/**
|
|
725
|
-
*
|
|
1020
|
+
* Implements `OverlayInterface` over a private map from normalized absolute path to candidate text,
|
|
1021
|
+
* minting at construction the `revision` a resident tool caches its answers against.
|
|
726
1022
|
*
|
|
727
1023
|
* @remarks
|
|
728
1024
|
* One overlay belongs to one inspection: the inspection creates it, records every candidate it
|
|
@@ -731,55 +1027,44 @@ function releaseListeners(emitter, capture) {
|
|
|
731
1027
|
* is what makes a resident tool re-read a path this overlay holds, so two inspections that supply
|
|
732
1028
|
* different text for one path never share a cached answer.
|
|
733
1029
|
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
* file
|
|
739
|
-
*
|
|
740
|
-
* against, so a folded key never reaches the paths a tool is handed and containment stays an exact
|
|
741
|
-
* comparison.
|
|
1030
|
+
* A lookup key matches a recorded path exactly, after both pass through `normalizePath`, so a tool
|
|
1031
|
+
* that reports its own paths with backslashes still reaches a candidate recorded with forward
|
|
1032
|
+
* slashes. Case is never folded: the recorded spelling is what `paths` reports, what `covers`
|
|
1033
|
+
* compares a directory against, and what `text` answers under. A stage whose tool resolves two
|
|
1034
|
+
* spellings of one file name to one file therefore reports the path its tool served rather than
|
|
1035
|
+
* matching the two spellings here.
|
|
742
1036
|
*
|
|
743
1037
|
* @example
|
|
744
1038
|
* ```ts
|
|
745
1039
|
* const overlay = new Overlay()
|
|
746
|
-
*
|
|
747
|
-
*
|
|
1040
|
+
* const path = '/srv/checkout/src/core/factories.ts'
|
|
1041
|
+
* overlay.set(path, "export function createGreeting(): string {\n\treturn 'hi'\n}\n")
|
|
1042
|
+
* console.log(overlay.text(path))
|
|
748
1043
|
* overlay.clear()
|
|
749
1044
|
* ```
|
|
750
1045
|
*/
|
|
751
1046
|
var Overlay = class {
|
|
752
1047
|
#revision = randomUUID();
|
|
753
|
-
#sensitive;
|
|
754
1048
|
#candidates = /* @__PURE__ */ new Map();
|
|
755
|
-
/**
|
|
756
|
-
* Creates an empty candidate set under a fresh identity.
|
|
757
|
-
*
|
|
758
|
-
* @param options - Construction options
|
|
759
|
-
*/
|
|
760
|
-
constructor(options = {}) {
|
|
761
|
-
this.#sensitive = options.sensitive ?? true;
|
|
762
|
-
}
|
|
763
1049
|
get revision() {
|
|
764
1050
|
return this.#revision;
|
|
765
1051
|
}
|
|
766
1052
|
get paths() {
|
|
767
|
-
return [...this.#candidates.
|
|
1053
|
+
return [...this.#candidates.keys()];
|
|
768
1054
|
}
|
|
769
1055
|
/**
|
|
770
1056
|
* Records one candidate's text against the absolute path it stands in for.
|
|
771
1057
|
*
|
|
772
1058
|
* @remarks
|
|
773
|
-
* Recording a candidate under a path this overlay
|
|
774
|
-
*
|
|
1059
|
+
* Recording a candidate under a path this overlay already holds replaces that candidate, the way
|
|
1060
|
+
* one file holds one text.
|
|
775
1061
|
*
|
|
776
1062
|
* @param path - The absolute path the candidate replaces
|
|
777
1063
|
* @param text - The candidate's full contents
|
|
778
1064
|
* @returns Nothing
|
|
779
1065
|
*/
|
|
780
1066
|
set(path, text) {
|
|
781
|
-
|
|
782
|
-
this.#candidates.set(this.#key(recorded), [recorded, text]);
|
|
1067
|
+
this.#candidates.set(normalizePath(path), text);
|
|
783
1068
|
}
|
|
784
1069
|
/**
|
|
785
1070
|
* Reads the candidate text recorded for one absolute path.
|
|
@@ -788,7 +1073,7 @@ var Overlay = class {
|
|
|
788
1073
|
* @returns The recorded text, or `undefined` when this overlay holds no candidate there
|
|
789
1074
|
*/
|
|
790
1075
|
text(path) {
|
|
791
|
-
return this.#candidates.get(
|
|
1076
|
+
return this.#candidates.get(normalizePath(path));
|
|
792
1077
|
}
|
|
793
1078
|
/**
|
|
794
1079
|
* Checks whether a candidate sits beneath one directory.
|
|
@@ -797,16 +1082,14 @@ var Overlay = class {
|
|
|
797
1082
|
* The answer is derived from the paths the overlay holds rather than stored, so it stops being
|
|
798
1083
|
* true exactly when the inspection that declared those candidates clears them. Both sides pass
|
|
799
1084
|
* through `normalizePath` first, because a tool that normalizes its own paths asks about a
|
|
800
|
-
* directory in a spelling the recorded path may not share.
|
|
801
|
-
* containment comparison, and it reads the recorded spelling whatever an overlay minted for a
|
|
802
|
-
* case-folding host matches its lookup keys by.
|
|
1085
|
+
* directory in a spelling the recorded path may not share.
|
|
803
1086
|
*
|
|
804
1087
|
* @param directory - The absolute directory path to check
|
|
805
1088
|
* @returns True if a candidate path sits beneath the directory; false otherwise
|
|
806
1089
|
*/
|
|
807
1090
|
covers(directory) {
|
|
808
1091
|
const base = `${normalizePath(directory).replace(/\/+$/, "")}/`;
|
|
809
|
-
for (const
|
|
1092
|
+
for (const path of this.#candidates.keys()) if (path.startsWith(base)) return true;
|
|
810
1093
|
return false;
|
|
811
1094
|
}
|
|
812
1095
|
/**
|
|
@@ -817,17 +1100,14 @@ var Overlay = class {
|
|
|
817
1100
|
clear() {
|
|
818
1101
|
this.#candidates.clear();
|
|
819
1102
|
}
|
|
820
|
-
#key(path) {
|
|
821
|
-
return this.#sensitive ? path : path.toLowerCase();
|
|
822
|
-
}
|
|
823
1103
|
};
|
|
824
1104
|
//#endregion
|
|
825
1105
|
//#region package.json
|
|
826
|
-
var version = "0.0.
|
|
1106
|
+
var version = "0.0.13";
|
|
827
1107
|
var peerDependencies = {
|
|
828
|
-
"oxlint": "^1.
|
|
829
|
-
"typescript": "^6.0.
|
|
830
|
-
"vitest": "^4.1.
|
|
1108
|
+
"oxlint": "^1.82.0",
|
|
1109
|
+
"typescript": "^6.0.3",
|
|
1110
|
+
"vitest": "^4.1.11"
|
|
831
1111
|
};
|
|
832
1112
|
//#endregion
|
|
833
1113
|
//#region src/server/stages/LintStage.ts
|
|
@@ -877,7 +1157,6 @@ var LintStage = class {
|
|
|
877
1157
|
#closing;
|
|
878
1158
|
#progress = 0;
|
|
879
1159
|
#revision = 0;
|
|
880
|
-
#destroyed = false;
|
|
881
1160
|
/**
|
|
882
1161
|
* Starts warming the target workspace's Oxlint language server.
|
|
883
1162
|
*
|
|
@@ -899,12 +1178,11 @@ var LintStage = class {
|
|
|
899
1178
|
}
|
|
900
1179
|
destroy() {
|
|
901
1180
|
if (this.#closing !== void 0) return this.#closing;
|
|
902
|
-
this.#destroyed = true;
|
|
903
1181
|
this.#closing = guardStage(this.stage, this.#destroy());
|
|
904
1182
|
return this.#closing;
|
|
905
1183
|
}
|
|
906
1184
|
async #inspect(subject, options) {
|
|
907
|
-
if (this.#
|
|
1185
|
+
if (this.#closing !== void 0) throw createDestroyedError("lint stage");
|
|
908
1186
|
if (options === void 0) throw new ProbeError("The lint stage inspects only under a bound its caller supplies", {
|
|
909
1187
|
origin: "claimant",
|
|
910
1188
|
code: "refused",
|
|
@@ -912,7 +1190,7 @@ var LintStage = class {
|
|
|
912
1190
|
});
|
|
913
1191
|
const started = performance.now();
|
|
914
1192
|
const client = await this.#warmed();
|
|
915
|
-
if (this.#
|
|
1193
|
+
if (this.#closing !== void 0) throw createDestroyedError("lint stage");
|
|
916
1194
|
const issues = [];
|
|
917
1195
|
for (const draft of [...subject.files, subject.test]) issues.push(...await this.#document(client, draft, options.signal));
|
|
918
1196
|
return {
|
|
@@ -997,7 +1275,7 @@ var LintStage = class {
|
|
|
997
1275
|
});
|
|
998
1276
|
}
|
|
999
1277
|
#translate(error, path) {
|
|
1000
|
-
if (this.#
|
|
1278
|
+
if (this.#closing !== void 0) return createDestroyedError("lint stage");
|
|
1001
1279
|
const ending = this.#ending;
|
|
1002
1280
|
if (ending !== void 0) return this.#fault(`The Oxlint language server exited with ${ending}`, error, path);
|
|
1003
1281
|
if (error instanceof Error) return error;
|
|
@@ -1052,6 +1330,11 @@ var LintStage = class {
|
|
|
1052
1330
|
* The runtime overlay leaves bare specifier resolution to Vite: it replaces a bare import only
|
|
1053
1331
|
* after Vite resolves that import to a covered file. The type stage overlays the declared test
|
|
1054
1332
|
* path, while this stage executes a generated sibling and overlays only the candidate files.
|
|
1333
|
+
* This stage mints its overlay with the default exact-match sensitivity, because it declares no
|
|
1334
|
+
* file-name case sensitivity to Vite. A covered path whose spelling an importer wrote differs from
|
|
1335
|
+
* the recorded candidate path is therefore served by whatever answers first, and `#misses` reports
|
|
1336
|
+
* it as `The workspace configuration served this module before the runtime overlay` with origin
|
|
1337
|
+
* `workspace` rather than leaving it answered silently.
|
|
1055
1338
|
*
|
|
1056
1339
|
* Vite retains one unresolved URL for every specification path, so the stage replaces its whole
|
|
1057
1340
|
* Vitest service after 64 specifications rather than deleting from each map that service owns. Any
|
|
@@ -1100,7 +1383,6 @@ var RuntimeStage = class {
|
|
|
1100
1383
|
#specifications = 0;
|
|
1101
1384
|
#closing;
|
|
1102
1385
|
#progress = 0;
|
|
1103
|
-
#destroyed = false;
|
|
1104
1386
|
/**
|
|
1105
1387
|
* Starts warming the target workspace's Vitest service.
|
|
1106
1388
|
*
|
|
@@ -1108,7 +1390,7 @@ var RuntimeStage = class {
|
|
|
1108
1390
|
*/
|
|
1109
1391
|
constructor(workspace = process.cwd()) {
|
|
1110
1392
|
this.#workspace = workspace;
|
|
1111
|
-
const vitest =
|
|
1393
|
+
const vitest = loadWorkspaceVitest(this.#workspace);
|
|
1112
1394
|
this.#store(this.#warm(vitest.createVitest));
|
|
1113
1395
|
}
|
|
1114
1396
|
get stage() {
|
|
@@ -1117,21 +1399,40 @@ var RuntimeStage = class {
|
|
|
1117
1399
|
get progress() {
|
|
1118
1400
|
return this.#progress;
|
|
1119
1401
|
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Inspects one case.
|
|
1404
|
+
*
|
|
1405
|
+
* @param subject - The candidate drafts and test to inspect
|
|
1406
|
+
* @returns One outcome for this stage
|
|
1407
|
+
* @throws When the workspace's own tool cannot start or has already been destroyed
|
|
1408
|
+
*/
|
|
1120
1409
|
inspect(subject) {
|
|
1121
1410
|
return guardStage(this.stage, this.#inspect(subject));
|
|
1122
1411
|
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Tears down the resident tool or the mirror and releases its resources.
|
|
1414
|
+
*
|
|
1415
|
+
* @remarks
|
|
1416
|
+
* A stage abandons every inspection it holds rather than waiting behind one, so teardown never
|
|
1417
|
+
* waits for an inspection to return. An abandoned inspection rejects, either at the stage's own
|
|
1418
|
+
* guard or as the owned tool closes. Teardown is bounded whatever the stage's own tool does: a
|
|
1419
|
+
* tool that answers neither its warming exchange nor its ending is signalled and released at the
|
|
1420
|
+
* stage's own deadline. A coordinator replaces a stage whose worker no longer returns because
|
|
1421
|
+
* teardown neither waits for an inspection nor waits past that deadline.
|
|
1422
|
+
*
|
|
1423
|
+
* @returns A promise that settles after the resident tool or the mirror releases its resources
|
|
1424
|
+
*/
|
|
1123
1425
|
destroy() {
|
|
1124
1426
|
if (this.#closing !== void 0) return this.#closing;
|
|
1125
|
-
this.#destroyed = true;
|
|
1126
1427
|
this.#closing = guardStage(this.stage, this.#destroy());
|
|
1127
1428
|
return this.#closing;
|
|
1128
1429
|
}
|
|
1129
1430
|
async #inspect(subject) {
|
|
1130
|
-
if (this.#
|
|
1431
|
+
if (this.#closing !== void 0) throw createDestroyedError("runtime stage");
|
|
1131
1432
|
const started = performance.now();
|
|
1132
1433
|
const exitCode = process.exitCode;
|
|
1133
1434
|
const vitest = await this.#runner();
|
|
1134
|
-
if (this.#
|
|
1435
|
+
if (this.#closing !== void 0) throw createDestroyedError("runtime stage");
|
|
1135
1436
|
const project = this.#project(vitest, subject.test.path);
|
|
1136
1437
|
if ("message" in project) return {
|
|
1137
1438
|
stage: this.stage,
|
|
@@ -1273,7 +1574,7 @@ var RuntimeStage = class {
|
|
|
1273
1574
|
return {
|
|
1274
1575
|
...config,
|
|
1275
1576
|
plugins: [...config.plugins ?? [], {
|
|
1276
|
-
name:
|
|
1577
|
+
name: RUNTIME_PLUGIN,
|
|
1277
1578
|
enforce: "pre",
|
|
1278
1579
|
resolveId: this.#resolve.bind(this),
|
|
1279
1580
|
load: this.#load.bind(this)
|
|
@@ -1315,7 +1616,7 @@ var RuntimeStage = class {
|
|
|
1315
1616
|
let generated;
|
|
1316
1617
|
const outcome = attempt(() => {
|
|
1317
1618
|
const revision = `${process.pid}-${randomUUID()}`;
|
|
1318
|
-
const file =
|
|
1619
|
+
const file = buildRevisionPath(this.#workspace, test.path, revision);
|
|
1319
1620
|
generated = file;
|
|
1320
1621
|
const target = relative(this.#workspace, file);
|
|
1321
1622
|
resolveWorkspaceFile(this.#workspace, target, true);
|
|
@@ -1373,7 +1674,7 @@ var RuntimeStage = class {
|
|
|
1373
1674
|
path
|
|
1374
1675
|
}
|
|
1375
1676
|
});
|
|
1376
|
-
if (!project.vite.config.plugins.some((plugin) => plugin.name ===
|
|
1677
|
+
if (!project.vite.config.plugins.some((plugin) => plugin.name === RUNTIME_PLUGIN)) return {
|
|
1377
1678
|
origin: "workspace",
|
|
1378
1679
|
path,
|
|
1379
1680
|
message: `The runtime stage cannot instrument the string-declared Vitest project ${name} because its configuration carries no runtime overlay plugin`
|
|
@@ -1462,15 +1763,15 @@ var RuntimeStage = class {
|
|
|
1462
1763
|
}
|
|
1463
1764
|
#runner() {
|
|
1464
1765
|
const current = this.#vitest;
|
|
1465
|
-
if (current !== void 0 && this.#specifications <
|
|
1766
|
+
if (current !== void 0 && this.#specifications < PROBE_SPECIFICATIONS) return current;
|
|
1466
1767
|
if (current !== void 0) return this.#store(this.#replace(current));
|
|
1467
|
-
const runner =
|
|
1468
|
-
return this.#store(this.#specifications <
|
|
1768
|
+
const runner = loadWorkspaceVitest(this.#workspace);
|
|
1769
|
+
return this.#store(this.#specifications < PROBE_SPECIFICATIONS ? this.#warm(runner.createVitest) : this.#replace(void 0, runner.createVitest));
|
|
1469
1770
|
}
|
|
1470
1771
|
async #replace(current, create) {
|
|
1471
1772
|
if (current !== void 0) await (await current).close();
|
|
1472
|
-
if (this.#
|
|
1473
|
-
const runner = create ??
|
|
1773
|
+
if (this.#closing !== void 0) throw createDestroyedError("runtime stage");
|
|
1774
|
+
const runner = create ?? loadWorkspaceVitest(this.#workspace).createVitest;
|
|
1474
1775
|
const replacement = await this.#warm(runner);
|
|
1475
1776
|
this.#specifications = 0;
|
|
1476
1777
|
return replacement;
|
|
@@ -1514,7 +1815,7 @@ var RuntimeStage = class {
|
|
|
1514
1815
|
}
|
|
1515
1816
|
#snapshot() {
|
|
1516
1817
|
const modules = /* @__PURE__ */ new Map();
|
|
1517
|
-
for (const path of this.#
|
|
1818
|
+
for (const path of collectWorkspaceFiles(this.#workspace)) {
|
|
1518
1819
|
if (!matchesWorkspaceModule(path)) continue;
|
|
1519
1820
|
try {
|
|
1520
1821
|
const digest = createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
@@ -1525,12 +1826,12 @@ var RuntimeStage = class {
|
|
|
1525
1826
|
return modules;
|
|
1526
1827
|
}
|
|
1527
1828
|
#sweep() {
|
|
1528
|
-
for (const path of this.#
|
|
1829
|
+
for (const path of collectWorkspaceFiles(this.#workspace)) {
|
|
1529
1830
|
const match = [...basename(path).matchAll(/\.probe-((\d+)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?=\.|$)/gu)].at(-1);
|
|
1530
1831
|
const revision = match?.[1];
|
|
1531
1832
|
const owner = match?.[2];
|
|
1532
1833
|
if (revision === void 0 || owner === void 0) continue;
|
|
1533
|
-
if (
|
|
1834
|
+
if (matchesLiveProcess(Number.parseInt(owner, 10))) continue;
|
|
1534
1835
|
if (!this.#owned(path, revision)) continue;
|
|
1535
1836
|
try {
|
|
1536
1837
|
unlinkSync(resolveWorkspaceFile(this.#workspace, relative(this.#workspace, path), true));
|
|
@@ -1541,35 +1842,6 @@ var RuntimeStage = class {
|
|
|
1541
1842
|
const outcome = attempt(() => readFileSync(path, "utf8"));
|
|
1542
1843
|
return outcome.success && matchesSpecification(outcome.value, revision);
|
|
1543
1844
|
}
|
|
1544
|
-
#alive(id) {
|
|
1545
|
-
if (!Number.isSafeInteger(id) || id <= 0) return true;
|
|
1546
|
-
try {
|
|
1547
|
-
process.kill(id, 0);
|
|
1548
|
-
return true;
|
|
1549
|
-
} catch (error) {
|
|
1550
|
-
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
*#walk() {
|
|
1554
|
-
const directories = [resolve(this.#workspace)];
|
|
1555
|
-
while (directories.length > 0) {
|
|
1556
|
-
const directory = directories.pop();
|
|
1557
|
-
if (directory === void 0) break;
|
|
1558
|
-
let entries = [];
|
|
1559
|
-
try {
|
|
1560
|
-
entries = readdirSync(directory, { withFileTypes: true });
|
|
1561
|
-
} catch {}
|
|
1562
|
-
for (const entry of entries) {
|
|
1563
|
-
if (entry.name === ".git" || entry.name === "dist" || entry.name === "node_modules") continue;
|
|
1564
|
-
const path = join(directory, entry.name);
|
|
1565
|
-
if (entry.isDirectory()) {
|
|
1566
|
-
directories.push(path);
|
|
1567
|
-
continue;
|
|
1568
|
-
}
|
|
1569
|
-
if (entry.isFile()) yield path;
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
1845
|
#issues(result, file, original) {
|
|
1574
1846
|
const specification = this.#real(file);
|
|
1575
1847
|
const issues = [];
|
|
@@ -1685,21 +1957,32 @@ var RuntimeStage = class {
|
|
|
1685
1957
|
//#endregion
|
|
1686
1958
|
//#region src/server/stages/TypeStage.ts
|
|
1687
1959
|
/**
|
|
1688
|
-
* Inspects TypeScript source
|
|
1960
|
+
* Inspects TypeScript source by running the target workspace's own compiler over a mirror of it.
|
|
1689
1961
|
*
|
|
1690
1962
|
* @remarks
|
|
1691
|
-
*
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1694
|
-
*
|
|
1695
|
-
*
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1698
|
-
*
|
|
1699
|
-
*
|
|
1700
|
-
*
|
|
1701
|
-
*
|
|
1702
|
-
*
|
|
1963
|
+
* The compiler is a process this stage spawns rather than a module it loads, so nothing here holds
|
|
1964
|
+
* a resident program and the host's event loop is free for the whole check. Construction sweeps a
|
|
1965
|
+
* mirror an earlier host left behind, copies the workspace into a fresh one under
|
|
1966
|
+
* {@link TYPE_MIRROR}, and builds each declared project's incremental state there, so the first
|
|
1967
|
+
* inspection is warm.
|
|
1968
|
+
*
|
|
1969
|
+
* Every inspection refreshes the mirror by content digest, writes each candidate draft and the test
|
|
1970
|
+
* at its mirrored declared path, and runs `tsc --noEmit --pretty false` once per distinct selected
|
|
1971
|
+
* project. A draft therefore shadows the file it replaces: a consumer of that path is checked
|
|
1972
|
+
* against the draft's text, and a draft importing a sibling draft resolves to the sibling draft.
|
|
1973
|
+
* Nothing is written outside `tmp/`, and the workspace's own copy of a drafted file never moves.
|
|
1974
|
+
*
|
|
1975
|
+
* Each run reads a scratch project this stage writes beside the mirrored project it extends, so
|
|
1976
|
+
* every relative path the project declares — its own `rootDir`, `include`, `paths`, and the
|
|
1977
|
+
* projects it extends — resolves inside the mirror without being rewritten. The scratch adds the
|
|
1978
|
+
* drafts as `files`, which the project's own `exclude` does not reach, and its incremental state
|
|
1979
|
+
* lives in the mirror so reuse survives the refresh.
|
|
1980
|
+
*
|
|
1981
|
+
* The diagnostics decide the outcome, never the exit code, which the supported compiler majors
|
|
1982
|
+
* disagree on. A diagnostic naming a project file, and one naming no file at all, is the target
|
|
1983
|
+
* tree's own configuration fault and raises rather than reporting, unless the `.json` file is one
|
|
1984
|
+
* the claim itself drafted; every other diagnostic is a claimant issue at the point the compiler
|
|
1985
|
+
* reported, which is the extent the plain-text output carries.
|
|
1703
1986
|
*
|
|
1704
1987
|
* @example
|
|
1705
1988
|
* ```ts
|
|
@@ -1710,27 +1993,27 @@ var RuntimeStage = class {
|
|
|
1710
1993
|
*/
|
|
1711
1994
|
var TypeStage = class {
|
|
1712
1995
|
#workspace;
|
|
1713
|
-
#
|
|
1714
|
-
#
|
|
1715
|
-
#
|
|
1716
|
-
#
|
|
1717
|
-
#
|
|
1718
|
-
#
|
|
1719
|
-
#
|
|
1720
|
-
#
|
|
1996
|
+
#compiler;
|
|
1997
|
+
#revision = `${process.pid}-${randomUUID()}`;
|
|
1998
|
+
#mirror;
|
|
1999
|
+
#configs = /* @__PURE__ */ new Map();
|
|
2000
|
+
#mirrored = /* @__PURE__ */ new Map();
|
|
2001
|
+
#drafts = /* @__PURE__ */ new Set();
|
|
2002
|
+
#children = /* @__PURE__ */ new Set();
|
|
2003
|
+
#warming;
|
|
1721
2004
|
#closing;
|
|
1722
2005
|
#progress = 0;
|
|
1723
|
-
#destroyed = false;
|
|
1724
2006
|
/**
|
|
1725
|
-
*
|
|
2007
|
+
* Resolves the target workspace's compiler and starts warming its mirror.
|
|
1726
2008
|
*
|
|
1727
2009
|
* @param workspace - The target workspace root. Default: the current working directory
|
|
1728
2010
|
*/
|
|
1729
2011
|
constructor(workspace = process.cwd()) {
|
|
1730
2012
|
this.#workspace = workspace;
|
|
1731
|
-
|
|
1732
|
-
this.#
|
|
1733
|
-
this.#
|
|
2013
|
+
this.#compiler = resolveWorkspaceBinary(workspace, "typescript", "tsc");
|
|
2014
|
+
this.#mirror = resolveWorkspaceFile(workspace, `${TYPE_MIRROR}/${this.#revision}`);
|
|
2015
|
+
this.#warming = this.#warm();
|
|
2016
|
+
this.#warming.catch(() => {});
|
|
1734
2017
|
}
|
|
1735
2018
|
get stage() {
|
|
1736
2019
|
return "type";
|
|
@@ -1750,7 +2033,8 @@ var TypeStage = class {
|
|
|
1750
2033
|
* @param project - The workspace-relative TypeScript project the candidate drafts are checked
|
|
1751
2034
|
* against. Default: the scoped project each candidate path infers
|
|
1752
2035
|
* @returns One outcome for this stage
|
|
1753
|
-
* @throws When the
|
|
2036
|
+
* @throws When the workspace refuses the mirror, when a project the run reads is malformed, or
|
|
2037
|
+
* when the stage has already been destroyed
|
|
1754
2038
|
*/
|
|
1755
2039
|
inspect(subject, project) {
|
|
1756
2040
|
return guardStage(this.stage, this.#inspect(subject, project));
|
|
@@ -1759,101 +2043,95 @@ var TypeStage = class {
|
|
|
1759
2043
|
* Resolves one project to the path and digest this stage applies for it.
|
|
1760
2044
|
*
|
|
1761
2045
|
* @remarks
|
|
1762
|
-
* Reads the
|
|
1763
|
-
*
|
|
1764
|
-
*
|
|
1765
|
-
*
|
|
2046
|
+
* Reads the configuration the compiler itself prints for the project as the mirror holds it,
|
|
2047
|
+
* refreshed from the workspace first on a cache miss, so the digest names the configuration the
|
|
2048
|
+
* check applies. The reading is cached per project for the life of the stage, so a draft written
|
|
2049
|
+
* afterward cannot move it.
|
|
1766
2050
|
*
|
|
1767
2051
|
* @param project - The workspace-relative TypeScript project to resolve
|
|
1768
2052
|
* @returns The resolved workspace-relative path and the digest of its compiler options
|
|
1769
|
-
* @throws When the project escapes the workspace,
|
|
1770
|
-
* been destroyed
|
|
2053
|
+
* @throws When the project escapes the workspace, when the compiler refuses it, or when the
|
|
2054
|
+
* stage has already been destroyed
|
|
1771
2055
|
*/
|
|
1772
2056
|
resolve(project) {
|
|
1773
2057
|
return guardStage(this.stage, this.#resolve(project));
|
|
1774
2058
|
}
|
|
1775
2059
|
destroy() {
|
|
1776
2060
|
if (this.#closing !== void 0) return this.#closing;
|
|
1777
|
-
this.#destroyed = true;
|
|
1778
2061
|
this.#closing = guardStage(this.stage, this.#destroy());
|
|
1779
2062
|
return this.#closing;
|
|
1780
2063
|
}
|
|
1781
2064
|
async #inspect(subject, project) {
|
|
1782
|
-
|
|
2065
|
+
this.#refuseDestroyed();
|
|
1783
2066
|
const started = performance.now();
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
const
|
|
2067
|
+
await this.#warming;
|
|
2068
|
+
this.#refuseDestroyed();
|
|
2069
|
+
const test = resolveWorkspaceFile(this.#workspace, subject.test.path);
|
|
2070
|
+
const resolved = subject.files.map((draft) => ({
|
|
1787
2071
|
draft,
|
|
1788
2072
|
path: resolveWorkspaceFile(this.#workspace, draft.path)
|
|
1789
|
-
})).map((candidate) => ({
|
|
1790
|
-
draft: candidate.draft,
|
|
1791
|
-
project: project ?? inferTypeProject(relativeWorkspaceFile(this.#workspace, candidate.path))
|
|
1792
2073
|
}));
|
|
1793
|
-
const
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2074
|
+
const groups = /* @__PURE__ */ new Map([[this.#contain("tsconfig.json"), [subject.test]]]);
|
|
2075
|
+
for (const candidate of resolved) {
|
|
2076
|
+
const selected = this.#contain(project ?? inferTypeProject(relativeWorkspaceFile(this.#workspace, candidate.path)));
|
|
2077
|
+
const drafts = groups.get(selected);
|
|
2078
|
+
if (drafts === void 0) groups.set(selected, [candidate.draft]);
|
|
2079
|
+
else drafts.push(candidate.draft);
|
|
1799
2080
|
}
|
|
1800
|
-
|
|
1801
|
-
this.#
|
|
2081
|
+
this.#progress += 1;
|
|
2082
|
+
for (const selected of groups.keys()) await this.#configure(selected);
|
|
2083
|
+
this.#refuseDestroyed();
|
|
1802
2084
|
try {
|
|
1803
|
-
this.#
|
|
1804
|
-
|
|
1805
|
-
this.#
|
|
2085
|
+
this.#refresh();
|
|
2086
|
+
this.#drafts.add(this.#place(subject.test.text, test));
|
|
2087
|
+
for (const candidate of resolved) this.#drafts.add(this.#place(candidate.draft.text, candidate.path));
|
|
1806
2088
|
const issues = [];
|
|
1807
|
-
const
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
await this.#unblock();
|
|
1811
|
-
for (const selection of selections) {
|
|
1812
|
-
const draft = selection.draft;
|
|
1813
|
-
const selected = selection.project;
|
|
1814
|
-
const service = this.#service(typescript, selected);
|
|
1815
|
-
issues.push(...this.#issues(typescript, service, draft, selected, project !== void 0, !projects.has(selected)));
|
|
1816
|
-
projects.add(selected);
|
|
1817
|
-
await this.#unblock();
|
|
2089
|
+
for (const [selected, drafts] of groups) {
|
|
2090
|
+
this.#refuseDestroyed();
|
|
2091
|
+
issues.push(...await this.#check(selected, drafts));
|
|
1818
2092
|
}
|
|
1819
2093
|
return {
|
|
1820
2094
|
stage: this.stage,
|
|
1821
2095
|
elapsed: Math.round(performance.now() - started),
|
|
1822
|
-
issues
|
|
2096
|
+
issues: filterUniqueIssues(issues)
|
|
1823
2097
|
};
|
|
1824
2098
|
} finally {
|
|
1825
|
-
|
|
2099
|
+
this.#release();
|
|
1826
2100
|
}
|
|
1827
2101
|
}
|
|
1828
2102
|
async #resolve(project) {
|
|
1829
|
-
|
|
1830
|
-
const
|
|
1831
|
-
if (this.#destroyed) throw createDestroyedError("type stage");
|
|
1832
|
-
const resolved = resolveWorkspaceFile(this.#workspace, project);
|
|
2103
|
+
this.#refuseDestroyed();
|
|
2104
|
+
const contained = this.#contain(project);
|
|
1833
2105
|
this.#progress += 1;
|
|
1834
|
-
this.#
|
|
1835
|
-
await this.#unblock();
|
|
2106
|
+
const config = await this.#configure(contained);
|
|
1836
2107
|
return {
|
|
1837
|
-
path:
|
|
1838
|
-
digest: computeDigest(this.#workspace,
|
|
2108
|
+
path: contained,
|
|
2109
|
+
digest: computeDigest(this.#workspace, config.compilerOptions)
|
|
1839
2110
|
};
|
|
1840
2111
|
}
|
|
1841
2112
|
async #destroy() {
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
this.#
|
|
1845
|
-
this.#
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2113
|
+
const children = [...this.#children];
|
|
2114
|
+
this.#children.clear();
|
|
2115
|
+
for (const child of children) this.#terminate(child);
|
|
2116
|
+
await this.#warming.catch(() => void 0);
|
|
2117
|
+
try {
|
|
2118
|
+
rmSync(this.#mirror, {
|
|
2119
|
+
recursive: true,
|
|
2120
|
+
force: true,
|
|
2121
|
+
maxRetries: 5,
|
|
2122
|
+
retryDelay: 20
|
|
2123
|
+
});
|
|
2124
|
+
} catch {}
|
|
2125
|
+
this.#configs.clear();
|
|
2126
|
+
this.#mirrored.clear();
|
|
2127
|
+
this.#drafts.clear();
|
|
2128
|
+
}
|
|
2129
|
+
async #warm() {
|
|
2130
|
+
this.#sweep();
|
|
2131
|
+
this.#createMirror();
|
|
2132
|
+
this.#refresh();
|
|
2133
|
+
this.#refuseDestroyed();
|
|
2134
|
+
await Promise.all(this.#projects().map((project) => this.#check(project, [])));
|
|
1857
2135
|
}
|
|
1858
2136
|
#projects() {
|
|
1859
2137
|
const projects = ["tsconfig.json"];
|
|
@@ -1869,162 +2147,291 @@ var TypeStage = class {
|
|
|
1869
2147
|
}
|
|
1870
2148
|
return projects;
|
|
1871
2149
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
if (this.#destroyed) throw createDestroyedError("type stage");
|
|
2150
|
+
#refuseDestroyed() {
|
|
2151
|
+
if (this.#closing !== void 0) throw createDestroyedError("type stage");
|
|
1875
2152
|
}
|
|
1876
|
-
#
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
if (existing !== void 0) return existing;
|
|
1883
|
-
const spelling = normalizePath(path);
|
|
1884
|
-
const config = typescript.readConfigFile(spelling, typescript.sys.readFile);
|
|
1885
|
-
if (config.error !== void 0) throw new ProbeError(this.#translate(typescript, config.error.messageText), {
|
|
1886
|
-
origin: "workspace",
|
|
1887
|
-
code: "malformed",
|
|
1888
|
-
context: {
|
|
1889
|
-
stage: this.stage,
|
|
1890
|
-
project
|
|
1891
|
-
}
|
|
2153
|
+
#createMirror() {
|
|
2154
|
+
const marker = resolveWorkspaceFile(this.#workspace, `${TYPE_MIRROR}/${this.#revision}/.probe/mirror.txt`, true);
|
|
2155
|
+
mkdirSync(dirname(marker), { recursive: true });
|
|
2156
|
+
writeFileSync(marker, formatSpecification("", this.#revision), {
|
|
2157
|
+
encoding: "utf8",
|
|
2158
|
+
flag: "wx"
|
|
1892
2159
|
});
|
|
1893
|
-
const parsed = typescript.parseJsonConfigFileContent(config.config, typescript.sys, dirname(spelling), void 0, spelling);
|
|
1894
|
-
if (parsed.errors.length > 0) throw new ProbeError(this.#translate(typescript, parsed.errors[0]?.messageText), {
|
|
1895
|
-
origin: "workspace",
|
|
1896
|
-
code: "malformed",
|
|
1897
|
-
context: {
|
|
1898
|
-
stage: this.stage,
|
|
1899
|
-
project
|
|
1900
|
-
}
|
|
1901
|
-
});
|
|
1902
|
-
this.#options.set(path, parsed.options);
|
|
1903
|
-
this.#files.set(path, parsed.fileNames);
|
|
1904
|
-
const host = {
|
|
1905
|
-
getCompilationSettings: this.#compilationSettings.bind(this, path),
|
|
1906
|
-
getScriptFileNames: this.#scriptFiles.bind(this, path),
|
|
1907
|
-
getScriptVersion: this.#version.bind(this),
|
|
1908
|
-
getScriptSnapshot: this.#snapshot.bind(this, typescript),
|
|
1909
|
-
getCurrentDirectory: this.#directory.bind(this),
|
|
1910
|
-
getDefaultLibFileName: this.#defaultLibrary.bind(this, typescript),
|
|
1911
|
-
fileExists: this.#fileExists.bind(this, typescript),
|
|
1912
|
-
readFile: this.#readFile.bind(this, typescript),
|
|
1913
|
-
readDirectory: typescript.sys.readDirectory,
|
|
1914
|
-
directoryExists: this.#directoryExists.bind(this, typescript),
|
|
1915
|
-
getDirectories: typescript.sys.getDirectories,
|
|
1916
|
-
useCaseSensitiveFileNames: this.#caseSensitive.bind(this, typescript),
|
|
1917
|
-
getNewLine: this.#newline.bind(this, typescript)
|
|
1918
|
-
};
|
|
1919
|
-
const service = typescript.createLanguageService(host);
|
|
1920
|
-
this.#services.set(path, service);
|
|
1921
|
-
if (!this.#resident.has(path)) this.#recycle(path);
|
|
1922
|
-
return service;
|
|
1923
|
-
}
|
|
1924
|
-
#compilationSettings(path) {
|
|
1925
|
-
return this.#options.get(path) ?? {};
|
|
1926
2160
|
}
|
|
1927
|
-
#
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2161
|
+
#sweep() {
|
|
2162
|
+
const root = resolveWorkspaceFile(this.#workspace, TYPE_MIRROR);
|
|
2163
|
+
let entries = [];
|
|
2164
|
+
try {
|
|
2165
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
2166
|
+
} catch {
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
for (const entry of entries) {
|
|
2170
|
+
if (!entry.isDirectory()) continue;
|
|
2171
|
+
const owner = parseRevisionOwner(entry.name);
|
|
2172
|
+
if (owner === void 0 || matchesLiveProcess(owner)) continue;
|
|
2173
|
+
const marker = attempt(() => readFileSync(join(root, entry.name, ".probe", "mirror.txt"), "utf8"));
|
|
2174
|
+
if (!marker.success || !matchesSpecification(marker.value, entry.name)) continue;
|
|
2175
|
+
try {
|
|
2176
|
+
rmSync(join(root, entry.name), {
|
|
2177
|
+
recursive: true,
|
|
2178
|
+
force: true,
|
|
2179
|
+
maxRetries: 5,
|
|
2180
|
+
retryDelay: 20
|
|
2181
|
+
});
|
|
2182
|
+
} catch {}
|
|
2183
|
+
}
|
|
1947
2184
|
}
|
|
1948
|
-
#
|
|
1949
|
-
|
|
2185
|
+
#refresh() {
|
|
2186
|
+
const present = /* @__PURE__ */ new Set();
|
|
2187
|
+
for (const path of collectWorkspaceFiles(this.#workspace)) {
|
|
2188
|
+
const contained = relativeWorkspaceFile(this.#workspace, path);
|
|
2189
|
+
present.add(contained);
|
|
2190
|
+
const reading = attempt(() => readFileSync(path));
|
|
2191
|
+
if (!reading.success) continue;
|
|
2192
|
+
const digest = createHash("sha256").update(reading.value).digest("hex");
|
|
2193
|
+
if (this.#mirrored.get(contained) === digest) continue;
|
|
2194
|
+
this.#place(reading.value, path);
|
|
2195
|
+
this.#mirrored.set(contained, digest);
|
|
2196
|
+
}
|
|
2197
|
+
for (const contained of [...this.#mirrored.keys()]) {
|
|
2198
|
+
if (present.has(contained)) continue;
|
|
2199
|
+
this.#remove(contained);
|
|
2200
|
+
this.#mirrored.delete(contained);
|
|
2201
|
+
}
|
|
1950
2202
|
}
|
|
1951
|
-
#
|
|
1952
|
-
|
|
2203
|
+
#release() {
|
|
2204
|
+
for (const contained of this.#drafts) {
|
|
2205
|
+
this.#remove(contained);
|
|
2206
|
+
this.#mirrored.delete(contained);
|
|
2207
|
+
}
|
|
2208
|
+
this.#drafts.clear();
|
|
2209
|
+
}
|
|
2210
|
+
#place(text, path) {
|
|
2211
|
+
const contained = relativeWorkspaceFile(this.#workspace, path);
|
|
2212
|
+
const target = this.#mirrorPath(contained);
|
|
2213
|
+
if (attempt(() => {
|
|
2214
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
2215
|
+
writeFileSync(target, text);
|
|
2216
|
+
}).success) return contained;
|
|
2217
|
+
this.#displace(target);
|
|
2218
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
2219
|
+
writeFileSync(target, text);
|
|
2220
|
+
return contained;
|
|
2221
|
+
}
|
|
2222
|
+
#displace(target) {
|
|
2223
|
+
const ancestors = [];
|
|
2224
|
+
let directory = dirname(target);
|
|
2225
|
+
while (directory !== this.#mirror && directory !== dirname(directory)) {
|
|
2226
|
+
ancestors.unshift(directory);
|
|
2227
|
+
directory = dirname(directory);
|
|
2228
|
+
}
|
|
2229
|
+
for (const ancestor of [...ancestors, target]) {
|
|
2230
|
+
const reading = attempt(() => lstatSync(ancestor));
|
|
2231
|
+
if (!reading.success) continue;
|
|
2232
|
+
if (ancestor !== target && reading.value.isDirectory()) continue;
|
|
2233
|
+
if (ancestor === target && !reading.value.isDirectory()) continue;
|
|
2234
|
+
rmSync(ancestor, {
|
|
2235
|
+
recursive: true,
|
|
2236
|
+
force: true,
|
|
2237
|
+
maxRetries: 5,
|
|
2238
|
+
retryDelay: 20
|
|
2239
|
+
});
|
|
2240
|
+
this.#mirrored.delete(relativeWorkspaceFile(this.#mirror, ancestor));
|
|
2241
|
+
}
|
|
1953
2242
|
}
|
|
1954
|
-
#
|
|
1955
|
-
|
|
1956
|
-
if (this.#diagnostics.has(path)) return;
|
|
1957
|
-
this.#diagnostics.set(path, service.getCompilerOptionsDiagnostics());
|
|
2243
|
+
#mirrorPath(contained) {
|
|
2244
|
+
return join(this.#mirror, contained);
|
|
1958
2245
|
}
|
|
1959
|
-
#
|
|
1960
|
-
|
|
1961
|
-
this.#recycled = path;
|
|
1962
|
-
if (previous === void 0 || previous === path) return;
|
|
1963
|
-
this.#services.get(previous)?.dispose();
|
|
1964
|
-
this.#services.delete(previous);
|
|
1965
|
-
this.#options.delete(previous);
|
|
1966
|
-
this.#files.delete(previous);
|
|
1967
|
-
this.#diagnostics.delete(previous);
|
|
2246
|
+
#contain(project) {
|
|
2247
|
+
return relativeWorkspaceFile(this.#workspace, resolveWorkspaceFile(this.#workspace, project));
|
|
1968
2248
|
}
|
|
1969
|
-
#
|
|
1970
|
-
if (this.#overlay.text(file) !== void 0) return `virtual:${this.#overlay.revision}`;
|
|
2249
|
+
#remove(contained) {
|
|
1971
2250
|
try {
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2251
|
+
rmSync(this.#mirrorPath(contained), {
|
|
2252
|
+
force: true,
|
|
2253
|
+
maxRetries: 5,
|
|
2254
|
+
retryDelay: 20
|
|
2255
|
+
});
|
|
2256
|
+
} catch {}
|
|
1976
2257
|
}
|
|
1977
|
-
#
|
|
1978
|
-
const
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
const
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2258
|
+
async #configure(project) {
|
|
2259
|
+
const contained = this.#contain(project);
|
|
2260
|
+
const existing = this.#configs.get(contained);
|
|
2261
|
+
if (existing !== void 0) return existing;
|
|
2262
|
+
this.#refresh();
|
|
2263
|
+
const execution = await this.#spawn([
|
|
2264
|
+
"--showConfig",
|
|
2265
|
+
"-p",
|
|
2266
|
+
contained
|
|
2267
|
+
], this.#mirror);
|
|
2268
|
+
this.#refuseDestroyed();
|
|
2269
|
+
const config = parseProjectConfig(execution.stdout);
|
|
2270
|
+
if (config === void 0) throw this.#fault(execution, contained);
|
|
2271
|
+
this.#configs.set(contained, config);
|
|
2272
|
+
return config;
|
|
2273
|
+
}
|
|
2274
|
+
async #check(project, drafts) {
|
|
2275
|
+
const config = await this.#configure(project);
|
|
2276
|
+
const scratch = this.#scratch(this.#contain(project), config, drafts);
|
|
2277
|
+
const execution = await this.#spawn([
|
|
2278
|
+
"--noEmit",
|
|
2279
|
+
"--pretty",
|
|
2280
|
+
"false",
|
|
2281
|
+
"-p",
|
|
2282
|
+
`./${scratch}`
|
|
2283
|
+
], this.#mirror);
|
|
2284
|
+
this.#refuseDestroyed();
|
|
2285
|
+
const diagnostics = scanDiagnostics(execution.stdout);
|
|
2286
|
+
if (diagnostics.length === 0 && execution.status !== 0) {
|
|
2287
|
+
const stderr = execution.stderr.trim();
|
|
2288
|
+
const message = stderr !== "" ? stderr : execution.status === void 0 ? "The compiler reported no diagnostic and was ended by a signal" : `The compiler reported no diagnostic and exited ${execution.status}`;
|
|
2289
|
+
throw new ProbeError(this.#translate(message), {
|
|
2290
|
+
origin: "instrument",
|
|
2291
|
+
code: "malformed",
|
|
1996
2292
|
context: {
|
|
1997
2293
|
stage: this.stage,
|
|
1998
2294
|
project
|
|
1999
2295
|
}
|
|
2000
2296
|
});
|
|
2001
|
-
return {
|
|
2002
|
-
origin: "workspace",
|
|
2003
|
-
path: project,
|
|
2004
|
-
message
|
|
2005
|
-
};
|
|
2006
2297
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2298
|
+
return this.#issues(diagnostics, project);
|
|
2299
|
+
}
|
|
2300
|
+
#scratch(project, config, drafts) {
|
|
2301
|
+
const directory = dirname(project);
|
|
2302
|
+
const stem = `${basename(project, extname(project))}.probe.json`;
|
|
2303
|
+
const contained = directory === "." ? stem : `${directory}/${stem}`;
|
|
2304
|
+
const target = this.#mirrorPath(contained);
|
|
2305
|
+
const buildinfo = join(this.#mirror, ".probe", `${project.replaceAll("/", "-")}.tsbuildinfo`);
|
|
2306
|
+
const files = [...(config.files ?? []).filter((entry) => existsSync(resolve(dirname(target), entry))), ...drafts.map((draft) => relativeWorkspaceFile(dirname(target), this.#mirrorPath(this.#contain(draft.path))))];
|
|
2307
|
+
const body = {
|
|
2308
|
+
extends: `./${basename(project)}`,
|
|
2309
|
+
compilerOptions: {
|
|
2310
|
+
noEmit: true,
|
|
2311
|
+
declaration: false,
|
|
2312
|
+
emitDeclarationOnly: false,
|
|
2313
|
+
composite: false,
|
|
2314
|
+
incremental: true,
|
|
2315
|
+
tsBuildInfoFile: relativeWorkspaceFile(dirname(target), buildinfo)
|
|
2316
|
+
},
|
|
2317
|
+
...files.length === 0 ? {} : { files }
|
|
2012
2318
|
};
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2319
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
2320
|
+
writeFileSync(target, `${JSON.stringify(body, void 0, " ")}\n`, "utf8");
|
|
2321
|
+
return contained;
|
|
2322
|
+
}
|
|
2323
|
+
#issues(diagnostics, project) {
|
|
2324
|
+
const issues = [];
|
|
2325
|
+
for (const diagnostic of diagnostics) {
|
|
2326
|
+
const message = this.#translate(diagnostic.message);
|
|
2327
|
+
const resolved = diagnostic.path === void 0 ? void 0 : resolve(this.#mirror, diagnostic.path);
|
|
2328
|
+
const drafted = resolved !== void 0 && this.#drafts.has(escapesRoot(this.#mirror, resolved) ? relativeWorkspaceFile(this.#workspace, resolved) : relativeWorkspaceFile(this.#mirror, resolved));
|
|
2329
|
+
if (resolved === void 0 || !drafted && extname(resolved) === ".json") throw new ProbeError(message, {
|
|
2330
|
+
origin: "workspace",
|
|
2331
|
+
code: "malformed",
|
|
2332
|
+
context: {
|
|
2333
|
+
stage: this.stage,
|
|
2334
|
+
project
|
|
2335
|
+
}
|
|
2336
|
+
});
|
|
2337
|
+
const path = escapesRoot(this.#mirror, resolved) ? escapesRoot(this.#workspace, resolved) ? normalizePath(resolved) : relativeWorkspaceFile(this.#workspace, resolved) : relativeWorkspaceFile(this.#mirror, resolved);
|
|
2338
|
+
issues.push({
|
|
2339
|
+
origin: "claimant",
|
|
2340
|
+
path,
|
|
2341
|
+
message,
|
|
2342
|
+
...diagnostic.range === void 0 ? {} : { range: diagnostic.range }
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
return issues;
|
|
2346
|
+
}
|
|
2347
|
+
#translate(message) {
|
|
2348
|
+
return relativeWorkspaceMessage(this.#workspace, relativeWorkspaceMessage(this.#mirror, message));
|
|
2349
|
+
}
|
|
2350
|
+
#fault(execution, project) {
|
|
2351
|
+
const reported = scanDiagnostics(execution.stdout).map((diagnostic) => diagnostic.message);
|
|
2352
|
+
const text = reported.length > 0 ? reported.join("\n") : execution.stdout.trim();
|
|
2353
|
+
return new ProbeError(this.#translate(text === "" ? "The compiler printed no configuration" : text), {
|
|
2354
|
+
origin: "workspace",
|
|
2355
|
+
code: "malformed",
|
|
2356
|
+
context: {
|
|
2357
|
+
stage: this.stage,
|
|
2358
|
+
project
|
|
2020
2359
|
}
|
|
2021
|
-
};
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
#spawn(args, cwd) {
|
|
2363
|
+
this.#refuseDestroyed();
|
|
2364
|
+
return new Promise((settle, refuse) => {
|
|
2365
|
+
const child = spawn(process.execPath, [this.#compiler, ...args], {
|
|
2366
|
+
cwd,
|
|
2367
|
+
stdio: [
|
|
2368
|
+
"ignore",
|
|
2369
|
+
"pipe",
|
|
2370
|
+
"pipe"
|
|
2371
|
+
]
|
|
2372
|
+
});
|
|
2373
|
+
const output = child.stdout;
|
|
2374
|
+
const errors = child.stderr;
|
|
2375
|
+
if (output === null || errors === null) {
|
|
2376
|
+
this.#terminate(child);
|
|
2377
|
+
refuse(new ProbeError("The compiler was spawned without its own output streams", {
|
|
2378
|
+
origin: "instrument",
|
|
2379
|
+
code: "malformed",
|
|
2380
|
+
context: { stage: this.stage }
|
|
2381
|
+
}));
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
this.#children.add(child);
|
|
2385
|
+
let stdout = "";
|
|
2386
|
+
let stderr = "";
|
|
2387
|
+
output.setEncoding("utf8");
|
|
2388
|
+
errors.setEncoding("utf8");
|
|
2389
|
+
output.on("data", (chunk) => {
|
|
2390
|
+
stdout += chunk;
|
|
2391
|
+
});
|
|
2392
|
+
errors.on("data", (chunk) => {
|
|
2393
|
+
stderr += chunk;
|
|
2394
|
+
});
|
|
2395
|
+
child.on("error", (error) => {
|
|
2396
|
+
this.#children.delete(child);
|
|
2397
|
+
refuse(new ProbeError("The workspace compiler could not be started", {
|
|
2398
|
+
origin: "workspace",
|
|
2399
|
+
code: "malformed",
|
|
2400
|
+
context: {
|
|
2401
|
+
stage: this.stage,
|
|
2402
|
+
path: this.#compiler
|
|
2403
|
+
},
|
|
2404
|
+
cause: error
|
|
2405
|
+
}));
|
|
2406
|
+
});
|
|
2407
|
+
child.on("close", (code) => {
|
|
2408
|
+
this.#children.delete(child);
|
|
2409
|
+
settle({
|
|
2410
|
+
...code === null ? {} : { status: code },
|
|
2411
|
+
stdout,
|
|
2412
|
+
stderr
|
|
2413
|
+
});
|
|
2414
|
+
});
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
#terminate(child) {
|
|
2418
|
+
const id = child.pid;
|
|
2419
|
+
if (process.platform === "win32" && id !== void 0) {
|
|
2420
|
+
spawnSync("taskkill", [
|
|
2421
|
+
"/pid",
|
|
2422
|
+
String(id),
|
|
2423
|
+
"/t",
|
|
2424
|
+
"/f"
|
|
2425
|
+
], { stdio: "ignore" });
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
child.kill("SIGTERM");
|
|
2022
2429
|
}
|
|
2023
2430
|
};
|
|
2024
2431
|
//#endregion
|
|
2025
2432
|
//#region src/server/Probe.ts
|
|
2026
2433
|
/**
|
|
2027
|
-
* Answers claims through
|
|
2434
|
+
* Answers claims through its type, lint, and runtime stages.
|
|
2028
2435
|
*
|
|
2029
2436
|
* @remarks
|
|
2030
2437
|
* Construction resolves the target workspace's toolchain and begins warming every stage. The boot
|
|
@@ -2036,10 +2443,45 @@ var TypeStage = class {
|
|
|
2036
2443
|
* claim costs that claim rather than the process. A failed boot is replaced the same way: the next
|
|
2037
2444
|
* claim runs the controls again rather than inheriting a refusal.
|
|
2038
2445
|
*
|
|
2039
|
-
* @example
|
|
2446
|
+
* @example The claim that earns a receipt
|
|
2040
2447
|
* ```ts
|
|
2041
|
-
*
|
|
2448
|
+
* import type { Claim } from '@orkestrel/probe'
|
|
2449
|
+
* import { Probe } from '@orkestrel/probe/server'
|
|
2450
|
+
*
|
|
2451
|
+
* const claim: Claim = {
|
|
2452
|
+
* project: 'configs/src/tsconfig.core.json',
|
|
2453
|
+
* case: {
|
|
2454
|
+
* files: [
|
|
2455
|
+
* {
|
|
2456
|
+
* path: 'src/core/factories.ts',
|
|
2457
|
+
* text: "export function createGreeting(): string {\n\treturn 'hi'\n}\n",
|
|
2458
|
+
* },
|
|
2459
|
+
* ],
|
|
2460
|
+
* test: {
|
|
2461
|
+
* path: 'tmp/probe/greeting.test.ts',
|
|
2462
|
+
* text: "import { expect, test } from 'vitest'\nimport { createGreeting } from '../../src/core/factories.js'\ntest('greets', () => expect(createGreeting()).toBe('hi'))\n",
|
|
2463
|
+
* },
|
|
2464
|
+
* },
|
|
2465
|
+
* control: {
|
|
2466
|
+
* files: [
|
|
2467
|
+
* {
|
|
2468
|
+
* path: 'src/core/factories.ts',
|
|
2469
|
+
* text: "export function createGreeting(): number {\n\treturn 'hi'\n}\n",
|
|
2470
|
+
* },
|
|
2471
|
+
* ],
|
|
2472
|
+
* test: {
|
|
2473
|
+
* path: 'tmp/probe/greeting.test.ts',
|
|
2474
|
+
* text: "import { expect, test } from 'vitest'\nimport { createGreeting } from '../../src/core/factories.js'\ntest('greets', () => expect(createGreeting()).toBe('hi'))\n",
|
|
2475
|
+
* },
|
|
2476
|
+
* stage: 'type',
|
|
2477
|
+
* reason: 'a string returned as a number must not compile',
|
|
2478
|
+
* },
|
|
2479
|
+
* }
|
|
2480
|
+
*
|
|
2481
|
+
* const probe = new Probe({ workspace: process.cwd() })
|
|
2042
2482
|
* const verdict = await probe.prove(claim)
|
|
2483
|
+
* verdict.digest // 'fcb88a2dee987b8673c1fc7107979470'
|
|
2484
|
+
* verdict.receipt // 'probe:fcb88a2dee987b8673c1fc7107979470:type:typescript@6.0.3:oxlint@1.82.0:vitest@4.1.11:configs/src/tsconfig.core.json@434f59254d58cf2683d453a26bd0d837'
|
|
2043
2485
|
* await probe.destroy()
|
|
2044
2486
|
* ```
|
|
2045
2487
|
*/
|
|
@@ -2059,9 +2501,8 @@ var Probe = class {
|
|
|
2059
2501
|
#typeTail = Promise.resolve();
|
|
2060
2502
|
#arming;
|
|
2061
2503
|
#closing;
|
|
2062
|
-
#destroyed = false;
|
|
2063
2504
|
/**
|
|
2064
|
-
* Resolves the target toolchain and starts warming
|
|
2505
|
+
* Resolves the target toolchain and starts warming every stage.
|
|
2065
2506
|
*
|
|
2066
2507
|
* @param options - Workspace, deadline, and initial observation hooks
|
|
2067
2508
|
*/
|
|
@@ -2109,7 +2550,7 @@ var Probe = class {
|
|
|
2109
2550
|
this.#support();
|
|
2110
2551
|
this.#admit(claim);
|
|
2111
2552
|
await this.#ready();
|
|
2112
|
-
if (this.#
|
|
2553
|
+
if (this.#closing !== void 0) throw createDestroyedError("probe");
|
|
2113
2554
|
const started = performance.now();
|
|
2114
2555
|
const id = randomUUID();
|
|
2115
2556
|
const project = await this.#resolve(claim);
|
|
@@ -2143,7 +2584,6 @@ var Probe = class {
|
|
|
2143
2584
|
}
|
|
2144
2585
|
destroy() {
|
|
2145
2586
|
if (this.#closing !== void 0) return this.#closing;
|
|
2146
|
-
this.#destroyed = true;
|
|
2147
2587
|
this.#closing = this.#destroy();
|
|
2148
2588
|
return this.#closing;
|
|
2149
2589
|
}
|
|
@@ -2153,7 +2593,7 @@ var Probe = class {
|
|
|
2153
2593
|
await attempt;
|
|
2154
2594
|
return;
|
|
2155
2595
|
} catch (error) {
|
|
2156
|
-
if (this.#
|
|
2596
|
+
if (this.#closing !== void 0) throw error;
|
|
2157
2597
|
}
|
|
2158
2598
|
if (this.#arming === attempt) {
|
|
2159
2599
|
this.#arming = this.#arm();
|
|
@@ -2205,8 +2645,8 @@ var Probe = class {
|
|
|
2205
2645
|
}
|
|
2206
2646
|
async #boot(created) {
|
|
2207
2647
|
const revision = `${process.pid}-${randomUUID()}`;
|
|
2208
|
-
const typeDependency =
|
|
2209
|
-
const runtimeDependency =
|
|
2648
|
+
const typeDependency = buildRevisionPath(this.#workspace, "tmp/probe/arm-type.ts", revision);
|
|
2649
|
+
const runtimeDependency = buildRevisionPath(this.#workspace, "tmp/probe/arm-runtime.ts", revision);
|
|
2210
2650
|
const typeModule = basename(typeDependency, ".ts");
|
|
2211
2651
|
const runtimeModule = basename(runtimeDependency, ".ts");
|
|
2212
2652
|
const typeTest = {
|
|
@@ -2227,7 +2667,7 @@ var Probe = class {
|
|
|
2227
2667
|
files: [],
|
|
2228
2668
|
test: typeTest,
|
|
2229
2669
|
stage: "type",
|
|
2230
|
-
reason: "the imported type changed
|
|
2670
|
+
reason: "the imported type changed on disk between the two inspections"
|
|
2231
2671
|
}
|
|
2232
2672
|
};
|
|
2233
2673
|
const runtimeClaim = {
|
|
@@ -2347,7 +2787,7 @@ var Probe = class {
|
|
|
2347
2787
|
}
|
|
2348
2788
|
}
|
|
2349
2789
|
async #recycle(stage) {
|
|
2350
|
-
if (this.#
|
|
2790
|
+
if (this.#closing !== void 0) return false;
|
|
2351
2791
|
const timeout = createTimeout({ ms: this.#deadline });
|
|
2352
2792
|
timeout.start();
|
|
2353
2793
|
try {
|
|
@@ -2355,7 +2795,7 @@ var Probe = class {
|
|
|
2355
2795
|
} catch {} finally {
|
|
2356
2796
|
timeout.clear();
|
|
2357
2797
|
}
|
|
2358
|
-
if (this.#
|
|
2798
|
+
if (this.#closing !== void 0) return false;
|
|
2359
2799
|
if (stage === this.#type) {
|
|
2360
2800
|
this.#type = new TypeStage(this.#workspace);
|
|
2361
2801
|
return true;
|
|
@@ -2413,13 +2853,17 @@ var Probe = class {
|
|
|
2413
2853
|
}
|
|
2414
2854
|
async #destroy() {
|
|
2415
2855
|
try {
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2856
|
+
try {
|
|
2857
|
+
await this.#arming;
|
|
2858
|
+
} catch {}
|
|
2859
|
+
await Promise.all([
|
|
2860
|
+
this.#destroyStage(this.#type),
|
|
2861
|
+
this.#destroyStage(this.#lint),
|
|
2862
|
+
this.#destroyStage(this.#runtime)
|
|
2863
|
+
]);
|
|
2864
|
+
} finally {
|
|
2865
|
+
this.#emitter.destroy();
|
|
2866
|
+
}
|
|
2423
2867
|
}
|
|
2424
2868
|
async #destroyStage(stage) {
|
|
2425
2869
|
try {
|
|
@@ -2470,7 +2914,9 @@ var Probe = class {
|
|
|
2470
2914
|
//#endregion
|
|
2471
2915
|
//#region src/server/ProbeServer.ts
|
|
2472
2916
|
/**
|
|
2473
|
-
*
|
|
2917
|
+
* Implements `ProbeServerInterface` over a `PassThrough` stream this server owns, binding the
|
|
2918
|
+
* published `prove` tool and the dual-era dispatcher to this process's Model Context Protocol
|
|
2919
|
+
* stdio transport.
|
|
2474
2920
|
*
|
|
2475
2921
|
* @remarks
|
|
2476
2922
|
* Construction creates the probe, publishes the `prove` tool, and binds the dual-era dispatcher to
|
|
@@ -2636,6 +3082,6 @@ var ProbeServer = class {
|
|
|
2636
3082
|
}
|
|
2637
3083
|
};
|
|
2638
3084
|
//#endregion
|
|
2639
|
-
export { LintStage, Overlay, Probe, ProbeServer, RuntimeStage, TypeStage, captureListeners,
|
|
3085
|
+
export { LintStage, Overlay, Probe, ProbeServer, RuntimeStage, TypeStage, buildRevisionPath, captureListeners, collectWorkspaceFiles, computeDigest, describeUnknown, escapesRoot, filterUniqueIssues, findRefusedPaths, guardStage, inferDocumentLanguage, inferTestProject, inferTypeProject, isRefusedName, loadWorkspaceVitest, matchesLiveProcess, matchesWorkspaceModule, normalizePath, normalizeValue, overwriteFile, parseProjectConfig, parseRevisionOwner, readFaultCode, readWorkspaceManifest, relativeWorkspaceFile, relativeWorkspaceMessage, releaseListeners, resolveWorkspaceBinary, resolveWorkspaceFile, resolveWorkspaceModule, scanDiagnostics };
|
|
2640
3086
|
|
|
2641
3087
|
//# sourceMappingURL=index.js.map
|