@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,374 @@
1
+ /**
2
+ * TypeScript Props Extractor
3
+ * Extracts prop types, default values, and JSDoc comments from React component files
4
+ */
5
+
6
+ import ts from "typescript";
7
+ import { readFileSync } from "node:fs";
8
+ import type { RegistryPropEntry } from "../fragment-types.js";
9
+
10
+ /**
11
+ * Result of extracting props from a component file
12
+ */
13
+ export interface ExtractedProps {
14
+ /** Component name */
15
+ componentName: string;
16
+ /** Props interface name (e.g., "ButtonProps") */
17
+ propsInterfaceName?: string;
18
+ /** Extracted props */
19
+ props: Record<string, RegistryPropEntry>;
20
+ /** Named exports from the file */
21
+ exports: string[];
22
+ /** Import statements (for dependency tracking) */
23
+ imports: string[];
24
+ }
25
+
26
+ /**
27
+ * Extract props from a TypeScript/TSX component file
28
+ */
29
+ export function extractPropsFromFile(filePath: string): ExtractedProps | null {
30
+ const sourceText = readFileSync(filePath, "utf-8");
31
+ return extractPropsFromSource(sourceText, filePath);
32
+ }
33
+
34
+ /**
35
+ * Extract props from TypeScript source code
36
+ */
37
+ export function extractPropsFromSource(
38
+ sourceText: string,
39
+ fileName = "component.tsx"
40
+ ): ExtractedProps | null {
41
+ const sourceFile = ts.createSourceFile(
42
+ fileName,
43
+ sourceText,
44
+ ts.ScriptTarget.Latest,
45
+ true,
46
+ fileName.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS
47
+ );
48
+
49
+ const result: ExtractedProps = {
50
+ componentName: "",
51
+ props: {},
52
+ exports: [],
53
+ imports: [],
54
+ };
55
+
56
+ // Find all exports and props interfaces
57
+ const propsInterfaces = new Map<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>();
58
+ const componentExports: string[] = [];
59
+ const importedModules: string[] = [];
60
+
61
+ ts.forEachChild(sourceFile, (node) => {
62
+ // Track imports
63
+ if (ts.isImportDeclaration(node)) {
64
+ const moduleSpecifier = node.moduleSpecifier;
65
+ if (ts.isStringLiteral(moduleSpecifier)) {
66
+ importedModules.push(moduleSpecifier.text);
67
+ }
68
+ }
69
+
70
+ // Find interface declarations (e.g., interface ButtonProps { ... })
71
+ if (ts.isInterfaceDeclaration(node)) {
72
+ const name = node.name.text;
73
+ if (name.endsWith("Props")) {
74
+ propsInterfaces.set(name, node);
75
+ }
76
+ }
77
+
78
+ // Find type alias declarations (e.g., type ButtonProps = { ... })
79
+ if (ts.isTypeAliasDeclaration(node)) {
80
+ const name = node.name.text;
81
+ if (name.endsWith("Props")) {
82
+ propsInterfaces.set(name, node);
83
+ }
84
+ }
85
+
86
+ // Find exported functions/components
87
+ if (ts.isFunctionDeclaration(node) && node.name) {
88
+ const hasExport = node.modifiers?.some(
89
+ (m) => m.kind === ts.SyntaxKind.ExportKeyword
90
+ );
91
+ if (hasExport) {
92
+ componentExports.push(node.name.text);
93
+ }
94
+ }
95
+
96
+ // Find exported variable declarations (e.g., export const Button = ...)
97
+ if (ts.isVariableStatement(node)) {
98
+ const hasExport = node.modifiers?.some(
99
+ (m) => m.kind === ts.SyntaxKind.ExportKeyword
100
+ );
101
+ if (hasExport) {
102
+ for (const decl of node.declarationList.declarations) {
103
+ if (ts.isIdentifier(decl.name)) {
104
+ componentExports.push(decl.name.text);
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ // Find export declarations (e.g., export { Button })
111
+ if (ts.isExportDeclaration(node) && node.exportClause) {
112
+ if (ts.isNamedExports(node.exportClause)) {
113
+ for (const element of node.exportClause.elements) {
114
+ componentExports.push(element.name.text);
115
+ }
116
+ }
117
+ }
118
+ });
119
+
120
+ result.exports = componentExports;
121
+ result.imports = importedModules;
122
+
123
+ // Find the main component (first PascalCase export)
124
+ const mainComponent = componentExports.find(
125
+ (name) => /^[A-Z]/.test(name) && !name.endsWith("Props")
126
+ );
127
+
128
+ if (!mainComponent) {
129
+ return null;
130
+ }
131
+
132
+ result.componentName = mainComponent;
133
+
134
+ // Find matching props interface
135
+ const propsInterfaceName = `${mainComponent}Props`;
136
+ const propsInterface = propsInterfaces.get(propsInterfaceName);
137
+
138
+ if (propsInterface) {
139
+ result.propsInterfaceName = propsInterfaceName;
140
+ result.props = extractPropsFromInterface(propsInterface, sourceFile);
141
+ }
142
+
143
+ return result;
144
+ }
145
+
146
+ /**
147
+ * Extract props from an interface or type alias declaration
148
+ */
149
+ function extractPropsFromInterface(
150
+ node: ts.InterfaceDeclaration | ts.TypeAliasDeclaration,
151
+ sourceFile: ts.SourceFile
152
+ ): Record<string, RegistryPropEntry> {
153
+ const props: Record<string, RegistryPropEntry> = {};
154
+
155
+ // Handle interface declaration
156
+ if (ts.isInterfaceDeclaration(node)) {
157
+ for (const member of node.members) {
158
+ if (ts.isPropertySignature(member) && member.name) {
159
+ const propName = member.name.getText(sourceFile);
160
+ const prop = extractPropFromMember(member, sourceFile);
161
+ if (prop) {
162
+ props[propName] = prop;
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ // Handle type alias declaration
169
+ if (ts.isTypeAliasDeclaration(node)) {
170
+ const typeNode = node.type;
171
+ if (ts.isTypeLiteralNode(typeNode)) {
172
+ for (const member of typeNode.members) {
173
+ if (ts.isPropertySignature(member) && member.name) {
174
+ const propName = member.name.getText(sourceFile);
175
+ const prop = extractPropFromMember(member, sourceFile);
176
+ if (prop) {
177
+ props[propName] = prop;
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ return props;
185
+ }
186
+
187
+ /**
188
+ * Extract prop info from a property signature
189
+ */
190
+ function extractPropFromMember(
191
+ member: ts.PropertySignature,
192
+ sourceFile: ts.SourceFile
193
+ ): RegistryPropEntry | null {
194
+ const entry: RegistryPropEntry = {};
195
+
196
+ // Check if required
197
+ entry.required = !member.questionToken;
198
+
199
+ // Get type
200
+ if (member.type) {
201
+ const typeInfo = parseTypeNode(member.type, sourceFile);
202
+ entry.type = typeInfo.type;
203
+ entry.typeKind = typeInfo.typeKind;
204
+ if (typeInfo.options) {
205
+ entry.options = typeInfo.options;
206
+ }
207
+ }
208
+
209
+ // Get JSDoc comment
210
+ const jsDocComment = getJSDocComment(member);
211
+ if (jsDocComment) {
212
+ entry.description = jsDocComment;
213
+ }
214
+
215
+ // Get default value from JSDoc @default tag
216
+ const defaultValue = getJSDocDefault(member);
217
+ if (defaultValue !== undefined) {
218
+ entry.default = defaultValue;
219
+ }
220
+
221
+ return entry;
222
+ }
223
+
224
+ /**
225
+ * Parse a TypeScript type node into a simplified representation
226
+ */
227
+ function parseTypeNode(
228
+ typeNode: ts.TypeNode,
229
+ sourceFile: ts.SourceFile
230
+ ): { type: string; typeKind: RegistryPropEntry["typeKind"]; options?: string[] } {
231
+ // String
232
+ if (typeNode.kind === ts.SyntaxKind.StringKeyword) {
233
+ return { type: "string", typeKind: "string" };
234
+ }
235
+
236
+ // Number
237
+ if (typeNode.kind === ts.SyntaxKind.NumberKeyword) {
238
+ return { type: "number", typeKind: "number" };
239
+ }
240
+
241
+ // Boolean
242
+ if (typeNode.kind === ts.SyntaxKind.BooleanKeyword) {
243
+ return { type: "boolean", typeKind: "boolean" };
244
+ }
245
+
246
+ // Union type (string literal union for enums)
247
+ if (ts.isUnionTypeNode(typeNode)) {
248
+ const options: string[] = [];
249
+ let allLiterals = true;
250
+
251
+ for (const subType of typeNode.types) {
252
+ if (ts.isLiteralTypeNode(subType)) {
253
+ if (ts.isStringLiteral(subType.literal)) {
254
+ options.push(subType.literal.text);
255
+ } else if (subType.literal.kind === ts.SyntaxKind.TrueKeyword) {
256
+ options.push("true");
257
+ } else if (subType.literal.kind === ts.SyntaxKind.FalseKeyword) {
258
+ options.push("false");
259
+ } else {
260
+ allLiterals = false;
261
+ }
262
+ } else {
263
+ allLiterals = false;
264
+ }
265
+ }
266
+
267
+ if (allLiterals && options.length > 0) {
268
+ return {
269
+ type: options.map((o) => `"${o}"`).join(" | "),
270
+ typeKind: "enum",
271
+ options,
272
+ };
273
+ }
274
+
275
+ return {
276
+ type: typeNode.getText(sourceFile),
277
+ typeKind: "union",
278
+ };
279
+ }
280
+
281
+ // Function type
282
+ if (ts.isFunctionTypeNode(typeNode)) {
283
+ return {
284
+ type: typeNode.getText(sourceFile),
285
+ typeKind: "function",
286
+ };
287
+ }
288
+
289
+ // Array type
290
+ if (ts.isArrayTypeNode(typeNode)) {
291
+ return {
292
+ type: typeNode.getText(sourceFile),
293
+ typeKind: "array",
294
+ };
295
+ }
296
+
297
+ // Type reference (e.g., ReactNode, React.ReactElement)
298
+ if (ts.isTypeReferenceNode(typeNode)) {
299
+ const typeName = typeNode.typeName.getText(sourceFile);
300
+
301
+ // React types
302
+ if (typeName === "ReactNode" || typeName === "React.ReactNode") {
303
+ return { type: "ReactNode", typeKind: "node" };
304
+ }
305
+ if (typeName === "ReactElement" || typeName === "React.ReactElement") {
306
+ return { type: "ReactElement", typeKind: "element" };
307
+ }
308
+ if (typeName === "JSX.Element") {
309
+ return { type: "JSX.Element", typeKind: "element" };
310
+ }
311
+
312
+ return {
313
+ type: typeNode.getText(sourceFile),
314
+ typeKind: "object",
315
+ };
316
+ }
317
+
318
+ // Object type literal
319
+ if (ts.isTypeLiteralNode(typeNode)) {
320
+ return {
321
+ type: typeNode.getText(sourceFile),
322
+ typeKind: "object",
323
+ };
324
+ }
325
+
326
+ // Default: unknown
327
+ return {
328
+ type: typeNode.getText(sourceFile),
329
+ typeKind: "unknown",
330
+ };
331
+ }
332
+
333
+ /**
334
+ * Get JSDoc comment from a node
335
+ */
336
+ function getJSDocComment(node: ts.Node): string | undefined {
337
+ const jsDocTags = ts.getJSDocTags(node);
338
+ const jsDoc = (node as unknown as { jsDoc?: ts.JSDoc[] }).jsDoc;
339
+
340
+ if (jsDoc && jsDoc.length > 0) {
341
+ const comment = jsDoc[0].comment;
342
+ if (typeof comment === "string") {
343
+ return comment;
344
+ }
345
+ if (Array.isArray(comment)) {
346
+ return comment.map((c) => (typeof c === "string" ? c : c.text)).join("");
347
+ }
348
+ }
349
+
350
+ return undefined;
351
+ }
352
+
353
+ /**
354
+ * Get @default value from JSDoc
355
+ */
356
+ function getJSDocDefault(node: ts.Node): unknown | undefined {
357
+ const jsDocTags = ts.getJSDocTags(node);
358
+
359
+ for (const tag of jsDocTags) {
360
+ if (tag.tagName.text === "default") {
361
+ const comment = tag.comment;
362
+ if (typeof comment === "string") {
363
+ // Try to parse as JSON
364
+ try {
365
+ return JSON.parse(comment);
366
+ } catch {
367
+ return comment;
368
+ }
369
+ }
370
+ }
371
+ }
372
+
373
+ return undefined;
374
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * AST-based import analysis for detecting component relationships
3
+ *
4
+ * Analyzes TypeScript/JavaScript files to find which components import others,
5
+ * providing accurate "used by" relationships for the component graph.
6
+ */
7
+
8
+ import ts from 'typescript';
9
+ import { readFileSync } from 'node:fs';
10
+ import { resolve, dirname, basename } from 'node:path';
11
+
12
+ export interface ComponentImport {
13
+ /** Name of the imported component */
14
+ name: string;
15
+ /** Import path (relative or package) */
16
+ path: string;
17
+ /** Whether this is a default import */
18
+ isDefault: boolean;
19
+ /** Whether this is a namespace import */
20
+ isNamespace: boolean;
21
+ }
22
+
23
+ export interface ImportAnalysisResult {
24
+ /** Source file path */
25
+ filePath: string;
26
+ /** Component name extracted from file */
27
+ componentName: string;
28
+ /** List of component imports found */
29
+ imports: ComponentImport[];
30
+ }
31
+
32
+ /**
33
+ * Extract component name from a file path
34
+ */
35
+ function extractComponentNameFromPath(filePath: string): string {
36
+ const fileName = basename(filePath);
37
+
38
+ // Handle index.tsx files - use parent directory name
39
+ if (fileName === 'index.tsx' || fileName === 'index.ts' || fileName === 'index.jsx' || fileName === 'index.js') {
40
+ const parentDir = basename(dirname(filePath));
41
+ return parentDir;
42
+ }
43
+
44
+ // Remove extension and common suffixes
45
+ return fileName
46
+ .replace(/\.(tsx?|jsx?)$/, '')
47
+ .replace(/\.stories$/, '')
48
+ .replace(/\.segment$/, '')
49
+ .replace(/\.fragment$/, '');
50
+ }
51
+
52
+ /**
53
+ * Check if an import path looks like a component import (not a utility/library)
54
+ */
55
+ function isComponentImportPath(importPath: string): boolean {
56
+ // Skip node_modules packages
57
+ if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
58
+ return false;
59
+ }
60
+
61
+ // Skip common utility/hook patterns
62
+ const skipPatterns = [
63
+ /\/hooks\//,
64
+ /\/utils\//,
65
+ /\/helpers\//,
66
+ /\/lib\//,
67
+ /\/types\//,
68
+ /\/constants\//,
69
+ /\/styles\//,
70
+ /\/context\//,
71
+ /\.css$/,
72
+ /\.scss$/,
73
+ /\.less$/,
74
+ /\.json$/,
75
+ ];
76
+
77
+ for (const pattern of skipPatterns) {
78
+ if (pattern.test(importPath)) {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ return true;
84
+ }
85
+
86
+ /**
87
+ * Check if an imported name looks like a React component (PascalCase)
88
+ */
89
+ function isPascalCase(name: string): boolean {
90
+ return /^[A-Z][a-zA-Z0-9]*$/.test(name);
91
+ }
92
+
93
+ /**
94
+ * Analyze a single file for component imports
95
+ */
96
+ export function analyzeFileImports(filePath: string): ImportAnalysisResult {
97
+ const componentName = extractComponentNameFromPath(filePath);
98
+ const imports: ComponentImport[] = [];
99
+
100
+ let sourceText: string;
101
+ try {
102
+ sourceText = readFileSync(filePath, 'utf-8');
103
+ } catch {
104
+ return { filePath, componentName, imports };
105
+ }
106
+
107
+ const sourceFile = ts.createSourceFile(
108
+ filePath,
109
+ sourceText,
110
+ ts.ScriptTarget.Latest,
111
+ true,
112
+ filePath.endsWith('.tsx') || filePath.endsWith('.jsx')
113
+ ? ts.ScriptKind.TSX
114
+ : ts.ScriptKind.TS
115
+ );
116
+
117
+ ts.forEachChild(sourceFile, (node) => {
118
+ if (ts.isImportDeclaration(node)) {
119
+ const moduleSpecifier = node.moduleSpecifier;
120
+ if (!ts.isStringLiteral(moduleSpecifier)) return;
121
+
122
+ const importPath = moduleSpecifier.text;
123
+
124
+ // Skip non-component imports
125
+ if (!isComponentImportPath(importPath)) return;
126
+
127
+ const importClause = node.importClause;
128
+ if (!importClause) return;
129
+
130
+ // Default import: import Button from './Button'
131
+ if (importClause.name) {
132
+ const name = importClause.name.text;
133
+ if (isPascalCase(name)) {
134
+ imports.push({
135
+ name,
136
+ path: importPath,
137
+ isDefault: true,
138
+ isNamespace: false,
139
+ });
140
+ }
141
+ }
142
+
143
+ // Named imports: import { Button, Icon } from './components'
144
+ const namedBindings = importClause.namedBindings;
145
+ if (namedBindings) {
146
+ if (ts.isNamedImports(namedBindings)) {
147
+ for (const element of namedBindings.elements) {
148
+ const name = element.name.text;
149
+ if (isPascalCase(name)) {
150
+ imports.push({
151
+ name,
152
+ path: importPath,
153
+ isDefault: false,
154
+ isNamespace: false,
155
+ });
156
+ }
157
+ }
158
+ } else if (ts.isNamespaceImport(namedBindings)) {
159
+ // Namespace import: import * as Components from './components'
160
+ // We can't reliably know what components are used without more analysis
161
+ const name = namedBindings.name.text;
162
+ imports.push({
163
+ name,
164
+ path: importPath,
165
+ isDefault: false,
166
+ isNamespace: true,
167
+ });
168
+ }
169
+ }
170
+ }
171
+ });
172
+
173
+ return { filePath, componentName, imports };
174
+ }
175
+
176
+ /**
177
+ * Build a map of which components are imported by which files
178
+ */
179
+ export function buildImportGraph(
180
+ filePaths: string[]
181
+ ): Map<string, string[]> {
182
+ // Map from component name -> list of component names that import it
183
+ const importedBy = new Map<string, string[]>();
184
+
185
+ for (const filePath of filePaths) {
186
+ const result = analyzeFileImports(filePath);
187
+
188
+ for (const imp of result.imports) {
189
+ // Skip namespace imports for now
190
+ if (imp.isNamespace) continue;
191
+
192
+ const importedComponent = imp.name;
193
+ const importingComponent = result.componentName;
194
+
195
+ // Don't add self-references
196
+ if (importedComponent === importingComponent) continue;
197
+
198
+ const existing = importedBy.get(importedComponent) || [];
199
+ if (!existing.includes(importingComponent)) {
200
+ existing.push(importingComponent);
201
+ importedBy.set(importedComponent, existing);
202
+ }
203
+ }
204
+ }
205
+
206
+ return importedBy;
207
+ }
208
+
209
+ /**
210
+ * Get components that import a specific component
211
+ */
212
+ export function getImportedBy(
213
+ componentName: string,
214
+ importGraph: Map<string, string[]>
215
+ ): string[] {
216
+ return importGraph.get(componentName) || [];
217
+ }