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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carrick",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.110",
|
|
4
4
|
"description": "Maps your entire TypeScript codebase across services and repositories, giving AI agents full context on existing types, routes, and function behaviours over MCP before they write duplicate or breaking code.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -53,20 +53,19 @@
|
|
|
53
53
|
"@clack/prompts": "1.8.1",
|
|
54
54
|
"picocolors": "1.1.1",
|
|
55
55
|
"pnpm": "10.34.5",
|
|
56
|
-
"ts-morph": "
|
|
57
|
-
"typescript": "
|
|
56
|
+
"ts-morph": "28.0.0",
|
|
57
|
+
"typescript": "6.0.2",
|
|
58
58
|
"zod": "^3.23.0"
|
|
59
59
|
},
|
|
60
60
|
"optionalDependencies": {
|
|
61
|
-
"@carrick-tools/cli-darwin-arm64": "0.3.
|
|
62
|
-
"@carrick-tools/cli-darwin-x64": "0.3.
|
|
63
|
-
"@carrick-tools/cli-linux-arm64": "0.3.
|
|
64
|
-
"@carrick-tools/cli-linux-x64": "0.3.
|
|
65
|
-
"@carrick-tools/cli-win32-x64": "0.3.
|
|
61
|
+
"@carrick-tools/cli-darwin-arm64": "0.3.110",
|
|
62
|
+
"@carrick-tools/cli-darwin-x64": "0.3.110",
|
|
63
|
+
"@carrick-tools/cli-linux-arm64": "0.3.110",
|
|
64
|
+
"@carrick-tools/cli-linux-x64": "0.3.110",
|
|
65
|
+
"@carrick-tools/cli-win32-x64": "0.3.110"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@types/node": "^24.13.3"
|
|
69
|
-
"typescript": "^5.9.2"
|
|
68
|
+
"@types/node": "^24.13.3"
|
|
70
69
|
},
|
|
71
70
|
"publishConfig": {
|
|
72
71
|
"access": "public",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carrick",
|
|
3
3
|
"description": "Claude Code plugin for Carrick, which indexes TypeScript codebases across service and repository boundaries. After each edit it adds the file's routes, calls, and cross-service type mismatches to the session, and it registers Carrick's language server. It pairs with the Carrick MCP server, which lets agents search functions by intent rather than name.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.110",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Carrick",
|
|
7
7
|
"email": "hello@carrick.tools"
|
|
@@ -50,6 +50,16 @@ export declare class TypeBundler {
|
|
|
50
50
|
*/
|
|
51
51
|
private expandOrigin;
|
|
52
52
|
private extractTypeDefinition;
|
|
53
|
+
/**
|
|
54
|
+
* carrick#1841: what a consumer receives when the symbol the model named for
|
|
55
|
+
* its response is a response table keyed by status code (`{ 200: Item;
|
|
56
|
+
* 404: Problem }`): the table's 2xx values, expanded like any other body.
|
|
57
|
+
* `unknown` where the table states no success body (no 2xx row, or only
|
|
58
|
+
* rows such as `204: void`). `undefined` when the symbol is not a table, so
|
|
59
|
+
* the caller bundles it as written. The capture's symbol anchor reads the
|
|
60
|
+
* same rule (`capture/anchors.ts`), so both answers agree.
|
|
61
|
+
*/
|
|
62
|
+
private statusTableBodyText;
|
|
53
63
|
private elementNeedsArrayParens;
|
|
54
64
|
private extractTypeDefinitionBase;
|
|
55
65
|
private isPathSource;
|
|
@@ -7,6 +7,7 @@ import * as path from 'node:path';
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import { expandTypeStructural, } from './type-structural-expander.js';
|
|
9
9
|
import { externalImportsOf } from './origin.js';
|
|
10
|
+
import { statusTableBody } from './capture/index.js';
|
|
10
11
|
/**
|
|
11
12
|
* #248: upper bound on `SymbolRequest.array_depth`. SDL list nesting is
|
|
12
13
|
* realistically 1-3 levels (`[Order!]!`, at most `[[Order!]!]!`); this is a
|
|
@@ -301,6 +302,17 @@ export class TypeBundler {
|
|
|
301
302
|
};
|
|
302
303
|
}
|
|
303
304
|
extractTypeDefinition(symbol) {
|
|
305
|
+
const tableBody = symbol.consumer_response ? this.statusTableBodyText(symbol) : undefined;
|
|
306
|
+
if (tableBody !== undefined) {
|
|
307
|
+
const alias = symbol.alias || symbol.symbol_name;
|
|
308
|
+
const depth = symbol.array_depth ?? 0;
|
|
309
|
+
if (depth > MAX_ARRAY_DEPTH)
|
|
310
|
+
return null;
|
|
311
|
+
const typeString = depth > 0 && tableBody !== 'unknown'
|
|
312
|
+
? `(${tableBody})${'[]'.repeat(depth)}`
|
|
313
|
+
: tableBody;
|
|
314
|
+
return { definition: `export type ${alias} = ${typeString};`, typeString };
|
|
315
|
+
}
|
|
304
316
|
const base = this.extractTypeDefinitionBase(symbol);
|
|
305
317
|
if (!base)
|
|
306
318
|
return null;
|
|
@@ -338,6 +350,36 @@ export class TypeBundler {
|
|
|
338
350
|
const typeString = `${element}${'[]'.repeat(depth)}`;
|
|
339
351
|
return { definition: `export type ${alias} = ${typeString};`, typeString };
|
|
340
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* carrick#1841: what a consumer receives when the symbol the model named for
|
|
355
|
+
* its response is a response table keyed by status code (`{ 200: Item;
|
|
356
|
+
* 404: Problem }`): the table's 2xx values, expanded like any other body.
|
|
357
|
+
* `unknown` where the table states no success body (no 2xx row, or only
|
|
358
|
+
* rows such as `204: void`). `undefined` when the symbol is not a table, so
|
|
359
|
+
* the caller bundles it as written. The capture's symbol anchor reads the
|
|
360
|
+
* same rule (`capture/anchors.ts`), so both answers agree.
|
|
361
|
+
*/
|
|
362
|
+
statusTableBodyText(symbol) {
|
|
363
|
+
const site = this.siteOf(symbol);
|
|
364
|
+
if (!site)
|
|
365
|
+
return undefined;
|
|
366
|
+
const declaration = site.sourceFile.getTypeAlias(site.name) ?? site.sourceFile.getInterface(site.name);
|
|
367
|
+
if (!declaration)
|
|
368
|
+
return undefined;
|
|
369
|
+
const table = declaration.getType();
|
|
370
|
+
const checker = this.project.getTypeChecker().compilerObject;
|
|
371
|
+
const read = statusTableBody(checker, table.compilerType, declaration.compilerNode);
|
|
372
|
+
if (!read)
|
|
373
|
+
return undefined;
|
|
374
|
+
if (read.kind === 'no_body')
|
|
375
|
+
return 'unknown';
|
|
376
|
+
const bodies = read.entries.map((entry) => {
|
|
377
|
+
const value = table.getProperty(entry.key)?.getTypeAtLocation(declaration);
|
|
378
|
+
return value ? expandTypeStructural(value, this.expandOrigin()) : 'unknown';
|
|
379
|
+
});
|
|
380
|
+
const distinct = [...new Set(bodies)];
|
|
381
|
+
return distinct.length === 1 ? distinct[0] : distinct.map((body) => `(${body})`).join(' | ');
|
|
382
|
+
}
|
|
341
383
|
/// Whether the element type must be parenthesised before an `[]` suffix.
|
|
342
384
|
/// Interfaces/classes/enums are always object/nominal (never a top-level
|
|
343
385
|
/// union), so only type aliases and typed variables can carry a bare
|
|
@@ -10,6 +10,7 @@ import { printTypeForDestination, substituteUndeclaredNames, undeclaredNamesIn,
|
|
|
10
10
|
import { typeIsOrContainsMachinery } from './machinery.js';
|
|
11
11
|
import { installedPackageSpecifier } from './installed-package.js';
|
|
12
12
|
import { realPath } from './service-config.js';
|
|
13
|
+
import { statusTableBody } from './status-table.js';
|
|
13
14
|
import { unresolvedAtAnchor, unresolvedSpecifiersReachableFrom, } from './unresolved.js';
|
|
14
15
|
/**
|
|
15
16
|
* Repo-root-relative source file -> the specifier the surface entry imports
|
|
@@ -155,6 +156,37 @@ export function resolveAnchor(program, request, args) {
|
|
|
155
156
|
}
|
|
156
157
|
const arrayDepth = Math.max(0, request.array_depth ?? 0);
|
|
157
158
|
const declared = checker.getDeclaredTypeOfSymbol(resolvedExport);
|
|
159
|
+
// carrick#1841: a symbol named for what a consumer receives that is a
|
|
160
|
+
// response table keyed by status code (`{ 200: Item; 404: Problem }`) is
|
|
161
|
+
// not the body. The body is its 2xx rows, indexed out of the table so the
|
|
162
|
+
// compiler still emits it (`import('./m').Table[200]`). A table with no
|
|
163
|
+
// success body abstains, decided. The v1 bundle reads the same rule
|
|
164
|
+
// (`TypeBundler.statusTableBodyText`).
|
|
165
|
+
if (request.consumer_response) {
|
|
166
|
+
const table = statusTableBody(checker, declared, resolvedExport.declarations?.[0] ?? sourceFile);
|
|
167
|
+
if (table?.kind === 'no_body') {
|
|
168
|
+
return {
|
|
169
|
+
request,
|
|
170
|
+
aliasText: 'unknown',
|
|
171
|
+
serialization: 'structural_fallback',
|
|
172
|
+
abstainReason: `'${request.symbol_name}' is a response table keyed by status code with no ` +
|
|
173
|
+
'success row that states a body, so this consumer states no response contract',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (table?.kind === 'body') {
|
|
177
|
+
const rows = [...new Set(table.entries.map((entry) => entry.key))].map((key) => /^\d+$/.test(key)
|
|
178
|
+
? `import('${spec}').${request.symbol_name}[${key}]`
|
|
179
|
+
: `import('${spec}').${request.symbol_name}[${JSON.stringify(key)}]`);
|
|
180
|
+
const body = rows.join(' | ');
|
|
181
|
+
const unresolvedBody = unresolvedAtAnchor(program, sourceFile, declared, resolvedExport.declarations?.[0] ?? sourceFile, '<0>'.repeat(arrayDepth));
|
|
182
|
+
return {
|
|
183
|
+
request,
|
|
184
|
+
aliasText: arraySuffix && rows.length > 1 ? `(${body})${arraySuffix}` : `${body}${arraySuffix}`,
|
|
185
|
+
serialization: 'emitted',
|
|
186
|
+
...(unresolvedBody ? { unresolved: unresolvedBody } : {}),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
158
190
|
const unresolved = unresolvedAtAnchor(program, sourceFile, declared, resolvedExport.declarations?.[0] ?? sourceFile, '<0>'.repeat(arrayDepth));
|
|
159
191
|
return {
|
|
160
192
|
request,
|
|
@@ -50,6 +50,11 @@ export interface SymbolAnchorRequest {
|
|
|
50
50
|
* `import('./m').Sym[]`. Omitted/0 captures the symbol as-is.
|
|
51
51
|
*/
|
|
52
52
|
array_depth?: number;
|
|
53
|
+
/**
|
|
54
|
+
* carrick#1841: the symbol names what a consumer receives, so a response
|
|
55
|
+
* table keyed by status code captures as its 2xx body, not as the table.
|
|
56
|
+
*/
|
|
57
|
+
consumer_response?: boolean;
|
|
53
58
|
}
|
|
54
59
|
/**
|
|
55
60
|
* Inline literal type text with no addressable symbol (the v1 inline-alias
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import ts from 'typescript';
|
|
19
19
|
import * as fs from 'node:fs';
|
|
20
20
|
import * as path from 'node:path';
|
|
21
|
+
import { sidecarCompilerOptions } from './compiler-options.js';
|
|
21
22
|
import { findDisqualifyingTopTypes, provenanceOf } from './deep-walk.js';
|
|
22
23
|
export function openProbeProgram(probesDir, plans) {
|
|
23
24
|
if (plans.length === 0)
|
|
@@ -35,7 +36,7 @@ export function openProbeProgram(probesDir, plans) {
|
|
|
35
36
|
.filter((f) => fs.existsSync(f));
|
|
36
37
|
if (fileNames.length === 0)
|
|
37
38
|
return undefined;
|
|
38
|
-
const program = ts.createProgram(fileNames, { ...parsed.options, noEmit: true });
|
|
39
|
+
const program = ts.createProgram(fileNames, sidecarCompilerOptions({ ...parsed.options, noEmit: true }));
|
|
39
40
|
return { program, checker: program.getTypeChecker(), probesDir };
|
|
40
41
|
}
|
|
41
42
|
catch {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import type { CheckStubInput } from './api.js';
|
|
16
16
|
import type { ProbePlan } from './check-probe.js';
|
|
17
17
|
import { WriteGuard } from './guarded-fs.js';
|
|
18
|
+
import { type ResolutionEdges } from './resolution-edges.js';
|
|
18
19
|
export interface AssembledStub {
|
|
19
20
|
serviceName: string;
|
|
20
21
|
/** Workspace package dir under packages/ (== the @carrick/<dir> suffix). */
|
|
@@ -45,6 +46,15 @@ export interface AssembledWorkspace {
|
|
|
45
46
|
export declare function computeDedupeOverrides(stubs: {
|
|
46
47
|
dependencies: Record<string, string>;
|
|
47
48
|
}[]): Record<string, string>;
|
|
49
|
+
/**
|
|
50
|
+
* pnpm parent-scoped overrides (`<parent>@<version>><child>`) that install
|
|
51
|
+
* each edge the stubs recorded at the version the scanned repo installed
|
|
52
|
+
* (carrick#2091). A parent-scoped override wins over a generic one, so an
|
|
53
|
+
* edge's version first passes through the dedupe table: one physical copy per
|
|
54
|
+
* compat group still holds. An edge two stubs record at versions the dedupe
|
|
55
|
+
* does not reconcile is left to the resolver.
|
|
56
|
+
*/
|
|
57
|
+
export declare function computeResolutionOverrides(edgeSets: ResolutionEdges[], dedupe: Record<string, string>): Record<string, string>;
|
|
48
58
|
export interface AssembleOptions {
|
|
49
59
|
stubs: CheckStubInput[];
|
|
50
60
|
workspaceRoot?: string;
|
|
@@ -53,3 +63,4 @@ export interface AssembleOptions {
|
|
|
53
63
|
export declare function assembleWorkspace(opts: AssembleOptions): AssembledWorkspace;
|
|
54
64
|
/** Write the generated probe files into the assembled probes package. */
|
|
55
65
|
export declare function writeProbes(ws: AssembledWorkspace, plans: ProbePlan[]): void;
|
|
66
|
+
export declare const CHECKER_TSCONFIG: string;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import * as fs from 'node:fs';
|
|
16
16
|
import * as path from 'node:path';
|
|
17
17
|
import { WriteGuard } from './guarded-fs.js';
|
|
18
|
+
import { RESOLUTION_FILE } from './resolution-edges.js';
|
|
18
19
|
const PROBES_PACKAGE = 'carrick-probes';
|
|
19
20
|
function parseVersion(v) {
|
|
20
21
|
const core = v.replace(/^[^\d]*/, '').split('-')[0].split('+')[0];
|
|
@@ -90,6 +91,57 @@ export function computeDedupeOverrides(stubs) {
|
|
|
90
91
|
.sort()
|
|
91
92
|
.map((k) => [k, overrides[k]]));
|
|
92
93
|
}
|
|
94
|
+
/** A stub's recorded edges; a stub stored without the file records none. */
|
|
95
|
+
function readStubResolution(stubDir) {
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = JSON.parse(fs.readFileSync(path.join(stubDir, RESOLUTION_FILE), 'utf8'));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
const edges = parsed?.edges;
|
|
104
|
+
if (!edges || typeof edges !== 'object')
|
|
105
|
+
return {};
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [parent, children] of Object.entries(edges)) {
|
|
108
|
+
if (!children || typeof children !== 'object')
|
|
109
|
+
continue;
|
|
110
|
+
for (const [child, version] of Object.entries(children)) {
|
|
111
|
+
if (typeof version === 'string')
|
|
112
|
+
(out[parent] ??= {})[child] = version;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* pnpm parent-scoped overrides (`<parent>@<version>><child>`) that install
|
|
119
|
+
* each edge the stubs recorded at the version the scanned repo installed
|
|
120
|
+
* (carrick#2091). A parent-scoped override wins over a generic one, so an
|
|
121
|
+
* edge's version first passes through the dedupe table: one physical copy per
|
|
122
|
+
* compat group still holds. An edge two stubs record at versions the dedupe
|
|
123
|
+
* does not reconcile is left to the resolver.
|
|
124
|
+
*/
|
|
125
|
+
export function computeResolutionOverrides(edgeSets, dedupe) {
|
|
126
|
+
const versionsOf = new Map();
|
|
127
|
+
for (const edges of edgeSets) {
|
|
128
|
+
for (const [parent, children] of Object.entries(edges)) {
|
|
129
|
+
for (const [child, version] of Object.entries(children)) {
|
|
130
|
+
const key = `${parent}>${child}`;
|
|
131
|
+
if (!versionsOf.has(key))
|
|
132
|
+
versionsOf.set(key, new Set());
|
|
133
|
+
versionsOf.get(key).add(dedupe[`${child}@${version}`] ?? version);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const overrides = {};
|
|
138
|
+
for (const key of [...versionsOf.keys()].sort()) {
|
|
139
|
+
const versions = versionsOf.get(key);
|
|
140
|
+
if (versions.size === 1)
|
|
141
|
+
overrides[key] = [...versions][0];
|
|
142
|
+
}
|
|
143
|
+
return overrides;
|
|
144
|
+
}
|
|
93
145
|
function readStubPackageName(stubDir) {
|
|
94
146
|
const pkgPath = path.join(stubDir, 'package.json');
|
|
95
147
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
@@ -116,6 +168,7 @@ export function assembleWorkspace(opts) {
|
|
|
116
168
|
guard.writeFile(path.join(workspaceDir, 'pnpm-workspace.yaml'), 'packages:\n - "packages/*"\n');
|
|
117
169
|
const assembled = [];
|
|
118
170
|
const dependencySets = [];
|
|
171
|
+
const edgeSets = [];
|
|
119
172
|
const svcToPkg = new Map();
|
|
120
173
|
const dirToPkg = new Map();
|
|
121
174
|
const dirToSvc = new Map();
|
|
@@ -126,12 +179,20 @@ export function assembleWorkspace(opts) {
|
|
|
126
179
|
guard.copyTree(stub.stub_dir, dest, (src) => !src.split(path.sep).includes('node_modules'));
|
|
127
180
|
assembled.push({ serviceName: stub.service_name, packageDir, packageName });
|
|
128
181
|
dependencySets.push({ dependencies: readStubDependencies(stub.stub_dir) });
|
|
182
|
+
edgeSets.push(readStubResolution(stub.stub_dir));
|
|
129
183
|
svcToPkg.set(stub.service_name, packageName);
|
|
130
184
|
dirToPkg.set(packageDir, packageName);
|
|
131
185
|
dirToSvc.set(packageDir, stub.service_name);
|
|
132
186
|
}
|
|
133
|
-
// Root manifest carries the semver-dedupe overrides.
|
|
134
|
-
const
|
|
187
|
+
// Root manifest carries the semver-dedupe overrides and the recorded edges.
|
|
188
|
+
const dedupe = computeDedupeOverrides(dependencySets);
|
|
189
|
+
const merged = {
|
|
190
|
+
...dedupe,
|
|
191
|
+
...computeResolutionOverrides(edgeSets, dedupe),
|
|
192
|
+
};
|
|
193
|
+
const overrides = Object.fromEntries(Object.keys(merged)
|
|
194
|
+
.sort()
|
|
195
|
+
.map((k) => [k, merged[k]]));
|
|
135
196
|
guard.writeFile(path.join(workspaceDir, 'package.json'), JSON.stringify({
|
|
136
197
|
name: 'carrick-check-workspace',
|
|
137
198
|
version: '0.0.0',
|
|
@@ -181,12 +242,13 @@ const NPMRC = [
|
|
|
181
242
|
'strict-peer-dependencies=false',
|
|
182
243
|
// Determinism: install exactly the pinned closure, no implicit peer pull-in.
|
|
183
244
|
'auto-install-peers=false',
|
|
184
|
-
// Determinism: the scratch workspace has no committed lockfile
|
|
185
|
-
//
|
|
186
|
-
//
|
|
245
|
+
// Determinism: the scratch workspace has no committed lockfile. Direct deps
|
|
246
|
+
// are exact-pinned by the stubs, and every transitive edge a stub recorded
|
|
247
|
+
// from the scanned repo's installed tree is pinned by a parent-scoped
|
|
248
|
+
// override in the root manifest. Only unrecorded edges resolve, and for
|
|
249
|
+
// those prefer-offline resolves from the local store/metadata cache
|
|
187
250
|
// whenever possible (byte-stable across runs on a host), and the explicit
|
|
188
251
|
// resolution-mode pins pnpm's resolver behavior across pnpm versions.
|
|
189
|
-
// Direct deps are exact-pinned by the stubs; only transitives resolve.
|
|
190
252
|
'prefer-offline=true',
|
|
191
253
|
'resolution-mode=highest',
|
|
192
254
|
'',
|
|
@@ -195,8 +257,11 @@ const NPMRC = [
|
|
|
195
257
|
// TS2717 collisions so the poison rule can convert them to honest
|
|
196
258
|
// unverifiables). noUnusedLocals/Parameters:false keep TS6133/6196 off the
|
|
197
259
|
// gate/assignment lines. moduleResolution bundler accepts both node16
|
|
198
|
-
// `.js`-suffixed and extensionless specifiers in one program.
|
|
199
|
-
|
|
260
|
+
// `.js`-suffixed and extensionless specifiers in one program. The rest is
|
|
261
|
+
// what `sidecarCompilerOptions` sets on every other program (carrick#2019):
|
|
262
|
+
// `tsc` reads this file itself, so each value is written out, and a test
|
|
263
|
+
// holds the file to what that function would make of it.
|
|
264
|
+
export const CHECKER_TSCONFIG = JSON.stringify({
|
|
200
265
|
compilerOptions: {
|
|
201
266
|
strict: true,
|
|
202
267
|
exactOptionalPropertyTypes: false,
|
|
@@ -210,6 +275,14 @@ const CHECKER_TSCONFIG = JSON.stringify({
|
|
|
210
275
|
types: [],
|
|
211
276
|
noEmit: true,
|
|
212
277
|
forceConsistentCasingInFileNames: true,
|
|
278
|
+
esModuleInterop: false,
|
|
279
|
+
allowSyntheticDefaultImports: true,
|
|
280
|
+
resolveJsonModule: true,
|
|
281
|
+
alwaysStrict: true,
|
|
282
|
+
noUncheckedSideEffectImports: false,
|
|
283
|
+
libReplacement: true,
|
|
284
|
+
stableTypeOrdering: true,
|
|
285
|
+
ignoreDeprecations: '6.0',
|
|
213
286
|
},
|
|
214
287
|
include: ['probes/**/*.ts'],
|
|
215
288
|
}, null, 2) + '\n';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The options every program the sidecar builds carries (carrick#2019).
|
|
3
|
+
*
|
|
4
|
+
* Every `ts.createProgram`, every ts-morph `Project` and the `tsconfig.json`
|
|
5
|
+
* the check hands to `tsc` goes through `sidecarCompilerOptions`, whatever
|
|
6
|
+
* its options came from: a project's tsconfig, a snapshot of one, a Deno
|
|
7
|
+
* config, or the sidecar's own literal set.
|
|
8
|
+
*
|
|
9
|
+
* Two things are added:
|
|
10
|
+
*
|
|
11
|
+
* - `stableTypeOrdering`. Without it the compiler prints a union's members in
|
|
12
|
+
* the order the checker first met them, and an object made by a mapped type
|
|
13
|
+
* lists its properties in the order its keys were created, so the same type
|
|
14
|
+
* printed text that depended on what the process had read before. With it
|
|
15
|
+
* the checker orders types by their content, and one process prints the
|
|
16
|
+
* same text whatever it did first.
|
|
17
|
+
* - TypeScript 5's effective value for every option whose default TypeScript 6
|
|
18
|
+
* changed, where the options leave it unset (`typescript5Defaults`). A
|
|
19
|
+
* project written against TypeScript 5 is read as its own compiler reads
|
|
20
|
+
* it, and as the sidecar read it before it moved to 6. A project that sets
|
|
21
|
+
* an option keeps its own value. Several of those values are deprecated in
|
|
22
|
+
* TypeScript 6 (target ES5, moduleResolution node10, esModuleInterop false),
|
|
23
|
+
* and so are options many projects still set themselves (baseUrl), so
|
|
24
|
+
* `ignoreDeprecations` is set too: a deprecated option still works in 6, and
|
|
25
|
+
* the error it would raise is not the project's.
|
|
26
|
+
*/
|
|
27
|
+
import ts from 'typescript';
|
|
28
|
+
/**
|
|
29
|
+
* For each option whose default TypeScript 6 changed and `options` leaves
|
|
30
|
+
* unset, the value TypeScript 5 (5.9, its last minor) computes for it from
|
|
31
|
+
* the options that are set. Only unset options appear in the result.
|
|
32
|
+
*/
|
|
33
|
+
export declare function typescript5Defaults(options: ts.CompilerOptions): ts.CompilerOptions;
|
|
34
|
+
/**
|
|
35
|
+
* `options` with TypeScript 5's defaults filled in and the sidecar's own set.
|
|
36
|
+
* Takes ts-morph's `CompilerOptions` as well as the compiler's: the two
|
|
37
|
+
* declare the same option enums separately, and the values are the same.
|
|
38
|
+
*/
|
|
39
|
+
export declare function sidecarCompilerOptions<O extends object>(options: O): O;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The options every program the sidecar builds carries (carrick#2019).
|
|
3
|
+
*
|
|
4
|
+
* Every `ts.createProgram`, every ts-morph `Project` and the `tsconfig.json`
|
|
5
|
+
* the check hands to `tsc` goes through `sidecarCompilerOptions`, whatever
|
|
6
|
+
* its options came from: a project's tsconfig, a snapshot of one, a Deno
|
|
7
|
+
* config, or the sidecar's own literal set.
|
|
8
|
+
*
|
|
9
|
+
* Two things are added:
|
|
10
|
+
*
|
|
11
|
+
* - `stableTypeOrdering`. Without it the compiler prints a union's members in
|
|
12
|
+
* the order the checker first met them, and an object made by a mapped type
|
|
13
|
+
* lists its properties in the order its keys were created, so the same type
|
|
14
|
+
* printed text that depended on what the process had read before. With it
|
|
15
|
+
* the checker orders types by their content, and one process prints the
|
|
16
|
+
* same text whatever it did first.
|
|
17
|
+
* - TypeScript 5's effective value for every option whose default TypeScript 6
|
|
18
|
+
* changed, where the options leave it unset (`typescript5Defaults`). A
|
|
19
|
+
* project written against TypeScript 5 is read as its own compiler reads
|
|
20
|
+
* it, and as the sidecar read it before it moved to 6. A project that sets
|
|
21
|
+
* an option keeps its own value. Several of those values are deprecated in
|
|
22
|
+
* TypeScript 6 (target ES5, moduleResolution node10, esModuleInterop false),
|
|
23
|
+
* and so are options many projects still set themselves (baseUrl), so
|
|
24
|
+
* `ignoreDeprecations` is set too: a deprecated option still works in 6, and
|
|
25
|
+
* the error it would raise is not the project's.
|
|
26
|
+
*/
|
|
27
|
+
import ts from 'typescript';
|
|
28
|
+
/** Options the sidecar sets on every program, over anything else. */
|
|
29
|
+
const ALWAYS = {
|
|
30
|
+
stableTypeOrdering: true,
|
|
31
|
+
ignoreDeprecations: '6.0',
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* For each option whose default TypeScript 6 changed and `options` leaves
|
|
35
|
+
* unset, the value TypeScript 5 (5.9, its last minor) computes for it from
|
|
36
|
+
* the options that are set. Only unset options appear in the result.
|
|
37
|
+
*/
|
|
38
|
+
export function typescript5Defaults(options) {
|
|
39
|
+
const fill = {};
|
|
40
|
+
const { ModuleKind: M, ModuleResolutionKind: R, ScriptTarget: T } = ts;
|
|
41
|
+
// 5.x ignores ES3 as it ignores no target: both take the default.
|
|
42
|
+
const statedTarget = options.target === T.ES3 ? undefined : options.target;
|
|
43
|
+
const target = statedTarget ??
|
|
44
|
+
(options.module === M.Node16 || options.module === M.Node18
|
|
45
|
+
? T.ES2022
|
|
46
|
+
: options.module === M.Node20
|
|
47
|
+
? T.ES2023
|
|
48
|
+
: options.module === M.NodeNext
|
|
49
|
+
? T.ESNext
|
|
50
|
+
: T.ES5);
|
|
51
|
+
if (statedTarget === undefined)
|
|
52
|
+
fill.target = target;
|
|
53
|
+
const module = options.module ?? (target >= T.ES2015 ? M.ES2015 : M.CommonJS);
|
|
54
|
+
if (options.module === undefined)
|
|
55
|
+
fill.module = module;
|
|
56
|
+
const moduleResolution = options.moduleResolution ??
|
|
57
|
+
(module === M.CommonJS
|
|
58
|
+
? R.Node10
|
|
59
|
+
: module === M.Node16 || module === M.Node18 || module === M.Node20
|
|
60
|
+
? R.Node16
|
|
61
|
+
: module === M.NodeNext
|
|
62
|
+
? R.NodeNext
|
|
63
|
+
: module === M.Preserve
|
|
64
|
+
? R.Bundler
|
|
65
|
+
: R.Classic);
|
|
66
|
+
if (options.moduleResolution === undefined)
|
|
67
|
+
fill.moduleResolution = moduleResolution;
|
|
68
|
+
const nodeFamily = module === M.Node16 || module === M.Node18 || module === M.Node20 || module === M.NodeNext;
|
|
69
|
+
const esModuleInterop = options.esModuleInterop ?? (nodeFamily || module === M.Preserve);
|
|
70
|
+
if (options.esModuleInterop === undefined)
|
|
71
|
+
fill.esModuleInterop = esModuleInterop;
|
|
72
|
+
if (options.allowSyntheticDefaultImports === undefined) {
|
|
73
|
+
fill.allowSyntheticDefaultImports = esModuleInterop || module === M.System || moduleResolution === R.Bundler;
|
|
74
|
+
}
|
|
75
|
+
if (options.resolveJsonModule === undefined) {
|
|
76
|
+
fill.resolveJsonModule = module === M.Node20 || module === M.NodeNext || moduleResolution === R.Bundler;
|
|
77
|
+
}
|
|
78
|
+
// Each strict flag a project leaves unset follows `strict`, which was off
|
|
79
|
+
// unless set; `alwaysStrict` followed it too.
|
|
80
|
+
if (options.strict === undefined)
|
|
81
|
+
fill.strict = false;
|
|
82
|
+
if (options.alwaysStrict === undefined)
|
|
83
|
+
fill.alwaysStrict = options.strict ?? false;
|
|
84
|
+
// Every package under each `typeRoots` directory (`@types/*`), not none.
|
|
85
|
+
if (options.types === undefined)
|
|
86
|
+
fill.types = ['*'];
|
|
87
|
+
if (options.noUncheckedSideEffectImports === undefined)
|
|
88
|
+
fill.noUncheckedSideEffectImports = false;
|
|
89
|
+
if (options.libReplacement === undefined)
|
|
90
|
+
fill.libReplacement = true;
|
|
91
|
+
return fill;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* `options` with TypeScript 5's defaults filled in and the sidecar's own set.
|
|
95
|
+
* Takes ts-morph's `CompilerOptions` as well as the compiler's: the two
|
|
96
|
+
* declare the same option enums separately, and the values are the same.
|
|
97
|
+
*/
|
|
98
|
+
export function sidecarCompilerOptions(options) {
|
|
99
|
+
return { ...options, ...typescript5Defaults(options), ...ALWAYS };
|
|
100
|
+
}
|
|
@@ -5,6 +5,7 @@ import * as path from 'node:path';
|
|
|
5
5
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
7
|
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { sidecarCompilerOptions } from './compiler-options.js';
|
|
8
9
|
import { rewriteSpecifiers } from './specifiers.js';
|
|
9
10
|
import { WriteGuard } from './guarded-fs.js';
|
|
10
11
|
function readConfig(file) {
|
|
@@ -452,11 +453,11 @@ export class DenoProject {
|
|
|
452
453
|
/** Runtime declarations belong to their producer, not the checker's globals. */
|
|
453
454
|
function isolateRuntime(guard, typesDir, files, runtimeRel) {
|
|
454
455
|
const runtimePath = path.join(typesDir, runtimeRel);
|
|
455
|
-
const program = ts.createProgram(files.map(file => path.join(typesDir, file)), {
|
|
456
|
+
const program = ts.createProgram(files.map(file => path.join(typesDir, file)), sidecarCompilerOptions({
|
|
456
457
|
strict: true, skipLibCheck: true, target: ts.ScriptTarget.ESNext,
|
|
457
458
|
module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
458
459
|
types: [],
|
|
459
|
-
});
|
|
460
|
+
}));
|
|
460
461
|
const checker = program.getTypeChecker();
|
|
461
462
|
const runtime = program.getSourceFile(runtimePath);
|
|
462
463
|
const retained = new Set();
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import type { CaptureStubOptions, CaptureStubResult } from './api.js';
|
|
31
31
|
export type { CaptureStubOptions, CaptureStubResult } from './api.js';
|
|
32
32
|
export { DenoProject, findDenoConfig } from './deno-project.js';
|
|
33
|
+
export { sidecarCompilerOptions } from './compiler-options.js';
|
|
33
34
|
export { serviceConfigPath } from './project-references.js';
|
|
34
35
|
export { findServiceTsconfig } from './service-config.js';
|
|
35
36
|
export { runCheck } from './check.js';
|
|
@@ -58,3 +59,4 @@ export declare function surfaceEntryFileName(): string;
|
|
|
58
59
|
/** Same normalization intent as bundle_file_stems on the Rust side. */
|
|
59
60
|
export declare function sanitizeServiceName(name: string): string;
|
|
60
61
|
export declare function captureStub(opts: CaptureStubOptions): CaptureStubResult;
|
|
62
|
+
export { classifyStatusCodes, constituents, httpStatus, statesNoBody, statusKeyCode, statusTableBody, statusTableOf, successEntries, } from './status-table.js';
|
|
@@ -33,6 +33,7 @@ import * as path from 'node:path';
|
|
|
33
33
|
import { entryRelativeSpecifier, resolveAnchor } from './anchors.js';
|
|
34
34
|
import { findAugmentationFiles } from './augmentations.js';
|
|
35
35
|
import { installedVersions, lockfileVersions } from './lockfile.js';
|
|
36
|
+
import { installedResolutionEdges, RESOLUTION_FILE } from './resolution-edges.js';
|
|
36
37
|
import { rewriteEmittedSpecifiers } from './paths-rewrite.js';
|
|
37
38
|
import { installedPackageSpecifier, typesPackageOf, withInstalledPackages } from './installed-package.js';
|
|
38
39
|
import { selfCheckStub } from './self-check.js';
|
|
@@ -42,7 +43,9 @@ import { emitsAlike, ProjectGraph } from './project-references.js';
|
|
|
42
43
|
import { findServiceTsconfig } from './service-config.js';
|
|
43
44
|
import { placeEmittedTree, surfaceModuleInTree } from './outside-root.js';
|
|
44
45
|
import { WriteGuard } from './guarded-fs.js';
|
|
46
|
+
import { sidecarCompilerOptions } from './compiler-options.js';
|
|
45
47
|
export { DenoProject, findDenoConfig } from './deno-project.js';
|
|
48
|
+
export { sidecarCompilerOptions } from './compiler-options.js';
|
|
46
49
|
export { serviceConfigPath } from './project-references.js';
|
|
47
50
|
export { findServiceTsconfig } from './service-config.js';
|
|
48
51
|
// v2 check core ("tsc as the judge"). Same bundle, same seam: the sidecar
|
|
@@ -282,7 +285,7 @@ export function captureStub(opts) {
|
|
|
282
285
|
try {
|
|
283
286
|
progress('emit', 'emitting declarations');
|
|
284
287
|
guard.writeFile(entryPath, entryLines.join('\n') + '\n');
|
|
285
|
-
const emitOptions = {
|
|
288
|
+
const emitOptions = sidecarCompilerOptions({
|
|
286
289
|
...parsed.options,
|
|
287
290
|
// The load-bearing trio: emit declarations without checking, so
|
|
288
291
|
// type-error-laden and bare (no node_modules) checkouts still emit.
|
|
@@ -295,7 +298,7 @@ export function captureStub(opts) {
|
|
|
295
298
|
incremental: false,
|
|
296
299
|
outDir: staging,
|
|
297
300
|
rootDir: entryDir,
|
|
298
|
-
};
|
|
301
|
+
});
|
|
299
302
|
written = emitDeclarations({
|
|
300
303
|
rootNames: [entryPath, ...augmentationSources],
|
|
301
304
|
options: emitOptions,
|
|
@@ -426,9 +429,8 @@ export function captureStub(opts) {
|
|
|
426
429
|
// repos whose lockfiles we do not parse (yarn classic v1, binary
|
|
427
430
|
// bun.lockb). The parsed lockfile (npm, pnpm, yarn-berry, text bun.lock)
|
|
428
431
|
// remains the bare-checkout fallback. Both paths pin only
|
|
429
|
-
// the directly-referenced externals;
|
|
430
|
-
//
|
|
431
|
-
// only transitives resolve").
|
|
432
|
+
// the directly-referenced externals; what those depend on is recorded
|
|
433
|
+
// from the installed tree below (RESOLUTION_FILE).
|
|
432
434
|
const installed = installedVersions(repoRoot, externalSpecs);
|
|
433
435
|
const lockVersions = lockfileVersions(repoRoot);
|
|
434
436
|
for (const name of Object.keys(deno?.pinned ?? {}))
|
|
@@ -449,6 +451,18 @@ export function captureStub(opts) {
|
|
|
449
451
|
else if (!rewritten.pins[typesPackageOf(name)])
|
|
450
452
|
unpinned.push(name);
|
|
451
453
|
}
|
|
454
|
+
// ---- Record how the installed tree resolved what the pins depend on ----
|
|
455
|
+
// The check pins each recorded edge, so it installs what the repo
|
|
456
|
+
// installed rather than the newest version in range (carrick#2091).
|
|
457
|
+
const resolution = deno
|
|
458
|
+
? { edges: {}, unrecorded: 0 }
|
|
459
|
+
: installedResolutionEdges(repoRoot, pinned);
|
|
460
|
+
const edgeCount = Object.values(resolution.edges).reduce((n, children) => n + Object.keys(children).length, 0);
|
|
461
|
+
if (edgeCount > 0) {
|
|
462
|
+
stubGuard.writeFile(path.join(stubDir, RESOLUTION_FILE), JSON.stringify({ edges: resolution.edges }, null, 2) + '\n');
|
|
463
|
+
}
|
|
464
|
+
console.error(`[sidecar] capture ${packageName}: ${edgeCount} dependency edge(s) recorded from the installed tree, ` +
|
|
465
|
+
`${resolution.unrecorded} left to the registry`);
|
|
452
466
|
const dependencyRoot = deno?.config.workspaceRoot ?? repoRoot;
|
|
453
467
|
const bareCheckout = !deno && !fs.existsSync(path.join(dependencyRoot, 'node_modules'));
|
|
454
468
|
stubGuard.writeFile(path.join(stubDir, 'package.json'), JSON.stringify({
|
|
@@ -523,7 +537,7 @@ function emitDeclarations(args) {
|
|
|
523
537
|
const emitted = new Map();
|
|
524
538
|
const declarationSources = new Map();
|
|
525
539
|
const sourceByEmitted = new Map();
|
|
526
|
-
const program = ts.createProgram(args.rootNames, args.options, args.host);
|
|
540
|
+
const program = ts.createProgram(args.rootNames, sidecarCompilerOptions(args.options), args.host);
|
|
527
541
|
const emitResult = program.emit(undefined, (fileName, text, _bom, _error, sources) => {
|
|
528
542
|
emitted.set(fileName, text);
|
|
529
543
|
if (sources?.[0])
|
|
@@ -690,10 +704,10 @@ function resolveAnchors(opts, parsed, ctx, deno) {
|
|
|
690
704
|
...new Set(opts.anchors
|
|
691
705
|
.flatMap((a) => (a.source_file ? [path.join(ctx.repoRoot, a.source_file)] : []))),
|
|
692
706
|
].filter((f) => fs.existsSync(f));
|
|
693
|
-
const options = {
|
|
707
|
+
const options = sidecarCompilerOptions({
|
|
694
708
|
...parsed.options,
|
|
695
709
|
noEmit: true,
|
|
696
|
-
};
|
|
710
|
+
});
|
|
697
711
|
const program = ts.createProgram([ctx.entryPath, ...anchorSources, ...(deno?.globals ?? [])], options, deno?.host(options));
|
|
698
712
|
// The checker binds every file of the program when it is first asked for,
|
|
699
713
|
// which the first anchor would otherwise do: asked for here, so that the
|
|
@@ -749,3 +763,6 @@ function resolveAnchors(opts, parsed, ctx, deno) {
|
|
|
749
763
|
ctx.guard.unlink(ctx.entryPath);
|
|
750
764
|
}
|
|
751
765
|
}
|
|
766
|
+
// carrick#1841: the response-table reading the inferrer and the v1 bundle
|
|
767
|
+
// share with the capture's symbol anchor.
|
|
768
|
+
export { classifyStatusCodes, constituents, httpStatus, statesNoBody, statusKeyCode, statusTableBody, statusTableOf, successEntries, } from './status-table.js';
|