@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,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";
|