@auto-engineer/component-implementor-react 1.98.0 → 1.100.0
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/.turbo/turbo-test.log +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +92 -0
- package/dist/src/commands/implement-component.d.ts +19 -0
- package/dist/src/commands/implement-component.d.ts.map +1 -1
- package/dist/src/commands/implement-component.js +109 -30
- package/dist/src/commands/implement-component.js.map +1 -1
- package/dist/src/commands/implement-component.test.js +259 -69
- package/dist/src/commands/implement-component.test.js.map +1 -1
- package/dist/src/extract-exports.d.ts +6 -0
- package/dist/src/extract-exports.d.ts.map +1 -0
- package/dist/src/extract-exports.js +46 -0
- package/dist/src/extract-exports.js.map +1 -0
- package/dist/src/generate-story-deterministic.d.ts +30 -0
- package/dist/src/generate-story-deterministic.d.ts.map +1 -0
- package/dist/src/generate-story-deterministic.js +229 -0
- package/dist/src/generate-story-deterministic.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/pipeline/run-pipeline.d.ts +69 -0
- package/dist/src/pipeline/run-pipeline.d.ts.map +1 -0
- package/dist/src/pipeline/run-pipeline.js +78 -0
- package/dist/src/pipeline/run-pipeline.js.map +1 -0
- package/dist/src/pipeline/run-pipeline.test.d.ts +2 -0
- package/dist/src/pipeline/run-pipeline.test.d.ts.map +1 -0
- package/dist/src/pipeline/run-pipeline.test.js +247 -0
- package/dist/src/pipeline/run-pipeline.test.js.map +1 -0
- package/dist/src/pipeline/steps/generate-component.d.ts +4 -0
- package/dist/src/pipeline/steps/generate-component.d.ts.map +1 -0
- package/dist/src/pipeline/steps/generate-component.js +50 -0
- package/dist/src/pipeline/steps/generate-component.js.map +1 -0
- package/dist/src/pipeline/steps/generate-component.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/generate-component.test.js +106 -0
- package/dist/src/pipeline/steps/generate-component.test.js.map +1 -0
- package/dist/src/pipeline/steps/generate-story.d.ts +3 -0
- package/dist/src/pipeline/steps/generate-story.d.ts.map +1 -0
- package/dist/src/pipeline/steps/generate-story.js +14 -0
- package/dist/src/pipeline/steps/generate-story.js.map +1 -0
- package/dist/src/pipeline/steps/generate-story.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/generate-story.test.js +41 -0
- package/dist/src/pipeline/steps/generate-story.test.js.map +1 -0
- package/dist/src/pipeline/steps/generate-test.d.ts +4 -0
- package/dist/src/pipeline/steps/generate-test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/generate-test.js +19 -0
- package/dist/src/pipeline/steps/generate-test.js.map +1 -0
- package/dist/src/pipeline/steps/generate-test.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/generate-test.test.js +60 -0
- package/dist/src/pipeline/steps/generate-test.test.js.map +1 -0
- package/dist/src/pipeline/steps/lint-fix-loop.d.ts +4 -0
- package/dist/src/pipeline/steps/lint-fix-loop.d.ts.map +1 -0
- package/dist/src/pipeline/steps/lint-fix-loop.js +45 -0
- package/dist/src/pipeline/steps/lint-fix-loop.js.map +1 -0
- package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts +2 -0
- package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/lint-fix-loop.test.js +119 -0
- package/dist/src/pipeline/steps/lint-fix-loop.test.js.map +1 -0
- package/dist/src/pipeline/steps/story-fix-loop.d.ts +4 -0
- package/dist/src/pipeline/steps/story-fix-loop.d.ts.map +1 -0
- package/dist/src/pipeline/steps/story-fix-loop.js +34 -0
- package/dist/src/pipeline/steps/story-fix-loop.js.map +1 -0
- package/dist/src/pipeline/steps/story-fix-loop.test.d.ts +2 -0
- package/dist/src/pipeline/steps/story-fix-loop.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/story-fix-loop.test.js +94 -0
- package/dist/src/pipeline/steps/story-fix-loop.test.js.map +1 -0
- package/dist/src/pipeline/steps/storybook-test.d.ts +3 -0
- package/dist/src/pipeline/steps/storybook-test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/storybook-test.js +22 -0
- package/dist/src/pipeline/steps/storybook-test.js.map +1 -0
- package/dist/src/pipeline/steps/storybook-test.test.d.ts +2 -0
- package/dist/src/pipeline/steps/storybook-test.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/storybook-test.test.js +66 -0
- package/dist/src/pipeline/steps/storybook-test.test.js.map +1 -0
- package/dist/src/pipeline/steps/test-fix-loop.d.ts +4 -0
- package/dist/src/pipeline/steps/test-fix-loop.d.ts.map +1 -0
- package/dist/src/pipeline/steps/test-fix-loop.js +44 -0
- package/dist/src/pipeline/steps/test-fix-loop.js.map +1 -0
- package/dist/src/pipeline/steps/test-fix-loop.test.d.ts +2 -0
- package/dist/src/pipeline/steps/test-fix-loop.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/test-fix-loop.test.js +168 -0
- package/dist/src/pipeline/steps/test-fix-loop.test.js.map +1 -0
- package/dist/src/pipeline/steps/type-fix-loop.d.ts +4 -0
- package/dist/src/pipeline/steps/type-fix-loop.d.ts.map +1 -0
- package/dist/src/pipeline/steps/type-fix-loop.js +43 -0
- package/dist/src/pipeline/steps/type-fix-loop.js.map +1 -0
- package/dist/src/pipeline/steps/type-fix-loop.test.d.ts +2 -0
- package/dist/src/pipeline/steps/type-fix-loop.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/type-fix-loop.test.js +112 -0
- package/dist/src/pipeline/steps/type-fix-loop.test.js.map +1 -0
- package/dist/src/pipeline/steps/visual-test.d.ts +3 -0
- package/dist/src/pipeline/steps/visual-test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/visual-test.js +4 -0
- package/dist/src/pipeline/steps/visual-test.js.map +1 -0
- package/dist/src/pipeline/steps/visual-test.test.d.ts +2 -0
- package/dist/src/pipeline/steps/visual-test.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/visual-test.test.js +9 -0
- package/dist/src/pipeline/steps/visual-test.test.js.map +1 -0
- package/dist/src/project-context.d.ts +10 -0
- package/dist/src/project-context.d.ts.map +1 -0
- package/dist/src/project-context.js +178 -0
- package/dist/src/project-context.js.map +1 -0
- package/dist/src/prompt.d.ts +39 -7
- package/dist/src/prompt.d.ts.map +1 -1
- package/dist/src/prompt.js +233 -23
- package/dist/src/prompt.js.map +1 -1
- package/dist/src/prompt.test.js +154 -9
- package/dist/src/prompt.test.js.map +1 -1
- package/dist/src/scaffold.d.ts +49 -0
- package/dist/src/scaffold.d.ts.map +1 -0
- package/dist/src/scaffold.js +208 -0
- package/dist/src/scaffold.js.map +1 -0
- package/dist/src/tools/lint-runner.d.ts +7 -0
- package/dist/src/tools/lint-runner.d.ts.map +1 -0
- package/dist/src/tools/lint-runner.js +48 -0
- package/dist/src/tools/lint-runner.js.map +1 -0
- package/dist/src/tools/lint-runner.test.d.ts +2 -0
- package/dist/src/tools/lint-runner.test.d.ts.map +1 -0
- package/dist/src/tools/lint-runner.test.js +90 -0
- package/dist/src/tools/lint-runner.test.js.map +1 -0
- package/dist/src/tools/storybook-runner.d.ts +6 -0
- package/dist/src/tools/storybook-runner.d.ts.map +1 -0
- package/dist/src/tools/storybook-runner.js +25 -0
- package/dist/src/tools/storybook-runner.js.map +1 -0
- package/dist/src/tools/storybook-runner.test.d.ts +2 -0
- package/dist/src/tools/storybook-runner.test.d.ts.map +1 -0
- package/dist/src/tools/storybook-runner.test.js +43 -0
- package/dist/src/tools/storybook-runner.test.js.map +1 -0
- package/dist/src/tools/test-runner.d.ts +9 -0
- package/dist/src/tools/test-runner.d.ts.map +1 -0
- package/dist/src/tools/test-runner.js +74 -0
- package/dist/src/tools/test-runner.js.map +1 -0
- package/dist/src/tools/test-runner.test.d.ts +2 -0
- package/dist/src/tools/test-runner.test.d.ts.map +1 -0
- package/dist/src/tools/test-runner.test.js +177 -0
- package/dist/src/tools/test-runner.test.js.map +1 -0
- package/dist/src/tools/type-checker.d.ts +6 -0
- package/dist/src/tools/type-checker.d.ts.map +1 -0
- package/dist/src/tools/type-checker.js +36 -0
- package/dist/src/tools/type-checker.js.map +1 -0
- package/dist/src/tools/type-checker.test.d.ts +2 -0
- package/dist/src/tools/type-checker.test.d.ts.map +1 -0
- package/dist/src/tools/type-checker.test.js +96 -0
- package/dist/src/tools/type-checker.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/inputs/model-a/spec-deltas.json +1460 -0
- package/inputs/model-b/spec-deltas.json +1424 -0
- package/inputs/model-c/spec-deltas.json +1432 -0
- package/inputs/model-d/spec-deltas.json +967 -0
- package/inputs/model-e/spec-deltas.json +2292 -0
- package/ketchup-plan.md +43 -8
- package/package.json +3 -3
- package/scoring-heuristic.md +138 -0
- package/scripts/improve.ts +23 -18
- package/src/commands/implement-component.test.ts +309 -76
- package/src/commands/implement-component.ts +155 -31
- package/src/extract-exports.ts +53 -0
- package/src/generate-story-deterministic.ts +267 -0
- package/src/index.ts +12 -0
- package/src/pipeline/run-pipeline.test.ts +292 -0
- package/src/pipeline/run-pipeline.ts +160 -0
- package/src/pipeline/steps/generate-component.test.ts +130 -0
- package/src/pipeline/steps/generate-component.ts +60 -0
- package/src/pipeline/steps/generate-story.test.ts +54 -0
- package/src/pipeline/steps/generate-story.ts +17 -0
- package/src/pipeline/steps/generate-test.test.ts +75 -0
- package/src/pipeline/steps/generate-test.ts +25 -0
- package/src/pipeline/steps/lint-fix-loop.test.ts +155 -0
- package/src/pipeline/steps/lint-fix-loop.ts +59 -0
- package/src/pipeline/steps/story-fix-loop.test.ts +123 -0
- package/src/pipeline/steps/story-fix-loop.ts +47 -0
- package/src/pipeline/steps/storybook-test.test.ts +82 -0
- package/src/pipeline/steps/storybook-test.ts +27 -0
- package/src/pipeline/steps/test-fix-loop.test.ts +201 -0
- package/src/pipeline/steps/test-fix-loop.ts +56 -0
- package/src/pipeline/steps/type-fix-loop.test.ts +145 -0
- package/src/pipeline/steps/type-fix-loop.ts +55 -0
- package/src/pipeline/steps/visual-test.test.ts +10 -0
- package/src/pipeline/steps/visual-test.ts +5 -0
- package/src/project-context.ts +205 -0
- package/src/prompt.test.ts +174 -8
- package/src/prompt.ts +301 -23
- package/src/scaffold.ts +281 -0
- package/src/tools/lint-runner.test.ts +112 -0
- package/src/tools/lint-runner.ts +52 -0
- package/src/tools/storybook-runner.test.ts +53 -0
- package/src/tools/storybook-runner.ts +29 -0
- package/src/tools/test-runner.test.ts +213 -0
- package/src/tools/test-runner.ts +84 -0
- package/src/tools/type-checker.test.ts +120 -0
- package/src/tools/type-checker.ts +42 -0
- package/vitest.config.ts +9 -1
- package/dist/src/generate-component.d.ts +0 -4
- package/dist/src/generate-component.d.ts.map +0 -1
- package/dist/src/generate-component.js +0 -14
- package/dist/src/generate-component.js.map +0 -1
- package/dist/src/generate-component.test.d.ts.map +0 -1
- package/dist/src/generate-component.test.js +0 -73
- package/dist/src/generate-component.test.js.map +0 -1
- package/dist/src/generate-story.d.ts +0 -4
- package/dist/src/generate-story.d.ts.map +0 -1
- package/dist/src/generate-story.js +0 -14
- package/dist/src/generate-story.js.map +0 -1
- package/dist/src/generate-story.test.d.ts.map +0 -1
- package/dist/src/generate-story.test.js +0 -58
- package/dist/src/generate-story.test.js.map +0 -1
- package/dist/src/generate-test.d.ts +0 -4
- package/dist/src/generate-test.d.ts.map +0 -1
- package/dist/src/generate-test.js +0 -14
- package/dist/src/generate-test.js.map +0 -1
- package/dist/src/generate-test.test.d.ts.map +0 -1
- package/dist/src/generate-test.test.js +0 -77
- package/dist/src/generate-test.test.js.map +0 -1
- package/dist/src/reconcile.d.ts +0 -8
- package/dist/src/reconcile.d.ts.map +0 -1
- package/dist/src/reconcile.js +0 -18
- package/dist/src/reconcile.js.map +0 -1
- package/dist/src/reconcile.test.d.ts +0 -2
- package/dist/src/reconcile.test.d.ts.map +0 -1
- package/dist/src/reconcile.test.js +0 -108
- package/dist/src/reconcile.test.js.map +0 -1
- package/src/generate-component.test.ts +0 -89
- package/src/generate-component.ts +0 -16
- package/src/generate-story.test.ts +0 -71
- package/src/generate-story.ts +0 -16
- package/src/generate-test.test.ts +0 -93
- package/src/generate-test.ts +0 -16
- package/src/reconcile.test.ts +0 -127
- package/src/reconcile.ts +0 -27
- /package/dist/src/{generate-component.test.d.ts → pipeline/steps/generate-component.test.d.ts} +0 -0
- /package/dist/src/{generate-story.test.d.ts → pipeline/steps/generate-story.test.d.ts} +0 -0
- /package/dist/src/{generate-test.test.d.ts → pipeline/steps/generate-test.test.d.ts} +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('ai', () => ({
|
|
4
|
+
generateText: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock('node:fs/promises', () => ({
|
|
8
|
+
readFile: vi.fn(),
|
|
9
|
+
writeFile: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('../../tools/test-runner', () => ({
|
|
13
|
+
runTests: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
17
|
+
import { generateText } from 'ai';
|
|
18
|
+
import { runTests } from '../../tools/test-runner';
|
|
19
|
+
import type { PipelineContext } from '../run-pipeline';
|
|
20
|
+
import { testFixLoop } from './test-fix-loop';
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
|
|
27
|
+
return {
|
|
28
|
+
componentName: 'MyButton',
|
|
29
|
+
componentPath: '/project/src/MyButton.tsx',
|
|
30
|
+
testPath: '/project/src/MyButton.test.tsx',
|
|
31
|
+
storyPath: '/project/src/MyButton.stories.tsx',
|
|
32
|
+
componentImportPath: '@/components/ui/MyButton',
|
|
33
|
+
targetDir: '/project',
|
|
34
|
+
specDeltas: { structure: [], rendering: [], interaction: [], styling: [] },
|
|
35
|
+
projectSection: '',
|
|
36
|
+
composes: [],
|
|
37
|
+
isModify: false,
|
|
38
|
+
llmCalls: 0,
|
|
39
|
+
fixIterations: 0,
|
|
40
|
+
componentCode: 'original component',
|
|
41
|
+
testCode: 'original test',
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('testFixLoop', () => {
|
|
47
|
+
it('returns success immediately when all tests pass', async () => {
|
|
48
|
+
vi.mocked(runTests).mockReturnValue({
|
|
49
|
+
passed: true,
|
|
50
|
+
numPassed: 3,
|
|
51
|
+
numFailed: 0,
|
|
52
|
+
failures: [],
|
|
53
|
+
output: '',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await testFixLoop('mock-model' as never, makeCtx(), 3);
|
|
57
|
+
|
|
58
|
+
expect(result).toEqual({ success: true });
|
|
59
|
+
expect(generateText).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('calls LLM to fix failures and writes fixed files', async () => {
|
|
63
|
+
vi.mocked(runTests)
|
|
64
|
+
.mockReturnValueOnce({
|
|
65
|
+
passed: false,
|
|
66
|
+
numPassed: 1,
|
|
67
|
+
numFailed: 1,
|
|
68
|
+
failures: ['renders button: element not found'],
|
|
69
|
+
output: 'FAIL',
|
|
70
|
+
})
|
|
71
|
+
.mockReturnValueOnce({
|
|
72
|
+
passed: true,
|
|
73
|
+
numPassed: 2,
|
|
74
|
+
numFailed: 0,
|
|
75
|
+
failures: [],
|
|
76
|
+
output: 'PASS',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
80
|
+
text: '```tsx\nfixed component\n```\n```tsx\nfixed test\n```',
|
|
81
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
82
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
83
|
+
|
|
84
|
+
const result = await testFixLoop('mock-model' as never, makeCtx(), 3);
|
|
85
|
+
|
|
86
|
+
expect(result).toEqual({ success: true });
|
|
87
|
+
expect(generateText).toHaveBeenCalledTimes(1);
|
|
88
|
+
expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.tsx', 'fixed component', 'utf-8');
|
|
89
|
+
expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.test.tsx', 'fixed test', 'utf-8');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('returns failure after exhausting max iterations', async () => {
|
|
93
|
+
vi.mocked(runTests).mockReturnValue({
|
|
94
|
+
passed: false,
|
|
95
|
+
numPassed: 0,
|
|
96
|
+
numFailed: 1,
|
|
97
|
+
failures: ['persistent failure'],
|
|
98
|
+
output: 'FAIL',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
102
|
+
text: '```tsx\nstill broken\n```\n```tsx\nstill broken test\n```',
|
|
103
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
104
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
105
|
+
vi.mocked(readFile).mockResolvedValue('still broken' as never);
|
|
106
|
+
|
|
107
|
+
const result = await testFixLoop('mock-model' as never, makeCtx(), 1);
|
|
108
|
+
|
|
109
|
+
expect(result).toEqual({
|
|
110
|
+
success: false,
|
|
111
|
+
error: expect.stringContaining('Test failures remain after 1 iterations'),
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('handles single code block response by updating only component', async () => {
|
|
116
|
+
vi.mocked(runTests)
|
|
117
|
+
.mockReturnValueOnce({
|
|
118
|
+
passed: false,
|
|
119
|
+
numPassed: 0,
|
|
120
|
+
numFailed: 1,
|
|
121
|
+
failures: ['failure'],
|
|
122
|
+
output: 'FAIL',
|
|
123
|
+
})
|
|
124
|
+
.mockReturnValueOnce({
|
|
125
|
+
passed: true,
|
|
126
|
+
numPassed: 1,
|
|
127
|
+
numFailed: 0,
|
|
128
|
+
failures: [],
|
|
129
|
+
output: 'PASS',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
133
|
+
text: '```tsx\nfixed component only\n```',
|
|
134
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
135
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
136
|
+
|
|
137
|
+
const result = await testFixLoop('mock-model' as never, makeCtx(), 3);
|
|
138
|
+
|
|
139
|
+
expect(result).toEqual({ success: true });
|
|
140
|
+
expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.tsx', 'fixed component only', 'utf-8');
|
|
141
|
+
expect(writeFile).toHaveBeenCalledTimes(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('returns success when final check passes after last iteration fix', async () => {
|
|
145
|
+
vi.mocked(runTests)
|
|
146
|
+
.mockReturnValueOnce({
|
|
147
|
+
passed: false,
|
|
148
|
+
numPassed: 0,
|
|
149
|
+
numFailed: 1,
|
|
150
|
+
failures: ['failure'],
|
|
151
|
+
output: 'FAIL',
|
|
152
|
+
})
|
|
153
|
+
.mockReturnValueOnce({
|
|
154
|
+
passed: true,
|
|
155
|
+
numPassed: 1,
|
|
156
|
+
numFailed: 0,
|
|
157
|
+
failures: [],
|
|
158
|
+
output: 'PASS',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
162
|
+
text: '```tsx\nfixed\n```\n```tsx\nfixed test\n```',
|
|
163
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
164
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
165
|
+
|
|
166
|
+
const result = await testFixLoop('mock-model' as never, makeCtx(), 1);
|
|
167
|
+
|
|
168
|
+
expect(result).toEqual({ success: true });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('uses empty string fallback when componentCode and testCode are undefined', async () => {
|
|
172
|
+
vi.mocked(runTests)
|
|
173
|
+
.mockReturnValueOnce({
|
|
174
|
+
passed: false,
|
|
175
|
+
numPassed: 0,
|
|
176
|
+
numFailed: 1,
|
|
177
|
+
failures: ['failure'],
|
|
178
|
+
output: 'FAIL',
|
|
179
|
+
})
|
|
180
|
+
.mockReturnValueOnce({
|
|
181
|
+
passed: true,
|
|
182
|
+
numPassed: 1,
|
|
183
|
+
numFailed: 0,
|
|
184
|
+
failures: [],
|
|
185
|
+
output: 'PASS',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
189
|
+
text: '```tsx\nfixed\n```\n```tsx\nfixed test\n```',
|
|
190
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
191
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
192
|
+
|
|
193
|
+
const result = await testFixLoop(
|
|
194
|
+
'mock-model' as never,
|
|
195
|
+
makeCtx({ componentCode: undefined, testCode: undefined }),
|
|
196
|
+
1,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
expect(result).toEqual({ success: true });
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import type { LanguageModel } from 'ai';
|
|
3
|
+
import { generateText } from 'ai';
|
|
4
|
+
import { extractCodeBlocks } from '../../extract-code-block';
|
|
5
|
+
import { buildTestFixPrompt } from '../../prompt';
|
|
6
|
+
import { runTests } from '../../tools/test-runner';
|
|
7
|
+
import type { PipelineContext, StepResult } from '../run-pipeline';
|
|
8
|
+
|
|
9
|
+
export async function testFixLoop(
|
|
10
|
+
model: LanguageModel,
|
|
11
|
+
ctx: PipelineContext,
|
|
12
|
+
maxIterations: number,
|
|
13
|
+
): Promise<StepResult> {
|
|
14
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
15
|
+
const result = runTests(ctx.testPath, ctx.targetDir);
|
|
16
|
+
|
|
17
|
+
if (result.passed) {
|
|
18
|
+
return { success: true };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { system, prompt } = buildTestFixPrompt({
|
|
22
|
+
componentCode: ctx.componentCode ?? '',
|
|
23
|
+
testCode: ctx.testCode ?? '',
|
|
24
|
+
failures: result.failures,
|
|
25
|
+
testOutput: result.output,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const { text } = await generateText({ model, system, prompt });
|
|
29
|
+
ctx.llmCalls++;
|
|
30
|
+
ctx.fixIterations++;
|
|
31
|
+
|
|
32
|
+
const blocks = extractCodeBlocks(text);
|
|
33
|
+
if (blocks.length >= 2) {
|
|
34
|
+
ctx.componentCode = blocks[0];
|
|
35
|
+
ctx.testCode = blocks[1];
|
|
36
|
+
await writeFile(ctx.componentPath, blocks[0], 'utf-8');
|
|
37
|
+
await writeFile(ctx.testPath, blocks[1], 'utf-8');
|
|
38
|
+
} else if (blocks.length === 1) {
|
|
39
|
+
ctx.componentCode = blocks[0];
|
|
40
|
+
await writeFile(ctx.componentPath, blocks[0], 'utf-8');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const finalResult = runTests(ctx.testPath, ctx.targetDir);
|
|
45
|
+
if (finalResult.passed) {
|
|
46
|
+
return { success: true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ctx.componentCode = await readFile(ctx.componentPath, 'utf-8');
|
|
50
|
+
ctx.testCode = await readFile(ctx.testPath, 'utf-8');
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
error: `Test failures remain after ${maxIterations} iterations: ${finalResult.failures.join('; ')}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('ai', () => ({
|
|
4
|
+
generateText: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock('node:fs/promises', () => ({
|
|
8
|
+
readFile: vi.fn(),
|
|
9
|
+
writeFile: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('../../tools/type-checker', () => ({
|
|
13
|
+
runTypeCheck: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
17
|
+
import { generateText } from 'ai';
|
|
18
|
+
import { runTypeCheck } from '../../tools/type-checker';
|
|
19
|
+
import type { PipelineContext } from '../run-pipeline';
|
|
20
|
+
import { typeFixLoop } from './type-fix-loop';
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
|
|
27
|
+
return {
|
|
28
|
+
componentName: 'MyButton',
|
|
29
|
+
componentPath: '/project/src/MyButton.tsx',
|
|
30
|
+
testPath: '/project/src/MyButton.test.tsx',
|
|
31
|
+
storyPath: '/project/src/MyButton.stories.tsx',
|
|
32
|
+
componentImportPath: '@/components/ui/MyButton',
|
|
33
|
+
targetDir: '/project',
|
|
34
|
+
specDeltas: { structure: [], rendering: [], interaction: [], styling: [] },
|
|
35
|
+
projectSection: '',
|
|
36
|
+
composes: [],
|
|
37
|
+
isModify: false,
|
|
38
|
+
llmCalls: 0,
|
|
39
|
+
fixIterations: 0,
|
|
40
|
+
componentCode: 'original component',
|
|
41
|
+
testCode: 'original test',
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('typeFixLoop', () => {
|
|
47
|
+
it('returns success immediately when type check passes', async () => {
|
|
48
|
+
vi.mocked(runTypeCheck).mockReturnValue({ passed: true, errors: [] });
|
|
49
|
+
|
|
50
|
+
const result = await typeFixLoop('mock-model' as never, makeCtx(), 3);
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual({ success: true });
|
|
53
|
+
expect(generateText).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('calls LLM to fix type errors and writes fixed files', async () => {
|
|
57
|
+
vi.mocked(runTypeCheck)
|
|
58
|
+
.mockReturnValueOnce({ passed: false, errors: ['error TS2307: Module not found'] })
|
|
59
|
+
.mockReturnValueOnce({ passed: true, errors: [] });
|
|
60
|
+
|
|
61
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
62
|
+
text: '```tsx\nfixed component\n```\n```tsx\nfixed test\n```',
|
|
63
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
64
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
65
|
+
|
|
66
|
+
const result = await typeFixLoop('mock-model' as never, makeCtx(), 3);
|
|
67
|
+
|
|
68
|
+
expect(result).toEqual({ success: true });
|
|
69
|
+
expect(generateText).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.tsx', 'fixed component', 'utf-8');
|
|
71
|
+
expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.test.tsx', 'fixed test', 'utf-8');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns failure after exhausting max iterations', async () => {
|
|
75
|
+
vi.mocked(runTypeCheck).mockReturnValue({
|
|
76
|
+
passed: false,
|
|
77
|
+
errors: ['persistent error'],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
81
|
+
text: '```tsx\nstill broken\n```\n```tsx\nstill broken test\n```',
|
|
82
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
83
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
84
|
+
vi.mocked(readFile).mockResolvedValue('still broken' as never);
|
|
85
|
+
|
|
86
|
+
const result = await typeFixLoop('mock-model' as never, makeCtx(), 2);
|
|
87
|
+
|
|
88
|
+
expect(result).toEqual({
|
|
89
|
+
success: false,
|
|
90
|
+
error: expect.stringContaining('Type errors remain after 2 iterations'),
|
|
91
|
+
});
|
|
92
|
+
expect(generateText).toHaveBeenCalledTimes(2);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('handles single code block response by updating only component', async () => {
|
|
96
|
+
vi.mocked(runTypeCheck)
|
|
97
|
+
.mockReturnValueOnce({ passed: false, errors: ['error in component'] })
|
|
98
|
+
.mockReturnValueOnce({ passed: true, errors: [] });
|
|
99
|
+
|
|
100
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
101
|
+
text: '```tsx\nfixed component only\n```',
|
|
102
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
103
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
104
|
+
|
|
105
|
+
const result = await typeFixLoop('mock-model' as never, makeCtx(), 3);
|
|
106
|
+
|
|
107
|
+
expect(result).toEqual({ success: true });
|
|
108
|
+
expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.tsx', 'fixed component only', 'utf-8');
|
|
109
|
+
expect(writeFile).toHaveBeenCalledTimes(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns success when final check passes after last iteration fix', async () => {
|
|
113
|
+
vi.mocked(runTypeCheck)
|
|
114
|
+
.mockReturnValueOnce({ passed: false, errors: ['error'] })
|
|
115
|
+
.mockReturnValueOnce({ passed: true, errors: [] });
|
|
116
|
+
|
|
117
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
118
|
+
text: '```tsx\nfixed\n```\n```tsx\nfixed test\n```',
|
|
119
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
120
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
121
|
+
|
|
122
|
+
const result = await typeFixLoop('mock-model' as never, makeCtx(), 1);
|
|
123
|
+
|
|
124
|
+
expect(result).toEqual({ success: true });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('uses empty string fallback when componentCode and testCode are undefined', async () => {
|
|
128
|
+
vi.mocked(runTypeCheck)
|
|
129
|
+
.mockReturnValueOnce({ passed: false, errors: ['error'] })
|
|
130
|
+
.mockReturnValueOnce({ passed: true, errors: [] });
|
|
131
|
+
|
|
132
|
+
vi.mocked(generateText).mockResolvedValue({
|
|
133
|
+
text: '```tsx\nfixed\n```\n```tsx\nfixed test\n```',
|
|
134
|
+
} as Awaited<ReturnType<typeof generateText>>);
|
|
135
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
136
|
+
|
|
137
|
+
const result = await typeFixLoop(
|
|
138
|
+
'mock-model' as never,
|
|
139
|
+
makeCtx({ componentCode: undefined, testCode: undefined }),
|
|
140
|
+
1,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(result).toEqual({ success: true });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import type { LanguageModel } from 'ai';
|
|
3
|
+
import { generateText } from 'ai';
|
|
4
|
+
import { extractCodeBlocks } from '../../extract-code-block';
|
|
5
|
+
import { buildTypeFixPrompt } from '../../prompt';
|
|
6
|
+
import { runTypeCheck } from '../../tools/type-checker';
|
|
7
|
+
import type { PipelineContext, StepResult } from '../run-pipeline';
|
|
8
|
+
|
|
9
|
+
export async function typeFixLoop(
|
|
10
|
+
model: LanguageModel,
|
|
11
|
+
ctx: PipelineContext,
|
|
12
|
+
maxIterations: number,
|
|
13
|
+
): Promise<StepResult> {
|
|
14
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
15
|
+
const result = runTypeCheck(ctx.targetDir, [ctx.componentPath, ctx.testPath]);
|
|
16
|
+
|
|
17
|
+
if (result.passed) {
|
|
18
|
+
return { success: true };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { system, prompt } = buildTypeFixPrompt({
|
|
22
|
+
componentCode: ctx.componentCode ?? '',
|
|
23
|
+
testCode: ctx.testCode ?? '',
|
|
24
|
+
errors: result.errors,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const { text } = await generateText({ model, system, prompt });
|
|
28
|
+
ctx.llmCalls++;
|
|
29
|
+
ctx.fixIterations++;
|
|
30
|
+
|
|
31
|
+
const blocks = extractCodeBlocks(text);
|
|
32
|
+
if (blocks.length >= 2) {
|
|
33
|
+
ctx.componentCode = blocks[0];
|
|
34
|
+
ctx.testCode = blocks[1];
|
|
35
|
+
await writeFile(ctx.componentPath, blocks[0], 'utf-8');
|
|
36
|
+
await writeFile(ctx.testPath, blocks[1], 'utf-8');
|
|
37
|
+
} else if (blocks.length === 1) {
|
|
38
|
+
ctx.componentCode = blocks[0];
|
|
39
|
+
await writeFile(ctx.componentPath, blocks[0], 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const finalResult = runTypeCheck(ctx.targetDir, [ctx.componentPath, ctx.testPath]);
|
|
44
|
+
if (finalResult.passed) {
|
|
45
|
+
return { success: true };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ctx.componentCode = await readFile(ctx.componentPath, 'utf-8');
|
|
49
|
+
ctx.testCode = await readFile(ctx.testPath, 'utf-8');
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
error: `Type errors remain after ${maxIterations} iterations: ${finalResult.errors.join('; ')}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { visualTestStep } from './visual-test';
|
|
3
|
+
|
|
4
|
+
describe('visualTestStep', () => {
|
|
5
|
+
it('returns success (placeholder)', async () => {
|
|
6
|
+
const result = await visualTestStep();
|
|
7
|
+
|
|
8
|
+
expect(result).toEqual({ success: true });
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export type ComposedComponent = { id: string; path: string };
|
|
5
|
+
|
|
6
|
+
function kebabToPascal(name: string): string {
|
|
7
|
+
return name
|
|
8
|
+
.split('-')
|
|
9
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
10
|
+
.join('');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract the component name from a file path.
|
|
15
|
+
* e.g. "./src/components/ui/Button.tsx" → "Button"
|
|
16
|
+
* "src/components/MyWidget.tsx" → "MyWidget"
|
|
17
|
+
*/
|
|
18
|
+
function componentNameFromPath(filePath: string): string {
|
|
19
|
+
const lastSlash = filePath.lastIndexOf('/');
|
|
20
|
+
const filename = lastSlash === -1 ? filePath : filePath.slice(lastSlash + 1);
|
|
21
|
+
const dotIndex = filename.lastIndexOf('.');
|
|
22
|
+
return dotIndex === -1 ? filename : filename.slice(0, dotIndex);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Converts a file path like `src/components/ui/Button.tsx` or `src/components/MyFeature`
|
|
27
|
+
* to a `@/` import alias like `@/components/ui/Button` or `@/components/MyFeature`.
|
|
28
|
+
*/
|
|
29
|
+
function filePathToImportAlias(filePath: string): string {
|
|
30
|
+
let rel = filePath;
|
|
31
|
+
if (rel.startsWith('./')) {
|
|
32
|
+
rel = rel.slice(2);
|
|
33
|
+
}
|
|
34
|
+
if (rel.startsWith('src/')) {
|
|
35
|
+
rel = rel.slice(4);
|
|
36
|
+
}
|
|
37
|
+
// Strip .tsx/.ts extension
|
|
38
|
+
rel = rel.replace(/\.tsx?$/, '');
|
|
39
|
+
return `@/${rel}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function readFileSafe(filePath: string): Promise<string | null> {
|
|
43
|
+
try {
|
|
44
|
+
return await readFile(filePath, 'utf-8');
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function buildPackageJsonSection(targetDir: string): Promise<string> {
|
|
51
|
+
const content = await readFileSafe(path.join(targetDir, 'package.json'));
|
|
52
|
+
if (!content) return '';
|
|
53
|
+
|
|
54
|
+
return [
|
|
55
|
+
'## Project Dependencies (package.json)',
|
|
56
|
+
'Only use packages listed here. Do not import packages that are not installed.',
|
|
57
|
+
'',
|
|
58
|
+
content,
|
|
59
|
+
].join('\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function buildBarrelFileSection(targetDir: string): Promise<string> {
|
|
63
|
+
const content = await readFileSafe(path.join(targetDir, 'src', 'components', 'ui', 'index.ts'));
|
|
64
|
+
if (!content) return '';
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
'## Component Library (src/components/ui/index.ts)',
|
|
68
|
+
"These are all available UI components. Import from '@/components/ui'.",
|
|
69
|
+
'',
|
|
70
|
+
content,
|
|
71
|
+
].join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function buildStylesSection(targetDir: string): Promise<string> {
|
|
75
|
+
const content = await readFileSafe(path.join(targetDir, 'src', 'index.css'));
|
|
76
|
+
if (!content) return '';
|
|
77
|
+
|
|
78
|
+
return [
|
|
79
|
+
'## Project Styles (Tailwind CSS v4)',
|
|
80
|
+
"This project uses Tailwind CSS v4. Here is the project's stylesheet with design tokens and base utilities:",
|
|
81
|
+
'',
|
|
82
|
+
content,
|
|
83
|
+
].join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract only the exported interface from a component file:
|
|
88
|
+
* - export type/interface declarations (props types)
|
|
89
|
+
* - export function/const signatures (without function body)
|
|
90
|
+
* - import statements (to understand dependencies)
|
|
91
|
+
*
|
|
92
|
+
* This reduces token usage by ~70% compared to sending full source.
|
|
93
|
+
*/
|
|
94
|
+
function extractSignature(content: string): string {
|
|
95
|
+
const lines = content.split('\n');
|
|
96
|
+
const signatureLines: string[] = [];
|
|
97
|
+
let inExportBlock = false;
|
|
98
|
+
let braceDepth = 0;
|
|
99
|
+
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
|
|
103
|
+
// Always include import statements
|
|
104
|
+
if (trimmed.startsWith('import ')) {
|
|
105
|
+
signatureLines.push(line);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Capture export type / export interface blocks
|
|
110
|
+
if (trimmed.startsWith('export type ') || trimmed.startsWith('export interface ')) {
|
|
111
|
+
inExportBlock = true;
|
|
112
|
+
braceDepth = 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Capture export { ... } re-export blocks (e.g. `export { Select, SelectTrigger, ... }`)
|
|
116
|
+
if (!inExportBlock && trimmed.startsWith('export {')) {
|
|
117
|
+
inExportBlock = true;
|
|
118
|
+
braceDepth = 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Track export function/const signatures (just the signature line)
|
|
122
|
+
if (
|
|
123
|
+
!inExportBlock &&
|
|
124
|
+
(trimmed.startsWith('export function ') ||
|
|
125
|
+
trimmed.startsWith('export const ') ||
|
|
126
|
+
trimmed.startsWith('export default '))
|
|
127
|
+
) {
|
|
128
|
+
// Grab just the signature — stop at opening brace
|
|
129
|
+
const sigLine = trimmed.replace(/\{[\s\S]*$/, '{ ... }');
|
|
130
|
+
signatureLines.push(sigLine);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (inExportBlock) {
|
|
135
|
+
signatureLines.push(line);
|
|
136
|
+
for (const ch of trimmed) {
|
|
137
|
+
if (ch === '{') braceDepth++;
|
|
138
|
+
if (ch === '}') braceDepth--;
|
|
139
|
+
}
|
|
140
|
+
if (braceDepth <= 0 && trimmed.includes('}')) {
|
|
141
|
+
inExportBlock = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return signatureLines.join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function buildComposedComponentsSection(
|
|
150
|
+
targetDir: string,
|
|
151
|
+
composes: ComposedComponent[],
|
|
152
|
+
): Promise<string> {
|
|
153
|
+
if (composes.length === 0) return '';
|
|
154
|
+
|
|
155
|
+
const sections: string[] = [
|
|
156
|
+
'## Composed Components',
|
|
157
|
+
'This component uses the following components. Import each using the exact path shown.',
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
for (const { id, path: componentPath } of composes) {
|
|
161
|
+
const pascalName = componentNameFromPath(componentPath);
|
|
162
|
+
const importAlias = filePathToImportAlias(componentPath);
|
|
163
|
+
|
|
164
|
+
// Try .tsx then without extension
|
|
165
|
+
const candidates = [
|
|
166
|
+
path.join(targetDir, componentPath + '.tsx'),
|
|
167
|
+
path.join(targetDir, componentPath + '.ts'),
|
|
168
|
+
path.join(targetDir, componentPath),
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
let content: string | null = null;
|
|
172
|
+
let resolvedRelative = componentPath;
|
|
173
|
+
for (const candidate of candidates) {
|
|
174
|
+
content = await readFileSafe(candidate);
|
|
175
|
+
if (content) {
|
|
176
|
+
resolvedRelative = path.relative(targetDir, candidate);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!content) continue;
|
|
182
|
+
|
|
183
|
+
// Extract only the signature (types + exports), not full implementation
|
|
184
|
+
const signature = extractSignature(content);
|
|
185
|
+
|
|
186
|
+
sections.push('');
|
|
187
|
+
sections.push(`### ${pascalName} — import from '${importAlias}' (${resolvedRelative})`);
|
|
188
|
+
sections.push('```tsx');
|
|
189
|
+
sections.push(signature);
|
|
190
|
+
sections.push('```');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return sections.join('\n');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function buildFullProjectSection(targetDir: string, composes: ComposedComponent[]): Promise<string> {
|
|
197
|
+
const parts = await Promise.all([
|
|
198
|
+
buildPackageJsonSection(targetDir),
|
|
199
|
+
buildBarrelFileSection(targetDir),
|
|
200
|
+
buildStylesSection(targetDir),
|
|
201
|
+
buildComposedComponentsSection(targetDir, composes),
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
return parts.filter(Boolean).join('\n\n');
|
|
205
|
+
}
|