@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
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { mkdir, readFile
|
|
2
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { type Command, defineCommandHandler, type Event } from '@auto-engineer/message-bus';
|
|
5
|
+
import { createModelFromEnv } from '@auto-engineer/model-factory';
|
|
5
6
|
import createDebug from 'debug';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import {
|
|
8
|
+
buildPipelineSteps,
|
|
9
|
+
type PipelineConfig,
|
|
10
|
+
type PipelineContext,
|
|
11
|
+
type PipelineModels,
|
|
12
|
+
runPipeline,
|
|
13
|
+
} from '../pipeline/run-pipeline';
|
|
14
|
+
import { buildFullProjectSection } from '../project-context';
|
|
10
15
|
|
|
11
16
|
const debug = createDebug('auto:component-implementor-react:command');
|
|
12
17
|
|
|
@@ -18,6 +23,22 @@ type ComponentJobPayload = {
|
|
|
18
23
|
styling: string[];
|
|
19
24
|
storybookPath: string;
|
|
20
25
|
files: { create?: string[]; modify?: string[] };
|
|
26
|
+
composes: { id: string; path: string }[];
|
|
27
|
+
props?: Array<{
|
|
28
|
+
name: string;
|
|
29
|
+
type: string;
|
|
30
|
+
required: boolean;
|
|
31
|
+
default?: string;
|
|
32
|
+
description: string;
|
|
33
|
+
category: 'data' | 'callback' | 'slot' | 'visual' | 'state' | 'config';
|
|
34
|
+
}>;
|
|
35
|
+
storyVariants?: Array<{
|
|
36
|
+
name: string;
|
|
37
|
+
description: string;
|
|
38
|
+
args: Record<string, unknown>;
|
|
39
|
+
needsPlayFunction?: boolean;
|
|
40
|
+
playDescription?: string;
|
|
41
|
+
}>;
|
|
21
42
|
};
|
|
22
43
|
|
|
23
44
|
type ComponentJob = {
|
|
@@ -63,6 +84,18 @@ function pascalCase(id: string): string {
|
|
|
63
84
|
.join('');
|
|
64
85
|
}
|
|
65
86
|
|
|
87
|
+
function filePathToImportAlias(filePath: string): string {
|
|
88
|
+
let rel = filePath;
|
|
89
|
+
if (rel.startsWith('./')) {
|
|
90
|
+
rel = rel.slice(2);
|
|
91
|
+
}
|
|
92
|
+
if (rel.startsWith('src/')) {
|
|
93
|
+
rel = rel.slice(4);
|
|
94
|
+
}
|
|
95
|
+
rel = rel.replace(/\.tsx?$/, '');
|
|
96
|
+
return `@/${rel}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
66
99
|
function deriveFilePaths(
|
|
67
100
|
targetDir: string,
|
|
68
101
|
payload: ComponentJobPayload,
|
|
@@ -81,6 +114,65 @@ function deriveFilePaths(
|
|
|
81
114
|
return { componentPath, testPath, storyPath, componentName };
|
|
82
115
|
}
|
|
83
116
|
|
|
117
|
+
function buildPipelineConfig(): PipelineConfig {
|
|
118
|
+
return {
|
|
119
|
+
models: {
|
|
120
|
+
generateTest:
|
|
121
|
+
process.env.STEP_GENERATE_TEST_MODEL ??
|
|
122
|
+
process.env.IMPL_MODEL ??
|
|
123
|
+
process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
|
|
124
|
+
'',
|
|
125
|
+
generateComponent:
|
|
126
|
+
process.env.STEP_GENERATE_COMPONENT_MODEL ??
|
|
127
|
+
process.env.IMPL_MODEL ??
|
|
128
|
+
process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
|
|
129
|
+
'',
|
|
130
|
+
typeFixer:
|
|
131
|
+
process.env.STEP_TYPE_FIXER_MODEL ??
|
|
132
|
+
process.env.IMPL_FIXER_MODEL ??
|
|
133
|
+
process.env.IMPL_MODEL ??
|
|
134
|
+
process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
|
|
135
|
+
'',
|
|
136
|
+
testFixer:
|
|
137
|
+
process.env.STEP_TEST_FIXER_MODEL ??
|
|
138
|
+
process.env.IMPL_FIXER_MODEL ??
|
|
139
|
+
process.env.IMPL_MODEL ??
|
|
140
|
+
process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
|
|
141
|
+
'',
|
|
142
|
+
lintFixer:
|
|
143
|
+
process.env.STEP_LINT_FIXER_MODEL ??
|
|
144
|
+
process.env.IMPL_FIXER_MODEL ??
|
|
145
|
+
process.env.IMPL_MODEL ??
|
|
146
|
+
process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
|
|
147
|
+
'',
|
|
148
|
+
storyFixer:
|
|
149
|
+
process.env.STEP_STORY_FIXER_MODEL ??
|
|
150
|
+
process.env.IMPL_FIXER_MODEL ??
|
|
151
|
+
process.env.IMPL_MODEL ??
|
|
152
|
+
process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
|
|
153
|
+
'',
|
|
154
|
+
},
|
|
155
|
+
maxTypeFixIterations: 3,
|
|
156
|
+
maxTestFixIterations: 3,
|
|
157
|
+
maxLintFixIterations: 2,
|
|
158
|
+
maxStoryFixIterations: 2,
|
|
159
|
+
enableStorybookTest: process.env.ENABLE_STORYBOOK_TEST === 'true',
|
|
160
|
+
enableVisualTest: process.env.ENABLE_VISUAL_TEST === 'true',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createPipelineModels(): PipelineModels {
|
|
165
|
+
const model = createModelFromEnv();
|
|
166
|
+
return {
|
|
167
|
+
generateTest: model,
|
|
168
|
+
generateComponent: model,
|
|
169
|
+
typeFixer: model,
|
|
170
|
+
testFixer: model,
|
|
171
|
+
lintFixer: model,
|
|
172
|
+
storyFixer: model,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
84
176
|
export async function handleImplementComponent(
|
|
85
177
|
command: ImplementComponentCommand,
|
|
86
178
|
): Promise<ComponentImplementedEvent | ComponentImplementationFailedEvent> {
|
|
@@ -88,16 +180,12 @@ export async function handleImplementComponent(
|
|
|
88
180
|
const { payload } = job;
|
|
89
181
|
const { componentPath, testPath, storyPath, componentName } = deriveFilePaths(targetDir, payload);
|
|
90
182
|
const isModify = (payload.files.modify?.length ?? 0) > 0;
|
|
183
|
+
const rawPath = payload.files.modify?.[0] ?? payload.files.create?.[0] ?? '';
|
|
184
|
+
const componentImportPath = filePathToImportAlias(rawPath);
|
|
91
185
|
|
|
92
|
-
debug('Implementing component: %s', componentName);
|
|
186
|
+
debug('Implementing component: %s (mode: %s)', componentName, isModify ? 'modify' : 'create');
|
|
93
187
|
|
|
94
188
|
try {
|
|
95
|
-
let existingComponent: string | undefined;
|
|
96
|
-
if (isModify && existsSync(componentPath)) {
|
|
97
|
-
existingComponent = await readFile(componentPath, 'utf-8');
|
|
98
|
-
debug('Read existing component: %d chars', existingComponent.length);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
189
|
const specDeltas = {
|
|
102
190
|
structure: payload.structure,
|
|
103
191
|
rendering: payload.rendering,
|
|
@@ -105,31 +193,67 @@ export async function handleImplementComponent(
|
|
|
105
193
|
styling: payload.styling,
|
|
106
194
|
};
|
|
107
195
|
|
|
108
|
-
debug('
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
196
|
+
debug('Building project context...');
|
|
197
|
+
const projectSection = await buildFullProjectSection(targetDir, payload.composes);
|
|
198
|
+
|
|
199
|
+
let existingComponent: string | undefined;
|
|
200
|
+
let existingTest: string | undefined;
|
|
201
|
+
|
|
202
|
+
if (isModify && existsSync(componentPath)) {
|
|
203
|
+
existingComponent = await readFile(componentPath, 'utf-8');
|
|
204
|
+
existingTest = existsSync(testPath) ? await readFile(testPath, 'utf-8') : undefined;
|
|
205
|
+
}
|
|
114
206
|
|
|
115
|
-
|
|
116
|
-
|
|
207
|
+
await mkdir(path.dirname(testPath), { recursive: true });
|
|
208
|
+
|
|
209
|
+
const ctx: PipelineContext = {
|
|
117
210
|
componentName,
|
|
211
|
+
componentPath,
|
|
212
|
+
testPath,
|
|
213
|
+
storyPath,
|
|
214
|
+
componentImportPath,
|
|
215
|
+
targetDir,
|
|
118
216
|
specDeltas,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
217
|
+
projectSection,
|
|
218
|
+
props: payload.props,
|
|
219
|
+
composes: payload.composes,
|
|
122
220
|
existingComponent,
|
|
123
|
-
|
|
221
|
+
existingTest,
|
|
222
|
+
isModify,
|
|
223
|
+
storyVariants: payload.storyVariants,
|
|
224
|
+
llmCalls: 0,
|
|
225
|
+
fixIterations: 0,
|
|
226
|
+
};
|
|
124
227
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
228
|
+
const config = buildPipelineConfig();
|
|
229
|
+
const models = createPipelineModels();
|
|
230
|
+
|
|
231
|
+
debug('Running TDD pipeline...');
|
|
232
|
+
const steps = buildPipelineSteps(models, config, ctx);
|
|
233
|
+
const result = await runPipeline(steps, ctx);
|
|
131
234
|
|
|
132
|
-
|
|
235
|
+
if (!result.success) {
|
|
236
|
+
debug(
|
|
237
|
+
'Pipeline failed: %s (LLM calls: %d, fix iterations: %d)',
|
|
238
|
+
result.error,
|
|
239
|
+
result.llmCalls,
|
|
240
|
+
result.fixIterations,
|
|
241
|
+
);
|
|
242
|
+
return {
|
|
243
|
+
type: 'ComponentImplementationFailed',
|
|
244
|
+
data: { error: result.error ?? 'Pipeline failed', name: componentName },
|
|
245
|
+
timestamp: new Date(),
|
|
246
|
+
requestId: command.requestId,
|
|
247
|
+
correlationId: command.correlationId,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
debug(
|
|
252
|
+
'Pipeline succeeded for %s (LLM calls: %d, fix iterations: %d)',
|
|
253
|
+
componentName,
|
|
254
|
+
result.llmCalls,
|
|
255
|
+
result.fixIterations,
|
|
256
|
+
);
|
|
133
257
|
|
|
134
258
|
return {
|
|
135
259
|
type: 'ComponentImplemented',
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
2
|
+
// Extract exported names from a TypeScript/TSX file using the TS AST
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parse a TypeScript/TSX source string and return all exported value names
|
|
9
|
+
* (functions, constants, classes) — excludes type-only exports.
|
|
10
|
+
*/
|
|
11
|
+
export function extractExportedNames(content: string): string[] {
|
|
12
|
+
const sourceFile = ts.createSourceFile('temp.tsx', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
13
|
+
|
|
14
|
+
const names: string[] = [];
|
|
15
|
+
|
|
16
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
17
|
+
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
|
|
18
|
+
const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
19
|
+
|
|
20
|
+
// export function Foo() { ... }
|
|
21
|
+
if (ts.isFunctionDeclaration(node) && isExported && node.name) {
|
|
22
|
+
names.push(node.name.text);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// export const Foo = ...
|
|
26
|
+
if (ts.isVariableStatement(node) && isExported) {
|
|
27
|
+
for (const decl of node.declarationList.declarations) {
|
|
28
|
+
if (ts.isIdentifier(decl.name)) {
|
|
29
|
+
names.push(decl.name.text);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// export class Foo { ... }
|
|
35
|
+
if (ts.isClassDeclaration(node) && isExported && node.name) {
|
|
36
|
+
names.push(node.name.text);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// export { Foo, Bar, Baz }
|
|
40
|
+
if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
41
|
+
// Skip type-only re-exports: export type { Foo }
|
|
42
|
+
if (node.isTypeOnly) return;
|
|
43
|
+
|
|
44
|
+
for (const spec of node.exportClause.elements) {
|
|
45
|
+
// Skip individual type-only exports: export { type Foo, Bar }
|
|
46
|
+
if (spec.isTypeOnly) continue;
|
|
47
|
+
names.push(spec.name.text);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return names;
|
|
53
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
2
|
+
// Deterministic story generator — no LLM call, pure template logic
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
export interface DeterministicStoryProp {
|
|
6
|
+
name: string;
|
|
7
|
+
type: string;
|
|
8
|
+
required: boolean;
|
|
9
|
+
default?: string;
|
|
10
|
+
description: string;
|
|
11
|
+
category: 'data' | 'callback' | 'slot' | 'visual' | 'state' | 'config';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface StoryVariant {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
args: Record<string, unknown>;
|
|
18
|
+
needsPlayFunction?: boolean;
|
|
19
|
+
playDescription?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DeterministicStoryInput {
|
|
23
|
+
componentName: string;
|
|
24
|
+
componentImportPath: string;
|
|
25
|
+
props?: DeterministicStoryProp[];
|
|
26
|
+
storyVariants?: StoryVariant[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generates a Storybook CSF3 story file deterministically (no LLM call).
|
|
31
|
+
*
|
|
32
|
+
* Uses structured prop data and optional story variants to emit a complete
|
|
33
|
+
* `.stories.tsx` file string. When no variants are provided, a Default
|
|
34
|
+
* story is synthesized from the props with sensible placeholder values.
|
|
35
|
+
*/
|
|
36
|
+
export function generateStoryDeterministic(input: DeterministicStoryInput): string {
|
|
37
|
+
const { componentName, componentImportPath, props, storyVariants } = input;
|
|
38
|
+
|
|
39
|
+
const lines: string[] = [];
|
|
40
|
+
|
|
41
|
+
// ── Imports ──────────────────────────────────────────────────────
|
|
42
|
+
lines.push(`import type { Meta, StoryObj } from '@storybook/react-vite';`);
|
|
43
|
+
lines.push(`import { ${componentName} } from '${componentImportPath}';`);
|
|
44
|
+
lines.push('');
|
|
45
|
+
|
|
46
|
+
// ── Meta ─────────────────────────────────────────────────────────
|
|
47
|
+
lines.push(`const meta: Meta<typeof ${componentName}> = {`);
|
|
48
|
+
lines.push(` title: 'UI Components/${componentName}',`);
|
|
49
|
+
lines.push(` component: ${componentName},`);
|
|
50
|
+
lines.push(`};`);
|
|
51
|
+
lines.push(`export default meta;`);
|
|
52
|
+
lines.push('');
|
|
53
|
+
|
|
54
|
+
// ── Story type alias ─────────────────────────────────────────────
|
|
55
|
+
lines.push(`type Story = StoryObj<typeof ${componentName}>;`);
|
|
56
|
+
|
|
57
|
+
// ── Stories ──────────────────────────────────────────────────────
|
|
58
|
+
if (storyVariants && storyVariants.length > 0) {
|
|
59
|
+
for (const variant of storyVariants) {
|
|
60
|
+
lines.push('');
|
|
61
|
+
lines.push(`export const ${variant.name}: Story = {`);
|
|
62
|
+
const argsBlock = serializeArgs(variant.args);
|
|
63
|
+
if (argsBlock) {
|
|
64
|
+
lines.push(` args: {`);
|
|
65
|
+
lines.push(argsBlock);
|
|
66
|
+
lines.push(` },`);
|
|
67
|
+
}
|
|
68
|
+
lines.push(`};`);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
// Derive a Default story from props
|
|
72
|
+
const defaultArgs = deriveDefaultArgs(props ?? []);
|
|
73
|
+
lines.push('');
|
|
74
|
+
lines.push(`export const Default: Story = {`);
|
|
75
|
+
const argsBlock = serializeArgs(defaultArgs);
|
|
76
|
+
if (argsBlock) {
|
|
77
|
+
lines.push(` args: {`);
|
|
78
|
+
lines.push(argsBlock);
|
|
79
|
+
lines.push(` },`);
|
|
80
|
+
}
|
|
81
|
+
lines.push(`};`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
lines.push('');
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
89
|
+
// Internals
|
|
90
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Derives sensible default arg values from structured prop definitions.
|
|
94
|
+
*
|
|
95
|
+
* - `callback` props are omitted (Storybook auto-detects them via argTypes).
|
|
96
|
+
* - `slot` props are omitted (ReactNode cannot be serialized to args).
|
|
97
|
+
* - `data` props get placeholder values based on their TypeScript type.
|
|
98
|
+
* - `visual` props use the declared default if present, otherwise a placeholder.
|
|
99
|
+
* - `state` props default to `false`.
|
|
100
|
+
* - `config` props use the declared default if present, otherwise a placeholder.
|
|
101
|
+
*/
|
|
102
|
+
function deriveDefaultArgs(props: DeterministicStoryProp[]): Record<string, unknown> {
|
|
103
|
+
const args: Record<string, unknown> = {};
|
|
104
|
+
|
|
105
|
+
for (const prop of props) {
|
|
106
|
+
// Skip categories that cannot or should not be serialized in args
|
|
107
|
+
if (prop.category === 'callback' || prop.category === 'slot') {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (prop.category === 'state') {
|
|
112
|
+
args[prop.name] = false;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// If a default is declared, try to use it
|
|
117
|
+
if (prop.default !== undefined) {
|
|
118
|
+
const parsed = parseDefaultValue(prop.default);
|
|
119
|
+
if (parsed !== undefined) {
|
|
120
|
+
args[prop.name] = parsed;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Otherwise generate a placeholder based on the TypeScript type string
|
|
126
|
+
args[prop.name] = placeholderForType(prop.type);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return args;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Attempts to parse a default value string into a JS primitive.
|
|
134
|
+
* Returns `undefined` when the string cannot be meaningfully parsed
|
|
135
|
+
* (e.g. an arrow function or complex expression).
|
|
136
|
+
*/
|
|
137
|
+
function parseDefaultValue(raw: string): unknown {
|
|
138
|
+
const trimmed = raw.trim();
|
|
139
|
+
if (trimmed === 'true') return true;
|
|
140
|
+
if (trimmed === 'false') return false;
|
|
141
|
+
if (trimmed === 'null') return null;
|
|
142
|
+
if (trimmed === 'undefined') return undefined;
|
|
143
|
+
|
|
144
|
+
// Quoted string
|
|
145
|
+
const stringMatch = trimmed.match(/^['"](.*)['"]$/);
|
|
146
|
+
if (stringMatch) return stringMatch[1];
|
|
147
|
+
|
|
148
|
+
// Numeric
|
|
149
|
+
const num = Number(trimmed);
|
|
150
|
+
if (!Number.isNaN(num) && trimmed !== '') return num;
|
|
151
|
+
|
|
152
|
+
// Array or object literal — attempt JSON parse
|
|
153
|
+
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(trimmed);
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Anything else (functions, expressions) → skip
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Returns a sensible placeholder value for a given TypeScript type string.
|
|
167
|
+
*/
|
|
168
|
+
function placeholderForType(typeStr: string): unknown {
|
|
169
|
+
const t = typeStr.trim();
|
|
170
|
+
|
|
171
|
+
if (t === 'string') return 'Example';
|
|
172
|
+
if (t === 'number') return 42;
|
|
173
|
+
if (t === 'boolean') return true;
|
|
174
|
+
|
|
175
|
+
// Union of string literals — pick the first literal
|
|
176
|
+
const literalUnion = t.match(/^['"]([^'"]+)['"]/);
|
|
177
|
+
if (literalUnion) return literalUnion[1];
|
|
178
|
+
|
|
179
|
+
// Array types
|
|
180
|
+
if (t.endsWith('[]') || t.startsWith('Array<')) return [];
|
|
181
|
+
|
|
182
|
+
// Generic object / Record
|
|
183
|
+
if (t === 'object' || t.startsWith('Record<') || t.startsWith('{')) return {};
|
|
184
|
+
|
|
185
|
+
// Fallback
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Serializes an args record into indented TypeScript object body lines.
|
|
191
|
+
* Returns an empty string when the args record is empty (caller should
|
|
192
|
+
* omit the `args` block entirely).
|
|
193
|
+
*
|
|
194
|
+
* Each value is formatted as a valid TypeScript literal:
|
|
195
|
+
* - strings → quoted
|
|
196
|
+
* - numbers / booleans → as-is
|
|
197
|
+
* - arrays / plain objects → JSON.stringify
|
|
198
|
+
* - undefined, null, functions → skipped
|
|
199
|
+
*/
|
|
200
|
+
function serializeArgs(args: Record<string, unknown>): string {
|
|
201
|
+
const entries: string[] = [];
|
|
202
|
+
|
|
203
|
+
for (const [key, value] of Object.entries(args)) {
|
|
204
|
+
const formatted = formatArgValue(value);
|
|
205
|
+
if (formatted === null) continue;
|
|
206
|
+
entries.push(` ${key}: ${formatted},`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return entries.join('\n');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function formatArgValue(value: unknown): string | null {
|
|
213
|
+
if (value === undefined) return null;
|
|
214
|
+
if (value === null) return null;
|
|
215
|
+
if (typeof value === 'function') return null;
|
|
216
|
+
|
|
217
|
+
if (typeof value === 'string') {
|
|
218
|
+
// Callback strings like "() => {}" — emit as raw JS, not quoted
|
|
219
|
+
if (/^\s*\(/.test(value) || /^\s*function\s*\(/.test(value)) {
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
return JSON.stringify(value);
|
|
223
|
+
}
|
|
224
|
+
if (typeof value === 'number') return String(value);
|
|
225
|
+
if (typeof value === 'boolean') return String(value);
|
|
226
|
+
|
|
227
|
+
// Arrays and plain objects — deep-fix stringified JSON values first
|
|
228
|
+
if (Array.isArray(value) || typeof value === 'object') {
|
|
229
|
+
const fixed = deepParseStringifiedJson(value);
|
|
230
|
+
return JSON.stringify(fixed);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Recursively walks a value and parses any string that looks like
|
|
238
|
+
* stringified JSON back into an actual object/array. This fixes
|
|
239
|
+
* FEA output where nested objects are accidentally double-stringified.
|
|
240
|
+
*/
|
|
241
|
+
function deepParseStringifiedJson(value: unknown): unknown {
|
|
242
|
+
if (typeof value === 'string') {
|
|
243
|
+
const trimmed = value.trim();
|
|
244
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
245
|
+
try {
|
|
246
|
+
return deepParseStringifiedJson(JSON.parse(trimmed));
|
|
247
|
+
} catch {
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (Array.isArray(value)) {
|
|
255
|
+
return value.map(deepParseStringifiedJson);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (value !== null && typeof value === 'object') {
|
|
259
|
+
const result: Record<string, unknown> = {};
|
|
260
|
+
for (const [k, v] of Object.entries(value)) {
|
|
261
|
+
result[k] = deepParseStringifiedJson(v);
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return value;
|
|
267
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,3 +9,15 @@ export type {
|
|
|
9
9
|
ImplementComponentEvents,
|
|
10
10
|
} from './commands/implement-component';
|
|
11
11
|
export { handleImplementComponent } from './commands/implement-component';
|
|
12
|
+
export { generateStoryDeterministic } from './generate-story-deterministic';
|
|
13
|
+
export type {
|
|
14
|
+
ModelConfig,
|
|
15
|
+
PipelineConfig,
|
|
16
|
+
PipelineContext,
|
|
17
|
+
PipelineModels,
|
|
18
|
+
PipelineResult,
|
|
19
|
+
PipelineStep,
|
|
20
|
+
StepResult,
|
|
21
|
+
} from './pipeline/run-pipeline';
|
|
22
|
+
export { buildPipelineSteps, runPipeline } from './pipeline/run-pipeline';
|
|
23
|
+
export { generateScaffold } from './scaffold';
|