@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.
Files changed (233) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +6 -6
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +92 -0
  5. package/dist/src/commands/implement-component.d.ts +19 -0
  6. package/dist/src/commands/implement-component.d.ts.map +1 -1
  7. package/dist/src/commands/implement-component.js +109 -30
  8. package/dist/src/commands/implement-component.js.map +1 -1
  9. package/dist/src/commands/implement-component.test.js +259 -69
  10. package/dist/src/commands/implement-component.test.js.map +1 -1
  11. package/dist/src/extract-exports.d.ts +6 -0
  12. package/dist/src/extract-exports.d.ts.map +1 -0
  13. package/dist/src/extract-exports.js +46 -0
  14. package/dist/src/extract-exports.js.map +1 -0
  15. package/dist/src/generate-story-deterministic.d.ts +30 -0
  16. package/dist/src/generate-story-deterministic.d.ts.map +1 -0
  17. package/dist/src/generate-story-deterministic.js +229 -0
  18. package/dist/src/generate-story-deterministic.js.map +1 -0
  19. package/dist/src/index.d.ts +4 -0
  20. package/dist/src/index.d.ts.map +1 -1
  21. package/dist/src/index.js +3 -0
  22. package/dist/src/index.js.map +1 -1
  23. package/dist/src/pipeline/run-pipeline.d.ts +69 -0
  24. package/dist/src/pipeline/run-pipeline.d.ts.map +1 -0
  25. package/dist/src/pipeline/run-pipeline.js +78 -0
  26. package/dist/src/pipeline/run-pipeline.js.map +1 -0
  27. package/dist/src/pipeline/run-pipeline.test.d.ts +2 -0
  28. package/dist/src/pipeline/run-pipeline.test.d.ts.map +1 -0
  29. package/dist/src/pipeline/run-pipeline.test.js +247 -0
  30. package/dist/src/pipeline/run-pipeline.test.js.map +1 -0
  31. package/dist/src/pipeline/steps/generate-component.d.ts +4 -0
  32. package/dist/src/pipeline/steps/generate-component.d.ts.map +1 -0
  33. package/dist/src/pipeline/steps/generate-component.js +50 -0
  34. package/dist/src/pipeline/steps/generate-component.js.map +1 -0
  35. package/dist/src/pipeline/steps/generate-component.test.d.ts.map +1 -0
  36. package/dist/src/pipeline/steps/generate-component.test.js +106 -0
  37. package/dist/src/pipeline/steps/generate-component.test.js.map +1 -0
  38. package/dist/src/pipeline/steps/generate-story.d.ts +3 -0
  39. package/dist/src/pipeline/steps/generate-story.d.ts.map +1 -0
  40. package/dist/src/pipeline/steps/generate-story.js +14 -0
  41. package/dist/src/pipeline/steps/generate-story.js.map +1 -0
  42. package/dist/src/pipeline/steps/generate-story.test.d.ts.map +1 -0
  43. package/dist/src/pipeline/steps/generate-story.test.js +41 -0
  44. package/dist/src/pipeline/steps/generate-story.test.js.map +1 -0
  45. package/dist/src/pipeline/steps/generate-test.d.ts +4 -0
  46. package/dist/src/pipeline/steps/generate-test.d.ts.map +1 -0
  47. package/dist/src/pipeline/steps/generate-test.js +19 -0
  48. package/dist/src/pipeline/steps/generate-test.js.map +1 -0
  49. package/dist/src/pipeline/steps/generate-test.test.d.ts.map +1 -0
  50. package/dist/src/pipeline/steps/generate-test.test.js +60 -0
  51. package/dist/src/pipeline/steps/generate-test.test.js.map +1 -0
  52. package/dist/src/pipeline/steps/lint-fix-loop.d.ts +4 -0
  53. package/dist/src/pipeline/steps/lint-fix-loop.d.ts.map +1 -0
  54. package/dist/src/pipeline/steps/lint-fix-loop.js +45 -0
  55. package/dist/src/pipeline/steps/lint-fix-loop.js.map +1 -0
  56. package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts +2 -0
  57. package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts.map +1 -0
  58. package/dist/src/pipeline/steps/lint-fix-loop.test.js +119 -0
  59. package/dist/src/pipeline/steps/lint-fix-loop.test.js.map +1 -0
  60. package/dist/src/pipeline/steps/story-fix-loop.d.ts +4 -0
  61. package/dist/src/pipeline/steps/story-fix-loop.d.ts.map +1 -0
  62. package/dist/src/pipeline/steps/story-fix-loop.js +34 -0
  63. package/dist/src/pipeline/steps/story-fix-loop.js.map +1 -0
  64. package/dist/src/pipeline/steps/story-fix-loop.test.d.ts +2 -0
  65. package/dist/src/pipeline/steps/story-fix-loop.test.d.ts.map +1 -0
  66. package/dist/src/pipeline/steps/story-fix-loop.test.js +94 -0
  67. package/dist/src/pipeline/steps/story-fix-loop.test.js.map +1 -0
  68. package/dist/src/pipeline/steps/storybook-test.d.ts +3 -0
  69. package/dist/src/pipeline/steps/storybook-test.d.ts.map +1 -0
  70. package/dist/src/pipeline/steps/storybook-test.js +22 -0
  71. package/dist/src/pipeline/steps/storybook-test.js.map +1 -0
  72. package/dist/src/pipeline/steps/storybook-test.test.d.ts +2 -0
  73. package/dist/src/pipeline/steps/storybook-test.test.d.ts.map +1 -0
  74. package/dist/src/pipeline/steps/storybook-test.test.js +66 -0
  75. package/dist/src/pipeline/steps/storybook-test.test.js.map +1 -0
  76. package/dist/src/pipeline/steps/test-fix-loop.d.ts +4 -0
  77. package/dist/src/pipeline/steps/test-fix-loop.d.ts.map +1 -0
  78. package/dist/src/pipeline/steps/test-fix-loop.js +44 -0
  79. package/dist/src/pipeline/steps/test-fix-loop.js.map +1 -0
  80. package/dist/src/pipeline/steps/test-fix-loop.test.d.ts +2 -0
  81. package/dist/src/pipeline/steps/test-fix-loop.test.d.ts.map +1 -0
  82. package/dist/src/pipeline/steps/test-fix-loop.test.js +168 -0
  83. package/dist/src/pipeline/steps/test-fix-loop.test.js.map +1 -0
  84. package/dist/src/pipeline/steps/type-fix-loop.d.ts +4 -0
  85. package/dist/src/pipeline/steps/type-fix-loop.d.ts.map +1 -0
  86. package/dist/src/pipeline/steps/type-fix-loop.js +43 -0
  87. package/dist/src/pipeline/steps/type-fix-loop.js.map +1 -0
  88. package/dist/src/pipeline/steps/type-fix-loop.test.d.ts +2 -0
  89. package/dist/src/pipeline/steps/type-fix-loop.test.d.ts.map +1 -0
  90. package/dist/src/pipeline/steps/type-fix-loop.test.js +112 -0
  91. package/dist/src/pipeline/steps/type-fix-loop.test.js.map +1 -0
  92. package/dist/src/pipeline/steps/visual-test.d.ts +3 -0
  93. package/dist/src/pipeline/steps/visual-test.d.ts.map +1 -0
  94. package/dist/src/pipeline/steps/visual-test.js +4 -0
  95. package/dist/src/pipeline/steps/visual-test.js.map +1 -0
  96. package/dist/src/pipeline/steps/visual-test.test.d.ts +2 -0
  97. package/dist/src/pipeline/steps/visual-test.test.d.ts.map +1 -0
  98. package/dist/src/pipeline/steps/visual-test.test.js +9 -0
  99. package/dist/src/pipeline/steps/visual-test.test.js.map +1 -0
  100. package/dist/src/project-context.d.ts +10 -0
  101. package/dist/src/project-context.d.ts.map +1 -0
  102. package/dist/src/project-context.js +178 -0
  103. package/dist/src/project-context.js.map +1 -0
  104. package/dist/src/prompt.d.ts +39 -7
  105. package/dist/src/prompt.d.ts.map +1 -1
  106. package/dist/src/prompt.js +233 -23
  107. package/dist/src/prompt.js.map +1 -1
  108. package/dist/src/prompt.test.js +154 -9
  109. package/dist/src/prompt.test.js.map +1 -1
  110. package/dist/src/scaffold.d.ts +49 -0
  111. package/dist/src/scaffold.d.ts.map +1 -0
  112. package/dist/src/scaffold.js +208 -0
  113. package/dist/src/scaffold.js.map +1 -0
  114. package/dist/src/tools/lint-runner.d.ts +7 -0
  115. package/dist/src/tools/lint-runner.d.ts.map +1 -0
  116. package/dist/src/tools/lint-runner.js +48 -0
  117. package/dist/src/tools/lint-runner.js.map +1 -0
  118. package/dist/src/tools/lint-runner.test.d.ts +2 -0
  119. package/dist/src/tools/lint-runner.test.d.ts.map +1 -0
  120. package/dist/src/tools/lint-runner.test.js +90 -0
  121. package/dist/src/tools/lint-runner.test.js.map +1 -0
  122. package/dist/src/tools/storybook-runner.d.ts +6 -0
  123. package/dist/src/tools/storybook-runner.d.ts.map +1 -0
  124. package/dist/src/tools/storybook-runner.js +25 -0
  125. package/dist/src/tools/storybook-runner.js.map +1 -0
  126. package/dist/src/tools/storybook-runner.test.d.ts +2 -0
  127. package/dist/src/tools/storybook-runner.test.d.ts.map +1 -0
  128. package/dist/src/tools/storybook-runner.test.js +43 -0
  129. package/dist/src/tools/storybook-runner.test.js.map +1 -0
  130. package/dist/src/tools/test-runner.d.ts +9 -0
  131. package/dist/src/tools/test-runner.d.ts.map +1 -0
  132. package/dist/src/tools/test-runner.js +74 -0
  133. package/dist/src/tools/test-runner.js.map +1 -0
  134. package/dist/src/tools/test-runner.test.d.ts +2 -0
  135. package/dist/src/tools/test-runner.test.d.ts.map +1 -0
  136. package/dist/src/tools/test-runner.test.js +177 -0
  137. package/dist/src/tools/test-runner.test.js.map +1 -0
  138. package/dist/src/tools/type-checker.d.ts +6 -0
  139. package/dist/src/tools/type-checker.d.ts.map +1 -0
  140. package/dist/src/tools/type-checker.js +36 -0
  141. package/dist/src/tools/type-checker.js.map +1 -0
  142. package/dist/src/tools/type-checker.test.d.ts +2 -0
  143. package/dist/src/tools/type-checker.test.d.ts.map +1 -0
  144. package/dist/src/tools/type-checker.test.js +96 -0
  145. package/dist/src/tools/type-checker.test.js.map +1 -0
  146. package/dist/tsconfig.tsbuildinfo +1 -1
  147. package/inputs/model-a/spec-deltas.json +1460 -0
  148. package/inputs/model-b/spec-deltas.json +1424 -0
  149. package/inputs/model-c/spec-deltas.json +1432 -0
  150. package/inputs/model-d/spec-deltas.json +967 -0
  151. package/inputs/model-e/spec-deltas.json +2292 -0
  152. package/ketchup-plan.md +43 -8
  153. package/package.json +3 -3
  154. package/scoring-heuristic.md +138 -0
  155. package/scripts/improve.ts +23 -18
  156. package/src/commands/implement-component.test.ts +309 -76
  157. package/src/commands/implement-component.ts +155 -31
  158. package/src/extract-exports.ts +53 -0
  159. package/src/generate-story-deterministic.ts +267 -0
  160. package/src/index.ts +12 -0
  161. package/src/pipeline/run-pipeline.test.ts +292 -0
  162. package/src/pipeline/run-pipeline.ts +160 -0
  163. package/src/pipeline/steps/generate-component.test.ts +130 -0
  164. package/src/pipeline/steps/generate-component.ts +60 -0
  165. package/src/pipeline/steps/generate-story.test.ts +54 -0
  166. package/src/pipeline/steps/generate-story.ts +17 -0
  167. package/src/pipeline/steps/generate-test.test.ts +75 -0
  168. package/src/pipeline/steps/generate-test.ts +25 -0
  169. package/src/pipeline/steps/lint-fix-loop.test.ts +155 -0
  170. package/src/pipeline/steps/lint-fix-loop.ts +59 -0
  171. package/src/pipeline/steps/story-fix-loop.test.ts +123 -0
  172. package/src/pipeline/steps/story-fix-loop.ts +47 -0
  173. package/src/pipeline/steps/storybook-test.test.ts +82 -0
  174. package/src/pipeline/steps/storybook-test.ts +27 -0
  175. package/src/pipeline/steps/test-fix-loop.test.ts +201 -0
  176. package/src/pipeline/steps/test-fix-loop.ts +56 -0
  177. package/src/pipeline/steps/type-fix-loop.test.ts +145 -0
  178. package/src/pipeline/steps/type-fix-loop.ts +55 -0
  179. package/src/pipeline/steps/visual-test.test.ts +10 -0
  180. package/src/pipeline/steps/visual-test.ts +5 -0
  181. package/src/project-context.ts +205 -0
  182. package/src/prompt.test.ts +174 -8
  183. package/src/prompt.ts +301 -23
  184. package/src/scaffold.ts +281 -0
  185. package/src/tools/lint-runner.test.ts +112 -0
  186. package/src/tools/lint-runner.ts +52 -0
  187. package/src/tools/storybook-runner.test.ts +53 -0
  188. package/src/tools/storybook-runner.ts +29 -0
  189. package/src/tools/test-runner.test.ts +213 -0
  190. package/src/tools/test-runner.ts +84 -0
  191. package/src/tools/type-checker.test.ts +120 -0
  192. package/src/tools/type-checker.ts +42 -0
  193. package/vitest.config.ts +9 -1
  194. package/dist/src/generate-component.d.ts +0 -4
  195. package/dist/src/generate-component.d.ts.map +0 -1
  196. package/dist/src/generate-component.js +0 -14
  197. package/dist/src/generate-component.js.map +0 -1
  198. package/dist/src/generate-component.test.d.ts.map +0 -1
  199. package/dist/src/generate-component.test.js +0 -73
  200. package/dist/src/generate-component.test.js.map +0 -1
  201. package/dist/src/generate-story.d.ts +0 -4
  202. package/dist/src/generate-story.d.ts.map +0 -1
  203. package/dist/src/generate-story.js +0 -14
  204. package/dist/src/generate-story.js.map +0 -1
  205. package/dist/src/generate-story.test.d.ts.map +0 -1
  206. package/dist/src/generate-story.test.js +0 -58
  207. package/dist/src/generate-story.test.js.map +0 -1
  208. package/dist/src/generate-test.d.ts +0 -4
  209. package/dist/src/generate-test.d.ts.map +0 -1
  210. package/dist/src/generate-test.js +0 -14
  211. package/dist/src/generate-test.js.map +0 -1
  212. package/dist/src/generate-test.test.d.ts.map +0 -1
  213. package/dist/src/generate-test.test.js +0 -77
  214. package/dist/src/generate-test.test.js.map +0 -1
  215. package/dist/src/reconcile.d.ts +0 -8
  216. package/dist/src/reconcile.d.ts.map +0 -1
  217. package/dist/src/reconcile.js +0 -18
  218. package/dist/src/reconcile.js.map +0 -1
  219. package/dist/src/reconcile.test.d.ts +0 -2
  220. package/dist/src/reconcile.test.d.ts.map +0 -1
  221. package/dist/src/reconcile.test.js +0 -108
  222. package/dist/src/reconcile.test.js.map +0 -1
  223. package/src/generate-component.test.ts +0 -89
  224. package/src/generate-component.ts +0 -16
  225. package/src/generate-story.test.ts +0 -71
  226. package/src/generate-story.ts +0 -16
  227. package/src/generate-test.test.ts +0 -93
  228. package/src/generate-test.ts +0 -16
  229. package/src/reconcile.test.ts +0 -127
  230. package/src/reconcile.ts +0 -27
  231. /package/dist/src/{generate-component.test.d.ts → pipeline/steps/generate-component.test.d.ts} +0 -0
  232. /package/dist/src/{generate-story.test.d.ts → pipeline/steps/generate-story.test.d.ts} +0 -0
  233. /package/dist/src/{generate-test.test.d.ts → pipeline/steps/generate-test.test.d.ts} +0 -0
@@ -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
+ }