carrick 0.3.102 → 0.3.104
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/api.d.ts +6 -0
- package/sidecar/dist/src/capture/check-classify.js +27 -0
- package/sidecar/dist/src/capture/check-fields.js +19 -5
- package/sidecar/dist/src/capture/check-probe.d.ts +6 -1
- package/sidecar/dist/src/capture/check-probe.js +45 -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.d.ts +1 -0
- package/sidecar/dist/src/capture/index.js +65 -27
- 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/capture/service-config.d.ts +19 -0
- package/sidecar/dist/src/capture/service-config.js +57 -0
- package/sidecar/dist/src/index.d.ts +1 -1
- package/sidecar/dist/src/index.js +4 -115
- package/sidecar/dist/src/origin.d.ts +49 -8
- package/sidecar/dist/src/origin.js +81 -8
- package/sidecar/dist/src/project-loader.d.ts +10 -2
- package/sidecar/dist/src/project-loader.js +22 -21
- 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 +39 -178
- package/sidecar/dist/src/validators.d.ts +70 -914
- package/sidecar/dist/src/validators.js +2 -55
- package/sidecar/dist/src/monorepo-builder.d.ts +0 -129
- package/sidecar/dist/src/monorepo-builder.js +0 -584
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carrick",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.104",
|
|
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",
|
|
@@ -58,11 +58,11 @@
|
|
|
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.104",
|
|
62
|
+
"@carrick-tools/cli-darwin-x64": "0.3.104",
|
|
63
|
+
"@carrick-tools/cli-linux-arm64": "0.3.104",
|
|
64
|
+
"@carrick-tools/cli-linux-x64": "0.3.104",
|
|
65
|
+
"@carrick-tools/cli-win32-x64": "0.3.104"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/node": "^24.13.3",
|
|
@@ -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.104",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Carrick",
|
|
7
7
|
"email": "hello@carrick.tools"
|
|
@@ -1,27 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* This module replaces the previous dts-bundle-generator approach with a
|
|
5
|
-
* ts-morph-based AST transformation and emission pipeline.
|
|
6
|
-
*
|
|
7
|
-
* Key features:
|
|
8
|
-
* - Does NOT bundle dependencies (unlike dts-bundle-generator)
|
|
9
|
-
* - Rewrites module specifiers to repo-specific aliases (@carrick/{repoName}/{spec})
|
|
10
|
-
* - Handles ImportDeclaration, ImportTypeNode, and ExportDeclaration
|
|
11
|
-
* - Supports scoped packages and subpaths
|
|
12
|
-
* - Includes a safety pass for any compiler-introduced raw specifiers
|
|
2
|
+
* TypeBundler - the `bundle` action: each requested symbol's declaration text,
|
|
3
|
+
* read from the init'd ts-morph project.
|
|
13
4
|
*/
|
|
14
5
|
import { Project } from 'ts-morph';
|
|
15
|
-
import type {
|
|
16
|
-
/**
|
|
17
|
-
* Options for SurfaceEmitter construction
|
|
18
|
-
*/
|
|
19
|
-
export interface SurfaceEmitterOptions {
|
|
20
|
-
/** The ts-morph Project instance */
|
|
21
|
-
project: Project;
|
|
22
|
-
/** The repository root directory (absolute path) */
|
|
23
|
-
repoRoot: string;
|
|
24
|
-
}
|
|
6
|
+
import type { SymbolRequest, BundleResult } from './types.js';
|
|
25
7
|
/**
|
|
26
8
|
* Options for TypeBundler construction (legacy)
|
|
27
9
|
*/
|
|
@@ -31,82 +13,11 @@ export interface TypeBundlerOptions {
|
|
|
31
13
|
/** The repository root directory (absolute path) */
|
|
32
14
|
repoRoot: string;
|
|
33
15
|
}
|
|
34
|
-
/**
|
|
35
|
-
* SurfaceEmitter - Generates .d.ts surface files with rewritten imports
|
|
36
|
-
*
|
|
37
|
-
* Usage:
|
|
38
|
-
* const emitter = new SurfaceEmitter({ project, repoRoot });
|
|
39
|
-
* const result = emitter.emit(repoName, payloads, outputPath);
|
|
40
|
-
*/
|
|
41
|
-
export declare class SurfaceEmitter {
|
|
42
|
-
private readonly project;
|
|
43
|
-
private readonly repoRoot;
|
|
44
|
-
constructor(options: SurfaceEmitterOptions);
|
|
45
|
-
/**
|
|
46
|
-
* Emit a surface .d.ts file containing payload type declarations
|
|
47
|
-
*
|
|
48
|
-
* @param repoName - Repository name for specifier rewriting
|
|
49
|
-
* @param payloads - Payload definitions to include
|
|
50
|
-
* @param outputPath - Path to write the surface file
|
|
51
|
-
* @returns SurfaceEmitResult
|
|
52
|
-
*/
|
|
53
|
-
emit(repoName: string, payloads: PayloadDefinition[], outputPath: string): SurfaceEmitResult;
|
|
54
|
-
/**
|
|
55
|
-
* Extract potential import references from a type string.
|
|
56
|
-
* These are module specifiers that might need rewriting.
|
|
57
|
-
*/
|
|
58
|
-
private extractImportReferences;
|
|
59
|
-
/**
|
|
60
|
-
* Rewrite all module specifiers in a source file to use repo-scoped aliases.
|
|
61
|
-
*
|
|
62
|
-
* Targets:
|
|
63
|
-
* 1. ImportDeclaration.moduleSpecifier
|
|
64
|
-
* 2. ImportTypeNode argument (import("..."))
|
|
65
|
-
* 3. ExportDeclaration.moduleSpecifier
|
|
66
|
-
*
|
|
67
|
-
* @returns Array of rewritten specifiers
|
|
68
|
-
*/
|
|
69
|
-
private rewriteModuleSpecifiers;
|
|
70
|
-
/**
|
|
71
|
-
* Determine if a module specifier should be rewritten.
|
|
72
|
-
* Only non-relative specifiers should be rewritten.
|
|
73
|
-
*/
|
|
74
|
-
private shouldRewriteSpecifier;
|
|
75
|
-
/**
|
|
76
|
-
* Rewrite a module specifier to the repo-scoped format.
|
|
77
|
-
*
|
|
78
|
-
* Format: @carrick/{repoName}/{originalSpecifier}
|
|
79
|
-
*
|
|
80
|
-
* Preserves:
|
|
81
|
-
* - Scoped packages: "@scope/pkg" → "@carrick/repo-a/@scope/pkg"
|
|
82
|
-
* - Subpaths: "zod/lib/helpers" → "@carrick/repo-a/zod/lib/helpers"
|
|
83
|
-
* - Combined: "@scope/pkg/subpath" → "@carrick/repo-a/@scope/pkg/subpath"
|
|
84
|
-
*/
|
|
85
|
-
private rewriteSpecifier;
|
|
86
|
-
/**
|
|
87
|
-
* Safety pass: Scan emitted content for any raw specifiers that might have
|
|
88
|
-
* been introduced by the TypeScript compiler and rewrite them.
|
|
89
|
-
*
|
|
90
|
-
* This catches edge cases where the compiler emits types with their original
|
|
91
|
-
* module specifiers that weren't caught by AST traversal.
|
|
92
|
-
*/
|
|
93
|
-
private safetyRewritePass;
|
|
94
|
-
/**
|
|
95
|
-
* Log a message to stderr
|
|
96
|
-
*/
|
|
97
|
-
private log;
|
|
98
|
-
/**
|
|
99
|
-
* Log an error to stderr
|
|
100
|
-
*/
|
|
101
|
-
private logError;
|
|
102
|
-
}
|
|
103
16
|
/**
|
|
104
17
|
* TypeBundler - Legacy bundler using simpler approach
|
|
105
18
|
*
|
|
106
19
|
* This is a simplified replacement that doesn't use dts-bundle-generator.
|
|
107
20
|
* It extracts type definitions directly using ts-morph.
|
|
108
|
-
*
|
|
109
|
-
* @deprecated Use SurfaceEmitter instead for the new architecture
|
|
110
21
|
*/
|
|
111
22
|
export declare class TypeBundler {
|
|
112
23
|
private readonly project;
|
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* This module replaces the previous dts-bundle-generator approach with a
|
|
5
|
-
* ts-morph-based AST transformation and emission pipeline.
|
|
6
|
-
*
|
|
7
|
-
* Key features:
|
|
8
|
-
* - Does NOT bundle dependencies (unlike dts-bundle-generator)
|
|
9
|
-
* - Rewrites module specifiers to repo-specific aliases (@carrick/{repoName}/{spec})
|
|
10
|
-
* - Handles ImportDeclaration, ImportTypeNode, and ExportDeclaration
|
|
11
|
-
* - Supports scoped packages and subpaths
|
|
12
|
-
* - Includes a safety pass for any compiler-introduced raw specifiers
|
|
2
|
+
* TypeBundler - the `bundle` action: each requested symbol's declaration text,
|
|
3
|
+
* read from the init'd ts-morph project.
|
|
13
4
|
*/
|
|
14
5
|
import { Node, ts, } from 'ts-morph';
|
|
15
6
|
import * as path from 'node:path';
|
|
16
7
|
import * as fs from 'node:fs';
|
|
17
8
|
import { expandTypeStructural, } from './type-structural-expander.js';
|
|
9
|
+
import { externalImportsOf } from './origin.js';
|
|
18
10
|
/**
|
|
19
11
|
* #248: upper bound on `SymbolRequest.array_depth`. SDL list nesting is
|
|
20
12
|
* realistically 1-3 levels (`[Order!]!`, at most `[[Order!]!]!`); this is a
|
|
@@ -116,267 +108,14 @@ function declarationSite(sourceFile, name, label) {
|
|
|
116
108
|
}
|
|
117
109
|
return { reason: `Symbol '${name}' not found in ${label}` };
|
|
118
110
|
}
|
|
119
|
-
/**
|
|
120
|
-
* SurfaceEmitter - Generates .d.ts surface files with rewritten imports
|
|
121
|
-
*
|
|
122
|
-
* Usage:
|
|
123
|
-
* const emitter = new SurfaceEmitter({ project, repoRoot });
|
|
124
|
-
* const result = emitter.emit(repoName, payloads, outputPath);
|
|
125
|
-
*/
|
|
126
|
-
export class SurfaceEmitter {
|
|
127
|
-
project;
|
|
128
|
-
repoRoot;
|
|
129
|
-
constructor(options) {
|
|
130
|
-
this.project = options.project;
|
|
131
|
-
this.repoRoot = options.repoRoot;
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Emit a surface .d.ts file containing payload type declarations
|
|
135
|
-
*
|
|
136
|
-
* @param repoName - Repository name for specifier rewriting
|
|
137
|
-
* @param payloads - Payload definitions to include
|
|
138
|
-
* @param outputPath - Path to write the surface file
|
|
139
|
-
* @returns SurfaceEmitResult
|
|
140
|
-
*/
|
|
141
|
-
emit(repoName, payloads, outputPath) {
|
|
142
|
-
const errors = [];
|
|
143
|
-
const manifest = [];
|
|
144
|
-
try {
|
|
145
|
-
// Phase 1: Generate surface content as a string
|
|
146
|
-
const surfaceLines = [
|
|
147
|
-
'// Generated surface file for type compatibility checking',
|
|
148
|
-
'// Do not edit - this file is auto-generated by Carrick',
|
|
149
|
-
'',
|
|
150
|
-
];
|
|
151
|
-
// Collect all import specifiers we need to track for rewriting
|
|
152
|
-
const importSpecifiers = new Set();
|
|
153
|
-
// Phase 2: Add payload type declarations
|
|
154
|
-
for (const payload of payloads) {
|
|
155
|
-
const { alias, type_string, source_file } = payload;
|
|
156
|
-
// Analyze type string for import references
|
|
157
|
-
const detectedImports = this.extractImportReferences(type_string);
|
|
158
|
-
for (const imp of detectedImports) {
|
|
159
|
-
importSpecifiers.add(imp);
|
|
160
|
-
}
|
|
161
|
-
// Add type alias declaration
|
|
162
|
-
surfaceLines.push(`export type ${alias} = ${type_string};`);
|
|
163
|
-
surfaceLines.push('');
|
|
164
|
-
manifest.push({
|
|
165
|
-
alias,
|
|
166
|
-
type_string,
|
|
167
|
-
rewritten_imports: [],
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
// Phase 3: Create a temporary source file for AST manipulation
|
|
171
|
-
const tempFileName = `__carrick_surface_${repoName}_${Date.now()}.d.ts`;
|
|
172
|
-
const tempFilePath = path.join(this.repoRoot, tempFileName);
|
|
173
|
-
const initialContent = surfaceLines.join('\n');
|
|
174
|
-
const tempSourceFile = this.project.createSourceFile(tempFilePath, initialContent, { overwrite: true });
|
|
175
|
-
// Phase 4: Rewrite module specifiers in the AST
|
|
176
|
-
const rewrittenImports = this.rewriteModuleSpecifiers(tempSourceFile, repoName);
|
|
177
|
-
// Update manifest with rewritten imports
|
|
178
|
-
for (const entry of manifest) {
|
|
179
|
-
entry.rewritten_imports = rewrittenImports;
|
|
180
|
-
}
|
|
181
|
-
// Phase 5: Get the transformed content
|
|
182
|
-
let surfaceContent = tempSourceFile.getFullText();
|
|
183
|
-
// Phase 6: Safety pass - catch any raw specifiers the compiler might have added
|
|
184
|
-
surfaceContent = this.safetyRewritePass(surfaceContent, repoName);
|
|
185
|
-
// Phase 7: Clean up temporary source file from project
|
|
186
|
-
this.project.removeSourceFile(tempSourceFile);
|
|
187
|
-
// Phase 8: Write output file
|
|
188
|
-
const absoluteOutputPath = path.isAbsolute(outputPath)
|
|
189
|
-
? outputPath
|
|
190
|
-
: path.join(this.repoRoot, outputPath);
|
|
191
|
-
// Ensure directory exists
|
|
192
|
-
const outputDir = path.dirname(absoluteOutputPath);
|
|
193
|
-
if (!fs.existsSync(outputDir)) {
|
|
194
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
195
|
-
}
|
|
196
|
-
fs.writeFileSync(absoluteOutputPath, surfaceContent, 'utf-8');
|
|
197
|
-
this.log(`Emitted surface file: ${absoluteOutputPath}`);
|
|
198
|
-
return {
|
|
199
|
-
success: true,
|
|
200
|
-
surface_content: surfaceContent,
|
|
201
|
-
output_path: absoluteOutputPath,
|
|
202
|
-
manifest,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
catch (err) {
|
|
206
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
207
|
-
this.logError(`Surface emission failed: ${error}`);
|
|
208
|
-
errors.push(error);
|
|
209
|
-
return {
|
|
210
|
-
success: false,
|
|
211
|
-
errors,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Extract potential import references from a type string.
|
|
217
|
-
* These are module specifiers that might need rewriting.
|
|
218
|
-
*/
|
|
219
|
-
extractImportReferences(typeString) {
|
|
220
|
-
const imports = [];
|
|
221
|
-
// Match import("...") syntax
|
|
222
|
-
const importTypeRegex = /import\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
223
|
-
let match;
|
|
224
|
-
while ((match = importTypeRegex.exec(typeString)) !== null) {
|
|
225
|
-
imports.push(match[1]);
|
|
226
|
-
}
|
|
227
|
-
// Match from "..." syntax (in case of inline imports)
|
|
228
|
-
const fromRegex = /from\s+["']([^"']+)["']/g;
|
|
229
|
-
while ((match = fromRegex.exec(typeString)) !== null) {
|
|
230
|
-
imports.push(match[1]);
|
|
231
|
-
}
|
|
232
|
-
return imports;
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Rewrite all module specifiers in a source file to use repo-scoped aliases.
|
|
236
|
-
*
|
|
237
|
-
* Targets:
|
|
238
|
-
* 1. ImportDeclaration.moduleSpecifier
|
|
239
|
-
* 2. ImportTypeNode argument (import("..."))
|
|
240
|
-
* 3. ExportDeclaration.moduleSpecifier
|
|
241
|
-
*
|
|
242
|
-
* @returns Array of rewritten specifiers
|
|
243
|
-
*/
|
|
244
|
-
rewriteModuleSpecifiers(sourceFile, repoName) {
|
|
245
|
-
const rewrittenSpecifiers = [];
|
|
246
|
-
// 1. Rewrite ImportDeclaration specifiers
|
|
247
|
-
const importDecls = sourceFile.getImportDeclarations();
|
|
248
|
-
for (const importDecl of importDecls) {
|
|
249
|
-
const specifier = importDecl.getModuleSpecifierValue();
|
|
250
|
-
if (this.shouldRewriteSpecifier(specifier)) {
|
|
251
|
-
const newSpecifier = this.rewriteSpecifier(specifier, repoName);
|
|
252
|
-
importDecl.setModuleSpecifier(newSpecifier);
|
|
253
|
-
rewrittenSpecifiers.push(`${specifier} → ${newSpecifier}`);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
// 2. Rewrite ExportDeclaration specifiers
|
|
257
|
-
const exportDecls = sourceFile.getExportDeclarations();
|
|
258
|
-
for (const exportDecl of exportDecls) {
|
|
259
|
-
const specifier = exportDecl.getModuleSpecifierValue();
|
|
260
|
-
if (specifier && this.shouldRewriteSpecifier(specifier)) {
|
|
261
|
-
const newSpecifier = this.rewriteSpecifier(specifier, repoName);
|
|
262
|
-
exportDecl.setModuleSpecifier(newSpecifier);
|
|
263
|
-
rewrittenSpecifiers.push(`${specifier} → ${newSpecifier}`);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
// 3. Rewrite ImportTypeNode arguments
|
|
267
|
-
// These appear in type positions: import("module").Type
|
|
268
|
-
sourceFile.forEachDescendant((node) => {
|
|
269
|
-
if (Node.isImportTypeNode(node)) {
|
|
270
|
-
const arg = node.getArgument();
|
|
271
|
-
if (Node.isLiteralTypeNode(arg)) {
|
|
272
|
-
const literal = arg.getLiteral();
|
|
273
|
-
if (Node.isStringLiteral(literal)) {
|
|
274
|
-
const specifier = literal.getLiteralValue();
|
|
275
|
-
if (this.shouldRewriteSpecifier(specifier)) {
|
|
276
|
-
const newSpecifier = this.rewriteSpecifier(specifier, repoName);
|
|
277
|
-
literal.setLiteralValue(newSpecifier);
|
|
278
|
-
rewrittenSpecifiers.push(`${specifier} → ${newSpecifier}`);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
return rewrittenSpecifiers;
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Determine if a module specifier should be rewritten.
|
|
288
|
-
* Only non-relative specifiers should be rewritten.
|
|
289
|
-
*/
|
|
290
|
-
shouldRewriteSpecifier(specifier) {
|
|
291
|
-
// Don't rewrite relative imports
|
|
292
|
-
if (specifier.startsWith('./') || specifier.startsWith('../')) {
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
// Don't rewrite already-rewritten specifiers
|
|
296
|
-
if (specifier.startsWith('@carrick/')) {
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
299
|
-
// Don't rewrite node built-ins
|
|
300
|
-
if (specifier.startsWith('node:')) {
|
|
301
|
-
return false;
|
|
302
|
-
}
|
|
303
|
-
// Some common node built-ins without prefix
|
|
304
|
-
const nodeBuiltins = [
|
|
305
|
-
'fs', 'path', 'os', 'util', 'events', 'stream', 'http', 'https',
|
|
306
|
-
'url', 'querystring', 'crypto', 'buffer', 'child_process', 'cluster',
|
|
307
|
-
'dgram', 'dns', 'net', 'readline', 'tls', 'tty', 'zlib', 'assert',
|
|
308
|
-
'async_hooks', 'console', 'constants', 'domain', 'inspector', 'module',
|
|
309
|
-
'perf_hooks', 'process', 'punycode', 'string_decoder', 'timers',
|
|
310
|
-
'trace_events', 'v8', 'vm', 'worker_threads'
|
|
311
|
-
];
|
|
312
|
-
if (nodeBuiltins.includes(specifier) || nodeBuiltins.includes(specifier.split('/')[0])) {
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Rewrite a module specifier to the repo-scoped format.
|
|
319
|
-
*
|
|
320
|
-
* Format: @carrick/{repoName}/{originalSpecifier}
|
|
321
|
-
*
|
|
322
|
-
* Preserves:
|
|
323
|
-
* - Scoped packages: "@scope/pkg" → "@carrick/repo-a/@scope/pkg"
|
|
324
|
-
* - Subpaths: "zod/lib/helpers" → "@carrick/repo-a/zod/lib/helpers"
|
|
325
|
-
* - Combined: "@scope/pkg/subpath" → "@carrick/repo-a/@scope/pkg/subpath"
|
|
326
|
-
*/
|
|
327
|
-
rewriteSpecifier(specifier, repoName) {
|
|
328
|
-
return `@carrick/${repoName}/${specifier}`;
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Safety pass: Scan emitted content for any raw specifiers that might have
|
|
332
|
-
* been introduced by the TypeScript compiler and rewrite them.
|
|
333
|
-
*
|
|
334
|
-
* This catches edge cases where the compiler emits types with their original
|
|
335
|
-
* module specifiers that weren't caught by AST traversal.
|
|
336
|
-
*/
|
|
337
|
-
safetyRewritePass(content, repoName) {
|
|
338
|
-
// Common patterns to catch:
|
|
339
|
-
// - from "package"
|
|
340
|
-
// - import("package")
|
|
341
|
-
// - export * from "package"
|
|
342
|
-
// We need to be careful not to double-rewrite @carrick/ specifiers
|
|
343
|
-
// Pattern for import("...") that's not already @carrick
|
|
344
|
-
const importTypePattern = /import\s*\(\s*["'](?!@carrick\/|\.\.?\/|node:)([^"']+)["']\s*\)/g;
|
|
345
|
-
content = content.replace(importTypePattern, (match, specifier) => {
|
|
346
|
-
const newSpecifier = this.rewriteSpecifier(specifier, repoName);
|
|
347
|
-
return match.replace(`"${specifier}"`, `"${newSpecifier}"`).replace(`'${specifier}'`, `'${newSpecifier}'`);
|
|
348
|
-
});
|
|
349
|
-
// Pattern for from "..." that's not already @carrick
|
|
350
|
-
const fromPattern = /from\s+["'](?!@carrick\/|\.\.?\/|node:)([^"']+)["']/g;
|
|
351
|
-
content = content.replace(fromPattern, (match, specifier) => {
|
|
352
|
-
const newSpecifier = this.rewriteSpecifier(specifier, repoName);
|
|
353
|
-
return match.replace(`"${specifier}"`, `"${newSpecifier}"`).replace(`'${specifier}'`, `'${newSpecifier}'`);
|
|
354
|
-
});
|
|
355
|
-
return content;
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Log a message to stderr
|
|
359
|
-
*/
|
|
360
|
-
log(message) {
|
|
361
|
-
console.error(`[sidecar:surface-emitter] ${message}`);
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Log an error to stderr
|
|
365
|
-
*/
|
|
366
|
-
logError(message) {
|
|
367
|
-
console.error(`[sidecar:surface-emitter:error] ${message}`);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
111
|
// =============================================================================
|
|
371
|
-
//
|
|
112
|
+
// TypeBundler
|
|
372
113
|
// =============================================================================
|
|
373
114
|
/**
|
|
374
115
|
* TypeBundler - Legacy bundler using simpler approach
|
|
375
116
|
*
|
|
376
117
|
* This is a simplified replacement that doesn't use dts-bundle-generator.
|
|
377
118
|
* It extracts type definitions directly using ts-morph.
|
|
378
|
-
*
|
|
379
|
-
* @deprecated Use SurfaceEmitter instead for the new architecture
|
|
380
119
|
*/
|
|
381
120
|
export class TypeBundler {
|
|
382
121
|
project;
|
|
@@ -558,6 +297,7 @@ export class TypeBundler {
|
|
|
558
297
|
return {
|
|
559
298
|
program: this.project.getProgram().compilerObject,
|
|
560
299
|
repoRoot: this.repoRoot,
|
|
300
|
+
imports: externalImportsOf(this.project),
|
|
561
301
|
};
|
|
562
302
|
}
|
|
563
303
|
extractTypeDefinition(symbol) {
|
|
@@ -54,13 +54,20 @@ export function resolveAnchor(program, request, args) {
|
|
|
54
54
|
// (a generated model that was never generated). The stub then self-checks
|
|
55
55
|
// such a name as an error placeholder, which no walk flags. A name a
|
|
56
56
|
// sibling symbol anchor imports is resolved by that import.
|
|
57
|
+
// carrick#1774: the text names types as they read where it was printed,
|
|
58
|
+
// so a name its source file means by a repo module's export is imported
|
|
59
|
+
// from that module (a string-union alias printed bare read `any`).
|
|
60
|
+
const scoped = siblingSpec
|
|
61
|
+
? undefined
|
|
62
|
+
: qualifyNamesFromSource(text, program, request.source_file, args);
|
|
63
|
+
const scopedText = scoped ?? text;
|
|
57
64
|
// carrick#1377: rewrite what nothing declares to `unknown` in place, so
|
|
58
65
|
// one member typed by a module the checkout does not have stops taking
|
|
59
66
|
// every member around it down with it.
|
|
60
67
|
const rewritten = siblingSpec || !args.placeholder
|
|
61
68
|
? undefined
|
|
62
|
-
: substituteUndeclaredNamesInText(
|
|
63
|
-
const aliasBody = rewritten?.text ??
|
|
69
|
+
: substituteUndeclaredNamesInText(scopedText, program, args.placeholder);
|
|
70
|
+
const aliasBody = rewritten?.text ?? scopedText;
|
|
64
71
|
const undeclaredNames = siblingSpec || !args.placeholder
|
|
65
72
|
? []
|
|
66
73
|
: undeclaredNamesInText(aliasBody, program, args.placeholder);
|
|
@@ -349,6 +356,95 @@ function substituteUndeclaredNamesInText(text, program, destination) {
|
|
|
349
356
|
paths: rewritten.substitutions.map((entry) => entry.path),
|
|
350
357
|
};
|
|
351
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* carrick#1774: literal text with every type name read where it was printed.
|
|
361
|
+
*
|
|
362
|
+
* The text was printed in `sourceFileRel` (the v1 inferrer prints at the node,
|
|
363
|
+
* where the module's own declarations and imports are in scope), so a type
|
|
364
|
+
* reference in it means what its leftmost name resolves to in that file. The
|
|
365
|
+
* surface entry is another scope: there such a name names nothing (TS2304, a
|
|
366
|
+
* member that reads `any`) or a global of the same name (`Notification`).
|
|
367
|
+
* Each reference whose name the source resolves to a type that a module
|
|
368
|
+
* inside the repo exports becomes `import('<module>').<export>`, with its
|
|
369
|
+
* qualifier and type arguments kept. Anything else is left as written: a
|
|
370
|
+
* global, a name only a function body declares, an unexported local, and a
|
|
371
|
+
* type declared outside the repo or under `node_modules`, which the stub does
|
|
372
|
+
* not ship.
|
|
373
|
+
*
|
|
374
|
+
* Returns the rewritten text, or undefined when nothing was rewritten, so a
|
|
375
|
+
* text with no such name stays byte-identical.
|
|
376
|
+
*/
|
|
377
|
+
function qualifyNamesFromSource(text, program, sourceFileRel, args) {
|
|
378
|
+
if (!sourceFileRel)
|
|
379
|
+
return undefined;
|
|
380
|
+
const source = program.getSourceFile(path.join(args.repoRoot, sourceFileRel));
|
|
381
|
+
const parsed = parseLiteralAnchor(text);
|
|
382
|
+
if (!source || !parsed)
|
|
383
|
+
return undefined;
|
|
384
|
+
const checker = program.getTypeChecker();
|
|
385
|
+
const meaning = ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias;
|
|
386
|
+
// Names the text declares itself (`[K in ...]`, `<T>(...)`, `infer U`).
|
|
387
|
+
const typeParameters = new Set();
|
|
388
|
+
const collect = (node) => {
|
|
389
|
+
if (ts.isTypeParameterDeclaration(node))
|
|
390
|
+
typeParameters.add(node.name.text);
|
|
391
|
+
ts.forEachChild(node, collect);
|
|
392
|
+
};
|
|
393
|
+
collect(parsed);
|
|
394
|
+
const imports = new Map();
|
|
395
|
+
const importFor = (name) => {
|
|
396
|
+
if (imports.has(name))
|
|
397
|
+
return imports.get(name);
|
|
398
|
+
let found;
|
|
399
|
+
const atSource = checker.resolveName(name, source, meaning, false);
|
|
400
|
+
const target = atSource && resolveSymbolAliases(checker, atSource);
|
|
401
|
+
const declaringFile = target?.declarations?.[0]?.getSourceFile();
|
|
402
|
+
const rel = declaringFile && path.relative(args.repoRoot, declaringFile.fileName);
|
|
403
|
+
if (target &&
|
|
404
|
+
declaringFile &&
|
|
405
|
+
rel &&
|
|
406
|
+
!rel.startsWith('..') &&
|
|
407
|
+
!rel.split(path.sep).includes('node_modules')) {
|
|
408
|
+
const moduleSymbol = checker.getSymbolAtLocation(declaringFile);
|
|
409
|
+
const exported = moduleSymbol
|
|
410
|
+
? checker
|
|
411
|
+
.getExportsOfModule(moduleSymbol)
|
|
412
|
+
.filter((candidate) => resolveSymbolAliases(checker, candidate) === target)
|
|
413
|
+
: [];
|
|
414
|
+
const chosen = exported.find((candidate) => candidate.getName() === name) ?? exported[0];
|
|
415
|
+
if (chosen) {
|
|
416
|
+
found = {
|
|
417
|
+
spec: entryRelativeSpecifier(args.entryDir, args.repoRoot, rel.split(path.sep).join('/')),
|
|
418
|
+
exportName: chosen.getName(),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
imports.set(name, found);
|
|
423
|
+
return found;
|
|
424
|
+
};
|
|
425
|
+
const leftmost = (name) => ts.isIdentifier(name) ? name : leftmost(name.left);
|
|
426
|
+
const renameLeftmost = (name, to) => ts.isIdentifier(name)
|
|
427
|
+
? ts.factory.createIdentifier(to)
|
|
428
|
+
: ts.factory.createQualifiedName(renameLeftmost(name.left, to), name.right);
|
|
429
|
+
let rewrites = 0;
|
|
430
|
+
const rewrite = (node) => {
|
|
431
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
432
|
+
const name = leftmost(node.typeName).text;
|
|
433
|
+
const target = typeParameters.has(name) ? undefined : importFor(name);
|
|
434
|
+
if (target) {
|
|
435
|
+
rewrites += 1;
|
|
436
|
+
return ts.factory.createImportTypeNode(ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(target.spec)), undefined, renameLeftmost(node.typeName, target.exportName), node.typeArguments?.map((argument) => rewrite(argument)), false);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return ts.visitEachChild(node, rewrite, /* context */ undefined);
|
|
440
|
+
};
|
|
441
|
+
const rewritten = rewrite(parsed);
|
|
442
|
+
if (rewrites === 0)
|
|
443
|
+
return undefined;
|
|
444
|
+
return ts
|
|
445
|
+
.createPrinter({ removeComments: true })
|
|
446
|
+
.printNode(ts.EmitHint.Unspecified, rewritten, parsed.getSourceFile());
|
|
447
|
+
}
|
|
352
448
|
/** The type node of `type __LiteralAnchor = <text>;`, or undefined. */
|
|
353
449
|
function parseLiteralAnchor(text) {
|
|
354
450
|
const parsed = ts.createSourceFile('literal-anchor.ts', `type __LiteralAnchor = ${text};`, ts.ScriptTarget.Latest, true);
|
|
@@ -839,17 +935,26 @@ function isPreferredTarget(node) {
|
|
|
839
935
|
ts.isParameter(node) ||
|
|
840
936
|
ts.isPropertyAssignment(node));
|
|
841
937
|
}
|
|
938
|
+
/**
|
|
939
|
+
* The deepest node a span names: a value-space target covering it, or a TYPE
|
|
940
|
+
* node it covers exactly (carrick#1775). A span is the scanner naming a node,
|
|
941
|
+
* and a declared type is named by its type node: the property type a generated
|
|
942
|
+
* GraphQL document declaration states for a field sits inside a type alias,
|
|
943
|
+
* where no value-space node covers it. Exactness keeps a span that names no
|
|
944
|
+
* node from being read as whatever type happens to enclose it.
|
|
945
|
+
*/
|
|
842
946
|
function tightestCoveringNode(sourceFile, start, end) {
|
|
843
947
|
let best;
|
|
844
948
|
const visit = (node) => {
|
|
845
|
-
|
|
846
|
-
|
|
949
|
+
const nodeStart = node.getStart(sourceFile);
|
|
950
|
+
const nodeEnd = node.getEnd();
|
|
951
|
+
if (nodeStart <= start && nodeEnd >= end) {
|
|
952
|
+
if (isPreferredTarget(node) ||
|
|
953
|
+
(ts.isTypeNode(node) && nodeStart === start && nodeEnd === end)) {
|
|
847
954
|
best = node;
|
|
848
|
-
|
|
849
|
-
}
|
|
850
|
-
else {
|
|
851
|
-
node.forEachChild(visit);
|
|
955
|
+
}
|
|
852
956
|
}
|
|
957
|
+
node.forEachChild(visit);
|
|
853
958
|
};
|
|
854
959
|
visit(sourceFile);
|
|
855
960
|
return best;
|
|
@@ -292,6 +292,12 @@ export interface CaptureStubOptions {
|
|
|
292
292
|
/** Directory the stub package is written into (created if missing). */
|
|
293
293
|
outDir: string;
|
|
294
294
|
tsconfigPath?: string;
|
|
295
|
+
/**
|
|
296
|
+
* The scanned repo's root, the upper bound of the search for a tsconfig
|
|
297
|
+
* above `repoRoot` when none is named (carrick#1776). Without it only
|
|
298
|
+
* `repoRoot` is searched.
|
|
299
|
+
*/
|
|
300
|
+
scanRoot?: string;
|
|
295
301
|
}
|
|
296
302
|
/** Wire protocol of a matched pair (drives the direction table). */
|
|
297
303
|
export type ProbeProtocol = 'http' | 'graphql' | 'socket' | 'pubsub';
|
|
@@ -171,6 +171,29 @@ export function classifyPair(input) {
|
|
|
171
171
|
...notAFact(`the ${side} sends a form-encoded body`, side),
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
|
+
// 4c. A body of bytes (carrick#1793): a blob, a buffer or a stream has no JSON
|
|
175
|
+
// shape, so a MISMATCH between the containers two sides hold the bytes
|
|
176
|
+
// in (a `Uint8Array` sent, read with `.blob()`) is not a drift. It
|
|
177
|
+
// overrides a mismatch only: bytes that assign to bytes (a stream sent
|
|
178
|
+
// where a stream is read) agree, and that verdict stands. When both
|
|
179
|
+
// sides are bytes the sent side is named, whatever order the
|
|
180
|
+
// diagnostics came in.
|
|
181
|
+
const firedGates = new Set(gateDiags.map((d) => plan.gateLines.get(d.line)));
|
|
182
|
+
const bytesGate = firedGates.has('sent:bytes')
|
|
183
|
+
? 'sent:bytes'
|
|
184
|
+
: firedGates.has('expected:bytes')
|
|
185
|
+
? 'expected:bytes'
|
|
186
|
+
: undefined;
|
|
187
|
+
const bytesVerdict = () => {
|
|
188
|
+
const { side } = sideForGate(bytesGate, plan);
|
|
189
|
+
return {
|
|
190
|
+
...base,
|
|
191
|
+
bucket: 'unverifiable',
|
|
192
|
+
gate: `${side}:bytes`,
|
|
193
|
+
diagnostic: `the ${side} body is bytes (a blob, a buffer or a stream), which has no JSON shape to compare with the other side.`,
|
|
194
|
+
...notAFact(`the ${side} body is bytes`, side),
|
|
195
|
+
};
|
|
196
|
+
};
|
|
174
197
|
// 5. Assignment-class error on the DECISIVE assignment line -> incompatible.
|
|
175
198
|
//
|
|
176
199
|
// On an `http` pair that line is the JSON wire assignment, not the declared
|
|
@@ -183,6 +206,8 @@ export function classifyPair(input) {
|
|
|
183
206
|
const decisiveLine = decisiveAssignmentLine(plan);
|
|
184
207
|
const assignDiag = probeDiags.find((d) => d.line === decisiveLine && ASSIGNMENT_CODES.has(d.code));
|
|
185
208
|
if (assignDiag) {
|
|
209
|
+
if (bytesGate)
|
|
210
|
+
return bytesVerdict();
|
|
186
211
|
const text = scrubDiagnostic(assignDiag.message, scrubCtx, plan.sentEndpoint.alias, plan.expectedEndpoint.alias);
|
|
187
212
|
return {
|
|
188
213
|
...base,
|
|
@@ -218,6 +243,8 @@ export function classifyPair(input) {
|
|
|
218
243
|
if (wireOther) {
|
|
219
244
|
const declaredMismatch = probeDiags.find((d) => d.line === plan.assignmentLine && ASSIGNMENT_CODES.has(d.code));
|
|
220
245
|
if (declaredMismatch) {
|
|
246
|
+
if (bytesGate)
|
|
247
|
+
return bytesVerdict();
|
|
221
248
|
return {
|
|
222
249
|
...base,
|
|
223
250
|
bucket: 'incompatible',
|