@orkestrel/probe 0.0.12 → 0.0.14
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 +755 -327
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +473 -149
- package/dist/src/server/index.d.ts +473 -149
- package/dist/src/server/index.js +750 -328
- 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.14";
|
|
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
|
};
|
|
@@ -1161,12 +1386,14 @@ var RuntimeStage = class {
|
|
|
1161
1386
|
/**
|
|
1162
1387
|
* Starts warming the target workspace's Vitest service.
|
|
1163
1388
|
*
|
|
1389
|
+
* @remarks Loading starts during this call. A loader refusal rejects the owned warm operation
|
|
1390
|
+
* rather than construction, so `inspect` reports it and `destroy` remains available.
|
|
1391
|
+
*
|
|
1164
1392
|
* @param workspace - The target workspace root. Default: the current working directory
|
|
1165
1393
|
*/
|
|
1166
1394
|
constructor(workspace = process.cwd()) {
|
|
1167
1395
|
this.#workspace = workspace;
|
|
1168
|
-
|
|
1169
|
-
this.#store(this.#warm(vitest.createVitest));
|
|
1396
|
+
this.#store(this.#warm());
|
|
1170
1397
|
}
|
|
1171
1398
|
get stage() {
|
|
1172
1399
|
return "runtime";
|
|
@@ -1174,9 +1401,29 @@ var RuntimeStage = class {
|
|
|
1174
1401
|
get progress() {
|
|
1175
1402
|
return this.#progress;
|
|
1176
1403
|
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Inspects one case.
|
|
1406
|
+
*
|
|
1407
|
+
* @param subject - The candidate drafts and test to inspect
|
|
1408
|
+
* @returns One outcome for this stage
|
|
1409
|
+
* @throws When the workspace's own tool cannot start or has already been destroyed
|
|
1410
|
+
*/
|
|
1177
1411
|
inspect(subject) {
|
|
1178
1412
|
return guardStage(this.stage, this.#inspect(subject));
|
|
1179
1413
|
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Tears down the resident tool or the mirror and releases its resources.
|
|
1416
|
+
*
|
|
1417
|
+
* @remarks
|
|
1418
|
+
* A stage abandons every inspection it holds rather than waiting behind one, so teardown never
|
|
1419
|
+
* waits for an inspection to return. An abandoned inspection rejects, either at the stage's own
|
|
1420
|
+
* guard or as the owned tool closes. Teardown is bounded whatever the stage's own tool does: a
|
|
1421
|
+
* tool that answers neither its warming exchange nor its ending is signalled and released at the
|
|
1422
|
+
* stage's own deadline. A coordinator replaces a stage whose worker no longer returns because
|
|
1423
|
+
* teardown neither waits for an inspection nor waits past that deadline.
|
|
1424
|
+
*
|
|
1425
|
+
* @returns A promise that settles after the resident tool or the mirror releases its resources
|
|
1426
|
+
*/
|
|
1180
1427
|
destroy() {
|
|
1181
1428
|
if (this.#closing !== void 0) return this.#closing;
|
|
1182
1429
|
this.#closing = guardStage(this.stage, this.#destroy());
|
|
@@ -1277,13 +1524,14 @@ var RuntimeStage = class {
|
|
|
1277
1524
|
}
|
|
1278
1525
|
}
|
|
1279
1526
|
async #warm(create) {
|
|
1527
|
+
const factory = create ?? loadWorkspaceVitest(this.#workspace).createVitest;
|
|
1280
1528
|
this.#sweep();
|
|
1281
1529
|
const output = new PassThrough();
|
|
1282
1530
|
output.resume();
|
|
1283
1531
|
const signals = captureListeners(process, ["SIGINT", "SIGTERM"]);
|
|
1284
1532
|
let warming;
|
|
1285
1533
|
try {
|
|
1286
|
-
warming =
|
|
1534
|
+
warming = factory("test", {
|
|
1287
1535
|
root: this.#workspace,
|
|
1288
1536
|
config: resolveWorkspaceFile(this.#workspace, "vite.config.ts"),
|
|
1289
1537
|
watch: false,
|
|
@@ -1520,13 +1768,13 @@ var RuntimeStage = class {
|
|
|
1520
1768
|
const current = this.#vitest;
|
|
1521
1769
|
if (current !== void 0 && this.#specifications < PROBE_SPECIFICATIONS) return current;
|
|
1522
1770
|
if (current !== void 0) return this.#store(this.#replace(current));
|
|
1523
|
-
const runner =
|
|
1771
|
+
const runner = loadWorkspaceVitest(this.#workspace);
|
|
1524
1772
|
return this.#store(this.#specifications < PROBE_SPECIFICATIONS ? this.#warm(runner.createVitest) : this.#replace(void 0, runner.createVitest));
|
|
1525
1773
|
}
|
|
1526
1774
|
async #replace(current, create) {
|
|
1527
1775
|
if (current !== void 0) await (await current).close();
|
|
1528
1776
|
if (this.#closing !== void 0) throw createDestroyedError("runtime stage");
|
|
1529
|
-
const runner = create ??
|
|
1777
|
+
const runner = create ?? loadWorkspaceVitest(this.#workspace).createVitest;
|
|
1530
1778
|
const replacement = await this.#warm(runner);
|
|
1531
1779
|
this.#specifications = 0;
|
|
1532
1780
|
return replacement;
|
|
@@ -1570,7 +1818,7 @@ var RuntimeStage = class {
|
|
|
1570
1818
|
}
|
|
1571
1819
|
#snapshot() {
|
|
1572
1820
|
const modules = /* @__PURE__ */ new Map();
|
|
1573
|
-
for (const path of this.#
|
|
1821
|
+
for (const path of collectWorkspaceFiles(this.#workspace)) {
|
|
1574
1822
|
if (!matchesWorkspaceModule(path)) continue;
|
|
1575
1823
|
try {
|
|
1576
1824
|
const digest = createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
@@ -1581,12 +1829,12 @@ var RuntimeStage = class {
|
|
|
1581
1829
|
return modules;
|
|
1582
1830
|
}
|
|
1583
1831
|
#sweep() {
|
|
1584
|
-
for (const path of this.#
|
|
1832
|
+
for (const path of collectWorkspaceFiles(this.#workspace)) {
|
|
1585
1833
|
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
1834
|
const revision = match?.[1];
|
|
1587
1835
|
const owner = match?.[2];
|
|
1588
1836
|
if (revision === void 0 || owner === void 0) continue;
|
|
1589
|
-
if (
|
|
1837
|
+
if (matchesLiveProcess(Number.parseInt(owner, 10))) continue;
|
|
1590
1838
|
if (!this.#owned(path, revision)) continue;
|
|
1591
1839
|
try {
|
|
1592
1840
|
unlinkSync(resolveWorkspaceFile(this.#workspace, relative(this.#workspace, path), true));
|
|
@@ -1597,35 +1845,6 @@ var RuntimeStage = class {
|
|
|
1597
1845
|
const outcome = attempt(() => readFileSync(path, "utf8"));
|
|
1598
1846
|
return outcome.success && matchesSpecification(outcome.value, revision);
|
|
1599
1847
|
}
|
|
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
1848
|
#issues(result, file, original) {
|
|
1630
1849
|
const specification = this.#real(file);
|
|
1631
1850
|
const issues = [];
|
|
@@ -1741,21 +1960,32 @@ var RuntimeStage = class {
|
|
|
1741
1960
|
//#endregion
|
|
1742
1961
|
//#region src/server/stages/TypeStage.ts
|
|
1743
1962
|
/**
|
|
1744
|
-
* Inspects TypeScript source
|
|
1963
|
+
* Inspects TypeScript source by running the target workspace's own compiler over a mirror of it.
|
|
1745
1964
|
*
|
|
1746
1965
|
* @remarks
|
|
1747
|
-
*
|
|
1748
|
-
*
|
|
1749
|
-
*
|
|
1750
|
-
*
|
|
1751
|
-
*
|
|
1752
|
-
*
|
|
1753
|
-
*
|
|
1754
|
-
*
|
|
1755
|
-
*
|
|
1756
|
-
*
|
|
1757
|
-
*
|
|
1758
|
-
*
|
|
1966
|
+
* The compiler is a process this stage spawns rather than a module it loads, so nothing here holds
|
|
1967
|
+
* a resident program and the host's event loop is free for the whole check. Construction sweeps a
|
|
1968
|
+
* mirror an earlier host left behind, copies the workspace into a fresh one under
|
|
1969
|
+
* {@link TYPE_MIRROR}, and builds each declared project's incremental state there, so the first
|
|
1970
|
+
* inspection is warm.
|
|
1971
|
+
*
|
|
1972
|
+
* Every inspection refreshes the mirror by content digest, writes each candidate draft and the test
|
|
1973
|
+
* at its mirrored declared path, and runs `tsc --noEmit --pretty false` once per distinct selected
|
|
1974
|
+
* project. A draft therefore shadows the file it replaces: a consumer of that path is checked
|
|
1975
|
+
* against the draft's text, and a draft importing a sibling draft resolves to the sibling draft.
|
|
1976
|
+
* Nothing is written outside `tmp/`, and the workspace's own copy of a drafted file never moves.
|
|
1977
|
+
*
|
|
1978
|
+
* Each run reads a scratch project this stage writes beside the mirrored project it extends, so
|
|
1979
|
+
* every relative path the project declares — its own `rootDir`, `include`, `paths`, and the
|
|
1980
|
+
* projects it extends — resolves inside the mirror without being rewritten. The scratch adds the
|
|
1981
|
+
* drafts as `files`, which the project's own `exclude` does not reach, and its incremental state
|
|
1982
|
+
* lives in the mirror so reuse survives the refresh.
|
|
1983
|
+
*
|
|
1984
|
+
* The diagnostics decide the outcome, never the exit code, which the supported compiler majors
|
|
1985
|
+
* disagree on. A diagnostic naming a project file, and one naming no file at all, is the target
|
|
1986
|
+
* tree's own configuration fault and raises rather than reporting, unless the `.json` file is one
|
|
1987
|
+
* the claim itself drafted; every other diagnostic is a claimant issue at the point the compiler
|
|
1988
|
+
* reported, which is the extent the plain-text output carries.
|
|
1759
1989
|
*
|
|
1760
1990
|
* @example
|
|
1761
1991
|
* ```ts
|
|
@@ -1766,26 +1996,27 @@ var RuntimeStage = class {
|
|
|
1766
1996
|
*/
|
|
1767
1997
|
var TypeStage = class {
|
|
1768
1998
|
#workspace;
|
|
1769
|
-
#
|
|
1770
|
-
#
|
|
1771
|
-
#
|
|
1772
|
-
#
|
|
1773
|
-
#
|
|
1774
|
-
#
|
|
1775
|
-
#
|
|
1776
|
-
#
|
|
1999
|
+
#compiler;
|
|
2000
|
+
#revision = `${process.pid}-${randomUUID()}`;
|
|
2001
|
+
#mirror;
|
|
2002
|
+
#configs = /* @__PURE__ */ new Map();
|
|
2003
|
+
#mirrored = /* @__PURE__ */ new Map();
|
|
2004
|
+
#drafts = /* @__PURE__ */ new Set();
|
|
2005
|
+
#children = /* @__PURE__ */ new Set();
|
|
2006
|
+
#warming;
|
|
1777
2007
|
#closing;
|
|
1778
2008
|
#progress = 0;
|
|
1779
2009
|
/**
|
|
1780
|
-
*
|
|
2010
|
+
* Resolves the target workspace's compiler and starts warming its mirror.
|
|
1781
2011
|
*
|
|
1782
2012
|
* @param workspace - The target workspace root. Default: the current working directory
|
|
1783
2013
|
*/
|
|
1784
2014
|
constructor(workspace = process.cwd()) {
|
|
1785
2015
|
this.#workspace = workspace;
|
|
1786
|
-
|
|
1787
|
-
this.#
|
|
1788
|
-
this.#
|
|
2016
|
+
this.#compiler = resolveWorkspaceBinary(workspace, "typescript", "tsc");
|
|
2017
|
+
this.#mirror = resolveWorkspaceFile(workspace, `${TYPE_MIRROR}/${this.#revision}`);
|
|
2018
|
+
this.#warming = this.#warm();
|
|
2019
|
+
this.#warming.catch(() => {});
|
|
1789
2020
|
}
|
|
1790
2021
|
get stage() {
|
|
1791
2022
|
return "type";
|
|
@@ -1805,7 +2036,8 @@ var TypeStage = class {
|
|
|
1805
2036
|
* @param project - The workspace-relative TypeScript project the candidate drafts are checked
|
|
1806
2037
|
* against. Default: the scoped project each candidate path infers
|
|
1807
2038
|
* @returns One outcome for this stage
|
|
1808
|
-
* @throws When the
|
|
2039
|
+
* @throws When the workspace refuses the mirror, when a project the run reads is malformed, or
|
|
2040
|
+
* when the stage has already been destroyed
|
|
1809
2041
|
*/
|
|
1810
2042
|
inspect(subject, project) {
|
|
1811
2043
|
return guardStage(this.stage, this.#inspect(subject, project));
|
|
@@ -1814,15 +2046,15 @@ var TypeStage = class {
|
|
|
1814
2046
|
* Resolves one project to the path and digest this stage applies for it.
|
|
1815
2047
|
*
|
|
1816
2048
|
* @remarks
|
|
1817
|
-
* Reads the
|
|
1818
|
-
*
|
|
1819
|
-
*
|
|
1820
|
-
*
|
|
2049
|
+
* Reads the configuration the compiler itself prints for the project as the mirror holds it,
|
|
2050
|
+
* refreshed from the workspace first on a cache miss, so the digest names the configuration the
|
|
2051
|
+
* check applies. The reading is cached per project for the life of the stage, so a draft written
|
|
2052
|
+
* afterward cannot move it.
|
|
1821
2053
|
*
|
|
1822
2054
|
* @param project - The workspace-relative TypeScript project to resolve
|
|
1823
2055
|
* @returns The resolved workspace-relative path and the digest of its compiler options
|
|
1824
|
-
* @throws When the project escapes the workspace,
|
|
1825
|
-
* been destroyed
|
|
2056
|
+
* @throws When the project escapes the workspace, when the compiler refuses it, or when the
|
|
2057
|
+
* stage has already been destroyed
|
|
1826
2058
|
*/
|
|
1827
2059
|
resolve(project) {
|
|
1828
2060
|
return guardStage(this.stage, this.#resolve(project));
|
|
@@ -1833,81 +2065,76 @@ var TypeStage = class {
|
|
|
1833
2065
|
return this.#closing;
|
|
1834
2066
|
}
|
|
1835
2067
|
async #inspect(subject, project) {
|
|
1836
|
-
|
|
2068
|
+
this.#refuseDestroyed();
|
|
1837
2069
|
const started = performance.now();
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
const
|
|
2070
|
+
await this.#warming;
|
|
2071
|
+
this.#refuseDestroyed();
|
|
2072
|
+
const test = resolveWorkspaceFile(this.#workspace, subject.test.path);
|
|
2073
|
+
const resolved = subject.files.map((draft) => ({
|
|
1841
2074
|
draft,
|
|
1842
2075
|
path: resolveWorkspaceFile(this.#workspace, draft.path)
|
|
1843
|
-
})).map((candidate) => ({
|
|
1844
|
-
draft: candidate.draft,
|
|
1845
|
-
project: project ?? inferTypeProject(relativeWorkspaceFile(this.#workspace, candidate.path))
|
|
1846
2076
|
}));
|
|
1847
|
-
const
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
2077
|
+
const groups = /* @__PURE__ */ new Map([[this.#contain("tsconfig.json"), [subject.test]]]);
|
|
2078
|
+
for (const candidate of resolved) {
|
|
2079
|
+
const selected = this.#contain(project ?? inferTypeProject(relativeWorkspaceFile(this.#workspace, candidate.path)));
|
|
2080
|
+
const drafts = groups.get(selected);
|
|
2081
|
+
if (drafts === void 0) groups.set(selected, [candidate.draft]);
|
|
2082
|
+
else drafts.push(candidate.draft);
|
|
1853
2083
|
}
|
|
1854
|
-
|
|
1855
|
-
this.#
|
|
2084
|
+
this.#progress += 1;
|
|
2085
|
+
for (const selected of groups.keys()) await this.#configure(selected);
|
|
2086
|
+
this.#refuseDestroyed();
|
|
1856
2087
|
try {
|
|
1857
|
-
this.#
|
|
1858
|
-
|
|
1859
|
-
this.#
|
|
2088
|
+
this.#refresh();
|
|
2089
|
+
this.#drafts.add(this.#place(subject.test.text, test));
|
|
2090
|
+
for (const candidate of resolved) this.#drafts.add(this.#place(candidate.draft.text, candidate.path));
|
|
1860
2091
|
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();
|
|
2092
|
+
for (const [selected, drafts] of groups) {
|
|
2093
|
+
this.#refuseDestroyed();
|
|
2094
|
+
issues.push(...await this.#check(selected, drafts));
|
|
1872
2095
|
}
|
|
1873
2096
|
return {
|
|
1874
2097
|
stage: this.stage,
|
|
1875
2098
|
elapsed: Math.round(performance.now() - started),
|
|
1876
|
-
issues
|
|
2099
|
+
issues: filterUniqueIssues(issues)
|
|
1877
2100
|
};
|
|
1878
2101
|
} finally {
|
|
1879
|
-
|
|
2102
|
+
this.#release();
|
|
1880
2103
|
}
|
|
1881
2104
|
}
|
|
1882
2105
|
async #resolve(project) {
|
|
1883
|
-
|
|
1884
|
-
const
|
|
1885
|
-
if (this.#closing !== void 0) throw createDestroyedError("type stage");
|
|
1886
|
-
const resolved = resolveWorkspaceFile(this.#workspace, project);
|
|
2106
|
+
this.#refuseDestroyed();
|
|
2107
|
+
const contained = this.#contain(project);
|
|
1887
2108
|
this.#progress += 1;
|
|
1888
|
-
this.#
|
|
1889
|
-
await this.#unblock();
|
|
2109
|
+
const config = await this.#configure(contained);
|
|
1890
2110
|
return {
|
|
1891
|
-
path:
|
|
1892
|
-
digest: computeDigest(this.#workspace,
|
|
2111
|
+
path: contained,
|
|
2112
|
+
digest: computeDigest(this.#workspace, config.compilerOptions)
|
|
1893
2113
|
};
|
|
1894
2114
|
}
|
|
1895
2115
|
async #destroy() {
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
this.#
|
|
1899
|
-
this.#
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2116
|
+
const children = [...this.#children];
|
|
2117
|
+
this.#children.clear();
|
|
2118
|
+
for (const child of children) this.#terminate(child);
|
|
2119
|
+
await this.#warming.catch(() => void 0);
|
|
2120
|
+
try {
|
|
2121
|
+
rmSync(this.#mirror, {
|
|
2122
|
+
recursive: true,
|
|
2123
|
+
force: true,
|
|
2124
|
+
maxRetries: 5,
|
|
2125
|
+
retryDelay: 20
|
|
2126
|
+
});
|
|
2127
|
+
} catch {}
|
|
2128
|
+
this.#configs.clear();
|
|
2129
|
+
this.#mirrored.clear();
|
|
2130
|
+
this.#drafts.clear();
|
|
2131
|
+
}
|
|
2132
|
+
async #warm() {
|
|
2133
|
+
this.#sweep();
|
|
2134
|
+
this.#createMirror();
|
|
2135
|
+
this.#refresh();
|
|
2136
|
+
this.#refuseDestroyed();
|
|
2137
|
+
await Promise.all(this.#projects().map((project) => this.#check(project, [])));
|
|
1911
2138
|
}
|
|
1912
2139
|
#projects() {
|
|
1913
2140
|
const projects = ["tsconfig.json"];
|
|
@@ -1923,162 +2150,291 @@ var TypeStage = class {
|
|
|
1923
2150
|
}
|
|
1924
2151
|
return projects;
|
|
1925
2152
|
}
|
|
1926
|
-
|
|
1927
|
-
await setTimeout(0);
|
|
2153
|
+
#refuseDestroyed() {
|
|
1928
2154
|
if (this.#closing !== void 0) throw createDestroyedError("type stage");
|
|
1929
2155
|
}
|
|
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
|
-
}
|
|
2156
|
+
#createMirror() {
|
|
2157
|
+
const marker = resolveWorkspaceFile(this.#workspace, `${TYPE_MIRROR}/${this.#revision}/.probe/mirror.txt`, true);
|
|
2158
|
+
mkdirSync(dirname(marker), { recursive: true });
|
|
2159
|
+
writeFileSync(marker, formatSpecification("", this.#revision), {
|
|
2160
|
+
encoding: "utf8",
|
|
2161
|
+
flag: "wx"
|
|
1946
2162
|
});
|
|
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
2163
|
}
|
|
1984
|
-
#
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2164
|
+
#sweep() {
|
|
2165
|
+
const root = resolveWorkspaceFile(this.#workspace, TYPE_MIRROR);
|
|
2166
|
+
let entries = [];
|
|
2167
|
+
try {
|
|
2168
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
2169
|
+
} catch {
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
for (const entry of entries) {
|
|
2173
|
+
if (!entry.isDirectory()) continue;
|
|
2174
|
+
const owner = parseRevisionOwner(entry.name);
|
|
2175
|
+
if (owner === void 0 || matchesLiveProcess(owner)) continue;
|
|
2176
|
+
const marker = attempt(() => readFileSync(join(root, entry.name, ".probe", "mirror.txt"), "utf8"));
|
|
2177
|
+
if (!marker.success || !matchesSpecification(marker.value, entry.name)) continue;
|
|
2178
|
+
try {
|
|
2179
|
+
rmSync(join(root, entry.name), {
|
|
2180
|
+
recursive: true,
|
|
2181
|
+
force: true,
|
|
2182
|
+
maxRetries: 5,
|
|
2183
|
+
retryDelay: 20
|
|
2184
|
+
});
|
|
2185
|
+
} catch {}
|
|
2186
|
+
}
|
|
2001
2187
|
}
|
|
2002
|
-
#
|
|
2003
|
-
|
|
2188
|
+
#refresh() {
|
|
2189
|
+
const present = /* @__PURE__ */ new Set();
|
|
2190
|
+
for (const path of collectWorkspaceFiles(this.#workspace)) {
|
|
2191
|
+
const contained = relativeWorkspaceFile(this.#workspace, path);
|
|
2192
|
+
present.add(contained);
|
|
2193
|
+
const reading = attempt(() => readFileSync(path));
|
|
2194
|
+
if (!reading.success) continue;
|
|
2195
|
+
const digest = createHash("sha256").update(reading.value).digest("hex");
|
|
2196
|
+
if (this.#mirrored.get(contained) === digest) continue;
|
|
2197
|
+
this.#place(reading.value, path);
|
|
2198
|
+
this.#mirrored.set(contained, digest);
|
|
2199
|
+
}
|
|
2200
|
+
for (const contained of [...this.#mirrored.keys()]) {
|
|
2201
|
+
if (present.has(contained)) continue;
|
|
2202
|
+
this.#remove(contained);
|
|
2203
|
+
this.#mirrored.delete(contained);
|
|
2204
|
+
}
|
|
2004
2205
|
}
|
|
2005
|
-
#
|
|
2006
|
-
|
|
2206
|
+
#release() {
|
|
2207
|
+
for (const contained of this.#drafts) {
|
|
2208
|
+
this.#remove(contained);
|
|
2209
|
+
this.#mirrored.delete(contained);
|
|
2210
|
+
}
|
|
2211
|
+
this.#drafts.clear();
|
|
2212
|
+
}
|
|
2213
|
+
#place(text, path) {
|
|
2214
|
+
const contained = relativeWorkspaceFile(this.#workspace, path);
|
|
2215
|
+
const target = this.#mirrorPath(contained);
|
|
2216
|
+
if (attempt(() => {
|
|
2217
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
2218
|
+
writeFileSync(target, text);
|
|
2219
|
+
}).success) return contained;
|
|
2220
|
+
this.#displace(target);
|
|
2221
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
2222
|
+
writeFileSync(target, text);
|
|
2223
|
+
return contained;
|
|
2224
|
+
}
|
|
2225
|
+
#displace(target) {
|
|
2226
|
+
const ancestors = [];
|
|
2227
|
+
let directory = dirname(target);
|
|
2228
|
+
while (directory !== this.#mirror && directory !== dirname(directory)) {
|
|
2229
|
+
ancestors.unshift(directory);
|
|
2230
|
+
directory = dirname(directory);
|
|
2231
|
+
}
|
|
2232
|
+
for (const ancestor of [...ancestors, target]) {
|
|
2233
|
+
const reading = attempt(() => lstatSync(ancestor));
|
|
2234
|
+
if (!reading.success) continue;
|
|
2235
|
+
if (ancestor !== target && reading.value.isDirectory()) continue;
|
|
2236
|
+
if (ancestor === target && !reading.value.isDirectory()) continue;
|
|
2237
|
+
rmSync(ancestor, {
|
|
2238
|
+
recursive: true,
|
|
2239
|
+
force: true,
|
|
2240
|
+
maxRetries: 5,
|
|
2241
|
+
retryDelay: 20
|
|
2242
|
+
});
|
|
2243
|
+
this.#mirrored.delete(relativeWorkspaceFile(this.#mirror, ancestor));
|
|
2244
|
+
}
|
|
2007
2245
|
}
|
|
2008
|
-
#
|
|
2009
|
-
|
|
2010
|
-
if (this.#diagnostics.has(path)) return;
|
|
2011
|
-
this.#diagnostics.set(path, service.getCompilerOptionsDiagnostics());
|
|
2246
|
+
#mirrorPath(contained) {
|
|
2247
|
+
return join(this.#mirror, contained);
|
|
2012
2248
|
}
|
|
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);
|
|
2249
|
+
#contain(project) {
|
|
2250
|
+
return relativeWorkspaceFile(this.#workspace, resolveWorkspaceFile(this.#workspace, project));
|
|
2022
2251
|
}
|
|
2023
|
-
#
|
|
2024
|
-
if (this.#overlay.text(file) !== void 0) return `virtual:${this.#overlay.revision}`;
|
|
2252
|
+
#remove(contained) {
|
|
2025
2253
|
try {
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2254
|
+
rmSync(this.#mirrorPath(contained), {
|
|
2255
|
+
force: true,
|
|
2256
|
+
maxRetries: 5,
|
|
2257
|
+
retryDelay: 20
|
|
2258
|
+
});
|
|
2259
|
+
} catch {}
|
|
2030
2260
|
}
|
|
2031
|
-
#
|
|
2032
|
-
const
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
const
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2261
|
+
async #configure(project) {
|
|
2262
|
+
const contained = this.#contain(project);
|
|
2263
|
+
const existing = this.#configs.get(contained);
|
|
2264
|
+
if (existing !== void 0) return existing;
|
|
2265
|
+
this.#refresh();
|
|
2266
|
+
const execution = await this.#spawn([
|
|
2267
|
+
"--showConfig",
|
|
2268
|
+
"-p",
|
|
2269
|
+
contained
|
|
2270
|
+
], this.#mirror);
|
|
2271
|
+
this.#refuseDestroyed();
|
|
2272
|
+
const config = parseProjectConfig(execution.stdout);
|
|
2273
|
+
if (config === void 0) throw this.#fault(execution, contained);
|
|
2274
|
+
this.#configs.set(contained, config);
|
|
2275
|
+
return config;
|
|
2276
|
+
}
|
|
2277
|
+
async #check(project, drafts) {
|
|
2278
|
+
const config = await this.#configure(project);
|
|
2279
|
+
const scratch = this.#scratch(this.#contain(project), config, drafts);
|
|
2280
|
+
const execution = await this.#spawn([
|
|
2281
|
+
"--noEmit",
|
|
2282
|
+
"--pretty",
|
|
2283
|
+
"false",
|
|
2284
|
+
"-p",
|
|
2285
|
+
`./${scratch}`
|
|
2286
|
+
], this.#mirror);
|
|
2287
|
+
this.#refuseDestroyed();
|
|
2288
|
+
const diagnostics = scanDiagnostics(execution.stdout);
|
|
2289
|
+
if (diagnostics.length === 0 && execution.status !== 0) {
|
|
2290
|
+
const stderr = execution.stderr.trim();
|
|
2291
|
+
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}`;
|
|
2292
|
+
throw new ProbeError(this.#translate(message), {
|
|
2293
|
+
origin: "instrument",
|
|
2294
|
+
code: "malformed",
|
|
2050
2295
|
context: {
|
|
2051
2296
|
stage: this.stage,
|
|
2052
2297
|
project
|
|
2053
2298
|
}
|
|
2054
2299
|
});
|
|
2055
|
-
return {
|
|
2056
|
-
origin: "workspace",
|
|
2057
|
-
path: project,
|
|
2058
|
-
message
|
|
2059
|
-
};
|
|
2060
2300
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2301
|
+
return this.#issues(diagnostics, project);
|
|
2302
|
+
}
|
|
2303
|
+
#scratch(project, config, drafts) {
|
|
2304
|
+
const directory = dirname(project);
|
|
2305
|
+
const stem = `${basename(project, extname(project))}.probe.json`;
|
|
2306
|
+
const contained = directory === "." ? stem : `${directory}/${stem}`;
|
|
2307
|
+
const target = this.#mirrorPath(contained);
|
|
2308
|
+
const buildinfo = join(this.#mirror, ".probe", `${project.replaceAll("/", "-")}.tsbuildinfo`);
|
|
2309
|
+
const files = [...(config.files ?? []).filter((entry) => existsSync(resolve(dirname(target), entry))), ...drafts.map((draft) => relativeWorkspaceFile(dirname(target), this.#mirrorPath(this.#contain(draft.path))))];
|
|
2310
|
+
const body = {
|
|
2311
|
+
extends: `./${basename(project)}`,
|
|
2312
|
+
compilerOptions: {
|
|
2313
|
+
noEmit: true,
|
|
2314
|
+
declaration: false,
|
|
2315
|
+
emitDeclarationOnly: false,
|
|
2316
|
+
composite: false,
|
|
2317
|
+
incremental: true,
|
|
2318
|
+
tsBuildInfoFile: relativeWorkspaceFile(dirname(target), buildinfo)
|
|
2319
|
+
},
|
|
2320
|
+
...files.length === 0 ? {} : { files }
|
|
2066
2321
|
};
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2322
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
2323
|
+
writeFileSync(target, `${JSON.stringify(body, void 0, " ")}\n`, "utf8");
|
|
2324
|
+
return contained;
|
|
2325
|
+
}
|
|
2326
|
+
#issues(diagnostics, project) {
|
|
2327
|
+
const issues = [];
|
|
2328
|
+
for (const diagnostic of diagnostics) {
|
|
2329
|
+
const message = this.#translate(diagnostic.message);
|
|
2330
|
+
const resolved = diagnostic.path === void 0 ? void 0 : resolve(this.#mirror, diagnostic.path);
|
|
2331
|
+
const drafted = resolved !== void 0 && this.#drafts.has(escapesRoot(this.#mirror, resolved) ? relativeWorkspaceFile(this.#workspace, resolved) : relativeWorkspaceFile(this.#mirror, resolved));
|
|
2332
|
+
if (resolved === void 0 || !drafted && extname(resolved) === ".json") throw new ProbeError(message, {
|
|
2333
|
+
origin: "workspace",
|
|
2334
|
+
code: "malformed",
|
|
2335
|
+
context: {
|
|
2336
|
+
stage: this.stage,
|
|
2337
|
+
project
|
|
2338
|
+
}
|
|
2339
|
+
});
|
|
2340
|
+
const path = escapesRoot(this.#mirror, resolved) ? escapesRoot(this.#workspace, resolved) ? normalizePath(resolved) : relativeWorkspaceFile(this.#workspace, resolved) : relativeWorkspaceFile(this.#mirror, resolved);
|
|
2341
|
+
issues.push({
|
|
2342
|
+
origin: "claimant",
|
|
2343
|
+
path,
|
|
2344
|
+
message,
|
|
2345
|
+
...diagnostic.range === void 0 ? {} : { range: diagnostic.range }
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
return issues;
|
|
2349
|
+
}
|
|
2350
|
+
#translate(message) {
|
|
2351
|
+
return relativeWorkspaceMessage(this.#workspace, relativeWorkspaceMessage(this.#mirror, message));
|
|
2352
|
+
}
|
|
2353
|
+
#fault(execution, project) {
|
|
2354
|
+
const reported = scanDiagnostics(execution.stdout).map((diagnostic) => diagnostic.message);
|
|
2355
|
+
const text = reported.length > 0 ? reported.join("\n") : execution.stdout.trim();
|
|
2356
|
+
return new ProbeError(this.#translate(text === "" ? "The compiler printed no configuration" : text), {
|
|
2357
|
+
origin: "workspace",
|
|
2358
|
+
code: "malformed",
|
|
2359
|
+
context: {
|
|
2360
|
+
stage: this.stage,
|
|
2361
|
+
project
|
|
2074
2362
|
}
|
|
2075
|
-
};
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
#spawn(args, cwd) {
|
|
2366
|
+
this.#refuseDestroyed();
|
|
2367
|
+
return new Promise((settle, refuse) => {
|
|
2368
|
+
const child = spawn(process.execPath, [this.#compiler, ...args], {
|
|
2369
|
+
cwd,
|
|
2370
|
+
stdio: [
|
|
2371
|
+
"ignore",
|
|
2372
|
+
"pipe",
|
|
2373
|
+
"pipe"
|
|
2374
|
+
]
|
|
2375
|
+
});
|
|
2376
|
+
const output = child.stdout;
|
|
2377
|
+
const errors = child.stderr;
|
|
2378
|
+
if (output === null || errors === null) {
|
|
2379
|
+
this.#terminate(child);
|
|
2380
|
+
refuse(new ProbeError("The compiler was spawned without its own output streams", {
|
|
2381
|
+
origin: "instrument",
|
|
2382
|
+
code: "malformed",
|
|
2383
|
+
context: { stage: this.stage }
|
|
2384
|
+
}));
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
this.#children.add(child);
|
|
2388
|
+
let stdout = "";
|
|
2389
|
+
let stderr = "";
|
|
2390
|
+
output.setEncoding("utf8");
|
|
2391
|
+
errors.setEncoding("utf8");
|
|
2392
|
+
output.on("data", (chunk) => {
|
|
2393
|
+
stdout += chunk;
|
|
2394
|
+
});
|
|
2395
|
+
errors.on("data", (chunk) => {
|
|
2396
|
+
stderr += chunk;
|
|
2397
|
+
});
|
|
2398
|
+
child.on("error", (error) => {
|
|
2399
|
+
this.#children.delete(child);
|
|
2400
|
+
refuse(new ProbeError("The workspace compiler could not be started", {
|
|
2401
|
+
origin: "workspace",
|
|
2402
|
+
code: "malformed",
|
|
2403
|
+
context: {
|
|
2404
|
+
stage: this.stage,
|
|
2405
|
+
path: this.#compiler
|
|
2406
|
+
},
|
|
2407
|
+
cause: error
|
|
2408
|
+
}));
|
|
2409
|
+
});
|
|
2410
|
+
child.on("close", (code) => {
|
|
2411
|
+
this.#children.delete(child);
|
|
2412
|
+
settle({
|
|
2413
|
+
...code === null ? {} : { status: code },
|
|
2414
|
+
stdout,
|
|
2415
|
+
stderr
|
|
2416
|
+
});
|
|
2417
|
+
});
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
#terminate(child) {
|
|
2421
|
+
const id = child.pid;
|
|
2422
|
+
if (process.platform === "win32" && id !== void 0) {
|
|
2423
|
+
spawnSync("taskkill", [
|
|
2424
|
+
"/pid",
|
|
2425
|
+
String(id),
|
|
2426
|
+
"/t",
|
|
2427
|
+
"/f"
|
|
2428
|
+
], { stdio: "ignore" });
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
child.kill("SIGTERM");
|
|
2076
2432
|
}
|
|
2077
2433
|
};
|
|
2078
2434
|
//#endregion
|
|
2079
2435
|
//#region src/server/Probe.ts
|
|
2080
2436
|
/**
|
|
2081
|
-
* Answers claims through
|
|
2437
|
+
* Answers claims through its type, lint, and runtime stages.
|
|
2082
2438
|
*
|
|
2083
2439
|
* @remarks
|
|
2084
2440
|
* Construction resolves the target workspace's toolchain and begins warming every stage. The boot
|
|
@@ -2090,10 +2446,45 @@ var TypeStage = class {
|
|
|
2090
2446
|
* claim costs that claim rather than the process. A failed boot is replaced the same way: the next
|
|
2091
2447
|
* claim runs the controls again rather than inheriting a refusal.
|
|
2092
2448
|
*
|
|
2093
|
-
* @example
|
|
2449
|
+
* @example The claim that earns a receipt
|
|
2094
2450
|
* ```ts
|
|
2095
|
-
*
|
|
2451
|
+
* import type { Claim } from '@orkestrel/probe'
|
|
2452
|
+
* import { Probe } from '@orkestrel/probe/server'
|
|
2453
|
+
*
|
|
2454
|
+
* const claim: Claim = {
|
|
2455
|
+
* project: 'configs/src/tsconfig.core.json',
|
|
2456
|
+
* case: {
|
|
2457
|
+
* files: [
|
|
2458
|
+
* {
|
|
2459
|
+
* path: 'src/core/factories.ts',
|
|
2460
|
+
* text: "export function createGreeting(): string {\n\treturn 'hi'\n}\n",
|
|
2461
|
+
* },
|
|
2462
|
+
* ],
|
|
2463
|
+
* test: {
|
|
2464
|
+
* path: 'tmp/probe/greeting.test.ts',
|
|
2465
|
+
* text: "import { expect, test } from 'vitest'\nimport { createGreeting } from '../../src/core/factories.js'\ntest('greets', () => expect(createGreeting()).toBe('hi'))\n",
|
|
2466
|
+
* },
|
|
2467
|
+
* },
|
|
2468
|
+
* control: {
|
|
2469
|
+
* files: [
|
|
2470
|
+
* {
|
|
2471
|
+
* path: 'src/core/factories.ts',
|
|
2472
|
+
* text: "export function createGreeting(): number {\n\treturn 'hi'\n}\n",
|
|
2473
|
+
* },
|
|
2474
|
+
* ],
|
|
2475
|
+
* test: {
|
|
2476
|
+
* path: 'tmp/probe/greeting.test.ts',
|
|
2477
|
+
* text: "import { expect, test } from 'vitest'\nimport { createGreeting } from '../../src/core/factories.js'\ntest('greets', () => expect(createGreeting()).toBe('hi'))\n",
|
|
2478
|
+
* },
|
|
2479
|
+
* stage: 'type',
|
|
2480
|
+
* reason: 'a string returned as a number must not compile',
|
|
2481
|
+
* },
|
|
2482
|
+
* }
|
|
2483
|
+
*
|
|
2484
|
+
* const probe = new Probe({ workspace: process.cwd() })
|
|
2096
2485
|
* const verdict = await probe.prove(claim)
|
|
2486
|
+
* verdict.digest // 'fcb88a2dee987b8673c1fc7107979470'
|
|
2487
|
+
* verdict.receipt // 'probe:fcb88a2dee987b8673c1fc7107979470:type:typescript@6.0.3:oxlint@1.83.0:vitest@4.1.11:configs/src/tsconfig.core.json@434f59254d58cf2683d453a26bd0d837'
|
|
2097
2488
|
* await probe.destroy()
|
|
2098
2489
|
* ```
|
|
2099
2490
|
*/
|
|
@@ -2114,7 +2505,7 @@ var Probe = class {
|
|
|
2114
2505
|
#arming;
|
|
2115
2506
|
#closing;
|
|
2116
2507
|
/**
|
|
2117
|
-
* Resolves the target toolchain and starts warming
|
|
2508
|
+
* Resolves the target toolchain and starts warming every stage.
|
|
2118
2509
|
*
|
|
2119
2510
|
* @param options - Workspace, deadline, and initial observation hooks
|
|
2120
2511
|
*/
|
|
@@ -2279,7 +2670,7 @@ var Probe = class {
|
|
|
2279
2670
|
files: [],
|
|
2280
2671
|
test: typeTest,
|
|
2281
2672
|
stage: "type",
|
|
2282
|
-
reason: "the imported type changed
|
|
2673
|
+
reason: "the imported type changed on disk between the two inspections"
|
|
2283
2674
|
}
|
|
2284
2675
|
};
|
|
2285
2676
|
const runtimeClaim = {
|
|
@@ -2526,12 +2917,16 @@ var Probe = class {
|
|
|
2526
2917
|
//#endregion
|
|
2527
2918
|
//#region src/server/ProbeServer.ts
|
|
2528
2919
|
/**
|
|
2529
|
-
*
|
|
2920
|
+
* Implements `ProbeServerInterface` over a `PassThrough` stream this server owns, binding the
|
|
2921
|
+
* published `prove` tool and the dual-era dispatcher to this process's Model Context Protocol
|
|
2922
|
+
* stdio transport.
|
|
2530
2923
|
*
|
|
2531
2924
|
* @remarks
|
|
2532
|
-
* Construction
|
|
2533
|
-
* the stdio transport.
|
|
2534
|
-
* arming
|
|
2925
|
+
* Construction snapshots the probe options, publishes the `prove` tool, and binds the dual-era
|
|
2926
|
+
* dispatcher to the stdio transport. An admitted `prove` call creates the probe and begins
|
|
2927
|
+
* workspace arming, so discovery does not depend on the workspace toolchain. Construction is held
|
|
2928
|
+
* before it begins, so teardown entered through a construction callback waits for the resulting
|
|
2929
|
+
* probe and releases it. A constructor refusal clears that held operation for a later retry.
|
|
2535
2930
|
*
|
|
2536
2931
|
* `start` seizes standard input and registers the signals a harness ends a child with, and records
|
|
2537
2932
|
* whether standard input was already flowing. Every listener it attaches is held as a field, and
|
|
@@ -2553,7 +2948,7 @@ var Probe = class {
|
|
|
2553
2948
|
* ```
|
|
2554
2949
|
*/
|
|
2555
2950
|
var ProbeServer = class {
|
|
2556
|
-
#
|
|
2951
|
+
#options;
|
|
2557
2952
|
#stream;
|
|
2558
2953
|
#transport;
|
|
2559
2954
|
#data;
|
|
@@ -2562,14 +2957,25 @@ var ProbeServer = class {
|
|
|
2562
2957
|
#signal;
|
|
2563
2958
|
#server;
|
|
2564
2959
|
#owns;
|
|
2960
|
+
#construction;
|
|
2565
2961
|
#closing;
|
|
2566
2962
|
/**
|
|
2567
|
-
*
|
|
2963
|
+
* Snapshots the probe options this server will use and binds the published schema to this
|
|
2964
|
+
* process's stdio transport. An admitted `prove` call creates the probe.
|
|
2568
2965
|
*
|
|
2569
|
-
* @param options - Workspace, deadline, and initial observation hooks for the probe
|
|
2966
|
+
* @param options - Workspace, deadline, and initial observation hooks for the deferred probe
|
|
2570
2967
|
*/
|
|
2571
2968
|
constructor(options) {
|
|
2572
|
-
|
|
2969
|
+
const workspace = options?.workspace;
|
|
2970
|
+
const deadline = options?.deadline;
|
|
2971
|
+
const on = options?.on;
|
|
2972
|
+
const error = options?.error;
|
|
2973
|
+
this.#options = Object.freeze({
|
|
2974
|
+
workspace: resolve(workspace ?? process.cwd()),
|
|
2975
|
+
...deadline === void 0 ? {} : { deadline },
|
|
2976
|
+
...on === void 0 ? {} : { on: Object.freeze({ ...on }) },
|
|
2977
|
+
...error === void 0 ? {} : { error }
|
|
2978
|
+
});
|
|
2573
2979
|
this.#stream = new PassThrough();
|
|
2574
2980
|
this.#server = this.#publish();
|
|
2575
2981
|
this.#transport = createStdioServer(createMCPLegacy(this.#server), { input: this.#stream });
|
|
@@ -2604,7 +3010,10 @@ var ProbeServer = class {
|
|
|
2604
3010
|
if (this.#owns === true && process.stdin.listenerCount("data") === 0) process.stdin.pause();
|
|
2605
3011
|
this.#owns = void 0;
|
|
2606
3012
|
this.#stream.destroy();
|
|
2607
|
-
|
|
3013
|
+
const construction = this.#construction;
|
|
3014
|
+
if (construction === void 0) return;
|
|
3015
|
+
const probe = await construction.catch(() => void 0);
|
|
3016
|
+
if (probe !== void 0) await probe.destroy();
|
|
2608
3017
|
}
|
|
2609
3018
|
#receive(chunk) {
|
|
2610
3019
|
this.#stream.write(chunk);
|
|
@@ -2650,7 +3059,20 @@ var ProbeServer = class {
|
|
|
2650
3059
|
context: { value: input }
|
|
2651
3060
|
});
|
|
2652
3061
|
}
|
|
2653
|
-
return this.#
|
|
3062
|
+
return (await this.#resolveProbe()).prove(input);
|
|
3063
|
+
}
|
|
3064
|
+
async #resolveProbe() {
|
|
3065
|
+
if (this.#closing !== void 0) throw createDestroyedError("probe server");
|
|
3066
|
+
const current = this.#construction;
|
|
3067
|
+
if (current !== void 0) return await current;
|
|
3068
|
+
const construction = Promise.resolve().then(() => new Probe(this.#options));
|
|
3069
|
+
this.#construction = construction;
|
|
3070
|
+
try {
|
|
3071
|
+
return await construction;
|
|
3072
|
+
} catch (error) {
|
|
3073
|
+
if (this.#construction === construction) this.#construction = void 0;
|
|
3074
|
+
throw error;
|
|
3075
|
+
}
|
|
2654
3076
|
}
|
|
2655
3077
|
async #execute(context) {
|
|
2656
3078
|
const result = await context.tools.execute(context.call);
|
|
@@ -2692,6 +3114,6 @@ var ProbeServer = class {
|
|
|
2692
3114
|
}
|
|
2693
3115
|
};
|
|
2694
3116
|
//#endregion
|
|
2695
|
-
export { LintStage, Overlay, Probe, ProbeServer, RuntimeStage, TypeStage, buildRevisionPath, captureListeners, computeDigest, describeUnknown, escapesRoot, findRefusedPaths, guardStage, inferDocumentLanguage, inferTestProject, inferTypeProject, isRefusedName,
|
|
3117
|
+
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
3118
|
|
|
2697
3119
|
//# sourceMappingURL=index.js.map
|