@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,665 @@
1
+ /**
2
+ * TypeScript Props Extractor
3
+ *
4
+ * Uses TypeScript AST to extract complete prop definitions
5
+ * from component TypeScript interfaces and types.
6
+ *
7
+ * This extracts:
8
+ * - Interface and type alias definitions
9
+ * - Property names, types, and optionality
10
+ * - JSDoc comments and descriptions
11
+ * - Union types (for enum-like values)
12
+ * - Default values from JSDoc tags
13
+ */
14
+
15
+ import * as ts from "typescript";
16
+ import { readFile } from "node:fs/promises";
17
+ import { existsSync } from "node:fs";
18
+ import { basename, dirname, join, resolve } from "node:path";
19
+ import type { PropDefinition, PropType } from "../../core/index.js";
20
+
21
+ /**
22
+ * Extracted prop with full type information
23
+ */
24
+ export interface ExtractedProp {
25
+ /** Prop name */
26
+ name: string;
27
+ /** Type string for display */
28
+ type: string;
29
+ /** Structured type information */
30
+ propType: PropType;
31
+ /** JSDoc description */
32
+ description: string;
33
+ /** Whether the prop is required */
34
+ required: boolean;
35
+ /** Default value if detected */
36
+ defaultValue?: unknown;
37
+ /** For enum/union types, the specific values */
38
+ enumValues?: string[];
39
+ /** Deprecation notice if present */
40
+ deprecated?: string;
41
+ /** Additional JSDoc tags */
42
+ tags?: Record<string, string>;
43
+ }
44
+
45
+ /**
46
+ * Result of props extraction from a component file
47
+ */
48
+ export interface PropsExtractionResult {
49
+ /** Path to the source file */
50
+ filePath: string;
51
+ /** Component name */
52
+ componentName: string;
53
+ /** All extracted props */
54
+ props: ExtractedProp[];
55
+ /** Name of the props interface/type found */
56
+ propsTypeName?: string;
57
+ /** Whether extraction was successful */
58
+ success: boolean;
59
+ /** Any warnings or errors encountered */
60
+ warnings: string[];
61
+ }
62
+
63
+ /**
64
+ * Options for props extraction
65
+ */
66
+ export interface PropsExtractionOptions {
67
+ /** Look for a specific props type name (e.g., "ButtonProps") */
68
+ propsTypeName?: string;
69
+ /** Include inherited props from extended interfaces */
70
+ includeInherited?: boolean;
71
+ }
72
+
73
+ /**
74
+ * Extract props from a TypeScript/TSX component file
75
+ */
76
+ export async function extractPropsFromFile(
77
+ filePath: string,
78
+ options: PropsExtractionOptions = {}
79
+ ): Promise<PropsExtractionResult> {
80
+ const absPath = resolve(filePath);
81
+
82
+ if (!existsSync(absPath)) {
83
+ return {
84
+ filePath: absPath,
85
+ componentName: inferComponentName(absPath),
86
+ props: [],
87
+ success: false,
88
+ warnings: [`File not found: ${absPath}`],
89
+ };
90
+ }
91
+
92
+ const content = await readFile(absPath, "utf-8");
93
+ return extractPropsFromSource(content, absPath, options);
94
+ }
95
+
96
+ /**
97
+ * Extract props from TypeScript source code
98
+ */
99
+ export function extractPropsFromSource(
100
+ source: string,
101
+ filePath: string,
102
+ options: PropsExtractionOptions = {}
103
+ ): PropsExtractionResult {
104
+ const { propsTypeName } = options;
105
+
106
+ const componentName = inferComponentName(filePath);
107
+ const result: PropsExtractionResult = {
108
+ filePath,
109
+ componentName,
110
+ props: [],
111
+ success: false,
112
+ warnings: [],
113
+ };
114
+
115
+ // Create a source file
116
+ const sourceFile = ts.createSourceFile(
117
+ filePath,
118
+ source,
119
+ ts.ScriptTarget.ESNext,
120
+ true,
121
+ filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS
122
+ );
123
+
124
+ // Find all interfaces and type aliases that could be props
125
+ const propsDeclarations: Array<{
126
+ name: string;
127
+ node: ts.InterfaceDeclaration | ts.TypeAliasDeclaration;
128
+ }> = [];
129
+
130
+ ts.forEachChild(sourceFile, (node) => {
131
+ if (ts.isInterfaceDeclaration(node)) {
132
+ propsDeclarations.push({ name: node.name.text, node });
133
+ }
134
+ if (ts.isTypeAliasDeclaration(node)) {
135
+ propsDeclarations.push({ name: node.name.text, node });
136
+ }
137
+ });
138
+
139
+ // Find the best matching props type
140
+ const targetName = propsTypeName || `${componentName}Props`;
141
+ let propsDecl = propsDeclarations.find((d) => d.name === targetName);
142
+
143
+ // Fallback to common names
144
+ if (!propsDecl) {
145
+ const fallbackNames = ["Props", `${componentName}Properties`];
146
+ for (const name of fallbackNames) {
147
+ propsDecl = propsDeclarations.find((d) => d.name === name);
148
+ if (propsDecl) break;
149
+ }
150
+ }
151
+
152
+ // Fallback to any *Props or *Properties
153
+ if (!propsDecl) {
154
+ propsDecl = propsDeclarations.find(
155
+ (d) => d.name.endsWith("Props") || d.name.endsWith("Properties")
156
+ );
157
+ }
158
+
159
+ if (!propsDecl) {
160
+ result.warnings.push(
161
+ `No props type found for ${componentName}. Looked for: ${targetName}`
162
+ );
163
+ return result;
164
+ }
165
+
166
+ result.propsTypeName = propsDecl.name;
167
+
168
+ // Extract props from the declaration
169
+ if (ts.isInterfaceDeclaration(propsDecl.node)) {
170
+ extractPropsFromInterface(propsDecl.node, sourceFile, result);
171
+ } else if (ts.isTypeAliasDeclaration(propsDecl.node)) {
172
+ extractPropsFromTypeAlias(propsDecl.node, sourceFile, result);
173
+ }
174
+
175
+ result.success = result.props.length > 0;
176
+ return result;
177
+ }
178
+
179
+ /**
180
+ * Extract props from an interface declaration
181
+ */
182
+ function extractPropsFromInterface(
183
+ node: ts.InterfaceDeclaration,
184
+ sourceFile: ts.SourceFile,
185
+ result: PropsExtractionResult
186
+ ): void {
187
+ for (const member of node.members) {
188
+ if (ts.isPropertySignature(member)) {
189
+ const prop = extractPropFromSignature(member, sourceFile);
190
+ if (prop) {
191
+ result.props.push(prop);
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Extract props from a type alias declaration
199
+ */
200
+ function extractPropsFromTypeAlias(
201
+ node: ts.TypeAliasDeclaration,
202
+ sourceFile: ts.SourceFile,
203
+ result: PropsExtractionResult
204
+ ): void {
205
+ if (ts.isTypeLiteralNode(node.type)) {
206
+ for (const member of node.type.members) {
207
+ if (ts.isPropertySignature(member)) {
208
+ const prop = extractPropFromSignature(member, sourceFile);
209
+ if (prop) {
210
+ result.props.push(prop);
211
+ }
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Extract a single prop from a property signature
219
+ */
220
+ function extractPropFromSignature(
221
+ member: ts.PropertySignature,
222
+ sourceFile: ts.SourceFile
223
+ ): ExtractedProp | null {
224
+ // Get prop name
225
+ const name = ts.isIdentifier(member.name)
226
+ ? member.name.text
227
+ : member.name.getText(sourceFile);
228
+
229
+ // Skip internal props
230
+ if (name.startsWith("_") || name.startsWith("$")) {
231
+ return null;
232
+ }
233
+
234
+ // Get type
235
+ const typeNode = member.type;
236
+ const typeString = typeNode ? typeNode.getText(sourceFile) : "unknown";
237
+
238
+ // Determine if optional
239
+ const isOptional = member.questionToken !== undefined;
240
+
241
+ // Get JSDoc information
242
+ const jsDoc = getJSDocInfo(member, sourceFile);
243
+
244
+ // Parse the type into our PropType structure
245
+ const typeInfo = typeNode
246
+ ? parseTypeNode(typeNode, sourceFile)
247
+ : { propType: { type: "custom" as const, typescript: "unknown" } };
248
+
249
+ const prop: ExtractedProp = {
250
+ name,
251
+ type: typeString,
252
+ propType: typeInfo.propType,
253
+ description: jsDoc.description,
254
+ required: !isOptional,
255
+ };
256
+
257
+ // Add enum values if available
258
+ if (typeInfo.enumValues && typeInfo.enumValues.length > 0) {
259
+ prop.enumValues = typeInfo.enumValues;
260
+ }
261
+
262
+ // Add default value if detected from JSDoc
263
+ if (jsDoc.defaultValue !== undefined) {
264
+ prop.defaultValue = jsDoc.defaultValue;
265
+ }
266
+
267
+ // Add deprecation notice
268
+ if (jsDoc.deprecated) {
269
+ prop.deprecated = jsDoc.deprecated;
270
+ }
271
+
272
+ // Add other tags
273
+ if (Object.keys(jsDoc.tags).length > 0) {
274
+ prop.tags = jsDoc.tags;
275
+ }
276
+
277
+ return prop;
278
+ }
279
+
280
+ /**
281
+ * Get JSDoc information from a node
282
+ */
283
+ function getJSDocInfo(
284
+ node: ts.Node,
285
+ sourceFile: ts.SourceFile
286
+ ): {
287
+ description: string;
288
+ defaultValue?: unknown;
289
+ deprecated?: string;
290
+ tags: Record<string, string>;
291
+ } {
292
+ const result = {
293
+ description: "",
294
+ defaultValue: undefined as unknown,
295
+ deprecated: undefined as string | undefined,
296
+ tags: {} as Record<string, string>,
297
+ };
298
+
299
+ // Get full text including comments
300
+ const fullText = sourceFile.getFullText();
301
+ const nodeStart = node.getFullStart();
302
+ const nodePos = node.getStart(sourceFile);
303
+ const leadingText = fullText.slice(nodeStart, nodePos);
304
+
305
+ // Find JSDoc comments (/** ... */)
306
+ const jsDocMatch = leadingText.match(/\/\*\*([\s\S]*?)\*\//);
307
+ if (jsDocMatch) {
308
+ const content = jsDocMatch[1];
309
+ parseJSDocContent(content, result);
310
+ }
311
+
312
+ // Also check line comment above (// ...)
313
+ const lineCommentMatch = leadingText.match(/\/\/\s*(.+)$/m);
314
+ if (lineCommentMatch && !result.description) {
315
+ result.description = lineCommentMatch[1].trim();
316
+ }
317
+
318
+ return result;
319
+ }
320
+
321
+ /**
322
+ * Parse JSDoc content into structured data
323
+ */
324
+ function parseJSDocContent(
325
+ content: string,
326
+ result: {
327
+ description: string;
328
+ defaultValue?: unknown;
329
+ deprecated?: string;
330
+ tags: Record<string, string>;
331
+ }
332
+ ): void {
333
+ const lines = content.split("\n");
334
+ let currentDescription: string[] = [];
335
+ let currentTag: string | null = null;
336
+ let currentTagContent: string[] = [];
337
+
338
+ for (const line of lines) {
339
+ // Remove leading * and whitespace
340
+ const trimmed = line.replace(/^\s*\*?\s?/, "").trim();
341
+
342
+ // Check for JSDoc tag
343
+ const tagMatch = trimmed.match(/^@(\w+)(?:\s+(.*))?$/);
344
+
345
+ if (tagMatch) {
346
+ // Save previous tag if exists
347
+ if (currentTag) {
348
+ processJSDocTag(result, currentTag, currentTagContent.join("\n").trim());
349
+ } else if (currentDescription.length > 0) {
350
+ result.description = currentDescription.join(" ").trim();
351
+ }
352
+
353
+ currentTag = tagMatch[1];
354
+ currentTagContent = tagMatch[2] ? [tagMatch[2]] : [];
355
+ } else if (currentTag) {
356
+ if (trimmed) {
357
+ currentTagContent.push(trimmed);
358
+ }
359
+ } else if (trimmed) {
360
+ currentDescription.push(trimmed);
361
+ }
362
+ }
363
+
364
+ // Process final tag
365
+ if (currentTag) {
366
+ processJSDocTag(result, currentTag, currentTagContent.join("\n").trim());
367
+ } else if (currentDescription.length > 0 && !result.description) {
368
+ result.description = currentDescription.join(" ").trim();
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Process a single JSDoc tag
374
+ */
375
+ function processJSDocTag(
376
+ result: {
377
+ description: string;
378
+ defaultValue?: unknown;
379
+ deprecated?: string;
380
+ tags: Record<string, string>;
381
+ },
382
+ tag: string,
383
+ content: string
384
+ ): void {
385
+ switch (tag) {
386
+ case "default":
387
+ case "defaultValue":
388
+ result.defaultValue = parseDefaultValue(content);
389
+ break;
390
+ case "deprecated":
391
+ result.deprecated = content || "This prop is deprecated";
392
+ break;
393
+ case "description":
394
+ result.description = content;
395
+ break;
396
+ default:
397
+ if (content) {
398
+ result.tags[tag] = content;
399
+ }
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Parse a default value from JSDoc string
405
+ */
406
+ function parseDefaultValue(value: string): unknown {
407
+ const trimmed = value.trim();
408
+ if (trimmed === "true") return true;
409
+ if (trimmed === "false") return false;
410
+ if (trimmed === "null") return null;
411
+ if (trimmed === "undefined") return undefined;
412
+ if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10);
413
+ if (/^-?\d+\.\d+$/.test(trimmed)) return parseFloat(trimmed);
414
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
415
+ return trimmed.slice(1, -1);
416
+ }
417
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
418
+ return trimmed.slice(1, -1);
419
+ }
420
+ return trimmed;
421
+ }
422
+
423
+ /**
424
+ * Parse a type node into our PropType structure
425
+ */
426
+ function parseTypeNode(
427
+ node: ts.TypeNode,
428
+ sourceFile: ts.SourceFile
429
+ ): { propType: PropType; enumValues?: string[] } {
430
+ // Boolean keyword
431
+ if (node.kind === ts.SyntaxKind.BooleanKeyword) {
432
+ return { propType: { type: "boolean" } };
433
+ }
434
+
435
+ // String keyword
436
+ if (node.kind === ts.SyntaxKind.StringKeyword) {
437
+ return { propType: { type: "string" } };
438
+ }
439
+
440
+ // Number keyword
441
+ if (node.kind === ts.SyntaxKind.NumberKeyword) {
442
+ return { propType: { type: "number" } };
443
+ }
444
+
445
+ // Union type (including string literal unions -> enum)
446
+ if (ts.isUnionTypeNode(node)) {
447
+ const stringLiterals: string[] = [];
448
+ const otherTypes: PropType[] = [];
449
+
450
+ for (const type of node.types) {
451
+ if (ts.isLiteralTypeNode(type) && ts.isStringLiteral(type.literal)) {
452
+ stringLiterals.push(type.literal.text);
453
+ } else if (
454
+ type.kind !== ts.SyntaxKind.NullKeyword &&
455
+ type.kind !== ts.SyntaxKind.UndefinedKeyword
456
+ ) {
457
+ const parsed = parseTypeNode(type, sourceFile);
458
+ otherTypes.push(parsed.propType);
459
+ }
460
+ }
461
+
462
+ // If all non-null types are string literals, it's an enum
463
+ if (stringLiterals.length > 0 && otherTypes.length === 0) {
464
+ return {
465
+ propType: { type: "enum", values: stringLiterals },
466
+ enumValues: stringLiterals,
467
+ };
468
+ }
469
+
470
+ // If we have string literals plus other types, still treat as enum for the values
471
+ if (stringLiterals.length > 0) {
472
+ return {
473
+ propType: { type: "enum", values: stringLiterals },
474
+ enumValues: stringLiterals,
475
+ };
476
+ }
477
+
478
+ // Generic union
479
+ if (otherTypes.length > 0) {
480
+ return {
481
+ propType: { type: "union", types: otherTypes },
482
+ };
483
+ }
484
+ }
485
+
486
+ // Array type
487
+ if (ts.isArrayTypeNode(node)) {
488
+ const elementType = parseTypeNode(node.elementType, sourceFile);
489
+ return {
490
+ propType: { type: "array", items: elementType.propType },
491
+ };
492
+ }
493
+
494
+ // Function type
495
+ if (ts.isFunctionTypeNode(node)) {
496
+ const params = node.parameters
497
+ .map((p) => {
498
+ const name = p.name.getText(sourceFile);
499
+ const type = p.type ? p.type.getText(sourceFile) : "any";
500
+ return `${name}: ${type}`;
501
+ })
502
+ .join(", ");
503
+ const returnType = node.type ? node.type.getText(sourceFile) : "void";
504
+ return {
505
+ propType: { type: "function", signature: `(${params}) => ${returnType}` },
506
+ };
507
+ }
508
+
509
+ // Type reference (like ReactNode, ReactElement, etc.)
510
+ if (ts.isTypeReferenceNode(node)) {
511
+ const typeName = node.typeName.getText(sourceFile);
512
+
513
+ // Check for React node types
514
+ if (
515
+ typeName === "ReactNode" ||
516
+ typeName === "React.ReactNode" ||
517
+ typeName.endsWith(".ReactNode")
518
+ ) {
519
+ return { propType: { type: "node" } };
520
+ }
521
+
522
+ if (
523
+ typeName === "ReactElement" ||
524
+ typeName === "React.ReactElement" ||
525
+ typeName === "JSX.Element"
526
+ ) {
527
+ return { propType: { type: "element" } };
528
+ }
529
+
530
+ // Generic type reference
531
+ return {
532
+ propType: { type: "custom", typescript: node.getText(sourceFile) },
533
+ };
534
+ }
535
+
536
+ // Type literal (inline object type)
537
+ if (ts.isTypeLiteralNode(node)) {
538
+ return { propType: { type: "object" } };
539
+ }
540
+
541
+ // Default: custom type
542
+ return {
543
+ propType: { type: "custom", typescript: node.getText(sourceFile) },
544
+ };
545
+ }
546
+
547
+ /**
548
+ * Infer component name from file path
549
+ */
550
+ function inferComponentName(filePath: string): string {
551
+ const fileName = basename(filePath);
552
+ // Remove extension
553
+ let name = fileName.replace(/\.(tsx?|jsx?)$/, "");
554
+ // Handle index files
555
+ if (name === "index") {
556
+ name = basename(dirname(filePath));
557
+ }
558
+ return name;
559
+ }
560
+
561
+ /**
562
+ * Convert extracted props to PropDefinition format for segments.json
563
+ */
564
+ export function convertToSegmentProps(
565
+ props: ExtractedProp[]
566
+ ): Record<string, PropDefinition> {
567
+ const result: Record<string, PropDefinition> = {};
568
+
569
+ for (const prop of props) {
570
+ const definition: PropDefinition = {
571
+ type: prop.propType.type,
572
+ description: prop.description,
573
+ required: prop.required,
574
+ };
575
+
576
+ // Add values for enum types
577
+ if (prop.enumValues && prop.enumValues.length > 0) {
578
+ definition.values = prop.enumValues;
579
+ }
580
+
581
+ // Add default value
582
+ if (prop.defaultValue !== undefined) {
583
+ definition.default = prop.defaultValue;
584
+ }
585
+
586
+ // Add type details for complex types
587
+ if (prop.propType.type === "object" && "shape" in prop.propType) {
588
+ definition.typeDetails = { shape: prop.propType.shape };
589
+ }
590
+ if (prop.propType.type === "array" && "items" in prop.propType) {
591
+ definition.typeDetails = { items: prop.propType.items };
592
+ }
593
+ if (prop.propType.type === "function" && "signature" in prop.propType) {
594
+ definition.typeDetails = { signature: prop.propType.signature };
595
+ }
596
+ if (prop.propType.type === "union" && "types" in prop.propType) {
597
+ definition.typeDetails = { types: prop.propType.types };
598
+ }
599
+ if (prop.propType.type === "custom" && "typescript" in prop.propType) {
600
+ definition.typeDetails = { typescript: prop.propType.typescript };
601
+ }
602
+
603
+ result[prop.name] = definition;
604
+ }
605
+
606
+ return result;
607
+ }
608
+
609
+ /**
610
+ * Find component source file and extract props
611
+ */
612
+ export async function extractPropsForComponent(
613
+ componentName: string,
614
+ searchDirs: string[]
615
+ ): Promise<PropsExtractionResult | null> {
616
+ const patterns = [
617
+ `${componentName}.tsx`,
618
+ `${componentName}.ts`,
619
+ `${componentName}/index.tsx`,
620
+ `${componentName}/index.ts`,
621
+ `${componentName}/${componentName}.tsx`,
622
+ `${componentName}/${componentName}.ts`,
623
+ ];
624
+
625
+ for (const dir of searchDirs) {
626
+ for (const pattern of patterns) {
627
+ const fullPath = join(dir, pattern);
628
+ if (existsSync(fullPath)) {
629
+ return extractPropsFromFile(fullPath, {
630
+ propsTypeName: `${componentName}Props`,
631
+ });
632
+ }
633
+ }
634
+ }
635
+
636
+ return null;
637
+ }
638
+
639
+ /**
640
+ * Extract props from multiple component files
641
+ */
642
+ export async function extractAllComponentProps(
643
+ componentFiles: Map<string, string>
644
+ ): Promise<Map<string, PropsExtractionResult>> {
645
+ const results = new Map<string, PropsExtractionResult>();
646
+
647
+ for (const [componentName, filePath] of componentFiles) {
648
+ try {
649
+ const extraction = await extractPropsFromFile(filePath, {
650
+ propsTypeName: `${componentName}Props`,
651
+ });
652
+ results.set(componentName, extraction);
653
+ } catch (error) {
654
+ results.set(componentName, {
655
+ filePath,
656
+ componentName,
657
+ props: [],
658
+ success: false,
659
+ warnings: [`Extraction failed: ${(error as Error).message}`],
660
+ });
661
+ }
662
+ }
663
+
664
+ return results;
665
+ }