@fragments-sdk/cli 0.2.2

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 (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,658 @@
1
+ /**
2
+ * Storybook to Segments Converter
3
+ *
4
+ * Transforms parsed Storybook data into Segment definitions.
5
+ */
6
+
7
+ import type { ParsedStoryFile, ParsedArgType, ConversionResult } from "./types.js";
8
+ import { storyNameToTitle, extractCategory } from "./parser.js";
9
+
10
+ /**
11
+ * Sanitize a component name to be a valid JavaScript identifier.
12
+ * "No recent searches" -> "NoRecentSearches"
13
+ * "My-Component" -> "MyComponent"
14
+ */
15
+ function sanitizeComponentName(name: string): string {
16
+ // Convert to PascalCase: remove spaces/hyphens, capitalize each word
17
+ return name
18
+ .split(/[\s-_]+/)
19
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
20
+ .join("")
21
+ .replace(/[^a-zA-Z0-9]/g, ""); // Remove any remaining invalid chars
22
+ }
23
+
24
+ /**
25
+ * Convert a parsed story file into a segment definition.
26
+ */
27
+ export function convertToSegment(parsed: ParsedStoryFile): ConversionResult {
28
+ const warnings: string[] = [...parsed.warnings];
29
+ const todos: string[] = [];
30
+
31
+ // Extract category and name from title
32
+ const category = extractCategory(parsed.meta.title);
33
+ const componentName = sanitizeComponentName(parsed.meta.componentName);
34
+
35
+ // Check if we have a valid component import path
36
+ // If not, the story likely defines the component locally or uses a pattern we can't parse
37
+ if (!parsed.meta.componentImport) {
38
+ warnings.push(`No importable component found - story may define component locally`);
39
+
40
+ // Determine output file path for the error result
41
+ const outputFile = parsed.filePath
42
+ .replace(/\.stories\.(tsx?|jsx?|mdx)$/, ".segment.tsx");
43
+
44
+ return {
45
+ sourceFile: parsed.filePath,
46
+ outputFile,
47
+ code: "",
48
+ componentName,
49
+ category,
50
+ variantCount: 0,
51
+ propCount: 0,
52
+ confidence: 0,
53
+ todos: [],
54
+ warnings,
55
+ success: false,
56
+ };
57
+ }
58
+
59
+ // Convert argTypes to props
60
+ const props = convertArgTypesToProps(parsed.argTypes);
61
+
62
+ // Convert stories to variants
63
+ const variants = convertStoriesToVariants(parsed, componentName);
64
+
65
+ // Track what's missing (needs human input)
66
+ todos.push("Add usage.when - scenarios where this component is appropriate");
67
+ todos.push("Add usage.whenNot - scenarios where alternatives should be used");
68
+ todos.push("Add usage.guidelines - best practices");
69
+ todos.push("Add relations - related components");
70
+
71
+ if (Object.keys(props).length > 0) {
72
+ const propsWithoutConstraints = Object.entries(props)
73
+ .filter(([, p]) => !p.constraints?.length)
74
+ .map(([name]) => name);
75
+
76
+ if (propsWithoutConstraints.length > 0) {
77
+ todos.push(`Add constraints for props: ${propsWithoutConstraints.slice(0, 3).join(", ")}${propsWithoutConstraints.length > 3 ? "..." : ""}`);
78
+ }
79
+ }
80
+
81
+ // Extract skipped variants info
82
+ const skippedVariants = variants
83
+ .filter(v => v.needsManualReview && v.skipReason)
84
+ .map(v => ({ name: v.name, reason: v.skipReason! }));
85
+
86
+ // Generate the segment code with _generated metadata
87
+ const code = generateSegmentCode({
88
+ componentName,
89
+ componentImport: parsed.meta.componentImport,
90
+ description: parsed.meta.description,
91
+ category,
92
+ tags: parsed.meta.tags,
93
+ props,
94
+ variants,
95
+ todos,
96
+ generated: {
97
+ source: "storybook",
98
+ sourceFile: parsed.filePath,
99
+ confidence: parsed.confidence,
100
+ timestamp: new Date().toISOString(),
101
+ skippedVariants: skippedVariants.length > 0 ? skippedVariants : undefined,
102
+ },
103
+ });
104
+
105
+ // Determine output file path
106
+ const outputFile = parsed.filePath
107
+ .replace(/\.stories\.(tsx?|jsx?|mdx)$/, ".segment.tsx");
108
+
109
+ return {
110
+ sourceFile: parsed.filePath,
111
+ outputFile,
112
+ code,
113
+ componentName,
114
+ category,
115
+ variantCount: variants.length,
116
+ propCount: Object.keys(props).length,
117
+ confidence: parsed.confidence,
118
+ todos,
119
+ warnings,
120
+ success: true,
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Convert Storybook argTypes to Segments props.
126
+ */
127
+ function convertArgTypesToProps(
128
+ argTypes: Record<string, ParsedArgType>
129
+ ): Record<string, PropDef> {
130
+ const props: Record<string, PropDef> = {};
131
+
132
+ for (const [name, argType] of Object.entries(argTypes)) {
133
+ props[name] = convertArgType(name, argType);
134
+ }
135
+
136
+ return props;
137
+ }
138
+
139
+ interface PropDef {
140
+ type: string;
141
+ description?: string;
142
+ default?: unknown;
143
+ required?: boolean;
144
+ values?: string[];
145
+ constraints?: string[];
146
+ }
147
+
148
+ /**
149
+ * Convert a single argType to a prop definition.
150
+ */
151
+ function convertArgType(name: string, argType: ParsedArgType): PropDef {
152
+ const prop: PropDef = {
153
+ type: inferPropType(argType),
154
+ };
155
+
156
+ if (argType.description) {
157
+ prop.description = argType.description;
158
+ }
159
+
160
+ if (argType.defaultValue !== undefined) {
161
+ prop.default = argType.defaultValue;
162
+ }
163
+
164
+ if (argType.required) {
165
+ prop.required = true;
166
+ }
167
+
168
+ // Add enum values if present
169
+ if (argType.options && argType.options.length > 0) {
170
+ prop.type = "enum";
171
+ prop.values = argType.options;
172
+ }
173
+
174
+ return prop;
175
+ }
176
+
177
+ /**
178
+ * Infer prop type from Storybook control type.
179
+ */
180
+ function inferPropType(argType: ParsedArgType): string {
181
+ // If explicit type is provided
182
+ if (argType.type) {
183
+ const typeMap: Record<string, string> = {
184
+ string: "string",
185
+ number: "number",
186
+ boolean: "boolean",
187
+ object: "object",
188
+ array: "array",
189
+ function: "function",
190
+ };
191
+ return typeMap[argType.type.toLowerCase()] ?? "custom";
192
+ }
193
+
194
+ // Infer from control type
195
+ if (argType.control) {
196
+ const controlMap: Record<string, string> = {
197
+ text: "string",
198
+ number: "number",
199
+ boolean: "boolean",
200
+ select: "enum",
201
+ radio: "enum",
202
+ "inline-radio": "enum",
203
+ check: "boolean",
204
+ "inline-check": "boolean",
205
+ range: "number",
206
+ object: "object",
207
+ array: "array",
208
+ date: "string",
209
+ color: "string",
210
+ };
211
+ return controlMap[argType.control] ?? "custom";
212
+ }
213
+
214
+ // If has options, it's an enum
215
+ if (argType.options && argType.options.length > 0) {
216
+ return "enum";
217
+ }
218
+
219
+ return "custom";
220
+ }
221
+
222
+ interface VariantDef {
223
+ name: string;
224
+ description: string;
225
+ renderCode: string;
226
+ needsManualReview: boolean;
227
+ skipReason?: string;
228
+ }
229
+
230
+ /**
231
+ * Convert stories to variant definitions.
232
+ *
233
+ * Stories with custom render functions or unrenderable args are marked
234
+ * as needing manual review since we can't reliably extract complex JSX.
235
+ * For these, use the viewer's native story support instead of migration.
236
+ */
237
+ function convertStoriesToVariants(
238
+ parsed: ParsedStoryFile,
239
+ componentName: string
240
+ ): VariantDef[] {
241
+ return parsed.stories.map((story) => {
242
+ // Check if story has custom render or args we can't statically render
243
+ const hasCustomRender = story.hasCustomRender === true;
244
+ const unrenderableReason = getUnrenderableReason(story.args);
245
+ const needsManualReview = hasCustomRender || unrenderableReason !== null;
246
+
247
+ // Determine skip reason
248
+ let skipReason: string | undefined;
249
+ if (hasCustomRender) {
250
+ skipReason = "uses custom render function";
251
+ } else if (unrenderableReason) {
252
+ skipReason = unrenderableReason;
253
+ }
254
+
255
+ // Generate render code - will include comments for unrenderable values
256
+ const renderCode = hasCustomRender
257
+ ? `<${componentName} />` // Placeholder for custom render
258
+ : generateRenderCode(componentName, story.args);
259
+
260
+ const description = story.description ??
261
+ `${storyNameToTitle(story.name)} variant`;
262
+
263
+ return {
264
+ name: storyNameToTitle(story.name),
265
+ description,
266
+ renderCode,
267
+ needsManualReview,
268
+ skipReason,
269
+ };
270
+ });
271
+ }
272
+
273
+ /**
274
+ * Check if args contain values that can't be statically rendered.
275
+ * Returns the reason string if unrenderable, null if renderable.
276
+ */
277
+ function getUnrenderableReason(args: Record<string, unknown>, path: string = ""): string | null {
278
+ for (const [key, value] of Object.entries(args)) {
279
+ const currentPath = path ? `${path}.${key}` : key;
280
+ if (typeof value === "string") {
281
+ // JSX, expressions, and variable references can't be rendered statically
282
+ if (value === "__JSX__") {
283
+ return `JSX element in prop "${currentPath}"`;
284
+ }
285
+ if (value === "__EXPR__") {
286
+ return `expression in prop "${currentPath}"`;
287
+ }
288
+ if (value.startsWith("__REF__")) {
289
+ const ref = value.slice(7);
290
+ return `variable reference "${ref}" in prop "${currentPath}"`;
291
+ }
292
+ if (value === "__SPREAD__" || key === "__SPREAD__") {
293
+ return `spread syntax in args`;
294
+ }
295
+ }
296
+ if (typeof value === "object" && value !== null) {
297
+ const nestedReason = getUnrenderableReason(value as Record<string, unknown>, currentPath);
298
+ if (nestedReason) {
299
+ return nestedReason;
300
+ }
301
+ }
302
+ }
303
+ return null;
304
+ }
305
+
306
+ // Backward compatibility wrapper
307
+ function hasUnrenderableArgs(args: Record<string, unknown>): boolean {
308
+ return getUnrenderableReason(args) !== null;
309
+ }
310
+
311
+ /**
312
+ * Generate render code from component name and args.
313
+ */
314
+ function generateRenderCode(
315
+ componentName: string,
316
+ args: Record<string, unknown>
317
+ ): string {
318
+ const entries = Object.entries(args);
319
+
320
+ if (entries.length === 0) {
321
+ return `<${componentName} />`;
322
+ }
323
+
324
+ // Check for children
325
+ const children = args.children;
326
+ const otherArgs = Object.entries(args).filter(([k]) => k !== "children");
327
+
328
+ // Build props string
329
+ const propsString = otherArgs
330
+ .map(([key, value]) => formatPropValue(key, value))
331
+ .filter(Boolean)
332
+ .join(" ");
333
+
334
+ if (children !== undefined) {
335
+ const childrenStr = formatChildrenValue(children);
336
+ return propsString
337
+ ? `<${componentName} ${propsString}>${childrenStr}</${componentName}>`
338
+ : `<${componentName}>${childrenStr}</${componentName}>`;
339
+ }
340
+
341
+ return propsString
342
+ ? `<${componentName} ${propsString} />`
343
+ : `<${componentName} />`;
344
+ }
345
+
346
+ /**
347
+ * Format children value for JSX.
348
+ */
349
+ function formatChildrenValue(value: unknown): string {
350
+ if (typeof value === "string") {
351
+ // Handle special markers
352
+ if (value === "__JSX__") return "{null /* JSX children */}";
353
+ if (value === "__EXPR__") return "{null /* expression */}";
354
+ if (value.startsWith("__REF__")) return `{${value.slice(7)}}`;
355
+ return value;
356
+ }
357
+ return String(value);
358
+ }
359
+
360
+ /**
361
+ * Format a prop value for JSX.
362
+ */
363
+ function formatPropValue(key: string, value: unknown): string {
364
+ if (value === undefined || value === null) {
365
+ return "";
366
+ }
367
+
368
+ if (typeof value === "string") {
369
+ // Handle special markers from parser
370
+ if (value === "__JSX__") {
371
+ // Use undefined as placeholder - JSX comments can't be prop values
372
+ return `${key}={undefined /* JSX */}`;
373
+ }
374
+ if (value === "__EXPR__") {
375
+ return `${key}={undefined /* expression */}`;
376
+ }
377
+ if (value.startsWith("__REF__")) {
378
+ // Variable reference - output as expression
379
+ return `${key}={${value.slice(7)}}`;
380
+ }
381
+ // For strings with special characters (quotes, backslashes, newlines),
382
+ // use JSX expression syntax {""} instead of attribute syntax ""
383
+ // This ensures proper JavaScript string escaping
384
+ if (value.includes('"') || value.includes("\\") || value.includes("\n")) {
385
+ return `${key}={"${escapeString(value)}"}`;
386
+ }
387
+ return `${key}="${value}"`;
388
+ }
389
+
390
+ if (typeof value === "boolean") {
391
+ return value ? key : `${key}={false}`;
392
+ }
393
+
394
+ if (typeof value === "number") {
395
+ return `${key}={${value}}`;
396
+ }
397
+
398
+ if (Array.isArray(value)) {
399
+ // Format array value
400
+ const formatted = formatArrayValue(value);
401
+ return `${key}={${formatted}}`;
402
+ }
403
+
404
+ if (typeof value === "object" && value !== null) {
405
+ // Format nested object
406
+ const formatted = formatObjectValue(value as Record<string, unknown>);
407
+ return `${key}={${formatted}}`;
408
+ }
409
+
410
+ // For other complex values, use JSON
411
+ return `${key}={${JSON.stringify(value)}}`;
412
+ }
413
+
414
+ /**
415
+ * Format a nested object value for JSX.
416
+ */
417
+ function formatObjectValue(obj: Record<string, unknown>): string {
418
+ const entries = Object.entries(obj);
419
+ if (entries.length === 0) return "{}";
420
+
421
+ const props = entries
422
+ .map(([k, v]) => {
423
+ const formatted = formatValueForObject(v);
424
+ return `${k}: ${formatted}`;
425
+ })
426
+ .join(", ");
427
+
428
+ return `{ ${props} }`;
429
+ }
430
+
431
+ /**
432
+ * Format an array value for JSX.
433
+ */
434
+ function formatArrayValue(arr: unknown[]): string {
435
+ if (arr.length === 0) return "[]";
436
+
437
+ const items = arr.map((item) => formatValueForObject(item));
438
+ return `[${items.join(", ")}]`;
439
+ }
440
+
441
+ /**
442
+ * Format a value for use inside an object/array literal.
443
+ */
444
+ function formatValueForObject(value: unknown): string {
445
+ if (value === undefined) return "undefined";
446
+ if (value === null) return "null";
447
+
448
+ if (typeof value === "string") {
449
+ // Handle special markers - use valid JS values with comments
450
+ if (value === "__JSX__") return "undefined /* JSX */";
451
+ if (value === "__EXPR__") return "undefined /* expression */";
452
+ if (value.startsWith("__REF__")) return value.slice(7);
453
+ return `"${escapeString(value)}"`;
454
+ }
455
+
456
+ if (typeof value === "boolean" || typeof value === "number") {
457
+ return String(value);
458
+ }
459
+
460
+ if (Array.isArray(value)) {
461
+ return formatArrayValue(value);
462
+ }
463
+
464
+ if (typeof value === "object") {
465
+ return formatObjectValue(value as Record<string, unknown>);
466
+ }
467
+
468
+ return JSON.stringify(value);
469
+ }
470
+
471
+ interface SkippedVariant {
472
+ name: string;
473
+ reason: string;
474
+ }
475
+
476
+ interface GeneratedMetadata {
477
+ source: "storybook" | "manual" | "ai";
478
+ sourceFile: string;
479
+ confidence: number;
480
+ timestamp: string;
481
+ skippedVariants?: SkippedVariant[];
482
+ }
483
+
484
+ interface GenerateOptions {
485
+ componentName: string;
486
+ componentImport: string;
487
+ description?: string;
488
+ category: string;
489
+ tags?: string[];
490
+ props: Record<string, PropDef>;
491
+ variants: VariantDef[];
492
+ todos: string[];
493
+ generated?: GeneratedMetadata;
494
+ }
495
+
496
+ /**
497
+ * Generate the full segment file code.
498
+ *
499
+ * Note: We use a placeholder component instead of importing the real component.
500
+ * This allows the segment build to work without needing all component dependencies.
501
+ * The viewer will load the actual component at runtime.
502
+ */
503
+ function generateSegmentCode(options: GenerateOptions): string {
504
+ const {
505
+ componentName,
506
+ componentImport,
507
+ description,
508
+ category,
509
+ tags,
510
+ props,
511
+ variants,
512
+ todos,
513
+ generated,
514
+ } = options;
515
+
516
+ // Format props
517
+ const propsCode = formatPropsCode(props);
518
+
519
+ // Format variants - stories with custom renders get TODO comments
520
+ const variantsCode = formatVariantsCode(componentName, variants);
521
+
522
+ // Format tags
523
+ const tagsCode = tags && tags.length > 0
524
+ ? `tags: [${tags.map((t) => `"${t}"`).join(", ")}],`
525
+ : "";
526
+
527
+ // Format TODOs as comments
528
+ const todosComments = todos.length > 0
529
+ ? todos.map((t) => ` // TODO: ${t}`).join("\n") + "\n"
530
+ : "";
531
+
532
+ // Format _generated metadata
533
+ let generatedCode = "";
534
+ if (generated) {
535
+ const skippedCode = generated.skippedVariants && generated.skippedVariants.length > 0
536
+ ? `
537
+ skippedVariants: [
538
+ ${generated.skippedVariants.map(sv => ` { name: "${escapeString(sv.name)}", reason: "${escapeString(sv.reason)}" },`).join("\n")}
539
+ ],`
540
+ : "";
541
+
542
+ generatedCode = `
543
+ _generated: {
544
+ source: "${generated.source}",
545
+ sourceFile: "${escapeString(generated.sourceFile)}",
546
+ confidence: ${generated.confidence.toFixed(2)},
547
+ timestamp: "${generated.timestamp}",${skippedCode}
548
+ },
549
+ `;
550
+ }
551
+
552
+ // Import the actual component - this makes the segment immediately usable
553
+ return `import { defineSegment } from "@fragments/core";
554
+ import { ${componentName} } from "${componentImport}";
555
+
556
+ export default defineSegment({
557
+ component: ${componentName},
558
+
559
+ meta: {
560
+ name: "${componentName}",
561
+ description: "${escapeString(description ?? `${componentName} component`)}",
562
+ category: "${category}",
563
+ ${tagsCode}
564
+ // status: undefined, // TODO: Set to stable/beta/deprecated/experimental
565
+ },
566
+
567
+ usage: {
568
+ // TODO: Add specific use cases - when should developers use this component?
569
+ when: [
570
+ ${todosComments} ],
571
+ // TODO: Add anti-patterns - when should developers NOT use this component?
572
+ whenNot: [],
573
+ },
574
+
575
+ ${propsCode}
576
+
577
+ relations: [
578
+ // TODO: Add related components
579
+ ],
580
+
581
+ ${variantsCode}
582
+ ${generatedCode}});
583
+ `;
584
+ }
585
+
586
+ /**
587
+ * Format props object for code generation.
588
+ */
589
+ function formatPropsCode(props: Record<string, PropDef>): string {
590
+ if (Object.keys(props).length === 0) {
591
+ return " props: {},";
592
+ }
593
+
594
+ const entries = Object.entries(props).map(([name, prop]) => {
595
+ const lines: string[] = [];
596
+ lines.push(` ${name}: {`);
597
+ lines.push(` type: "${prop.type}",`);
598
+
599
+ if (prop.values && prop.values.length > 0) {
600
+ lines.push(` values: [${prop.values.map((v) => `"${v}"`).join(", ")}],`);
601
+ }
602
+
603
+ if (prop.default !== undefined) {
604
+ const defaultVal = typeof prop.default === "string"
605
+ ? `"${prop.default}"`
606
+ : String(prop.default);
607
+ lines.push(` default: ${defaultVal},`);
608
+ }
609
+
610
+ if (prop.required) {
611
+ lines.push(` required: true,`);
612
+ }
613
+
614
+ if (prop.description) {
615
+ lines.push(` description: "${escapeString(prop.description)}",`);
616
+ }
617
+
618
+ lines.push(` },`);
619
+ return lines.join("\n");
620
+ });
621
+
622
+ return ` props: {\n${entries.join("\n")}\n },`;
623
+ }
624
+
625
+ /**
626
+ * Format variants array for code generation.
627
+ *
628
+ * Variants needing manual review get a placeholder render that won't crash.
629
+ * This makes the segment file a starting point that humans can enhance.
630
+ */
631
+ function formatVariantsCode(componentName: string, variants: VariantDef[]): string {
632
+ // Filter out variants that can't be rendered - they would cause runtime errors
633
+ const renderableVariants = variants.filter((v) => !v.needsManualReview);
634
+
635
+ if (renderableVariants.length === 0) {
636
+ return " variants: [],";
637
+ }
638
+
639
+ const entries = renderableVariants.map((variant) => {
640
+ return ` {
641
+ name: "${variant.name}",
642
+ description: "${escapeString(variant.description)}",
643
+ render: () => ${variant.renderCode},
644
+ },`;
645
+ });
646
+
647
+ return ` variants: [\n${entries.join("\n")}\n ],`;
648
+ }
649
+
650
+ /**
651
+ * Escape special characters in strings for code generation.
652
+ */
653
+ function escapeString(str: string): string {
654
+ return str
655
+ .replace(/\\/g, "\\\\")
656
+ .replace(/"/g, '\\"')
657
+ .replace(/\n/g, "\\n");
658
+ }