carrick 0.3.97 → 0.3.99
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/bin/carrick.mjs +19 -14
- package/dist/auth/run.d.ts +1 -1
- package/dist/auth/run.js +1 -1
- package/dist/contract.d.ts +15 -0
- package/dist/contract.js.map +1 -1
- package/dist/global-install.js +6 -5
- package/dist/global-install.js.map +1 -1
- package/dist/hook/reuse.d.ts +8 -3
- package/dist/hook/reuse.js +15 -3
- package/dist/hook/reuse.js.map +1 -1
- package/dist/init/connect.js +1 -1
- package/dist/init/connect.js.map +1 -1
- package/dist/init/doctor.js +5 -2
- package/dist/init/doctor.js.map +1 -1
- package/dist/init/hosted.d.ts +18 -1
- package/dist/init/hosted.js +84 -16
- package/dist/init/hosted.js.map +1 -1
- package/dist/init/install-id.d.ts +2 -2
- package/dist/init/install-id.js +3 -3
- package/dist/init/install-id.js.map +1 -1
- package/dist/init/mcp.d.ts +2 -2
- package/dist/init/mcp.js +2 -2
- package/dist/init/mcp.js.map +1 -1
- package/dist/init/outdated.d.ts +1 -1
- package/dist/init/outdated.js +1 -1
- package/dist/init/outdated.js.map +1 -1
- package/dist/init/output.d.ts +18 -1
- package/dist/init/output.js +40 -1
- package/dist/init/output.js.map +1 -1
- package/dist/init/remove.d.ts +71 -2
- package/dist/init/remove.js +474 -145
- package/dist/init/remove.js.map +1 -1
- package/dist/init/repo-copies.d.ts +16 -4
- package/dist/init/repo-copies.js +20 -6
- package/dist/init/repo-copies.js.map +1 -1
- package/dist/init/run.d.ts +12 -16
- package/dist/init/run.js +25 -42
- package/dist/init/run.js.map +1 -1
- package/dist/update.js +1 -0
- package/dist/update.js.map +1 -1
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/bundler.d.ts +2 -0
- package/sidecar/dist/src/bundler.js +115 -39
- package/sidecar/dist/src/capture/index.d.ts +1 -0
- package/sidecar/dist/src/capture/index.js +91 -10
- package/sidecar/dist/src/capture/project-references.d.ts +67 -0
- package/sidecar/dist/src/capture/project-references.js +157 -0
- package/sidecar/dist/src/client-semantics.d.ts +36 -0
- package/sidecar/dist/src/client-semantics.js +1008 -0
- package/sidecar/dist/src/index.js +110 -10
- package/sidecar/dist/src/module-format.d.ts +32 -0
- package/sidecar/dist/src/module-format.js +102 -0
- package/sidecar/dist/src/project-loader.d.ts +24 -0
- package/sidecar/dist/src/project-loader.js +53 -5
- package/sidecar/dist/src/types.d.ts +86 -4
- package/sidecar/dist/src/validators.d.ts +463 -0
- package/sidecar/dist/src/validators.js +50 -0
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - stderr is for logging
|
|
12
12
|
* - Process stays alive between requests (warm standby)
|
|
13
13
|
*/
|
|
14
|
+
import * as path from 'node:path';
|
|
14
15
|
import * as readline from 'node:readline';
|
|
15
16
|
import { parseRequest } from './validators.js';
|
|
16
17
|
import { ProjectLoader } from './project-loader.js';
|
|
@@ -20,29 +21,36 @@ import { MonorepoBuilder } from './monorepo-builder.js';
|
|
|
20
21
|
import { DefinitionResolver } from './definition-resolver.js';
|
|
21
22
|
import { captureStub, findDisqualifyingTopTypes, jsonWireDeclarations, runCheck, } from './capture/index.js';
|
|
22
23
|
import { Retyper } from './retype.js';
|
|
24
|
+
import { ClientSemanticsVerifier } from './client-semantics.js';
|
|
23
25
|
// ===========================================================================
|
|
24
26
|
// Module-level state
|
|
25
27
|
// ===========================================================================
|
|
26
28
|
let projectLoader = null;
|
|
27
29
|
let monorepoBuilder = null;
|
|
28
30
|
let initTimeMs = null;
|
|
29
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Components by project key (`ProjectLoader.projectKeyFor`): `''` is the
|
|
33
|
+
* default project, any other key the program of a project that owns some of
|
|
34
|
+
* the service's files (carrick#1604).
|
|
35
|
+
*/
|
|
36
|
+
let components = new Map();
|
|
30
37
|
/**
|
|
31
38
|
* Get the project-backed components, building the project if this is the
|
|
32
39
|
* first request that needs it.
|
|
33
40
|
*
|
|
34
41
|
* @throws if init has not run, or if the project cannot be built
|
|
35
42
|
*/
|
|
36
|
-
function projectComponents() {
|
|
43
|
+
function projectComponents(key = '') {
|
|
37
44
|
if (!projectLoader?.isInitialized()) {
|
|
38
45
|
throw new Error('Sidecar not initialized. Call init first.');
|
|
39
46
|
}
|
|
40
|
-
|
|
47
|
+
let built = components.get(key);
|
|
48
|
+
if (!built) {
|
|
41
49
|
// Bound here rather than read from the module slot inside the components:
|
|
42
50
|
// a re-init drops `components` and points the slot at another service, and
|
|
43
51
|
// nothing built over this project may follow it there.
|
|
44
52
|
const loader = projectLoader;
|
|
45
|
-
const project = loader.
|
|
53
|
+
const project = loader.getProjectFor(key);
|
|
46
54
|
const repoRoot = loader.getRepoRoot();
|
|
47
55
|
// The module graph, where the project resolved through one, is the only
|
|
48
56
|
// thing that can name the package a file belongs to: a Deno service
|
|
@@ -52,7 +60,7 @@ function projectComponents() {
|
|
|
52
60
|
repoRoot,
|
|
53
61
|
packageOf: (filePath) => loader.packageOf(filePath),
|
|
54
62
|
});
|
|
55
|
-
|
|
63
|
+
built = {
|
|
56
64
|
typeBundler: new TypeBundler({ project, repoRoot }),
|
|
57
65
|
surfaceEmitter: new SurfaceEmitter({ project, repoRoot }),
|
|
58
66
|
typeInferrer,
|
|
@@ -66,9 +74,43 @@ function projectComponents() {
|
|
|
66
74
|
// (`npm/carrick/package.json`) resolves `typescript ^5.8.0` unpinned,
|
|
67
75
|
// so an install can pair ts-morph's copy with a newer 5.x.
|
|
68
76
|
retyper: new Retyper(project, typeInferrer, jsonWireDeclarations, findDisqualifyingTopTypes),
|
|
77
|
+
semanticsVerifier: new ClientSemanticsVerifier(project),
|
|
69
78
|
};
|
|
79
|
+
components.set(key, built);
|
|
80
|
+
}
|
|
81
|
+
return built;
|
|
82
|
+
}
|
|
83
|
+
/** One bundle answer from the answers of several programs. */
|
|
84
|
+
function mergeBundles(results) {
|
|
85
|
+
const symbol_failures = results.flatMap((r) => r.symbol_failures ?? []);
|
|
86
|
+
const answered = results.filter((r) => r.success);
|
|
87
|
+
if (answered.length === 0) {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
symbol_failures,
|
|
91
|
+
errors: [...new Set(results.flatMap((r) => r.errors ?? []))],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
success: true,
|
|
96
|
+
dts_content: answered.map((r) => r.dts_content ?? '').join('\n'),
|
|
97
|
+
manifest: answered.flatMap((r) => r.manifest ?? []),
|
|
98
|
+
symbol_failures: symbol_failures.length > 0 ? symbol_failures : undefined,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Split a request's items by the project that types their file, keeping each
|
|
103
|
+
* group in request order. One group, the default project's, for a service
|
|
104
|
+
* whose tsconfig references nothing.
|
|
105
|
+
*/
|
|
106
|
+
function byProject(items, fileOf) {
|
|
107
|
+
// An empty request is still answered by the default project, as before.
|
|
108
|
+
const groups = new Map(items.length === 0 ? [['', []]] : []);
|
|
109
|
+
for (const item of items) {
|
|
110
|
+
const key = projectLoader?.projectKeyFor(fileOf(item)) ?? '';
|
|
111
|
+
groups.set(key, [...(groups.get(key) ?? []), item]);
|
|
70
112
|
}
|
|
71
|
-
return
|
|
113
|
+
return groups;
|
|
72
114
|
}
|
|
73
115
|
// ===========================================================================
|
|
74
116
|
// Request Handlers
|
|
@@ -82,7 +124,7 @@ function handleInit(request) {
|
|
|
82
124
|
log(`Initializing with repo_root: ${request.repo_root}`);
|
|
83
125
|
// Re-init re-scopes the sidecar to another root: drop everything built
|
|
84
126
|
// over the previous project before resolving the new one.
|
|
85
|
-
components =
|
|
127
|
+
components = new Map();
|
|
86
128
|
projectLoader = new ProjectLoader({
|
|
87
129
|
repoRoot: request.repo_root,
|
|
88
130
|
tsconfigPath: request.tsconfig_path,
|
|
@@ -128,7 +170,10 @@ function handleInit(request) {
|
|
|
128
170
|
function handleBundle(request) {
|
|
129
171
|
try {
|
|
130
172
|
log(`Bundling ${request.symbols.length} symbol(s)`);
|
|
131
|
-
|
|
173
|
+
// A symbol named by a path is bundled from the program of the project
|
|
174
|
+
// that owns that file (carrick#1604).
|
|
175
|
+
const results = [...byProject(request.symbols, (symbol) => symbol.source_file)].map(([key, symbols]) => projectComponents(key).typeBundler.bundle(symbols));
|
|
176
|
+
const result = results.length === 1 ? results[0] : mergeBundles(results);
|
|
132
177
|
if (!result.success) {
|
|
133
178
|
return {
|
|
134
179
|
request_id: request.request_id,
|
|
@@ -274,7 +319,18 @@ async function handleCheckV2Async(request) {
|
|
|
274
319
|
function handleInfer(request) {
|
|
275
320
|
try {
|
|
276
321
|
log(`Inferring ${request.requests.length} type(s)`);
|
|
277
|
-
const
|
|
322
|
+
const results = [...byProject(request.requests, (item) => item.file_path)].map(([key, items]) => projectComponents(key).typeInferrer.infer(items, request.extraction_config));
|
|
323
|
+
const result = results.length === 1
|
|
324
|
+
? results[0]
|
|
325
|
+
: (() => {
|
|
326
|
+
const inferred_types = results.flatMap((r) => r.inferred_types ?? []);
|
|
327
|
+
const errors = results.flatMap((r) => r.errors ?? []);
|
|
328
|
+
return {
|
|
329
|
+
success: errors.length === 0 || inferred_types.length > 0,
|
|
330
|
+
inferred_types,
|
|
331
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
332
|
+
};
|
|
333
|
+
})();
|
|
278
334
|
return {
|
|
279
335
|
request_id: request.request_id,
|
|
280
336
|
status: result.success ? 'success' : 'error',
|
|
@@ -304,7 +360,19 @@ const RETYPE_BUDGET_MS = 600_000;
|
|
|
304
360
|
function handleRetypeCheck(request) {
|
|
305
361
|
try {
|
|
306
362
|
log(`Retyping ${request.items.length} consumer call(s)`);
|
|
307
|
-
const
|
|
363
|
+
const budget = request.budget_ms ?? RETYPE_BUDGET_MS;
|
|
364
|
+
const groups = byProject(request.items.map((item, index) => ({ item, index })), ({ item }) => item.file_path);
|
|
365
|
+
const deadline = performance.now() + budget;
|
|
366
|
+
const outcomes = new Array(request.items.length);
|
|
367
|
+
for (const [key, entries] of groups) {
|
|
368
|
+
// One budget for the request, whichever programs it spans.
|
|
369
|
+
const remaining = groups.size === 1 ? budget : Math.max(0, deadline - performance.now());
|
|
370
|
+
projectComponents(key)
|
|
371
|
+
.retyper.run(entries.map(({ item }) => item), remaining)
|
|
372
|
+
.forEach((outcome, position) => {
|
|
373
|
+
outcomes[entries[position].index] = outcome;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
308
376
|
return { request_id: request.request_id, status: 'success', outcomes };
|
|
309
377
|
}
|
|
310
378
|
catch (err) {
|
|
@@ -313,6 +381,36 @@ function handleRetypeCheck(request) {
|
|
|
313
381
|
return { request_id: request.request_id, status: 'error', errors: [error] };
|
|
314
382
|
}
|
|
315
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* How long one verify_client_semantics request may spend before the checks it
|
|
386
|
+
* has not reached come back `unchecked` with reason `budget`. The first request
|
|
387
|
+
* on a service may pay the program build, which counts against it; the checks
|
|
388
|
+
* themselves are cheap. Well inside the scanner's 900s read deadline.
|
|
389
|
+
*/
|
|
390
|
+
const SEMANTICS_BUDGET_MS = 600_000;
|
|
391
|
+
/**
|
|
392
|
+
* Handle the 'verify_client_semantics' action - check library-semantics
|
|
393
|
+
* claims against each package's type declarations (carrick#1564)
|
|
394
|
+
*/
|
|
395
|
+
function handleVerifyClientSemantics(request) {
|
|
396
|
+
try {
|
|
397
|
+
const { semanticsVerifier } = projectComponents();
|
|
398
|
+
const fromDir = path.resolve(projectLoader.getRepoRoot(), request.from_dir);
|
|
399
|
+
log(`Verifying ${request.checks.length} client-semantics claim(s) from ${fromDir}`);
|
|
400
|
+
const { semantics, modules } = semanticsVerifier.run(fromDir, request.checks, request.budget_ms ?? SEMANTICS_BUDGET_MS);
|
|
401
|
+
return {
|
|
402
|
+
request_id: request.request_id,
|
|
403
|
+
status: 'success',
|
|
404
|
+
semantics,
|
|
405
|
+
semantics_modules: modules,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
410
|
+
logError(`Client-semantics check failed: ${error}`);
|
|
411
|
+
return { request_id: request.request_id, status: 'error', errors: [error] };
|
|
412
|
+
}
|
|
413
|
+
}
|
|
316
414
|
/**
|
|
317
415
|
* Handle the 'build_workspace' action - build synthetic monorepo workspace
|
|
318
416
|
*/
|
|
@@ -463,6 +561,8 @@ function handleRequest(request) {
|
|
|
463
561
|
return handleResolveDefinitions(request);
|
|
464
562
|
case 'retype_check':
|
|
465
563
|
return handleRetypeCheck(request);
|
|
564
|
+
case 'verify_client_semantics':
|
|
565
|
+
return handleVerifyClientSemantics(request);
|
|
466
566
|
case 'health':
|
|
467
567
|
return handleHealth(request);
|
|
468
568
|
case 'shutdown':
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the init'd project resolves an import: in the mode the compiler picks
|
|
3
|
+
* for the file that imports it (carrick#1619).
|
|
4
|
+
*
|
|
5
|
+
* Under `module` `node16`..`nodenext` the compiler reads each file's own
|
|
6
|
+
* format (its nearest `package.json` `"type"`, or its extension) to decide
|
|
7
|
+
* whether an import resolves with the `import` or the `require` export
|
|
8
|
+
* conditions. The program asks its host to create each file with that format,
|
|
9
|
+
* but ts-morph's host creates every file with the script target only, so no
|
|
10
|
+
* file had a format and every import resolved as a `require`. A package whose
|
|
11
|
+
* `exports` entry only has an `import` branch did not resolve at all, while
|
|
12
|
+
* the capture, which uses the compiler directly, resolved the same import.
|
|
13
|
+
*
|
|
14
|
+
* This resolution host gives the importing file its format before its imports
|
|
15
|
+
* are resolved, then resolves each import as the compiler's default does:
|
|
16
|
+
* `ts.resolveModuleName` in the mode `ts.getModeForUsageLocation` reads off
|
|
17
|
+
* that import. The format is only set under `node16`..`nodenext`. Under other
|
|
18
|
+
* `module` settings the compiler still takes an import's mode from its syntax
|
|
19
|
+
* (`import`, `require`, a `resolution-mode` attribute) and reads a file's
|
|
20
|
+
* format only in narrow cases; setting it there made answers worse, because
|
|
21
|
+
* ts-morph's TypeScript copy shares one resolution-cache entry across modes
|
|
22
|
+
* under Bundler (carrick#1632), so those settings keep their old modes.
|
|
23
|
+
*
|
|
24
|
+
* The hook receives one entry per import, by module name, so one name
|
|
25
|
+
* imported twice in different modes (a `require` beside an `import`) needs
|
|
26
|
+
* each entry matched to its own import. It is, by position, when every import
|
|
27
|
+
* of the name is being resolved; when only some of them are (the compiler
|
|
28
|
+
* reusing earlier answers) and their modes differ, the entry cannot be placed
|
|
29
|
+
* and is left unresolved rather than given another import's mode.
|
|
30
|
+
*/
|
|
31
|
+
import { type ResolutionHostFactory } from 'ts-morph';
|
|
32
|
+
export declare const moduleFormatResolutionHost: ResolutionHostFactory;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the init'd project resolves an import: in the mode the compiler picks
|
|
3
|
+
* for the file that imports it (carrick#1619).
|
|
4
|
+
*
|
|
5
|
+
* Under `module` `node16`..`nodenext` the compiler reads each file's own
|
|
6
|
+
* format (its nearest `package.json` `"type"`, or its extension) to decide
|
|
7
|
+
* whether an import resolves with the `import` or the `require` export
|
|
8
|
+
* conditions. The program asks its host to create each file with that format,
|
|
9
|
+
* but ts-morph's host creates every file with the script target only, so no
|
|
10
|
+
* file had a format and every import resolved as a `require`. A package whose
|
|
11
|
+
* `exports` entry only has an `import` branch did not resolve at all, while
|
|
12
|
+
* the capture, which uses the compiler directly, resolved the same import.
|
|
13
|
+
*
|
|
14
|
+
* This resolution host gives the importing file its format before its imports
|
|
15
|
+
* are resolved, then resolves each import as the compiler's default does:
|
|
16
|
+
* `ts.resolveModuleName` in the mode `ts.getModeForUsageLocation` reads off
|
|
17
|
+
* that import. The format is only set under `node16`..`nodenext`. Under other
|
|
18
|
+
* `module` settings the compiler still takes an import's mode from its syntax
|
|
19
|
+
* (`import`, `require`, a `resolution-mode` attribute) and reads a file's
|
|
20
|
+
* format only in narrow cases; setting it there made answers worse, because
|
|
21
|
+
* ts-morph's TypeScript copy shares one resolution-cache entry across modes
|
|
22
|
+
* under Bundler (carrick#1632), so those settings keep their old modes.
|
|
23
|
+
*
|
|
24
|
+
* The hook receives one entry per import, by module name, so one name
|
|
25
|
+
* imported twice in different modes (a `require` beside an `import`) needs
|
|
26
|
+
* each entry matched to its own import. It is, by position, when every import
|
|
27
|
+
* of the name is being resolved; when only some of them are (the compiler
|
|
28
|
+
* reusing earlier answers) and their modes differ, the entry cannot be placed
|
|
29
|
+
* and is left unresolved rather than given another import's mode.
|
|
30
|
+
*/
|
|
31
|
+
import { ts } from 'ts-morph';
|
|
32
|
+
/** Whether the compiler decides an import's mode from the importing file's format. */
|
|
33
|
+
function formatDecidesMode(options) {
|
|
34
|
+
const kind = options.module;
|
|
35
|
+
return kind !== undefined && kind >= ts.ModuleKind.Node16 && kind <= ts.ModuleKind.NodeNext;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The literals in `file` the program resolves, by module name and in source
|
|
39
|
+
* order: its imports, then its module augmentations. Neither list is in the
|
|
40
|
+
* public declaration file.
|
|
41
|
+
*/
|
|
42
|
+
function usagesOf(file) {
|
|
43
|
+
const internal = file;
|
|
44
|
+
const usages = new Map();
|
|
45
|
+
const add = (literal) => {
|
|
46
|
+
usages.set(literal.text, [...(usages.get(literal.text) ?? []), literal]);
|
|
47
|
+
};
|
|
48
|
+
for (const literal of internal.imports ?? [])
|
|
49
|
+
add(literal);
|
|
50
|
+
for (const literal of internal.moduleAugmentations ?? []) {
|
|
51
|
+
if (ts.isStringLiteral(literal))
|
|
52
|
+
add(literal);
|
|
53
|
+
}
|
|
54
|
+
return usages;
|
|
55
|
+
}
|
|
56
|
+
/** Why a module name cannot be given one mode: see the header. */
|
|
57
|
+
const UNPLACED = Symbol('unplaced');
|
|
58
|
+
export const moduleFormatResolutionHost = (host, getOptions) => {
|
|
59
|
+
let cache;
|
|
60
|
+
let cacheOptions;
|
|
61
|
+
const cacheFor = (options) => {
|
|
62
|
+
if (!cache || cacheOptions !== options) {
|
|
63
|
+
cache = ts.createModuleResolutionCache(process.cwd(), (fileName) => fileName, options);
|
|
64
|
+
cacheOptions = options;
|
|
65
|
+
}
|
|
66
|
+
return cache;
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
resolveModuleNames: (moduleNames, containingFile, _reusedNames, redirectedReference, options, containingSourceFile) => {
|
|
70
|
+
const compilerOptions = options ?? getOptions();
|
|
71
|
+
const resolutionCache = cacheFor(compilerOptions);
|
|
72
|
+
if (containingSourceFile &&
|
|
73
|
+
containingSourceFile.impliedNodeFormat === undefined &&
|
|
74
|
+
formatDecidesMode(compilerOptions)) {
|
|
75
|
+
containingSourceFile.impliedNodeFormat =
|
|
76
|
+
ts.getImpliedNodeFormatForFile(containingFile, resolutionCache.getPackageJsonInfoCache(), host, compilerOptions);
|
|
77
|
+
}
|
|
78
|
+
const usages = containingSourceFile ? usagesOf(containingSourceFile) : undefined;
|
|
79
|
+
const asked = new Map();
|
|
80
|
+
for (const name of moduleNames)
|
|
81
|
+
asked.set(name, (asked.get(name) ?? 0) + 1);
|
|
82
|
+
const seen = new Map();
|
|
83
|
+
const modeOf = (name) => {
|
|
84
|
+
const occurrence = seen.get(name) ?? 0;
|
|
85
|
+
seen.set(name, occurrence + 1);
|
|
86
|
+
const literals = usages?.get(name) ?? [];
|
|
87
|
+
if (!containingSourceFile || literals.length === 0)
|
|
88
|
+
return undefined;
|
|
89
|
+
const modes = literals.map((literal) => ts.getModeForUsageLocation(containingSourceFile, literal, compilerOptions));
|
|
90
|
+
if (new Set(modes).size === 1)
|
|
91
|
+
return modes[0];
|
|
92
|
+
return asked.get(name) === literals.length ? modes[occurrence] : UNPLACED;
|
|
93
|
+
};
|
|
94
|
+
return moduleNames.map((name) => {
|
|
95
|
+
const mode = modeOf(name);
|
|
96
|
+
if (mode === UNPLACED)
|
|
97
|
+
return undefined;
|
|
98
|
+
return ts.resolveModuleName(name, containingFile, compilerOptions, host, resolutionCache, redirectedReference, mode).resolvedModule;
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
};
|
|
@@ -61,6 +61,12 @@ export declare class ProjectLoader {
|
|
|
61
61
|
* build, so it is there from the first `getProject()` onwards.
|
|
62
62
|
*/
|
|
63
63
|
private denoProject;
|
|
64
|
+
/** The tsconfig the service names, when the project is built from one. */
|
|
65
|
+
private namedTsconfigPath;
|
|
66
|
+
/** Each file's owning config (carrick#1604), resolved on first use. */
|
|
67
|
+
private configPathFor;
|
|
68
|
+
/** Programs built for owning projects other than the named tsconfig. */
|
|
69
|
+
private readonly ownerProjects;
|
|
64
70
|
private readonly repoRoot;
|
|
65
71
|
private readonly tsconfigPath;
|
|
66
72
|
private readonly tsconfigSnapshot;
|
|
@@ -82,6 +88,24 @@ export declare class ProjectLoader {
|
|
|
82
88
|
* Convert a TsconfigSnapshot to ts-morph CompilerOptions
|
|
83
89
|
*/
|
|
84
90
|
private snapshotToCompilerOptions;
|
|
91
|
+
/** A ts-morph project built from one tsconfig and the files it lists. */
|
|
92
|
+
private projectFromConfig;
|
|
93
|
+
/**
|
|
94
|
+
* Which project types `file` (carrick#1604): the key of its owning
|
|
95
|
+
* project's program, or `''` for the default one. A tsconfig that
|
|
96
|
+
* references other projects owns only the files it lists itself; any other
|
|
97
|
+
* file belongs to the first project it references (depth-first, in
|
|
98
|
+
* declared order) whose file list includes it, and is typed under that
|
|
99
|
+
* project's options. A file no project includes, and every request that
|
|
100
|
+
* names no file, uses the default project, built from the named tsconfig
|
|
101
|
+
* as before.
|
|
102
|
+
*/
|
|
103
|
+
projectKeyFor(file?: string): string;
|
|
104
|
+
/**
|
|
105
|
+
* The project behind a key from `projectKeyFor`, built on first use and
|
|
106
|
+
* kept: a service whose files all belong to one project builds one.
|
|
107
|
+
*/
|
|
108
|
+
getProjectFor(key: string): Project;
|
|
85
109
|
/**
|
|
86
110
|
* Get the ts-morph Project, building it on first call.
|
|
87
111
|
*
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
import { Project } from 'ts-morph';
|
|
13
13
|
import * as path from 'node:path';
|
|
14
14
|
import * as fs from 'node:fs';
|
|
15
|
-
import { DenoProject, findDenoConfig } from './capture/index.js';
|
|
15
|
+
import { DenoProject, findDenoConfig, serviceConfigPath } from './capture/index.js';
|
|
16
|
+
import { moduleFormatResolutionHost } from './module-format.js';
|
|
16
17
|
/**
|
|
17
18
|
* Source-file patterns used when the repo declares no tsconfig, relative to
|
|
18
19
|
* the repo root. `node_modules` is excluded explicitly: a glob that matches
|
|
@@ -115,6 +116,12 @@ export class ProjectLoader {
|
|
|
115
116
|
* build, so it is there from the first `getProject()` onwards.
|
|
116
117
|
*/
|
|
117
118
|
denoProject;
|
|
119
|
+
/** The tsconfig the service names, when the project is built from one. */
|
|
120
|
+
namedTsconfigPath;
|
|
121
|
+
/** Each file's owning config (carrick#1604), resolved on first use. */
|
|
122
|
+
configPathFor;
|
|
123
|
+
/** Programs built for owning projects other than the named tsconfig. */
|
|
124
|
+
ownerProjects = new Map();
|
|
118
125
|
repoRoot;
|
|
119
126
|
tsconfigPath;
|
|
120
127
|
tsconfigSnapshot;
|
|
@@ -200,10 +207,8 @@ export class ProjectLoader {
|
|
|
200
207
|
}
|
|
201
208
|
else if (tsconfigPath) {
|
|
202
209
|
this.log(`Project will load with tsconfig: ${tsconfigPath}`);
|
|
203
|
-
this.
|
|
204
|
-
|
|
205
|
-
skipAddingFilesFromTsConfig: false,
|
|
206
|
-
});
|
|
210
|
+
this.namedTsconfigPath = tsconfigPath;
|
|
211
|
+
this.buildProject = () => this.projectFromConfig(tsconfigPath);
|
|
207
212
|
}
|
|
208
213
|
else {
|
|
209
214
|
this.log('No tsconfig.json found, using default compiler options');
|
|
@@ -306,6 +311,49 @@ export class ProjectLoader {
|
|
|
306
311
|
}
|
|
307
312
|
return result;
|
|
308
313
|
}
|
|
314
|
+
/** A ts-morph project built from one tsconfig and the files it lists. */
|
|
315
|
+
projectFromConfig(configPath) {
|
|
316
|
+
return new Project({
|
|
317
|
+
tsConfigFilePath: configPath,
|
|
318
|
+
skipAddingFilesFromTsConfig: false,
|
|
319
|
+
// Each import resolves in its file's own module format (carrick#1619).
|
|
320
|
+
resolutionHost: moduleFormatResolutionHost,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Which project types `file` (carrick#1604): the key of its owning
|
|
325
|
+
* project's program, or `''` for the default one. A tsconfig that
|
|
326
|
+
* references other projects owns only the files it lists itself; any other
|
|
327
|
+
* file belongs to the first project it references (depth-first, in
|
|
328
|
+
* declared order) whose file list includes it, and is typed under that
|
|
329
|
+
* project's options. A file no project includes, and every request that
|
|
330
|
+
* names no file, uses the default project, built from the named tsconfig
|
|
331
|
+
* as before.
|
|
332
|
+
*/
|
|
333
|
+
projectKeyFor(file) {
|
|
334
|
+
if (!this.namedTsconfigPath || file === undefined)
|
|
335
|
+
return '';
|
|
336
|
+
this.configPathFor ??= serviceConfigPath(this.namedTsconfigPath, (d) => this.logError(d));
|
|
337
|
+
const config = this.configPathFor(path.resolve(this.repoRoot, file));
|
|
338
|
+
return config === this.namedTsconfigPath ? '' : config;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The project behind a key from `projectKeyFor`, built on first use and
|
|
342
|
+
* kept: a service whose files all belong to one project builds one.
|
|
343
|
+
*/
|
|
344
|
+
getProjectFor(key) {
|
|
345
|
+
if (key === '')
|
|
346
|
+
return this.getProject();
|
|
347
|
+
let project = this.ownerProjects.get(key);
|
|
348
|
+
if (!project) {
|
|
349
|
+
const startTime = performance.now();
|
|
350
|
+
project = this.projectFromConfig(key);
|
|
351
|
+
this.ownerProjects.set(key, project);
|
|
352
|
+
this.log(`Project for ${key}, the project that owns the requested files, built in ` +
|
|
353
|
+
`${Math.round(performance.now() - startTime)}ms (${project.getSourceFiles().length} source files)`);
|
|
354
|
+
}
|
|
355
|
+
return project;
|
|
356
|
+
}
|
|
309
357
|
/**
|
|
310
358
|
* Get the ts-morph Project, building it on first call.
|
|
311
359
|
*
|
|
@@ -320,10 +320,64 @@ export interface RetypeCheckRequest extends BaseRequest {
|
|
|
320
320
|
/** Time the request may spend; items it does not reach abstain. */
|
|
321
321
|
budget_ms?: number;
|
|
322
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* One library-semantics claim to check against the package's own type
|
|
325
|
+
* declarations, on one receiver (carrick#1564). The unit of verification is
|
|
326
|
+
* the pair `(claim_id, receiver)`.
|
|
327
|
+
*/
|
|
328
|
+
export interface SemanticsCheck {
|
|
329
|
+
claim_id: string;
|
|
330
|
+
/** Module specifier, resolved from the request's `from_dir`. */
|
|
331
|
+
package: string;
|
|
332
|
+
/** `"default"` or a named export. */
|
|
333
|
+
export: string;
|
|
334
|
+
/** `"export"` or `"instance:<factory member>"`. */
|
|
335
|
+
receiver: string;
|
|
336
|
+
claim: {
|
|
337
|
+
kind: 'factory';
|
|
338
|
+
member: string;
|
|
339
|
+
base_url_key: string;
|
|
340
|
+
} | {
|
|
341
|
+
kind: 'verb';
|
|
342
|
+
member: string;
|
|
343
|
+
method: string;
|
|
344
|
+
} | {
|
|
345
|
+
kind: 'verb_body';
|
|
346
|
+
member: string;
|
|
347
|
+
args: 'path_body' | 'path_options';
|
|
348
|
+
body_key?: string;
|
|
349
|
+
} | {
|
|
350
|
+
kind: 'request';
|
|
351
|
+
member: string | null;
|
|
352
|
+
args: 'config' | 'path_options';
|
|
353
|
+
url_key?: string;
|
|
354
|
+
method_key: string;
|
|
355
|
+
} | {
|
|
356
|
+
kind: 'request_body';
|
|
357
|
+
member: string | null;
|
|
358
|
+
args: 'config' | 'path_options';
|
|
359
|
+
url_key?: string;
|
|
360
|
+
method_key: string;
|
|
361
|
+
body_key: string;
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Check library-semantics claims against each package's type declarations.
|
|
366
|
+
* Needs `init`: module resolution runs under the service's compiler options,
|
|
367
|
+
* in the service's program.
|
|
368
|
+
*/
|
|
369
|
+
export interface VerifyClientSemanticsRequest extends BaseRequest {
|
|
370
|
+
action: 'verify_client_semantics';
|
|
371
|
+
/** Absolute service root; module resolution starts here. */
|
|
372
|
+
from_dir: string;
|
|
373
|
+
checks: SemanticsCheck[];
|
|
374
|
+
/** Checks not reached in time come back `unchecked` with reason `budget`. */
|
|
375
|
+
budget_ms?: number;
|
|
376
|
+
}
|
|
323
377
|
/**
|
|
324
378
|
* Union type for all possible sidecar requests
|
|
325
379
|
*/
|
|
326
|
-
export type SidecarRequest = RetypeCheckRequest | InitRequest | BundleRequest | EmitSurfaceRequest | CaptureV2Request | CheckV2Request | InferRequest | BuildWorkspaceRequest | CheckCompatibilityRequest | ResolveDefinitionsRequest | HealthRequest | ShutdownRequest;
|
|
380
|
+
export type SidecarRequest = RetypeCheckRequest | VerifyClientSemanticsRequest | InitRequest | BundleRequest | EmitSurfaceRequest | CaptureV2Request | CheckV2Request | InferRequest | BuildWorkspaceRequest | CheckCompatibilityRequest | ResolveDefinitionsRequest | HealthRequest | ShutdownRequest;
|
|
327
381
|
/**
|
|
328
382
|
* Request for a specific symbol to be bundled
|
|
329
383
|
*/
|
|
@@ -532,10 +586,37 @@ export interface RetypeCheckResponse extends BaseResponse {
|
|
|
532
586
|
outcomes?: RetypeOutcome[];
|
|
533
587
|
errors?: string[];
|
|
534
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* The verdict on one `(claim_id, receiver)` pair.
|
|
591
|
+
*
|
|
592
|
+
* - `verified`: the declarations satisfy the claim.
|
|
593
|
+
* - `failed`: the declarations resolved and contradict it.
|
|
594
|
+
* - `unchecked`: the declarations could not be read.
|
|
595
|
+
*/
|
|
596
|
+
export interface SemanticsResult {
|
|
597
|
+
claim_id: string;
|
|
598
|
+
receiver: string;
|
|
599
|
+
verdict: 'verified' | 'failed' | 'unchecked';
|
|
600
|
+
/** Absent exactly when verified. */
|
|
601
|
+
reason?: string;
|
|
602
|
+
}
|
|
603
|
+
/** How one package of the request resolved, for logs. */
|
|
604
|
+
export interface SemanticsModule {
|
|
605
|
+
package: string;
|
|
606
|
+
resolved_file?: string;
|
|
607
|
+
installed_version?: string;
|
|
608
|
+
reason?: string;
|
|
609
|
+
}
|
|
610
|
+
export interface VerifyClientSemanticsResponse extends BaseResponse {
|
|
611
|
+
/** Exactly one per check, in request order. */
|
|
612
|
+
semantics?: SemanticsResult[];
|
|
613
|
+
semantics_modules?: SemanticsModule[];
|
|
614
|
+
errors?: string[];
|
|
615
|
+
}
|
|
535
616
|
/**
|
|
536
617
|
* Union type for all possible sidecar responses
|
|
537
618
|
*/
|
|
538
|
-
export type SidecarResponse = RetypeCheckResponse | InitResponse | BundleResponse | EmitSurfaceResponse | CaptureV2Response | CheckV2Response | InferResponse | BuildWorkspaceResponse | CheckCompatibilityResponse | ResolveDefinitionsResponse | HealthResponse | ShutdownResponse | ErrorResponse;
|
|
619
|
+
export type SidecarResponse = RetypeCheckResponse | VerifyClientSemanticsResponse | InitResponse | BundleResponse | EmitSurfaceResponse | CaptureV2Response | CheckV2Response | InferResponse | BuildWorkspaceResponse | CheckCompatibilityResponse | ResolveDefinitionsResponse | HealthResponse | ShutdownResponse | ErrorResponse;
|
|
539
620
|
/**
|
|
540
621
|
* An entry in the type manifest
|
|
541
622
|
*/
|
|
@@ -604,8 +685,9 @@ export interface InferredType {
|
|
|
604
685
|
* anchor symbol has a resolvable source declaration. Lets the scanner's
|
|
605
686
|
* pub/sub two-anchor arbitration (carrick#413) re-aim a demoted explicit
|
|
606
687
|
* `SymbolRequest` at the tsc-witnessed payload type: the bundler requires
|
|
607
|
-
* the symbol to be
|
|
608
|
-
* inference is the only party that knows where that
|
|
688
|
+
* the symbol to be declared in, or re-exported by, the request's
|
|
689
|
+
* `source_file`, and the inference is the only party that knows where that
|
|
690
|
+
* is. Reported only by
|
|
609
691
|
* the pub/sub infer kinds (`function_param`, `expression`); other kinds
|
|
610
692
|
* omit it.
|
|
611
693
|
*/
|