@orkestrel/probe 0.0.12 → 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/src/core/index.cjs +43 -17
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +148 -96
- package/dist/src/core/index.d.ts +148 -96
- package/dist/src/core/index.js +43 -18
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +712 -316
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +447 -136
- package/dist/src/server/index.d.ts +447 -136
- package/dist/src/server/index.js +707 -317
- package/dist/src/server/index.js.map +1 -1
- package/package.json +18 -19
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, PROBE_SPECIFICATIONS, ProbeError, RUNTIME_PLUGIN, 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";
|
|
@@ -324,6 +324,65 @@ function relativeWorkspaceMessage(workspace, message) {
|
|
|
324
324
|
return text;
|
|
325
325
|
}
|
|
326
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
|
+
/**
|
|
327
386
|
* Resolves one installed module from the target workspace.
|
|
328
387
|
*
|
|
329
388
|
* @param workspace - The target workspace root
|
|
@@ -349,18 +408,35 @@ function resolveWorkspaceModule(workspace, specifier) {
|
|
|
349
408
|
cause: error
|
|
350
409
|
});
|
|
351
410
|
}
|
|
352
|
-
|
|
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) {
|
|
353
430
|
const outcome = attempt(() => {
|
|
354
|
-
|
|
355
|
-
return specifier === "typescript" ? require("typescript") : require("vitest/node");
|
|
431
|
+
return createRequire(resolve(workspace, "package.json"))("vitest/node");
|
|
356
432
|
});
|
|
357
433
|
if (outcome.success) return outcome.value;
|
|
358
434
|
const error = outcome.error;
|
|
359
435
|
const missing = readFaultCode(error) === "MODULE_NOT_FOUND";
|
|
360
|
-
throw new ProbeError(
|
|
436
|
+
throw new ProbeError("The workspace cannot load vitest/node", {
|
|
361
437
|
origin: "workspace",
|
|
362
438
|
code: missing ? "missing" : "malformed",
|
|
363
|
-
context: { name:
|
|
439
|
+
context: { name: "vitest/node" },
|
|
364
440
|
cause: error
|
|
365
441
|
});
|
|
366
442
|
}
|
|
@@ -433,20 +509,26 @@ function readWorkspaceManifest(workspace, name) {
|
|
|
433
509
|
/**
|
|
434
510
|
* Resolves a package's portable JavaScript binary from the target workspace.
|
|
435
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
|
+
*
|
|
436
517
|
* @param workspace - The target workspace root
|
|
437
|
-
* @param name - The installed package name
|
|
518
|
+
* @param name - The installed package name
|
|
519
|
+
* @param command - The `bin` key to read. Default: `name`
|
|
438
520
|
* @returns The absolute JavaScript entry named by the package's `bin` field
|
|
439
|
-
* @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
|
|
440
522
|
*
|
|
441
523
|
* @example
|
|
442
524
|
* ```ts
|
|
443
525
|
* relativeWorkspaceFile(process.cwd(), resolveWorkspaceBinary(process.cwd(), 'oxlint'))
|
|
444
526
|
* // 'node_modules/oxlint/bin/oxlint'
|
|
445
|
-
* resolveWorkspaceBinary(process.cwd(), 'typescript')
|
|
446
|
-
* //
|
|
527
|
+
* relativeWorkspaceFile(process.cwd(), resolveWorkspaceBinary(process.cwd(), 'typescript', 'tsc'))
|
|
528
|
+
* // 'node_modules/typescript/bin/tsc'
|
|
447
529
|
* ```
|
|
448
530
|
*/
|
|
449
|
-
function resolveWorkspaceBinary(workspace, name) {
|
|
531
|
+
function resolveWorkspaceBinary(workspace, name, command = name) {
|
|
450
532
|
const manifest = readWorkspaceManifest(workspace, name);
|
|
451
533
|
const bin = manifest.contents.bin;
|
|
452
534
|
if (bin === void 0) throw new ProbeError(`${name} does not publish a bin field`, {
|
|
@@ -455,13 +537,13 @@ function resolveWorkspaceBinary(workspace, name) {
|
|
|
455
537
|
context: { name }
|
|
456
538
|
});
|
|
457
539
|
if (typeof bin === "string") return resolve(manifest.path, "..", bin);
|
|
458
|
-
if (!isRecord(bin) || !(
|
|
540
|
+
if (!isRecord(bin) || !(command in bin)) throw new ProbeError(`${name} does not publish the ${command} binary`, {
|
|
459
541
|
origin: "workspace",
|
|
460
542
|
code: "missing",
|
|
461
543
|
context: { name }
|
|
462
544
|
});
|
|
463
|
-
const entry = bin[
|
|
464
|
-
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`, {
|
|
465
547
|
origin: "workspace",
|
|
466
548
|
code: "malformed",
|
|
467
549
|
context: {
|
|
@@ -562,6 +644,39 @@ function buildRevisionPath(workspace, path, revision) {
|
|
|
562
644
|
return `${extension === "" ? file : file.slice(0, -extension.length)}.probe-${revision}${extension}`;
|
|
563
645
|
}
|
|
564
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
|
+
/**
|
|
565
680
|
* Reports whether a path is a workspace module Vitest can cache.
|
|
566
681
|
*
|
|
567
682
|
* @param path - The candidate file path
|
|
@@ -577,6 +692,75 @@ function matchesWorkspaceModule(path) {
|
|
|
577
692
|
return /\.(?:[cm]?[jt]sx?|vue|json)$/.test(path);
|
|
578
693
|
}
|
|
579
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
|
+
/**
|
|
580
764
|
* Normalizes a caught or foreign error into readable text.
|
|
581
765
|
*
|
|
582
766
|
* @param value - The value to describe
|
|
@@ -595,9 +779,9 @@ function describeUnknown(value) {
|
|
|
595
779
|
return String(value);
|
|
596
780
|
}
|
|
597
781
|
/**
|
|
598
|
-
* Guards one
|
|
782
|
+
* Guards one stage operation with the stage failure contract.
|
|
599
783
|
*
|
|
600
|
-
* @param stage - The
|
|
784
|
+
* @param stage - The stage serving the operation
|
|
601
785
|
* @param operation - The operation to settle
|
|
602
786
|
* @returns The operation's fulfilled value
|
|
603
787
|
* @throws The original `ProbeError`, or an instrument-owned malformed failure retaining the cause
|
|
@@ -669,12 +853,12 @@ function findRefusedPaths(value) {
|
|
|
669
853
|
* sorts every record's keys.
|
|
670
854
|
*
|
|
671
855
|
* @remarks
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
* 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.
|
|
678
862
|
*
|
|
679
863
|
* @param workspace - The target workspace root
|
|
680
864
|
* @param value - The value to rewrite
|
|
@@ -775,9 +959,66 @@ function releaseListeners(emitter, capture) {
|
|
|
775
959
|
}
|
|
776
960
|
}
|
|
777
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
|
|
778
1018
|
//#region src/server/Overlay.ts
|
|
779
1019
|
/**
|
|
780
|
-
*
|
|
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.
|
|
781
1022
|
*
|
|
782
1023
|
* @remarks
|
|
783
1024
|
* One overlay belongs to one inspection: the inspection creates it, records every candidate it
|
|
@@ -786,55 +1027,44 @@ function releaseListeners(emitter, capture) {
|
|
|
786
1027
|
* is what makes a resident tool re-read a path this overlay holds, so two inspections that supply
|
|
787
1028
|
* different text for one path never share a cached answer.
|
|
788
1029
|
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
* file
|
|
794
|
-
*
|
|
795
|
-
* against, so a folded key never reaches the paths a tool is handed and containment stays an exact
|
|
796
|
-
* 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.
|
|
797
1036
|
*
|
|
798
1037
|
* @example
|
|
799
1038
|
* ```ts
|
|
800
1039
|
* const overlay = new Overlay()
|
|
801
|
-
*
|
|
802
|
-
*
|
|
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))
|
|
803
1043
|
* overlay.clear()
|
|
804
1044
|
* ```
|
|
805
1045
|
*/
|
|
806
1046
|
var Overlay = class {
|
|
807
1047
|
#revision = randomUUID();
|
|
808
|
-
#sensitive;
|
|
809
1048
|
#candidates = /* @__PURE__ */ new Map();
|
|
810
|
-
/**
|
|
811
|
-
* Creates an empty candidate set under a fresh identity.
|
|
812
|
-
*
|
|
813
|
-
* @param options - Construction options
|
|
814
|
-
*/
|
|
815
|
-
constructor(options = {}) {
|
|
816
|
-
this.#sensitive = options.sensitive ?? true;
|
|
817
|
-
}
|
|
818
1049
|
get revision() {
|
|
819
1050
|
return this.#revision;
|
|
820
1051
|
}
|
|
821
1052
|
get paths() {
|
|
822
|
-
return [...this.#candidates.
|
|
1053
|
+
return [...this.#candidates.keys()];
|
|
823
1054
|
}
|
|
824
1055
|
/**
|
|
825
1056
|
* Records one candidate's text against the absolute path it stands in for.
|
|
826
1057
|
*
|
|
827
1058
|
* @remarks
|
|
828
|
-
* Recording a candidate under a path this overlay
|
|
829
|
-
*
|
|
1059
|
+
* Recording a candidate under a path this overlay already holds replaces that candidate, the way
|
|
1060
|
+
* one file holds one text.
|
|
830
1061
|
*
|
|
831
1062
|
* @param path - The absolute path the candidate replaces
|
|
832
1063
|
* @param text - The candidate's full contents
|
|
833
1064
|
* @returns Nothing
|
|
834
1065
|
*/
|
|
835
1066
|
set(path, text) {
|
|
836
|
-
|
|
837
|
-
this.#candidates.set(this.#key(recorded), [recorded, text]);
|
|
1067
|
+
this.#candidates.set(normalizePath(path), text);
|
|
838
1068
|
}
|
|
839
1069
|
/**
|
|
840
1070
|
* Reads the candidate text recorded for one absolute path.
|
|
@@ -843,7 +1073,7 @@ var Overlay = class {
|
|
|
843
1073
|
* @returns The recorded text, or `undefined` when this overlay holds no candidate there
|
|
844
1074
|
*/
|
|
845
1075
|
text(path) {
|
|
846
|
-
return this.#candidates.get(
|
|
1076
|
+
return this.#candidates.get(normalizePath(path));
|
|
847
1077
|
}
|
|
848
1078
|
/**
|
|
849
1079
|
* Checks whether a candidate sits beneath one directory.
|
|
@@ -852,16 +1082,14 @@ var Overlay = class {
|
|
|
852
1082
|
* The answer is derived from the paths the overlay holds rather than stored, so it stops being
|
|
853
1083
|
* true exactly when the inspection that declared those candidates clears them. Both sides pass
|
|
854
1084
|
* through `normalizePath` first, because a tool that normalizes its own paths asks about a
|
|
855
|
-
* directory in a spelling the recorded path may not share.
|
|
856
|
-
* containment comparison, and it reads the recorded spelling whatever an overlay minted for a
|
|
857
|
-
* case-folding host matches its lookup keys by.
|
|
1085
|
+
* directory in a spelling the recorded path may not share.
|
|
858
1086
|
*
|
|
859
1087
|
* @param directory - The absolute directory path to check
|
|
860
1088
|
* @returns True if a candidate path sits beneath the directory; false otherwise
|
|
861
1089
|
*/
|
|
862
1090
|
covers(directory) {
|
|
863
1091
|
const base = `${normalizePath(directory).replace(/\/+$/, "")}/`;
|
|
864
|
-
for (const
|
|
1092
|
+
for (const path of this.#candidates.keys()) if (path.startsWith(base)) return true;
|
|
865
1093
|
return false;
|
|
866
1094
|
}
|
|
867
1095
|
/**
|
|
@@ -872,15 +1100,12 @@ var Overlay = class {
|
|
|
872
1100
|
clear() {
|
|
873
1101
|
this.#candidates.clear();
|
|
874
1102
|
}
|
|
875
|
-
#key(path) {
|
|
876
|
-
return this.#sensitive ? path : path.toLowerCase();
|
|
877
|
-
}
|
|
878
1103
|
};
|
|
879
1104
|
//#endregion
|
|
880
1105
|
//#region package.json
|
|
881
|
-
var version = "0.0.
|
|
1106
|
+
var version = "0.0.13";
|
|
882
1107
|
var peerDependencies = {
|
|
883
|
-
"oxlint": "^1.
|
|
1108
|
+
"oxlint": "^1.82.0",
|
|
884
1109
|
"typescript": "^6.0.3",
|
|
885
1110
|
"vitest": "^4.1.11"
|
|
886
1111
|
};
|
|
@@ -1165,7 +1390,7 @@ var RuntimeStage = class {
|
|
|
1165
1390
|
*/
|
|
1166
1391
|
constructor(workspace = process.cwd()) {
|
|
1167
1392
|
this.#workspace = workspace;
|
|
1168
|
-
const vitest =
|
|
1393
|
+
const vitest = loadWorkspaceVitest(this.#workspace);
|
|
1169
1394
|
this.#store(this.#warm(vitest.createVitest));
|
|
1170
1395
|
}
|
|
1171
1396
|
get stage() {
|
|
@@ -1174,9 +1399,29 @@ var RuntimeStage = class {
|
|
|
1174
1399
|
get progress() {
|
|
1175
1400
|
return this.#progress;
|
|
1176
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
|
+
*/
|
|
1177
1409
|
inspect(subject) {
|
|
1178
1410
|
return guardStage(this.stage, this.#inspect(subject));
|
|
1179
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
|
+
*/
|
|
1180
1425
|
destroy() {
|
|
1181
1426
|
if (this.#closing !== void 0) return this.#closing;
|
|
1182
1427
|
this.#closing = guardStage(this.stage, this.#destroy());
|
|
@@ -1520,13 +1765,13 @@ var RuntimeStage = class {
|
|
|
1520
1765
|
const current = this.#vitest;
|
|
1521
1766
|
if (current !== void 0 && this.#specifications < PROBE_SPECIFICATIONS) return current;
|
|
1522
1767
|
if (current !== void 0) return this.#store(this.#replace(current));
|
|
1523
|
-
const runner =
|
|
1768
|
+
const runner = loadWorkspaceVitest(this.#workspace);
|
|
1524
1769
|
return this.#store(this.#specifications < PROBE_SPECIFICATIONS ? this.#warm(runner.createVitest) : this.#replace(void 0, runner.createVitest));
|
|
1525
1770
|
}
|
|
1526
1771
|
async #replace(current, create) {
|
|
1527
1772
|
if (current !== void 0) await (await current).close();
|
|
1528
1773
|
if (this.#closing !== void 0) throw createDestroyedError("runtime stage");
|
|
1529
|
-
const runner = create ??
|
|
1774
|
+
const runner = create ?? loadWorkspaceVitest(this.#workspace).createVitest;
|
|
1530
1775
|
const replacement = await this.#warm(runner);
|
|
1531
1776
|
this.#specifications = 0;
|
|
1532
1777
|
return replacement;
|
|
@@ -1570,7 +1815,7 @@ var RuntimeStage = class {
|
|
|
1570
1815
|
}
|
|
1571
1816
|
#snapshot() {
|
|
1572
1817
|
const modules = /* @__PURE__ */ new Map();
|
|
1573
|
-
for (const path of this.#
|
|
1818
|
+
for (const path of collectWorkspaceFiles(this.#workspace)) {
|
|
1574
1819
|
if (!matchesWorkspaceModule(path)) continue;
|
|
1575
1820
|
try {
|
|
1576
1821
|
const digest = createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
@@ -1581,12 +1826,12 @@ var RuntimeStage = class {
|
|
|
1581
1826
|
return modules;
|
|
1582
1827
|
}
|
|
1583
1828
|
#sweep() {
|
|
1584
|
-
for (const path of this.#
|
|
1829
|
+
for (const path of collectWorkspaceFiles(this.#workspace)) {
|
|
1585
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);
|
|
1586
1831
|
const revision = match?.[1];
|
|
1587
1832
|
const owner = match?.[2];
|
|
1588
1833
|
if (revision === void 0 || owner === void 0) continue;
|
|
1589
|
-
if (
|
|
1834
|
+
if (matchesLiveProcess(Number.parseInt(owner, 10))) continue;
|
|
1590
1835
|
if (!this.#owned(path, revision)) continue;
|
|
1591
1836
|
try {
|
|
1592
1837
|
unlinkSync(resolveWorkspaceFile(this.#workspace, relative(this.#workspace, path), true));
|
|
@@ -1597,35 +1842,6 @@ var RuntimeStage = class {
|
|
|
1597
1842
|
const outcome = attempt(() => readFileSync(path, "utf8"));
|
|
1598
1843
|
return outcome.success && matchesSpecification(outcome.value, revision);
|
|
1599
1844
|
}
|
|
1600
|
-
#alive(id) {
|
|
1601
|
-
if (!Number.isSafeInteger(id) || id <= 0) return true;
|
|
1602
|
-
try {
|
|
1603
|
-
process.kill(id, 0);
|
|
1604
|
-
return true;
|
|
1605
|
-
} catch (error) {
|
|
1606
|
-
return readFaultCode(error) === "EPERM";
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
*#walk() {
|
|
1610
|
-
const directories = [resolve(this.#workspace)];
|
|
1611
|
-
while (directories.length > 0) {
|
|
1612
|
-
const directory = directories.pop();
|
|
1613
|
-
if (directory === void 0) break;
|
|
1614
|
-
let entries = [];
|
|
1615
|
-
try {
|
|
1616
|
-
entries = readdirSync(directory, { withFileTypes: true });
|
|
1617
|
-
} catch {}
|
|
1618
|
-
for (const entry of entries) {
|
|
1619
|
-
if (entry.name === ".git" || entry.name === "dist" || entry.name === "node_modules") continue;
|
|
1620
|
-
const path = join(directory, entry.name);
|
|
1621
|
-
if (entry.isDirectory()) {
|
|
1622
|
-
directories.push(path);
|
|
1623
|
-
continue;
|
|
1624
|
-
}
|
|
1625
|
-
if (entry.isFile()) yield path;
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
1845
|
#issues(result, file, original) {
|
|
1630
1846
|
const specification = this.#real(file);
|
|
1631
1847
|
const issues = [];
|
|
@@ -1741,21 +1957,32 @@ var RuntimeStage = class {
|
|
|
1741
1957
|
//#endregion
|
|
1742
1958
|
//#region src/server/stages/TypeStage.ts
|
|
1743
1959
|
/**
|
|
1744
|
-
* Inspects TypeScript source
|
|
1960
|
+
* Inspects TypeScript source by running the target workspace's own compiler over a mirror of it.
|
|
1745
1961
|
*
|
|
1746
1962
|
* @remarks
|
|
1747
|
-
*
|
|
1748
|
-
*
|
|
1749
|
-
*
|
|
1750
|
-
*
|
|
1751
|
-
*
|
|
1752
|
-
*
|
|
1753
|
-
*
|
|
1754
|
-
*
|
|
1755
|
-
*
|
|
1756
|
-
*
|
|
1757
|
-
*
|
|
1758
|
-
*
|
|
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.
|
|
1759
1986
|
*
|
|
1760
1987
|
* @example
|
|
1761
1988
|
* ```ts
|
|
@@ -1766,26 +1993,27 @@ var RuntimeStage = class {
|
|
|
1766
1993
|
*/
|
|
1767
1994
|
var TypeStage = class {
|
|
1768
1995
|
#workspace;
|
|
1769
|
-
#
|
|
1770
|
-
#
|
|
1771
|
-
#
|
|
1772
|
-
#
|
|
1773
|
-
#
|
|
1774
|
-
#
|
|
1775
|
-
#
|
|
1776
|
-
#
|
|
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;
|
|
1777
2004
|
#closing;
|
|
1778
2005
|
#progress = 0;
|
|
1779
2006
|
/**
|
|
1780
|
-
*
|
|
2007
|
+
* Resolves the target workspace's compiler and starts warming its mirror.
|
|
1781
2008
|
*
|
|
1782
2009
|
* @param workspace - The target workspace root. Default: the current working directory
|
|
1783
2010
|
*/
|
|
1784
2011
|
constructor(workspace = process.cwd()) {
|
|
1785
2012
|
this.#workspace = workspace;
|
|
1786
|
-
|
|
1787
|
-
this.#
|
|
1788
|
-
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(() => {});
|
|
1789
2017
|
}
|
|
1790
2018
|
get stage() {
|
|
1791
2019
|
return "type";
|
|
@@ -1805,7 +2033,8 @@ var TypeStage = class {
|
|
|
1805
2033
|
* @param project - The workspace-relative TypeScript project the candidate drafts are checked
|
|
1806
2034
|
* against. Default: the scoped project each candidate path infers
|
|
1807
2035
|
* @returns One outcome for this stage
|
|
1808
|
-
* @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
|
|
1809
2038
|
*/
|
|
1810
2039
|
inspect(subject, project) {
|
|
1811
2040
|
return guardStage(this.stage, this.#inspect(subject, project));
|
|
@@ -1814,15 +2043,15 @@ var TypeStage = class {
|
|
|
1814
2043
|
* Resolves one project to the path and digest this stage applies for it.
|
|
1815
2044
|
*
|
|
1816
2045
|
* @remarks
|
|
1817
|
-
* Reads the
|
|
1818
|
-
*
|
|
1819
|
-
*
|
|
1820
|
-
*
|
|
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.
|
|
1821
2050
|
*
|
|
1822
2051
|
* @param project - The workspace-relative TypeScript project to resolve
|
|
1823
2052
|
* @returns The resolved workspace-relative path and the digest of its compiler options
|
|
1824
|
-
* @throws When the project escapes the workspace,
|
|
1825
|
-
* been destroyed
|
|
2053
|
+
* @throws When the project escapes the workspace, when the compiler refuses it, or when the
|
|
2054
|
+
* stage has already been destroyed
|
|
1826
2055
|
*/
|
|
1827
2056
|
resolve(project) {
|
|
1828
2057
|
return guardStage(this.stage, this.#resolve(project));
|
|
@@ -1833,81 +2062,76 @@ var TypeStage = class {
|
|
|
1833
2062
|
return this.#closing;
|
|
1834
2063
|
}
|
|
1835
2064
|
async #inspect(subject, project) {
|
|
1836
|
-
|
|
2065
|
+
this.#refuseDestroyed();
|
|
1837
2066
|
const started = performance.now();
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
const
|
|
2067
|
+
await this.#warming;
|
|
2068
|
+
this.#refuseDestroyed();
|
|
2069
|
+
const test = resolveWorkspaceFile(this.#workspace, subject.test.path);
|
|
2070
|
+
const resolved = subject.files.map((draft) => ({
|
|
1841
2071
|
draft,
|
|
1842
2072
|
path: resolveWorkspaceFile(this.#workspace, draft.path)
|
|
1843
|
-
})).map((candidate) => ({
|
|
1844
|
-
draft: candidate.draft,
|
|
1845
|
-
project: project ?? inferTypeProject(relativeWorkspaceFile(this.#workspace, candidate.path))
|
|
1846
2073
|
}));
|
|
1847
|
-
const
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
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);
|
|
1853
2080
|
}
|
|
1854
|
-
|
|
1855
|
-
this.#
|
|
2081
|
+
this.#progress += 1;
|
|
2082
|
+
for (const selected of groups.keys()) await this.#configure(selected);
|
|
2083
|
+
this.#refuseDestroyed();
|
|
1856
2084
|
try {
|
|
1857
|
-
this.#
|
|
1858
|
-
|
|
1859
|
-
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));
|
|
1860
2088
|
const issues = [];
|
|
1861
|
-
const
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
await this.#unblock();
|
|
1865
|
-
for (const selection of selections) {
|
|
1866
|
-
const draft = selection.draft;
|
|
1867
|
-
const selected = selection.project;
|
|
1868
|
-
const service = this.#service(typescript, selected);
|
|
1869
|
-
issues.push(...this.#issues(typescript, service, draft, selected, project !== void 0, !projects.has(selected)));
|
|
1870
|
-
projects.add(selected);
|
|
1871
|
-
await this.#unblock();
|
|
2089
|
+
for (const [selected, drafts] of groups) {
|
|
2090
|
+
this.#refuseDestroyed();
|
|
2091
|
+
issues.push(...await this.#check(selected, drafts));
|
|
1872
2092
|
}
|
|
1873
2093
|
return {
|
|
1874
2094
|
stage: this.stage,
|
|
1875
2095
|
elapsed: Math.round(performance.now() - started),
|
|
1876
|
-
issues
|
|
2096
|
+
issues: filterUniqueIssues(issues)
|
|
1877
2097
|
};
|
|
1878
2098
|
} finally {
|
|
1879
|
-
|
|
2099
|
+
this.#release();
|
|
1880
2100
|
}
|
|
1881
2101
|
}
|
|
1882
2102
|
async #resolve(project) {
|
|
1883
|
-
|
|
1884
|
-
const
|
|
1885
|
-
if (this.#closing !== void 0) throw createDestroyedError("type stage");
|
|
1886
|
-
const resolved = resolveWorkspaceFile(this.#workspace, project);
|
|
2103
|
+
this.#refuseDestroyed();
|
|
2104
|
+
const contained = this.#contain(project);
|
|
1887
2105
|
this.#progress += 1;
|
|
1888
|
-
this.#
|
|
1889
|
-
await this.#unblock();
|
|
2106
|
+
const config = await this.#configure(contained);
|
|
1890
2107
|
return {
|
|
1891
|
-
path:
|
|
1892
|
-
digest: computeDigest(this.#workspace,
|
|
2108
|
+
path: contained,
|
|
2109
|
+
digest: computeDigest(this.#workspace, config.compilerOptions)
|
|
1893
2110
|
};
|
|
1894
2111
|
}
|
|
1895
2112
|
async #destroy() {
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
this.#
|
|
1899
|
-
this.#
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
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, [])));
|
|
1911
2135
|
}
|
|
1912
2136
|
#projects() {
|
|
1913
2137
|
const projects = ["tsconfig.json"];
|
|
@@ -1923,162 +2147,291 @@ var TypeStage = class {
|
|
|
1923
2147
|
}
|
|
1924
2148
|
return projects;
|
|
1925
2149
|
}
|
|
1926
|
-
|
|
1927
|
-
await setTimeout(0);
|
|
2150
|
+
#refuseDestroyed() {
|
|
1928
2151
|
if (this.#closing !== void 0) throw createDestroyedError("type stage");
|
|
1929
2152
|
}
|
|
1930
|
-
#
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
if (existing !== void 0) return existing;
|
|
1937
|
-
const spelling = normalizePath(path);
|
|
1938
|
-
const config = typescript.readConfigFile(spelling, typescript.sys.readFile);
|
|
1939
|
-
if (config.error !== void 0) throw new ProbeError(this.#translate(typescript, config.error.messageText), {
|
|
1940
|
-
origin: "workspace",
|
|
1941
|
-
code: "malformed",
|
|
1942
|
-
context: {
|
|
1943
|
-
stage: this.stage,
|
|
1944
|
-
project
|
|
1945
|
-
}
|
|
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"
|
|
1946
2159
|
});
|
|
1947
|
-
const parsed = typescript.parseJsonConfigFileContent(config.config, typescript.sys, dirname(spelling), void 0, spelling);
|
|
1948
|
-
if (parsed.errors.length > 0) throw new ProbeError(this.#translate(typescript, parsed.errors[0]?.messageText), {
|
|
1949
|
-
origin: "workspace",
|
|
1950
|
-
code: "malformed",
|
|
1951
|
-
context: {
|
|
1952
|
-
stage: this.stage,
|
|
1953
|
-
project
|
|
1954
|
-
}
|
|
1955
|
-
});
|
|
1956
|
-
this.#options.set(path, parsed.options);
|
|
1957
|
-
this.#files.set(path, parsed.fileNames);
|
|
1958
|
-
const host = {
|
|
1959
|
-
getCompilationSettings: this.#compilationSettings.bind(this, path),
|
|
1960
|
-
getScriptFileNames: this.#scriptFiles.bind(this, path),
|
|
1961
|
-
getScriptVersion: this.#version.bind(this),
|
|
1962
|
-
getScriptSnapshot: this.#snapshot.bind(this, typescript),
|
|
1963
|
-
getCurrentDirectory: this.#directory.bind(this),
|
|
1964
|
-
getDefaultLibFileName: this.#defaultLibrary.bind(this, typescript),
|
|
1965
|
-
fileExists: this.#fileExists.bind(this, typescript),
|
|
1966
|
-
readFile: this.#readFile.bind(this, typescript),
|
|
1967
|
-
readDirectory: typescript.sys.readDirectory,
|
|
1968
|
-
directoryExists: this.#directoryExists.bind(this, typescript),
|
|
1969
|
-
getDirectories: typescript.sys.getDirectories,
|
|
1970
|
-
useCaseSensitiveFileNames: this.#caseSensitive.bind(this, typescript),
|
|
1971
|
-
getNewLine: this.#newline.bind(this, typescript)
|
|
1972
|
-
};
|
|
1973
|
-
const service = typescript.createLanguageService(host);
|
|
1974
|
-
this.#services.set(path, service);
|
|
1975
|
-
if (!this.#resident.has(path)) this.#recycle(path);
|
|
1976
|
-
return service;
|
|
1977
|
-
}
|
|
1978
|
-
#compilationSettings(path) {
|
|
1979
|
-
return this.#options.get(path) ?? {};
|
|
1980
|
-
}
|
|
1981
|
-
#scriptFiles(path) {
|
|
1982
|
-
return [...this.#files.get(path) ?? [], ...this.#overlay.paths];
|
|
1983
|
-
}
|
|
1984
|
-
#directory() {
|
|
1985
|
-
return this.#workspace;
|
|
1986
|
-
}
|
|
1987
|
-
#defaultLibrary(typescript, options) {
|
|
1988
|
-
return typescript.getDefaultLibFilePath(options);
|
|
1989
|
-
}
|
|
1990
|
-
#fileExists(typescript, file) {
|
|
1991
|
-
return this.#overlay.text(file) !== void 0 || typescript.sys.fileExists(file);
|
|
1992
|
-
}
|
|
1993
|
-
#readFile(typescript, file) {
|
|
1994
|
-
return this.#overlay.text(file) ?? typescript.sys.readFile(file);
|
|
1995
2160
|
}
|
|
1996
|
-
#
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
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
|
+
}
|
|
2001
2184
|
}
|
|
2002
|
-
#
|
|
2003
|
-
|
|
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
|
+
}
|
|
2004
2202
|
}
|
|
2005
|
-
#
|
|
2006
|
-
|
|
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
|
+
}
|
|
2007
2242
|
}
|
|
2008
|
-
#
|
|
2009
|
-
|
|
2010
|
-
if (this.#diagnostics.has(path)) return;
|
|
2011
|
-
this.#diagnostics.set(path, service.getCompilerOptionsDiagnostics());
|
|
2243
|
+
#mirrorPath(contained) {
|
|
2244
|
+
return join(this.#mirror, contained);
|
|
2012
2245
|
}
|
|
2013
|
-
#
|
|
2014
|
-
|
|
2015
|
-
this.#recycled = path;
|
|
2016
|
-
if (previous === void 0 || previous === path) return;
|
|
2017
|
-
this.#services.get(previous)?.dispose();
|
|
2018
|
-
this.#services.delete(previous);
|
|
2019
|
-
this.#options.delete(previous);
|
|
2020
|
-
this.#files.delete(previous);
|
|
2021
|
-
this.#diagnostics.delete(previous);
|
|
2246
|
+
#contain(project) {
|
|
2247
|
+
return relativeWorkspaceFile(this.#workspace, resolveWorkspaceFile(this.#workspace, project));
|
|
2022
2248
|
}
|
|
2023
|
-
#
|
|
2024
|
-
if (this.#overlay.text(file) !== void 0) return `virtual:${this.#overlay.revision}`;
|
|
2249
|
+
#remove(contained) {
|
|
2025
2250
|
try {
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2251
|
+
rmSync(this.#mirrorPath(contained), {
|
|
2252
|
+
force: true,
|
|
2253
|
+
maxRetries: 5,
|
|
2254
|
+
retryDelay: 20
|
|
2255
|
+
});
|
|
2256
|
+
} catch {}
|
|
2030
2257
|
}
|
|
2031
|
-
#
|
|
2032
|
-
const
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
const
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
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",
|
|
2050
2292
|
context: {
|
|
2051
2293
|
stage: this.stage,
|
|
2052
2294
|
project
|
|
2053
2295
|
}
|
|
2054
2296
|
});
|
|
2055
|
-
return {
|
|
2056
|
-
origin: "workspace",
|
|
2057
|
-
path: project,
|
|
2058
|
-
message
|
|
2059
|
-
};
|
|
2060
2297
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
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 }
|
|
2066
2318
|
};
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
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
|
|
2074
2359
|
}
|
|
2075
|
-
};
|
|
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");
|
|
2076
2429
|
}
|
|
2077
2430
|
};
|
|
2078
2431
|
//#endregion
|
|
2079
2432
|
//#region src/server/Probe.ts
|
|
2080
2433
|
/**
|
|
2081
|
-
* Answers claims through
|
|
2434
|
+
* Answers claims through its type, lint, and runtime stages.
|
|
2082
2435
|
*
|
|
2083
2436
|
* @remarks
|
|
2084
2437
|
* Construction resolves the target workspace's toolchain and begins warming every stage. The boot
|
|
@@ -2090,10 +2443,45 @@ var TypeStage = class {
|
|
|
2090
2443
|
* claim costs that claim rather than the process. A failed boot is replaced the same way: the next
|
|
2091
2444
|
* claim runs the controls again rather than inheriting a refusal.
|
|
2092
2445
|
*
|
|
2093
|
-
* @example
|
|
2446
|
+
* @example The claim that earns a receipt
|
|
2094
2447
|
* ```ts
|
|
2095
|
-
*
|
|
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() })
|
|
2096
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'
|
|
2097
2485
|
* await probe.destroy()
|
|
2098
2486
|
* ```
|
|
2099
2487
|
*/
|
|
@@ -2114,7 +2502,7 @@ var Probe = class {
|
|
|
2114
2502
|
#arming;
|
|
2115
2503
|
#closing;
|
|
2116
2504
|
/**
|
|
2117
|
-
* Resolves the target toolchain and starts warming
|
|
2505
|
+
* Resolves the target toolchain and starts warming every stage.
|
|
2118
2506
|
*
|
|
2119
2507
|
* @param options - Workspace, deadline, and initial observation hooks
|
|
2120
2508
|
*/
|
|
@@ -2279,7 +2667,7 @@ var Probe = class {
|
|
|
2279
2667
|
files: [],
|
|
2280
2668
|
test: typeTest,
|
|
2281
2669
|
stage: "type",
|
|
2282
|
-
reason: "the imported type changed
|
|
2670
|
+
reason: "the imported type changed on disk between the two inspections"
|
|
2283
2671
|
}
|
|
2284
2672
|
};
|
|
2285
2673
|
const runtimeClaim = {
|
|
@@ -2526,7 +2914,9 @@ var Probe = class {
|
|
|
2526
2914
|
//#endregion
|
|
2527
2915
|
//#region src/server/ProbeServer.ts
|
|
2528
2916
|
/**
|
|
2529
|
-
*
|
|
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.
|
|
2530
2920
|
*
|
|
2531
2921
|
* @remarks
|
|
2532
2922
|
* Construction creates the probe, publishes the `prove` tool, and binds the dual-era dispatcher to
|
|
@@ -2692,6 +3082,6 @@ var ProbeServer = class {
|
|
|
2692
3082
|
}
|
|
2693
3083
|
};
|
|
2694
3084
|
//#endregion
|
|
2695
|
-
export { LintStage, Overlay, Probe, ProbeServer, RuntimeStage, TypeStage, buildRevisionPath, captureListeners, computeDigest, describeUnknown, escapesRoot, findRefusedPaths, guardStage, inferDocumentLanguage, inferTestProject, inferTypeProject, isRefusedName,
|
|
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 };
|
|
2696
3086
|
|
|
2697
3087
|
//# sourceMappingURL=index.js.map
|