@auto-engineer/component-implementer 0.11.14 → 0.11.16
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +18 -0
- package/dist/src/agent.d.ts +1 -1
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +3 -3
- package/dist/src/agent.js.map +1 -1
- package/dist/src/commands/implement-component.d.ts +8 -0
- package/dist/src/commands/implement-component.d.ts.map +1 -1
- package/dist/src/commands/implement-component.js +927 -215
- package/dist/src/commands/implement-component.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/agent.ts +3 -3
- package/src/commands/implement-component.ts +1258 -237
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
// noinspection ExceptionCaughtLocallyJS
|
|
2
|
-
|
|
3
1
|
import { type Command, defineCommandHandler, type Event } from '@auto-engineer/message-bus';
|
|
4
2
|
import * as fs from 'fs/promises';
|
|
5
3
|
import * as path from 'path';
|
|
4
|
+
import * as ts from 'typescript';
|
|
6
5
|
import createDebug from 'debug';
|
|
7
6
|
import { callAI, loadScheme } from '../agent';
|
|
8
7
|
import { execa } from 'execa';
|
|
@@ -13,6 +12,15 @@ const debugTypeCheck = createDebug('auto:client-implementer:component:typecheck'
|
|
|
13
12
|
const debugProcess = createDebug('auto:client-implementer:component:process');
|
|
14
13
|
const debugResult = createDebug('auto:client-implementer:component:result');
|
|
15
14
|
|
|
15
|
+
interface ComponentRegistryEntry {
|
|
16
|
+
name: string;
|
|
17
|
+
actualFilename: string;
|
|
18
|
+
type: 'atoms' | 'molecules' | 'organisms';
|
|
19
|
+
exports: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type ComponentRegistry = Map<string, ComponentRegistryEntry>;
|
|
23
|
+
|
|
16
24
|
export type ImplementComponentCommand = Command<
|
|
17
25
|
'ImplementComponent',
|
|
18
26
|
{
|
|
@@ -23,6 +31,10 @@ export type ImplementComponentCommand = Command<
|
|
|
23
31
|
filePath: string;
|
|
24
32
|
componentName: string;
|
|
25
33
|
failures?: string[];
|
|
34
|
+
aiOptions?: {
|
|
35
|
+
temperature?: number;
|
|
36
|
+
maxTokens?: number;
|
|
37
|
+
};
|
|
26
38
|
}
|
|
27
39
|
>;
|
|
28
40
|
|
|
@@ -67,6 +79,10 @@ export const commandHandler = defineCommandHandler<
|
|
|
67
79
|
filePath: { description: 'Component file path', required: true },
|
|
68
80
|
componentName: { description: 'Name of component to implement', required: true },
|
|
69
81
|
failures: { description: 'Any failures from previous implementations', required: false },
|
|
82
|
+
aiOptions: {
|
|
83
|
+
description: 'AI generation options',
|
|
84
|
+
required: false,
|
|
85
|
+
},
|
|
70
86
|
},
|
|
71
87
|
examples: [
|
|
72
88
|
'$ auto implement:component --project-dir=./client --ia-scheme-dir=./.context --design-system-path=./design-system.md --component-type=molecule --component-name=SurveyCard',
|
|
@@ -85,122 +101,317 @@ export const commandHandler = defineCommandHandler<
|
|
|
85
101
|
},
|
|
86
102
|
});
|
|
87
103
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
interface ComponentLoadData {
|
|
105
|
+
scheme: Record<string, unknown>;
|
|
106
|
+
componentDef: Record<string, unknown>;
|
|
107
|
+
existingScaffold: string;
|
|
108
|
+
projectConfig: Record<string, string>;
|
|
109
|
+
designSystemReference: string;
|
|
110
|
+
registry: ComponentRegistry;
|
|
111
|
+
outPath: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function loadComponentDataForImplementation(
|
|
115
|
+
iaSchemeDir: string,
|
|
116
|
+
componentType: string,
|
|
117
|
+
componentName: string,
|
|
118
|
+
projectDir: string,
|
|
119
|
+
designSystemPath: string,
|
|
120
|
+
filePath: string,
|
|
121
|
+
): Promise<ComponentLoadData> {
|
|
122
|
+
const t1 = performance.now();
|
|
123
|
+
const scheme = await loadScheme(iaSchemeDir);
|
|
124
|
+
debugProcess(`[1] Loaded IA scheme in ${(performance.now() - t1).toFixed(2)} ms`);
|
|
125
|
+
if (!scheme) throw new Error('IA scheme not found');
|
|
126
|
+
|
|
127
|
+
const pluralKey = `${componentType}s`;
|
|
128
|
+
const collection = (scheme as Record<string, unknown>)[pluralKey];
|
|
129
|
+
if (!isValidCollection(collection)) throw new Error(`Invalid IA schema structure for ${pluralKey}`);
|
|
130
|
+
|
|
131
|
+
const items = (collection as { items: Record<string, unknown> }).items;
|
|
132
|
+
const componentDef = items[componentName] as Record<string, unknown> | undefined;
|
|
133
|
+
if (!componentDef) throw new Error(`Component ${componentType}:${componentName} not found in IA schema`);
|
|
93
134
|
|
|
135
|
+
const outPath = path.join(projectDir, '..', filePath);
|
|
136
|
+
|
|
137
|
+
const t2 = performance.now();
|
|
138
|
+
let existingScaffold = '';
|
|
94
139
|
try {
|
|
95
|
-
|
|
96
|
-
debugProcess(`
|
|
140
|
+
existingScaffold = await fs.readFile(outPath, 'utf-8');
|
|
141
|
+
debugProcess(`[2] Found existing scaffold in ${(performance.now() - t2).toFixed(2)} ms`);
|
|
142
|
+
} catch {
|
|
143
|
+
debugProcess(`[2] No existing scaffold found (${(performance.now() - t2).toFixed(2)} ms)`);
|
|
144
|
+
}
|
|
97
145
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
146
|
+
const t3 = performance.now();
|
|
147
|
+
const projectConfig = await readProjectContext(projectDir);
|
|
148
|
+
debugProcess(`[3] Loaded project context in ${(performance.now() - t3).toFixed(2)} ms`);
|
|
149
|
+
|
|
150
|
+
const t4 = performance.now();
|
|
151
|
+
const designSystemReference = await readDesignSystem(designSystemPath, { projectDir, iaSchemeDir });
|
|
152
|
+
debugProcess(`[4] Loaded design system reference in ${(performance.now() - t4).toFixed(2)} ms`);
|
|
153
|
+
|
|
154
|
+
const t5 = performance.now();
|
|
155
|
+
const registry = await buildComponentRegistry(projectDir);
|
|
156
|
+
debugProcess(`[5] Built component registry in ${(performance.now() - t5).toFixed(2)} ms`);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
scheme: scheme as Record<string, unknown>,
|
|
160
|
+
componentDef,
|
|
161
|
+
existingScaffold,
|
|
162
|
+
projectConfig,
|
|
163
|
+
designSystemReference,
|
|
164
|
+
registry,
|
|
165
|
+
outPath,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
102
168
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
169
|
+
async function loadDependencySources(
|
|
170
|
+
scheme: Record<string, unknown>,
|
|
171
|
+
componentType: string,
|
|
172
|
+
componentName: string,
|
|
173
|
+
projectDir: string,
|
|
174
|
+
registry: ComponentRegistry,
|
|
175
|
+
): Promise<Record<string, string>> {
|
|
176
|
+
const dependencyList = await resolveDependenciesRecursively(scheme, componentType, componentName);
|
|
177
|
+
debugProcess(`[6] Resolved ${dependencyList.length} dependencies for ${componentName}`);
|
|
178
|
+
|
|
179
|
+
const { primary: primaryDeps } = resolveDependenciesToRegistry(dependencyList, registry);
|
|
180
|
+
const dependencySources: Record<string, string> = {};
|
|
181
|
+
|
|
182
|
+
const allAtoms = Array.from(registry.entries())
|
|
183
|
+
.filter(([_, entry]) => entry.type === 'atoms')
|
|
184
|
+
.map(([name, _]) => name);
|
|
185
|
+
|
|
186
|
+
for (const atomName of allAtoms) {
|
|
187
|
+
const atomSource = await readComponentPropsInterface(projectDir, 'atoms', atomName, registry);
|
|
188
|
+
if (atomSource !== null) {
|
|
189
|
+
dependencySources[`atoms/${atomName}`] = atomSource;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
106
192
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
193
|
+
for (const dep of primaryDeps) {
|
|
194
|
+
debugProcess(`[loadDependencySources] Attempting to read dependency ${dep.type}/${dep.name}`);
|
|
195
|
+
const depContent = await readComponentFullImplementation(projectDir, dep.type, dep.name, registry);
|
|
196
|
+
if (depContent != null) {
|
|
197
|
+
debugProcess(`[loadDependencySources] Successfully read full implementation for ${dep.type}/${dep.name}`);
|
|
198
|
+
dependencySources[`${dep.type}/${dep.name}`] = depContent;
|
|
199
|
+
} else {
|
|
200
|
+
debugProcess(`[loadDependencySources] Failed to read implementation for ${dep.type}/${dep.name} (returned null)`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
110
203
|
|
|
111
|
-
|
|
204
|
+
return dependencySources;
|
|
205
|
+
}
|
|
112
206
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
207
|
+
async function generateCodeWithRetryLoop(params: {
|
|
208
|
+
componentName: string;
|
|
209
|
+
componentDef: Record<string, unknown>;
|
|
210
|
+
basePrompt: string;
|
|
211
|
+
composition: string[];
|
|
212
|
+
dependencySummary: string;
|
|
213
|
+
projectConfig: Record<string, string>;
|
|
214
|
+
graphqlFiles: Record<string, string>;
|
|
215
|
+
outPath: string;
|
|
216
|
+
projectDir: string;
|
|
217
|
+
registry: ComponentRegistry;
|
|
218
|
+
maxTokens: number;
|
|
219
|
+
existingScaffold: string;
|
|
220
|
+
}): Promise<string> {
|
|
221
|
+
const {
|
|
222
|
+
componentName,
|
|
223
|
+
componentDef,
|
|
224
|
+
basePrompt,
|
|
225
|
+
composition,
|
|
226
|
+
dependencySummary,
|
|
227
|
+
projectConfig,
|
|
228
|
+
graphqlFiles,
|
|
229
|
+
outPath,
|
|
230
|
+
projectDir,
|
|
231
|
+
registry,
|
|
232
|
+
existingScaffold,
|
|
233
|
+
} = params;
|
|
234
|
+
|
|
235
|
+
let attempt = 1;
|
|
236
|
+
let code = '';
|
|
237
|
+
let lastErrors = '';
|
|
238
|
+
let lastImportErrors: string[] = [];
|
|
239
|
+
const maxAttempts = 3;
|
|
240
|
+
let currentMaxTokens = params.maxTokens;
|
|
241
|
+
const description = (componentDef.description as string) ?? '';
|
|
242
|
+
|
|
243
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
244
|
+
|
|
245
|
+
while (attempt <= maxAttempts) {
|
|
246
|
+
const genStart = performance.now();
|
|
247
|
+
|
|
248
|
+
const prompt =
|
|
249
|
+
attempt === 1
|
|
250
|
+
? makeImplementPrompt(basePrompt, graphqlFiles)
|
|
251
|
+
: makeRetryPrompt(
|
|
252
|
+
componentName,
|
|
253
|
+
lastErrors,
|
|
254
|
+
composition,
|
|
255
|
+
dependencySummary,
|
|
256
|
+
lastImportErrors,
|
|
257
|
+
description,
|
|
258
|
+
existingScaffold,
|
|
259
|
+
projectConfig['package.json'] ?? '',
|
|
260
|
+
graphqlFiles,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const promptPath = `/tmp/prompt-${componentName}-attempt-${attempt}.txt`;
|
|
264
|
+
await fs.writeFile(promptPath, prompt, 'utf-8');
|
|
265
|
+
debugProcess(`[DEBUG] Saved prompt to ${promptPath} (${prompt.length} chars)`);
|
|
266
|
+
|
|
267
|
+
const aiRaw = await callAI(prompt, { maxTokens: currentMaxTokens });
|
|
268
|
+
code = extractCodeBlock(aiRaw);
|
|
269
|
+
|
|
270
|
+
const isTruncated = detectTruncation(code);
|
|
271
|
+
if (isTruncated) {
|
|
272
|
+
const suggestedMaxTokens = Math.ceil(currentMaxTokens * 1.5);
|
|
273
|
+
debugProcess(
|
|
274
|
+
`[WARNING] Truncation detected at attempt ${attempt}. Increasing maxTokens: ${currentMaxTokens} → ${suggestedMaxTokens}`,
|
|
275
|
+
);
|
|
276
|
+
currentMaxTokens = suggestedMaxTokens;
|
|
120
277
|
}
|
|
121
278
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
279
|
+
await fs.writeFile(outPath, code, 'utf-8');
|
|
280
|
+
debugProcess(
|
|
281
|
+
`[6.${attempt}] AI output written (${code.length} chars, truncated: ${isTruncated}) in ${(performance.now() - genStart).toFixed(2)} ms`,
|
|
282
|
+
);
|
|
125
283
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
284
|
+
const importValidation = validateImports(code, registry);
|
|
285
|
+
lastImportErrors = importValidation.errors;
|
|
286
|
+
if (!importValidation.valid) {
|
|
287
|
+
debugProcess(`[WARN] Invalid imports detected: ${importValidation.errors.join('; ')}`);
|
|
288
|
+
}
|
|
129
289
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
290
|
+
const checkStart = performance.now();
|
|
291
|
+
const { success, errors } = await runTypeCheckForFile(projectDir, outPath);
|
|
292
|
+
debugTypeCheck(
|
|
293
|
+
`[7.${attempt}] Type check in ${(performance.now() - checkStart).toFixed(2)} ms (success: ${success})`,
|
|
134
294
|
);
|
|
135
295
|
|
|
136
|
-
|
|
296
|
+
if (success) {
|
|
297
|
+
return code;
|
|
298
|
+
}
|
|
137
299
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
300
|
+
lastErrors = errors;
|
|
301
|
+
if (attempt === maxAttempts) {
|
|
302
|
+
const wasTruncated = detectTruncation(code);
|
|
303
|
+
const errorMessage = wasTruncated
|
|
304
|
+
? `Component generation failed after ${attempt} attempts due to output truncation.\n` +
|
|
305
|
+
`Final maxTokens used: ${currentMaxTokens}\n` +
|
|
306
|
+
`Suggestion: Increase aiOptions.maxTokens in your config (try ${Math.ceil(currentMaxTokens * 1.5)} or higher)\n\n` +
|
|
307
|
+
`TypeScript errors:\n${errors}`
|
|
308
|
+
: `Type errors persist after ${attempt} attempts:\n${errors}`;
|
|
309
|
+
throw new Error(errorMessage);
|
|
142
310
|
}
|
|
311
|
+
attempt += 1;
|
|
312
|
+
}
|
|
143
313
|
|
|
144
|
-
|
|
314
|
+
throw new Error('Unreachable state');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function handleImplementComponentCommandInternal(
|
|
318
|
+
command: ImplementComponentCommand,
|
|
319
|
+
): Promise<ComponentImplementedEvent | ComponentImplementationFailedEvent> {
|
|
320
|
+
const { projectDir, iaSchemeDir, designSystemPath, componentType, componentName, filePath } = command.data;
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const start = performance.now();
|
|
324
|
+
debugProcess(`Starting ${componentType}:${componentName}`);
|
|
325
|
+
|
|
326
|
+
const loadData = await loadComponentDataForImplementation(
|
|
327
|
+
iaSchemeDir,
|
|
145
328
|
componentType,
|
|
146
329
|
componentName,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
designSystemReference,
|
|
151
|
-
dependencySources,
|
|
330
|
+
projectDir,
|
|
331
|
+
designSystemPath,
|
|
332
|
+
filePath,
|
|
152
333
|
);
|
|
153
334
|
|
|
154
|
-
|
|
335
|
+
const dependencySources = await loadDependencySources(
|
|
336
|
+
loadData.scheme,
|
|
337
|
+
componentType,
|
|
338
|
+
componentName,
|
|
339
|
+
projectDir,
|
|
340
|
+
loadData.registry,
|
|
341
|
+
);
|
|
155
342
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
343
|
+
const usageInfo = await extractComponentUsageFromScaffolds(
|
|
344
|
+
componentName,
|
|
345
|
+
componentType as 'molecule' | 'organism',
|
|
346
|
+
projectDir,
|
|
347
|
+
);
|
|
348
|
+
debugProcess(
|
|
349
|
+
`[extractComponentUsageFromScaffolds] Found ${usageInfo.usageExamples.length} usage examples, requiresChildren: ${usageInfo.requiresChildren}`,
|
|
350
|
+
);
|
|
160
351
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
352
|
+
const basePrompt = makeBasePrompt(
|
|
353
|
+
componentType,
|
|
354
|
+
componentName,
|
|
355
|
+
loadData.componentDef,
|
|
356
|
+
loadData.existingScaffold,
|
|
357
|
+
loadData.projectConfig,
|
|
358
|
+
loadData.designSystemReference,
|
|
359
|
+
dependencySources,
|
|
360
|
+
usageInfo,
|
|
361
|
+
);
|
|
167
362
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
363
|
+
const composition = extractComposition(loadData.componentDef);
|
|
364
|
+
const dependencySummary =
|
|
365
|
+
Object.entries(dependencySources)
|
|
366
|
+
.map(([name, src]) => `### ${name}\n${src}`)
|
|
367
|
+
.join('\n\n') || '(No dependencies found)';
|
|
368
|
+
|
|
369
|
+
const hasOwnDataRequirements = hasDataRequirements(loadData.componentDef);
|
|
370
|
+
const hasParentDataRequirements =
|
|
371
|
+
componentType === 'molecule' ? findParentDataRequirements(loadData.scheme, componentName) : false;
|
|
372
|
+
const needsGraphQLFiles = hasOwnDataRequirements || hasParentDataRequirements;
|
|
373
|
+
|
|
374
|
+
let graphqlFiles: Record<string, string> = {};
|
|
375
|
+
if (needsGraphQLFiles) {
|
|
376
|
+
const t6 = performance.now();
|
|
377
|
+
graphqlFiles = await readGraphQLFiles(projectDir);
|
|
378
|
+
const reason = hasOwnDataRequirements ? 'has own data requirements' : 'parent has data requirements';
|
|
171
379
|
debugProcess(
|
|
172
|
-
`[6
|
|
380
|
+
`[6] Loaded GraphQL files for ${componentName} (${reason}) in ${(performance.now() - t6).toFixed(2)} ms`,
|
|
173
381
|
);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const { success, errors } = await runTypeCheckForFile(projectDir, outPath);
|
|
177
|
-
debugTypeCheck(
|
|
178
|
-
`[7.${attempt}] Type check in ${(performance.now() - checkStart).toFixed(2)} ms (success: ${success})`,
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
if (success) {
|
|
182
|
-
debugResult(`[✓] Implementation succeeded in ${(performance.now() - start).toFixed(2)} ms total`);
|
|
183
|
-
return {
|
|
184
|
-
type: 'ComponentImplemented',
|
|
185
|
-
data: {
|
|
186
|
-
filePath: outPath,
|
|
187
|
-
componentType,
|
|
188
|
-
componentName,
|
|
189
|
-
composition: extractComposition(componentDef),
|
|
190
|
-
specs: extractSpecs(componentDef),
|
|
191
|
-
},
|
|
192
|
-
timestamp: new Date(),
|
|
193
|
-
requestId: command.requestId,
|
|
194
|
-
correlationId: command.correlationId,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
lastErrors = errors;
|
|
199
|
-
if (attempt === maxAttempts) throw new Error(`Type errors persist after ${attempt} attempts:\n${errors}`);
|
|
200
|
-
attempt += 1;
|
|
382
|
+
} else {
|
|
383
|
+
debugProcess(`[6] Skipped GraphQL files for ${componentName} (no data requirements)`);
|
|
201
384
|
}
|
|
202
385
|
|
|
203
|
-
|
|
386
|
+
await generateCodeWithRetryLoop({
|
|
387
|
+
componentName,
|
|
388
|
+
componentDef: loadData.componentDef,
|
|
389
|
+
basePrompt,
|
|
390
|
+
composition,
|
|
391
|
+
dependencySummary,
|
|
392
|
+
projectConfig: loadData.projectConfig,
|
|
393
|
+
graphqlFiles,
|
|
394
|
+
outPath: loadData.outPath,
|
|
395
|
+
projectDir,
|
|
396
|
+
registry: loadData.registry,
|
|
397
|
+
maxTokens: command.data.aiOptions?.maxTokens ?? 2000,
|
|
398
|
+
existingScaffold: loadData.existingScaffold,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
debugResult(`[✓] Implementation succeeded in ${(performance.now() - start).toFixed(2)} ms total`);
|
|
402
|
+
return {
|
|
403
|
+
type: 'ComponentImplemented',
|
|
404
|
+
data: {
|
|
405
|
+
filePath: loadData.outPath,
|
|
406
|
+
componentType,
|
|
407
|
+
componentName,
|
|
408
|
+
composition: extractComposition(loadData.componentDef),
|
|
409
|
+
specs: extractSpecs(loadData.componentDef),
|
|
410
|
+
},
|
|
411
|
+
timestamp: new Date(),
|
|
412
|
+
requestId: command.requestId,
|
|
413
|
+
correlationId: command.correlationId,
|
|
414
|
+
};
|
|
204
415
|
} catch (error: unknown) {
|
|
205
416
|
debug('[Error] Component implementation failed: %O', error);
|
|
206
417
|
return {
|
|
@@ -218,7 +429,53 @@ async function handleImplementComponentCommandInternal(
|
|
|
218
429
|
}
|
|
219
430
|
}
|
|
220
431
|
|
|
221
|
-
|
|
432
|
+
async function resolveCompositionDependencies(
|
|
433
|
+
scheme: Record<string, unknown>,
|
|
434
|
+
composition: Record<string, string[]>,
|
|
435
|
+
visited: Set<string>,
|
|
436
|
+
): Promise<{ type: string; name: string }[]> {
|
|
437
|
+
const result: { type: string; name: string }[] = [];
|
|
438
|
+
for (const [subType, subNames] of Object.entries(composition)) {
|
|
439
|
+
if (!Array.isArray(subNames)) continue;
|
|
440
|
+
for (const subName of subNames) {
|
|
441
|
+
result.push({ type: subType, name: subName });
|
|
442
|
+
const nested = await resolveDependenciesRecursively(scheme, subType, subName, visited);
|
|
443
|
+
result.push(...nested);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return result;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function resolveLayoutDependencies(
|
|
450
|
+
scheme: Record<string, unknown>,
|
|
451
|
+
layout: Record<string, unknown>,
|
|
452
|
+
visited: Set<string>,
|
|
453
|
+
): Promise<{ type: string; name: string }[]> {
|
|
454
|
+
const result: { type: string; name: string }[] = [];
|
|
455
|
+
if ('organisms' in layout && Array.isArray(layout.organisms)) {
|
|
456
|
+
for (const organismName of layout.organisms as string[]) {
|
|
457
|
+
result.push({ type: 'organisms', name: organismName });
|
|
458
|
+
const nested = await resolveDependenciesRecursively(scheme, 'organisms', organismName, visited);
|
|
459
|
+
result.push(...nested);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function getComponentDefinitionFromScheme(
|
|
466
|
+
scheme: Record<string, unknown>,
|
|
467
|
+
type: string,
|
|
468
|
+
name: string,
|
|
469
|
+
): Record<string, unknown> | null {
|
|
470
|
+
const collection = scheme[`${type}s`];
|
|
471
|
+
if (collection === null || collection === undefined || !isValidCollection(collection)) return null;
|
|
472
|
+
|
|
473
|
+
const def = collection.items[name];
|
|
474
|
+
if (def === null || def === undefined || typeof def !== 'object') return null;
|
|
475
|
+
|
|
476
|
+
return def as Record<string, unknown>;
|
|
477
|
+
}
|
|
478
|
+
|
|
222
479
|
async function resolveDependenciesRecursively(
|
|
223
480
|
scheme: Record<string, unknown>,
|
|
224
481
|
type: string,
|
|
@@ -229,34 +486,310 @@ async function resolveDependenciesRecursively(
|
|
|
229
486
|
if (visited.has(key)) return [];
|
|
230
487
|
visited.add(key);
|
|
231
488
|
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
235
|
-
if (!collection || !isValidCollection(collection)) return [];
|
|
236
|
-
|
|
237
|
-
const def = collection.items[name];
|
|
238
|
-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
239
|
-
if (!def || typeof def !== 'object' || !('composition' in def)) return [];
|
|
489
|
+
const def = getComponentDefinitionFromScheme(scheme, type, name);
|
|
490
|
+
if (def === null) return [];
|
|
240
491
|
|
|
241
492
|
const result: { type: string; name: string }[] = [];
|
|
242
493
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
494
|
+
if ('composition' in def) {
|
|
495
|
+
const compositionDeps = await resolveCompositionDependencies(
|
|
496
|
+
scheme,
|
|
497
|
+
def.composition as Record<string, string[]>,
|
|
498
|
+
visited,
|
|
499
|
+
);
|
|
500
|
+
result.push(...compositionDeps);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if ('layout' in def && typeof def.layout === 'object' && def.layout !== null) {
|
|
504
|
+
const layoutDeps = await resolveLayoutDependencies(scheme, def.layout as Record<string, unknown>, visited);
|
|
505
|
+
result.push(...layoutDeps);
|
|
251
506
|
}
|
|
507
|
+
|
|
252
508
|
return result;
|
|
253
509
|
}
|
|
254
510
|
|
|
255
|
-
|
|
256
|
-
const
|
|
511
|
+
function hasExportModifier(node: ts.Node): boolean {
|
|
512
|
+
const modifiers = 'modifiers' in node ? (node.modifiers as readonly ts.Modifier[] | undefined) : undefined;
|
|
513
|
+
return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function extractFromVariableStatement(node: ts.VariableStatement, exports: string[]): void {
|
|
517
|
+
node.declarationList.declarations.forEach((decl) => {
|
|
518
|
+
if (ts.isIdentifier(decl.name)) {
|
|
519
|
+
exports.push(decl.name.text);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function extractFromExportDeclaration(node: ts.ExportDeclaration, exports: string[]): void {
|
|
525
|
+
if (node.exportClause !== undefined && ts.isNamedExports(node.exportClause)) {
|
|
526
|
+
node.exportClause.elements.forEach((element) => {
|
|
527
|
+
exports.push(element.name.text);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function extractExportedComponentNames(sourceFile: ts.SourceFile): string[] {
|
|
533
|
+
const exports: string[] = [];
|
|
534
|
+
|
|
535
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
536
|
+
if (ts.isVariableStatement(node) && hasExportModifier(node)) {
|
|
537
|
+
extractFromVariableStatement(node, exports);
|
|
538
|
+
} else if (ts.isExportDeclaration(node)) {
|
|
539
|
+
extractFromExportDeclaration(node, exports);
|
|
540
|
+
} else if (ts.isFunctionDeclaration(node) && hasExportModifier(node) && node.name !== undefined) {
|
|
541
|
+
exports.push(node.name.text);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
return exports;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function extractAllExportedTypes(sourceFile: ts.SourceFile): string[] {
|
|
549
|
+
const types: string[] = [];
|
|
550
|
+
|
|
551
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
552
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
553
|
+
const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
554
|
+
if (hasExport) {
|
|
555
|
+
types.push(node.name.text);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (ts.isTypeAliasDeclaration(node)) {
|
|
559
|
+
const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
560
|
+
if (hasExport) {
|
|
561
|
+
types.push(node.name.text);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
return types;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function extractTypeReferencesFromProps(
|
|
570
|
+
node: ts.InterfaceDeclaration | ts.TypeAliasDeclaration,
|
|
571
|
+
sourceFile: ts.SourceFile,
|
|
572
|
+
): Set<string> {
|
|
573
|
+
const refs = new Set<string>();
|
|
574
|
+
|
|
575
|
+
function visitType(typeNode: ts.TypeNode): void {
|
|
576
|
+
if (ts.isTypeReferenceNode(typeNode)) {
|
|
577
|
+
const typeName = typeNode.typeName.getText(sourceFile);
|
|
578
|
+
refs.add(typeName);
|
|
579
|
+
}
|
|
580
|
+
if (ts.isArrayTypeNode(typeNode)) {
|
|
581
|
+
visitType(typeNode.elementType);
|
|
582
|
+
}
|
|
583
|
+
if (ts.isUnionTypeNode(typeNode) || ts.isIntersectionTypeNode(typeNode)) {
|
|
584
|
+
typeNode.types.forEach(visitType);
|
|
585
|
+
}
|
|
586
|
+
if (ts.isFunctionTypeNode(typeNode)) {
|
|
587
|
+
typeNode.parameters.forEach((param) => {
|
|
588
|
+
if (param.type !== undefined) visitType(param.type);
|
|
589
|
+
});
|
|
590
|
+
if (typeNode.type !== undefined) visitType(typeNode.type);
|
|
591
|
+
}
|
|
592
|
+
if (ts.isParenthesizedTypeNode(typeNode)) {
|
|
593
|
+
visitType(typeNode.type);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
598
|
+
node.members.forEach((member) => {
|
|
599
|
+
if (ts.isPropertySignature(member) && member.type !== undefined) {
|
|
600
|
+
visitType(member.type);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
} else if (ts.isTypeAliasDeclaration(node) && node.type !== undefined) {
|
|
604
|
+
visitType(node.type);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return refs;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function extractTypeAlias(node: ts.TypeAliasDeclaration, sourceFile: ts.SourceFile): string {
|
|
611
|
+
const typeName = node.name.text;
|
|
612
|
+
const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
613
|
+
const exportPrefix = hasExport ? 'export ' : '';
|
|
614
|
+
const typeValue = node.type?.getText(sourceFile) ?? 'unknown';
|
|
615
|
+
return `${exportPrefix}type ${typeName} = ${typeValue}`;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function extractEnumDefinition(node: ts.EnumDeclaration, sourceFile: ts.SourceFile): string {
|
|
619
|
+
const enumName = node.name.text;
|
|
620
|
+
const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
621
|
+
const exportPrefix = hasExport ? 'export ' : '';
|
|
622
|
+
const members = node.members
|
|
623
|
+
.map((m) => {
|
|
624
|
+
const name = m.name.getText(sourceFile);
|
|
625
|
+
const value = m.initializer?.getText(sourceFile);
|
|
626
|
+
return value !== undefined ? `${name} = ${value}` : name;
|
|
627
|
+
})
|
|
628
|
+
.join(', ');
|
|
629
|
+
return `${exportPrefix}enum ${enumName} { ${members} }`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function extractInterfaceDefinition(node: ts.InterfaceDeclaration, sourceFile: ts.SourceFile): string {
|
|
633
|
+
const interfaceName = node.name.text;
|
|
634
|
+
const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
635
|
+
const exportPrefix = hasExport ? 'export ' : '';
|
|
636
|
+
const members = node.members
|
|
637
|
+
.map((member) => {
|
|
638
|
+
if (ts.isPropertySignature(member) && member.name !== undefined) {
|
|
639
|
+
const propName = member.name.getText(sourceFile);
|
|
640
|
+
const optional = member.questionToken !== undefined ? '?' : '';
|
|
641
|
+
const propType = member.type !== undefined ? member.type.getText(sourceFile) : 'any';
|
|
642
|
+
return `${propName}${optional}: ${propType}`;
|
|
643
|
+
}
|
|
644
|
+
return '';
|
|
645
|
+
})
|
|
646
|
+
.filter((m) => m !== '')
|
|
647
|
+
.join('; ');
|
|
648
|
+
return `${exportPrefix}interface ${interfaceName} { ${members} }`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function extractReferencedTypeDefinitions(sourceFile: ts.SourceFile, typeReferences: Set<string>): string {
|
|
652
|
+
const definitions: string[] = [];
|
|
653
|
+
|
|
654
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
655
|
+
if (ts.isTypeAliasDeclaration(node) && typeReferences.has(node.name.text)) {
|
|
656
|
+
definitions.push(extractTypeAlias(node, sourceFile));
|
|
657
|
+
} else if (ts.isEnumDeclaration(node) && typeReferences.has(node.name.text)) {
|
|
658
|
+
definitions.push(extractEnumDefinition(node, sourceFile));
|
|
659
|
+
} else if (
|
|
660
|
+
ts.isInterfaceDeclaration(node) &&
|
|
661
|
+
typeReferences.has(node.name.text) &&
|
|
662
|
+
!node.name.text.includes('Props')
|
|
663
|
+
) {
|
|
664
|
+
definitions.push(extractInterfaceDefinition(node, sourceFile));
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
return definitions.length > 0 ? definitions.join('\n ') : '';
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function flattenPropsInterface(
|
|
672
|
+
node: ts.InterfaceDeclaration | ts.TypeAliasDeclaration,
|
|
673
|
+
sourceFile: ts.SourceFile,
|
|
674
|
+
): string {
|
|
675
|
+
const properties: string[] = [];
|
|
676
|
+
|
|
677
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
678
|
+
node.members.forEach((member) => {
|
|
679
|
+
if (ts.isPropertySignature(member) && member.name !== undefined) {
|
|
680
|
+
const propName = member.name.getText(sourceFile);
|
|
681
|
+
const optional = member.questionToken ? '?' : '';
|
|
682
|
+
const propType = member.type !== undefined ? member.type.getText(sourceFile) : 'any';
|
|
683
|
+
properties.push(`${propName}${optional}: ${propType}`);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
} else if (ts.isTypeAliasDeclaration(node) && node.type !== undefined) {
|
|
687
|
+
return node.type.getText(sourceFile);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return properties.join('; ');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function extractPropsAndImport(
|
|
694
|
+
sourceCode: string,
|
|
695
|
+
componentName: string,
|
|
696
|
+
type: string,
|
|
697
|
+
actualFilename: string,
|
|
698
|
+
): string {
|
|
699
|
+
const sourceFile = ts.createSourceFile(`${componentName}.tsx`, sourceCode, ts.ScriptTarget.Latest, true);
|
|
700
|
+
|
|
701
|
+
const exportedComponents = extractExportedComponentNames(sourceFile);
|
|
702
|
+
const allExportedTypes = extractAllExportedTypes(sourceFile);
|
|
703
|
+
|
|
704
|
+
let propsInline = '';
|
|
705
|
+
let propsNode: ts.InterfaceDeclaration | ts.TypeAliasDeclaration | null = null;
|
|
706
|
+
|
|
707
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
708
|
+
if (!propsInline) {
|
|
709
|
+
if (ts.isInterfaceDeclaration(node) && node.name.text.includes(`${componentName}Props`)) {
|
|
710
|
+
propsInline = flattenPropsInterface(node, sourceFile);
|
|
711
|
+
propsNode = node;
|
|
712
|
+
} else if (ts.isTypeAliasDeclaration(node) && node.name.text.includes(`${componentName}Props`)) {
|
|
713
|
+
propsInline = flattenPropsInterface(node, sourceFile);
|
|
714
|
+
propsNode = node;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const typeReferences = propsNode !== null ? extractTypeReferencesFromProps(propsNode, sourceFile) : new Set<string>();
|
|
720
|
+
|
|
721
|
+
const toImport = exportedComponents.length > 0 ? [...exportedComponents] : [componentName];
|
|
722
|
+
|
|
723
|
+
for (const typeRef of typeReferences) {
|
|
724
|
+
if (allExportedTypes.includes(typeRef) && !toImport.includes(typeRef)) {
|
|
725
|
+
toImport.push(typeRef);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const typeDefinitions = extractReferencedTypeDefinitions(sourceFile, typeReferences);
|
|
730
|
+
|
|
731
|
+
const importStatement = `import { ${toImport.join(', ')} } from '@/components/${type}/${actualFilename}';`;
|
|
732
|
+
|
|
733
|
+
const importLine = `**Import**: \`${importStatement}\``;
|
|
734
|
+
const typeDefsLine = typeDefinitions ? `**Type Definitions**:\n ${typeDefinitions}` : '';
|
|
735
|
+
const propsLine = propsInline ? `**Props**: \`${propsInline}\`` : '**Props**: None';
|
|
736
|
+
|
|
737
|
+
const parts = [importLine, typeDefsLine, propsLine].filter((p) => p !== '');
|
|
738
|
+
return parts.join('\n');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async function readComponentPropsInterface(
|
|
742
|
+
projectDir: string,
|
|
743
|
+
_type: string,
|
|
744
|
+
name: string,
|
|
745
|
+
registry: ComponentRegistry,
|
|
746
|
+
): Promise<string | null> {
|
|
747
|
+
const entry = registry.get(name);
|
|
748
|
+
if (entry === undefined) {
|
|
749
|
+
debugProcess(`[readComponentPropsInterface] No registry entry found for ${name}`);
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const file = path.join(projectDir, 'src', 'components', entry.type, `${entry.actualFilename}.tsx`);
|
|
754
|
+
debugProcess(`[readComponentPropsInterface] Reading file: ${file}`);
|
|
257
755
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
756
|
+
const sourceCode = await fs.readFile(file, 'utf-8');
|
|
757
|
+
const result = extractPropsAndImport(sourceCode, name, entry.type, entry.actualFilename);
|
|
758
|
+
debugProcess(`[readComponentPropsInterface] extractPropsAndImport returned ${result ? result.length : 0} chars`);
|
|
759
|
+
return result;
|
|
760
|
+
} catch (error) {
|
|
761
|
+
debugProcess(`[readComponentPropsInterface] Error reading file: ${String(error)}`);
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async function readComponentFullImplementation(
|
|
767
|
+
projectDir: string,
|
|
768
|
+
_type: string,
|
|
769
|
+
name: string,
|
|
770
|
+
registry: ComponentRegistry,
|
|
771
|
+
): Promise<string | null> {
|
|
772
|
+
const entry = registry.get(name);
|
|
773
|
+
if (entry === undefined) {
|
|
774
|
+
debugProcess(`[readComponentFullImplementation] No registry entry found for ${name}`);
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const file = path.join(projectDir, 'src', 'components', entry.type, `${entry.actualFilename}.tsx`);
|
|
779
|
+
debugProcess(`[readComponentFullImplementation] Reading file: ${file}`);
|
|
780
|
+
try {
|
|
781
|
+
const sourceCode = await fs.readFile(file, 'utf-8');
|
|
782
|
+
const sourceFile = ts.createSourceFile(`${name}.tsx`, sourceCode, ts.ScriptTarget.Latest, true);
|
|
783
|
+
const exportedComponents = extractExportedComponentNames(sourceFile);
|
|
784
|
+
const toImport = exportedComponents.length > 0 ? [...exportedComponents] : [name];
|
|
785
|
+
const importStatement = `import { ${toImport.join(', ')} } from '@/components/${entry.type}/${entry.actualFilename}';`;
|
|
786
|
+
|
|
787
|
+
const formattedOutput = `**Import**: \`${importStatement}\`\n\n**Implementation**:\n\`\`\`tsx\n${sourceCode}\n\`\`\``;
|
|
788
|
+
|
|
789
|
+
debugProcess(`[readComponentFullImplementation] Returning ${formattedOutput.length} chars for ${name}`);
|
|
790
|
+
return formattedOutput;
|
|
791
|
+
} catch (error) {
|
|
792
|
+
debugProcess(`[readComponentFullImplementation] Error reading file: ${String(error)}`);
|
|
260
793
|
return null;
|
|
261
794
|
}
|
|
262
795
|
}
|
|
@@ -268,34 +801,172 @@ function extractCodeBlock(text: string): string {
|
|
|
268
801
|
.trim();
|
|
269
802
|
}
|
|
270
803
|
|
|
271
|
-
|
|
272
|
-
|
|
804
|
+
function detectTruncation(code: string): boolean {
|
|
805
|
+
const truncationIndicators = [/<\w+[^/>]*$/, /<\/\w*$/, /"[^"]*$/, /'[^']*$/, /`[^`]*$/, /\{[^}]*$/m, /\([^)]*$/m];
|
|
806
|
+
|
|
807
|
+
const lastLines = code.split('\n').slice(-5).join('\n');
|
|
808
|
+
|
|
809
|
+
return truncationIndicators.some((pattern) => pattern.test(lastLines));
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async function readEssentialFiles(projectDir: string): Promise<Record<string, string>> {
|
|
813
|
+
debugProcess('[readEssentialFiles] Reading essential config files from %s', projectDir);
|
|
273
814
|
const start = performance.now();
|
|
274
815
|
const config: Record<string, string> = {};
|
|
275
816
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
} else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))) {
|
|
285
|
-
try {
|
|
286
|
-
config[relativePath] = await fs.readFile(fullPath, 'utf-8');
|
|
287
|
-
} catch (err) {
|
|
288
|
-
debugProcess(`Failed to read ${relativePath}: ${(err as Error).message}`);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
817
|
+
const essentialFiles = ['package.json', 'tsconfig.json', 'tailwind.config.js', 'tailwind.config.ts'];
|
|
818
|
+
|
|
819
|
+
for (const file of essentialFiles) {
|
|
820
|
+
try {
|
|
821
|
+
config[file] = await fs.readFile(path.join(projectDir, file), 'utf-8');
|
|
822
|
+
debugProcess(`Read essential file: ${file}`);
|
|
823
|
+
} catch {
|
|
824
|
+
debugProcess(`Essential file not found: ${file}`);
|
|
291
825
|
}
|
|
292
826
|
}
|
|
293
827
|
|
|
294
|
-
|
|
295
|
-
debugProcess(`[readAllTopLevelFiles] Completed in ${(performance.now() - start).toFixed(2)} ms`);
|
|
828
|
+
debugProcess(`[readEssentialFiles] Completed in ${(performance.now() - start).toFixed(2)} ms`);
|
|
296
829
|
return config;
|
|
297
830
|
}
|
|
298
831
|
|
|
832
|
+
function hasDataRequirements(componentDef: Record<string, unknown>): boolean {
|
|
833
|
+
return (
|
|
834
|
+
'data_requirements' in componentDef &&
|
|
835
|
+
Array.isArray(componentDef.data_requirements) &&
|
|
836
|
+
componentDef.data_requirements.length > 0
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function checkOrganismForMolecule(moleculeName: string, organismName: string, organismDef: unknown): boolean {
|
|
841
|
+
if (typeof organismDef !== 'object' || organismDef === null) return false;
|
|
842
|
+
|
|
843
|
+
const composition = (organismDef as Record<string, unknown>).composition as { molecules?: string[] } | undefined;
|
|
844
|
+
|
|
845
|
+
const includesMolecule = composition?.molecules?.includes(moleculeName) ?? false;
|
|
846
|
+
if (!includesMolecule) return false;
|
|
847
|
+
|
|
848
|
+
const hasData = hasDataRequirements(organismDef as Record<string, unknown>);
|
|
849
|
+
if (hasData) {
|
|
850
|
+
debugProcess(
|
|
851
|
+
`[findParentDataRequirements] Molecule ${moleculeName} is used by organism ${organismName} which has data_requirements`,
|
|
852
|
+
);
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function checkPageLayoutOrganisms(
|
|
860
|
+
layout: { organisms?: string[] } | undefined,
|
|
861
|
+
organisms: { items: Record<string, unknown> },
|
|
862
|
+
moleculeName: string,
|
|
863
|
+
): boolean {
|
|
864
|
+
if (layout?.organisms === undefined) return false;
|
|
865
|
+
|
|
866
|
+
for (const organismName of layout.organisms) {
|
|
867
|
+
const organismDef = organisms.items[organismName];
|
|
868
|
+
if (checkOrganismForMolecule(moleculeName, organismName, organismDef)) {
|
|
869
|
+
return true;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function checkPagesForMolecule(
|
|
877
|
+
pages: { items?: Record<string, unknown> } | undefined,
|
|
878
|
+
organisms: { items?: Record<string, unknown> } | undefined,
|
|
879
|
+
moleculeName: string,
|
|
880
|
+
): boolean {
|
|
881
|
+
if (pages?.items === undefined || organisms?.items === undefined) return false;
|
|
882
|
+
|
|
883
|
+
const organismsWithItems = { items: organisms.items };
|
|
884
|
+
|
|
885
|
+
for (const [, pageDef] of Object.entries(pages.items)) {
|
|
886
|
+
if (typeof pageDef !== 'object' || pageDef === null) continue;
|
|
887
|
+
|
|
888
|
+
const layout = (pageDef as Record<string, unknown>).layout as { organisms?: string[] } | undefined;
|
|
889
|
+
if (checkPageLayoutOrganisms(layout, organismsWithItems, moleculeName)) {
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function findParentDataRequirements(scheme: Record<string, unknown>, moleculeName: string): boolean {
|
|
898
|
+
debugProcess(`[findParentDataRequirements] Checking if molecule ${moleculeName} has parents with data requirements`);
|
|
899
|
+
|
|
900
|
+
const organisms = scheme.organisms as { items?: Record<string, unknown> } | undefined;
|
|
901
|
+
|
|
902
|
+
if (organisms?.items !== undefined) {
|
|
903
|
+
for (const [organismName, organismDef] of Object.entries(organisms.items)) {
|
|
904
|
+
if (checkOrganismForMolecule(moleculeName, organismName, organismDef)) {
|
|
905
|
+
return true;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const pages = scheme.pages as { items?: Record<string, unknown> } | undefined;
|
|
911
|
+
if (checkPagesForMolecule(pages, organisms, moleculeName)) {
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
debugProcess(`[findParentDataRequirements] No parents with data_requirements found for molecule ${moleculeName}`);
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
async function readGraphQLFiles(projectDir: string): Promise<Record<string, string>> {
|
|
920
|
+
debugProcess('[readGraphQLFiles] Reading GraphQL type definition files from %s', projectDir);
|
|
921
|
+
const start = performance.now();
|
|
922
|
+
const graphqlFiles: Record<string, string> = {};
|
|
923
|
+
|
|
924
|
+
const graphqlFilePaths = ['src/gql/graphql.ts', 'src/graphql/queries.ts', 'src/graphql/mutations.ts'];
|
|
925
|
+
|
|
926
|
+
for (const filePath of graphqlFilePaths) {
|
|
927
|
+
try {
|
|
928
|
+
graphqlFiles[filePath] = await fs.readFile(path.join(projectDir, filePath), 'utf-8');
|
|
929
|
+
debugProcess(`Read GraphQL file: ${filePath}`);
|
|
930
|
+
} catch {
|
|
931
|
+
debugProcess(`GraphQL file not found: ${filePath}`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
debugProcess(`[readGraphQLFiles] Completed in ${(performance.now() - start).toFixed(2)} ms`);
|
|
936
|
+
return graphqlFiles;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
async function readProjectContext(projectDir: string): Promise<Record<string, string>> {
|
|
940
|
+
return await readEssentialFiles(projectDir);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
async function executeTypeCheck(tsconfigRoot: string, strict: boolean): Promise<string> {
|
|
944
|
+
const args = strict
|
|
945
|
+
? ['tsc', '--noEmit', '--skipLibCheck', '--strict', '--pretty', 'false']
|
|
946
|
+
: ['tsc', '--noEmit', '--skipLibCheck', '--pretty', 'false'];
|
|
947
|
+
|
|
948
|
+
const result = await execa('npx', args, {
|
|
949
|
+
cwd: tsconfigRoot,
|
|
950
|
+
stdio: 'pipe',
|
|
951
|
+
reject: false,
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
return (result.stdout ?? '') + (result.stderr ?? '');
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async function runGraphQLStrictCheck(
|
|
958
|
+
tsconfigRoot: string,
|
|
959
|
+
relativeFilePath: string,
|
|
960
|
+
normalizedRelative: string,
|
|
961
|
+
filePath: string,
|
|
962
|
+
): Promise<string> {
|
|
963
|
+
debugTypeCheck(`[runTypeCheckForFile] Running strict GraphQL type check...`);
|
|
964
|
+
const strictOutput = await executeTypeCheck(tsconfigRoot, true);
|
|
965
|
+
const graphqlStrictErrors = filterErrorsForFile(strictOutput, relativeFilePath, normalizedRelative, filePath);
|
|
966
|
+
debugTypeCheck(`[runTypeCheckForFile] GraphQL strict errors length: ${graphqlStrictErrors.length} chars`);
|
|
967
|
+
return graphqlStrictErrors;
|
|
968
|
+
}
|
|
969
|
+
|
|
299
970
|
async function runTypeCheckForFile(
|
|
300
971
|
projectDir: string,
|
|
301
972
|
filePath: string,
|
|
@@ -303,40 +974,124 @@ async function runTypeCheckForFile(
|
|
|
303
974
|
const start = performance.now();
|
|
304
975
|
try {
|
|
305
976
|
const tsconfigRoot = await findProjectRoot(projectDir);
|
|
306
|
-
const
|
|
977
|
+
const absoluteFilePath = path.isAbsolute(filePath) ? filePath : path.resolve(tsconfigRoot, filePath);
|
|
978
|
+
const relativeFilePath = path.relative(tsconfigRoot, absoluteFilePath).replace(/\\/g, '/');
|
|
307
979
|
const normalizedRelative = relativeFilePath.replace(/^client\//, '');
|
|
308
|
-
const result = await execa('npx', ['tsc', '--noEmit', '--skipLibCheck', '--pretty', 'false'], {
|
|
309
|
-
cwd: tsconfigRoot,
|
|
310
|
-
stdio: 'pipe',
|
|
311
|
-
reject: false,
|
|
312
|
-
});
|
|
313
980
|
|
|
314
|
-
|
|
981
|
+
debugTypeCheck(`[runTypeCheckForFile] tsconfigRoot: ${tsconfigRoot}`);
|
|
982
|
+
debugTypeCheck(`[runTypeCheckForFile] absoluteFilePath: ${absoluteFilePath}`);
|
|
983
|
+
debugTypeCheck(`[runTypeCheckForFile] relativeFilePath: ${relativeFilePath}`);
|
|
984
|
+
debugTypeCheck(`[runTypeCheckForFile] normalizedRelative: ${normalizedRelative}`);
|
|
985
|
+
|
|
986
|
+
const isGraphQLFile = await detectGraphQLFile(absoluteFilePath, relativeFilePath);
|
|
987
|
+
debugTypeCheck(`[runTypeCheckForFile] isGraphQLFile: ${isGraphQLFile}`);
|
|
988
|
+
|
|
989
|
+
const output = await executeTypeCheck(tsconfigRoot, false);
|
|
315
990
|
debugTypeCheck(`[runTypeCheckForFile] Finished tsc in ${(performance.now() - start).toFixed(2)} ms`);
|
|
991
|
+
debugTypeCheck(`[runTypeCheckForFile] Total output length: ${output.length} chars`);
|
|
992
|
+
debugTypeCheck(`[runTypeCheckForFile] Output preview (first 2000 chars):\n${output.substring(0, 2000)}`);
|
|
993
|
+
|
|
994
|
+
const filteredErrors = filterErrorsForFile(output, relativeFilePath, normalizedRelative, filePath);
|
|
995
|
+
|
|
996
|
+
const graphqlStrictErrors = isGraphQLFile
|
|
997
|
+
? await runGraphQLStrictCheck(tsconfigRoot, relativeFilePath, normalizedRelative, filePath)
|
|
998
|
+
: '';
|
|
999
|
+
|
|
1000
|
+
const formattedErrors = formatTypeCheckErrors(filteredErrors, graphqlStrictErrors);
|
|
316
1001
|
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const matchesTarget =
|
|
325
|
-
line.includes(relativeFilePath) ||
|
|
326
|
-
line.includes(normalizedRelative) ||
|
|
327
|
-
line.includes(path.basename(filePath));
|
|
328
|
-
return hasError && notNodeModules && matchesTarget;
|
|
329
|
-
})
|
|
330
|
-
.join('\n');
|
|
331
|
-
|
|
332
|
-
if (filteredErrors.trim().length === 0) return { success: true, errors: '' };
|
|
333
|
-
return { success: false, errors: filteredErrors };
|
|
1002
|
+
if (!output.includes('error TS') && formattedErrors.trim().length === 0) {
|
|
1003
|
+
debugTypeCheck(`[runTypeCheckForFile] No errors found`);
|
|
1004
|
+
return { success: true, errors: '' };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (formattedErrors.trim().length === 0) return { success: true, errors: '' };
|
|
1008
|
+
return { success: false, errors: formattedErrors };
|
|
334
1009
|
} catch (err) {
|
|
335
1010
|
const message = err instanceof Error ? err.message : String(err);
|
|
336
1011
|
return { success: false, errors: message };
|
|
337
1012
|
}
|
|
338
1013
|
}
|
|
339
1014
|
|
|
1015
|
+
async function detectGraphQLFile(absoluteFilePath: string, relativeFilePath: string): Promise<boolean> {
|
|
1016
|
+
const isInGraphQLDir =
|
|
1017
|
+
relativeFilePath.includes('/graphql/') ||
|
|
1018
|
+
relativeFilePath.includes('/gql/') ||
|
|
1019
|
+
relativeFilePath.includes('\\graphql\\') ||
|
|
1020
|
+
relativeFilePath.includes('\\gql\\');
|
|
1021
|
+
|
|
1022
|
+
if (isInGraphQLDir) return true;
|
|
1023
|
+
|
|
1024
|
+
try {
|
|
1025
|
+
const content = await fs.readFile(absoluteFilePath, 'utf-8');
|
|
1026
|
+
return (
|
|
1027
|
+
content.includes("from '@/gql/graphql'") ||
|
|
1028
|
+
content.includes('from "@/gql/graphql"') ||
|
|
1029
|
+
content.includes("from '@/graphql/") ||
|
|
1030
|
+
content.includes('from "@/graphql/') ||
|
|
1031
|
+
content.includes('@apollo/client')
|
|
1032
|
+
);
|
|
1033
|
+
} catch {
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function filterErrorsForFile(
|
|
1039
|
+
output: string,
|
|
1040
|
+
relativeFilePath: string,
|
|
1041
|
+
normalizedRelative: string,
|
|
1042
|
+
filePath: string,
|
|
1043
|
+
): string {
|
|
1044
|
+
const allLines = output.split('\n');
|
|
1045
|
+
const errorLines = allLines.filter((line) => line.includes('error TS'));
|
|
1046
|
+
debugTypeCheck(`[filterErrorsForFile] Total lines: ${allLines.length}, Error lines: ${errorLines.length}`);
|
|
1047
|
+
|
|
1048
|
+
const filteredErrors = output
|
|
1049
|
+
.split('\n')
|
|
1050
|
+
.filter((line) => {
|
|
1051
|
+
const hasError = line.includes('error TS');
|
|
1052
|
+
const notNodeModules = !line.includes('node_modules');
|
|
1053
|
+
const matchesTarget =
|
|
1054
|
+
line.includes(relativeFilePath) || line.includes(normalizedRelative) || line.includes(path.basename(filePath));
|
|
1055
|
+
|
|
1056
|
+
if (hasError) {
|
|
1057
|
+
debugTypeCheck(`[filterErrorsForFile] Checking error line: ${line.substring(0, 150)}`);
|
|
1058
|
+
debugTypeCheck(
|
|
1059
|
+
`[filterErrorsForFile] hasError: ${hasError}, notNodeModules: ${notNodeModules}, matchesTarget: ${matchesTarget}`,
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
return hasError && notNodeModules && matchesTarget;
|
|
1064
|
+
})
|
|
1065
|
+
.join('\n');
|
|
1066
|
+
|
|
1067
|
+
debugTypeCheck(`[filterErrorsForFile] Filtered errors length: ${filteredErrors.length} chars`);
|
|
1068
|
+
return filteredErrors;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function formatTypeCheckErrors(regularErrors: string, graphqlStrictErrors: string): string {
|
|
1072
|
+
let formattedOutput = '';
|
|
1073
|
+
|
|
1074
|
+
if (graphqlStrictErrors.trim().length > 0) {
|
|
1075
|
+
formattedOutput += '## GraphQL Schema Type Errors (strict mode)\n\n';
|
|
1076
|
+
formattedOutput += 'These errors indicate fields/properties that violate GraphQL schema type contracts.\n';
|
|
1077
|
+
formattedOutput += 'Common causes:\n';
|
|
1078
|
+
formattedOutput += '- Using fields not defined in the GraphQL schema\n';
|
|
1079
|
+
formattedOutput += '- Incorrect property types in mutation variables\n';
|
|
1080
|
+
formattedOutput += '- Missing required fields in input types\n\n';
|
|
1081
|
+
formattedOutput += graphqlStrictErrors;
|
|
1082
|
+
formattedOutput += '\n\n';
|
|
1083
|
+
formattedOutput += '**Fix**: Check @/gql/graphql.ts for the exact type definition and valid fields.\n\n';
|
|
1084
|
+
formattedOutput += '---\n\n';
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (regularErrors.trim().length > 0) {
|
|
1088
|
+
formattedOutput += '## TypeScript Errors\n\n';
|
|
1089
|
+
formattedOutput += regularErrors;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
return formattedOutput.trim();
|
|
1093
|
+
}
|
|
1094
|
+
|
|
340
1095
|
async function findProjectRoot(startDir: string): Promise<string> {
|
|
341
1096
|
let dir = startDir;
|
|
342
1097
|
while (dir !== path.dirname(dir)) {
|
|
@@ -351,7 +1106,75 @@ async function findProjectRoot(startDir: string): Promise<string> {
|
|
|
351
1106
|
throw new Error('Could not find project root (no package.json or tsconfig.json found)');
|
|
352
1107
|
}
|
|
353
1108
|
|
|
354
|
-
|
|
1109
|
+
interface ComponentUsageInfo {
|
|
1110
|
+
usageExamples: Array<{ file: string; snippet: string }>;
|
|
1111
|
+
requiresChildren: boolean;
|
|
1112
|
+
detectedProps: string[];
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
async function extractComponentUsageFromScaffolds(
|
|
1116
|
+
componentName: string,
|
|
1117
|
+
componentType: 'molecule' | 'organism',
|
|
1118
|
+
projectDir: string,
|
|
1119
|
+
): Promise<ComponentUsageInfo> {
|
|
1120
|
+
const usageExamples: Array<{ file: string; snippet: string }> = [];
|
|
1121
|
+
let requiresChildren = false;
|
|
1122
|
+
const detectedProps: string[] = [];
|
|
1123
|
+
|
|
1124
|
+
const searchDirs: string[] = [];
|
|
1125
|
+
if (componentType === 'molecule') {
|
|
1126
|
+
searchDirs.push(path.join(projectDir, 'src', 'components', 'organisms'));
|
|
1127
|
+
searchDirs.push(path.join(projectDir, 'src', 'components', 'pages'));
|
|
1128
|
+
} else if (componentType === 'organism') {
|
|
1129
|
+
searchDirs.push(path.join(projectDir, 'src', 'components', 'pages'));
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
for (const dir of searchDirs) {
|
|
1133
|
+
try {
|
|
1134
|
+
const files = await fs.readdir(dir);
|
|
1135
|
+
for (const file of files) {
|
|
1136
|
+
if (!file.endsWith('.tsx')) continue;
|
|
1137
|
+
|
|
1138
|
+
const filePath = path.join(dir, file);
|
|
1139
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
1140
|
+
|
|
1141
|
+
const openTagPattern = new RegExp(`<${componentName}(?:\\s|>)`, 'g');
|
|
1142
|
+
if (!openTagPattern.test(content)) continue;
|
|
1143
|
+
|
|
1144
|
+
const withChildrenPattern = new RegExp(`<${componentName}[^>]*>([\\s\\S]*?)</${componentName}>`, 'g');
|
|
1145
|
+
|
|
1146
|
+
const hasChildren = withChildrenPattern.test(content);
|
|
1147
|
+
if (hasChildren) {
|
|
1148
|
+
requiresChildren = true;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const lines = content.split('\n');
|
|
1152
|
+
const usageLineIndexes: number[] = [];
|
|
1153
|
+
lines.forEach((line, idx) => {
|
|
1154
|
+
if (line.includes(`<${componentName}`)) {
|
|
1155
|
+
usageLineIndexes.push(idx);
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
if (usageLineIndexes.length > 0) {
|
|
1160
|
+
const lineIdx = usageLineIndexes[0];
|
|
1161
|
+
const start = Math.max(0, lineIdx - 2);
|
|
1162
|
+
const end = Math.min(lines.length, lineIdx + 8);
|
|
1163
|
+
const snippet = lines.slice(start, end).join('\n');
|
|
1164
|
+
usageExamples.push({
|
|
1165
|
+
file: path.relative(projectDir, filePath),
|
|
1166
|
+
snippet,
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
} catch {
|
|
1171
|
+
// ignore read errors
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
return { usageExamples, requiresChildren, detectedProps };
|
|
1176
|
+
}
|
|
1177
|
+
|
|
355
1178
|
function makeBasePrompt(
|
|
356
1179
|
componentType: string,
|
|
357
1180
|
componentName: string,
|
|
@@ -360,35 +1183,12 @@ function makeBasePrompt(
|
|
|
360
1183
|
projectConfig: Record<string, string>,
|
|
361
1184
|
designSystemReference: string,
|
|
362
1185
|
dependencySources: Record<string, string>,
|
|
1186
|
+
usageInfo: ComponentUsageInfo,
|
|
363
1187
|
): string {
|
|
364
1188
|
const hasScaffold = Boolean(existingScaffold?.trim());
|
|
365
1189
|
|
|
366
|
-
const gqlFiles: Record<string, string> = {};
|
|
367
|
-
const graphqlFiles: Record<string, string> = {};
|
|
368
|
-
const otherFiles: Record<string, string> = {};
|
|
369
|
-
|
|
370
|
-
for (const [filePath, content] of Object.entries(projectConfig)) {
|
|
371
|
-
const lower = filePath.toLowerCase();
|
|
372
|
-
if (lower.includes('src/gql/')) gqlFiles[filePath] = content;
|
|
373
|
-
else if (lower.includes('src/graphql/')) graphqlFiles[filePath] = content;
|
|
374
|
-
else otherFiles[filePath] = content;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const queriesFile = Object.entries(graphqlFiles).find(([n]) => n.endsWith('queries.ts'))?.[1] ?? '';
|
|
378
|
-
const mutationsFile = Object.entries(graphqlFiles).find(([n]) => n.endsWith('mutations.ts'))?.[1] ?? '';
|
|
379
|
-
|
|
380
|
-
const gqlSection =
|
|
381
|
-
Object.entries(gqlFiles)
|
|
382
|
-
.map(([p, c]) => `### ${p}\n${c}`)
|
|
383
|
-
.join('\n\n') || '(No gql folder found)';
|
|
384
|
-
|
|
385
|
-
const graphqlSection =
|
|
386
|
-
Object.entries(graphqlFiles)
|
|
387
|
-
.map(([p, c]) => `### ${p}\n${c}`)
|
|
388
|
-
.join('\n\n') || '(No graphql folder found)';
|
|
389
|
-
|
|
390
1190
|
const configSection =
|
|
391
|
-
Object.entries(
|
|
1191
|
+
Object.entries(projectConfig)
|
|
392
1192
|
.map(([p, c]) => `### ${p}\n${c}`)
|
|
393
1193
|
.join('\n\n') || '(No additional config files)';
|
|
394
1194
|
|
|
@@ -402,7 +1202,7 @@ function makeBasePrompt(
|
|
|
402
1202
|
.join('\n\n') || '(No dependencies found)';
|
|
403
1203
|
|
|
404
1204
|
return `
|
|
405
|
-
#
|
|
1205
|
+
# Implement ${componentName} (${componentType})
|
|
406
1206
|
|
|
407
1207
|
You are a senior frontend engineer specializing in **React + TypeScript + Apollo Client**.
|
|
408
1208
|
Your task is to build a visually excellent, type-safe, and production-ready ${componentType} component.
|
|
@@ -421,55 +1221,85 @@ Your component must:
|
|
|
421
1221
|
|
|
422
1222
|
---
|
|
423
1223
|
|
|
1224
|
+
## IA Schema
|
|
1225
|
+
${JSON.stringify(componentDef, null, 2)}
|
|
1226
|
+
|
|
1227
|
+
---
|
|
1228
|
+
|
|
424
1229
|
## Project Context
|
|
425
1230
|
|
|
426
|
-
**File Path:** src/components/${componentType}/${componentName}.tsx
|
|
427
1231
|
**Purpose:** A reusable UI element connected to the GraphQL layer and design system.
|
|
428
1232
|
|
|
429
|
-
|
|
430
|
-
${JSON.stringify(componentDef, null, 2)}
|
|
1233
|
+
## Component Scaffold
|
|
431
1234
|
|
|
432
|
-
|
|
433
|
-
|
|
1235
|
+
${
|
|
1236
|
+
hasScaffold
|
|
1237
|
+
? `The scaffold below contains:
|
|
1238
|
+
- Import statements for all dependencies (use these exact imports)
|
|
1239
|
+
- Type guidance comments showing GraphQL queries/mutations/enums to use
|
|
1240
|
+
- Specs describing required functionality
|
|
1241
|
+
- Component structure to implement
|
|
434
1242
|
|
|
435
|
-
|
|
436
|
-
${designSystemBlock}
|
|
1243
|
+
**CRITICAL**: Follow the import statements and type guidance comments in the scaffold exactly.
|
|
437
1244
|
|
|
438
|
-
|
|
439
|
-
|
|
1245
|
+
${existingScaffold}`
|
|
1246
|
+
: '(No existing scaffold found - create component from scratch)'
|
|
1247
|
+
}
|
|
440
1248
|
|
|
441
|
-
|
|
442
|
-
${graphqlSection}
|
|
1249
|
+
---
|
|
443
1250
|
|
|
444
|
-
|
|
445
|
-
${
|
|
1251
|
+
## Design System
|
|
1252
|
+
${designSystemBlock}
|
|
446
1253
|
|
|
447
|
-
|
|
448
|
-
${mutationsFile || '(mutations.ts not found)'}
|
|
1254
|
+
---
|
|
449
1255
|
|
|
450
|
-
|
|
451
|
-
${gqlSection}
|
|
1256
|
+
## Available Dependencies
|
|
452
1257
|
|
|
453
|
-
|
|
454
|
-
${configSection}
|
|
1258
|
+
${dependencySection}
|
|
455
1259
|
|
|
456
1260
|
---
|
|
457
1261
|
|
|
458
|
-
##
|
|
1262
|
+
## Project Configuration
|
|
1263
|
+
${configSection}
|
|
459
1264
|
|
|
460
|
-
|
|
461
|
-
- Explicitly type all props, state, and GraphQL responses.
|
|
462
|
-
- Avoid \`any\` — prefer discriminated unions, interfaces, and generics.
|
|
1265
|
+
---
|
|
463
1266
|
|
|
464
|
-
|
|
465
|
-
- Never call setState during render.
|
|
466
|
-
- Always use dependency arrays in effects.
|
|
467
|
-
- Memoize computed values and callbacks.
|
|
468
|
-
- Keep rendering pure and predictable.
|
|
1267
|
+
## Implementation Rules
|
|
469
1268
|
|
|
470
|
-
**
|
|
471
|
-
-
|
|
472
|
-
-
|
|
1269
|
+
**Type Safety**
|
|
1270
|
+
- No \`any\` or \`as SomeType\` - type correctly
|
|
1271
|
+
- Import types from dependencies - never redefine them locally
|
|
1272
|
+
- Type all props, state, and GraphQL responses explicitly
|
|
1273
|
+
|
|
1274
|
+
**Imports**
|
|
1275
|
+
- Use exact imports from scaffold and dependencies section
|
|
1276
|
+
- Pattern: \`@/components/{type}/{component}\`
|
|
1277
|
+
- Never use relative paths (\`../\`)
|
|
1278
|
+
- Only use packages from package.json shown above
|
|
1279
|
+
|
|
1280
|
+
**GraphQL (if applicable)**
|
|
1281
|
+
- **CRITICAL**: NEVER use inline gql template literals or import gql from @apollo/client
|
|
1282
|
+
- **CRITICAL**: ALWAYS import pre-generated operations from @/graphql/queries or @/graphql/mutations
|
|
1283
|
+
- Follow type guidance comments in scaffold for exact query/mutation names
|
|
1284
|
+
- Use pattern: \`const { data } = useQuery(QueryName)\` where QueryName is imported from @/graphql/queries
|
|
1285
|
+
- Use pattern: \`const [mutate, { loading }] = useMutation(MutationName, { refetchQueries: [...], awaitRefetchQueries: true })\`
|
|
1286
|
+
- Access enum values: \`EnumName.Value\` (e.g., \`TodoStateStatus.Pending\`)
|
|
1287
|
+
- **CRITICAL**: ALL mutations return \`{ success: Boolean!, error: { type: String!, message: String } }\`
|
|
1288
|
+
- **CRITICAL**: ALWAYS check \`data?.mutationName?.success\` before considering mutation successful
|
|
1289
|
+
- **CRITICAL**: ALWAYS handle \`data?.mutationName?.error?.message\` when \`success\` is false
|
|
1290
|
+
- **CRITICAL**: ALWAYS use \`loading\` state to disable buttons during mutations: \`disabled={loading}\`
|
|
1291
|
+
- **CRITICAL**: ALWAYS wrap mutations in try-catch for network errors
|
|
1292
|
+
- **CRITICAL**: Use \`awaitRefetchQueries: true\` to prevent race conditions with polling queries
|
|
1293
|
+
|
|
1294
|
+
**React Best Practices**
|
|
1295
|
+
- No setState during render
|
|
1296
|
+
- Include dependency arrays in useEffect
|
|
1297
|
+
- Use optional chaining (?.) and nullish coalescing (??)
|
|
1298
|
+
${
|
|
1299
|
+
usageInfo.requiresChildren
|
|
1300
|
+
? `\n**CRITICAL**: This component MUST accept \`children?: React.ReactNode\` prop based on parent usage patterns.`
|
|
1301
|
+
: ''
|
|
1302
|
+
}
|
|
473
1303
|
|
|
474
1304
|
**Visual & UX Quality**
|
|
475
1305
|
- Perfect spacing and alignment using Tailwind or the design system tokens.
|
|
@@ -486,9 +1316,10 @@ ${configSection}
|
|
|
486
1316
|
- Match button, card, and badge styles with existing components.
|
|
487
1317
|
|
|
488
1318
|
**Prohibited**
|
|
489
|
-
- No placeholder data
|
|
490
|
-
- No new external packages.
|
|
491
|
-
- No
|
|
1319
|
+
- No placeholder data or TODOs
|
|
1320
|
+
- No new external packages not in package.json
|
|
1321
|
+
- No reimplementing dependencies inline
|
|
1322
|
+
- No redefining types that dependencies export
|
|
492
1323
|
|
|
493
1324
|
---
|
|
494
1325
|
|
|
@@ -510,51 +1341,170 @@ ${configSection}
|
|
|
510
1341
|
|
|
511
1342
|
---
|
|
512
1343
|
|
|
513
|
-
**
|
|
514
|
-
Return only the complete \`.tsx\` source code for this component — no markdown fences, commentary, or extra text.
|
|
1344
|
+
**Output**: Return ONLY the complete .tsx source code - no markdown fences or commentary.
|
|
515
1345
|
`.trim();
|
|
516
1346
|
}
|
|
517
1347
|
|
|
518
|
-
function makeImplementPrompt(basePrompt: string): string {
|
|
519
|
-
|
|
1348
|
+
function makeImplementPrompt(basePrompt: string, graphqlFiles?: Record<string, string>): string {
|
|
1349
|
+
const hasGraphQLFiles = graphqlFiles !== undefined && Object.keys(graphqlFiles).length > 0;
|
|
1350
|
+
const graphqlSection = hasGraphQLFiles
|
|
1351
|
+
? `
|
|
1352
|
+
## GraphQL Type Definitions (Source of Truth)
|
|
1353
|
+
|
|
1354
|
+
**CRITICAL**: Use these exact TypeScript definitions for GraphQL types, queries, and mutations.
|
|
1355
|
+
|
|
1356
|
+
${Object.entries(graphqlFiles)
|
|
1357
|
+
.map(([filePath, content]) => `### ${filePath}\n\`\`\`typescript\n${content}\n\`\`\``)
|
|
1358
|
+
.join('\n\n')}
|
|
520
1359
|
|
|
521
1360
|
---
|
|
522
1361
|
|
|
523
|
-
|
|
524
|
-
|
|
1362
|
+
`
|
|
1363
|
+
: '';
|
|
1364
|
+
|
|
1365
|
+
return `${basePrompt}
|
|
1366
|
+
|
|
1367
|
+
${graphqlSection}---
|
|
1368
|
+
|
|
1369
|
+
Begin directly with import statements and end with the export statement.
|
|
525
1370
|
Do not include markdown fences, comments, or explanations — only the valid .tsx file content.
|
|
526
1371
|
`.trim();
|
|
527
1372
|
}
|
|
528
1373
|
|
|
1374
|
+
function validateComponentImport(componentType: string, filename: string, registry: ComponentRegistry): string | null {
|
|
1375
|
+
const importPath = `@/components/${componentType}/${filename}`;
|
|
1376
|
+
const exists = Array.from(registry.values()).some(
|
|
1377
|
+
(entry) => entry.type === componentType && entry.actualFilename === filename,
|
|
1378
|
+
);
|
|
1379
|
+
|
|
1380
|
+
if (exists) {
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const suggestions = Array.from(registry.values())
|
|
1385
|
+
.filter((entry) => entry.actualFilename.includes(filename) || filename.includes(entry.actualFilename))
|
|
1386
|
+
.map((entry) => `@/components/${entry.type}/${entry.actualFilename}`)
|
|
1387
|
+
.slice(0, 3);
|
|
1388
|
+
|
|
1389
|
+
if (suggestions.length > 0) {
|
|
1390
|
+
return `Import not found: ${importPath}\nDid you mean: ${suggestions.join(', ')}?`;
|
|
1391
|
+
}
|
|
1392
|
+
return `Import not found: ${importPath}`;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function validateNonComponentImport(importPath: string): string | null {
|
|
1396
|
+
if (importPath.startsWith('@/store/')) {
|
|
1397
|
+
return `Invalid import: ${importPath}\nThis project uses Apollo Client with GraphQL. Check the GraphQL files in context for available queries and mutations.`;
|
|
1398
|
+
}
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function validateImports(code: string, registry: ComponentRegistry): { valid: boolean; errors: string[] } {
|
|
1403
|
+
const errors: string[] = [];
|
|
1404
|
+
|
|
1405
|
+
const componentImportPattern = /import\s+[^'"]*from\s+['"]@\/components\/([^/]+)\/([^'"]+)['"]/g;
|
|
1406
|
+
let match;
|
|
1407
|
+
while ((match = componentImportPattern.exec(code)) !== null) {
|
|
1408
|
+
const error = validateComponentImport(match[1], match[2], registry);
|
|
1409
|
+
if (error !== null) {
|
|
1410
|
+
errors.push(error);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const allImportPattern = /import\s+[^'"]*from\s+['"](@\/[^'"]+)['"]/g;
|
|
1415
|
+
while ((match = allImportPattern.exec(code)) !== null) {
|
|
1416
|
+
const error = validateNonComponentImport(match[1]);
|
|
1417
|
+
if (error !== null) {
|
|
1418
|
+
errors.push(error);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
return { valid: errors.length === 0, errors };
|
|
1423
|
+
}
|
|
1424
|
+
|
|
529
1425
|
function makeRetryPrompt(
|
|
530
|
-
basePrompt: string,
|
|
531
|
-
componentType: string,
|
|
532
1426
|
componentName: string,
|
|
533
|
-
previousCode: string,
|
|
534
1427
|
previousErrors: string,
|
|
1428
|
+
composition: string[],
|
|
1429
|
+
dependencySummary: string,
|
|
1430
|
+
importValidationErrors: string[],
|
|
1431
|
+
description: string,
|
|
1432
|
+
existingScaffold: string,
|
|
1433
|
+
packageJson: string,
|
|
1434
|
+
graphqlFiles?: Record<string, string>,
|
|
535
1435
|
): string {
|
|
1436
|
+
const compositionHint = composition.length > 0 ? `\n**Required Components**: ${composition.join(', ')}\n` : '';
|
|
1437
|
+
|
|
1438
|
+
const importErrorsHint =
|
|
1439
|
+
importValidationErrors.length > 0
|
|
1440
|
+
? `\n**Import Errors**:\n${importValidationErrors.map((err) => `- ${err}`).join('\n')}\n`
|
|
1441
|
+
: '';
|
|
1442
|
+
|
|
1443
|
+
const hasScaffold = Boolean(existingScaffold?.trim());
|
|
1444
|
+
const scaffoldSection = hasScaffold
|
|
1445
|
+
? `
|
|
1446
|
+
## Scaffold with Type Guidance
|
|
1447
|
+
|
|
1448
|
+
**CRITICAL**: Follow the type guidance comments in the scaffold for exact GraphQL operation names, enum values, and import patterns.
|
|
1449
|
+
|
|
1450
|
+
${existingScaffold}
|
|
1451
|
+
|
|
1452
|
+
---
|
|
1453
|
+
`
|
|
1454
|
+
: '';
|
|
1455
|
+
|
|
1456
|
+
const hasGraphQLFiles = graphqlFiles !== undefined && Object.keys(graphqlFiles).length > 0;
|
|
1457
|
+
const graphqlSection = hasGraphQLFiles
|
|
1458
|
+
? `
|
|
1459
|
+
## GraphQL Type Definitions (Source of Truth)
|
|
1460
|
+
|
|
1461
|
+
**CRITICAL**: Use these exact TypeScript definitions for GraphQL types, queries, and mutations.
|
|
1462
|
+
|
|
1463
|
+
${Object.entries(graphqlFiles)
|
|
1464
|
+
.map(([filePath, content]) => `### ${filePath}\n\`\`\`typescript\n${content}\n\`\`\``)
|
|
1465
|
+
.join('\n\n')}
|
|
1466
|
+
|
|
1467
|
+
---
|
|
1468
|
+
`
|
|
1469
|
+
: '';
|
|
1470
|
+
|
|
536
1471
|
return `
|
|
537
|
-
${
|
|
1472
|
+
# Fix TypeScript Errors: ${componentName}
|
|
1473
|
+
|
|
1474
|
+
${description ? `**Description**: ${description}\n` : ''}
|
|
1475
|
+
${compositionHint}
|
|
1476
|
+
${scaffoldSection}
|
|
1477
|
+
${graphqlSection}
|
|
1478
|
+
## Project Dependencies (package.json)
|
|
1479
|
+
|
|
1480
|
+
**CRITICAL**: Only import packages that exist in the dependencies below.
|
|
1481
|
+
|
|
1482
|
+
${packageJson}
|
|
538
1483
|
|
|
539
1484
|
---
|
|
540
1485
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
1486
|
+
## Available Components
|
|
1487
|
+
|
|
1488
|
+
${dependencySummary}
|
|
1489
|
+
|
|
1490
|
+
---
|
|
1491
|
+
|
|
1492
|
+
## Errors to Fix
|
|
544
1493
|
|
|
545
|
-
**Errors**
|
|
546
1494
|
${previousErrors}
|
|
547
1495
|
|
|
548
|
-
|
|
549
|
-
|
|
1496
|
+
${importErrorsHint}
|
|
1497
|
+
**Hints**:
|
|
1498
|
+
- Follow scaffold's type guidance comments for exact operation names and enum values
|
|
1499
|
+
${hasGraphQLFiles ? '- Use the GraphQL Type Definitions section above for exact types, interfaces, and enums' : '- Check @/gql/graphql.ts for GraphQL type definitions'}
|
|
1500
|
+
- **CRITICAL**: NEVER use inline \`gql\` template literals - ALWAYS import from @/graphql/queries or @/graphql/mutations
|
|
1501
|
+
- Use pattern: \`mutate({ variables: { input: InputType } })\`
|
|
1502
|
+
- Import pattern: \`@/components/{type}/{component}\`
|
|
1503
|
+
- Import types from dependencies - never redefine them
|
|
550
1504
|
|
|
551
1505
|
---
|
|
552
1506
|
|
|
553
|
-
|
|
554
|
-
- Fix only TypeScript or import errors.
|
|
555
|
-
- Do not change working logic or structure.
|
|
556
|
-
- Keep eslint directives and formatting intact.
|
|
557
|
-
- Return the corrected \`.tsx\` file only, with no markdown fences or commentary.
|
|
1507
|
+
**Output**: Return ONLY the corrected ${componentName}.tsx code - no markdown fences or commentary.
|
|
558
1508
|
`.trim();
|
|
559
1509
|
}
|
|
560
1510
|
|
|
@@ -581,6 +1531,77 @@ function isValidCollection(collection: unknown): collection is { items: Record<s
|
|
|
581
1531
|
return typeof items === 'object' && items !== null;
|
|
582
1532
|
}
|
|
583
1533
|
|
|
1534
|
+
async function buildComponentRegistry(projectDir: string): Promise<ComponentRegistry> {
|
|
1535
|
+
const registry: ComponentRegistry = new Map();
|
|
1536
|
+
const types: Array<'atoms' | 'molecules' | 'organisms'> = ['atoms', 'molecules', 'organisms'];
|
|
1537
|
+
|
|
1538
|
+
for (const type of types) {
|
|
1539
|
+
const dir = path.join(projectDir, 'src', 'components', type);
|
|
1540
|
+
try {
|
|
1541
|
+
const files = await fs.readdir(dir);
|
|
1542
|
+
|
|
1543
|
+
for (const file of files) {
|
|
1544
|
+
if (!file.endsWith('.tsx')) continue;
|
|
1545
|
+
|
|
1546
|
+
const fullPath = path.join(dir, file);
|
|
1547
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
1548
|
+
const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
|
|
1549
|
+
|
|
1550
|
+
const exportedTypes = extractAllExportedTypes(sourceFile);
|
|
1551
|
+
const exportedComponents = extractExportedComponentNames(sourceFile);
|
|
1552
|
+
const allExports = [...new Set([...exportedTypes, ...exportedComponents])];
|
|
1553
|
+
|
|
1554
|
+
if (allExports.length === 0) continue;
|
|
1555
|
+
|
|
1556
|
+
const actualFilename = file.replace('.tsx', '');
|
|
1557
|
+
|
|
1558
|
+
for (const exportName of allExports) {
|
|
1559
|
+
registry.set(exportName, {
|
|
1560
|
+
name: exportName,
|
|
1561
|
+
actualFilename,
|
|
1562
|
+
type,
|
|
1563
|
+
exports: allExports,
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
debugProcess(`[buildComponentRegistry] Could not read ${type} directory: ${String(error)}`);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
debugProcess(`[buildComponentRegistry] Indexed ${registry.size} components`);
|
|
1573
|
+
return registry;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function resolveDependenciesToRegistry(
|
|
1577
|
+
dependencies: Array<{ type: string; name: string }>,
|
|
1578
|
+
registry: ComponentRegistry,
|
|
1579
|
+
): {
|
|
1580
|
+
primary: ComponentRegistryEntry[];
|
|
1581
|
+
available: ComponentRegistryEntry[];
|
|
1582
|
+
} {
|
|
1583
|
+
const primary: ComponentRegistryEntry[] = [];
|
|
1584
|
+
|
|
1585
|
+
debugProcess(
|
|
1586
|
+
`[resolveDependenciesToRegistry] Processing ${dependencies.length} dependencies: ${dependencies.map((d) => `${d.type}/${d.name}`).join(', ')}`,
|
|
1587
|
+
);
|
|
1588
|
+
|
|
1589
|
+
for (const dep of dependencies) {
|
|
1590
|
+
const entry = registry.get(dep.name);
|
|
1591
|
+
if (entry !== undefined) {
|
|
1592
|
+
debugProcess(`[resolveDependenciesToRegistry] Found registry entry for ${dep.name}`);
|
|
1593
|
+
primary.push(entry);
|
|
1594
|
+
} else {
|
|
1595
|
+
debugProcess(`[resolveDependenciesToRegistry] No registry entry for ${dep.name}`);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
const available = Array.from(registry.values()).filter((entry) => entry.type === 'atoms');
|
|
1600
|
+
|
|
1601
|
+
debugProcess(`[resolveDependenciesToRegistry] Resolved ${primary.length} primary dependencies`);
|
|
1602
|
+
return { primary, available };
|
|
1603
|
+
}
|
|
1604
|
+
|
|
584
1605
|
async function readDesignSystem(
|
|
585
1606
|
providedPath: string,
|
|
586
1607
|
refs: { projectDir: string; iaSchemeDir: string },
|