@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,445 @@
1
+ /**
2
+ * AST Scanner for Component Usage Analysis
3
+ *
4
+ * Scans TypeScript/TSX files to find:
5
+ * - Component imports (named, default, aliased)
6
+ * - Component usages in JSX with props
7
+ */
8
+
9
+ import { parse, type ParserOptions } from "@babel/parser";
10
+ import _traverse from "@babel/traverse";
11
+ import * as t from "@babel/types";
12
+
13
+ // Handle CommonJS/ESM interop
14
+ const traverse = (_traverse as unknown as { default: typeof _traverse }).default || _traverse;
15
+ import { readFile } from "node:fs/promises";
16
+ import type {
17
+ ComponentImport,
18
+ ComponentUsage,
19
+ UsageProps,
20
+ } from "./types.js";
21
+
22
+ /**
23
+ * Babel parser options for React/TypeScript files
24
+ */
25
+ const PARSER_OPTIONS: ParserOptions = {
26
+ sourceType: "module",
27
+ plugins: [
28
+ "jsx",
29
+ "typescript",
30
+ ["decorators", { decoratorsBeforeExport: true }],
31
+ "classProperties",
32
+ "classPrivateProperties",
33
+ "classPrivateMethods",
34
+ "exportDefaultFrom",
35
+ "exportNamespaceFrom",
36
+ "dynamicImport",
37
+ "nullishCoalescingOperator",
38
+ "optionalChaining",
39
+ "objectRestSpread",
40
+ ],
41
+ };
42
+
43
+ /**
44
+ * Scan a file for component imports
45
+ *
46
+ * Handles:
47
+ * - Named imports: import { Button } from './Button'
48
+ * - Default imports: import Button from './Button'
49
+ * - Aliased imports: import { Button as Btn } from './Button'
50
+ * - Namespace imports: import * as Components from './index'
51
+ */
52
+ export async function scanFileForImports(
53
+ filePath: string
54
+ ): Promise<ComponentImport[]> {
55
+ const imports: ComponentImport[] = [];
56
+
57
+ try {
58
+ const sourceCode = await readFile(filePath, "utf-8");
59
+ const ast = parse(sourceCode, PARSER_OPTIONS);
60
+
61
+ traverse(ast, {
62
+ ImportDeclaration(path) {
63
+ const source = path.node.source.value;
64
+ const line = path.node.loc?.start.line ?? 0;
65
+
66
+ for (const specifier of path.node.specifiers) {
67
+ if (t.isImportSpecifier(specifier)) {
68
+ // Named import: import { Button } from './Button'
69
+ // or aliased: import { Button as Btn } from './Button'
70
+ const importedName = t.isIdentifier(specifier.imported)
71
+ ? specifier.imported.name
72
+ : specifier.imported.value;
73
+ const localName = specifier.local.name;
74
+
75
+ // Only track PascalCase names (likely components)
76
+ if (isPascalCase(importedName)) {
77
+ imports.push({
78
+ componentName: importedName,
79
+ localName,
80
+ source,
81
+ isDefault: false,
82
+ filePath,
83
+ line,
84
+ });
85
+ }
86
+ } else if (t.isImportDefaultSpecifier(specifier)) {
87
+ // Default import: import Button from './Button'
88
+ const localName = specifier.local.name;
89
+
90
+ // Only track PascalCase names (likely components)
91
+ if (isPascalCase(localName)) {
92
+ imports.push({
93
+ componentName: localName,
94
+ localName,
95
+ source,
96
+ isDefault: true,
97
+ filePath,
98
+ line,
99
+ });
100
+ }
101
+ }
102
+ // Skip namespace imports for now (import * as X)
103
+ }
104
+ },
105
+ });
106
+ } catch (error) {
107
+ // Return empty array for files with parse errors
108
+ // Caller can check if empty means "no imports" or "parse error"
109
+ console.warn(`Failed to parse ${filePath}:`, (error as Error).message);
110
+ return [];
111
+ }
112
+
113
+ return imports;
114
+ }
115
+
116
+ /**
117
+ * Scan a file for component usages in JSX
118
+ *
119
+ * @param filePath - File to scan
120
+ * @param componentNames - Map of localName -> componentName for components to track
121
+ */
122
+ export async function scanFileForUsages(
123
+ filePath: string,
124
+ componentNames: Map<string, string>
125
+ ): Promise<ComponentUsage[]> {
126
+ const usages: ComponentUsage[] = [];
127
+
128
+ try {
129
+ const sourceCode = await readFile(filePath, "utf-8");
130
+ const lines = sourceCode.split("\n");
131
+ const ast = parse(sourceCode, PARSER_OPTIONS);
132
+
133
+ traverse(ast, {
134
+ JSXOpeningElement(path) {
135
+ const elementName = getJSXElementName(path.node.name);
136
+ if (!elementName) return;
137
+
138
+ // Check if this is a tracked component (by local name)
139
+ const componentName = componentNames.get(elementName);
140
+ if (!componentName) return;
141
+
142
+ const loc = path.node.loc;
143
+ if (!loc) return;
144
+
145
+ // Extract props
146
+ const props = extractProps(path.node.attributes);
147
+
148
+ // Get surrounding context (3 lines before and after)
149
+ const contextLines = getContextLines(lines, loc.start.line - 1, 3);
150
+
151
+ // Check if conditionally rendered
152
+ const isConditional = checkIfConditional(path);
153
+
154
+ // Get parent element name
155
+ const parentElement = getParentElementName(path);
156
+
157
+ usages.push({
158
+ componentName,
159
+ filePath,
160
+ line: loc.start.line,
161
+ column: loc.start.column,
162
+ props,
163
+ context: contextLines,
164
+ parentElement,
165
+ hasSpreadProps: props.spreads.length > 0,
166
+ isConditional,
167
+ });
168
+ },
169
+ });
170
+ } catch (error) {
171
+ console.warn(`Failed to parse ${filePath}:`, (error as Error).message);
172
+ return [];
173
+ }
174
+
175
+ return usages;
176
+ }
177
+
178
+ /**
179
+ * Scan a file for both imports and usages in a single pass
180
+ * More efficient for scanning many files
181
+ */
182
+ export async function scanFile(
183
+ filePath: string,
184
+ trackedComponents?: Set<string>
185
+ ): Promise<{ imports: ComponentImport[]; usages: ComponentUsage[] }> {
186
+ const imports: ComponentImport[] = [];
187
+ const usages: ComponentUsage[] = [];
188
+
189
+ try {
190
+ const sourceCode = await readFile(filePath, "utf-8");
191
+ const lines = sourceCode.split("\n");
192
+ const ast = parse(sourceCode, PARSER_OPTIONS);
193
+
194
+ // Track local names to component names mapping within this file
195
+ const localToComponent = new Map<string, string>();
196
+
197
+ traverse(ast, {
198
+ ImportDeclaration(path) {
199
+ const source = path.node.source.value;
200
+ const line = path.node.loc?.start.line ?? 0;
201
+
202
+ for (const specifier of path.node.specifiers) {
203
+ if (t.isImportSpecifier(specifier)) {
204
+ const importedName = t.isIdentifier(specifier.imported)
205
+ ? specifier.imported.name
206
+ : specifier.imported.value;
207
+ const localName = specifier.local.name;
208
+
209
+ if (isPascalCase(importedName)) {
210
+ // If we're tracking specific components, only add those
211
+ if (!trackedComponents || trackedComponents.has(importedName)) {
212
+ imports.push({
213
+ componentName: importedName,
214
+ localName,
215
+ source,
216
+ isDefault: false,
217
+ filePath,
218
+ line,
219
+ });
220
+ localToComponent.set(localName, importedName);
221
+ }
222
+ }
223
+ } else if (t.isImportDefaultSpecifier(specifier)) {
224
+ const localName = specifier.local.name;
225
+
226
+ if (isPascalCase(localName)) {
227
+ if (!trackedComponents || trackedComponents.has(localName)) {
228
+ imports.push({
229
+ componentName: localName,
230
+ localName,
231
+ source,
232
+ isDefault: true,
233
+ filePath,
234
+ line,
235
+ });
236
+ localToComponent.set(localName, localName);
237
+ }
238
+ }
239
+ }
240
+ }
241
+ },
242
+
243
+ JSXOpeningElement(path) {
244
+ const elementName = getJSXElementName(path.node.name);
245
+ if (!elementName) return;
246
+
247
+ // Check if this is a tracked component (by local name)
248
+ const componentName = localToComponent.get(elementName);
249
+ if (!componentName) return;
250
+
251
+ const loc = path.node.loc;
252
+ if (!loc) return;
253
+
254
+ const props = extractProps(path.node.attributes);
255
+ const contextLines = getContextLines(lines, loc.start.line - 1, 3);
256
+ const isConditional = checkIfConditional(path);
257
+ const parentElement = getParentElementName(path);
258
+
259
+ usages.push({
260
+ componentName,
261
+ filePath,
262
+ line: loc.start.line,
263
+ column: loc.start.column,
264
+ props,
265
+ context: contextLines,
266
+ parentElement,
267
+ hasSpreadProps: props.spreads.length > 0,
268
+ isConditional,
269
+ });
270
+ },
271
+ });
272
+ } catch (error) {
273
+ console.warn(`Failed to parse ${filePath}:`, (error as Error).message);
274
+ return { imports: [], usages: [] };
275
+ }
276
+
277
+ return { imports, usages };
278
+ }
279
+
280
+ /**
281
+ * Check if a string is PascalCase (likely a component name)
282
+ */
283
+ function isPascalCase(str: string): boolean {
284
+ return /^[A-Z][a-zA-Z0-9]*$/.test(str);
285
+ }
286
+
287
+ /**
288
+ * Get the element name from a JSX element name node
289
+ */
290
+ function getJSXElementName(
291
+ name: t.JSXIdentifier | t.JSXMemberExpression | t.JSXNamespacedName
292
+ ): string | null {
293
+ if (t.isJSXIdentifier(name)) {
294
+ return name.name;
295
+ }
296
+ if (t.isJSXMemberExpression(name)) {
297
+ // Handle Component.SubComponent - return the full path
298
+ const parts: string[] = [];
299
+ let current: t.JSXMemberExpression | t.JSXIdentifier = name;
300
+ while (t.isJSXMemberExpression(current)) {
301
+ parts.unshift(current.property.name);
302
+ current = current.object as t.JSXMemberExpression | t.JSXIdentifier;
303
+ }
304
+ if (t.isJSXIdentifier(current)) {
305
+ parts.unshift(current.name);
306
+ }
307
+ return parts.join(".");
308
+ }
309
+ return null;
310
+ }
311
+
312
+ /**
313
+ * Extract props from JSX attributes
314
+ */
315
+ function extractProps(
316
+ attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[]
317
+ ): UsageProps {
318
+ const result: UsageProps = {
319
+ static: {},
320
+ dynamic: [],
321
+ spreads: [],
322
+ };
323
+
324
+ for (const attr of attributes) {
325
+ if (t.isJSXSpreadAttribute(attr)) {
326
+ // {...props} or {...otherProps}
327
+ if (t.isIdentifier(attr.argument)) {
328
+ result.spreads.push(attr.argument.name);
329
+ } else {
330
+ result.spreads.push("(expression)");
331
+ }
332
+ } else if (t.isJSXAttribute(attr)) {
333
+ const propName = t.isJSXIdentifier(attr.name)
334
+ ? attr.name.name
335
+ : `${attr.name.namespace.name}:${attr.name.name.name}`;
336
+
337
+ const value = attr.value;
338
+
339
+ if (value === null) {
340
+ // Boolean shorthand: <Button disabled />
341
+ result.static[propName] = true;
342
+ } else if (t.isStringLiteral(value)) {
343
+ // String literal: <Button variant="primary" />
344
+ result.static[propName] = value.value;
345
+ } else if (t.isJSXExpressionContainer(value)) {
346
+ const expr = value.expression;
347
+
348
+ if (t.isStringLiteral(expr)) {
349
+ result.static[propName] = expr.value;
350
+ } else if (t.isNumericLiteral(expr)) {
351
+ result.static[propName] = expr.value;
352
+ } else if (t.isBooleanLiteral(expr)) {
353
+ result.static[propName] = expr.value;
354
+ } else if (t.isTemplateLiteral(expr) && expr.expressions.length === 0) {
355
+ // Static template literal: variant={`primary`}
356
+ result.static[propName] = expr.quasis.map((q) => q.value.raw).join("");
357
+ } else {
358
+ // Dynamic expression
359
+ result.dynamic.push(propName);
360
+ }
361
+ }
362
+ }
363
+ }
364
+
365
+ return result;
366
+ }
367
+
368
+ /**
369
+ * Get surrounding context lines
370
+ */
371
+ function getContextLines(
372
+ lines: string[],
373
+ centerLine: number,
374
+ radius: number
375
+ ): string {
376
+ const start = Math.max(0, centerLine - radius);
377
+ const end = Math.min(lines.length, centerLine + radius + 1);
378
+ return lines.slice(start, end).join("\n");
379
+ }
380
+
381
+ /**
382
+ * Check if JSX element is conditionally rendered
383
+ */
384
+ function checkIfConditional(path: babel.NodePath<t.JSXOpeningElement>): boolean {
385
+ let current: babel.NodePath | null = path.parentPath;
386
+
387
+ while (current) {
388
+ // Check for {condition && <Component />}
389
+ if (
390
+ current.isLogicalExpression() &&
391
+ current.node.operator === "&&"
392
+ ) {
393
+ return true;
394
+ }
395
+
396
+ // Check for {condition ? <Component /> : null}
397
+ if (current.isConditionalExpression()) {
398
+ return true;
399
+ }
400
+
401
+ // Check for ternary in JSX expression container
402
+ if (
403
+ current.isJSXExpressionContainer() &&
404
+ current.parentPath?.isJSXElement()
405
+ ) {
406
+ const expr = current.node.expression;
407
+ if (
408
+ t.isLogicalExpression(expr) ||
409
+ t.isConditionalExpression(expr)
410
+ ) {
411
+ return true;
412
+ }
413
+ }
414
+
415
+ current = current.parentPath;
416
+ }
417
+
418
+ return false;
419
+ }
420
+
421
+ /**
422
+ * Get the parent JSX element name
423
+ */
424
+ function getParentElementName(
425
+ path: babel.NodePath<t.JSXOpeningElement>
426
+ ): string | undefined {
427
+ let current: babel.NodePath | null = path.parentPath;
428
+ const currentElementName = getJSXElementName(path.node.name);
429
+
430
+ while (current) {
431
+ if (current.isJSXElement()) {
432
+ const opening = current.node.openingElement;
433
+ const name = getJSXElementName(opening.name);
434
+ if (name && name !== currentElementName) {
435
+ return name;
436
+ }
437
+ }
438
+ current = current.parentPath;
439
+ }
440
+
441
+ return undefined;
442
+ }
443
+
444
+ // Type import for babel NodePath
445
+ import type * as babel from "@babel/traverse";