carrick 0.3.108 → 0.3.110
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/package.json +9 -10
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/bundler.d.ts +10 -0
- package/sidecar/dist/src/bundler.js +42 -0
- package/sidecar/dist/src/capture/anchors.js +32 -0
- package/sidecar/dist/src/capture/api.d.ts +5 -0
- package/sidecar/dist/src/capture/check-deep.js +2 -1
- package/sidecar/dist/src/capture/check-workspace.d.ts +11 -0
- package/sidecar/dist/src/capture/check-workspace.js +81 -8
- package/sidecar/dist/src/capture/compiler-options.d.ts +39 -0
- package/sidecar/dist/src/capture/compiler-options.js +100 -0
- package/sidecar/dist/src/capture/deno-project.js +3 -2
- package/sidecar/dist/src/capture/index.d.ts +2 -0
- package/sidecar/dist/src/capture/index.js +25 -8
- package/sidecar/dist/src/capture/installed-package-directory.d.ts +22 -0
- package/sidecar/dist/src/capture/installed-package-directory.js +30 -0
- package/sidecar/dist/src/capture/installed-package.js +3 -2
- package/sidecar/dist/src/capture/resolution-edges.d.ts +36 -0
- package/sidecar/dist/src/capture/resolution-edges.js +150 -0
- package/sidecar/dist/src/capture/self-check.js +3 -2
- package/sidecar/dist/src/capture/status-table.d.ts +62 -0
- package/sidecar/dist/src/capture/status-table.js +104 -0
- package/sidecar/dist/src/definition-resolver.js +3 -2
- package/sidecar/dist/src/failure-path.d.ts +42 -2
- package/sidecar/dist/src/failure-path.js +38 -13
- package/sidecar/dist/src/index.js +91 -7
- package/sidecar/dist/src/library-claims.js +2 -0
- package/sidecar/dist/src/line-index.d.ts +9 -0
- package/sidecar/dist/src/line-index.js +24 -0
- package/sidecar/dist/src/print-type.d.ts +48 -0
- package/sidecar/dist/src/print-type.js +71 -0
- package/sidecar/dist/src/printed-names.js +2 -1
- package/sidecar/dist/src/program-files.d.ts +44 -0
- package/sidecar/dist/src/program-files.js +64 -0
- package/sidecar/dist/src/project-loader.d.ts +5 -0
- package/sidecar/dist/src/project-loader.js +14 -5
- package/sidecar/dist/src/response-modes.d.ts +77 -0
- package/sidecar/dist/src/response-modes.js +459 -0
- package/sidecar/dist/src/retype.js +7 -1
- package/sidecar/dist/src/type-inferrer.d.ts +53 -0
- package/sidecar/dist/src/type-inferrer.js +267 -79
- package/sidecar/dist/src/types.d.ts +50 -2
- package/sidecar/dist/src/validators.d.ts +159 -88
- package/sidecar/dist/src/validators.js +13 -0
|
@@ -28,6 +28,7 @@ import { Retyper } from './retype.js';
|
|
|
28
28
|
import { LibraryClaimsVerifier, httpCheck } from './library-claims.js';
|
|
29
29
|
import { PROGRESS_INTERVAL_MS, atMostEvery } from './progress.js';
|
|
30
30
|
import { mergeInferTimings } from './infer-timing.js';
|
|
31
|
+
import { addProgramFiles, listProgramFiles } from './program-files.js';
|
|
31
32
|
// ===========================================================================
|
|
32
33
|
// Module-level state
|
|
33
34
|
// ===========================================================================
|
|
@@ -46,6 +47,13 @@ let components = new Map();
|
|
|
46
47
|
* (carrick#1927).
|
|
47
48
|
*/
|
|
48
49
|
const definitionResolver = new DefinitionResolver();
|
|
50
|
+
/** The init'd project loader, or the error every project-backed action gives without one. */
|
|
51
|
+
function initializedLoader() {
|
|
52
|
+
if (!projectLoader?.isInitialized()) {
|
|
53
|
+
throw new Error('Sidecar not initialized. Call init first.');
|
|
54
|
+
}
|
|
55
|
+
return projectLoader;
|
|
56
|
+
}
|
|
49
57
|
/**
|
|
50
58
|
* Get the project-backed components, building the project if this is the
|
|
51
59
|
* first request that needs it.
|
|
@@ -53,15 +61,12 @@ const definitionResolver = new DefinitionResolver();
|
|
|
53
61
|
* @throws if init has not run, or if the project cannot be built
|
|
54
62
|
*/
|
|
55
63
|
function projectComponents(key = '') {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
// Bound here rather than read from the module slot inside the components:
|
|
65
|
+
// a re-init drops `components` and points the slot at another service, and
|
|
66
|
+
// nothing built over this project may follow it there.
|
|
67
|
+
const loader = initializedLoader();
|
|
59
68
|
let built = components.get(key);
|
|
60
69
|
if (!built) {
|
|
61
|
-
// Bound here rather than read from the module slot inside the components:
|
|
62
|
-
// a re-init drops `components` and points the slot at another service, and
|
|
63
|
-
// nothing built over this project may follow it there.
|
|
64
|
-
const loader = projectLoader;
|
|
65
70
|
const project = loader.getProjectFor(key);
|
|
66
71
|
const repoRoot = loader.getRepoRoot();
|
|
67
72
|
// The module graph, where the project resolved through one, is the only
|
|
@@ -501,6 +506,55 @@ function handleResolveDefinitions(request) {
|
|
|
501
506
|
};
|
|
502
507
|
}
|
|
503
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Handle the 'list_program_files' action - the root files the default
|
|
511
|
+
* project's program was built from, in order (carrick#2027). It is the only
|
|
512
|
+
* program a request grows: a file a referenced project owns is in that
|
|
513
|
+
* project's program from its tsconfig, so a program built for an owning
|
|
514
|
+
* project (carrick#1604) holds the same files in every process. A process
|
|
515
|
+
* that has not built the default project lists nothing, and does not build
|
|
516
|
+
* it to answer.
|
|
517
|
+
*/
|
|
518
|
+
function handleListProgramFiles(request) {
|
|
519
|
+
try {
|
|
520
|
+
const project = initializedLoader().builtProject();
|
|
521
|
+
const files = project ? listProgramFiles(project) : [];
|
|
522
|
+
log(`Listing ${files.length} program file(s)`);
|
|
523
|
+
return { request_id: request.request_id, status: 'success', files };
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
527
|
+
logError(`Listing program files failed: ${error}`);
|
|
528
|
+
return { request_id: request.request_id, status: 'error', errors: [error] };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Handle the 'add_program_files' action - add the given files to the default
|
|
533
|
+
* project's program in the given order, rebuilding it at most once
|
|
534
|
+
* (carrick#2027). A file a referenced project owns is skipped, as an `infer`
|
|
535
|
+
* on it would never add it to this program: its owner's program already holds
|
|
536
|
+
* it.
|
|
537
|
+
*/
|
|
538
|
+
function handleAddProgramFiles(request) {
|
|
539
|
+
try {
|
|
540
|
+
const loader = initializedLoader();
|
|
541
|
+
const startTime = performance.now();
|
|
542
|
+
const files = request.files
|
|
543
|
+
.map((file) => path.resolve(loader.getRepoRoot(), file))
|
|
544
|
+
.filter((file) => loader.projectKeyFor(file) === '');
|
|
545
|
+
const added = files.length === 0
|
|
546
|
+
? 0
|
|
547
|
+
: addProgramFiles(loader.getProject(), files, (file, reason) => logError(`Not added to the program: ${file}: ${reason}`));
|
|
548
|
+
log(`Added ${added} of ${request.files.length} file(s) to the program in ` +
|
|
549
|
+
`${Math.round(performance.now() - startTime)}ms`);
|
|
550
|
+
return { request_id: request.request_id, status: 'success', added };
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
554
|
+
logError(`Adding program files failed: ${error}`);
|
|
555
|
+
return { request_id: request.request_id, status: 'error', errors: [error] };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
504
558
|
/**
|
|
505
559
|
* Handle the 'health' action - report initialization status
|
|
506
560
|
*/
|
|
@@ -553,6 +607,10 @@ function handleRequest(request) {
|
|
|
553
607
|
return handleVerifyLibraryClaims(request);
|
|
554
608
|
case 'list_library_surface':
|
|
555
609
|
return handleListLibrarySurface(request);
|
|
610
|
+
case 'list_program_files':
|
|
611
|
+
return handleListProgramFiles(request);
|
|
612
|
+
case 'add_program_files':
|
|
613
|
+
return handleAddProgramFiles(request);
|
|
556
614
|
case 'health':
|
|
557
615
|
return handleHealth(request);
|
|
558
616
|
case 'shutdown':
|
|
@@ -648,6 +706,31 @@ function processLine(line) {
|
|
|
648
706
|
const response = handleRequest(parseResult.request);
|
|
649
707
|
writeResponse(response);
|
|
650
708
|
}
|
|
709
|
+
/** How often the sidecar looks for the process that started it. */
|
|
710
|
+
const PARENT_CHECK_MS = 1000;
|
|
711
|
+
/**
|
|
712
|
+
* Exit once the process that started this one is gone (carrick#2029).
|
|
713
|
+
*
|
|
714
|
+
* Its death closes stdin, which ends this process, unless another process
|
|
715
|
+
* still holds the write end. Then the only sign is that this process has a
|
|
716
|
+
* new parent: whatever adopts it (init, or a subreaper on Linux) has a pid of
|
|
717
|
+
* its own. So the parent at startup is kept and compared, never 1. A process
|
|
718
|
+
* already adopted at startup has no parent to watch, and stdin alone decides.
|
|
719
|
+
*
|
|
720
|
+
* The timer is unref'd: it never keeps an otherwise finished process alive.
|
|
721
|
+
* A handler that blocks the event loop holds it off until it returns.
|
|
722
|
+
*/
|
|
723
|
+
function exitWhenParentIsGone() {
|
|
724
|
+
const startedBy = process.ppid;
|
|
725
|
+
if (startedBy <= 1)
|
|
726
|
+
return;
|
|
727
|
+
setInterval(() => {
|
|
728
|
+
if (process.ppid !== startedBy) {
|
|
729
|
+
log(`parent ${startedBy} is gone, exiting`);
|
|
730
|
+
process.exit(0);
|
|
731
|
+
}
|
|
732
|
+
}, PARENT_CHECK_MS).unref();
|
|
733
|
+
}
|
|
651
734
|
/**
|
|
652
735
|
* Start the message loop
|
|
653
736
|
*/
|
|
@@ -663,6 +746,7 @@ function main() {
|
|
|
663
746
|
log('stdin closed, exiting');
|
|
664
747
|
process.exit(0);
|
|
665
748
|
});
|
|
749
|
+
exitWhenParentIsGone();
|
|
666
750
|
// Handle process signals
|
|
667
751
|
process.on('SIGINT', () => {
|
|
668
752
|
log('SIGINT received, exiting');
|
|
@@ -2584,6 +2584,8 @@ class DeclarationReader {
|
|
|
2584
2584
|
* repository checked out under a `node_modules` ancestor, is not installed.
|
|
2585
2585
|
* An install hoisted above the service root (`../../node_modules/pkg`) and
|
|
2586
2586
|
* a pnpm store (`node_modules/.pnpm/pkg@1/node_modules/pkg`) are.
|
|
2587
|
+
* Mirrored by `installedPackageDirectory` in capture/, which the capture
|
|
2588
|
+
* seam keeps from being shared.
|
|
2587
2589
|
*/
|
|
2588
2590
|
installedPackage(fileName) {
|
|
2589
2591
|
const segments = path.relative(this.root, this.realpath(fileName)).split(path.sep);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Node } from 'ts-morph';
|
|
1
2
|
/**
|
|
2
3
|
* The 1-based line of a position in a text, counted by line feeds alone: one
|
|
3
4
|
* more than the `\n`s before the position. That is the count ts-morph's
|
|
@@ -7,3 +8,11 @@
|
|
|
7
8
|
* The text is read once; each lookup after that is a binary search.
|
|
8
9
|
*/
|
|
9
10
|
export declare function lineIndex(text: string): (pos: number) => number;
|
|
11
|
+
/**
|
|
12
|
+
* `node.getStartLineNumber()` from a per-file index. ts-morph counts the
|
|
13
|
+
* file's line feeds from position 0 on every call, so a walk that asks of
|
|
14
|
+
* every node of a file pays the file's size once per node (carrick#1935).
|
|
15
|
+
*/
|
|
16
|
+
export declare function startLineOf(node: Node): number;
|
|
17
|
+
/** `node.getEndLineNumber()` from the same per-file index. */
|
|
18
|
+
export declare function endLineOf(node: Node): number;
|
|
@@ -24,3 +24,27 @@ export function lineIndex(text) {
|
|
|
24
24
|
return lo + 1;
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
+
const fileLineIndexes = new WeakMap();
|
|
28
|
+
function lineAtIn(node, pos) {
|
|
29
|
+
// Keyed on the compiler node: a manipulation (`replaceWithText`, a retype)
|
|
30
|
+
// gives the file a new one, so the index is built again from the new text.
|
|
31
|
+
const key = node.getSourceFile().compilerNode;
|
|
32
|
+
let lineAt = fileLineIndexes.get(key);
|
|
33
|
+
if (!lineAt) {
|
|
34
|
+
lineAt = lineIndex(key.text);
|
|
35
|
+
fileLineIndexes.set(key, lineAt);
|
|
36
|
+
}
|
|
37
|
+
return lineAt(pos);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `node.getStartLineNumber()` from a per-file index. ts-morph counts the
|
|
41
|
+
* file's line feeds from position 0 on every call, so a walk that asks of
|
|
42
|
+
* every node of a file pays the file's size once per node (carrick#1935).
|
|
43
|
+
*/
|
|
44
|
+
export function startLineOf(node) {
|
|
45
|
+
return lineAtIn(node, node.getStart());
|
|
46
|
+
}
|
|
47
|
+
/** `node.getEndLineNumber()` from the same per-file index. */
|
|
48
|
+
export function endLineOf(node) {
|
|
49
|
+
return lineAtIn(node, node.getEnd());
|
|
50
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compiler's own print of a type, with a module the print names written
|
|
3
|
+
* as the file that declares it (carrick#2019).
|
|
4
|
+
*
|
|
5
|
+
* When a type comes from a module the enclosing file does not import, the
|
|
6
|
+
* compiler writes it as `import("<module>").Name`. TypeScript 5 wrote the
|
|
7
|
+
* declaring file's path there: the node builder had no module-specifier host
|
|
8
|
+
* unless a declaration emit gave it one. TypeScript 6 gives every print a
|
|
9
|
+
* host, so the same type printed at an enclosing node reads
|
|
10
|
+
* `import("../model").Name` or `import("<package>").Name`: a specifier that
|
|
11
|
+
* depends on which file it was printed from, so one type printed from two
|
|
12
|
+
* files gives two texts. Everything downstream reads the declaring file's
|
|
13
|
+
* path: the scanner's path scrub writes it repo-relative or as
|
|
14
|
+
* `<package>@<version>`, and a capture pastes an inferred text into its
|
|
15
|
+
* surface entry, where a specifier relative to another file names nothing.
|
|
16
|
+
*
|
|
17
|
+
* These functions run the compiler's own code with no host: a tracker whose
|
|
18
|
+
* `moduleResolverHost` is `false` (falsy, so the builder writes the declaring
|
|
19
|
+
* file; not nullish, so the builder does not supply its own). Nothing in the
|
|
20
|
+
* text is rewritten afterwards. `typeToStringAsDeclared` is the checker's
|
|
21
|
+
* `typeToString`, step for step, with that one tracker.
|
|
22
|
+
*
|
|
23
|
+
* Only prints with an enclosing node differ: with none, the compiler has no
|
|
24
|
+
* file to write a specifier relative to and writes the declaring file
|
|
25
|
+
* anyway. A print that must name modules the way a declaration file would
|
|
26
|
+
* (the capture's surface emit) passes a host of its own and does not use
|
|
27
|
+
* this.
|
|
28
|
+
*/
|
|
29
|
+
import { ts } from 'ts-morph';
|
|
30
|
+
/**
|
|
31
|
+
* The compiler's internal `SymbolTracker`: callbacks the node builder reports
|
|
32
|
+
* to, and the module-specifier host it writes specifiers with.
|
|
33
|
+
*/
|
|
34
|
+
export interface SymbolTracker {
|
|
35
|
+
moduleResolverHost?: unknown;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The checker's `typeToTypeNode`, writing each module a node names as the
|
|
39
|
+
* file that declares it. A caller's own tracker keeps its callbacks.
|
|
40
|
+
*/
|
|
41
|
+
export declare function typeToTypeNodeAsDeclared(checker: ts.TypeChecker, type: ts.Type, enclosing: ts.Node | undefined, flags: ts.NodeBuilderFlags, tracker?: SymbolTracker): ts.TypeNode | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* The checker's `typeToString(type, enclosing, flags)`, writing each module
|
|
44
|
+
* the print names as the file that declares it. `noErrorTruncation` is the
|
|
45
|
+
* program's option of that name, which `typeToString` reads when `flags`
|
|
46
|
+
* leave truncation on.
|
|
47
|
+
*/
|
|
48
|
+
export declare function typeToStringAsDeclared(checker: ts.TypeChecker, type: ts.Type, enclosing: ts.Node | undefined, flags?: ts.TypeFormatFlags, noErrorTruncation?: boolean): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compiler's own print of a type, with a module the print names written
|
|
3
|
+
* as the file that declares it (carrick#2019).
|
|
4
|
+
*
|
|
5
|
+
* When a type comes from a module the enclosing file does not import, the
|
|
6
|
+
* compiler writes it as `import("<module>").Name`. TypeScript 5 wrote the
|
|
7
|
+
* declaring file's path there: the node builder had no module-specifier host
|
|
8
|
+
* unless a declaration emit gave it one. TypeScript 6 gives every print a
|
|
9
|
+
* host, so the same type printed at an enclosing node reads
|
|
10
|
+
* `import("../model").Name` or `import("<package>").Name`: a specifier that
|
|
11
|
+
* depends on which file it was printed from, so one type printed from two
|
|
12
|
+
* files gives two texts. Everything downstream reads the declaring file's
|
|
13
|
+
* path: the scanner's path scrub writes it repo-relative or as
|
|
14
|
+
* `<package>@<version>`, and a capture pastes an inferred text into its
|
|
15
|
+
* surface entry, where a specifier relative to another file names nothing.
|
|
16
|
+
*
|
|
17
|
+
* These functions run the compiler's own code with no host: a tracker whose
|
|
18
|
+
* `moduleResolverHost` is `false` (falsy, so the builder writes the declaring
|
|
19
|
+
* file; not nullish, so the builder does not supply its own). Nothing in the
|
|
20
|
+
* text is rewritten afterwards. `typeToStringAsDeclared` is the checker's
|
|
21
|
+
* `typeToString`, step for step, with that one tracker.
|
|
22
|
+
*
|
|
23
|
+
* Only prints with an enclosing node differ: with none, the compiler has no
|
|
24
|
+
* file to write a specifier relative to and writes the declaring file
|
|
25
|
+
* anyway. A print that must name modules the way a declaration file would
|
|
26
|
+
* (the capture's surface emit) passes a host of its own and does not use
|
|
27
|
+
* this.
|
|
28
|
+
*/
|
|
29
|
+
import { ts } from 'ts-morph';
|
|
30
|
+
/** A symbol tracker that leaves the node builder without a module-specifier host. */
|
|
31
|
+
const NO_SPECIFIER_HOST = { moduleResolverHost: false };
|
|
32
|
+
/** The checker's own default for `typeToString`. */
|
|
33
|
+
const DEFAULT_TYPE_FORMAT = ts.TypeFormatFlags.AllowUniqueESSymbolType | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope;
|
|
34
|
+
/** `typeToString`'s caps: a print this long is cut, `...` included. */
|
|
35
|
+
const DEFAULT_MAXIMUM_LENGTH = 160;
|
|
36
|
+
const NO_TRUNCATION_MAXIMUM_LENGTH = 1_000_000;
|
|
37
|
+
/** `typeToString`'s printers: comments removed, except for the unresolved type. */
|
|
38
|
+
const PRINTER = ts.createPrinter({ removeComments: true });
|
|
39
|
+
const PRINTER_WITH_COMMENTS = ts.createPrinter({});
|
|
40
|
+
/**
|
|
41
|
+
* The checker's `typeToTypeNode`, writing each module a node names as the
|
|
42
|
+
* file that declares it. A caller's own tracker keeps its callbacks.
|
|
43
|
+
*/
|
|
44
|
+
export function typeToTypeNodeAsDeclared(checker, type, enclosing, flags, tracker) {
|
|
45
|
+
const withoutHost = tracker
|
|
46
|
+
? Object.assign(Object.create(tracker), { moduleResolverHost: false })
|
|
47
|
+
: NO_SPECIFIER_HOST;
|
|
48
|
+
return checker.typeToTypeNode(type, enclosing, flags, undefined, withoutHost);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The checker's `typeToString(type, enclosing, flags)`, writing each module
|
|
52
|
+
* the print names as the file that declares it. `noErrorTruncation` is the
|
|
53
|
+
* program's option of that name, which `typeToString` reads when `flags`
|
|
54
|
+
* leave truncation on.
|
|
55
|
+
*/
|
|
56
|
+
export function typeToStringAsDeclared(checker, type, enclosing, flags = DEFAULT_TYPE_FORMAT, noErrorTruncation = false) {
|
|
57
|
+
const noTruncation = noErrorTruncation || (flags & ts.TypeFormatFlags.NoTruncation) !== 0;
|
|
58
|
+
const node = typeToTypeNodeAsDeclared(checker, type, enclosing, (flags & ts.TypeFormatFlags.NodeBuilderFlagsMask) |
|
|
59
|
+
ts.NodeBuilderFlags.IgnoreErrors |
|
|
60
|
+
(noTruncation ? ts.NodeBuilderFlags.NoTruncation : 0));
|
|
61
|
+
// `typeToString` asserts a node here too (`Debug.fail`). Falling back to it
|
|
62
|
+
// would print the other specifier form, so fail the same way it does.
|
|
63
|
+
if (node === undefined)
|
|
64
|
+
throw new Error('the node builder gave no type node to print');
|
|
65
|
+
const unresolved = type.intrinsicName === 'unresolved';
|
|
66
|
+
const writer = ts.createTextWriter('');
|
|
67
|
+
(unresolved ? PRINTER_WITH_COMMENTS : PRINTER).writeNode(ts.EmitHint.Unspecified, node, enclosing?.getSourceFile(), writer);
|
|
68
|
+
const result = writer.getText();
|
|
69
|
+
const maximum = (noTruncation ? NO_TRUNCATION_MAXIMUM_LENGTH : DEFAULT_MAXIMUM_LENGTH) * 2;
|
|
70
|
+
return result.length >= maximum ? result.substring(0, maximum - 3) + '...' : result;
|
|
71
|
+
}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* no more than keeping the list.
|
|
24
24
|
*/
|
|
25
25
|
import { ts } from 'ts-morph';
|
|
26
|
+
import { typeToTypeNodeAsDeclared } from './print-type.js';
|
|
26
27
|
/** The prints of the inference running now, when one is recording. */
|
|
27
28
|
let recording;
|
|
28
29
|
/**
|
|
@@ -82,7 +83,7 @@ export class PrintedTypes {
|
|
|
82
83
|
visited.set(type, at.add(enclosing));
|
|
83
84
|
let node;
|
|
84
85
|
try {
|
|
85
|
-
node = checker
|
|
86
|
+
node = typeToTypeNodeAsDeclared(checker, type, enclosing, NODE_FLAGS);
|
|
86
87
|
}
|
|
87
88
|
catch {
|
|
88
89
|
continue;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files a program was built from, and files added to a program in an
|
|
3
|
+
* order the caller gives (carrick#2027).
|
|
4
|
+
*
|
|
5
|
+
* With `stableTypeOrdering` the compiler orders two types of the same name by
|
|
6
|
+
* where their declarations sit in the program's file list. That list follows
|
|
7
|
+
* the program's root files: the compiler lists each root after the files it
|
|
8
|
+
* imports. ts-morph hands the compiler every file it has loaded as a root, in
|
|
9
|
+
* the order it loaded them, and a process asked about a file outside its
|
|
10
|
+
* tsconfig loads it when it is asked. Two processes asked the same questions
|
|
11
|
+
* in different orders therefore build from roots in different orders, and
|
|
12
|
+
* print a union of two same-named types in different orders.
|
|
13
|
+
*
|
|
14
|
+
* The roots decide the whole program, not only its order. A dependency file
|
|
15
|
+
* the compiler reached by resolving an import is not the same to it as the
|
|
16
|
+
* same file named as a root: a second copy of a package reached by resolution
|
|
17
|
+
* is folded into the first, and named as a root it is read as a file of its
|
|
18
|
+
* own and its imports are followed. So the list here is the roots themselves.
|
|
19
|
+
* A process that has built only what its tsconfig names, given another
|
|
20
|
+
* process's roots in order, ends with the same roots and builds the same
|
|
21
|
+
* program.
|
|
22
|
+
*/
|
|
23
|
+
import type { Project } from 'ts-morph';
|
|
24
|
+
/**
|
|
25
|
+
* The root files `project`'s program was built from, in order, as absolute
|
|
26
|
+
* paths: every file the project had loaded when the program was last built,
|
|
27
|
+
* the compiler's library files and the dependencies earlier builds reached
|
|
28
|
+
* among them.
|
|
29
|
+
*/
|
|
30
|
+
export declare function listProgramFiles(project: Project): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Add to `project` each of `files` it has not loaded, in the given order, and
|
|
33
|
+
* build the program once if any was added. A file it has loaded keeps its
|
|
34
|
+
* place. A file that cannot be read is skipped and reported to `onSkipped`.
|
|
35
|
+
*
|
|
36
|
+
* Nothing is built before the files join. A project built from a tsconfig has
|
|
37
|
+
* built once already, from the files the tsconfig names, as every process's
|
|
38
|
+
* does; one built from source patterns or a Deno graph has loaded only its
|
|
39
|
+
* own files, and the given list names whatever the other process's first
|
|
40
|
+
* build loaded, in the place that build put it.
|
|
41
|
+
*
|
|
42
|
+
* @returns how many files were added
|
|
43
|
+
*/
|
|
44
|
+
export declare function addProgramFiles(project: Project, files: readonly string[], onSkipped: (file: string, reason: string) => void): number;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files a program was built from, and files added to a program in an
|
|
3
|
+
* order the caller gives (carrick#2027).
|
|
4
|
+
*
|
|
5
|
+
* With `stableTypeOrdering` the compiler orders two types of the same name by
|
|
6
|
+
* where their declarations sit in the program's file list. That list follows
|
|
7
|
+
* the program's root files: the compiler lists each root after the files it
|
|
8
|
+
* imports. ts-morph hands the compiler every file it has loaded as a root, in
|
|
9
|
+
* the order it loaded them, and a process asked about a file outside its
|
|
10
|
+
* tsconfig loads it when it is asked. Two processes asked the same questions
|
|
11
|
+
* in different orders therefore build from roots in different orders, and
|
|
12
|
+
* print a union of two same-named types in different orders.
|
|
13
|
+
*
|
|
14
|
+
* The roots decide the whole program, not only its order. A dependency file
|
|
15
|
+
* the compiler reached by resolving an import is not the same to it as the
|
|
16
|
+
* same file named as a root: a second copy of a package reached by resolution
|
|
17
|
+
* is folded into the first, and named as a root it is read as a file of its
|
|
18
|
+
* own and its imports are followed. So the list here is the roots themselves.
|
|
19
|
+
* A process that has built only what its tsconfig names, given another
|
|
20
|
+
* process's roots in order, ends with the same roots and builds the same
|
|
21
|
+
* program.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* The root files `project`'s program was built from, in order, as absolute
|
|
25
|
+
* paths: every file the project had loaded when the program was last built,
|
|
26
|
+
* the compiler's library files and the dependencies earlier builds reached
|
|
27
|
+
* among them.
|
|
28
|
+
*/
|
|
29
|
+
export function listProgramFiles(project) {
|
|
30
|
+
return [...project.getProgram().compilerObject.getRootFileNames()];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Add to `project` each of `files` it has not loaded, in the given order, and
|
|
34
|
+
* build the program once if any was added. A file it has loaded keeps its
|
|
35
|
+
* place. A file that cannot be read is skipped and reported to `onSkipped`.
|
|
36
|
+
*
|
|
37
|
+
* Nothing is built before the files join. A project built from a tsconfig has
|
|
38
|
+
* built once already, from the files the tsconfig names, as every process's
|
|
39
|
+
* does; one built from source patterns or a Deno graph has loaded only its
|
|
40
|
+
* own files, and the given list names whatever the other process's first
|
|
41
|
+
* build loaded, in the place that build put it.
|
|
42
|
+
*
|
|
43
|
+
* @returns how many files were added
|
|
44
|
+
*/
|
|
45
|
+
export function addProgramFiles(project, files, onSkipped) {
|
|
46
|
+
let added = 0;
|
|
47
|
+
for (const file of new Set(files)) {
|
|
48
|
+
if (project.getSourceFile(file) !== undefined)
|
|
49
|
+
continue;
|
|
50
|
+
try {
|
|
51
|
+
project.addSourceFileAtPath(file);
|
|
52
|
+
added += 1;
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
onSkipped(file, err instanceof Error ? err.message : String(err));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// ts-morph drops its program on each add and builds the next one when it is
|
|
59
|
+
// read, so reading it once here is the one rebuild the adds need, done in
|
|
60
|
+
// this request rather than in the next one.
|
|
61
|
+
if (added > 0)
|
|
62
|
+
project.getProgram().compilerObject;
|
|
63
|
+
return added;
|
|
64
|
+
}
|
|
@@ -120,6 +120,11 @@ export declare class ProjectLoader {
|
|
|
120
120
|
* @throws Error if load() has not succeeded, or if the build fails
|
|
121
121
|
*/
|
|
122
122
|
getProject(): Project;
|
|
123
|
+
/**
|
|
124
|
+
* The default project when a request has built it, and `undefined` before
|
|
125
|
+
* that, without building it (carrick#2027).
|
|
126
|
+
*/
|
|
127
|
+
builtProject(): Project | undefined;
|
|
123
128
|
/**
|
|
124
129
|
* The registry package a resolved file belongs to, when the module graph
|
|
125
130
|
* names one (carrick#1260). `undefined` for a project that did not resolve
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* - Tsconfig snapshot (closed/merged extends chains) for synthetic monorepo
|
|
10
10
|
* - Pinned dependency snapshots for deterministic builds
|
|
11
11
|
*/
|
|
12
|
-
import { Project } from 'ts-morph';
|
|
12
|
+
import { Project, getCompilerOptionsFromTsConfig } from 'ts-morph';
|
|
13
13
|
import * as path from 'node:path';
|
|
14
14
|
import * as fs from 'node:fs';
|
|
15
|
-
import { DenoProject, findDenoConfig, findServiceTsconfig, serviceConfigPath } from './capture/index.js';
|
|
15
|
+
import { DenoProject, findDenoConfig, findServiceTsconfig, serviceConfigPath, sidecarCompilerOptions, } from './capture/index.js';
|
|
16
16
|
import { moduleFormatResolutionHost } from './module-format.js';
|
|
17
17
|
import { ExternalImports, registerExternalImports } from './origin.js';
|
|
18
18
|
/**
|
|
@@ -177,7 +177,7 @@ export class ProjectLoader {
|
|
|
177
177
|
this.log('Project will load with tsconfig snapshot');
|
|
178
178
|
this.buildProject = () => {
|
|
179
179
|
const project = new Project({
|
|
180
|
-
compilerOptions: this.snapshotToCompilerOptions(snapshot),
|
|
180
|
+
compilerOptions: sidecarCompilerOptions(this.snapshotToCompilerOptions(snapshot)),
|
|
181
181
|
skipAddingFilesFromTsConfig: true,
|
|
182
182
|
});
|
|
183
183
|
this.addDefaultSourceFiles(project);
|
|
@@ -197,7 +197,7 @@ export class ProjectLoader {
|
|
|
197
197
|
// rebuilds that drop them (carrick#1731).
|
|
198
198
|
const imports = new ExternalImports();
|
|
199
199
|
const project = new Project({
|
|
200
|
-
compilerOptions: deno.parsed.options,
|
|
200
|
+
compilerOptions: sidecarCompilerOptions(deno.parsed.options),
|
|
201
201
|
skipAddingFilesFromTsConfig: true,
|
|
202
202
|
resolutionHost: imports.recording((host, getOptions) => ({
|
|
203
203
|
resolveModuleNames: (names, from) => names.map(name => deno.resolve(name, from, getOptions(), host)),
|
|
@@ -221,7 +221,7 @@ export class ProjectLoader {
|
|
|
221
221
|
this.log('No tsconfig.json found, using default compiler options');
|
|
222
222
|
this.buildProject = () => {
|
|
223
223
|
const project = new Project({
|
|
224
|
-
compilerOptions: DEFAULT_COMPILER_OPTIONS,
|
|
224
|
+
compilerOptions: sidecarCompilerOptions(DEFAULT_COMPILER_OPTIONS),
|
|
225
225
|
skipAddingFilesFromTsConfig: true,
|
|
226
226
|
});
|
|
227
227
|
// Add source files from common locations
|
|
@@ -323,6 +323,8 @@ export class ProjectLoader {
|
|
|
323
323
|
const imports = new ExternalImports();
|
|
324
324
|
const project = new Project({
|
|
325
325
|
tsConfigFilePath: configPath,
|
|
326
|
+
// Over the tsconfig's own options: ts-morph lays these on top.
|
|
327
|
+
compilerOptions: sidecarCompilerOptions(getCompilerOptionsFromTsConfig(configPath).options),
|
|
326
328
|
skipAddingFilesFromTsConfig: false,
|
|
327
329
|
// Each import resolves in its file's own module format (carrick#1619),
|
|
328
330
|
// and every external-library answer is kept past the program rebuilds
|
|
@@ -385,6 +387,13 @@ export class ProjectLoader {
|
|
|
385
387
|
}
|
|
386
388
|
return this.project;
|
|
387
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* The default project when a request has built it, and `undefined` before
|
|
392
|
+
* that, without building it (carrick#2027).
|
|
393
|
+
*/
|
|
394
|
+
builtProject() {
|
|
395
|
+
return this.project ?? undefined;
|
|
396
|
+
}
|
|
388
397
|
/**
|
|
389
398
|
* The registry package a resolved file belongs to, when the module graph
|
|
390
399
|
* names one (carrick#1260). `undefined` for a project that did not resolve
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which incoming-message field picks each member of a response union (carrick#2054).
|
|
3
|
+
*
|
|
4
|
+
* A handler that sends `{ x }` when `?mode=a` and `{ y }` otherwise publishes
|
|
5
|
+
* `{ x } | { y }`. A call that states `mode=a` receives only `{ x }`, and a
|
|
6
|
+
* call that states nothing may receive either. The union alone cannot say
|
|
7
|
+
* which, so this reads it from the handler: for each success body the union
|
|
8
|
+
* joined, the tests on its path that compare a value read from the incoming message
|
|
9
|
+
* with a string literal.
|
|
10
|
+
*
|
|
11
|
+
* The reading, in order:
|
|
12
|
+
*
|
|
13
|
+
* - A test counts only when it separates two of the joined bodies: one on
|
|
14
|
+
* each side of it, or, for an earlier `if` that cannot complete, one in
|
|
15
|
+
* its branch and one after it. A validation guard whose branch only sends
|
|
16
|
+
* an error separates nothing the join kept, and is ignored.
|
|
17
|
+
* - A counted test is a message read when a value it tests is read from a
|
|
18
|
+
* parameter of a function enclosing the send (`messageRead`). A test that
|
|
19
|
+
* reads nothing from the incoming message (server state, a local count) is ignored,
|
|
20
|
+
* so a body under it is under no test of the field and stays in every case.
|
|
21
|
+
* - `===`, `==`, `!==` and `!=` against a string literal, with `!` flipping
|
|
22
|
+
* the test, and `switch` labels, are read as values. Any other test of a
|
|
23
|
+
* message read leaves its field read with its values unread.
|
|
24
|
+
* - `reads` lists every message read among the counted tests. `cases` is
|
|
25
|
+
* read only for one placed field whose tests were all read: one case per
|
|
26
|
+
* literal compared, plus "any other value" (`value: null`) when a body
|
|
27
|
+
* admits one, each the union of the bodies it admits.
|
|
28
|
+
* - When every case is the same union, the route has no modes.
|
|
29
|
+
*
|
|
30
|
+
* The published union is not changed by any of this.
|
|
31
|
+
*/
|
|
32
|
+
import { Node, type Symbol as TsSymbol } from 'ts-morph';
|
|
33
|
+
/**
|
|
34
|
+
* Where in the incoming message a field is read from. Only the HTTP sources
|
|
35
|
+
* are read so far; another protocol (a socket message, a pub/sub payload)
|
|
36
|
+
* adds its own source here.
|
|
37
|
+
*/
|
|
38
|
+
export type MessageSource = 'query' | 'body' | 'unplaced';
|
|
39
|
+
/** One incoming-message field a response union depends on. */
|
|
40
|
+
export interface ResponseModeRead {
|
|
41
|
+
/**
|
|
42
|
+
* `query`: read with `.get(...)` on the default lib's `URLSearchParams`.
|
|
43
|
+
* `body`: a top-level property of the awaited result of a zero-argument
|
|
44
|
+
* `json()` the default lib or an installed library declares. `unplaced`:
|
|
45
|
+
* anything else (a header, a path parameter, the method, a framework's own
|
|
46
|
+
* accessor, a `json()` the repo declares).
|
|
47
|
+
*/
|
|
48
|
+
location: MessageSource;
|
|
49
|
+
field: string;
|
|
50
|
+
}
|
|
51
|
+
/** The union a stated value of the field receives. */
|
|
52
|
+
export interface ResponseModeCase {
|
|
53
|
+
/** The value; `null` for any value no other case names. */
|
|
54
|
+
value: string | null;
|
|
55
|
+
type_string: string;
|
|
56
|
+
}
|
|
57
|
+
export interface ResponseModes {
|
|
58
|
+
/** Every message field the union depends on, sorted by location, then field. */
|
|
59
|
+
reads: ResponseModeRead[];
|
|
60
|
+
/**
|
|
61
|
+
* Present only when `reads` is one placed field and every test of it was
|
|
62
|
+
* read. Sorted by value, the `null` case last.
|
|
63
|
+
*/
|
|
64
|
+
cases?: ResponseModeCase[];
|
|
65
|
+
}
|
|
66
|
+
/** One success body the union joined, in source order. */
|
|
67
|
+
export interface JoinedBody {
|
|
68
|
+
node: Node;
|
|
69
|
+
type_string: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The modes of the union joined from `bodies`, or `undefined` when no message
|
|
73
|
+
* read separates them. `join` builds a union text from member texts with the
|
|
74
|
+
* rules the published union is built with. `isLibOrExternal` says whether a
|
|
75
|
+
* symbol is declared by a TypeScript lib or an installed library.
|
|
76
|
+
*/
|
|
77
|
+
export declare function readResponseModes(bodies: JoinedBody[], join: (texts: string[]) => string, isLibOrExternal: (symbol: TsSymbol | undefined) => boolean): ResponseModes | undefined;
|