carrick 0.3.102 → 0.3.103
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 +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/bundler.d.ts +3 -92
- package/sidecar/dist/src/bundler.js +5 -265
- package/sidecar/dist/src/capture/anchors.js +113 -8
- package/sidecar/dist/src/capture/check-fields.js +19 -5
- package/sidecar/dist/src/capture/check-probe.d.ts +5 -0
- package/sidecar/dist/src/capture/check-probe.js +31 -2
- package/sidecar/dist/src/capture/check-workspace.d.ts +3 -0
- package/sidecar/dist/src/capture/check-workspace.js +13 -16
- package/sidecar/dist/src/capture/check.js +5 -5
- package/sidecar/dist/src/capture/deep-walk.js +45 -12
- package/sidecar/dist/src/capture/deno-project.d.ts +2 -1
- package/sidecar/dist/src/capture/deno-project.js +22 -18
- package/sidecar/dist/src/capture/guarded-fs.d.ts +76 -0
- package/sidecar/dist/src/capture/guarded-fs.js +182 -0
- package/sidecar/dist/src/capture/index.js +60 -26
- package/sidecar/dist/src/capture/outside-root.d.ts +41 -0
- package/sidecar/dist/src/capture/outside-root.js +101 -0
- package/sidecar/dist/src/capture/paths-rewrite.d.ts +8 -0
- package/sidecar/dist/src/capture/paths-rewrite.js +9 -7
- package/sidecar/dist/src/capture/repair-dangling.d.ts +11 -1
- package/sidecar/dist/src/capture/repair-dangling.js +15 -4
- package/sidecar/dist/src/capture/self-check.d.ts +3 -0
- package/sidecar/dist/src/capture/self-check.js +3 -3
- package/sidecar/dist/src/index.d.ts +1 -1
- package/sidecar/dist/src/index.js +2 -115
- package/sidecar/dist/src/origin.d.ts +49 -8
- package/sidecar/dist/src/origin.js +81 -8
- package/sidecar/dist/src/project-loader.js +15 -5
- package/sidecar/dist/src/type-inferrer.d.ts +72 -0
- package/sidecar/dist/src/type-inferrer.js +248 -8
- package/sidecar/dist/src/type-structural-expander.d.ts +5 -1
- package/sidecar/dist/src/type-structural-expander.js +1 -1
- package/sidecar/dist/src/types.d.ts +31 -178
- package/sidecar/dist/src/validators.d.ts +58 -914
- package/sidecar/dist/src/validators.js +0 -55
- package/sidecar/dist/src/monorepo-builder.d.ts +0 -129
- package/sidecar/dist/src/monorepo-builder.js +0 -584
|
@@ -37,17 +37,28 @@ import * as fs from 'node:fs';
|
|
|
37
37
|
* `failing` maps an absolute file path to the specifiers the stub could not
|
|
38
38
|
* resolve from it. Returns the files actually rewritten; a file whose names
|
|
39
39
|
* cannot all be replaced by `unknown` is left exactly as it was.
|
|
40
|
+
*
|
|
41
|
+
* Only a file `tree` (a guard over the stub's types tree) lets it write is
|
|
42
|
+
* repaired (carrick#1742). The self-check program also loads files the stub
|
|
43
|
+
* merely reaches: the repo's own sources, through a workspace package linked
|
|
44
|
+
* into the `node_modules` the stub borrows, or a runtime's dependency cache.
|
|
45
|
+
* Those report their missing modules exactly as an emitted declaration does,
|
|
46
|
+
* and they belong to the user: a scan reads them and never writes them. The
|
|
47
|
+
* guard decides on the resolved path, so a file reached through the link is
|
|
48
|
+
* the repo's, whatever path the program loaded it by.
|
|
40
49
|
*/
|
|
41
|
-
export function repairDanglingImports(failing) {
|
|
50
|
+
export function repairDanglingImports(failing, tree) {
|
|
42
51
|
const repaired = new Map();
|
|
43
52
|
for (const [file, specifiers] of failing) {
|
|
44
|
-
|
|
53
|
+
if (!tree.allowsWrite(file))
|
|
54
|
+
continue;
|
|
55
|
+
const result = repairFile(file, specifiers, tree);
|
|
45
56
|
if (result)
|
|
46
57
|
repaired.set(file, result);
|
|
47
58
|
}
|
|
48
59
|
return repaired;
|
|
49
60
|
}
|
|
50
|
-
function repairFile(file, specifiers) {
|
|
61
|
+
function repairFile(file, specifiers, tree) {
|
|
51
62
|
let text;
|
|
52
63
|
try {
|
|
53
64
|
text = fs.readFileSync(file, 'utf8');
|
|
@@ -116,7 +127,7 @@ function repairFile(file, specifiers) {
|
|
|
116
127
|
for (const edit of edits) {
|
|
117
128
|
out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
|
|
118
129
|
}
|
|
119
|
-
|
|
130
|
+
tree.writeFile(file, out);
|
|
120
131
|
return { specifiers: [...removedSpecifiers].sort(), names: [...names].sort() };
|
|
121
132
|
}
|
|
122
133
|
/** The module specifier an import STATEMENT names, if it names one. */
|
|
@@ -32,7 +32,10 @@
|
|
|
32
32
|
import ts from 'typescript';
|
|
33
33
|
import type { CaptureAliasRecord } from './api.js';
|
|
34
34
|
import type { ResolvedAnchor } from './anchors.js';
|
|
35
|
+
import type { WriteGuard } from './guarded-fs.js';
|
|
35
36
|
export interface SelfCheckArgs {
|
|
37
|
+
/** Writes are held to the stub (carrick#1748). */
|
|
38
|
+
guard: WriteGuard;
|
|
36
39
|
stubDir: string;
|
|
37
40
|
surfaceAbsPath: string;
|
|
38
41
|
resolved: ResolvedAnchor[];
|
|
@@ -53,7 +53,7 @@ export function selfCheckStub(args) {
|
|
|
53
53
|
const linkPath = path.join(args.stubDir, 'node_modules');
|
|
54
54
|
let linked = false;
|
|
55
55
|
if (!args.bareCheckout && fs.existsSync(repoNodeModules) && !fs.existsSync(linkPath)) {
|
|
56
|
-
|
|
56
|
+
args.guard.symlink(repoNodeModules, linkPath, 'dir');
|
|
57
57
|
linked = true;
|
|
58
58
|
}
|
|
59
59
|
try {
|
|
@@ -65,7 +65,7 @@ export function selfCheckStub(args) {
|
|
|
65
65
|
// the verdict: it reads the repaired text, and a name the rewrite did not
|
|
66
66
|
// reach comes back as a `Cannot find name` diagnostic that
|
|
67
67
|
// `repairedNameFailures` folds back into the same dangling specifier.
|
|
68
|
-
const repaired = repairDanglingImports(first.internalFailuresByFile);
|
|
68
|
+
const repaired = repairDanglingImports(first.internalFailuresByFile, args.guard.narrow(typesDir));
|
|
69
69
|
if (repaired.size === 0)
|
|
70
70
|
return first.records;
|
|
71
71
|
return runSelfCheck(args, treeFiles, repaired).records;
|
|
@@ -74,7 +74,7 @@ export function selfCheckStub(args) {
|
|
|
74
74
|
// unlinkSync, not rmSync: the link target is a directory and rmSync
|
|
75
75
|
// refuses symlinks-to-directories with EISDIR.
|
|
76
76
|
if (linked)
|
|
77
|
-
|
|
77
|
+
args.guard.unlink(linkPath);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
function runSelfCheck(args, treeFiles, repaired) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This module implements a message loop that:
|
|
5
5
|
* 1. Listens on stdin for JSON requests
|
|
6
|
-
* 2. Processes each request (
|
|
6
|
+
* 2. Processes each request (see handleRequest for the actions)
|
|
7
7
|
* 3. Writes JSON responses to stdout
|
|
8
8
|
*
|
|
9
9
|
* IMPORTANT:
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This module implements a message loop that:
|
|
5
5
|
* 1. Listens on stdin for JSON requests
|
|
6
|
-
* 2. Processes each request (
|
|
6
|
+
* 2. Processes each request (see handleRequest for the actions)
|
|
7
7
|
* 3. Writes JSON responses to stdout
|
|
8
8
|
*
|
|
9
9
|
* IMPORTANT:
|
|
@@ -15,9 +15,8 @@ import * as path from 'node:path';
|
|
|
15
15
|
import * as readline from 'node:readline';
|
|
16
16
|
import { parseRequest } from './validators.js';
|
|
17
17
|
import { ProjectLoader } from './project-loader.js';
|
|
18
|
-
import { TypeBundler
|
|
18
|
+
import { TypeBundler } from './bundler.js';
|
|
19
19
|
import { TypeInferrer } from './type-inferrer.js';
|
|
20
|
-
import { MonorepoBuilder } from './monorepo-builder.js';
|
|
21
20
|
import { DefinitionResolver } from './definition-resolver.js';
|
|
22
21
|
import { captureStub, findDisqualifyingTopTypes, jsonWireDeclarations, runCheck, } from './capture/index.js';
|
|
23
22
|
import { Retyper } from './retype.js';
|
|
@@ -26,7 +25,6 @@ import { LibraryClaimsVerifier, httpCheck } from './library-claims.js';
|
|
|
26
25
|
// Module-level state
|
|
27
26
|
// ===========================================================================
|
|
28
27
|
let projectLoader = null;
|
|
29
|
-
let monorepoBuilder = null;
|
|
30
28
|
let initTimeMs = null;
|
|
31
29
|
/**
|
|
32
30
|
* Components by project key (`ProjectLoader.projectKeyFor`): `''` is the
|
|
@@ -62,7 +60,6 @@ function projectComponents(key = '') {
|
|
|
62
60
|
});
|
|
63
61
|
built = {
|
|
64
62
|
typeBundler: new TypeBundler({ project, repoRoot }),
|
|
65
|
-
surfaceEmitter: new SurfaceEmitter({ project, repoRoot }),
|
|
66
63
|
typeInferrer,
|
|
67
64
|
definitionResolver: new DefinitionResolver({ project }),
|
|
68
65
|
// Locates calls exactly as `infer` does, so it rewrites the node the
|
|
@@ -143,8 +140,6 @@ function handleInit(request) {
|
|
|
143
140
|
// The project and everything that reads it are built by the first request
|
|
144
141
|
// that needs them, so readiness costs the same on a bare checkout as on
|
|
145
142
|
// one with its dependencies installed (carrick#749).
|
|
146
|
-
// Initialize monorepo builder (doesn't need project)
|
|
147
|
-
monorepoBuilder = new MonorepoBuilder();
|
|
148
143
|
initTimeMs = result.initTimeMs || Math.round(performance.now() - startTime);
|
|
149
144
|
log(`Initialization complete in ${initTimeMs}ms`);
|
|
150
145
|
return {
|
|
@@ -202,38 +197,6 @@ function handleBundle(request) {
|
|
|
202
197
|
};
|
|
203
198
|
}
|
|
204
199
|
}
|
|
205
|
-
/**
|
|
206
|
-
* Handle the 'emit_surface' action - emit a surface .d.ts with rewritten specifiers
|
|
207
|
-
*/
|
|
208
|
-
function handleEmitSurface(request) {
|
|
209
|
-
try {
|
|
210
|
-
log(`Emitting surface for repo '${request.repo_name}' with ${request.payloads.length} payload(s)`);
|
|
211
|
-
const result = projectComponents().surfaceEmitter.emit(request.repo_name, request.payloads, request.output_path);
|
|
212
|
-
if (!result.success) {
|
|
213
|
-
return {
|
|
214
|
-
request_id: request.request_id,
|
|
215
|
-
status: 'error',
|
|
216
|
-
errors: result.errors,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
return {
|
|
220
|
-
request_id: request.request_id,
|
|
221
|
-
status: 'success',
|
|
222
|
-
output_path: result.output_path,
|
|
223
|
-
surface_content: result.surface_content,
|
|
224
|
-
manifest: result.manifest,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
catch (err) {
|
|
228
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
229
|
-
logError(`Surface emission failed: ${error}`);
|
|
230
|
-
return {
|
|
231
|
-
request_id: request.request_id,
|
|
232
|
-
status: 'error',
|
|
233
|
-
errors: [error],
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
200
|
/**
|
|
238
201
|
* Handle the 'capture_v2' action - v2 "tsc as serializer" capture.
|
|
239
202
|
* Stateless by design: unlike bundle/infer it needs no init'd ts-morph
|
|
@@ -461,76 +424,6 @@ function handleListLibrarySurface(request) {
|
|
|
461
424
|
return { request_id: request.request_id, status: 'error', errors: [error] };
|
|
462
425
|
}
|
|
463
426
|
}
|
|
464
|
-
/**
|
|
465
|
-
* Handle the 'build_workspace' action - build synthetic monorepo workspace
|
|
466
|
-
*/
|
|
467
|
-
function handleBuildWorkspace(request) {
|
|
468
|
-
// MonorepoBuilder doesn't require project initialization
|
|
469
|
-
if (!monorepoBuilder) {
|
|
470
|
-
monorepoBuilder = new MonorepoBuilder();
|
|
471
|
-
}
|
|
472
|
-
try {
|
|
473
|
-
log(`Building synthetic workspace with ${request.repos.length} repo(s)`);
|
|
474
|
-
const result = monorepoBuilder.build(request.repos, request.workspace_root);
|
|
475
|
-
if (!result.success) {
|
|
476
|
-
return {
|
|
477
|
-
request_id: request.request_id,
|
|
478
|
-
status: 'error',
|
|
479
|
-
errors: result.errors,
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
return {
|
|
483
|
-
request_id: request.request_id,
|
|
484
|
-
status: 'success',
|
|
485
|
-
workspace_path: result.workspace_path,
|
|
486
|
-
stub_packages: result.stub_packages,
|
|
487
|
-
checker_path: result.checker_path,
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
catch (err) {
|
|
491
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
492
|
-
logError(`Workspace build failed: ${error}`);
|
|
493
|
-
return {
|
|
494
|
-
request_id: request.request_id,
|
|
495
|
-
status: 'error',
|
|
496
|
-
errors: [error],
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* Handle the 'check_compatibility' action - run type compatibility checks
|
|
502
|
-
*/
|
|
503
|
-
function handleCheckCompatibility(request) {
|
|
504
|
-
if (!monorepoBuilder) {
|
|
505
|
-
monorepoBuilder = new MonorepoBuilder();
|
|
506
|
-
}
|
|
507
|
-
try {
|
|
508
|
-
log(`Running compatibility checks: ${request.checks.length} check(s)`);
|
|
509
|
-
const result = monorepoBuilder.checkCompatibility(request.workspace_root, request.checks);
|
|
510
|
-
if (!result.success) {
|
|
511
|
-
return {
|
|
512
|
-
request_id: request.request_id,
|
|
513
|
-
status: 'error',
|
|
514
|
-
errors: result.errors,
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
return {
|
|
518
|
-
request_id: request.request_id,
|
|
519
|
-
status: 'success',
|
|
520
|
-
results: result.results,
|
|
521
|
-
diagnostics: result.diagnostics,
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
catch (err) {
|
|
525
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
526
|
-
logError(`Compatibility check failed: ${error}`);
|
|
527
|
-
return {
|
|
528
|
-
request_id: request.request_id,
|
|
529
|
-
status: 'error',
|
|
530
|
-
errors: [error],
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
427
|
/**
|
|
535
428
|
* Handle the 'resolve_definitions' action - resolve surface aliases from a
|
|
536
429
|
* v2 capture stub package's declaration tree.
|
|
@@ -597,16 +490,10 @@ function handleRequest(request) {
|
|
|
597
490
|
return handleInit(request);
|
|
598
491
|
case 'bundle':
|
|
599
492
|
return handleBundle(request);
|
|
600
|
-
case 'emit_surface':
|
|
601
|
-
return handleEmitSurface(request);
|
|
602
493
|
case 'capture_v2':
|
|
603
494
|
return handleCaptureV2(request);
|
|
604
495
|
case 'infer':
|
|
605
496
|
return handleInfer(request);
|
|
606
|
-
case 'build_workspace':
|
|
607
|
-
return handleBuildWorkspace(request);
|
|
608
|
-
case 'check_compatibility':
|
|
609
|
-
return handleCheckCompatibility(request);
|
|
610
497
|
case 'resolve_definitions':
|
|
611
498
|
return handleResolveDefinitions(request);
|
|
612
499
|
case 'retype_check':
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* bundle, so the two are kept in lockstep by `machinery-indicator-mirror.test.ts`
|
|
14
14
|
* rather than by sharing this module.
|
|
15
15
|
*/
|
|
16
|
-
import type { ts } from 'ts-morph';
|
|
16
|
+
import type { Project, ResolutionHostFactory, ts } from 'ts-morph';
|
|
17
17
|
/**
|
|
18
18
|
* True when a declaration's source file is runtime/library origin rather than
|
|
19
19
|
* user source. Four answers, in order:
|
|
@@ -32,14 +32,55 @@ import type { ts } from 'ts-morph';
|
|
|
32
32
|
* types from its own cache, a path with no `node_modules` segment, and
|
|
33
33
|
* `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
|
|
34
34
|
* the graph, which the compiler records on the file. Nothing crosses the
|
|
35
|
-
* capture seam; both programs are built with that host.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* capture seam; both programs are built with that host. The compiler's
|
|
36
|
+
* record does not survive an edit to a ts-morph project, so `imports`, the
|
|
37
|
+
* resolver's answers kept for the project's life, is read beside it
|
|
38
|
+
* (`ExternalImports`, carrick#1731). One exclusion: a workspace package
|
|
39
|
+
* reached through a `node_modules` symlink is also marked external by the
|
|
40
|
+
* compiler but is the user's own source, so a file inside the checkout
|
|
41
|
+
* (the nearest `.git` above the service root) that carries no
|
|
42
|
+
* `node_modules` segment stays user source.
|
|
40
43
|
*
|
|
41
44
|
* Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
|
|
42
45
|
* seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
|
|
43
|
-
* the pair on a real program.
|
|
46
|
+
* the pair on a real program. The capture builds each program once and never
|
|
47
|
+
* edits it, so it has no `imports` to read.
|
|
44
48
|
*/
|
|
45
|
-
export declare function isExternalOrigin(program: ts.Program, sourceFile: ts.SourceFile, repoRoot: string): boolean;
|
|
49
|
+
export declare function isExternalOrigin(program: ts.Program, sourceFile: ts.SourceFile, repoRoot: string, imports?: ExternalImports): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Every file a ts-morph project's resolver reached as an external-library
|
|
52
|
+
* import, recorded as the resolver answers (carrick#1731).
|
|
53
|
+
*
|
|
54
|
+
* The compiler keeps that verdict on each program (rule 4 of
|
|
55
|
+
* `isExternalOrigin`), but ts-morph rebuilds the program after any edit to the
|
|
56
|
+
* project, such as a probe file created and removed or a file rewritten and
|
|
57
|
+
* restored, and passes every file it has loaded as a ROOT file. The compiler
|
|
58
|
+
* marks no root file as external, so after the first edit no dependency is.
|
|
59
|
+
* Where a path test cannot answer (a Deno project serves npm types from its own
|
|
60
|
+
* cache), every library type then read as the user's: framework machinery was
|
|
61
|
+
* published as a contract and the printer inlined library types it keeps by
|
|
62
|
+
* name. The resolver's answer for a file does not change with the project's
|
|
63
|
+
* history, so it is kept here for as long as the project lives.
|
|
64
|
+
*
|
|
65
|
+
* The compiler's verdict also carries down: a file a library imports, even by
|
|
66
|
+
* a relative path the resolver answers as not external, is reached while the
|
|
67
|
+
* compiler is inside an external import and is marked external too. A Deno
|
|
68
|
+
* npm package's own relative imports are answered that way, so an answer for
|
|
69
|
+
* an import made from a recorded file is recorded as well.
|
|
70
|
+
*/
|
|
71
|
+
export declare class ExternalImports {
|
|
72
|
+
private readonly files;
|
|
73
|
+
/** `factory` as it was, with every answer it gives recorded. */
|
|
74
|
+
recording(factory: ResolutionHostFactory): ResolutionHostFactory;
|
|
75
|
+
/** Whether a resolution reached `fileName` as an external-library import. */
|
|
76
|
+
has(fileName: string): boolean;
|
|
77
|
+
/** Record the external answers to `containingFile`'s imports. */
|
|
78
|
+
private record;
|
|
79
|
+
}
|
|
80
|
+
/** Keep `imports` as the record of `project`, which resolves through it. */
|
|
81
|
+
export declare function registerExternalImports(project: Project, imports: ExternalImports): void;
|
|
82
|
+
/**
|
|
83
|
+
* The record of a project the loader built with a recording resolver, or
|
|
84
|
+
* `undefined` for any other project, which reads the compiler's flag alone.
|
|
85
|
+
*/
|
|
86
|
+
export declare function externalImportsOf(project: Project): ExternalImports | undefined;
|
|
@@ -33,17 +33,21 @@ import * as path from 'node:path';
|
|
|
33
33
|
* types from its own cache, a path with no `node_modules` segment, and
|
|
34
34
|
* `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
|
|
35
35
|
* the graph, which the compiler records on the file. Nothing crosses the
|
|
36
|
-
* capture seam; both programs are built with that host.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
36
|
+
* capture seam; both programs are built with that host. The compiler's
|
|
37
|
+
* record does not survive an edit to a ts-morph project, so `imports`, the
|
|
38
|
+
* resolver's answers kept for the project's life, is read beside it
|
|
39
|
+
* (`ExternalImports`, carrick#1731). One exclusion: a workspace package
|
|
40
|
+
* reached through a `node_modules` symlink is also marked external by the
|
|
41
|
+
* compiler but is the user's own source, so a file inside the checkout
|
|
42
|
+
* (the nearest `.git` above the service root) that carries no
|
|
43
|
+
* `node_modules` segment stays user source.
|
|
41
44
|
*
|
|
42
45
|
* Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
|
|
43
46
|
* seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
|
|
44
|
-
* the pair on a real program.
|
|
47
|
+
* the pair on a real program. The capture builds each program once and never
|
|
48
|
+
* edits it, so it has no `imports` to read.
|
|
45
49
|
*/
|
|
46
|
-
export function isExternalOrigin(program, sourceFile, repoRoot) {
|
|
50
|
+
export function isExternalOrigin(program, sourceFile, repoRoot, imports) {
|
|
47
51
|
const file = sourceFile.fileName.replace(/\\/g, '/');
|
|
48
52
|
if (file.includes('/.carrick/deno/')) {
|
|
49
53
|
return true;
|
|
@@ -54,7 +58,76 @@ export function isExternalOrigin(program, sourceFile, repoRoot) {
|
|
|
54
58
|
if (file.includes('/node_modules/')) {
|
|
55
59
|
return true;
|
|
56
60
|
}
|
|
57
|
-
|
|
61
|
+
const external = program.isSourceFileFromExternalLibrary(sourceFile) || imports?.has(file) === true;
|
|
62
|
+
return external && !isInsideCheckout(file, repoRoot);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Every file a ts-morph project's resolver reached as an external-library
|
|
66
|
+
* import, recorded as the resolver answers (carrick#1731).
|
|
67
|
+
*
|
|
68
|
+
* The compiler keeps that verdict on each program (rule 4 of
|
|
69
|
+
* `isExternalOrigin`), but ts-morph rebuilds the program after any edit to the
|
|
70
|
+
* project, such as a probe file created and removed or a file rewritten and
|
|
71
|
+
* restored, and passes every file it has loaded as a ROOT file. The compiler
|
|
72
|
+
* marks no root file as external, so after the first edit no dependency is.
|
|
73
|
+
* Where a path test cannot answer (a Deno project serves npm types from its own
|
|
74
|
+
* cache), every library type then read as the user's: framework machinery was
|
|
75
|
+
* published as a contract and the printer inlined library types it keeps by
|
|
76
|
+
* name. The resolver's answer for a file does not change with the project's
|
|
77
|
+
* history, so it is kept here for as long as the project lives.
|
|
78
|
+
*
|
|
79
|
+
* The compiler's verdict also carries down: a file a library imports, even by
|
|
80
|
+
* a relative path the resolver answers as not external, is reached while the
|
|
81
|
+
* compiler is inside an external import and is marked external too. A Deno
|
|
82
|
+
* npm package's own relative imports are answered that way, so an answer for
|
|
83
|
+
* an import made from a recorded file is recorded as well.
|
|
84
|
+
*/
|
|
85
|
+
export class ExternalImports {
|
|
86
|
+
files = new Set();
|
|
87
|
+
/** `factory` as it was, with every answer it gives recorded. */
|
|
88
|
+
recording(factory) {
|
|
89
|
+
return (moduleResolutionHost, getCompilerOptions) => {
|
|
90
|
+
const host = factory(moduleResolutionHost, getCompilerOptions);
|
|
91
|
+
return {
|
|
92
|
+
...host,
|
|
93
|
+
...(host.resolveModuleNames && {
|
|
94
|
+
resolveModuleNames: (...args) => this.record(args[1], host.resolveModuleNames(...args)),
|
|
95
|
+
}),
|
|
96
|
+
...(host.resolveTypeReferenceDirectives && {
|
|
97
|
+
resolveTypeReferenceDirectives: (...args) => this.record(args[1], host.resolveTypeReferenceDirectives(...args)),
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Whether a resolution reached `fileName` as an external-library import. */
|
|
103
|
+
has(fileName) {
|
|
104
|
+
return this.files.has(normalised(fileName));
|
|
105
|
+
}
|
|
106
|
+
/** Record the external answers to `containingFile`'s imports. */
|
|
107
|
+
record(containingFile, answers) {
|
|
108
|
+
const fromLibrary = this.has(containingFile);
|
|
109
|
+
for (const answer of answers) {
|
|
110
|
+
if (answer?.resolvedFileName && (fromLibrary || answer.isExternalLibraryImport)) {
|
|
111
|
+
this.files.add(normalised(answer.resolvedFileName));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return answers;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function normalised(fileName) {
|
|
118
|
+
return path.resolve(fileName).replace(/\\/g, '/');
|
|
119
|
+
}
|
|
120
|
+
const externalImportsByProject = new WeakMap();
|
|
121
|
+
/** Keep `imports` as the record of `project`, which resolves through it. */
|
|
122
|
+
export function registerExternalImports(project, imports) {
|
|
123
|
+
externalImportsByProject.set(project, imports);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The record of a project the loader built with a recording resolver, or
|
|
127
|
+
* `undefined` for any other project, which reads the compiler's flag alone.
|
|
128
|
+
*/
|
|
129
|
+
export function externalImportsOf(project) {
|
|
130
|
+
return externalImportsByProject.get(project);
|
|
58
131
|
}
|
|
59
132
|
const checkoutRoots = new Map();
|
|
60
133
|
/** The checkout the service root sits in: the nearest ancestor holding a
|
|
@@ -14,6 +14,7 @@ import * as path from 'node:path';
|
|
|
14
14
|
import * as fs from 'node:fs';
|
|
15
15
|
import { DenoProject, findDenoConfig, serviceConfigPath } from './capture/index.js';
|
|
16
16
|
import { moduleFormatResolutionHost } from './module-format.js';
|
|
17
|
+
import { ExternalImports, registerExternalImports } from './origin.js';
|
|
17
18
|
/**
|
|
18
19
|
* Source-file patterns used when the repo declares no tsconfig, relative to
|
|
19
20
|
* the repo root. `node_modules` is excluded explicitly: a glob that matches
|
|
@@ -190,14 +191,18 @@ export class ProjectLoader {
|
|
|
190
191
|
this.buildProject = () => {
|
|
191
192
|
const deno = new DenoProject(denoConfig, this.repoRoot);
|
|
192
193
|
this.denoProject = deno;
|
|
194
|
+
// The graph's external-library verdicts, kept past the program
|
|
195
|
+
// rebuilds that drop them (carrick#1731).
|
|
196
|
+
const imports = new ExternalImports();
|
|
193
197
|
const project = new Project({
|
|
194
198
|
compilerOptions: deno.parsed.options,
|
|
195
199
|
skipAddingFilesFromTsConfig: true,
|
|
196
|
-
resolutionHost: (host, getOptions) => ({
|
|
200
|
+
resolutionHost: imports.recording((host, getOptions) => ({
|
|
197
201
|
resolveModuleNames: (names, from) => names.map(name => deno.resolve(name, from, getOptions(), host)),
|
|
198
202
|
resolveTypeReferenceDirectives: (names, from) => names.map(name => deno.resolveTypeReference(typeof name === 'string' ? name : name.fileName, from, getOptions(), host)),
|
|
199
|
-
}),
|
|
203
|
+
})),
|
|
200
204
|
});
|
|
205
|
+
registerExternalImports(project, imports);
|
|
201
206
|
for (const file of deno.parsed.fileNames)
|
|
202
207
|
project.addSourceFileAtPath(file);
|
|
203
208
|
for (const diagnostic of deno.diagnostics)
|
|
@@ -313,12 +318,17 @@ export class ProjectLoader {
|
|
|
313
318
|
}
|
|
314
319
|
/** A ts-morph project built from one tsconfig and the files it lists. */
|
|
315
320
|
projectFromConfig(configPath) {
|
|
316
|
-
|
|
321
|
+
const imports = new ExternalImports();
|
|
322
|
+
const project = new Project({
|
|
317
323
|
tsConfigFilePath: configPath,
|
|
318
324
|
skipAddingFilesFromTsConfig: false,
|
|
319
|
-
// Each import resolves in its file's own module format (carrick#1619)
|
|
320
|
-
|
|
325
|
+
// Each import resolves in its file's own module format (carrick#1619),
|
|
326
|
+
// and every external-library answer is kept past the program rebuilds
|
|
327
|
+
// that drop it (carrick#1731).
|
|
328
|
+
resolutionHost: imports.recording(moduleFormatResolutionHost),
|
|
321
329
|
});
|
|
330
|
+
registerExternalImports(project, imports);
|
|
331
|
+
return project;
|
|
322
332
|
}
|
|
323
333
|
/**
|
|
324
334
|
* Which project types `file` (carrick#1604): the key of its owning
|
|
@@ -182,6 +182,25 @@ export declare class TypeInferrer {
|
|
|
182
182
|
*/
|
|
183
183
|
private resolveParamTarget;
|
|
184
184
|
private inferResponseBody;
|
|
185
|
+
/**
|
|
186
|
+
* True when a located call's own result is the route's payload, so the
|
|
187
|
+
* transitional drill into its first argument must not run (carrick#1732).
|
|
188
|
+
*
|
|
189
|
+
* `res.json(users)` reached that drill because nothing above it recognised
|
|
190
|
+
* the send: its result reads `void`, `any` or `unknown`, and the payload is
|
|
191
|
+
* the argument. `toPublicView(row)` is the opposite case: a mapper building
|
|
192
|
+
* the object the route sends. Its first argument is the row it was built
|
|
193
|
+
* FROM, which carries columns the route never sends.
|
|
194
|
+
*
|
|
195
|
+
* The call's result decides, not where its callee is declared. It is the
|
|
196
|
+
* payload when it reads as one by the rule a response helper's argument is
|
|
197
|
+
* read with (`nodeCarriesPayloadContract`: object-shaped, not machinery,
|
|
198
|
+
* not `void`/`any`/`unknown`) and the object is not a library's own: a
|
|
199
|
+
* codec's writer from `encode(message)` or a reply builder from a send is
|
|
200
|
+
* the library describing itself, and keeps the drill. A library call that
|
|
201
|
+
* returns the repo's own type (`toInstance(View, plain)`) is the payload.
|
|
202
|
+
*/
|
|
203
|
+
private callResultIsPayload;
|
|
185
204
|
/**
|
|
186
205
|
* The wire representation a request's printed type takes. A route response
|
|
187
206
|
* is serialised as JSON by every sender this layer reads a payload out of,
|
|
@@ -238,6 +257,30 @@ export declare class TypeInferrer {
|
|
|
238
257
|
*/
|
|
239
258
|
private receivingCallOf;
|
|
240
259
|
private inferCallResult;
|
|
260
|
+
/**
|
|
261
|
+
* What the source states the body read at `terminal` to be, when the type
|
|
262
|
+
* `extractExplicitTypeFromAncestor` printed for it is stated AT the read
|
|
263
|
+
* (carrick#1749): the read is the operand of that cast, or the initializer
|
|
264
|
+
* of that annotated declaration, through wrappers that leave a value as it
|
|
265
|
+
* is (parentheses, `await`, `!`, another cast). An annotation further out —
|
|
266
|
+
* the declared type of the function the read sits in — describes something
|
|
267
|
+
* else, and is not reported.
|
|
268
|
+
*/
|
|
269
|
+
private statedBodyAtRead;
|
|
270
|
+
/**
|
|
271
|
+
* A stated type with `any` or `unknown` written anywhere in it (`unknown`,
|
|
272
|
+
* `Record<string, unknown>`, `{ items: any[] }`) leaves a position open: it
|
|
273
|
+
* is a placeholder the source narrows later (`const data: unknown = await
|
|
274
|
+
* res.json()`, then `data as Entry[]`), not its statement of the body.
|
|
275
|
+
*/
|
|
276
|
+
private leavesAPositionOpen;
|
|
277
|
+
/**
|
|
278
|
+
* The named root of a stated body type: `Promise<...>` and `PromiseLike`
|
|
279
|
+
* (the language's await protocol), array levels, parentheses, `readonly`
|
|
280
|
+
* and `| null`/`| undefined` are peeled, and what is left is either a type
|
|
281
|
+
* reference, whose name is the root, or anything else, which has none.
|
|
282
|
+
*/
|
|
283
|
+
private statedRoot;
|
|
241
284
|
private inferVariable;
|
|
242
285
|
private inferExpression;
|
|
243
286
|
private inferRequestBody;
|
|
@@ -361,6 +404,33 @@ export declare class TypeInferrer {
|
|
|
361
404
|
* HTTP response is read exactly this way whatever produced the response, so
|
|
362
405
|
* the shape is structural, not a framework's name.
|
|
363
406
|
*/
|
|
407
|
+
/**
|
|
408
|
+
* The body read inside a chain that starts at `callExpr` (carrick#1749):
|
|
409
|
+
* `request(url).check(ok).mapOk(response => { const data = response as
|
|
410
|
+
* SearchResponse; ... })`.
|
|
411
|
+
*
|
|
412
|
+
* The chain is the run of member calls whose receiver is the call, then
|
|
413
|
+
* that call's result, and so on. A callback passed to one of them whose
|
|
414
|
+
* first parameter the compiler types `unknown` is handed a value nothing
|
|
415
|
+
* has typed yet, which is what a parsed body is; where the source casts
|
|
416
|
+
* that parameter, the cast is what the caller says the body is. The value
|
|
417
|
+
* the chain ends in is what the caller computed from it, and publishing
|
|
418
|
+
* that as the body is the false mismatch this rule exists for.
|
|
419
|
+
*
|
|
420
|
+
* Exactly one such cast across the whole chain answers. None, or more than
|
|
421
|
+
* one (a callback on the failure side can be handed an `unknown` too), and
|
|
422
|
+
* the walk carries on as before. A parameter typed `any` is not read: an
|
|
423
|
+
* unresolved library types every callback that way, success and failure
|
|
424
|
+
* alike, so it says nothing about which one is the body.
|
|
425
|
+
*/
|
|
426
|
+
private bodyReadInCallbackChain;
|
|
427
|
+
/**
|
|
428
|
+
* The casts of a callback's first parameter, when the compiler types that
|
|
429
|
+
* parameter `unknown`: `response as T` and `<T>response`, the operand being
|
|
430
|
+
* the parameter itself. A cast that leaves a position open states nothing
|
|
431
|
+
* about the body and is skipped.
|
|
432
|
+
*/
|
|
433
|
+
private castsOfUnreadParameter;
|
|
364
434
|
private bodyReadOnReceiver;
|
|
365
435
|
private collectDefUseNodes;
|
|
366
436
|
private expressionUsesNames;
|
|
@@ -539,6 +609,8 @@ export declare class TypeInferrer {
|
|
|
539
609
|
*/
|
|
540
610
|
private unwrapJsonStringifyArg;
|
|
541
611
|
private extractExplicitTypeFromAncestor;
|
|
612
|
+
/** The annotation `extractExplicitTypeFromAncestor` prints, as a node. */
|
|
613
|
+
private explicitTypeNodeFromAncestor;
|
|
542
614
|
/**
|
|
543
615
|
* Render an explicit annotation (`as T`, `<T>`, or a typed binding) as
|
|
544
616
|
* fully-structural text.
|