@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,479 @@
1
+ /**
2
+ * Variant Renderer
3
+ *
4
+ * Uses Playwright to render Storybook stories and capture actual JSX/props
5
+ * from rendered components. This solves the problem where static AST extraction
6
+ * fails on complex components with variable references, expressions, or runtime values.
7
+ *
8
+ * How it works:
9
+ * 1. Navigates to a Storybook story URL in the iframe
10
+ * 2. Waits for the component to render
11
+ * 3. Extracts rendered props and component structure from the DOM
12
+ * 4. Generates JSX code representation of the rendered component
13
+ */
14
+
15
+ import type { Page, BrowserContext } from "playwright";
16
+ import { getSharedPool, shutdownSharedPool } from "../browser-pool.js";
17
+
18
+ /**
19
+ * Rendered variant with extracted props and code
20
+ */
21
+ export interface RenderedVariant {
22
+ /** Story name */
23
+ name: string;
24
+ /** Story display name */
25
+ displayName: string;
26
+ /** Description if available */
27
+ description?: string;
28
+ /** Extracted props from rendered component */
29
+ props: Record<string, unknown>;
30
+ /** Generated JSX code representation */
31
+ code: string;
32
+ /** Whether this is the default story */
33
+ isDefault: boolean;
34
+ /** Storybook story ID */
35
+ storyId: string;
36
+ /** Raw DOM element info for debugging */
37
+ domInfo?: {
38
+ tagName: string;
39
+ attributes: Record<string, string>;
40
+ childCount: number;
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Result of rendering all variants for a component
46
+ */
47
+ export interface VariantRenderResult {
48
+ /** Component name */
49
+ componentName: string;
50
+ /** Successfully rendered variants */
51
+ variants: RenderedVariant[];
52
+ /** Variants that failed to render with error messages */
53
+ failed: Array<{ storyId: string; error: string }>;
54
+ /** Total time taken to render all variants (ms) */
55
+ totalTimeMs: number;
56
+ }
57
+
58
+ /**
59
+ * Options for variant rendering
60
+ */
61
+ export interface VariantRenderOptions {
62
+ /** Storybook base URL (e.g., "http://localhost:6006") */
63
+ storybookUrl: string;
64
+ /** Component name */
65
+ componentName: string;
66
+ /** Story IDs to render */
67
+ storyIds: string[];
68
+ /** Timeout for each story render (ms) */
69
+ timeout?: number;
70
+ /** Wait time after navigation for component to render (ms) */
71
+ renderWait?: number;
72
+ /** Selector for the component root (default: "#storybook-root") */
73
+ rootSelector?: string;
74
+ }
75
+
76
+ const DEFAULT_TIMEOUT = 10000;
77
+ const DEFAULT_RENDER_WAIT = 1000;
78
+ const DEFAULT_ROOT_SELECTOR = "#storybook-root";
79
+
80
+ /**
81
+ * Render Storybook variants and extract props/code
82
+ */
83
+ export async function renderVariants(
84
+ options: VariantRenderOptions
85
+ ): Promise<VariantRenderResult> {
86
+ const {
87
+ storybookUrl,
88
+ componentName,
89
+ storyIds,
90
+ timeout = DEFAULT_TIMEOUT,
91
+ renderWait = DEFAULT_RENDER_WAIT,
92
+ rootSelector = DEFAULT_ROOT_SELECTOR,
93
+ } = options;
94
+
95
+ const startTime = Date.now();
96
+ const result: VariantRenderResult = {
97
+ componentName,
98
+ variants: [],
99
+ failed: [],
100
+ totalTimeMs: 0,
101
+ };
102
+
103
+ if (storyIds.length === 0) {
104
+ result.totalTimeMs = Date.now() - startTime;
105
+ return result;
106
+ }
107
+
108
+ // Acquire browser context from pool
109
+ const pool = getSharedPool();
110
+ let context: BrowserContext | null = null;
111
+ let page: Page | null = null;
112
+
113
+ try {
114
+ context = await pool.acquire();
115
+ page = await context.newPage();
116
+
117
+ // Process each story
118
+ for (const storyId of storyIds) {
119
+ try {
120
+ const variant = await renderSingleVariant(
121
+ page,
122
+ storybookUrl,
123
+ storyId,
124
+ componentName,
125
+ rootSelector,
126
+ timeout,
127
+ renderWait
128
+ );
129
+ result.variants.push(variant);
130
+ } catch (error) {
131
+ result.failed.push({
132
+ storyId,
133
+ error: (error as Error).message,
134
+ });
135
+ }
136
+ }
137
+ } finally {
138
+ // Clean up
139
+ if (page) {
140
+ try {
141
+ await page.close();
142
+ } catch {
143
+ // Ignore cleanup errors
144
+ }
145
+ }
146
+ if (context) {
147
+ await pool.release(context);
148
+ }
149
+ }
150
+
151
+ result.totalTimeMs = Date.now() - startTime;
152
+ return result;
153
+ }
154
+
155
+ /**
156
+ * Render a single story and extract props/code
157
+ */
158
+ async function renderSingleVariant(
159
+ page: Page,
160
+ storybookUrl: string,
161
+ storyId: string,
162
+ componentName: string,
163
+ rootSelector: string,
164
+ timeout: number,
165
+ renderWait: number
166
+ ): Promise<RenderedVariant> {
167
+ // Navigate to the story iframe URL
168
+ const storyUrl = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story`;
169
+ await page.goto(storyUrl, { timeout, waitUntil: "domcontentloaded" });
170
+
171
+ // Wait for the root element to appear
172
+ await page.waitForSelector(rootSelector, { timeout });
173
+
174
+ // Wait for component to render
175
+ await page.waitForTimeout(renderWait);
176
+
177
+ // Extract component information from the DOM
178
+ const extractionResult = await page.evaluate(
179
+ ({ rootSelector, componentName }) => {
180
+ const root = document.querySelector(rootSelector);
181
+ if (!root) {
182
+ throw new Error(`Root element not found: ${rootSelector}`);
183
+ }
184
+
185
+ // Find the first meaningful child element (skip wrapper divs)
186
+ function findComponentElement(el: Element): Element | null {
187
+ // If this element has the component name in its class or data attribute, use it
188
+ const className = el.className || "";
189
+ if (className.toLowerCase().includes(componentName.toLowerCase())) {
190
+ return el;
191
+ }
192
+
193
+ // Check for data attributes that might indicate component
194
+ if (el.getAttribute("data-component") || el.getAttribute("data-testid")) {
195
+ return el;
196
+ }
197
+
198
+ // If it's a simple wrapper with one child, go deeper
199
+ const children = Array.from(el.children);
200
+ if (children.length === 1 && el.tagName === "DIV") {
201
+ return findComponentElement(children[0]) || children[0];
202
+ }
203
+
204
+ // Return the first meaningful element
205
+ return el.firstElementChild;
206
+ }
207
+
208
+ const componentEl = findComponentElement(root) || root.firstElementChild;
209
+ if (!componentEl) {
210
+ throw new Error("No component element found in story");
211
+ }
212
+
213
+ // Extract attributes as props
214
+ const props: Record<string, unknown> = {};
215
+ const attributes: Record<string, string> = {};
216
+
217
+ for (const attr of Array.from(componentEl.attributes)) {
218
+ const name = attr.name;
219
+ let value: unknown = attr.value;
220
+
221
+ // Skip internal React/browser attributes
222
+ if (name.startsWith("data-") && !name.startsWith("data-testid")) {
223
+ continue;
224
+ }
225
+ if (name === "class" || name === "style") {
226
+ attributes[name] = attr.value;
227
+ continue;
228
+ }
229
+
230
+ // Try to parse boolean/number values
231
+ if (value === "true") value = true;
232
+ else if (value === "false") value = false;
233
+ else if (value !== "" && !isNaN(Number(value))) value = Number(value);
234
+
235
+ props[name] = value;
236
+ attributes[name] = attr.value;
237
+ }
238
+
239
+ // Check for special props through data attributes
240
+ const dataVariant = componentEl.getAttribute("data-variant");
241
+ if (dataVariant) props.variant = dataVariant;
242
+
243
+ const dataSize = componentEl.getAttribute("data-size");
244
+ if (dataSize) props.size = dataSize;
245
+
246
+ // Get DOM info for debugging
247
+ const domInfo = {
248
+ tagName: componentEl.tagName.toLowerCase(),
249
+ attributes,
250
+ childCount: componentEl.children.length,
251
+ };
252
+
253
+ // Get text content for simple components
254
+ const textContent = componentEl.textContent?.trim() || "";
255
+ if (textContent && !props.children && componentEl.children.length === 0) {
256
+ props.children = textContent;
257
+ }
258
+
259
+ return { props, domInfo };
260
+ },
261
+ { rootSelector, componentName }
262
+ );
263
+
264
+ // Generate JSX code from extracted props
265
+ const code = generateJSXCode(componentName, extractionResult.props);
266
+
267
+ // Parse story ID to get display name
268
+ const displayName = storyIdToDisplayName(storyId);
269
+ const isDefault =
270
+ storyId.toLowerCase().includes("--default") ||
271
+ storyId.toLowerCase().includes("--primary");
272
+
273
+ return {
274
+ name: displayName.replace(/\s+/g, ""),
275
+ displayName,
276
+ props: extractionResult.props,
277
+ code,
278
+ isDefault,
279
+ storyId,
280
+ domInfo: extractionResult.domInfo,
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Generate JSX code from component name and props
286
+ */
287
+ function generateJSXCode(
288
+ componentName: string,
289
+ props: Record<string, unknown>
290
+ ): string {
291
+ const { children, ...restProps } = props;
292
+
293
+ const propStrings = Object.entries(restProps)
294
+ .filter(([, value]) => value !== undefined && value !== null)
295
+ .map(([key, value]) => {
296
+ if (typeof value === "string") {
297
+ return `${key}="${value}"`;
298
+ }
299
+ if (typeof value === "boolean") {
300
+ return value ? key : null;
301
+ }
302
+ return `${key}={${JSON.stringify(value)}}`;
303
+ })
304
+ .filter(Boolean);
305
+
306
+ const propsString = propStrings.length > 0 ? ` ${propStrings.join(" ")}` : "";
307
+
308
+ if (children) {
309
+ return `<${componentName}${propsString}>${children}</${componentName}>`;
310
+ }
311
+ return `<${componentName}${propsString} />`;
312
+ }
313
+
314
+ /**
315
+ * Convert story ID to display name
316
+ * e.g., "components-button--primary" -> "Primary"
317
+ */
318
+ function storyIdToDisplayName(storyId: string): string {
319
+ // Get the part after the last "--"
320
+ const parts = storyId.split("--");
321
+ const name = parts[parts.length - 1];
322
+
323
+ // Convert kebab-case to Title Case
324
+ return name
325
+ .split("-")
326
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
327
+ .join(" ");
328
+ }
329
+
330
+ /**
331
+ * Check if Storybook is running at the given URL
332
+ */
333
+ export async function checkStorybookRunning(
334
+ storybookUrl: string,
335
+ timeout: number = 5000
336
+ ): Promise<boolean> {
337
+ const pool = getSharedPool();
338
+ let context: BrowserContext | null = null;
339
+ let page: Page | null = null;
340
+
341
+ try {
342
+ context = await pool.acquire();
343
+ page = await context.newPage();
344
+
345
+ await page.goto(storybookUrl, { timeout, waitUntil: "domcontentloaded" });
346
+
347
+ // Check for Storybook-specific elements
348
+ const isStorybook = await page.evaluate(() => {
349
+ // Check for Storybook manager or preview
350
+ return !!(
351
+ document.querySelector("#storybook-root") ||
352
+ document.querySelector("#storybook-preview-wrapper") ||
353
+ document.querySelector("[data-is-storybook]") ||
354
+ // Check for common Storybook title patterns
355
+ document.title.includes("Storybook")
356
+ );
357
+ });
358
+
359
+ return isStorybook;
360
+ } catch {
361
+ return false;
362
+ } finally {
363
+ if (page) {
364
+ try {
365
+ await page.close();
366
+ } catch {
367
+ // Ignore
368
+ }
369
+ }
370
+ if (context) {
371
+ await pool.release(context);
372
+ }
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Get all story IDs for a component from Storybook
378
+ */
379
+ export async function getStorybookStoryIds(
380
+ storybookUrl: string,
381
+ componentName: string,
382
+ timeout: number = 10000
383
+ ): Promise<string[]> {
384
+ const pool = getSharedPool();
385
+ let context: BrowserContext | null = null;
386
+ let page: Page | null = null;
387
+
388
+ try {
389
+ context = await pool.acquire();
390
+ page = await context.newPage();
391
+
392
+ // Navigate to Storybook index
393
+ await page.goto(`${storybookUrl}/index.json`, {
394
+ timeout,
395
+ waitUntil: "networkidle",
396
+ });
397
+
398
+ // Parse the index.json to get stories
399
+ const content = await page.evaluate(() => {
400
+ const pre = document.querySelector("pre");
401
+ return pre?.textContent || document.body.textContent || "";
402
+ });
403
+
404
+ const data = JSON.parse(content);
405
+
406
+ // Find stories matching the component name
407
+ const storyIds: string[] = [];
408
+ const componentLower = componentName.toLowerCase();
409
+
410
+ if (data.entries) {
411
+ // Storybook 7+ format
412
+ for (const [id, entry] of Object.entries(data.entries)) {
413
+ const e = entry as { type?: string; title?: string };
414
+ if (
415
+ e.type === "story" &&
416
+ e.title?.toLowerCase().includes(componentLower)
417
+ ) {
418
+ storyIds.push(id);
419
+ }
420
+ }
421
+ } else if (data.stories) {
422
+ // Legacy format
423
+ for (const [id, story] of Object.entries(data.stories)) {
424
+ const s = story as { kind?: string };
425
+ if (s.kind?.toLowerCase().includes(componentLower)) {
426
+ storyIds.push(id);
427
+ }
428
+ }
429
+ }
430
+
431
+ return storyIds;
432
+ } catch {
433
+ return [];
434
+ } finally {
435
+ if (page) {
436
+ try {
437
+ await page.close();
438
+ } catch {
439
+ // Ignore
440
+ }
441
+ }
442
+ if (context) {
443
+ await pool.release(context);
444
+ }
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Render all variants for a component from a running Storybook
450
+ */
451
+ export async function renderAllComponentVariants(
452
+ storybookUrl: string,
453
+ componentName: string,
454
+ options: Partial<VariantRenderOptions> = {}
455
+ ): Promise<VariantRenderResult> {
456
+ // Get story IDs for this component
457
+ const storyIds = await getStorybookStoryIds(storybookUrl, componentName);
458
+
459
+ if (storyIds.length === 0) {
460
+ return {
461
+ componentName,
462
+ variants: [],
463
+ failed: [],
464
+ totalTimeMs: 0,
465
+ };
466
+ }
467
+
468
+ return renderVariants({
469
+ storybookUrl,
470
+ componentName,
471
+ storyIds,
472
+ ...options,
473
+ });
474
+ }
475
+
476
+ /**
477
+ * Shutdown the browser pool when done
478
+ */
479
+ export { shutdownSharedPool };