@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
package/src/scaffold.ts
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
2
|
+
// Scaffold generator — deterministic component skeleton with __BODY__
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
export interface ScaffoldProp {
|
|
6
|
+
name: string;
|
|
7
|
+
type: string;
|
|
8
|
+
required: boolean;
|
|
9
|
+
default?: string;
|
|
10
|
+
description: string;
|
|
11
|
+
category: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ScaffoldComposedComponent {
|
|
15
|
+
id: string;
|
|
16
|
+
path: string;
|
|
17
|
+
/** Actual exported value names from the file, resolved via TS AST. */
|
|
18
|
+
exports?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ScaffoldInput {
|
|
22
|
+
componentName: string;
|
|
23
|
+
props?: ScaffoldProp[];
|
|
24
|
+
composes: ScaffoldComposedComponent[];
|
|
25
|
+
specDeltas: {
|
|
26
|
+
structure: string[];
|
|
27
|
+
rendering: string[];
|
|
28
|
+
interaction: string[];
|
|
29
|
+
styling: string[];
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ScaffoldResult {
|
|
34
|
+
/** The exported props type definition (e.g. `export type ButtonProps = { ... };`) */
|
|
35
|
+
propsInterface: string;
|
|
36
|
+
/** React + composed component import statements */
|
|
37
|
+
importStatements: string;
|
|
38
|
+
/** The function signature line with destructured props */
|
|
39
|
+
functionSignature: string;
|
|
40
|
+
/** Complete file with `__BODY__` placeholder where the LLM fills in the JSX */
|
|
41
|
+
scaffoldedFile: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Converts a file path like `src/components/ui/Button.tsx` or
|
|
46
|
+
* `src/components/MyFeature` to a `@/` import alias like
|
|
47
|
+
* `@/components/ui/Button` or `@/components/MyFeature`.
|
|
48
|
+
*/
|
|
49
|
+
export function filePathToImportAlias(filePath: string): string {
|
|
50
|
+
let rel = filePath;
|
|
51
|
+
if (rel.startsWith('./')) {
|
|
52
|
+
rel = rel.slice(2);
|
|
53
|
+
}
|
|
54
|
+
if (rel.startsWith('src/')) {
|
|
55
|
+
rel = rel.slice(4);
|
|
56
|
+
}
|
|
57
|
+
// Strip .tsx/.ts extension
|
|
58
|
+
rel = rel.replace(/\.tsx?$/, '');
|
|
59
|
+
return `@/${rel}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generates the deterministic scaffold of a React component — imports,
|
|
64
|
+
* props type, function signature, and export — leaving a `__BODY__`
|
|
65
|
+
* placeholder for the LLM to fill in the JSX return statement and any
|
|
66
|
+
* internal hooks/logic.
|
|
67
|
+
*/
|
|
68
|
+
export function generateScaffold(input: ScaffoldInput): ScaffoldResult {
|
|
69
|
+
const { componentName, props = [], composes } = input;
|
|
70
|
+
|
|
71
|
+
const propsTypeName = `${componentName}Props`;
|
|
72
|
+
|
|
73
|
+
// ── Determine if we need React import ────────────────────────────
|
|
74
|
+
const hasSlotProps = props.some((p) => isReactNodeType(p.type));
|
|
75
|
+
|
|
76
|
+
// ── Build import statements ──────────────────────────────────────
|
|
77
|
+
const importLines: string[] = [];
|
|
78
|
+
|
|
79
|
+
if (hasSlotProps) {
|
|
80
|
+
importLines.push(`import React from 'react';`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// cn utility — almost always needed for Tailwind class composition
|
|
84
|
+
importLines.push(`import { cn } from '@/lib/utils';`);
|
|
85
|
+
|
|
86
|
+
// Composed component imports — use AST-resolved exports when available
|
|
87
|
+
for (const composed of composes) {
|
|
88
|
+
const alias = filePathToImportAlias(composed.path);
|
|
89
|
+
|
|
90
|
+
if (composed.exports && composed.exports.length > 0) {
|
|
91
|
+
// Use actual exports from TS AST
|
|
92
|
+
const names = composed.exports.join(', ');
|
|
93
|
+
importLines.push(`import { ${names} } from '${alias}';`);
|
|
94
|
+
} else {
|
|
95
|
+
// Fallback: derive from filename
|
|
96
|
+
const pascalName = componentNameFromPath(composed.path);
|
|
97
|
+
importLines.push(`import { ${pascalName} } from '${alias}';`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const importStatements = importLines.join('\n');
|
|
102
|
+
|
|
103
|
+
// ── Build props interface ────────────────────────────────────────
|
|
104
|
+
const propsInterface = buildPropsInterface(propsTypeName, props);
|
|
105
|
+
|
|
106
|
+
// ── Build function signature ─────────────────────────────────────
|
|
107
|
+
const functionSignature = buildFunctionSignature(componentName, propsTypeName, props);
|
|
108
|
+
|
|
109
|
+
// ── Assemble scaffolded file ─────────────────────────────────────
|
|
110
|
+
const fileParts: string[] = [];
|
|
111
|
+
|
|
112
|
+
fileParts.push(importStatements);
|
|
113
|
+
fileParts.push('');
|
|
114
|
+
fileParts.push(propsInterface);
|
|
115
|
+
fileParts.push('');
|
|
116
|
+
fileParts.push(`${functionSignature} {`);
|
|
117
|
+
fileParts.push(` __BODY__`);
|
|
118
|
+
fileParts.push(`}`);
|
|
119
|
+
fileParts.push('');
|
|
120
|
+
|
|
121
|
+
const scaffoldedFile = fileParts.join('\n');
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
propsInterface,
|
|
125
|
+
importStatements,
|
|
126
|
+
functionSignature,
|
|
127
|
+
scaffoldedFile,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
132
|
+
// Internal helpers
|
|
133
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
134
|
+
|
|
135
|
+
function kebabToPascal(name: string): string {
|
|
136
|
+
return name
|
|
137
|
+
.split('-')
|
|
138
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
139
|
+
.join('');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Extract the component name from a file path.
|
|
144
|
+
* e.g. "./src/components/ui/Button.tsx" → "Button"
|
|
145
|
+
* "src/components/MyWidget.tsx" → "MyWidget"
|
|
146
|
+
*/
|
|
147
|
+
function componentNameFromPath(filePath: string): string {
|
|
148
|
+
const lastSlash = filePath.lastIndexOf('/');
|
|
149
|
+
const filename = lastSlash === -1 ? filePath : filePath.slice(lastSlash + 1);
|
|
150
|
+
const dotIndex = filename.lastIndexOf('.');
|
|
151
|
+
return dotIndex === -1 ? filename : filename.slice(0, dotIndex);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Returns true when the type string represents React.ReactNode or
|
|
156
|
+
* similar renderable slot types.
|
|
157
|
+
*/
|
|
158
|
+
function isReactNodeType(typeStr: string): boolean {
|
|
159
|
+
const t = typeStr.trim();
|
|
160
|
+
return (
|
|
161
|
+
t === 'React.ReactNode' ||
|
|
162
|
+
t === 'ReactNode' ||
|
|
163
|
+
t === 'React.ReactElement' ||
|
|
164
|
+
t === 'ReactElement' ||
|
|
165
|
+
t === 'JSX.Element' ||
|
|
166
|
+
t.includes('ReactNode') ||
|
|
167
|
+
t.includes('ReactElement')
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Builds the exported TypeScript props type definition.
|
|
173
|
+
*
|
|
174
|
+
* Example output:
|
|
175
|
+
* ```
|
|
176
|
+
* export type ButtonProps = {
|
|
177
|
+
* /** Label text for the button * /
|
|
178
|
+
* label: string;
|
|
179
|
+
* /** Whether the button is disabled * /
|
|
180
|
+
* disabled?: boolean;
|
|
181
|
+
* };
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
function buildPropsInterface(typeName: string, props: ScaffoldProp[]): string {
|
|
185
|
+
if (props.length === 0) {
|
|
186
|
+
return `export type ${typeName} = Record<string, never>;`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const lines: string[] = [];
|
|
190
|
+
lines.push(`export type ${typeName} = {`);
|
|
191
|
+
|
|
192
|
+
for (const prop of props) {
|
|
193
|
+
// JSDoc comment for the prop
|
|
194
|
+
if (prop.description) {
|
|
195
|
+
lines.push(` /** ${prop.description} */`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const optional = prop.required ? '' : '?';
|
|
199
|
+
lines.push(` ${prop.name}${optional}: ${prop.type};`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
lines.push(`};`);
|
|
203
|
+
return lines.join('\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Builds the function signature with destructured props and inline
|
|
208
|
+
* defaults where declared.
|
|
209
|
+
*
|
|
210
|
+
* Example output:
|
|
211
|
+
* ```
|
|
212
|
+
* export function Button({ variant = 'primary', onClick, children }: ButtonProps)
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
function buildFunctionSignature(componentName: string, propsTypeName: string, props: ScaffoldProp[]): string {
|
|
216
|
+
if (props.length === 0) {
|
|
217
|
+
return `export function ${componentName}(props: ${propsTypeName})`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const destructuredParts: string[] = [];
|
|
221
|
+
|
|
222
|
+
for (const prop of props) {
|
|
223
|
+
if (prop.default !== undefined && prop.default !== '') {
|
|
224
|
+
// Inline default in destructuring
|
|
225
|
+
const defaultLiteral = formatDefaultForDestructuring(prop.default);
|
|
226
|
+
destructuredParts.push(`${prop.name} = ${defaultLiteral}`);
|
|
227
|
+
} else {
|
|
228
|
+
destructuredParts.push(prop.name);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Decide whether to format single-line or multi-line based on length
|
|
233
|
+
const singleLine = `{ ${destructuredParts.join(', ')} }`;
|
|
234
|
+
const signaturePrefix = `export function ${componentName}(`;
|
|
235
|
+
const signatureSuffix = `: ${propsTypeName})`;
|
|
236
|
+
|
|
237
|
+
if ((signaturePrefix + singleLine + signatureSuffix).length <= 100) {
|
|
238
|
+
return `${signaturePrefix}${singleLine}${signatureSuffix}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Multi-line destructuring
|
|
242
|
+
const multiLines: string[] = [];
|
|
243
|
+
multiLines.push(`${signaturePrefix}{`);
|
|
244
|
+
for (let i = 0; i < destructuredParts.length; i++) {
|
|
245
|
+
const trailing = i < destructuredParts.length - 1 ? ',' : ',';
|
|
246
|
+
multiLines.push(` ${destructuredParts[i]}${trailing}`);
|
|
247
|
+
}
|
|
248
|
+
multiLines.push(`}${signatureSuffix}`);
|
|
249
|
+
return multiLines.join('\n');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Formats a default value string for use in destructuring assignment.
|
|
254
|
+
*
|
|
255
|
+
* If the value is already a valid JS literal (quoted string, number,
|
|
256
|
+
* boolean, array, object), it is returned as-is. Otherwise it is
|
|
257
|
+
* returned unchanged (it may be a constant reference or expression
|
|
258
|
+
* that the LLM-generated body will define).
|
|
259
|
+
*/
|
|
260
|
+
function formatDefaultForDestructuring(raw: string): string {
|
|
261
|
+
const trimmed = raw.trim();
|
|
262
|
+
|
|
263
|
+
// Already looks like a valid literal — return as-is
|
|
264
|
+
if (
|
|
265
|
+
trimmed === 'true' ||
|
|
266
|
+
trimmed === 'false' ||
|
|
267
|
+
trimmed === 'null' ||
|
|
268
|
+
trimmed === 'undefined' ||
|
|
269
|
+
/^-?\d+(\.\d+)?$/.test(trimmed) || // numeric
|
|
270
|
+
/^['"]/.test(trimmed) || // string literal
|
|
271
|
+
trimmed.startsWith('[') || // array literal
|
|
272
|
+
trimmed.startsWith('{') || // object literal
|
|
273
|
+
trimmed.startsWith('`') // template literal
|
|
274
|
+
) {
|
|
275
|
+
return trimmed;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Wrap bare strings that look like identifiers or expressions — return as-is
|
|
279
|
+
// (e.g. a constant name like `DEFAULT_SIZE`)
|
|
280
|
+
return trimmed;
|
|
281
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('node:child_process', () => ({
|
|
4
|
+
execSync: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import { runLint, runLintFix } from './lint-runner';
|
|
9
|
+
|
|
10
|
+
describe('runLint', () => {
|
|
11
|
+
it('returns passed when biome check succeeds', () => {
|
|
12
|
+
vi.mocked(execSync).mockReturnValue('');
|
|
13
|
+
|
|
14
|
+
const result = runLint(['src/Button.tsx'], '/project');
|
|
15
|
+
|
|
16
|
+
expect(result).toEqual({ passed: true, errors: [] });
|
|
17
|
+
expect(execSync).toHaveBeenCalledWith('npx biome check src/Button.tsx', {
|
|
18
|
+
cwd: '/project',
|
|
19
|
+
stdio: 'pipe',
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns errors when biome check fails', () => {
|
|
25
|
+
const error = {
|
|
26
|
+
stdout: 'src/Button.tsx:5:1 lint/error: some lint issue\n',
|
|
27
|
+
stderr: '',
|
|
28
|
+
};
|
|
29
|
+
vi.mocked(execSync).mockImplementation(() => {
|
|
30
|
+
throw error;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const result = runLint(['src/Button.tsx'], '/project');
|
|
34
|
+
|
|
35
|
+
expect(result).toEqual({
|
|
36
|
+
passed: false,
|
|
37
|
+
errors: ['src/Button.tsx:5:1 lint/error: some lint issue'],
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('passes multiple file paths to biome', () => {
|
|
42
|
+
vi.mocked(execSync).mockReturnValue('');
|
|
43
|
+
|
|
44
|
+
runLint(['src/Button.tsx', 'src/Button.test.tsx'], '/project');
|
|
45
|
+
|
|
46
|
+
expect(execSync).toHaveBeenCalledWith('npx biome check src/Button.tsx src/Button.test.tsx', {
|
|
47
|
+
cwd: '/project',
|
|
48
|
+
stdio: 'pipe',
|
|
49
|
+
encoding: 'utf-8',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('runLintFix', () => {
|
|
55
|
+
it('returns passed when biome check --write succeeds', () => {
|
|
56
|
+
vi.mocked(execSync).mockReturnValue('');
|
|
57
|
+
|
|
58
|
+
const result = runLintFix(['src/Button.tsx'], '/project');
|
|
59
|
+
|
|
60
|
+
expect(result).toEqual({ passed: true, errors: [] });
|
|
61
|
+
expect(execSync).toHaveBeenCalledWith('npx biome check --write src/Button.tsx', {
|
|
62
|
+
cwd: '/project',
|
|
63
|
+
stdio: 'pipe',
|
|
64
|
+
encoding: 'utf-8',
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('returns errors when auto-fix still has remaining issues', () => {
|
|
69
|
+
const error = {
|
|
70
|
+
stdout: 'src/Button.tsx:3:1 lint/error: unfixable issue\n',
|
|
71
|
+
stderr: '',
|
|
72
|
+
};
|
|
73
|
+
vi.mocked(execSync).mockImplementation(() => {
|
|
74
|
+
throw error;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const result = runLintFix(['src/Button.tsx'], '/project');
|
|
78
|
+
|
|
79
|
+
expect(result).toEqual({
|
|
80
|
+
passed: false,
|
|
81
|
+
errors: ['src/Button.tsx:3:1 lint/error: unfixable issue'],
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('handles error object without stdout or stderr properties', () => {
|
|
86
|
+
vi.mocked(execSync).mockImplementation(() => {
|
|
87
|
+
throw new Error('command failed');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const result = runLintFix(['src/Button.tsx'], '/project');
|
|
91
|
+
|
|
92
|
+
expect(result).toEqual({
|
|
93
|
+
passed: false,
|
|
94
|
+
errors: [],
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('runLint error without stdout/stderr', () => {
|
|
100
|
+
it('handles error object without stdout or stderr properties', () => {
|
|
101
|
+
vi.mocked(execSync).mockImplementation(() => {
|
|
102
|
+
throw new Error('command failed');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const result = runLint(['src/Button.tsx'], '/project');
|
|
106
|
+
|
|
107
|
+
expect(result).toEqual({
|
|
108
|
+
passed: false,
|
|
109
|
+
errors: [],
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export type LintResult = {
|
|
4
|
+
passed: boolean;
|
|
5
|
+
errors: string[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function runLint(filePaths: string[], targetDir: string): LintResult {
|
|
9
|
+
try {
|
|
10
|
+
execSync(`npx biome check ${filePaths.join(' ')}`, {
|
|
11
|
+
cwd: targetDir,
|
|
12
|
+
stdio: 'pipe',
|
|
13
|
+
encoding: 'utf-8',
|
|
14
|
+
});
|
|
15
|
+
return { passed: true, errors: [] };
|
|
16
|
+
} catch (err: unknown) {
|
|
17
|
+
let stdout = '';
|
|
18
|
+
let stderr = '';
|
|
19
|
+
if (typeof err === 'object' && err !== null) {
|
|
20
|
+
if ('stdout' in err && typeof err.stdout === 'string') stdout = err.stdout;
|
|
21
|
+
if ('stderr' in err && typeof err.stderr === 'string') stderr = err.stderr;
|
|
22
|
+
}
|
|
23
|
+
const combined = stdout + '\n' + stderr;
|
|
24
|
+
|
|
25
|
+
const errors = combined.split('\n').filter((line) => line.trim().length > 0);
|
|
26
|
+
|
|
27
|
+
return { passed: false, errors };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function runLintFix(filePaths: string[], targetDir: string): LintResult {
|
|
32
|
+
try {
|
|
33
|
+
execSync(`npx biome check --write ${filePaths.join(' ')}`, {
|
|
34
|
+
cwd: targetDir,
|
|
35
|
+
stdio: 'pipe',
|
|
36
|
+
encoding: 'utf-8',
|
|
37
|
+
});
|
|
38
|
+
return { passed: true, errors: [] };
|
|
39
|
+
} catch (err: unknown) {
|
|
40
|
+
let stdout = '';
|
|
41
|
+
let stderr = '';
|
|
42
|
+
if (typeof err === 'object' && err !== null) {
|
|
43
|
+
if ('stdout' in err && typeof err.stdout === 'string') stdout = err.stdout;
|
|
44
|
+
if ('stderr' in err && typeof err.stderr === 'string') stderr = err.stderr;
|
|
45
|
+
}
|
|
46
|
+
const combined = stdout + '\n' + stderr;
|
|
47
|
+
|
|
48
|
+
const errors = combined.split('\n').filter((line) => line.trim().length > 0);
|
|
49
|
+
|
|
50
|
+
return { passed: false, errors };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('node:child_process', () => ({
|
|
4
|
+
execSync: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import { runStorybookTest } from './storybook-runner';
|
|
9
|
+
|
|
10
|
+
describe('runStorybookTest', () => {
|
|
11
|
+
it('returns passed when storybook test succeeds', () => {
|
|
12
|
+
vi.mocked(execSync).mockReturnValue('');
|
|
13
|
+
|
|
14
|
+
const result = runStorybookTest('src/Button.stories.tsx', '/project');
|
|
15
|
+
|
|
16
|
+
expect(result).toEqual({ passed: true, errors: [] });
|
|
17
|
+
expect(execSync).toHaveBeenCalledWith('npx storybook test --stories src/Button.stories.tsx', {
|
|
18
|
+
cwd: '/project',
|
|
19
|
+
stdio: 'pipe',
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns errors when storybook test fails', () => {
|
|
25
|
+
const error = {
|
|
26
|
+
stdout: 'FAIL src/Button.stories.tsx\nStory "Default" failed\n',
|
|
27
|
+
stderr: '',
|
|
28
|
+
};
|
|
29
|
+
vi.mocked(execSync).mockImplementation(() => {
|
|
30
|
+
throw error;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const result = runStorybookTest('src/Button.stories.tsx', '/project');
|
|
34
|
+
|
|
35
|
+
expect(result).toEqual({
|
|
36
|
+
passed: false,
|
|
37
|
+
errors: ['FAIL src/Button.stories.tsx', 'Story "Default" failed'],
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('handles error object without stdout or stderr properties', () => {
|
|
42
|
+
vi.mocked(execSync).mockImplementation(() => {
|
|
43
|
+
throw new Error('command failed');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const result = runStorybookTest('src/Button.stories.tsx', '/project');
|
|
47
|
+
|
|
48
|
+
expect(result).toEqual({
|
|
49
|
+
passed: false,
|
|
50
|
+
errors: [],
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export type StorybookTestResult = {
|
|
4
|
+
passed: boolean;
|
|
5
|
+
errors: string[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function runStorybookTest(storyPath: string, targetDir: string): StorybookTestResult {
|
|
9
|
+
try {
|
|
10
|
+
execSync(`npx storybook test --stories ${storyPath}`, {
|
|
11
|
+
cwd: targetDir,
|
|
12
|
+
stdio: 'pipe',
|
|
13
|
+
encoding: 'utf-8',
|
|
14
|
+
});
|
|
15
|
+
return { passed: true, errors: [] };
|
|
16
|
+
} catch (err: unknown) {
|
|
17
|
+
let stdout = '';
|
|
18
|
+
let stderr = '';
|
|
19
|
+
if (typeof err === 'object' && err !== null) {
|
|
20
|
+
if ('stdout' in err && typeof err.stdout === 'string') stdout = err.stdout;
|
|
21
|
+
if ('stderr' in err && typeof err.stderr === 'string') stderr = err.stderr;
|
|
22
|
+
}
|
|
23
|
+
const combined = stdout + '\n' + stderr;
|
|
24
|
+
|
|
25
|
+
const errors = combined.split('\n').filter((line) => line.trim().length > 0);
|
|
26
|
+
|
|
27
|
+
return { passed: false, errors };
|
|
28
|
+
}
|
|
29
|
+
}
|