@auto-engineer/component-implementer 0.11.15 → 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 +10 -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,7 +1,7 @@
|
|
|
1
|
-
// noinspection ExceptionCaughtLocallyJS
|
|
2
1
|
import { defineCommandHandler } from '@auto-engineer/message-bus';
|
|
3
2
|
import * as fs from 'fs/promises';
|
|
4
3
|
import * as path from 'path';
|
|
4
|
+
import * as ts from 'typescript';
|
|
5
5
|
import createDebug from 'debug';
|
|
6
6
|
import { callAI, loadScheme } from '../agent.js';
|
|
7
7
|
import { execa } from 'execa';
|
|
@@ -27,6 +27,10 @@ export const commandHandler = defineCommandHandler({
|
|
|
27
27
|
filePath: { description: 'Component file path', required: true },
|
|
28
28
|
componentName: { description: 'Name of component to implement', required: true },
|
|
29
29
|
failures: { description: 'Any failures from previous implementations', required: false },
|
|
30
|
+
aiOptions: {
|
|
31
|
+
description: 'AI generation options',
|
|
32
|
+
required: false,
|
|
33
|
+
},
|
|
30
34
|
},
|
|
31
35
|
examples: [
|
|
32
36
|
'$ auto implement:component --project-dir=./client --ia-scheme-dir=./.context --design-system-path=./design-system.md --component-type=molecule --component-name=SurveyCard',
|
|
@@ -43,89 +47,185 @@ export const commandHandler = defineCommandHandler({
|
|
|
43
47
|
return result;
|
|
44
48
|
},
|
|
45
49
|
});
|
|
46
|
-
|
|
50
|
+
async function loadComponentDataForImplementation(iaSchemeDir, componentType, componentName, projectDir, designSystemPath, filePath) {
|
|
51
|
+
const t1 = performance.now();
|
|
52
|
+
const scheme = await loadScheme(iaSchemeDir);
|
|
53
|
+
debugProcess(`[1] Loaded IA scheme in ${(performance.now() - t1).toFixed(2)} ms`);
|
|
54
|
+
if (!scheme)
|
|
55
|
+
throw new Error('IA scheme not found');
|
|
56
|
+
const pluralKey = `${componentType}s`;
|
|
57
|
+
const collection = scheme[pluralKey];
|
|
58
|
+
if (!isValidCollection(collection))
|
|
59
|
+
throw new Error(`Invalid IA schema structure for ${pluralKey}`);
|
|
60
|
+
const items = collection.items;
|
|
61
|
+
const componentDef = items[componentName];
|
|
62
|
+
if (!componentDef)
|
|
63
|
+
throw new Error(`Component ${componentType}:${componentName} not found in IA schema`);
|
|
64
|
+
const outPath = path.join(projectDir, '..', filePath);
|
|
65
|
+
const t2 = performance.now();
|
|
66
|
+
let existingScaffold = '';
|
|
67
|
+
try {
|
|
68
|
+
existingScaffold = await fs.readFile(outPath, 'utf-8');
|
|
69
|
+
debugProcess(`[2] Found existing scaffold in ${(performance.now() - t2).toFixed(2)} ms`);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
debugProcess(`[2] No existing scaffold found (${(performance.now() - t2).toFixed(2)} ms)`);
|
|
73
|
+
}
|
|
74
|
+
const t3 = performance.now();
|
|
75
|
+
const projectConfig = await readProjectContext(projectDir);
|
|
76
|
+
debugProcess(`[3] Loaded project context in ${(performance.now() - t3).toFixed(2)} ms`);
|
|
77
|
+
const t4 = performance.now();
|
|
78
|
+
const designSystemReference = await readDesignSystem(designSystemPath, { projectDir, iaSchemeDir });
|
|
79
|
+
debugProcess(`[4] Loaded design system reference in ${(performance.now() - t4).toFixed(2)} ms`);
|
|
80
|
+
const t5 = performance.now();
|
|
81
|
+
const registry = await buildComponentRegistry(projectDir);
|
|
82
|
+
debugProcess(`[5] Built component registry in ${(performance.now() - t5).toFixed(2)} ms`);
|
|
83
|
+
return {
|
|
84
|
+
scheme: scheme,
|
|
85
|
+
componentDef,
|
|
86
|
+
existingScaffold,
|
|
87
|
+
projectConfig,
|
|
88
|
+
designSystemReference,
|
|
89
|
+
registry,
|
|
90
|
+
outPath,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function loadDependencySources(scheme, componentType, componentName, projectDir, registry) {
|
|
94
|
+
const dependencyList = await resolveDependenciesRecursively(scheme, componentType, componentName);
|
|
95
|
+
debugProcess(`[6] Resolved ${dependencyList.length} dependencies for ${componentName}`);
|
|
96
|
+
const { primary: primaryDeps } = resolveDependenciesToRegistry(dependencyList, registry);
|
|
97
|
+
const dependencySources = {};
|
|
98
|
+
const allAtoms = Array.from(registry.entries())
|
|
99
|
+
.filter(([_, entry]) => entry.type === 'atoms')
|
|
100
|
+
.map(([name, _]) => name);
|
|
101
|
+
for (const atomName of allAtoms) {
|
|
102
|
+
const atomSource = await readComponentPropsInterface(projectDir, 'atoms', atomName, registry);
|
|
103
|
+
if (atomSource !== null) {
|
|
104
|
+
dependencySources[`atoms/${atomName}`] = atomSource;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const dep of primaryDeps) {
|
|
108
|
+
debugProcess(`[loadDependencySources] Attempting to read dependency ${dep.type}/${dep.name}`);
|
|
109
|
+
const depContent = await readComponentFullImplementation(projectDir, dep.type, dep.name, registry);
|
|
110
|
+
if (depContent != null) {
|
|
111
|
+
debugProcess(`[loadDependencySources] Successfully read full implementation for ${dep.type}/${dep.name}`);
|
|
112
|
+
dependencySources[`${dep.type}/${dep.name}`] = depContent;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
debugProcess(`[loadDependencySources] Failed to read implementation for ${dep.type}/${dep.name} (returned null)`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return dependencySources;
|
|
119
|
+
}
|
|
120
|
+
async function generateCodeWithRetryLoop(params) {
|
|
121
|
+
const { componentName, componentDef, basePrompt, composition, dependencySummary, projectConfig, graphqlFiles, outPath, projectDir, registry, existingScaffold, } = params;
|
|
122
|
+
let attempt = 1;
|
|
123
|
+
let code = '';
|
|
124
|
+
let lastErrors = '';
|
|
125
|
+
let lastImportErrors = [];
|
|
126
|
+
const maxAttempts = 3;
|
|
127
|
+
let currentMaxTokens = params.maxTokens;
|
|
128
|
+
const description = componentDef.description ?? '';
|
|
129
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
130
|
+
while (attempt <= maxAttempts) {
|
|
131
|
+
const genStart = performance.now();
|
|
132
|
+
const prompt = attempt === 1
|
|
133
|
+
? makeImplementPrompt(basePrompt, graphqlFiles)
|
|
134
|
+
: makeRetryPrompt(componentName, lastErrors, composition, dependencySummary, lastImportErrors, description, existingScaffold, projectConfig['package.json'] ?? '', graphqlFiles);
|
|
135
|
+
const promptPath = `/tmp/prompt-${componentName}-attempt-${attempt}.txt`;
|
|
136
|
+
await fs.writeFile(promptPath, prompt, 'utf-8');
|
|
137
|
+
debugProcess(`[DEBUG] Saved prompt to ${promptPath} (${prompt.length} chars)`);
|
|
138
|
+
const aiRaw = await callAI(prompt, { maxTokens: currentMaxTokens });
|
|
139
|
+
code = extractCodeBlock(aiRaw);
|
|
140
|
+
const isTruncated = detectTruncation(code);
|
|
141
|
+
if (isTruncated) {
|
|
142
|
+
const suggestedMaxTokens = Math.ceil(currentMaxTokens * 1.5);
|
|
143
|
+
debugProcess(`[WARNING] Truncation detected at attempt ${attempt}. Increasing maxTokens: ${currentMaxTokens} → ${suggestedMaxTokens}`);
|
|
144
|
+
currentMaxTokens = suggestedMaxTokens;
|
|
145
|
+
}
|
|
146
|
+
await fs.writeFile(outPath, code, 'utf-8');
|
|
147
|
+
debugProcess(`[6.${attempt}] AI output written (${code.length} chars, truncated: ${isTruncated}) in ${(performance.now() - genStart).toFixed(2)} ms`);
|
|
148
|
+
const importValidation = validateImports(code, registry);
|
|
149
|
+
lastImportErrors = importValidation.errors;
|
|
150
|
+
if (!importValidation.valid) {
|
|
151
|
+
debugProcess(`[WARN] Invalid imports detected: ${importValidation.errors.join('; ')}`);
|
|
152
|
+
}
|
|
153
|
+
const checkStart = performance.now();
|
|
154
|
+
const { success, errors } = await runTypeCheckForFile(projectDir, outPath);
|
|
155
|
+
debugTypeCheck(`[7.${attempt}] Type check in ${(performance.now() - checkStart).toFixed(2)} ms (success: ${success})`);
|
|
156
|
+
if (success) {
|
|
157
|
+
return code;
|
|
158
|
+
}
|
|
159
|
+
lastErrors = errors;
|
|
160
|
+
if (attempt === maxAttempts) {
|
|
161
|
+
const wasTruncated = detectTruncation(code);
|
|
162
|
+
const errorMessage = wasTruncated
|
|
163
|
+
? `Component generation failed after ${attempt} attempts due to output truncation.\n` +
|
|
164
|
+
`Final maxTokens used: ${currentMaxTokens}\n` +
|
|
165
|
+
`Suggestion: Increase aiOptions.maxTokens in your config (try ${Math.ceil(currentMaxTokens * 1.5)} or higher)\n\n` +
|
|
166
|
+
`TypeScript errors:\n${errors}`
|
|
167
|
+
: `Type errors persist after ${attempt} attempts:\n${errors}`;
|
|
168
|
+
throw new Error(errorMessage);
|
|
169
|
+
}
|
|
170
|
+
attempt += 1;
|
|
171
|
+
}
|
|
172
|
+
throw new Error('Unreachable state');
|
|
173
|
+
}
|
|
47
174
|
async function handleImplementComponentCommandInternal(command) {
|
|
48
175
|
const { projectDir, iaSchemeDir, designSystemPath, componentType, componentName, filePath } = command.data;
|
|
49
176
|
try {
|
|
50
177
|
const start = performance.now();
|
|
51
178
|
debugProcess(`Starting ${componentType}:${componentName}`);
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
debugProcess(`[2] Found existing scaffold in ${(performance.now() - t2).toFixed(2)} ms`);
|
|
179
|
+
const loadData = await loadComponentDataForImplementation(iaSchemeDir, componentType, componentName, projectDir, designSystemPath, filePath);
|
|
180
|
+
const dependencySources = await loadDependencySources(loadData.scheme, componentType, componentName, projectDir, loadData.registry);
|
|
181
|
+
const usageInfo = await extractComponentUsageFromScaffolds(componentName, componentType, projectDir);
|
|
182
|
+
debugProcess(`[extractComponentUsageFromScaffolds] Found ${usageInfo.usageExamples.length} usage examples, requiresChildren: ${usageInfo.requiresChildren}`);
|
|
183
|
+
const basePrompt = makeBasePrompt(componentType, componentName, loadData.componentDef, loadData.existingScaffold, loadData.projectConfig, loadData.designSystemReference, dependencySources, usageInfo);
|
|
184
|
+
const composition = extractComposition(loadData.componentDef);
|
|
185
|
+
const dependencySummary = Object.entries(dependencySources)
|
|
186
|
+
.map(([name, src]) => `### ${name}\n${src}`)
|
|
187
|
+
.join('\n\n') || '(No dependencies found)';
|
|
188
|
+
const hasOwnDataRequirements = hasDataRequirements(loadData.componentDef);
|
|
189
|
+
const hasParentDataRequirements = componentType === 'molecule' ? findParentDataRequirements(loadData.scheme, componentName) : false;
|
|
190
|
+
const needsGraphQLFiles = hasOwnDataRequirements || hasParentDataRequirements;
|
|
191
|
+
let graphqlFiles = {};
|
|
192
|
+
if (needsGraphQLFiles) {
|
|
193
|
+
const t6 = performance.now();
|
|
194
|
+
graphqlFiles = await readGraphQLFiles(projectDir);
|
|
195
|
+
const reason = hasOwnDataRequirements ? 'has own data requirements' : 'parent has data requirements';
|
|
196
|
+
debugProcess(`[6] Loaded GraphQL files for ${componentName} (${reason}) in ${(performance.now() - t6).toFixed(2)} ms`);
|
|
71
197
|
}
|
|
72
|
-
|
|
73
|
-
debugProcess(`[
|
|
74
|
-
}
|
|
75
|
-
const t3 = performance.now();
|
|
76
|
-
const projectConfig = await readAllTopLevelFiles(projectDir);
|
|
77
|
-
debugProcess(`[3] Loaded project + gql/graphql files in ${(performance.now() - t3).toFixed(2)} ms`);
|
|
78
|
-
const t4 = performance.now();
|
|
79
|
-
const designSystemReference = await readDesignSystem(designSystemPath, { projectDir, iaSchemeDir });
|
|
80
|
-
debugProcess(`[4] Loaded design system reference in ${(performance.now() - t4).toFixed(2)} ms`);
|
|
81
|
-
const dependencyList = await resolveDependenciesRecursively(scheme, componentType, componentName);
|
|
82
|
-
debugProcess(`[5] Resolved ${dependencyList.length} dependencies for ${componentName}`);
|
|
83
|
-
const dependencySources = {};
|
|
84
|
-
for (const dep of dependencyList) {
|
|
85
|
-
const depSource = await readComponentSource(projectDir, dep.type, dep.name);
|
|
86
|
-
if (depSource != null)
|
|
87
|
-
dependencySources[`${dep.type}/${dep.name}`] = depSource;
|
|
88
|
-
}
|
|
89
|
-
const basePrompt = makeBasePrompt(componentType, componentName, componentDef, existingScaffold, projectConfig, designSystemReference, dependencySources);
|
|
90
|
-
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
91
|
-
let attempt = 1;
|
|
92
|
-
let code = '';
|
|
93
|
-
let lastErrors = '';
|
|
94
|
-
const maxAttempts = 3;
|
|
95
|
-
while (attempt <= maxAttempts) {
|
|
96
|
-
const genStart = performance.now();
|
|
97
|
-
const prompt = attempt === 1
|
|
98
|
-
? makeImplementPrompt(basePrompt)
|
|
99
|
-
: makeRetryPrompt(basePrompt, componentType, componentName, code, lastErrors);
|
|
100
|
-
const aiRaw = await callAI(prompt);
|
|
101
|
-
code = extractCodeBlock(aiRaw);
|
|
102
|
-
await fs.writeFile(outPath, code, 'utf-8');
|
|
103
|
-
debugProcess(`[6.${attempt}] AI output written (${code.length} chars) in ${(performance.now() - genStart).toFixed(2)} ms`);
|
|
104
|
-
const checkStart = performance.now();
|
|
105
|
-
const { success, errors } = await runTypeCheckForFile(projectDir, outPath);
|
|
106
|
-
debugTypeCheck(`[7.${attempt}] Type check in ${(performance.now() - checkStart).toFixed(2)} ms (success: ${success})`);
|
|
107
|
-
if (success) {
|
|
108
|
-
debugResult(`[✓] Implementation succeeded in ${(performance.now() - start).toFixed(2)} ms total`);
|
|
109
|
-
return {
|
|
110
|
-
type: 'ComponentImplemented',
|
|
111
|
-
data: {
|
|
112
|
-
filePath: outPath,
|
|
113
|
-
componentType,
|
|
114
|
-
componentName,
|
|
115
|
-
composition: extractComposition(componentDef),
|
|
116
|
-
specs: extractSpecs(componentDef),
|
|
117
|
-
},
|
|
118
|
-
timestamp: new Date(),
|
|
119
|
-
requestId: command.requestId,
|
|
120
|
-
correlationId: command.correlationId,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
lastErrors = errors;
|
|
124
|
-
if (attempt === maxAttempts)
|
|
125
|
-
throw new Error(`Type errors persist after ${attempt} attempts:\n${errors}`);
|
|
126
|
-
attempt += 1;
|
|
198
|
+
else {
|
|
199
|
+
debugProcess(`[6] Skipped GraphQL files for ${componentName} (no data requirements)`);
|
|
127
200
|
}
|
|
128
|
-
|
|
201
|
+
await generateCodeWithRetryLoop({
|
|
202
|
+
componentName,
|
|
203
|
+
componentDef: loadData.componentDef,
|
|
204
|
+
basePrompt,
|
|
205
|
+
composition,
|
|
206
|
+
dependencySummary,
|
|
207
|
+
projectConfig: loadData.projectConfig,
|
|
208
|
+
graphqlFiles,
|
|
209
|
+
outPath: loadData.outPath,
|
|
210
|
+
projectDir,
|
|
211
|
+
registry: loadData.registry,
|
|
212
|
+
maxTokens: command.data.aiOptions?.maxTokens ?? 2000,
|
|
213
|
+
existingScaffold: loadData.existingScaffold,
|
|
214
|
+
});
|
|
215
|
+
debugResult(`[✓] Implementation succeeded in ${(performance.now() - start).toFixed(2)} ms total`);
|
|
216
|
+
return {
|
|
217
|
+
type: 'ComponentImplemented',
|
|
218
|
+
data: {
|
|
219
|
+
filePath: loadData.outPath,
|
|
220
|
+
componentType,
|
|
221
|
+
componentName,
|
|
222
|
+
composition: extractComposition(loadData.componentDef),
|
|
223
|
+
specs: extractSpecs(loadData.componentDef),
|
|
224
|
+
},
|
|
225
|
+
timestamp: new Date(),
|
|
226
|
+
requestId: command.requestId,
|
|
227
|
+
correlationId: command.correlationId,
|
|
228
|
+
};
|
|
129
229
|
}
|
|
130
230
|
catch (error) {
|
|
131
231
|
debug('[Error] Component implementation failed: %O', error);
|
|
@@ -143,23 +243,8 @@ async function handleImplementComponentCommandInternal(command) {
|
|
|
143
243
|
};
|
|
144
244
|
}
|
|
145
245
|
}
|
|
146
|
-
|
|
147
|
-
async function resolveDependenciesRecursively(scheme, type, name, visited = new Set()) {
|
|
148
|
-
const key = `${type}:${name}`;
|
|
149
|
-
if (visited.has(key))
|
|
150
|
-
return [];
|
|
151
|
-
visited.add(key);
|
|
152
|
-
const collection = scheme[`${type}s`];
|
|
153
|
-
//
|
|
154
|
-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
155
|
-
if (!collection || !isValidCollection(collection))
|
|
156
|
-
return [];
|
|
157
|
-
const def = collection.items[name];
|
|
158
|
-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
159
|
-
if (!def || typeof def !== 'object' || !('composition' in def))
|
|
160
|
-
return [];
|
|
246
|
+
async function resolveCompositionDependencies(scheme, composition, visited) {
|
|
161
247
|
const result = [];
|
|
162
|
-
const composition = def.composition;
|
|
163
248
|
for (const [subType, subNames] of Object.entries(composition)) {
|
|
164
249
|
if (!Array.isArray(subNames))
|
|
165
250
|
continue;
|
|
@@ -171,12 +256,277 @@ async function resolveDependenciesRecursively(scheme, type, name, visited = new
|
|
|
171
256
|
}
|
|
172
257
|
return result;
|
|
173
258
|
}
|
|
174
|
-
async function
|
|
175
|
-
const
|
|
259
|
+
async function resolveLayoutDependencies(scheme, layout, visited) {
|
|
260
|
+
const result = [];
|
|
261
|
+
if ('organisms' in layout && Array.isArray(layout.organisms)) {
|
|
262
|
+
for (const organismName of layout.organisms) {
|
|
263
|
+
result.push({ type: 'organisms', name: organismName });
|
|
264
|
+
const nested = await resolveDependenciesRecursively(scheme, 'organisms', organismName, visited);
|
|
265
|
+
result.push(...nested);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
function getComponentDefinitionFromScheme(scheme, type, name) {
|
|
271
|
+
const collection = scheme[`${type}s`];
|
|
272
|
+
if (collection === null || collection === undefined || !isValidCollection(collection))
|
|
273
|
+
return null;
|
|
274
|
+
const def = collection.items[name];
|
|
275
|
+
if (def === null || def === undefined || typeof def !== 'object')
|
|
276
|
+
return null;
|
|
277
|
+
return def;
|
|
278
|
+
}
|
|
279
|
+
async function resolveDependenciesRecursively(scheme, type, name, visited = new Set()) {
|
|
280
|
+
const key = `${type}:${name}`;
|
|
281
|
+
if (visited.has(key))
|
|
282
|
+
return [];
|
|
283
|
+
visited.add(key);
|
|
284
|
+
const def = getComponentDefinitionFromScheme(scheme, type, name);
|
|
285
|
+
if (def === null)
|
|
286
|
+
return [];
|
|
287
|
+
const result = [];
|
|
288
|
+
if ('composition' in def) {
|
|
289
|
+
const compositionDeps = await resolveCompositionDependencies(scheme, def.composition, visited);
|
|
290
|
+
result.push(...compositionDeps);
|
|
291
|
+
}
|
|
292
|
+
if ('layout' in def && typeof def.layout === 'object' && def.layout !== null) {
|
|
293
|
+
const layoutDeps = await resolveLayoutDependencies(scheme, def.layout, visited);
|
|
294
|
+
result.push(...layoutDeps);
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
function hasExportModifier(node) {
|
|
299
|
+
const modifiers = 'modifiers' in node ? node.modifiers : undefined;
|
|
300
|
+
return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
301
|
+
}
|
|
302
|
+
function extractFromVariableStatement(node, exports) {
|
|
303
|
+
node.declarationList.declarations.forEach((decl) => {
|
|
304
|
+
if (ts.isIdentifier(decl.name)) {
|
|
305
|
+
exports.push(decl.name.text);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function extractFromExportDeclaration(node, exports) {
|
|
310
|
+
if (node.exportClause !== undefined && ts.isNamedExports(node.exportClause)) {
|
|
311
|
+
node.exportClause.elements.forEach((element) => {
|
|
312
|
+
exports.push(element.name.text);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function extractExportedComponentNames(sourceFile) {
|
|
317
|
+
const exports = [];
|
|
318
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
319
|
+
if (ts.isVariableStatement(node) && hasExportModifier(node)) {
|
|
320
|
+
extractFromVariableStatement(node, exports);
|
|
321
|
+
}
|
|
322
|
+
else if (ts.isExportDeclaration(node)) {
|
|
323
|
+
extractFromExportDeclaration(node, exports);
|
|
324
|
+
}
|
|
325
|
+
else if (ts.isFunctionDeclaration(node) && hasExportModifier(node) && node.name !== undefined) {
|
|
326
|
+
exports.push(node.name.text);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
return exports;
|
|
330
|
+
}
|
|
331
|
+
function extractAllExportedTypes(sourceFile) {
|
|
332
|
+
const types = [];
|
|
333
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
334
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
335
|
+
const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
336
|
+
if (hasExport) {
|
|
337
|
+
types.push(node.name.text);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (ts.isTypeAliasDeclaration(node)) {
|
|
341
|
+
const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
342
|
+
if (hasExport) {
|
|
343
|
+
types.push(node.name.text);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
return types;
|
|
348
|
+
}
|
|
349
|
+
function extractTypeReferencesFromProps(node, sourceFile) {
|
|
350
|
+
const refs = new Set();
|
|
351
|
+
function visitType(typeNode) {
|
|
352
|
+
if (ts.isTypeReferenceNode(typeNode)) {
|
|
353
|
+
const typeName = typeNode.typeName.getText(sourceFile);
|
|
354
|
+
refs.add(typeName);
|
|
355
|
+
}
|
|
356
|
+
if (ts.isArrayTypeNode(typeNode)) {
|
|
357
|
+
visitType(typeNode.elementType);
|
|
358
|
+
}
|
|
359
|
+
if (ts.isUnionTypeNode(typeNode) || ts.isIntersectionTypeNode(typeNode)) {
|
|
360
|
+
typeNode.types.forEach(visitType);
|
|
361
|
+
}
|
|
362
|
+
if (ts.isFunctionTypeNode(typeNode)) {
|
|
363
|
+
typeNode.parameters.forEach((param) => {
|
|
364
|
+
if (param.type !== undefined)
|
|
365
|
+
visitType(param.type);
|
|
366
|
+
});
|
|
367
|
+
if (typeNode.type !== undefined)
|
|
368
|
+
visitType(typeNode.type);
|
|
369
|
+
}
|
|
370
|
+
if (ts.isParenthesizedTypeNode(typeNode)) {
|
|
371
|
+
visitType(typeNode.type);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
375
|
+
node.members.forEach((member) => {
|
|
376
|
+
if (ts.isPropertySignature(member) && member.type !== undefined) {
|
|
377
|
+
visitType(member.type);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
else if (ts.isTypeAliasDeclaration(node) && node.type !== undefined) {
|
|
382
|
+
visitType(node.type);
|
|
383
|
+
}
|
|
384
|
+
return refs;
|
|
385
|
+
}
|
|
386
|
+
function extractTypeAlias(node, sourceFile) {
|
|
387
|
+
const typeName = node.name.text;
|
|
388
|
+
const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
389
|
+
const exportPrefix = hasExport ? 'export ' : '';
|
|
390
|
+
const typeValue = node.type?.getText(sourceFile) ?? 'unknown';
|
|
391
|
+
return `${exportPrefix}type ${typeName} = ${typeValue}`;
|
|
392
|
+
}
|
|
393
|
+
function extractEnumDefinition(node, sourceFile) {
|
|
394
|
+
const enumName = node.name.text;
|
|
395
|
+
const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
396
|
+
const exportPrefix = hasExport ? 'export ' : '';
|
|
397
|
+
const members = node.members
|
|
398
|
+
.map((m) => {
|
|
399
|
+
const name = m.name.getText(sourceFile);
|
|
400
|
+
const value = m.initializer?.getText(sourceFile);
|
|
401
|
+
return value !== undefined ? `${name} = ${value}` : name;
|
|
402
|
+
})
|
|
403
|
+
.join(', ');
|
|
404
|
+
return `${exportPrefix}enum ${enumName} { ${members} }`;
|
|
405
|
+
}
|
|
406
|
+
function extractInterfaceDefinition(node, sourceFile) {
|
|
407
|
+
const interfaceName = node.name.text;
|
|
408
|
+
const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
409
|
+
const exportPrefix = hasExport ? 'export ' : '';
|
|
410
|
+
const members = node.members
|
|
411
|
+
.map((member) => {
|
|
412
|
+
if (ts.isPropertySignature(member) && member.name !== undefined) {
|
|
413
|
+
const propName = member.name.getText(sourceFile);
|
|
414
|
+
const optional = member.questionToken !== undefined ? '?' : '';
|
|
415
|
+
const propType = member.type !== undefined ? member.type.getText(sourceFile) : 'any';
|
|
416
|
+
return `${propName}${optional}: ${propType}`;
|
|
417
|
+
}
|
|
418
|
+
return '';
|
|
419
|
+
})
|
|
420
|
+
.filter((m) => m !== '')
|
|
421
|
+
.join('; ');
|
|
422
|
+
return `${exportPrefix}interface ${interfaceName} { ${members} }`;
|
|
423
|
+
}
|
|
424
|
+
function extractReferencedTypeDefinitions(sourceFile, typeReferences) {
|
|
425
|
+
const definitions = [];
|
|
426
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
427
|
+
if (ts.isTypeAliasDeclaration(node) && typeReferences.has(node.name.text)) {
|
|
428
|
+
definitions.push(extractTypeAlias(node, sourceFile));
|
|
429
|
+
}
|
|
430
|
+
else if (ts.isEnumDeclaration(node) && typeReferences.has(node.name.text)) {
|
|
431
|
+
definitions.push(extractEnumDefinition(node, sourceFile));
|
|
432
|
+
}
|
|
433
|
+
else if (ts.isInterfaceDeclaration(node) &&
|
|
434
|
+
typeReferences.has(node.name.text) &&
|
|
435
|
+
!node.name.text.includes('Props')) {
|
|
436
|
+
definitions.push(extractInterfaceDefinition(node, sourceFile));
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
return definitions.length > 0 ? definitions.join('\n ') : '';
|
|
440
|
+
}
|
|
441
|
+
function flattenPropsInterface(node, sourceFile) {
|
|
442
|
+
const properties = [];
|
|
443
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
444
|
+
node.members.forEach((member) => {
|
|
445
|
+
if (ts.isPropertySignature(member) && member.name !== undefined) {
|
|
446
|
+
const propName = member.name.getText(sourceFile);
|
|
447
|
+
const optional = member.questionToken ? '?' : '';
|
|
448
|
+
const propType = member.type !== undefined ? member.type.getText(sourceFile) : 'any';
|
|
449
|
+
properties.push(`${propName}${optional}: ${propType}`);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
else if (ts.isTypeAliasDeclaration(node) && node.type !== undefined) {
|
|
454
|
+
return node.type.getText(sourceFile);
|
|
455
|
+
}
|
|
456
|
+
return properties.join('; ');
|
|
457
|
+
}
|
|
458
|
+
function extractPropsAndImport(sourceCode, componentName, type, actualFilename) {
|
|
459
|
+
const sourceFile = ts.createSourceFile(`${componentName}.tsx`, sourceCode, ts.ScriptTarget.Latest, true);
|
|
460
|
+
const exportedComponents = extractExportedComponentNames(sourceFile);
|
|
461
|
+
const allExportedTypes = extractAllExportedTypes(sourceFile);
|
|
462
|
+
let propsInline = '';
|
|
463
|
+
let propsNode = null;
|
|
464
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
465
|
+
if (!propsInline) {
|
|
466
|
+
if (ts.isInterfaceDeclaration(node) && node.name.text.includes(`${componentName}Props`)) {
|
|
467
|
+
propsInline = flattenPropsInterface(node, sourceFile);
|
|
468
|
+
propsNode = node;
|
|
469
|
+
}
|
|
470
|
+
else if (ts.isTypeAliasDeclaration(node) && node.name.text.includes(`${componentName}Props`)) {
|
|
471
|
+
propsInline = flattenPropsInterface(node, sourceFile);
|
|
472
|
+
propsNode = node;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
const typeReferences = propsNode !== null ? extractTypeReferencesFromProps(propsNode, sourceFile) : new Set();
|
|
477
|
+
const toImport = exportedComponents.length > 0 ? [...exportedComponents] : [componentName];
|
|
478
|
+
for (const typeRef of typeReferences) {
|
|
479
|
+
if (allExportedTypes.includes(typeRef) && !toImport.includes(typeRef)) {
|
|
480
|
+
toImport.push(typeRef);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const typeDefinitions = extractReferencedTypeDefinitions(sourceFile, typeReferences);
|
|
484
|
+
const importStatement = `import { ${toImport.join(', ')} } from '@/components/${type}/${actualFilename}';`;
|
|
485
|
+
const importLine = `**Import**: \`${importStatement}\``;
|
|
486
|
+
const typeDefsLine = typeDefinitions ? `**Type Definitions**:\n ${typeDefinitions}` : '';
|
|
487
|
+
const propsLine = propsInline ? `**Props**: \`${propsInline}\`` : '**Props**: None';
|
|
488
|
+
const parts = [importLine, typeDefsLine, propsLine].filter((p) => p !== '');
|
|
489
|
+
return parts.join('\n');
|
|
490
|
+
}
|
|
491
|
+
async function readComponentPropsInterface(projectDir, _type, name, registry) {
|
|
492
|
+
const entry = registry.get(name);
|
|
493
|
+
if (entry === undefined) {
|
|
494
|
+
debugProcess(`[readComponentPropsInterface] No registry entry found for ${name}`);
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
const file = path.join(projectDir, 'src', 'components', entry.type, `${entry.actualFilename}.tsx`);
|
|
498
|
+
debugProcess(`[readComponentPropsInterface] Reading file: ${file}`);
|
|
499
|
+
try {
|
|
500
|
+
const sourceCode = await fs.readFile(file, 'utf-8');
|
|
501
|
+
const result = extractPropsAndImport(sourceCode, name, entry.type, entry.actualFilename);
|
|
502
|
+
debugProcess(`[readComponentPropsInterface] extractPropsAndImport returned ${result ? result.length : 0} chars`);
|
|
503
|
+
return result;
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
debugProcess(`[readComponentPropsInterface] Error reading file: ${String(error)}`);
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async function readComponentFullImplementation(projectDir, _type, name, registry) {
|
|
511
|
+
const entry = registry.get(name);
|
|
512
|
+
if (entry === undefined) {
|
|
513
|
+
debugProcess(`[readComponentFullImplementation] No registry entry found for ${name}`);
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
const file = path.join(projectDir, 'src', 'components', entry.type, `${entry.actualFilename}.tsx`);
|
|
517
|
+
debugProcess(`[readComponentFullImplementation] Reading file: ${file}`);
|
|
176
518
|
try {
|
|
177
|
-
|
|
519
|
+
const sourceCode = await fs.readFile(file, 'utf-8');
|
|
520
|
+
const sourceFile = ts.createSourceFile(`${name}.tsx`, sourceCode, ts.ScriptTarget.Latest, true);
|
|
521
|
+
const exportedComponents = extractExportedComponentNames(sourceFile);
|
|
522
|
+
const toImport = exportedComponents.length > 0 ? [...exportedComponents] : [name];
|
|
523
|
+
const importStatement = `import { ${toImport.join(', ')} } from '@/components/${entry.type}/${entry.actualFilename}';`;
|
|
524
|
+
const formattedOutput = `**Import**: \`${importStatement}\`\n\n**Implementation**:\n\`\`\`tsx\n${sourceCode}\n\`\`\``;
|
|
525
|
+
debugProcess(`[readComponentFullImplementation] Returning ${formattedOutput.length} chars for ${name}`);
|
|
526
|
+
return formattedOutput;
|
|
178
527
|
}
|
|
179
|
-
catch {
|
|
528
|
+
catch (error) {
|
|
529
|
+
debugProcess(`[readComponentFullImplementation] Error reading file: ${String(error)}`);
|
|
180
530
|
return null;
|
|
181
531
|
}
|
|
182
532
|
}
|
|
@@ -186,69 +536,221 @@ function extractCodeBlock(text) {
|
|
|
186
536
|
.replace(/```/g, '')
|
|
187
537
|
.trim();
|
|
188
538
|
}
|
|
189
|
-
|
|
190
|
-
|
|
539
|
+
function detectTruncation(code) {
|
|
540
|
+
const truncationIndicators = [/<\w+[^/>]*$/, /<\/\w*$/, /"[^"]*$/, /'[^']*$/, /`[^`]*$/, /\{[^}]*$/m, /\([^)]*$/m];
|
|
541
|
+
const lastLines = code.split('\n').slice(-5).join('\n');
|
|
542
|
+
return truncationIndicators.some((pattern) => pattern.test(lastLines));
|
|
543
|
+
}
|
|
544
|
+
async function readEssentialFiles(projectDir) {
|
|
545
|
+
debugProcess('[readEssentialFiles] Reading essential config files from %s', projectDir);
|
|
191
546
|
const start = performance.now();
|
|
192
547
|
const config = {};
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
await readRecursive(fullPath);
|
|
202
|
-
}
|
|
203
|
-
else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))) {
|
|
204
|
-
try {
|
|
205
|
-
config[relativePath] = await fs.readFile(fullPath, 'utf-8');
|
|
206
|
-
}
|
|
207
|
-
catch (err) {
|
|
208
|
-
debugProcess(`Failed to read ${relativePath}: ${err.message}`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
548
|
+
const essentialFiles = ['package.json', 'tsconfig.json', 'tailwind.config.js', 'tailwind.config.ts'];
|
|
549
|
+
for (const file of essentialFiles) {
|
|
550
|
+
try {
|
|
551
|
+
config[file] = await fs.readFile(path.join(projectDir, file), 'utf-8');
|
|
552
|
+
debugProcess(`Read essential file: ${file}`);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
debugProcess(`Essential file not found: ${file}`);
|
|
211
556
|
}
|
|
212
557
|
}
|
|
213
|
-
|
|
214
|
-
debugProcess(`[readAllTopLevelFiles] Completed in ${(performance.now() - start).toFixed(2)} ms`);
|
|
558
|
+
debugProcess(`[readEssentialFiles] Completed in ${(performance.now() - start).toFixed(2)} ms`);
|
|
215
559
|
return config;
|
|
216
560
|
}
|
|
561
|
+
function hasDataRequirements(componentDef) {
|
|
562
|
+
return ('data_requirements' in componentDef &&
|
|
563
|
+
Array.isArray(componentDef.data_requirements) &&
|
|
564
|
+
componentDef.data_requirements.length > 0);
|
|
565
|
+
}
|
|
566
|
+
function checkOrganismForMolecule(moleculeName, organismName, organismDef) {
|
|
567
|
+
if (typeof organismDef !== 'object' || organismDef === null)
|
|
568
|
+
return false;
|
|
569
|
+
const composition = organismDef.composition;
|
|
570
|
+
const includesMolecule = composition?.molecules?.includes(moleculeName) ?? false;
|
|
571
|
+
if (!includesMolecule)
|
|
572
|
+
return false;
|
|
573
|
+
const hasData = hasDataRequirements(organismDef);
|
|
574
|
+
if (hasData) {
|
|
575
|
+
debugProcess(`[findParentDataRequirements] Molecule ${moleculeName} is used by organism ${organismName} which has data_requirements`);
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
function checkPageLayoutOrganisms(layout, organisms, moleculeName) {
|
|
581
|
+
if (layout?.organisms === undefined)
|
|
582
|
+
return false;
|
|
583
|
+
for (const organismName of layout.organisms) {
|
|
584
|
+
const organismDef = organisms.items[organismName];
|
|
585
|
+
if (checkOrganismForMolecule(moleculeName, organismName, organismDef)) {
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
function checkPagesForMolecule(pages, organisms, moleculeName) {
|
|
592
|
+
if (pages?.items === undefined || organisms?.items === undefined)
|
|
593
|
+
return false;
|
|
594
|
+
const organismsWithItems = { items: organisms.items };
|
|
595
|
+
for (const [, pageDef] of Object.entries(pages.items)) {
|
|
596
|
+
if (typeof pageDef !== 'object' || pageDef === null)
|
|
597
|
+
continue;
|
|
598
|
+
const layout = pageDef.layout;
|
|
599
|
+
if (checkPageLayoutOrganisms(layout, organismsWithItems, moleculeName)) {
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
function findParentDataRequirements(scheme, moleculeName) {
|
|
606
|
+
debugProcess(`[findParentDataRequirements] Checking if molecule ${moleculeName} has parents with data requirements`);
|
|
607
|
+
const organisms = scheme.organisms;
|
|
608
|
+
if (organisms?.items !== undefined) {
|
|
609
|
+
for (const [organismName, organismDef] of Object.entries(organisms.items)) {
|
|
610
|
+
if (checkOrganismForMolecule(moleculeName, organismName, organismDef)) {
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const pages = scheme.pages;
|
|
616
|
+
if (checkPagesForMolecule(pages, organisms, moleculeName)) {
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
debugProcess(`[findParentDataRequirements] No parents with data_requirements found for molecule ${moleculeName}`);
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
async function readGraphQLFiles(projectDir) {
|
|
623
|
+
debugProcess('[readGraphQLFiles] Reading GraphQL type definition files from %s', projectDir);
|
|
624
|
+
const start = performance.now();
|
|
625
|
+
const graphqlFiles = {};
|
|
626
|
+
const graphqlFilePaths = ['src/gql/graphql.ts', 'src/graphql/queries.ts', 'src/graphql/mutations.ts'];
|
|
627
|
+
for (const filePath of graphqlFilePaths) {
|
|
628
|
+
try {
|
|
629
|
+
graphqlFiles[filePath] = await fs.readFile(path.join(projectDir, filePath), 'utf-8');
|
|
630
|
+
debugProcess(`Read GraphQL file: ${filePath}`);
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
debugProcess(`GraphQL file not found: ${filePath}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
debugProcess(`[readGraphQLFiles] Completed in ${(performance.now() - start).toFixed(2)} ms`);
|
|
637
|
+
return graphqlFiles;
|
|
638
|
+
}
|
|
639
|
+
async function readProjectContext(projectDir) {
|
|
640
|
+
return await readEssentialFiles(projectDir);
|
|
641
|
+
}
|
|
642
|
+
async function executeTypeCheck(tsconfigRoot, strict) {
|
|
643
|
+
const args = strict
|
|
644
|
+
? ['tsc', '--noEmit', '--skipLibCheck', '--strict', '--pretty', 'false']
|
|
645
|
+
: ['tsc', '--noEmit', '--skipLibCheck', '--pretty', 'false'];
|
|
646
|
+
const result = await execa('npx', args, {
|
|
647
|
+
cwd: tsconfigRoot,
|
|
648
|
+
stdio: 'pipe',
|
|
649
|
+
reject: false,
|
|
650
|
+
});
|
|
651
|
+
return (result.stdout ?? '') + (result.stderr ?? '');
|
|
652
|
+
}
|
|
653
|
+
async function runGraphQLStrictCheck(tsconfigRoot, relativeFilePath, normalizedRelative, filePath) {
|
|
654
|
+
debugTypeCheck(`[runTypeCheckForFile] Running strict GraphQL type check...`);
|
|
655
|
+
const strictOutput = await executeTypeCheck(tsconfigRoot, true);
|
|
656
|
+
const graphqlStrictErrors = filterErrorsForFile(strictOutput, relativeFilePath, normalizedRelative, filePath);
|
|
657
|
+
debugTypeCheck(`[runTypeCheckForFile] GraphQL strict errors length: ${graphqlStrictErrors.length} chars`);
|
|
658
|
+
return graphqlStrictErrors;
|
|
659
|
+
}
|
|
217
660
|
async function runTypeCheckForFile(projectDir, filePath) {
|
|
218
661
|
const start = performance.now();
|
|
219
662
|
try {
|
|
220
663
|
const tsconfigRoot = await findProjectRoot(projectDir);
|
|
221
|
-
const
|
|
664
|
+
const absoluteFilePath = path.isAbsolute(filePath) ? filePath : path.resolve(tsconfigRoot, filePath);
|
|
665
|
+
const relativeFilePath = path.relative(tsconfigRoot, absoluteFilePath).replace(/\\/g, '/');
|
|
222
666
|
const normalizedRelative = relativeFilePath.replace(/^client\//, '');
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
667
|
+
debugTypeCheck(`[runTypeCheckForFile] tsconfigRoot: ${tsconfigRoot}`);
|
|
668
|
+
debugTypeCheck(`[runTypeCheckForFile] absoluteFilePath: ${absoluteFilePath}`);
|
|
669
|
+
debugTypeCheck(`[runTypeCheckForFile] relativeFilePath: ${relativeFilePath}`);
|
|
670
|
+
debugTypeCheck(`[runTypeCheckForFile] normalizedRelative: ${normalizedRelative}`);
|
|
671
|
+
const isGraphQLFile = await detectGraphQLFile(absoluteFilePath, relativeFilePath);
|
|
672
|
+
debugTypeCheck(`[runTypeCheckForFile] isGraphQLFile: ${isGraphQLFile}`);
|
|
673
|
+
const output = await executeTypeCheck(tsconfigRoot, false);
|
|
229
674
|
debugTypeCheck(`[runTypeCheckForFile] Finished tsc in ${(performance.now() - start).toFixed(2)} ms`);
|
|
230
|
-
|
|
675
|
+
debugTypeCheck(`[runTypeCheckForFile] Total output length: ${output.length} chars`);
|
|
676
|
+
debugTypeCheck(`[runTypeCheckForFile] Output preview (first 2000 chars):\n${output.substring(0, 2000)}`);
|
|
677
|
+
const filteredErrors = filterErrorsForFile(output, relativeFilePath, normalizedRelative, filePath);
|
|
678
|
+
const graphqlStrictErrors = isGraphQLFile
|
|
679
|
+
? await runGraphQLStrictCheck(tsconfigRoot, relativeFilePath, normalizedRelative, filePath)
|
|
680
|
+
: '';
|
|
681
|
+
const formattedErrors = formatTypeCheckErrors(filteredErrors, graphqlStrictErrors);
|
|
682
|
+
if (!output.includes('error TS') && formattedErrors.trim().length === 0) {
|
|
683
|
+
debugTypeCheck(`[runTypeCheckForFile] No errors found`);
|
|
231
684
|
return { success: true, errors: '' };
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
.filter((line) => {
|
|
235
|
-
const hasError = line.includes('error TS');
|
|
236
|
-
const notNodeModules = !line.includes('node_modules');
|
|
237
|
-
const matchesTarget = line.includes(relativeFilePath) ||
|
|
238
|
-
line.includes(normalizedRelative) ||
|
|
239
|
-
line.includes(path.basename(filePath));
|
|
240
|
-
return hasError && notNodeModules && matchesTarget;
|
|
241
|
-
})
|
|
242
|
-
.join('\n');
|
|
243
|
-
if (filteredErrors.trim().length === 0)
|
|
685
|
+
}
|
|
686
|
+
if (formattedErrors.trim().length === 0)
|
|
244
687
|
return { success: true, errors: '' };
|
|
245
|
-
return { success: false, errors:
|
|
688
|
+
return { success: false, errors: formattedErrors };
|
|
246
689
|
}
|
|
247
690
|
catch (err) {
|
|
248
691
|
const message = err instanceof Error ? err.message : String(err);
|
|
249
692
|
return { success: false, errors: message };
|
|
250
693
|
}
|
|
251
694
|
}
|
|
695
|
+
async function detectGraphQLFile(absoluteFilePath, relativeFilePath) {
|
|
696
|
+
const isInGraphQLDir = relativeFilePath.includes('/graphql/') ||
|
|
697
|
+
relativeFilePath.includes('/gql/') ||
|
|
698
|
+
relativeFilePath.includes('\\graphql\\') ||
|
|
699
|
+
relativeFilePath.includes('\\gql\\');
|
|
700
|
+
if (isInGraphQLDir)
|
|
701
|
+
return true;
|
|
702
|
+
try {
|
|
703
|
+
const content = await fs.readFile(absoluteFilePath, 'utf-8');
|
|
704
|
+
return (content.includes("from '@/gql/graphql'") ||
|
|
705
|
+
content.includes('from "@/gql/graphql"') ||
|
|
706
|
+
content.includes("from '@/graphql/") ||
|
|
707
|
+
content.includes('from "@/graphql/') ||
|
|
708
|
+
content.includes('@apollo/client'));
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function filterErrorsForFile(output, relativeFilePath, normalizedRelative, filePath) {
|
|
715
|
+
const allLines = output.split('\n');
|
|
716
|
+
const errorLines = allLines.filter((line) => line.includes('error TS'));
|
|
717
|
+
debugTypeCheck(`[filterErrorsForFile] Total lines: ${allLines.length}, Error lines: ${errorLines.length}`);
|
|
718
|
+
const filteredErrors = output
|
|
719
|
+
.split('\n')
|
|
720
|
+
.filter((line) => {
|
|
721
|
+
const hasError = line.includes('error TS');
|
|
722
|
+
const notNodeModules = !line.includes('node_modules');
|
|
723
|
+
const matchesTarget = line.includes(relativeFilePath) || line.includes(normalizedRelative) || line.includes(path.basename(filePath));
|
|
724
|
+
if (hasError) {
|
|
725
|
+
debugTypeCheck(`[filterErrorsForFile] Checking error line: ${line.substring(0, 150)}`);
|
|
726
|
+
debugTypeCheck(`[filterErrorsForFile] hasError: ${hasError}, notNodeModules: ${notNodeModules}, matchesTarget: ${matchesTarget}`);
|
|
727
|
+
}
|
|
728
|
+
return hasError && notNodeModules && matchesTarget;
|
|
729
|
+
})
|
|
730
|
+
.join('\n');
|
|
731
|
+
debugTypeCheck(`[filterErrorsForFile] Filtered errors length: ${filteredErrors.length} chars`);
|
|
732
|
+
return filteredErrors;
|
|
733
|
+
}
|
|
734
|
+
function formatTypeCheckErrors(regularErrors, graphqlStrictErrors) {
|
|
735
|
+
let formattedOutput = '';
|
|
736
|
+
if (graphqlStrictErrors.trim().length > 0) {
|
|
737
|
+
formattedOutput += '## GraphQL Schema Type Errors (strict mode)\n\n';
|
|
738
|
+
formattedOutput += 'These errors indicate fields/properties that violate GraphQL schema type contracts.\n';
|
|
739
|
+
formattedOutput += 'Common causes:\n';
|
|
740
|
+
formattedOutput += '- Using fields not defined in the GraphQL schema\n';
|
|
741
|
+
formattedOutput += '- Incorrect property types in mutation variables\n';
|
|
742
|
+
formattedOutput += '- Missing required fields in input types\n\n';
|
|
743
|
+
formattedOutput += graphqlStrictErrors;
|
|
744
|
+
formattedOutput += '\n\n';
|
|
745
|
+
formattedOutput += '**Fix**: Check @/gql/graphql.ts for the exact type definition and valid fields.\n\n';
|
|
746
|
+
formattedOutput += '---\n\n';
|
|
747
|
+
}
|
|
748
|
+
if (regularErrors.trim().length > 0) {
|
|
749
|
+
formattedOutput += '## TypeScript Errors\n\n';
|
|
750
|
+
formattedOutput += regularErrors;
|
|
751
|
+
}
|
|
752
|
+
return formattedOutput.trim();
|
|
753
|
+
}
|
|
252
754
|
async function findProjectRoot(startDir) {
|
|
253
755
|
let dir = startDir;
|
|
254
756
|
while (dir !== path.dirname(dir)) {
|
|
@@ -263,30 +765,62 @@ async function findProjectRoot(startDir) {
|
|
|
263
765
|
}
|
|
264
766
|
throw new Error('Could not find project root (no package.json or tsconfig.json found)');
|
|
265
767
|
}
|
|
266
|
-
|
|
267
|
-
|
|
768
|
+
async function extractComponentUsageFromScaffolds(componentName, componentType, projectDir) {
|
|
769
|
+
const usageExamples = [];
|
|
770
|
+
let requiresChildren = false;
|
|
771
|
+
const detectedProps = [];
|
|
772
|
+
const searchDirs = [];
|
|
773
|
+
if (componentType === 'molecule') {
|
|
774
|
+
searchDirs.push(path.join(projectDir, 'src', 'components', 'organisms'));
|
|
775
|
+
searchDirs.push(path.join(projectDir, 'src', 'components', 'pages'));
|
|
776
|
+
}
|
|
777
|
+
else if (componentType === 'organism') {
|
|
778
|
+
searchDirs.push(path.join(projectDir, 'src', 'components', 'pages'));
|
|
779
|
+
}
|
|
780
|
+
for (const dir of searchDirs) {
|
|
781
|
+
try {
|
|
782
|
+
const files = await fs.readdir(dir);
|
|
783
|
+
for (const file of files) {
|
|
784
|
+
if (!file.endsWith('.tsx'))
|
|
785
|
+
continue;
|
|
786
|
+
const filePath = path.join(dir, file);
|
|
787
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
788
|
+
const openTagPattern = new RegExp(`<${componentName}(?:\\s|>)`, 'g');
|
|
789
|
+
if (!openTagPattern.test(content))
|
|
790
|
+
continue;
|
|
791
|
+
const withChildrenPattern = new RegExp(`<${componentName}[^>]*>([\\s\\S]*?)</${componentName}>`, 'g');
|
|
792
|
+
const hasChildren = withChildrenPattern.test(content);
|
|
793
|
+
if (hasChildren) {
|
|
794
|
+
requiresChildren = true;
|
|
795
|
+
}
|
|
796
|
+
const lines = content.split('\n');
|
|
797
|
+
const usageLineIndexes = [];
|
|
798
|
+
lines.forEach((line, idx) => {
|
|
799
|
+
if (line.includes(`<${componentName}`)) {
|
|
800
|
+
usageLineIndexes.push(idx);
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
if (usageLineIndexes.length > 0) {
|
|
804
|
+
const lineIdx = usageLineIndexes[0];
|
|
805
|
+
const start = Math.max(0, lineIdx - 2);
|
|
806
|
+
const end = Math.min(lines.length, lineIdx + 8);
|
|
807
|
+
const snippet = lines.slice(start, end).join('\n');
|
|
808
|
+
usageExamples.push({
|
|
809
|
+
file: path.relative(projectDir, filePath),
|
|
810
|
+
snippet,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
// ignore read errors
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return { usageExamples, requiresChildren, detectedProps };
|
|
820
|
+
}
|
|
821
|
+
function makeBasePrompt(componentType, componentName, componentDef, existingScaffold, projectConfig, designSystemReference, dependencySources, usageInfo) {
|
|
268
822
|
const hasScaffold = Boolean(existingScaffold?.trim());
|
|
269
|
-
const
|
|
270
|
-
const graphqlFiles = {};
|
|
271
|
-
const otherFiles = {};
|
|
272
|
-
for (const [filePath, content] of Object.entries(projectConfig)) {
|
|
273
|
-
const lower = filePath.toLowerCase();
|
|
274
|
-
if (lower.includes('src/gql/'))
|
|
275
|
-
gqlFiles[filePath] = content;
|
|
276
|
-
else if (lower.includes('src/graphql/'))
|
|
277
|
-
graphqlFiles[filePath] = content;
|
|
278
|
-
else
|
|
279
|
-
otherFiles[filePath] = content;
|
|
280
|
-
}
|
|
281
|
-
const queriesFile = Object.entries(graphqlFiles).find(([n]) => n.endsWith('queries.ts'))?.[1] ?? '';
|
|
282
|
-
const mutationsFile = Object.entries(graphqlFiles).find(([n]) => n.endsWith('mutations.ts'))?.[1] ?? '';
|
|
283
|
-
const gqlSection = Object.entries(gqlFiles)
|
|
284
|
-
.map(([p, c]) => `### ${p}\n${c}`)
|
|
285
|
-
.join('\n\n') || '(No gql folder found)';
|
|
286
|
-
const graphqlSection = Object.entries(graphqlFiles)
|
|
287
|
-
.map(([p, c]) => `### ${p}\n${c}`)
|
|
288
|
-
.join('\n\n') || '(No graphql folder found)';
|
|
289
|
-
const configSection = Object.entries(otherFiles)
|
|
823
|
+
const configSection = Object.entries(projectConfig)
|
|
290
824
|
.map(([p, c]) => `### ${p}\n${c}`)
|
|
291
825
|
.join('\n\n') || '(No additional config files)';
|
|
292
826
|
const designSystemBlock = designSystemReference.trim()
|
|
@@ -296,7 +830,7 @@ function makeBasePrompt(componentType, componentName, componentDef, existingScaf
|
|
|
296
830
|
.map(([name, src]) => `### ${name}\n${src}`)
|
|
297
831
|
.join('\n\n') || '(No dependencies found)';
|
|
298
832
|
return `
|
|
299
|
-
#
|
|
833
|
+
# Implement ${componentName} (${componentType})
|
|
300
834
|
|
|
301
835
|
You are a senior frontend engineer specializing in **React + TypeScript + Apollo Client**.
|
|
302
836
|
Your task is to build a visually excellent, type-safe, and production-ready ${componentType} component.
|
|
@@ -315,55 +849,81 @@ Your component must:
|
|
|
315
849
|
|
|
316
850
|
---
|
|
317
851
|
|
|
852
|
+
## IA Schema
|
|
853
|
+
${JSON.stringify(componentDef, null, 2)}
|
|
854
|
+
|
|
855
|
+
---
|
|
856
|
+
|
|
318
857
|
## Project Context
|
|
319
858
|
|
|
320
|
-
**File Path:** src/components/${componentType}/${componentName}.tsx
|
|
321
859
|
**Purpose:** A reusable UI element connected to the GraphQL layer and design system.
|
|
322
860
|
|
|
323
|
-
|
|
324
|
-
${JSON.stringify(componentDef, null, 2)}
|
|
861
|
+
## Component Scaffold
|
|
325
862
|
|
|
326
|
-
|
|
327
|
-
|
|
863
|
+
${hasScaffold
|
|
864
|
+
? `The scaffold below contains:
|
|
865
|
+
- Import statements for all dependencies (use these exact imports)
|
|
866
|
+
- Type guidance comments showing GraphQL queries/mutations/enums to use
|
|
867
|
+
- Specs describing required functionality
|
|
868
|
+
- Component structure to implement
|
|
328
869
|
|
|
329
|
-
|
|
330
|
-
${designSystemBlock}
|
|
870
|
+
**CRITICAL**: Follow the import statements and type guidance comments in the scaffold exactly.
|
|
331
871
|
|
|
332
|
-
|
|
333
|
-
|
|
872
|
+
${existingScaffold}`
|
|
873
|
+
: '(No existing scaffold found - create component from scratch)'}
|
|
334
874
|
|
|
335
|
-
|
|
336
|
-
${graphqlSection}
|
|
875
|
+
---
|
|
337
876
|
|
|
338
|
-
|
|
339
|
-
${
|
|
877
|
+
## Design System
|
|
878
|
+
${designSystemBlock}
|
|
879
|
+
|
|
880
|
+
---
|
|
881
|
+
|
|
882
|
+
## Available Dependencies
|
|
340
883
|
|
|
341
|
-
|
|
342
|
-
${mutationsFile || '(mutations.ts not found)'}
|
|
884
|
+
${dependencySection}
|
|
343
885
|
|
|
344
|
-
|
|
345
|
-
${gqlSection}
|
|
886
|
+
---
|
|
346
887
|
|
|
347
|
-
|
|
888
|
+
## Project Configuration
|
|
348
889
|
${configSection}
|
|
349
890
|
|
|
350
891
|
---
|
|
351
892
|
|
|
352
|
-
##
|
|
893
|
+
## Implementation Rules
|
|
353
894
|
|
|
354
895
|
**Type Safety**
|
|
355
|
-
-
|
|
356
|
-
-
|
|
896
|
+
- No \`any\` or \`as SomeType\` - type correctly
|
|
897
|
+
- Import types from dependencies - never redefine them locally
|
|
898
|
+
- Type all props, state, and GraphQL responses explicitly
|
|
357
899
|
|
|
358
|
-
**
|
|
359
|
-
-
|
|
360
|
-
-
|
|
361
|
-
-
|
|
362
|
-
-
|
|
900
|
+
**Imports**
|
|
901
|
+
- Use exact imports from scaffold and dependencies section
|
|
902
|
+
- Pattern: \`@/components/{type}/{component}\`
|
|
903
|
+
- Never use relative paths (\`../\`)
|
|
904
|
+
- Only use packages from package.json shown above
|
|
363
905
|
|
|
364
|
-
**
|
|
365
|
-
-
|
|
366
|
-
-
|
|
906
|
+
**GraphQL (if applicable)**
|
|
907
|
+
- **CRITICAL**: NEVER use inline gql template literals or import gql from @apollo/client
|
|
908
|
+
- **CRITICAL**: ALWAYS import pre-generated operations from @/graphql/queries or @/graphql/mutations
|
|
909
|
+
- Follow type guidance comments in scaffold for exact query/mutation names
|
|
910
|
+
- Use pattern: \`const { data } = useQuery(QueryName)\` where QueryName is imported from @/graphql/queries
|
|
911
|
+
- Use pattern: \`const [mutate, { loading }] = useMutation(MutationName, { refetchQueries: [...], awaitRefetchQueries: true })\`
|
|
912
|
+
- Access enum values: \`EnumName.Value\` (e.g., \`TodoStateStatus.Pending\`)
|
|
913
|
+
- **CRITICAL**: ALL mutations return \`{ success: Boolean!, error: { type: String!, message: String } }\`
|
|
914
|
+
- **CRITICAL**: ALWAYS check \`data?.mutationName?.success\` before considering mutation successful
|
|
915
|
+
- **CRITICAL**: ALWAYS handle \`data?.mutationName?.error?.message\` when \`success\` is false
|
|
916
|
+
- **CRITICAL**: ALWAYS use \`loading\` state to disable buttons during mutations: \`disabled={loading}\`
|
|
917
|
+
- **CRITICAL**: ALWAYS wrap mutations in try-catch for network errors
|
|
918
|
+
- **CRITICAL**: Use \`awaitRefetchQueries: true\` to prevent race conditions with polling queries
|
|
919
|
+
|
|
920
|
+
**React Best Practices**
|
|
921
|
+
- No setState during render
|
|
922
|
+
- Include dependency arrays in useEffect
|
|
923
|
+
- Use optional chaining (?.) and nullish coalescing (??)
|
|
924
|
+
${usageInfo.requiresChildren
|
|
925
|
+
? `\n**CRITICAL**: This component MUST accept \`children?: React.ReactNode\` prop based on parent usage patterns.`
|
|
926
|
+
: ''}
|
|
367
927
|
|
|
368
928
|
**Visual & UX Quality**
|
|
369
929
|
- Perfect spacing and alignment using Tailwind or the design system tokens.
|
|
@@ -380,9 +940,10 @@ ${configSection}
|
|
|
380
940
|
- Match button, card, and badge styles with existing components.
|
|
381
941
|
|
|
382
942
|
**Prohibited**
|
|
383
|
-
- No placeholder data
|
|
384
|
-
- No new external packages.
|
|
385
|
-
- No
|
|
943
|
+
- No placeholder data or TODOs
|
|
944
|
+
- No new external packages not in package.json
|
|
945
|
+
- No reimplementing dependencies inline
|
|
946
|
+
- No redefining types that dependencies export
|
|
386
947
|
|
|
387
948
|
---
|
|
388
949
|
|
|
@@ -404,43 +965,141 @@ ${configSection}
|
|
|
404
965
|
|
|
405
966
|
---
|
|
406
967
|
|
|
407
|
-
**
|
|
408
|
-
Return only the complete \`.tsx\` source code for this component — no markdown fences, commentary, or extra text.
|
|
968
|
+
**Output**: Return ONLY the complete .tsx source code - no markdown fences or commentary.
|
|
409
969
|
`.trim();
|
|
410
970
|
}
|
|
411
|
-
function makeImplementPrompt(basePrompt) {
|
|
412
|
-
|
|
971
|
+
function makeImplementPrompt(basePrompt, graphqlFiles) {
|
|
972
|
+
const hasGraphQLFiles = graphqlFiles !== undefined && Object.keys(graphqlFiles).length > 0;
|
|
973
|
+
const graphqlSection = hasGraphQLFiles
|
|
974
|
+
? `
|
|
975
|
+
## GraphQL Type Definitions (Source of Truth)
|
|
976
|
+
|
|
977
|
+
**CRITICAL**: Use these exact TypeScript definitions for GraphQL types, queries, and mutations.
|
|
978
|
+
|
|
979
|
+
${Object.entries(graphqlFiles)
|
|
980
|
+
.map(([filePath, content]) => `### ${filePath}\n\`\`\`typescript\n${content}\n\`\`\``)
|
|
981
|
+
.join('\n\n')}
|
|
413
982
|
|
|
414
983
|
---
|
|
415
984
|
|
|
416
|
-
|
|
417
|
-
|
|
985
|
+
`
|
|
986
|
+
: '';
|
|
987
|
+
return `${basePrompt}
|
|
988
|
+
|
|
989
|
+
${graphqlSection}---
|
|
990
|
+
|
|
991
|
+
Begin directly with import statements and end with the export statement.
|
|
418
992
|
Do not include markdown fences, comments, or explanations — only the valid .tsx file content.
|
|
419
993
|
`.trim();
|
|
420
994
|
}
|
|
421
|
-
function
|
|
995
|
+
function validateComponentImport(componentType, filename, registry) {
|
|
996
|
+
const importPath = `@/components/${componentType}/${filename}`;
|
|
997
|
+
const exists = Array.from(registry.values()).some((entry) => entry.type === componentType && entry.actualFilename === filename);
|
|
998
|
+
if (exists) {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
const suggestions = Array.from(registry.values())
|
|
1002
|
+
.filter((entry) => entry.actualFilename.includes(filename) || filename.includes(entry.actualFilename))
|
|
1003
|
+
.map((entry) => `@/components/${entry.type}/${entry.actualFilename}`)
|
|
1004
|
+
.slice(0, 3);
|
|
1005
|
+
if (suggestions.length > 0) {
|
|
1006
|
+
return `Import not found: ${importPath}\nDid you mean: ${suggestions.join(', ')}?`;
|
|
1007
|
+
}
|
|
1008
|
+
return `Import not found: ${importPath}`;
|
|
1009
|
+
}
|
|
1010
|
+
function validateNonComponentImport(importPath) {
|
|
1011
|
+
if (importPath.startsWith('@/store/')) {
|
|
1012
|
+
return `Invalid import: ${importPath}\nThis project uses Apollo Client with GraphQL. Check the GraphQL files in context for available queries and mutations.`;
|
|
1013
|
+
}
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
function validateImports(code, registry) {
|
|
1017
|
+
const errors = [];
|
|
1018
|
+
const componentImportPattern = /import\s+[^'"]*from\s+['"]@\/components\/([^/]+)\/([^'"]+)['"]/g;
|
|
1019
|
+
let match;
|
|
1020
|
+
while ((match = componentImportPattern.exec(code)) !== null) {
|
|
1021
|
+
const error = validateComponentImport(match[1], match[2], registry);
|
|
1022
|
+
if (error !== null) {
|
|
1023
|
+
errors.push(error);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const allImportPattern = /import\s+[^'"]*from\s+['"](@\/[^'"]+)['"]/g;
|
|
1027
|
+
while ((match = allImportPattern.exec(code)) !== null) {
|
|
1028
|
+
const error = validateNonComponentImport(match[1]);
|
|
1029
|
+
if (error !== null) {
|
|
1030
|
+
errors.push(error);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return { valid: errors.length === 0, errors };
|
|
1034
|
+
}
|
|
1035
|
+
function makeRetryPrompt(componentName, previousErrors, composition, dependencySummary, importValidationErrors, description, existingScaffold, packageJson, graphqlFiles) {
|
|
1036
|
+
const compositionHint = composition.length > 0 ? `\n**Required Components**: ${composition.join(', ')}\n` : '';
|
|
1037
|
+
const importErrorsHint = importValidationErrors.length > 0
|
|
1038
|
+
? `\n**Import Errors**:\n${importValidationErrors.map((err) => `- ${err}`).join('\n')}\n`
|
|
1039
|
+
: '';
|
|
1040
|
+
const hasScaffold = Boolean(existingScaffold?.trim());
|
|
1041
|
+
const scaffoldSection = hasScaffold
|
|
1042
|
+
? `
|
|
1043
|
+
## Scaffold with Type Guidance
|
|
1044
|
+
|
|
1045
|
+
**CRITICAL**: Follow the type guidance comments in the scaffold for exact GraphQL operation names, enum values, and import patterns.
|
|
1046
|
+
|
|
1047
|
+
${existingScaffold}
|
|
1048
|
+
|
|
1049
|
+
---
|
|
1050
|
+
`
|
|
1051
|
+
: '';
|
|
1052
|
+
const hasGraphQLFiles = graphqlFiles !== undefined && Object.keys(graphqlFiles).length > 0;
|
|
1053
|
+
const graphqlSection = hasGraphQLFiles
|
|
1054
|
+
? `
|
|
1055
|
+
## GraphQL Type Definitions (Source of Truth)
|
|
1056
|
+
|
|
1057
|
+
**CRITICAL**: Use these exact TypeScript definitions for GraphQL types, queries, and mutations.
|
|
1058
|
+
|
|
1059
|
+
${Object.entries(graphqlFiles)
|
|
1060
|
+
.map(([filePath, content]) => `### ${filePath}\n\`\`\`typescript\n${content}\n\`\`\``)
|
|
1061
|
+
.join('\n\n')}
|
|
1062
|
+
|
|
1063
|
+
---
|
|
1064
|
+
`
|
|
1065
|
+
: '';
|
|
422
1066
|
return `
|
|
423
|
-
${
|
|
1067
|
+
# Fix TypeScript Errors: ${componentName}
|
|
1068
|
+
|
|
1069
|
+
${description ? `**Description**: ${description}\n` : ''}
|
|
1070
|
+
${compositionHint}
|
|
1071
|
+
${scaffoldSection}
|
|
1072
|
+
${graphqlSection}
|
|
1073
|
+
## Project Dependencies (package.json)
|
|
1074
|
+
|
|
1075
|
+
**CRITICAL**: Only import packages that exist in the dependencies below.
|
|
1076
|
+
|
|
1077
|
+
${packageJson}
|
|
424
1078
|
|
|
425
1079
|
---
|
|
426
1080
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
1081
|
+
## Available Components
|
|
1082
|
+
|
|
1083
|
+
${dependencySummary}
|
|
1084
|
+
|
|
1085
|
+
---
|
|
1086
|
+
|
|
1087
|
+
## Errors to Fix
|
|
430
1088
|
|
|
431
|
-
**Errors**
|
|
432
1089
|
${previousErrors}
|
|
433
1090
|
|
|
434
|
-
|
|
435
|
-
|
|
1091
|
+
${importErrorsHint}
|
|
1092
|
+
**Hints**:
|
|
1093
|
+
- Follow scaffold's type guidance comments for exact operation names and enum values
|
|
1094
|
+
${hasGraphQLFiles ? '- Use the GraphQL Type Definitions section above for exact types, interfaces, and enums' : '- Check @/gql/graphql.ts for GraphQL type definitions'}
|
|
1095
|
+
- **CRITICAL**: NEVER use inline \`gql\` template literals - ALWAYS import from @/graphql/queries or @/graphql/mutations
|
|
1096
|
+
- Use pattern: \`mutate({ variables: { input: InputType } })\`
|
|
1097
|
+
- Import pattern: \`@/components/{type}/{component}\`
|
|
1098
|
+
- Import types from dependencies - never redefine them
|
|
436
1099
|
|
|
437
1100
|
---
|
|
438
1101
|
|
|
439
|
-
|
|
440
|
-
- Fix only TypeScript or import errors.
|
|
441
|
-
- Do not change working logic or structure.
|
|
442
|
-
- Keep eslint directives and formatting intact.
|
|
443
|
-
- Return the corrected \`.tsx\` file only, with no markdown fences or commentary.
|
|
1102
|
+
**Output**: Return ONLY the corrected ${componentName}.tsx code - no markdown fences or commentary.
|
|
444
1103
|
`.trim();
|
|
445
1104
|
}
|
|
446
1105
|
/* -------------------------------------------------------------------------- */
|
|
@@ -467,6 +1126,59 @@ function isValidCollection(collection) {
|
|
|
467
1126
|
const items = collection.items;
|
|
468
1127
|
return typeof items === 'object' && items !== null;
|
|
469
1128
|
}
|
|
1129
|
+
async function buildComponentRegistry(projectDir) {
|
|
1130
|
+
const registry = new Map();
|
|
1131
|
+
const types = ['atoms', 'molecules', 'organisms'];
|
|
1132
|
+
for (const type of types) {
|
|
1133
|
+
const dir = path.join(projectDir, 'src', 'components', type);
|
|
1134
|
+
try {
|
|
1135
|
+
const files = await fs.readdir(dir);
|
|
1136
|
+
for (const file of files) {
|
|
1137
|
+
if (!file.endsWith('.tsx'))
|
|
1138
|
+
continue;
|
|
1139
|
+
const fullPath = path.join(dir, file);
|
|
1140
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
1141
|
+
const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
|
|
1142
|
+
const exportedTypes = extractAllExportedTypes(sourceFile);
|
|
1143
|
+
const exportedComponents = extractExportedComponentNames(sourceFile);
|
|
1144
|
+
const allExports = [...new Set([...exportedTypes, ...exportedComponents])];
|
|
1145
|
+
if (allExports.length === 0)
|
|
1146
|
+
continue;
|
|
1147
|
+
const actualFilename = file.replace('.tsx', '');
|
|
1148
|
+
for (const exportName of allExports) {
|
|
1149
|
+
registry.set(exportName, {
|
|
1150
|
+
name: exportName,
|
|
1151
|
+
actualFilename,
|
|
1152
|
+
type,
|
|
1153
|
+
exports: allExports,
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
catch (error) {
|
|
1159
|
+
debugProcess(`[buildComponentRegistry] Could not read ${type} directory: ${String(error)}`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
debugProcess(`[buildComponentRegistry] Indexed ${registry.size} components`);
|
|
1163
|
+
return registry;
|
|
1164
|
+
}
|
|
1165
|
+
function resolveDependenciesToRegistry(dependencies, registry) {
|
|
1166
|
+
const primary = [];
|
|
1167
|
+
debugProcess(`[resolveDependenciesToRegistry] Processing ${dependencies.length} dependencies: ${dependencies.map((d) => `${d.type}/${d.name}`).join(', ')}`);
|
|
1168
|
+
for (const dep of dependencies) {
|
|
1169
|
+
const entry = registry.get(dep.name);
|
|
1170
|
+
if (entry !== undefined) {
|
|
1171
|
+
debugProcess(`[resolveDependenciesToRegistry] Found registry entry for ${dep.name}`);
|
|
1172
|
+
primary.push(entry);
|
|
1173
|
+
}
|
|
1174
|
+
else {
|
|
1175
|
+
debugProcess(`[resolveDependenciesToRegistry] No registry entry for ${dep.name}`);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
const available = Array.from(registry.values()).filter((entry) => entry.type === 'atoms');
|
|
1179
|
+
debugProcess(`[resolveDependenciesToRegistry] Resolved ${primary.length} primary dependencies`);
|
|
1180
|
+
return { primary, available };
|
|
1181
|
+
}
|
|
470
1182
|
async function readDesignSystem(providedPath, refs) {
|
|
471
1183
|
const start = performance.now();
|
|
472
1184
|
const candidates = [];
|