@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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- 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";
|