@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,1136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storybook CSF Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses Storybook Component Story Format (CSF) files and extracts
|
|
5
|
+
* structured data that can be converted to Segments.
|
|
6
|
+
*
|
|
7
|
+
* Supports CSF 2.0 and 3.0 formats.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import type {
|
|
12
|
+
ParsedStoryFile,
|
|
13
|
+
ParsedMeta,
|
|
14
|
+
ParsedArgType,
|
|
15
|
+
ParsedStory,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse a Storybook story file into structured data.
|
|
20
|
+
*/
|
|
21
|
+
export async function parseStoryFile(filePath: string): Promise<ParsedStoryFile> {
|
|
22
|
+
const content = await readFile(filePath, "utf-8");
|
|
23
|
+
return parseStoryContent(content, filePath);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse story content directly (useful for testing).
|
|
28
|
+
*/
|
|
29
|
+
export function parseStoryContent(content: string, filePath: string): ParsedStoryFile {
|
|
30
|
+
const warnings: string[] = [];
|
|
31
|
+
|
|
32
|
+
// Parse meta/default export
|
|
33
|
+
const meta = parseMeta(content, filePath, warnings);
|
|
34
|
+
|
|
35
|
+
// Parse argTypes
|
|
36
|
+
const argTypes = parseArgTypes(content, warnings);
|
|
37
|
+
|
|
38
|
+
// Extract top-level const declarations for spread resolution
|
|
39
|
+
const constDeclarations = extractConstDeclarations(content);
|
|
40
|
+
|
|
41
|
+
// Parse individual stories (pass const declarations for spread resolution)
|
|
42
|
+
const stories = parseStories(content, meta.componentName, warnings, constDeclarations);
|
|
43
|
+
|
|
44
|
+
// Calculate confidence score based on extraction quality
|
|
45
|
+
const confidence = calculateConfidence(meta, argTypes, stories, warnings);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
filePath,
|
|
49
|
+
meta,
|
|
50
|
+
argTypes,
|
|
51
|
+
stories,
|
|
52
|
+
warnings,
|
|
53
|
+
confidence,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Calculate confidence score based on parsing quality.
|
|
59
|
+
* Higher score = more reliable extraction.
|
|
60
|
+
*/
|
|
61
|
+
function calculateConfidence(
|
|
62
|
+
meta: ParsedMeta,
|
|
63
|
+
argTypes: Record<string, ParsedArgType>,
|
|
64
|
+
stories: ParsedStory[],
|
|
65
|
+
warnings: string[]
|
|
66
|
+
): number {
|
|
67
|
+
let score = 1.0;
|
|
68
|
+
|
|
69
|
+
// Penalize for warnings
|
|
70
|
+
score -= warnings.length * 0.1;
|
|
71
|
+
|
|
72
|
+
// Penalize if component name is unknown
|
|
73
|
+
if (meta.componentName === "Unknown") {
|
|
74
|
+
score -= 0.3;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Penalize if no component import found
|
|
78
|
+
if (!meta.componentImport) {
|
|
79
|
+
score -= 0.1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Penalize if no argTypes found (less prop info)
|
|
83
|
+
if (Object.keys(argTypes).length === 0) {
|
|
84
|
+
score -= 0.1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Penalize if no stories found
|
|
88
|
+
if (stories.length === 0) {
|
|
89
|
+
score -= 0.3;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Penalize for stories with custom renders (can't auto-migrate)
|
|
93
|
+
const customRenderCount = stories.filter((s) => s.hasCustomRender).length;
|
|
94
|
+
if (customRenderCount > 0) {
|
|
95
|
+
score -= (customRenderCount / Math.max(stories.length, 1)) * 0.2;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Clamp to [0, 1]
|
|
99
|
+
return Math.max(0, Math.min(1, score));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Extract top-level const declarations that can be used for spread resolution.
|
|
104
|
+
* Looks for patterns like: const defaultArgs = { ... }
|
|
105
|
+
* Returns a map of variable name -> parsed object value.
|
|
106
|
+
*/
|
|
107
|
+
function extractConstDeclarations(content: string): Map<string, Record<string, unknown>> {
|
|
108
|
+
const declarations = new Map<string, Record<string, unknown>>();
|
|
109
|
+
|
|
110
|
+
// Pattern: const variableName = { ... } or const variableName: Type = { ... }
|
|
111
|
+
// We need to find each const declaration and extract its object value
|
|
112
|
+
const constPattern = /const\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*\{/g;
|
|
113
|
+
|
|
114
|
+
let match;
|
|
115
|
+
while ((match = constPattern.exec(content)) !== null) {
|
|
116
|
+
const varName = match[1];
|
|
117
|
+
|
|
118
|
+
// Skip common non-args patterns
|
|
119
|
+
if (varName === "meta" || varName === "default" || varName === "Template") {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Find the opening brace position
|
|
124
|
+
const braceStart = match.index + match[0].length - 1;
|
|
125
|
+
|
|
126
|
+
// Find matching closing brace
|
|
127
|
+
const braceEnd = findMatchingBraceInContent(content, braceStart, "{", "}");
|
|
128
|
+
if (braceEnd === -1) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Extract the object content
|
|
133
|
+
const objectContent = content.slice(braceStart + 1, braceEnd);
|
|
134
|
+
|
|
135
|
+
// Parse it (without spread resolution to avoid infinite loops)
|
|
136
|
+
const parsed = parseArgsSimple(objectContent);
|
|
137
|
+
|
|
138
|
+
// Only store if we got something useful
|
|
139
|
+
if (Object.keys(parsed).length > 0) {
|
|
140
|
+
declarations.set(varName, parsed);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return declarations;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Simple args parser that doesn't do spread resolution.
|
|
149
|
+
* Used for extracting const declarations to avoid infinite recursion.
|
|
150
|
+
*/
|
|
151
|
+
function parseArgsSimple(content: string): Record<string, unknown> {
|
|
152
|
+
const args: Record<string, unknown> = {};
|
|
153
|
+
|
|
154
|
+
const pairs = splitAtTopLevelCommas(content);
|
|
155
|
+
|
|
156
|
+
for (const pair of pairs) {
|
|
157
|
+
const trimmed = pair.trim();
|
|
158
|
+
if (!trimmed) continue;
|
|
159
|
+
|
|
160
|
+
// Skip spreads in simple parsing
|
|
161
|
+
if (trimmed.startsWith("...")) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Handle shorthand property
|
|
166
|
+
if (/^\w+$/.test(trimmed)) {
|
|
167
|
+
args[trimmed] = `__REF__${trimmed}`;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Pattern: key: value
|
|
172
|
+
const colonIndex = trimmed.indexOf(":");
|
|
173
|
+
if (colonIndex > 0) {
|
|
174
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
175
|
+
const valueStr = trimmed.slice(colonIndex + 1).trim();
|
|
176
|
+
|
|
177
|
+
if (/^\w+$/.test(key)) {
|
|
178
|
+
args[key] = parseArgValue(valueStr);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return args;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Parse the default export (meta) from a story file.
|
|
188
|
+
*/
|
|
189
|
+
function parseMeta(
|
|
190
|
+
content: string,
|
|
191
|
+
filePath: string,
|
|
192
|
+
warnings: string[]
|
|
193
|
+
): ParsedMeta {
|
|
194
|
+
const result: ParsedMeta = {
|
|
195
|
+
title: "",
|
|
196
|
+
componentName: "",
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Try to extract title
|
|
200
|
+
// Pattern: title: 'Components/Forms/Input' or title: "Components/Forms/Input"
|
|
201
|
+
// We need to be careful here - there might be multiple `title:` properties in the file
|
|
202
|
+
// (e.g., in translations objects). Storybook meta titles typically contain `/`.
|
|
203
|
+
const titleRegex = /title:\s*['"`]([^'"`]+)['"`]/g;
|
|
204
|
+
const titleMatches: string[] = [];
|
|
205
|
+
let match;
|
|
206
|
+
while ((match = titleRegex.exec(content)) !== null) {
|
|
207
|
+
titleMatches.push(match[1]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Prefer titles with "/" as they're Storybook component paths
|
|
211
|
+
// e.g., "Components/Forms/Button" vs "No recent searches"
|
|
212
|
+
const componentPathTitle = titleMatches.find((t) => t.includes("/"));
|
|
213
|
+
const selectedTitle = componentPathTitle ?? titleMatches[0];
|
|
214
|
+
|
|
215
|
+
if (selectedTitle) {
|
|
216
|
+
result.title = selectedTitle;
|
|
217
|
+
// Extract component name from title (last segment)
|
|
218
|
+
const segments = result.title.split("/");
|
|
219
|
+
result.componentName = segments[segments.length - 1];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Try to extract component reference
|
|
223
|
+
// Pattern: component: Button or component: MyComponent
|
|
224
|
+
const componentMatch = content.match(/component:\s*(\w+)/);
|
|
225
|
+
if (componentMatch) {
|
|
226
|
+
// Use component name from component field if title didn't provide one
|
|
227
|
+
if (!result.componentName) {
|
|
228
|
+
result.componentName = componentMatch[1];
|
|
229
|
+
}
|
|
230
|
+
// Try to find the import for this component
|
|
231
|
+
const importMatch = content.match(
|
|
232
|
+
new RegExp(
|
|
233
|
+
`import\\s*{[^}]*\\b${componentMatch[1]}\\b[^}]*}\\s*from\\s*['"\`]([^'"\`]+)['"\`]`
|
|
234
|
+
)
|
|
235
|
+
);
|
|
236
|
+
if (importMatch) {
|
|
237
|
+
result.componentImport = importMatch[1];
|
|
238
|
+
} else {
|
|
239
|
+
// Try default import
|
|
240
|
+
const defaultImportMatch = content.match(
|
|
241
|
+
new RegExp(
|
|
242
|
+
`import\\s+${componentMatch[1]}\\s+from\\s*['"\`]([^'"\`]+)['"\`]`
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
if (defaultImportMatch) {
|
|
246
|
+
result.componentImport = defaultImportMatch[1];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Extract tags
|
|
252
|
+
// Pattern: tags: ['autodocs', 'stable']
|
|
253
|
+
const tagsMatch = content.match(/tags:\s*\[([^\]]+)\]/);
|
|
254
|
+
if (tagsMatch) {
|
|
255
|
+
const tagsContent = tagsMatch[1];
|
|
256
|
+
result.tags = tagsContent
|
|
257
|
+
.split(",")
|
|
258
|
+
.map((t) => t.trim().replace(/['"`]/g, ""))
|
|
259
|
+
.filter(Boolean);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Extract description from parameters.docs
|
|
263
|
+
// Pattern: description: { component: '...' }
|
|
264
|
+
const descMatch = content.match(
|
|
265
|
+
/description:\s*\{[^}]*component:\s*['"`]([^'"`]+)['"`]/
|
|
266
|
+
);
|
|
267
|
+
if (descMatch) {
|
|
268
|
+
result.description = descMatch[1];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Fallback: infer component name from file path
|
|
272
|
+
if (!result.componentName) {
|
|
273
|
+
const match = filePath.match(/([^/\\]+)\.stories\.(tsx?|jsx?|mdx)$/);
|
|
274
|
+
if (match) {
|
|
275
|
+
result.componentName = match[1];
|
|
276
|
+
} else {
|
|
277
|
+
result.componentName = "Unknown";
|
|
278
|
+
warnings.push("Could not determine component name");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Fallback: generate title from component name
|
|
283
|
+
if (!result.title) {
|
|
284
|
+
result.title = `Components/${result.componentName}`;
|
|
285
|
+
warnings.push(`No title found, using default: ${result.title}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Parse argTypes from the meta export.
|
|
293
|
+
*/
|
|
294
|
+
function parseArgTypes(
|
|
295
|
+
content: string,
|
|
296
|
+
warnings: string[]
|
|
297
|
+
): Record<string, ParsedArgType> {
|
|
298
|
+
const result: Record<string, ParsedArgType> = {};
|
|
299
|
+
|
|
300
|
+
// Find argTypes block - match until we find the closing brace at the right level
|
|
301
|
+
const argTypesStart = content.indexOf("argTypes:");
|
|
302
|
+
if (argTypesStart === -1) {
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Find the opening brace
|
|
307
|
+
const braceStart = content.indexOf("{", argTypesStart);
|
|
308
|
+
if (braceStart === -1) {
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Find matching closing brace by counting nesting
|
|
313
|
+
let depth = 1;
|
|
314
|
+
let braceEnd = braceStart + 1;
|
|
315
|
+
while (depth > 0 && braceEnd < content.length) {
|
|
316
|
+
if (content[braceEnd] === "{") depth++;
|
|
317
|
+
if (content[braceEnd] === "}") depth--;
|
|
318
|
+
braceEnd++;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const argTypesContent = content.slice(braceStart + 1, braceEnd - 1);
|
|
322
|
+
|
|
323
|
+
// Parse individual argType entries by looking for property patterns
|
|
324
|
+
// Match property: { ... } at the top level of argTypes
|
|
325
|
+
let pos = 0;
|
|
326
|
+
while (pos < argTypesContent.length) {
|
|
327
|
+
// Skip whitespace and commas
|
|
328
|
+
while (pos < argTypesContent.length && /[\s,]/.test(argTypesContent[pos])) {
|
|
329
|
+
pos++;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Find property name
|
|
333
|
+
const nameMatch = argTypesContent.slice(pos).match(/^(\w+)\s*:\s*\{/);
|
|
334
|
+
if (!nameMatch) break;
|
|
335
|
+
|
|
336
|
+
const propName = nameMatch[1];
|
|
337
|
+
pos += nameMatch[0].length - 1; // Position at opening brace
|
|
338
|
+
|
|
339
|
+
// Find matching closing brace
|
|
340
|
+
let propDepth = 1;
|
|
341
|
+
const propStart = pos + 1;
|
|
342
|
+
pos++;
|
|
343
|
+
while (propDepth > 0 && pos < argTypesContent.length) {
|
|
344
|
+
if (argTypesContent[pos] === "{") propDepth++;
|
|
345
|
+
if (argTypesContent[pos] === "}") propDepth--;
|
|
346
|
+
pos++;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const propContent = argTypesContent.slice(propStart, pos - 1);
|
|
350
|
+
result[propName] = parseArgTypeContent(propContent, warnings);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Parse the content of a single argType definition.
|
|
358
|
+
*/
|
|
359
|
+
function parseArgTypeContent(content: string, warnings: string[]): ParsedArgType {
|
|
360
|
+
const result: ParsedArgType = {};
|
|
361
|
+
|
|
362
|
+
// Extract control type
|
|
363
|
+
// Pattern: control: 'select' or control: { type: 'select' }
|
|
364
|
+
const controlMatch = content.match(/control:\s*['"`](\w+)['"`]/);
|
|
365
|
+
if (controlMatch) {
|
|
366
|
+
result.control = controlMatch[1];
|
|
367
|
+
} else {
|
|
368
|
+
const controlTypeMatch = content.match(/control:\s*\{[^}]*type:\s*['"`](\w+)['"`]/);
|
|
369
|
+
if (controlTypeMatch) {
|
|
370
|
+
result.control = controlTypeMatch[1];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Extract options
|
|
375
|
+
// Pattern: options: ['a', 'b', 'c']
|
|
376
|
+
const optionsMatch = content.match(/options:\s*\[([^\]]+)\]/);
|
|
377
|
+
if (optionsMatch) {
|
|
378
|
+
result.options = optionsMatch[1]
|
|
379
|
+
.split(",")
|
|
380
|
+
.map((o) => o.trim().replace(/['"`]/g, ""))
|
|
381
|
+
.filter(Boolean);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Extract description
|
|
385
|
+
// Pattern: description: '...'
|
|
386
|
+
const descMatch = content.match(/description:\s*['"`]([^'"`]+)['"`]/);
|
|
387
|
+
if (descMatch) {
|
|
388
|
+
result.description = descMatch[1];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Extract default value
|
|
392
|
+
// Pattern: defaultValue: { summary: '...' } or defaultValue: '...'
|
|
393
|
+
const defaultMatch = content.match(/defaultValue:\s*\{[^}]*summary:\s*['"`]([^'"`]+)['"`]/);
|
|
394
|
+
if (defaultMatch) {
|
|
395
|
+
result.defaultValue = defaultMatch[1];
|
|
396
|
+
} else {
|
|
397
|
+
const simpleDefaultMatch = content.match(/defaultValue:\s*['"`]?([^,\s'"`]+)['"`]?/);
|
|
398
|
+
if (simpleDefaultMatch && simpleDefaultMatch[1] !== "{") {
|
|
399
|
+
result.defaultValue = simpleDefaultMatch[1];
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Parse individual story exports.
|
|
408
|
+
*
|
|
409
|
+
* Supports both CSF 2.0 and CSF 3.0 formats:
|
|
410
|
+
* - CSF 3.0: export const Story = { args: {...} }
|
|
411
|
+
* - CSF 2.0: export const Story = Template.bind({}) + Story.args = {...}
|
|
412
|
+
*/
|
|
413
|
+
function parseStories(
|
|
414
|
+
content: string,
|
|
415
|
+
componentName: string,
|
|
416
|
+
warnings: string[],
|
|
417
|
+
constDeclarations: Map<string, Record<string, unknown>>
|
|
418
|
+
): ParsedStory[] {
|
|
419
|
+
const stories: ParsedStory[] = [];
|
|
420
|
+
const storyNames = new Set<string>();
|
|
421
|
+
|
|
422
|
+
// CSF 3.0: export const StoryName: Story = { ... }
|
|
423
|
+
const csf3Pattern = /export\s+const\s+(\w+)(?::\s*\w+)?\s*=\s*\{([^;]*(?:\{[^}]*\}[^;]*)*)\}/g;
|
|
424
|
+
|
|
425
|
+
let match;
|
|
426
|
+
while ((match = csf3Pattern.exec(content)) !== null) {
|
|
427
|
+
const storyName = match[1];
|
|
428
|
+
const storyContent = match[2];
|
|
429
|
+
|
|
430
|
+
// Skip if it's the meta export
|
|
431
|
+
if (storyName === "default" || storyName === "meta") {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Skip if it looks like a type definition
|
|
436
|
+
if (storyContent.includes("typeof")) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const story = parseStoryContent2(storyName, storyContent, componentName, warnings, constDeclarations);
|
|
441
|
+
if (story) {
|
|
442
|
+
stories.push(story);
|
|
443
|
+
storyNames.add(storyName);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// CSF 2.0: export const StoryName = Template.bind({})
|
|
448
|
+
// Followed by StoryName.args = { ... }
|
|
449
|
+
const csf2Pattern = /export\s+const\s+(\w+)\s*=\s*(\w+)\.bind\s*\(\s*\{\s*\}\s*\)/g;
|
|
450
|
+
|
|
451
|
+
while ((match = csf2Pattern.exec(content)) !== null) {
|
|
452
|
+
const storyName = match[1];
|
|
453
|
+
const templateName = match[2];
|
|
454
|
+
|
|
455
|
+
// Skip if already found via CSF 3.0 pattern
|
|
456
|
+
if (storyNames.has(storyName)) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Skip meta-like names
|
|
461
|
+
if (storyName === "default" || storyName === "meta") {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Look for StoryName.args = { ... } using proper brace matching
|
|
466
|
+
const argsContent = extractArgsContent(content, storyName);
|
|
467
|
+
|
|
468
|
+
const story: ParsedStory = {
|
|
469
|
+
name: storyName,
|
|
470
|
+
args: argsContent ? parseArgs(argsContent, constDeclarations) : {},
|
|
471
|
+
hasCustomRender: isCustomTemplate(content, templateName),
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
// Look for StoryName.parameters.docs.description.story
|
|
475
|
+
const descPattern = new RegExp(
|
|
476
|
+
`${storyName}\\.parameters\\s*=\\s*\\{[^}]*docs:\\s*\\{[^}]*description:\\s*\\{[^}]*story:\\s*['"\`]([^'"\`]+)['"\`]`
|
|
477
|
+
);
|
|
478
|
+
const descMatch = content.match(descPattern);
|
|
479
|
+
if (descMatch) {
|
|
480
|
+
story.description = descMatch[1];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
stories.push(story);
|
|
484
|
+
storyNames.add(storyName);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return stories;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Extract the content inside StoryName.args = { ... } using proper brace matching.
|
|
492
|
+
*/
|
|
493
|
+
function extractArgsContent(content: string, storyName: string): string | null {
|
|
494
|
+
// Find the position of StoryName.args =
|
|
495
|
+
const argsAssignPattern = new RegExp(`${storyName}\\.args\\s*=\\s*\\{`);
|
|
496
|
+
const argsMatch = content.match(argsAssignPattern);
|
|
497
|
+
|
|
498
|
+
if (!argsMatch || argsMatch.index === undefined) {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Find the opening brace position
|
|
503
|
+
const startPos = argsMatch.index + argsMatch[0].length - 1; // Position of {
|
|
504
|
+
|
|
505
|
+
// Find the matching closing brace
|
|
506
|
+
const closingIndex = findMatchingBraceInContent(content, startPos, "{", "}");
|
|
507
|
+
|
|
508
|
+
if (closingIndex === -1) {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Return the content between the braces
|
|
513
|
+
return content.slice(startPos + 1, closingIndex);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Find matching brace in content string, respecting strings and nesting.
|
|
518
|
+
*/
|
|
519
|
+
function findMatchingBraceInContent(
|
|
520
|
+
content: string,
|
|
521
|
+
startIndex: number,
|
|
522
|
+
openChar: string,
|
|
523
|
+
closeChar: string
|
|
524
|
+
): number {
|
|
525
|
+
let depth = 0;
|
|
526
|
+
let inString: string | null = null;
|
|
527
|
+
|
|
528
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
529
|
+
const char = content[i];
|
|
530
|
+
const prevChar = i > 0 ? content[i - 1] : "";
|
|
531
|
+
|
|
532
|
+
// Handle string boundaries (but not escaped quotes)
|
|
533
|
+
if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
|
|
534
|
+
if (inString === char) {
|
|
535
|
+
inString = null;
|
|
536
|
+
} else if (inString === null) {
|
|
537
|
+
inString = char;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (inString === null) {
|
|
542
|
+
if (char === openChar) {
|
|
543
|
+
depth++;
|
|
544
|
+
} else if (char === closeChar) {
|
|
545
|
+
depth--;
|
|
546
|
+
if (depth === 0) {
|
|
547
|
+
return i;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return -1;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Check if a template has custom rendering logic beyond simple args spreading.
|
|
558
|
+
*
|
|
559
|
+
* Simple templates look like: (args) => <Component {...args} />
|
|
560
|
+
* Custom templates have hooks, state, or complex logic.
|
|
561
|
+
*/
|
|
562
|
+
function isCustomTemplate(content: string, templateName: string): boolean {
|
|
563
|
+
// Look for the template definition
|
|
564
|
+
const templatePattern = new RegExp(
|
|
565
|
+
`const\\s+${templateName}[^=]*=\\s*\\([^)]*\\)\\s*=>\\s*([\\s\\S]*?)(?=\\n(?:export|const\\s+\\w+\\s*=)|$)`,
|
|
566
|
+
"m"
|
|
567
|
+
);
|
|
568
|
+
const match = content.match(templatePattern);
|
|
569
|
+
|
|
570
|
+
if (!match) {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const templateBody = match[1].trim();
|
|
575
|
+
|
|
576
|
+
// Simple template patterns:
|
|
577
|
+
// (args) => <Component {...args} />
|
|
578
|
+
// (args) => <Component {...args}>text</Component>
|
|
579
|
+
// (args) => (<Component {...args} />)
|
|
580
|
+
const simplePatterns = [
|
|
581
|
+
/^<\w+\s+\{\.\.\.args\}\s*\/?>/, // <Comp {...args} /> or <Comp {...args}>
|
|
582
|
+
/^\(\s*<\w+\s+\{\.\.\.args\}\s*\/?>\s*\)/, // (<Comp {...args} />)
|
|
583
|
+
];
|
|
584
|
+
|
|
585
|
+
for (const pattern of simplePatterns) {
|
|
586
|
+
if (pattern.test(templateBody)) {
|
|
587
|
+
return false; // It's a simple template
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Check for hooks or complex patterns that indicate custom logic
|
|
592
|
+
const customIndicators = [
|
|
593
|
+
"useState",
|
|
594
|
+
"useEffect",
|
|
595
|
+
"useRef",
|
|
596
|
+
"useCallback",
|
|
597
|
+
"useMemo",
|
|
598
|
+
"useContext",
|
|
599
|
+
"return (", // Multi-line return with logic
|
|
600
|
+
];
|
|
601
|
+
|
|
602
|
+
for (const indicator of customIndicators) {
|
|
603
|
+
if (templateBody.includes(indicator)) {
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Check for variable declarations inside template (indicates logic)
|
|
609
|
+
// But exclude the template itself
|
|
610
|
+
const bodyWithoutFirstLine = templateBody.split("\n").slice(1).join("\n");
|
|
611
|
+
if (/const\s+\w+\s*=/.test(bodyWithoutFirstLine)) {
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// If the template body is a single JSX element with {...args}, it's simple
|
|
616
|
+
// Otherwise assume it might have complexity we can't detect
|
|
617
|
+
if (templateBody.includes("{...args}") && !templateBody.includes("return")) {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Default: if we're not sure, treat as simple to get the args
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Parse a single story's content.
|
|
627
|
+
*/
|
|
628
|
+
function parseStoryContent2(
|
|
629
|
+
name: string,
|
|
630
|
+
content: string,
|
|
631
|
+
componentName: string,
|
|
632
|
+
warnings: string[],
|
|
633
|
+
constDeclarations: Map<string, Record<string, unknown>>
|
|
634
|
+
): ParsedStory | null {
|
|
635
|
+
const result: ParsedStory = {
|
|
636
|
+
name,
|
|
637
|
+
args: {},
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// Extract args
|
|
641
|
+
// Pattern: args: { ... }
|
|
642
|
+
const argsMatch = content.match(/args:\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/);
|
|
643
|
+
if (argsMatch) {
|
|
644
|
+
result.args = parseArgs(argsMatch[1], constDeclarations);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Check for custom render function
|
|
648
|
+
// Note: We only mark hasCustomRender=true, we don't try to extract the render code.
|
|
649
|
+
// Regex-based extraction fails on complex JSX with nested braces.
|
|
650
|
+
// Stories with custom renders should be migrated manually or loaded directly via viewer.
|
|
651
|
+
if (content.includes("render:") || content.includes("render(")) {
|
|
652
|
+
result.hasCustomRender = true;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Extract story description if present
|
|
656
|
+
const descMatch = content.match(/parameters:\s*\{[^}]*docs:\s*\{[^}]*description:\s*\{[^}]*story:\s*['"`]([^'"`]+)['"`]/);
|
|
657
|
+
if (descMatch) {
|
|
658
|
+
result.description = descMatch[1];
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return result;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Parse args object content into a Record.
|
|
666
|
+
*
|
|
667
|
+
* Handles nested objects, arrays, and various value types.
|
|
668
|
+
* Uses brace/bracket matching to correctly split at top-level commas.
|
|
669
|
+
*
|
|
670
|
+
* @param content - The string content inside the args object
|
|
671
|
+
* @param constDeclarations - Optional map of const variable names to their parsed values,
|
|
672
|
+
* used to resolve spread syntax like ...defaultArgs
|
|
673
|
+
*/
|
|
674
|
+
function parseArgs(
|
|
675
|
+
content: string,
|
|
676
|
+
constDeclarations?: Map<string, Record<string, unknown>>
|
|
677
|
+
): Record<string, unknown> {
|
|
678
|
+
const args: Record<string, unknown> = {};
|
|
679
|
+
|
|
680
|
+
// Split into key-value pairs at top-level commas only
|
|
681
|
+
const pairs = splitAtTopLevelCommas(content);
|
|
682
|
+
|
|
683
|
+
for (const pair of pairs) {
|
|
684
|
+
const trimmed = pair.trim();
|
|
685
|
+
if (!trimmed) continue;
|
|
686
|
+
|
|
687
|
+
// Handle spread syntax: ...variable or ...variable.property
|
|
688
|
+
if (trimmed.startsWith("...")) {
|
|
689
|
+
const spreadValue = trimmed.slice(3).trim();
|
|
690
|
+
|
|
691
|
+
// Try to resolve the spread from const declarations
|
|
692
|
+
if (constDeclarations && /^\w+$/.test(spreadValue)) {
|
|
693
|
+
const resolved = constDeclarations.get(spreadValue);
|
|
694
|
+
if (resolved) {
|
|
695
|
+
// Inline all properties from the resolved object
|
|
696
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
697
|
+
// Don't overwrite existing properties (spread comes first, explicit props override)
|
|
698
|
+
if (!(key in args)) {
|
|
699
|
+
args[key] = value;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Couldn't resolve - mark as unresolvable reference
|
|
707
|
+
args["__SPREAD__"] = `__REF__${spreadValue}`;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Handle shorthand property: just `varName` instead of `varName: varName`
|
|
712
|
+
if (/^\w+$/.test(trimmed)) {
|
|
713
|
+
// Variable reference shorthand - mark as unresolved reference
|
|
714
|
+
args[trimmed] = `__REF__${trimmed}`;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Pattern: key: value
|
|
719
|
+
const colonIndex = trimmed.indexOf(":");
|
|
720
|
+
if (colonIndex > 0) {
|
|
721
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
722
|
+
const valueStr = trimmed.slice(colonIndex + 1).trim();
|
|
723
|
+
|
|
724
|
+
// Only parse if key is a valid identifier
|
|
725
|
+
if (/^\w+$/.test(key)) {
|
|
726
|
+
args[key] = parseArgValue(valueStr, constDeclarations);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return args;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Split a string at top-level commas, respecting nested braces/brackets/parens.
|
|
736
|
+
*/
|
|
737
|
+
function splitAtTopLevelCommas(content: string): string[] {
|
|
738
|
+
const parts: string[] = [];
|
|
739
|
+
let current = "";
|
|
740
|
+
let depth = 0;
|
|
741
|
+
let inString: string | null = null;
|
|
742
|
+
|
|
743
|
+
for (let i = 0; i < content.length; i++) {
|
|
744
|
+
const char = content[i];
|
|
745
|
+
const prevChar = i > 0 ? content[i - 1] : "";
|
|
746
|
+
|
|
747
|
+
// Handle string boundaries
|
|
748
|
+
if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
|
|
749
|
+
if (inString === char) {
|
|
750
|
+
inString = null;
|
|
751
|
+
} else if (inString === null) {
|
|
752
|
+
inString = char;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Track nesting depth (only when not in a string)
|
|
757
|
+
if (inString === null) {
|
|
758
|
+
if (char === "{" || char === "[" || char === "(") {
|
|
759
|
+
depth++;
|
|
760
|
+
} else if (char === "}" || char === "]" || char === ")") {
|
|
761
|
+
depth--;
|
|
762
|
+
} else if (char === "," && depth === 0) {
|
|
763
|
+
parts.push(current);
|
|
764
|
+
current = "";
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
current += char;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (current.trim()) {
|
|
773
|
+
parts.push(current);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return parts;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Resolve JavaScript escape sequences in a string.
|
|
781
|
+
* Converts \n, \t, \\, \", \', etc. to their actual characters.
|
|
782
|
+
*/
|
|
783
|
+
function resolveEscapeSequences(str: string): string {
|
|
784
|
+
let result = "";
|
|
785
|
+
let i = 0;
|
|
786
|
+
|
|
787
|
+
while (i < str.length) {
|
|
788
|
+
if (str[i] === "\\" && i + 1 < str.length) {
|
|
789
|
+
const next = str[i + 1];
|
|
790
|
+
switch (next) {
|
|
791
|
+
case "n": result += "\n"; i += 2; break;
|
|
792
|
+
case "t": result += "\t"; i += 2; break;
|
|
793
|
+
case "r": result += "\r"; i += 2; break;
|
|
794
|
+
case "\\": result += "\\"; i += 2; break;
|
|
795
|
+
case '"': result += '"'; i += 2; break;
|
|
796
|
+
case "'": result += "'"; i += 2; break;
|
|
797
|
+
case "`": result += "`"; i += 2; break;
|
|
798
|
+
default:
|
|
799
|
+
// Keep unrecognized escape sequences as-is
|
|
800
|
+
result += str[i];
|
|
801
|
+
i += 1;
|
|
802
|
+
}
|
|
803
|
+
} else {
|
|
804
|
+
result += str[i];
|
|
805
|
+
i += 1;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return result;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Try to parse and resolve string concatenation.
|
|
814
|
+
* Returns the joined string if the value is string concatenation, null otherwise.
|
|
815
|
+
*
|
|
816
|
+
* Examples:
|
|
817
|
+
* "text1" + "text2" -> "text1text2"
|
|
818
|
+
* 'text1' + 'text2' -> "text1text2"
|
|
819
|
+
*/
|
|
820
|
+
function tryParseStringConcatenation(value: string): string | null {
|
|
821
|
+
// Quick check: must contain a + operator with quotes on both sides
|
|
822
|
+
if (!value.includes("+")) {
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Pattern for string concatenation: strings joined by +
|
|
827
|
+
// Split by + but respect string boundaries
|
|
828
|
+
const parts: string[] = [];
|
|
829
|
+
let current = "";
|
|
830
|
+
let inString: string | null = null;
|
|
831
|
+
|
|
832
|
+
for (let i = 0; i < value.length; i++) {
|
|
833
|
+
const char = value[i];
|
|
834
|
+
const prevChar = i > 0 ? value[i - 1] : "";
|
|
835
|
+
|
|
836
|
+
// Handle string boundaries
|
|
837
|
+
if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
|
|
838
|
+
if (inString === char) {
|
|
839
|
+
inString = null;
|
|
840
|
+
} else if (inString === null) {
|
|
841
|
+
inString = char;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Split at + when not in a string
|
|
846
|
+
if (char === "+" && inString === null) {
|
|
847
|
+
const trimmed = current.trim();
|
|
848
|
+
if (trimmed) {
|
|
849
|
+
parts.push(trimmed);
|
|
850
|
+
}
|
|
851
|
+
current = "";
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
current += char;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Don't forget the last part
|
|
859
|
+
const lastTrimmed = current.trim();
|
|
860
|
+
if (lastTrimmed) {
|
|
861
|
+
parts.push(lastTrimmed);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Need at least 2 parts for concatenation
|
|
865
|
+
if (parts.length < 2) {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// All parts must be string literals
|
|
870
|
+
const stringParts: string[] = [];
|
|
871
|
+
for (const part of parts) {
|
|
872
|
+
// Check if it's a quoted string
|
|
873
|
+
if ((part.startsWith('"') && part.endsWith('"')) ||
|
|
874
|
+
(part.startsWith("'") && part.endsWith("'"))) {
|
|
875
|
+
// Resolve escape sequences like \" \' \\ \n etc.
|
|
876
|
+
stringParts.push(resolveEscapeSequences(part.slice(1, -1)));
|
|
877
|
+
} else if (part.startsWith("`") && part.endsWith("`")) {
|
|
878
|
+
// Template literal without interpolation
|
|
879
|
+
if (!part.includes("${")) {
|
|
880
|
+
stringParts.push(resolveEscapeSequences(part.slice(1, -1)));
|
|
881
|
+
} else {
|
|
882
|
+
return null; // Has interpolation, can't resolve statically
|
|
883
|
+
}
|
|
884
|
+
} else {
|
|
885
|
+
// Not a string literal, can't resolve statically
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Join all string parts
|
|
891
|
+
return stringParts.join("");
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Parse a single arg value, handling nested structures.
|
|
896
|
+
*
|
|
897
|
+
* @param value - The string value to parse
|
|
898
|
+
* @param constDeclarations - Optional map of const variable names to their parsed values
|
|
899
|
+
*/
|
|
900
|
+
function parseArgValue(
|
|
901
|
+
value: string,
|
|
902
|
+
constDeclarations?: Map<string, Record<string, unknown>>
|
|
903
|
+
): unknown {
|
|
904
|
+
value = value.trim();
|
|
905
|
+
|
|
906
|
+
// Remove trailing comma if present
|
|
907
|
+
value = value.replace(/,\s*$/, "");
|
|
908
|
+
|
|
909
|
+
if (!value) return undefined;
|
|
910
|
+
|
|
911
|
+
// Handle string concatenation: "text1" + "text2" + "text3"
|
|
912
|
+
// This is common in Storybook for long strings
|
|
913
|
+
const concatenationResult = tryParseStringConcatenation(value);
|
|
914
|
+
if (concatenationResult !== null) {
|
|
915
|
+
return concatenationResult;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Handle TypeScript "as const" assertions - treat as the underlying value
|
|
919
|
+
// "left" as const -> "left"
|
|
920
|
+
const asConstMatch = value.match(/^(['"`])(.+?)\1\s+as\s+const$/);
|
|
921
|
+
if (asConstMatch) {
|
|
922
|
+
return resolveEscapeSequences(asConstMatch[2]); // Return just the string content
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// String (single or double quoted)
|
|
926
|
+
if ((value.startsWith("'") && value.endsWith("'")) ||
|
|
927
|
+
(value.startsWith('"') && value.endsWith('"'))) {
|
|
928
|
+
return resolveEscapeSequences(value.slice(1, -1));
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Template literal - just extract the content
|
|
932
|
+
if (value.startsWith("`") && value.endsWith("`")) {
|
|
933
|
+
return resolveEscapeSequences(value.slice(1, -1));
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Boolean
|
|
937
|
+
if (value === "true") return true;
|
|
938
|
+
if (value === "false") return false;
|
|
939
|
+
|
|
940
|
+
// Number
|
|
941
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
942
|
+
return parseFloat(value);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// null/undefined
|
|
946
|
+
if (value === "null") return null;
|
|
947
|
+
if (value === "undefined") return undefined;
|
|
948
|
+
|
|
949
|
+
// Nested object: { ... } - find matching braces
|
|
950
|
+
if (value.startsWith("{")) {
|
|
951
|
+
const closingIndex = findMatchingBrace(value, 0, "{", "}");
|
|
952
|
+
if (closingIndex !== -1) {
|
|
953
|
+
const inner = value.slice(1, closingIndex).trim();
|
|
954
|
+
if (inner) {
|
|
955
|
+
return parseArgs(inner, constDeclarations);
|
|
956
|
+
}
|
|
957
|
+
return {};
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Array: [ ... ] - find matching brackets
|
|
962
|
+
if (value.startsWith("[")) {
|
|
963
|
+
const closingIndex = findMatchingBrace(value, 0, "[", "]");
|
|
964
|
+
if (closingIndex !== -1) {
|
|
965
|
+
const inner = value.slice(1, closingIndex).trim();
|
|
966
|
+
if (!inner) return [];
|
|
967
|
+
|
|
968
|
+
const items = splitAtTopLevelCommas(inner);
|
|
969
|
+
return items.map((item) => parseArgValue(item.trim(), constDeclarations)).filter((v) => v !== undefined);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// JSX element: <Component ... /> or <Component>...</Component>
|
|
974
|
+
if (value.startsWith("<")) {
|
|
975
|
+
const trimmed = value.trimEnd();
|
|
976
|
+
if (trimmed.endsWith("/>") || trimmed.endsWith(">")) {
|
|
977
|
+
return `__JSX__`;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Function call or complex expression
|
|
982
|
+
if (value.includes("(") || value.includes("=>")) {
|
|
983
|
+
return `__EXPR__`;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// TypeScript `as` type assertions (not `as const` which we handled above)
|
|
987
|
+
// e.g., `someVar as SomeType` or `obj as Type<T>["prop"]`
|
|
988
|
+
// These are expressions we can't safely convert to static JSX
|
|
989
|
+
if (/\s+as\s+[A-Z]/.test(value)) {
|
|
990
|
+
return `__EXPR__`;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Variable reference (identifier) - try to resolve from constDeclarations
|
|
994
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value)) {
|
|
995
|
+
// Check if we can resolve this variable from const declarations
|
|
996
|
+
if (constDeclarations) {
|
|
997
|
+
const resolved = constDeclarations.get(value);
|
|
998
|
+
if (resolved !== undefined) {
|
|
999
|
+
return resolved;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return `__REF__${value}`;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Property access like obj.prop or obj?.prop - try to resolve from constDeclarations
|
|
1006
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*(\??\.[a-zA-Z_$][a-zA-Z0-9_$]*)+$/.test(value)) {
|
|
1007
|
+
// Try to resolve the base variable and access the property
|
|
1008
|
+
if (constDeclarations) {
|
|
1009
|
+
const parts = value.replace(/\?/g, '').split('.');
|
|
1010
|
+
const baseVar = parts[0];
|
|
1011
|
+
const resolved = constDeclarations.get(baseVar);
|
|
1012
|
+
if (resolved !== undefined) {
|
|
1013
|
+
// Navigate through the property path
|
|
1014
|
+
let current: unknown = resolved;
|
|
1015
|
+
for (let i = 1; i < parts.length && current !== undefined; i++) {
|
|
1016
|
+
if (typeof current === 'object' && current !== null) {
|
|
1017
|
+
current = (current as Record<string, unknown>)[parts[i]];
|
|
1018
|
+
} else {
|
|
1019
|
+
current = undefined;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (current !== undefined) {
|
|
1023
|
+
return current;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return `__REF__${value}`;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Array index access with optional property chain and non-null assertions
|
|
1031
|
+
// Examples: arr[0], arr[0]!, obj.arr[0], mockOptions[0]!.label
|
|
1032
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*(\??\.[a-zA-Z_$][a-zA-Z0-9_$]*)*\[\d+\]!?(\??\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(value)) {
|
|
1033
|
+
return `__REF__${value}`;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// TypeScript non-null assertion on variable: variable!
|
|
1037
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*!$/.test(value)) {
|
|
1038
|
+
return `__REF__${value}`;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Return as-is for other values
|
|
1042
|
+
return value;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Find the index of the matching closing brace/bracket.
|
|
1047
|
+
* Returns the index of the closing char, or -1 if not found.
|
|
1048
|
+
*/
|
|
1049
|
+
function findMatchingBrace(
|
|
1050
|
+
content: string,
|
|
1051
|
+
startIndex: number,
|
|
1052
|
+
openChar: string,
|
|
1053
|
+
closeChar: string
|
|
1054
|
+
): number {
|
|
1055
|
+
let depth = 0;
|
|
1056
|
+
let inString: string | null = null;
|
|
1057
|
+
|
|
1058
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
1059
|
+
const char = content[i];
|
|
1060
|
+
const prevChar = i > 0 ? content[i - 1] : "";
|
|
1061
|
+
|
|
1062
|
+
// Handle string boundaries
|
|
1063
|
+
if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
|
|
1064
|
+
if (inString === char) {
|
|
1065
|
+
inString = null;
|
|
1066
|
+
} else if (inString === null) {
|
|
1067
|
+
inString = char;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (inString === null) {
|
|
1072
|
+
if (char === openChar) {
|
|
1073
|
+
depth++;
|
|
1074
|
+
} else if (char === closeChar) {
|
|
1075
|
+
depth--;
|
|
1076
|
+
if (depth === 0) {
|
|
1077
|
+
return i;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
return -1;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Convert PascalCase to Title Case with spaces.
|
|
1088
|
+
*/
|
|
1089
|
+
export function storyNameToTitle(name: string): string {
|
|
1090
|
+
// Handle common patterns
|
|
1091
|
+
// PrimaryButton -> Primary Button
|
|
1092
|
+
// DisabledState -> Disabled State
|
|
1093
|
+
return name
|
|
1094
|
+
.replace(/([A-Z])/g, " $1")
|
|
1095
|
+
.trim()
|
|
1096
|
+
.replace(/\s+/g, " ");
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Extract category from Storybook title path.
|
|
1101
|
+
*/
|
|
1102
|
+
export function extractCategory(title: string): string {
|
|
1103
|
+
const segments = title.split("/");
|
|
1104
|
+
|
|
1105
|
+
// If we have at least 2 segments, use the second-to-last as category
|
|
1106
|
+
// "Components/Forms/Input" -> "forms"
|
|
1107
|
+
// "Actions/Button" -> "actions"
|
|
1108
|
+
if (segments.length >= 2) {
|
|
1109
|
+
const category = segments[segments.length - 2];
|
|
1110
|
+
return category.toLowerCase();
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Default to "components" if no category in path
|
|
1114
|
+
return "components";
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Export internal functions for testing
|
|
1118
|
+
export const __testing = {
|
|
1119
|
+
parseMeta,
|
|
1120
|
+
parseArgTypes,
|
|
1121
|
+
parseArgTypeContent,
|
|
1122
|
+
parseStories,
|
|
1123
|
+
parseStoryContent2,
|
|
1124
|
+
parseArgs,
|
|
1125
|
+
parseArgsSimple,
|
|
1126
|
+
parseArgValue,
|
|
1127
|
+
extractConstDeclarations,
|
|
1128
|
+
extractArgsContent,
|
|
1129
|
+
findMatchingBrace,
|
|
1130
|
+
findMatchingBraceInContent,
|
|
1131
|
+
splitAtTopLevelCommas,
|
|
1132
|
+
resolveEscapeSequences,
|
|
1133
|
+
tryParseStringConcatenation,
|
|
1134
|
+
isCustomTemplate,
|
|
1135
|
+
calculateConfidence,
|
|
1136
|
+
};
|