@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,275 @@
1
+ /**
2
+ * Analysis Cache Layer
3
+ *
4
+ * Caches file analysis results to enable fast incremental updates.
5
+ */
6
+
7
+ import { readFile, writeFile, stat } from "node:fs/promises";
8
+ import { existsSync } from "node:fs";
9
+ import { createHash } from "node:crypto";
10
+ import { join, dirname } from "node:path";
11
+ import { mkdir } from "node:fs/promises";
12
+ import type {
13
+ AnalysisCache,
14
+ FileCacheEntry,
15
+ FileChanges,
16
+ ComponentImport,
17
+ ComponentUsage,
18
+ CACHE_VERSION,
19
+ } from "./types.js";
20
+
21
+ const CURRENT_CACHE_VERSION = 1;
22
+ const CACHE_FILENAME = "analysis-cache.json";
23
+
24
+ /**
25
+ * Get the cache file path for a project
26
+ */
27
+ export function getCachePath(rootDir: string): string {
28
+ return join(rootDir, ".fragments", CACHE_FILENAME);
29
+ }
30
+
31
+ /**
32
+ * Load cache from disk
33
+ * Returns null if cache doesn't exist or is invalid
34
+ */
35
+ export async function loadCache(rootDir: string): Promise<AnalysisCache | null> {
36
+ const cachePath = getCachePath(rootDir);
37
+
38
+ if (!existsSync(cachePath)) {
39
+ return null;
40
+ }
41
+
42
+ try {
43
+ const content = await readFile(cachePath, "utf-8");
44
+ const cache = JSON.parse(content) as AnalysisCache;
45
+
46
+ // Validate version
47
+ if (cache.version !== CURRENT_CACHE_VERSION) {
48
+ console.warn(
49
+ `Cache version mismatch: expected ${CURRENT_CACHE_VERSION}, got ${cache.version}. Invalidating cache.`
50
+ );
51
+ return null;
52
+ }
53
+
54
+ // Validate root dir matches
55
+ if (cache.rootDir !== rootDir) {
56
+ console.warn(`Cache root mismatch. Invalidating cache.`);
57
+ return null;
58
+ }
59
+
60
+ return cache;
61
+ } catch (error) {
62
+ console.warn(`Failed to load cache: ${(error as Error).message}`);
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Save cache to disk
69
+ */
70
+ export async function saveCache(
71
+ rootDir: string,
72
+ cache: AnalysisCache
73
+ ): Promise<void> {
74
+ const cachePath = getCachePath(rootDir);
75
+ const cacheDir = dirname(cachePath);
76
+
77
+ // Ensure .fragments directory exists
78
+ if (!existsSync(cacheDir)) {
79
+ await mkdir(cacheDir, { recursive: true });
80
+ }
81
+
82
+ cache.updatedAt = new Date().toISOString();
83
+ await writeFile(cachePath, JSON.stringify(cache, null, 2));
84
+ }
85
+
86
+ /**
87
+ * Create a new empty cache
88
+ */
89
+ export function createEmptyCache(rootDir: string): AnalysisCache {
90
+ const now = new Date().toISOString();
91
+ return {
92
+ version: CURRENT_CACHE_VERSION,
93
+ createdAt: now,
94
+ updatedAt: now,
95
+ rootDir,
96
+ files: {},
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Compute content hash for a file
102
+ */
103
+ export function computeFileHash(content: string): string {
104
+ return createHash("md5").update(content).digest("hex");
105
+ }
106
+
107
+ /**
108
+ * Compute file hash from disk
109
+ */
110
+ export async function computeFileHashFromDisk(
111
+ filePath: string
112
+ ): Promise<string> {
113
+ const content = await readFile(filePath, "utf-8");
114
+ return computeFileHash(content);
115
+ }
116
+
117
+ /**
118
+ * Check if a file is cached and unchanged
119
+ */
120
+ export function isFileCached(
121
+ cache: AnalysisCache,
122
+ filePath: string,
123
+ currentHash: string
124
+ ): boolean {
125
+ const entry = cache.files[filePath];
126
+ if (!entry) return false;
127
+ return entry.hash === currentHash;
128
+ }
129
+
130
+ /**
131
+ * Get cached data for a file
132
+ */
133
+ export function getCachedFile(
134
+ cache: AnalysisCache,
135
+ filePath: string
136
+ ): FileCacheEntry | null {
137
+ return cache.files[filePath] ?? null;
138
+ }
139
+
140
+ /**
141
+ * Update cache with results for a file
142
+ */
143
+ export function updateCacheFile(
144
+ cache: AnalysisCache,
145
+ filePath: string,
146
+ hash: string,
147
+ imports: ComponentImport[],
148
+ usages: ComponentUsage[]
149
+ ): void {
150
+ cache.files[filePath] = {
151
+ hash,
152
+ timestamp: new Date().toISOString(),
153
+ imports,
154
+ usages,
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Remove a file from cache
160
+ */
161
+ export function removeCacheFile(cache: AnalysisCache, filePath: string): void {
162
+ delete cache.files[filePath];
163
+ }
164
+
165
+ /**
166
+ * Invalidate cache entries for specific files
167
+ */
168
+ export function invalidateCache(
169
+ cache: AnalysisCache,
170
+ filePaths: string[]
171
+ ): void {
172
+ for (const filePath of filePaths) {
173
+ delete cache.files[filePath];
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Detect file changes by comparing current files to cache
179
+ */
180
+ export async function detectFileChanges(
181
+ cache: AnalysisCache,
182
+ currentFiles: string[],
183
+ getFileHash: (filePath: string) => Promise<string>
184
+ ): Promise<FileChanges> {
185
+ const changes: FileChanges = {
186
+ added: [],
187
+ modified: [],
188
+ deleted: [],
189
+ unchanged: [],
190
+ };
191
+
192
+ const currentFileSet = new Set(currentFiles);
193
+ const cachedFileSet = new Set(Object.keys(cache.files));
194
+
195
+ // Check for added and modified files
196
+ for (const filePath of currentFiles) {
197
+ const entry = cache.files[filePath];
198
+
199
+ if (!entry) {
200
+ changes.added.push(filePath);
201
+ } else {
202
+ const currentHash = await getFileHash(filePath);
203
+ if (currentHash !== entry.hash) {
204
+ changes.modified.push(filePath);
205
+ } else {
206
+ changes.unchanged.push(filePath);
207
+ }
208
+ }
209
+ }
210
+
211
+ // Check for deleted files
212
+ for (const cachedFile of cachedFileSet) {
213
+ if (!currentFileSet.has(cachedFile)) {
214
+ changes.deleted.push(cachedFile);
215
+ }
216
+ }
217
+
218
+ return changes;
219
+ }
220
+
221
+ /**
222
+ * Get all cached imports
223
+ */
224
+ export function getAllCachedImports(cache: AnalysisCache): ComponentImport[] {
225
+ const imports: ComponentImport[] = [];
226
+ for (const entry of Object.values(cache.files)) {
227
+ imports.push(...entry.imports);
228
+ }
229
+ return imports;
230
+ }
231
+
232
+ /**
233
+ * Get all cached usages
234
+ */
235
+ export function getAllCachedUsages(cache: AnalysisCache): ComponentUsage[] {
236
+ const usages: ComponentUsage[] = [];
237
+ for (const entry of Object.values(cache.files)) {
238
+ usages.push(...entry.usages);
239
+ }
240
+ return usages;
241
+ }
242
+
243
+ /**
244
+ * Get cache statistics
245
+ */
246
+ export function getCacheStats(cache: AnalysisCache): {
247
+ totalFiles: number;
248
+ totalImports: number;
249
+ totalUsages: number;
250
+ cacheAge: string;
251
+ } {
252
+ let totalImports = 0;
253
+ let totalUsages = 0;
254
+
255
+ for (const entry of Object.values(cache.files)) {
256
+ totalImports += entry.imports.length;
257
+ totalUsages += entry.usages.length;
258
+ }
259
+
260
+ const ageMs = Date.now() - new Date(cache.updatedAt).getTime();
261
+ const ageMinutes = Math.round(ageMs / 60000);
262
+ const cacheAge =
263
+ ageMinutes < 60
264
+ ? `${ageMinutes} minutes ago`
265
+ : ageMinutes < 1440
266
+ ? `${Math.round(ageMinutes / 60)} hours ago`
267
+ : `${Math.round(ageMinutes / 1440)} days ago`;
268
+
269
+ return {
270
+ totalFiles: Object.keys(cache.files).length,
271
+ totalImports,
272
+ totalUsages,
273
+ cacheAge,
274
+ };
275
+ }
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Full Codebase Scanner
3
+ *
4
+ * Scans entire codebase for component usage patterns with caching and progress.
5
+ */
6
+
7
+ import fg from "fast-glob";
8
+ import { readFile } from "node:fs/promises";
9
+ import { relative, resolve, dirname, basename } from "node:path";
10
+ import type {
11
+ ScanOptions,
12
+ ScanProgress,
13
+ UsageAnalysis,
14
+ ComponentImport,
15
+ ComponentUsage,
16
+ FileChanges,
17
+ AnalysisCache,
18
+ } from "./types.js";
19
+ import { scanFile } from "./scanner.js";
20
+ import { aggregateAllUsages } from "./aggregator.js";
21
+ import {
22
+ loadCache,
23
+ saveCache,
24
+ createEmptyCache,
25
+ computeFileHash,
26
+ isFileCached,
27
+ updateCacheFile,
28
+ removeCacheFile,
29
+ getCachedFile,
30
+ detectFileChanges,
31
+ getCacheStats,
32
+ } from "./cache.js";
33
+
34
+ /**
35
+ * Default patterns for files to scan
36
+ */
37
+ const DEFAULT_INCLUDE = [
38
+ "**/*.tsx",
39
+ "**/*.ts",
40
+ "**/*.jsx",
41
+ "**/*.js",
42
+ ];
43
+
44
+ /**
45
+ * Default patterns for files to exclude
46
+ */
47
+ const DEFAULT_EXCLUDE = [
48
+ "**/node_modules/**",
49
+ "**/dist/**",
50
+ "**/build/**",
51
+ "**/.next/**",
52
+ "**/coverage/**",
53
+ "**/__tests__/**",
54
+ "**/*.test.*",
55
+ "**/*.spec.*",
56
+ "**/*.stories.*",
57
+ "**/*.segment.*",
58
+ "**/storybook-static/**",
59
+ ];
60
+
61
+ /**
62
+ * Scan entire codebase for component usages
63
+ */
64
+ export async function scanCodebase(
65
+ options: ScanOptions
66
+ ): Promise<UsageAnalysis> {
67
+ const {
68
+ rootDir,
69
+ include = DEFAULT_INCLUDE,
70
+ exclude = DEFAULT_EXCLUDE,
71
+ componentNames,
72
+ useCache = true,
73
+ onProgress,
74
+ } = options;
75
+
76
+ const absoluteRoot = resolve(rootDir);
77
+
78
+ // Load or create cache
79
+ let cache: AnalysisCache | null = null;
80
+ if (useCache) {
81
+ cache = await loadCache(absoluteRoot);
82
+ }
83
+ if (!cache) {
84
+ cache = createEmptyCache(absoluteRoot);
85
+ }
86
+
87
+ // Discover all files
88
+ onProgress?.({
89
+ current: 0,
90
+ total: 0,
91
+ currentFile: "",
92
+ phase: "discovering",
93
+ });
94
+
95
+ const files = await fg(include, {
96
+ cwd: absoluteRoot,
97
+ ignore: exclude,
98
+ absolute: true,
99
+ onlyFiles: true,
100
+ });
101
+
102
+ const totalFiles = files.length;
103
+
104
+ // Track components to find (if specific ones provided)
105
+ const trackedComponents = componentNames
106
+ ? new Set(componentNames)
107
+ : undefined;
108
+
109
+ // Collect all imports and usages
110
+ const allImports: ComponentImport[] = [];
111
+ const allUsages: ComponentUsage[] = [];
112
+ const errorFiles: string[] = [];
113
+ const componentSources = new Map<string, string>();
114
+
115
+ // Process files
116
+ for (let i = 0; i < files.length; i++) {
117
+ const filePath = files[i];
118
+ const relativePath = relative(absoluteRoot, filePath);
119
+
120
+ onProgress?.({
121
+ current: i + 1,
122
+ total: totalFiles,
123
+ currentFile: relativePath,
124
+ phase: "scanning",
125
+ });
126
+
127
+ try {
128
+ // Read file content for hash
129
+ const content = await readFile(filePath, "utf-8");
130
+ const fileHash = computeFileHash(content);
131
+
132
+ // Check cache
133
+ const cachedEntry = getCachedFile(cache, filePath);
134
+ if (cachedEntry && cachedEntry.hash === fileHash) {
135
+ // Use cached results
136
+ allImports.push(...cachedEntry.imports);
137
+ allUsages.push(...cachedEntry.usages);
138
+
139
+ // Track component sources from imports
140
+ for (const imp of cachedEntry.imports) {
141
+ if (!componentSources.has(imp.componentName)) {
142
+ const sourceFile = resolveImportSource(filePath, imp.source);
143
+ if (sourceFile) {
144
+ componentSources.set(imp.componentName, sourceFile);
145
+ }
146
+ }
147
+ }
148
+ continue;
149
+ }
150
+
151
+ // Scan file
152
+ const { imports, usages } = await scanFile(filePath, trackedComponents);
153
+
154
+ // Update cache
155
+ updateCacheFile(cache, filePath, fileHash, imports, usages);
156
+
157
+ allImports.push(...imports);
158
+ allUsages.push(...usages);
159
+
160
+ // Track component sources
161
+ for (const imp of imports) {
162
+ if (!componentSources.has(imp.componentName)) {
163
+ const sourceFile = resolveImportSource(filePath, imp.source);
164
+ if (sourceFile) {
165
+ componentSources.set(imp.componentName, sourceFile);
166
+ }
167
+ }
168
+ }
169
+ } catch (error) {
170
+ errorFiles.push(relativePath);
171
+ console.warn(`Error scanning ${relativePath}:`, (error as Error).message);
172
+ }
173
+ }
174
+
175
+ // Save cache
176
+ if (useCache) {
177
+ await saveCache(absoluteRoot, cache);
178
+ }
179
+
180
+ // Aggregate results
181
+ onProgress?.({
182
+ current: totalFiles,
183
+ total: totalFiles,
184
+ currentFile: "",
185
+ phase: "aggregating",
186
+ });
187
+
188
+ const analysis = aggregateAllUsages(allUsages, allImports, componentSources);
189
+ analysis.rootDir = absoluteRoot;
190
+ analysis.totalFiles = totalFiles;
191
+ analysis.errorFiles = errorFiles;
192
+
193
+ return analysis;
194
+ }
195
+
196
+ /**
197
+ * Perform incremental scan for changed files only
198
+ */
199
+ export async function incrementalScan(
200
+ rootDir: string,
201
+ changes: FileChanges,
202
+ existingCache: AnalysisCache,
203
+ onProgress?: (progress: ScanProgress) => void
204
+ ): Promise<{ analysis: UsageAnalysis; cache: AnalysisCache }> {
205
+ const absoluteRoot = resolve(rootDir);
206
+ const cache = { ...existingCache, files: { ...existingCache.files } };
207
+
208
+ // Remove deleted files from cache
209
+ for (const filePath of changes.deleted) {
210
+ removeCacheFile(cache, filePath);
211
+ }
212
+
213
+ // Files to scan (added + modified)
214
+ const filesToScan = [...changes.added, ...changes.modified];
215
+ const totalFiles = filesToScan.length;
216
+
217
+ const allImports: ComponentImport[] = [];
218
+ const allUsages: ComponentUsage[] = [];
219
+ const errorFiles: string[] = [];
220
+ const componentSources = new Map<string, string>();
221
+
222
+ // First, collect unchanged file data from cache
223
+ for (const filePath of changes.unchanged) {
224
+ const cachedEntry = getCachedFile(cache, filePath);
225
+ if (cachedEntry) {
226
+ allImports.push(...cachedEntry.imports);
227
+ allUsages.push(...cachedEntry.usages);
228
+
229
+ for (const imp of cachedEntry.imports) {
230
+ if (!componentSources.has(imp.componentName)) {
231
+ const sourceFile = resolveImportSource(filePath, imp.source);
232
+ if (sourceFile) {
233
+ componentSources.set(imp.componentName, sourceFile);
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ // Scan changed files
241
+ for (let i = 0; i < filesToScan.length; i++) {
242
+ const filePath = filesToScan[i];
243
+ const relativePath = relative(absoluteRoot, filePath);
244
+
245
+ onProgress?.({
246
+ current: i + 1,
247
+ total: totalFiles,
248
+ currentFile: relativePath,
249
+ phase: "scanning",
250
+ });
251
+
252
+ try {
253
+ const content = await readFile(filePath, "utf-8");
254
+ const fileHash = computeFileHash(content);
255
+
256
+ const { imports, usages } = await scanFile(filePath);
257
+
258
+ updateCacheFile(cache, filePath, fileHash, imports, usages);
259
+
260
+ allImports.push(...imports);
261
+ allUsages.push(...usages);
262
+
263
+ for (const imp of imports) {
264
+ if (!componentSources.has(imp.componentName)) {
265
+ const sourceFile = resolveImportSource(filePath, imp.source);
266
+ if (sourceFile) {
267
+ componentSources.set(imp.componentName, sourceFile);
268
+ }
269
+ }
270
+ }
271
+ } catch (error) {
272
+ errorFiles.push(relativePath);
273
+ }
274
+ }
275
+
276
+ // Save updated cache
277
+ await saveCache(absoluteRoot, cache);
278
+
279
+ // Aggregate
280
+ const analysis = aggregateAllUsages(allUsages, allImports, componentSources);
281
+ analysis.rootDir = absoluteRoot;
282
+ analysis.totalFiles = changes.unchanged.length + filesToScan.length;
283
+ analysis.errorFiles = errorFiles;
284
+
285
+ return { analysis, cache };
286
+ }
287
+
288
+ /**
289
+ * Resolve import source to absolute file path
290
+ */
291
+ function resolveImportSource(
292
+ importingFile: string,
293
+ source: string
294
+ ): string | null {
295
+ // Skip node_modules imports
296
+ if (!source.startsWith(".") && !source.startsWith("/")) {
297
+ return null;
298
+ }
299
+
300
+ const importDir = dirname(importingFile);
301
+
302
+ // Try common extensions
303
+ const extensions = ["", ".tsx", ".ts", ".jsx", ".js", "/index.tsx", "/index.ts"];
304
+ for (const ext of extensions) {
305
+ const fullPath = resolve(importDir, source + ext);
306
+ // We don't check if file exists here for performance
307
+ // The source path is mainly for reference
308
+ if (ext === "" && source.endsWith(".tsx")) {
309
+ return fullPath;
310
+ }
311
+ if (ext) {
312
+ return resolve(importDir, source) + ext;
313
+ }
314
+ }
315
+
316
+ return resolve(importDir, source);
317
+ }
318
+
319
+ /**
320
+ * Get scan statistics for display
321
+ */
322
+ export function getScanStats(analysis: UsageAnalysis): {
323
+ totalFiles: number;
324
+ totalComponents: number;
325
+ totalUsages: number;
326
+ topComponents: { name: string; usages: number }[];
327
+ } {
328
+ const totalUsages = Object.values(analysis.components).reduce(
329
+ (sum, c) => sum + c.totalUsages,
330
+ 0
331
+ );
332
+
333
+ const topComponents = Object.values(analysis.components)
334
+ .map((c) => ({ name: c.name, usages: c.totalUsages }))
335
+ .sort((a, b) => b.usages - a.usages)
336
+ .slice(0, 10);
337
+
338
+ return {
339
+ totalFiles: analysis.totalFiles,
340
+ totalComponents: analysis.totalComponents,
341
+ totalUsages,
342
+ topComponents,
343
+ };
344
+ }
345
+
346
+ /**
347
+ * Export for quick check if cache exists and is valid
348
+ */
349
+ export async function hasCachedAnalysis(rootDir: string): Promise<boolean> {
350
+ const cache = await loadCache(resolve(rootDir));
351
+ if (!cache) return false;
352
+ const stats = getCacheStats(cache);
353
+ return stats.totalFiles > 0;
354
+ }
355
+
356
+ // Re-export types and utilities
357
+ export { loadCache, getCacheStats, detectFileChanges } from "./cache.js";