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