carrick 0.3.53
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/LICENSE.md +99 -0
- package/README.md +79 -0
- package/bin/carrick.mjs +168 -0
- package/dist/channel.d.ts +16 -0
- package/dist/channel.js +37 -0
- package/dist/channel.js.map +1 -0
- package/dist/cli.d.ts +29 -0
- package/dist/cli.js +60 -0
- package/dist/cli.js.map +1 -0
- package/dist/contract.d.ts +154 -0
- package/dist/contract.js +141 -0
- package/dist/contract.js.map +1 -0
- package/dist/diagnostics.d.ts +54 -0
- package/dist/diagnostics.js +199 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/hook/post-edit.d.ts +13 -0
- package/dist/hook/post-edit.js +76 -0
- package/dist/hook/post-edit.js.map +1 -0
- package/dist/hook/session-start.d.ts +2 -0
- package/dist/hook/session-start.js +44 -0
- package/dist/hook/session-start.js.map +1 -0
- package/dist/init/identity.d.ts +20 -0
- package/dist/init/identity.js +60 -0
- package/dist/init/identity.js.map +1 -0
- package/dist/init/repos.d.ts +19 -0
- package/dist/init/repos.js +46 -0
- package/dist/init/repos.js.map +1 -0
- package/dist/init/run.d.ts +11 -0
- package/dist/init/run.js +228 -0
- package/dist/init/run.js.map +1 -0
- package/dist/init/settings.d.ts +50 -0
- package/dist/init/settings.js +138 -0
- package/dist/init/settings.js.map +1 -0
- package/dist/log.d.ts +2 -0
- package/dist/log.js +35 -0
- package/dist/log.js.map +1 -0
- package/dist/native.d.ts +48 -0
- package/dist/native.js +127 -0
- package/dist/native.js.map +1 -0
- package/dist/render.d.ts +65 -0
- package/dist/render.js +334 -0
- package/dist/render.js.map +1 -0
- package/dist/root.d.ts +30 -0
- package/dist/root.js +82 -0
- package/dist/root.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +255 -0
- package/dist/server.js.map +1 -0
- package/dist/templates.d.ts +18 -0
- package/dist/templates.js +66 -0
- package/dist/templates.js.map +1 -0
- package/package.json +72 -0
- package/plugin/.claude-plugin/plugin.json +6 -0
- package/plugin/.lsp.json +14 -0
- package/plugin/hooks/hooks.json +27 -0
- package/sidecar/dist/src/bundler.d.ts +141 -0
- package/sidecar/dist/src/bundler.js +680 -0
- package/sidecar/dist/src/capture/anchors.d.ts +61 -0
- package/sidecar/dist/src/capture/anchors.js +1132 -0
- package/sidecar/dist/src/capture/api.d.ts +378 -0
- package/sidecar/dist/src/capture/api.js +10 -0
- package/sidecar/dist/src/capture/augmentations.d.ts +20 -0
- package/sidecar/dist/src/capture/augmentations.js +60 -0
- package/sidecar/dist/src/capture/check-classify.d.ts +58 -0
- package/sidecar/dist/src/capture/check-classify.js +189 -0
- package/sidecar/dist/src/capture/check-deep.d.ts +32 -0
- package/sidecar/dist/src/capture/check-deep.js +91 -0
- package/sidecar/dist/src/capture/check-poison.d.ts +40 -0
- package/sidecar/dist/src/capture/check-poison.js +155 -0
- package/sidecar/dist/src/capture/check-probe.d.ts +70 -0
- package/sidecar/dist/src/capture/check-probe.js +147 -0
- package/sidecar/dist/src/capture/check-scrub.d.ts +31 -0
- package/sidecar/dist/src/capture/check-scrub.js +71 -0
- package/sidecar/dist/src/capture/check-workspace.d.ts +52 -0
- package/sidecar/dist/src/capture/check-workspace.js +218 -0
- package/sidecar/dist/src/capture/check.d.ts +39 -0
- package/sidecar/dist/src/capture/check.js +450 -0
- package/sidecar/dist/src/capture/deep-walk.d.ts +62 -0
- package/sidecar/dist/src/capture/deep-walk.js +243 -0
- package/sidecar/dist/src/capture/index.d.ts +36 -0
- package/sidecar/dist/src/capture/index.js +477 -0
- package/sidecar/dist/src/capture/lockfile.d.ts +48 -0
- package/sidecar/dist/src/capture/lockfile.js +490 -0
- package/sidecar/dist/src/capture/machinery.d.ts +59 -0
- package/sidecar/dist/src/capture/machinery.js +160 -0
- package/sidecar/dist/src/capture/node-builder.d.ts +37 -0
- package/sidecar/dist/src/capture/node-builder.js +123 -0
- package/sidecar/dist/src/capture/paths-rewrite.d.ts +34 -0
- package/sidecar/dist/src/capture/paths-rewrite.js +100 -0
- package/sidecar/dist/src/capture/self-check.d.ts +38 -0
- package/sidecar/dist/src/capture/self-check.js +317 -0
- package/sidecar/dist/src/capture/specifiers.d.ts +39 -0
- package/sidecar/dist/src/capture/specifiers.js +56 -0
- package/sidecar/dist/src/definition-resolver.d.ts +57 -0
- package/sidecar/dist/src/definition-resolver.js +153 -0
- package/sidecar/dist/src/index.d.ts +14 -0
- package/sidecar/dist/src/index.js +564 -0
- package/sidecar/dist/src/monorepo-builder.d.ts +129 -0
- package/sidecar/dist/src/monorepo-builder.js +584 -0
- package/sidecar/dist/src/project-loader.d.ts +130 -0
- package/sidecar/dist/src/project-loader.js +399 -0
- package/sidecar/dist/src/type-inferrer.d.ts +940 -0
- package/sidecar/dist/src/type-inferrer.js +3540 -0
- package/sidecar/dist/src/type-structural-expander.d.ts +61 -0
- package/sidecar/dist/src/type-structural-expander.js +283 -0
- package/sidecar/dist/src/type-text-canonicalizer.d.ts +40 -0
- package/sidecar/dist/src/type-text-canonicalizer.js +297 -0
- package/sidecar/dist/src/types.d.ts +669 -0
- package/sidecar/dist/src/types.js +5 -0
- package/sidecar/dist/src/validators.d.ts +2214 -0
- package/sidecar/dist/src/validators.js +336 -0
- package/sidecar/package.json +6 -0
- package/templates/carrick.json +7 -0
- package/templates/carrick.yml +45 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface Emitter - Emits .d.ts surface files with rewritten module specifiers
|
|
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
|
|
13
|
+
*/
|
|
14
|
+
import { Node, } from 'ts-morph';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import { expandTypeStructural } from './type-structural-expander.js';
|
|
18
|
+
/**
|
|
19
|
+
* #248: upper bound on `SymbolRequest.array_depth`. SDL list nesting is
|
|
20
|
+
* realistically 1-3 levels (`[Order!]!`, at most `[[Order!]!]!`); this is a
|
|
21
|
+
* generous ceiling, not a realistic value. `array_depth` wraps via string
|
|
22
|
+
* repetition (`'[]'.repeat(depth)`), so an unbounded value from the model
|
|
23
|
+
* would let a single symbol blow up the emitted `.d.ts` (and the memory/CPU
|
|
24
|
+
* spent producing it). Depths above this are treated as unresolvable, same
|
|
25
|
+
* as any other symbol the bundler can't extract a type for.
|
|
26
|
+
*/
|
|
27
|
+
const MAX_ARRAY_DEPTH = 10;
|
|
28
|
+
/**
|
|
29
|
+
* SurfaceEmitter - Generates .d.ts surface files with rewritten imports
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* const emitter = new SurfaceEmitter({ project, repoRoot });
|
|
33
|
+
* const result = emitter.emit(repoName, payloads, outputPath);
|
|
34
|
+
*/
|
|
35
|
+
export class SurfaceEmitter {
|
|
36
|
+
project;
|
|
37
|
+
repoRoot;
|
|
38
|
+
constructor(options) {
|
|
39
|
+
this.project = options.project;
|
|
40
|
+
this.repoRoot = options.repoRoot;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Emit a surface .d.ts file containing payload type declarations
|
|
44
|
+
*
|
|
45
|
+
* @param repoName - Repository name for specifier rewriting
|
|
46
|
+
* @param payloads - Payload definitions to include
|
|
47
|
+
* @param outputPath - Path to write the surface file
|
|
48
|
+
* @returns SurfaceEmitResult
|
|
49
|
+
*/
|
|
50
|
+
emit(repoName, payloads, outputPath) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
const manifest = [];
|
|
53
|
+
try {
|
|
54
|
+
// Phase 1: Generate surface content as a string
|
|
55
|
+
const surfaceLines = [
|
|
56
|
+
'// Generated surface file for type compatibility checking',
|
|
57
|
+
'// Do not edit - this file is auto-generated by Carrick',
|
|
58
|
+
'',
|
|
59
|
+
];
|
|
60
|
+
// Collect all import specifiers we need to track for rewriting
|
|
61
|
+
const importSpecifiers = new Set();
|
|
62
|
+
// Phase 2: Add payload type declarations
|
|
63
|
+
for (const payload of payloads) {
|
|
64
|
+
const { alias, type_string, source_file } = payload;
|
|
65
|
+
// Analyze type string for import references
|
|
66
|
+
const detectedImports = this.extractImportReferences(type_string);
|
|
67
|
+
for (const imp of detectedImports) {
|
|
68
|
+
importSpecifiers.add(imp);
|
|
69
|
+
}
|
|
70
|
+
// Add type alias declaration
|
|
71
|
+
surfaceLines.push(`export type ${alias} = ${type_string};`);
|
|
72
|
+
surfaceLines.push('');
|
|
73
|
+
manifest.push({
|
|
74
|
+
alias,
|
|
75
|
+
type_string,
|
|
76
|
+
rewritten_imports: [],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Phase 3: Create a temporary source file for AST manipulation
|
|
80
|
+
const tempFileName = `__carrick_surface_${repoName}_${Date.now()}.d.ts`;
|
|
81
|
+
const tempFilePath = path.join(this.repoRoot, tempFileName);
|
|
82
|
+
const initialContent = surfaceLines.join('\n');
|
|
83
|
+
const tempSourceFile = this.project.createSourceFile(tempFilePath, initialContent, { overwrite: true });
|
|
84
|
+
// Phase 4: Rewrite module specifiers in the AST
|
|
85
|
+
const rewrittenImports = this.rewriteModuleSpecifiers(tempSourceFile, repoName);
|
|
86
|
+
// Update manifest with rewritten imports
|
|
87
|
+
for (const entry of manifest) {
|
|
88
|
+
entry.rewritten_imports = rewrittenImports;
|
|
89
|
+
}
|
|
90
|
+
// Phase 5: Get the transformed content
|
|
91
|
+
let surfaceContent = tempSourceFile.getFullText();
|
|
92
|
+
// Phase 6: Safety pass - catch any raw specifiers the compiler might have added
|
|
93
|
+
surfaceContent = this.safetyRewritePass(surfaceContent, repoName);
|
|
94
|
+
// Phase 7: Clean up temporary source file from project
|
|
95
|
+
this.project.removeSourceFile(tempSourceFile);
|
|
96
|
+
// Phase 8: Write output file
|
|
97
|
+
const absoluteOutputPath = path.isAbsolute(outputPath)
|
|
98
|
+
? outputPath
|
|
99
|
+
: path.join(this.repoRoot, outputPath);
|
|
100
|
+
// Ensure directory exists
|
|
101
|
+
const outputDir = path.dirname(absoluteOutputPath);
|
|
102
|
+
if (!fs.existsSync(outputDir)) {
|
|
103
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
fs.writeFileSync(absoluteOutputPath, surfaceContent, 'utf-8');
|
|
106
|
+
this.log(`Emitted surface file: ${absoluteOutputPath}`);
|
|
107
|
+
return {
|
|
108
|
+
success: true,
|
|
109
|
+
surface_content: surfaceContent,
|
|
110
|
+
output_path: absoluteOutputPath,
|
|
111
|
+
manifest,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
116
|
+
this.logError(`Surface emission failed: ${error}`);
|
|
117
|
+
errors.push(error);
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
errors,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Extract potential import references from a type string.
|
|
126
|
+
* These are module specifiers that might need rewriting.
|
|
127
|
+
*/
|
|
128
|
+
extractImportReferences(typeString) {
|
|
129
|
+
const imports = [];
|
|
130
|
+
// Match import("...") syntax
|
|
131
|
+
const importTypeRegex = /import\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
132
|
+
let match;
|
|
133
|
+
while ((match = importTypeRegex.exec(typeString)) !== null) {
|
|
134
|
+
imports.push(match[1]);
|
|
135
|
+
}
|
|
136
|
+
// Match from "..." syntax (in case of inline imports)
|
|
137
|
+
const fromRegex = /from\s+["']([^"']+)["']/g;
|
|
138
|
+
while ((match = fromRegex.exec(typeString)) !== null) {
|
|
139
|
+
imports.push(match[1]);
|
|
140
|
+
}
|
|
141
|
+
return imports;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Rewrite all module specifiers in a source file to use repo-scoped aliases.
|
|
145
|
+
*
|
|
146
|
+
* Targets:
|
|
147
|
+
* 1. ImportDeclaration.moduleSpecifier
|
|
148
|
+
* 2. ImportTypeNode argument (import("..."))
|
|
149
|
+
* 3. ExportDeclaration.moduleSpecifier
|
|
150
|
+
*
|
|
151
|
+
* @returns Array of rewritten specifiers
|
|
152
|
+
*/
|
|
153
|
+
rewriteModuleSpecifiers(sourceFile, repoName) {
|
|
154
|
+
const rewrittenSpecifiers = [];
|
|
155
|
+
// 1. Rewrite ImportDeclaration specifiers
|
|
156
|
+
const importDecls = sourceFile.getImportDeclarations();
|
|
157
|
+
for (const importDecl of importDecls) {
|
|
158
|
+
const specifier = importDecl.getModuleSpecifierValue();
|
|
159
|
+
if (this.shouldRewriteSpecifier(specifier)) {
|
|
160
|
+
const newSpecifier = this.rewriteSpecifier(specifier, repoName);
|
|
161
|
+
importDecl.setModuleSpecifier(newSpecifier);
|
|
162
|
+
rewrittenSpecifiers.push(`${specifier} → ${newSpecifier}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// 2. Rewrite ExportDeclaration specifiers
|
|
166
|
+
const exportDecls = sourceFile.getExportDeclarations();
|
|
167
|
+
for (const exportDecl of exportDecls) {
|
|
168
|
+
const specifier = exportDecl.getModuleSpecifierValue();
|
|
169
|
+
if (specifier && this.shouldRewriteSpecifier(specifier)) {
|
|
170
|
+
const newSpecifier = this.rewriteSpecifier(specifier, repoName);
|
|
171
|
+
exportDecl.setModuleSpecifier(newSpecifier);
|
|
172
|
+
rewrittenSpecifiers.push(`${specifier} → ${newSpecifier}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// 3. Rewrite ImportTypeNode arguments
|
|
176
|
+
// These appear in type positions: import("module").Type
|
|
177
|
+
sourceFile.forEachDescendant((node) => {
|
|
178
|
+
if (Node.isImportTypeNode(node)) {
|
|
179
|
+
const arg = node.getArgument();
|
|
180
|
+
if (Node.isLiteralTypeNode(arg)) {
|
|
181
|
+
const literal = arg.getLiteral();
|
|
182
|
+
if (Node.isStringLiteral(literal)) {
|
|
183
|
+
const specifier = literal.getLiteralValue();
|
|
184
|
+
if (this.shouldRewriteSpecifier(specifier)) {
|
|
185
|
+
const newSpecifier = this.rewriteSpecifier(specifier, repoName);
|
|
186
|
+
literal.setLiteralValue(newSpecifier);
|
|
187
|
+
rewrittenSpecifiers.push(`${specifier} → ${newSpecifier}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
return rewrittenSpecifiers;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Determine if a module specifier should be rewritten.
|
|
197
|
+
* Only non-relative specifiers should be rewritten.
|
|
198
|
+
*/
|
|
199
|
+
shouldRewriteSpecifier(specifier) {
|
|
200
|
+
// Don't rewrite relative imports
|
|
201
|
+
if (specifier.startsWith('./') || specifier.startsWith('../')) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
// Don't rewrite already-rewritten specifiers
|
|
205
|
+
if (specifier.startsWith('@carrick/')) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
// Don't rewrite node built-ins
|
|
209
|
+
if (specifier.startsWith('node:')) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
// Some common node built-ins without prefix
|
|
213
|
+
const nodeBuiltins = [
|
|
214
|
+
'fs', 'path', 'os', 'util', 'events', 'stream', 'http', 'https',
|
|
215
|
+
'url', 'querystring', 'crypto', 'buffer', 'child_process', 'cluster',
|
|
216
|
+
'dgram', 'dns', 'net', 'readline', 'tls', 'tty', 'zlib', 'assert',
|
|
217
|
+
'async_hooks', 'console', 'constants', 'domain', 'inspector', 'module',
|
|
218
|
+
'perf_hooks', 'process', 'punycode', 'string_decoder', 'timers',
|
|
219
|
+
'trace_events', 'v8', 'vm', 'worker_threads'
|
|
220
|
+
];
|
|
221
|
+
if (nodeBuiltins.includes(specifier) || nodeBuiltins.includes(specifier.split('/')[0])) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Rewrite a module specifier to the repo-scoped format.
|
|
228
|
+
*
|
|
229
|
+
* Format: @carrick/{repoName}/{originalSpecifier}
|
|
230
|
+
*
|
|
231
|
+
* Preserves:
|
|
232
|
+
* - Scoped packages: "@scope/pkg" → "@carrick/repo-a/@scope/pkg"
|
|
233
|
+
* - Subpaths: "zod/lib/helpers" → "@carrick/repo-a/zod/lib/helpers"
|
|
234
|
+
* - Combined: "@scope/pkg/subpath" → "@carrick/repo-a/@scope/pkg/subpath"
|
|
235
|
+
*/
|
|
236
|
+
rewriteSpecifier(specifier, repoName) {
|
|
237
|
+
return `@carrick/${repoName}/${specifier}`;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Safety pass: Scan emitted content for any raw specifiers that might have
|
|
241
|
+
* been introduced by the TypeScript compiler and rewrite them.
|
|
242
|
+
*
|
|
243
|
+
* This catches edge cases where the compiler emits types with their original
|
|
244
|
+
* module specifiers that weren't caught by AST traversal.
|
|
245
|
+
*/
|
|
246
|
+
safetyRewritePass(content, repoName) {
|
|
247
|
+
// Common patterns to catch:
|
|
248
|
+
// - from "package"
|
|
249
|
+
// - import("package")
|
|
250
|
+
// - export * from "package"
|
|
251
|
+
// We need to be careful not to double-rewrite @carrick/ specifiers
|
|
252
|
+
// Pattern for import("...") that's not already @carrick
|
|
253
|
+
const importTypePattern = /import\s*\(\s*["'](?!@carrick\/|\.\.?\/|node:)([^"']+)["']\s*\)/g;
|
|
254
|
+
content = content.replace(importTypePattern, (match, specifier) => {
|
|
255
|
+
const newSpecifier = this.rewriteSpecifier(specifier, repoName);
|
|
256
|
+
return match.replace(`"${specifier}"`, `"${newSpecifier}"`).replace(`'${specifier}'`, `'${newSpecifier}'`);
|
|
257
|
+
});
|
|
258
|
+
// Pattern for from "..." that's not already @carrick
|
|
259
|
+
const fromPattern = /from\s+["'](?!@carrick\/|\.\.?\/|node:)([^"']+)["']/g;
|
|
260
|
+
content = content.replace(fromPattern, (match, specifier) => {
|
|
261
|
+
const newSpecifier = this.rewriteSpecifier(specifier, repoName);
|
|
262
|
+
return match.replace(`"${specifier}"`, `"${newSpecifier}"`).replace(`'${specifier}'`, `'${newSpecifier}'`);
|
|
263
|
+
});
|
|
264
|
+
return content;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Log a message to stderr
|
|
268
|
+
*/
|
|
269
|
+
log(message) {
|
|
270
|
+
console.error(`[sidecar:surface-emitter] ${message}`);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Log an error to stderr
|
|
274
|
+
*/
|
|
275
|
+
logError(message) {
|
|
276
|
+
console.error(`[sidecar:surface-emitter:error] ${message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// =============================================================================
|
|
280
|
+
// Legacy TypeBundler (kept for backwards compatibility during migration)
|
|
281
|
+
// =============================================================================
|
|
282
|
+
/**
|
|
283
|
+
* TypeBundler - Legacy bundler using simpler approach
|
|
284
|
+
*
|
|
285
|
+
* This is a simplified replacement that doesn't use dts-bundle-generator.
|
|
286
|
+
* It extracts type definitions directly using ts-morph.
|
|
287
|
+
*
|
|
288
|
+
* @deprecated Use SurfaceEmitter instead for the new architecture
|
|
289
|
+
*/
|
|
290
|
+
export class TypeBundler {
|
|
291
|
+
project;
|
|
292
|
+
repoRoot;
|
|
293
|
+
constructor(options) {
|
|
294
|
+
this.project = options.project;
|
|
295
|
+
this.repoRoot = options.repoRoot;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Bundle the requested symbols into type definitions
|
|
299
|
+
*
|
|
300
|
+
* @param symbols - Array of symbol requests to bundle
|
|
301
|
+
* @returns BundleResult with the bundled content or errors
|
|
302
|
+
*/
|
|
303
|
+
bundle(symbols) {
|
|
304
|
+
const manifest = [];
|
|
305
|
+
const symbolFailures = [];
|
|
306
|
+
const errors = [];
|
|
307
|
+
// Dedupe symbols
|
|
308
|
+
const uniqueSymbols = this.dedupeSymbols(symbols);
|
|
309
|
+
// Validate all symbols exist
|
|
310
|
+
const validatedSymbols = this.validateSymbols(uniqueSymbols, symbolFailures);
|
|
311
|
+
if (validatedSymbols.length === 0) {
|
|
312
|
+
return {
|
|
313
|
+
success: false,
|
|
314
|
+
symbol_failures: symbolFailures,
|
|
315
|
+
errors: symbolFailures.length > 0
|
|
316
|
+
? ['All requested symbols failed to resolve']
|
|
317
|
+
: ['No symbols provided'],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
// Generate type definitions directly
|
|
322
|
+
const dtsLines = [
|
|
323
|
+
'// Generated by Carrick TypeBundler',
|
|
324
|
+
'// This file contains extracted type definitions',
|
|
325
|
+
'',
|
|
326
|
+
];
|
|
327
|
+
for (const symbol of validatedSymbols) {
|
|
328
|
+
const typeInfo = this.extractTypeDefinition(symbol);
|
|
329
|
+
if (typeInfo) {
|
|
330
|
+
dtsLines.push(typeInfo.definition);
|
|
331
|
+
dtsLines.push('');
|
|
332
|
+
manifest.push({
|
|
333
|
+
alias: symbol.alias || symbol.symbol_name,
|
|
334
|
+
original_name: symbol.symbol_name,
|
|
335
|
+
source_file: symbol.source_file,
|
|
336
|
+
type_string: typeInfo.typeString,
|
|
337
|
+
is_explicit: true,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
symbolFailures.push({
|
|
342
|
+
symbol_name: symbol.symbol_name,
|
|
343
|
+
source_file: symbol.source_file,
|
|
344
|
+
reason: `Could not extract type definition for '${symbol.symbol_name}'`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const dtsContent = dtsLines.join('\n');
|
|
349
|
+
return {
|
|
350
|
+
success: true,
|
|
351
|
+
dts_content: dtsContent,
|
|
352
|
+
manifest,
|
|
353
|
+
symbol_failures: symbolFailures.length > 0 ? symbolFailures : undefined,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
358
|
+
this.logError(`Bundle generation failed: ${error}`);
|
|
359
|
+
errors.push(error);
|
|
360
|
+
return {
|
|
361
|
+
success: false,
|
|
362
|
+
symbol_failures: symbolFailures.length > 0 ? symbolFailures : undefined,
|
|
363
|
+
errors,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
dedupeSymbols(symbols) {
|
|
368
|
+
const seen = new Set();
|
|
369
|
+
const unique = [];
|
|
370
|
+
for (const symbol of symbols) {
|
|
371
|
+
const key = `${symbol.source_file}::${symbol.symbol_name}::${symbol.alias || ''}`;
|
|
372
|
+
if (seen.has(key)) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
seen.add(key);
|
|
376
|
+
unique.push(symbol);
|
|
377
|
+
}
|
|
378
|
+
return unique;
|
|
379
|
+
}
|
|
380
|
+
validateSymbols(symbols, failures) {
|
|
381
|
+
const validated = [];
|
|
382
|
+
for (const symbol of symbols) {
|
|
383
|
+
const result = this.validateSymbol(symbol);
|
|
384
|
+
if (result.valid) {
|
|
385
|
+
validated.push(symbol);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
failures.push({
|
|
389
|
+
symbol_name: symbol.symbol_name,
|
|
390
|
+
source_file: symbol.source_file,
|
|
391
|
+
reason: result.reason || 'Unknown error',
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return validated;
|
|
396
|
+
}
|
|
397
|
+
validateSymbol(symbol) {
|
|
398
|
+
if (this.isPlaceholderSource(symbol.source_file)) {
|
|
399
|
+
return {
|
|
400
|
+
valid: false,
|
|
401
|
+
reason: `Unsupported source placeholder: ${symbol.source_file}`,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (!this.isPathSource(symbol.source_file)) {
|
|
405
|
+
return { valid: true };
|
|
406
|
+
}
|
|
407
|
+
// Resolve the source file path
|
|
408
|
+
const absolutePath = path.isAbsolute(symbol.source_file)
|
|
409
|
+
? symbol.source_file
|
|
410
|
+
: path.resolve(this.repoRoot, symbol.source_file);
|
|
411
|
+
// Check if file exists
|
|
412
|
+
if (!fs.existsSync(absolutePath)) {
|
|
413
|
+
return {
|
|
414
|
+
valid: false,
|
|
415
|
+
reason: `Source file not found: ${symbol.source_file}`,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
// Get or add the source file to the project
|
|
419
|
+
let sourceFile;
|
|
420
|
+
try {
|
|
421
|
+
sourceFile =
|
|
422
|
+
this.project.getSourceFile(absolutePath) ||
|
|
423
|
+
this.project.addSourceFileAtPath(absolutePath);
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
return {
|
|
427
|
+
valid: false,
|
|
428
|
+
reason: `Failed to load source file: ${err instanceof Error ? err.message : String(err)}`,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
// Look for the symbol in the file
|
|
432
|
+
const symbolName = symbol.symbol_name;
|
|
433
|
+
// Check interfaces
|
|
434
|
+
if (sourceFile.getInterface(symbolName))
|
|
435
|
+
return { valid: true };
|
|
436
|
+
// Check type aliases
|
|
437
|
+
if (sourceFile.getTypeAlias(symbolName))
|
|
438
|
+
return { valid: true };
|
|
439
|
+
// Check classes
|
|
440
|
+
if (sourceFile.getClass(symbolName))
|
|
441
|
+
return { valid: true };
|
|
442
|
+
// Check enums
|
|
443
|
+
if (sourceFile.getEnum(symbolName))
|
|
444
|
+
return { valid: true };
|
|
445
|
+
// Check exported variables
|
|
446
|
+
if (sourceFile.getVariableDeclaration(symbolName))
|
|
447
|
+
return { valid: true };
|
|
448
|
+
// Check functions
|
|
449
|
+
if (sourceFile.getFunction(symbolName))
|
|
450
|
+
return { valid: true };
|
|
451
|
+
// Symbol not found
|
|
452
|
+
return {
|
|
453
|
+
valid: false,
|
|
454
|
+
reason: `Symbol '${symbolName}' not found in ${symbol.source_file}`,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Structural body (`{ ... }`) for an interface/object type, with every nested
|
|
459
|
+
* named member inlined recursively. Returns null when the expansion is not a
|
|
460
|
+
* usable object body (e.g. the expander fell back to a bare name), so the
|
|
461
|
+
* caller can keep the verbatim declaration instead of emitting an invalid
|
|
462
|
+
* interface. The expander already guards recursion (depth + per-branch cycle
|
|
463
|
+
* set) and keeps builtins/`node_modules` types by name.
|
|
464
|
+
*/
|
|
465
|
+
expandObjectBody(type) {
|
|
466
|
+
try {
|
|
467
|
+
const expanded = expandTypeStructural(type);
|
|
468
|
+
return expanded.startsWith('{') ? expanded : null;
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
extractTypeDefinition(symbol) {
|
|
475
|
+
const base = this.extractTypeDefinitionBase(symbol);
|
|
476
|
+
if (!base)
|
|
477
|
+
return null;
|
|
478
|
+
// #248: wrap the resolved element type in the SDL list depth. A GraphQL
|
|
479
|
+
// producer field `[Order!]!` backed by `interface Order` bundles `Order`
|
|
480
|
+
// with `array_depth: 1`; the alias becomes `Order[]` (arrays can't be
|
|
481
|
+
// interfaces, so it is always emitted as a type alias). The element's
|
|
482
|
+
// structurally-expanded body carries the shape, so downstream resolution
|
|
483
|
+
// and the eval's whitespace-collapsed `{...}[]` comparison both hold.
|
|
484
|
+
const depth = symbol.array_depth ?? 0;
|
|
485
|
+
if (depth <= 0)
|
|
486
|
+
return base;
|
|
487
|
+
// Reject rather than clamp: silently clamping would emit a type at a
|
|
488
|
+
// depth the model never asked for, which is wrong in a different way.
|
|
489
|
+
// Falling through to the symbol's normal "could not extract" failure
|
|
490
|
+
// keeps this on the existing per-symbol failure path (`symbol_failures`)
|
|
491
|
+
// instead of rejecting the whole batch.
|
|
492
|
+
if (depth > MAX_ARRAY_DEPTH)
|
|
493
|
+
return null;
|
|
494
|
+
// The wrap suffixes `typeString` with `[]`, which is only valid when it is
|
|
495
|
+
// a standalone type EXPRESSION. The base's fallback branches (interface
|
|
496
|
+
// whose expansion failed, class/enum/function) return declaration text,
|
|
497
|
+
// and `export type A = enum X {...}[]` is not parseable .d.ts — fail the
|
|
498
|
+
// symbol instead, same per-symbol path as an over-depth request.
|
|
499
|
+
if (!base.typeStringIsExpression)
|
|
500
|
+
return null;
|
|
501
|
+
const alias = symbol.alias || symbol.symbol_name;
|
|
502
|
+
// Parenthesise a union/intersection/function element so `(A | B)[]` doesn't
|
|
503
|
+
// misparse as `A | B[]` and `(() => T)[]` as `() => T[]`. Decide from the
|
|
504
|
+
// TYPE (not the string): a single object literal `{ a: A | B }` is NOT a
|
|
505
|
+
// top-level union and must stay unparenthesised (`{ a: A | B }[]`).
|
|
506
|
+
const element = this.elementNeedsArrayParens(symbol)
|
|
507
|
+
? `(${base.typeString})`
|
|
508
|
+
: base.typeString;
|
|
509
|
+
const typeString = `${element}${'[]'.repeat(depth)}`;
|
|
510
|
+
return { definition: `export type ${alias} = ${typeString};`, typeString };
|
|
511
|
+
}
|
|
512
|
+
/// Whether the element type must be parenthesised before an `[]` suffix.
|
|
513
|
+
/// Interfaces/classes/enums are always object/nominal (never a top-level
|
|
514
|
+
/// union), so only type aliases and typed variables can carry a bare
|
|
515
|
+
/// union/intersection/function that would misparse; those are re-resolved
|
|
516
|
+
/// from the source Type here (this runs only on the rare array_depth path).
|
|
517
|
+
elementNeedsArrayParens(symbol) {
|
|
518
|
+
const absolutePath = path.isAbsolute(symbol.source_file)
|
|
519
|
+
? symbol.source_file
|
|
520
|
+
: path.resolve(this.repoRoot, symbol.source_file);
|
|
521
|
+
const sourceFile = this.project.getSourceFile(absolutePath);
|
|
522
|
+
if (!sourceFile)
|
|
523
|
+
return false;
|
|
524
|
+
const name = symbol.symbol_name;
|
|
525
|
+
const decl = sourceFile.getTypeAlias(name) ?? sourceFile.getVariableDeclaration(name);
|
|
526
|
+
if (!decl)
|
|
527
|
+
return false;
|
|
528
|
+
const t = decl.getType();
|
|
529
|
+
return (t.isUnion() ||
|
|
530
|
+
t.isIntersection() ||
|
|
531
|
+
t.getCallSignatures().length > 0 ||
|
|
532
|
+
t.getConstructSignatures().length > 0);
|
|
533
|
+
}
|
|
534
|
+
// `typeStringIsExpression`: whether `typeString` is a standalone type
|
|
535
|
+
// expression (an object body, alias RHS, or `Type#getText()`), as opposed to
|
|
536
|
+
// verbatim declaration text from a fallback branch. Only expressions can be
|
|
537
|
+
// array-wrapped by `extractTypeDefinition`.
|
|
538
|
+
extractTypeDefinitionBase(symbol) {
|
|
539
|
+
const absolutePath = path.isAbsolute(symbol.source_file)
|
|
540
|
+
? symbol.source_file
|
|
541
|
+
: path.resolve(this.repoRoot, symbol.source_file);
|
|
542
|
+
const sourceFile = this.project.getSourceFile(absolutePath);
|
|
543
|
+
if (!sourceFile) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
const symbolName = symbol.symbol_name;
|
|
547
|
+
const alias = symbol.alias || symbolName;
|
|
548
|
+
// Try interface
|
|
549
|
+
const iface = sourceFile.getInterface(symbolName);
|
|
550
|
+
if (iface) {
|
|
551
|
+
// Inline nested named members structurally (`total: { amountCents: number;
|
|
552
|
+
// currency: string; }`, not `total: MoneyView`). The bundle carries only
|
|
553
|
+
// the alias declaration — never the source decls of nested types — so a
|
|
554
|
+
// bare named reference is dangling and resolves to `any` downstream. The
|
|
555
|
+
// expansion happens here, in the real source project, where the nested
|
|
556
|
+
// symbol still resolves. Falls back to the verbatim rename if the type
|
|
557
|
+
// can't be safely rendered as an object body.
|
|
558
|
+
const expanded = this.expandObjectBody(iface.getType());
|
|
559
|
+
const text = iface.getText();
|
|
560
|
+
if (expanded) {
|
|
561
|
+
const definition = `export interface ${alias} ${expanded}`;
|
|
562
|
+
return { definition, typeString: expanded, typeStringIsExpression: true };
|
|
563
|
+
}
|
|
564
|
+
const typeString = text;
|
|
565
|
+
const alreadyExported = text.trimStart().startsWith('export');
|
|
566
|
+
const definition = alias !== symbolName
|
|
567
|
+
? (alreadyExported
|
|
568
|
+
? text.replace(`interface ${symbolName}`, `interface ${alias}`)
|
|
569
|
+
: text.replace(`interface ${symbolName}`, `export interface ${alias}`))
|
|
570
|
+
: alreadyExported
|
|
571
|
+
? text
|
|
572
|
+
: `export ${text}`;
|
|
573
|
+
return { definition, typeString, typeStringIsExpression: false };
|
|
574
|
+
}
|
|
575
|
+
// Try type alias
|
|
576
|
+
const typeAlias = sourceFile.getTypeAlias(symbolName);
|
|
577
|
+
if (typeAlias) {
|
|
578
|
+
// Same nested-member inlining as the interface branch above. A type alias
|
|
579
|
+
// can resolve to any shape (object, union, array, primitive); the
|
|
580
|
+
// structural expander renders each correctly and keeps builtins/library
|
|
581
|
+
// types by name, so the alias RHS is always emittable.
|
|
582
|
+
const text = typeAlias.getText();
|
|
583
|
+
const expanded = expandTypeStructural(typeAlias.getType());
|
|
584
|
+
if (expanded && expanded !== 'unknown') {
|
|
585
|
+
const definition = `export type ${alias} = ${expanded};`;
|
|
586
|
+
return { definition, typeString: expanded, typeStringIsExpression: true };
|
|
587
|
+
}
|
|
588
|
+
const typeString = typeAlias.getType().getText();
|
|
589
|
+
const alreadyExported = text.trimStart().startsWith('export');
|
|
590
|
+
const definition = alias !== symbolName
|
|
591
|
+
? (alreadyExported
|
|
592
|
+
? text.replace(`type ${symbolName}`, `type ${alias}`)
|
|
593
|
+
: text.replace(`type ${symbolName}`, `export type ${alias}`))
|
|
594
|
+
: alreadyExported
|
|
595
|
+
? text
|
|
596
|
+
: `export ${text}`;
|
|
597
|
+
return { definition, typeString, typeStringIsExpression: true };
|
|
598
|
+
}
|
|
599
|
+
// Try class
|
|
600
|
+
const classDecl = sourceFile.getClass(symbolName);
|
|
601
|
+
if (classDecl) {
|
|
602
|
+
const text = classDecl.getText();
|
|
603
|
+
const typeString = text;
|
|
604
|
+
const alreadyExported = text.trimStart().startsWith('export');
|
|
605
|
+
const definition = alias !== symbolName
|
|
606
|
+
? (alreadyExported
|
|
607
|
+
? text.replace(`class ${symbolName}`, `declare class ${alias}`)
|
|
608
|
+
: text.replace(`class ${symbolName}`, `export declare class ${alias}`))
|
|
609
|
+
: alreadyExported
|
|
610
|
+
? text
|
|
611
|
+
: `export declare ${text}`;
|
|
612
|
+
return { definition, typeString, typeStringIsExpression: false };
|
|
613
|
+
}
|
|
614
|
+
// Try enum
|
|
615
|
+
const enumDecl = sourceFile.getEnum(symbolName);
|
|
616
|
+
if (enumDecl) {
|
|
617
|
+
const text = enumDecl.getText();
|
|
618
|
+
const typeString = text;
|
|
619
|
+
const alreadyExported = text.trimStart().startsWith('export');
|
|
620
|
+
const definition = alias !== symbolName
|
|
621
|
+
? (alreadyExported
|
|
622
|
+
? text.replace(`enum ${symbolName}`, `enum ${alias}`)
|
|
623
|
+
: text.replace(`enum ${symbolName}`, `export enum ${alias}`))
|
|
624
|
+
: alreadyExported
|
|
625
|
+
? text
|
|
626
|
+
: `export ${text}`;
|
|
627
|
+
return { definition, typeString, typeStringIsExpression: false };
|
|
628
|
+
}
|
|
629
|
+
// Try variable (const type definitions)
|
|
630
|
+
const varDecl = sourceFile.getVariableDeclaration(symbolName);
|
|
631
|
+
if (varDecl) {
|
|
632
|
+
const type = varDecl.getType();
|
|
633
|
+
const typeString = type.getText();
|
|
634
|
+
const definition = `export type ${alias} = ${typeString};`;
|
|
635
|
+
return { definition, typeString, typeStringIsExpression: true };
|
|
636
|
+
}
|
|
637
|
+
// Try function
|
|
638
|
+
const funcDecl = sourceFile.getFunction(symbolName);
|
|
639
|
+
if (funcDecl) {
|
|
640
|
+
const text = funcDecl.getText();
|
|
641
|
+
const typeString = text;
|
|
642
|
+
const alreadyExported = text.trimStart().startsWith('export');
|
|
643
|
+
const definition = alias !== symbolName
|
|
644
|
+
? (alreadyExported
|
|
645
|
+
? text.replace(`function ${symbolName}`, `declare function ${alias}`)
|
|
646
|
+
: text.replace(`function ${symbolName}`, `export declare function ${alias}`))
|
|
647
|
+
: alreadyExported
|
|
648
|
+
? text
|
|
649
|
+
: `export declare ${text}`;
|
|
650
|
+
return { definition, typeString, typeStringIsExpression: false };
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
isPathSource(sourceFile) {
|
|
655
|
+
if (sourceFile.startsWith('.') ||
|
|
656
|
+
sourceFile.startsWith('/') ||
|
|
657
|
+
path.isAbsolute(sourceFile)) {
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
const basePath = path.resolve(this.repoRoot, sourceFile);
|
|
661
|
+
if (fs.existsSync(basePath)) {
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
if (path.extname(sourceFile) !== '') {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.d.ts'];
|
|
668
|
+
return extensions.some((ext) => fs.existsSync(basePath + ext));
|
|
669
|
+
}
|
|
670
|
+
isPlaceholderSource(sourceFile) {
|
|
671
|
+
const trimmed = sourceFile.trim();
|
|
672
|
+
return trimmed === '' || trimmed === '%none%' || trimmed === '%inline%';
|
|
673
|
+
}
|
|
674
|
+
log(message) {
|
|
675
|
+
console.error(`[sidecar:bundler] ${message}`);
|
|
676
|
+
}
|
|
677
|
+
logError(message) {
|
|
678
|
+
console.error(`[sidecar:bundler:error] ${message}`);
|
|
679
|
+
}
|
|
680
|
+
}
|