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