@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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- 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 };
|