@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,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
+ };