@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,665 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript Props Extractor
|
|
3
|
+
*
|
|
4
|
+
* Uses TypeScript AST to extract complete prop definitions
|
|
5
|
+
* from component TypeScript interfaces and types.
|
|
6
|
+
*
|
|
7
|
+
* This extracts:
|
|
8
|
+
* - Interface and type alias definitions
|
|
9
|
+
* - Property names, types, and optionality
|
|
10
|
+
* - JSDoc comments and descriptions
|
|
11
|
+
* - Union types (for enum-like values)
|
|
12
|
+
* - Default values from JSDoc tags
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as ts from "typescript";
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
19
|
+
import type { PropDefinition, PropType } from "../../core/index.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extracted prop with full type information
|
|
23
|
+
*/
|
|
24
|
+
export interface ExtractedProp {
|
|
25
|
+
/** Prop name */
|
|
26
|
+
name: string;
|
|
27
|
+
/** Type string for display */
|
|
28
|
+
type: string;
|
|
29
|
+
/** Structured type information */
|
|
30
|
+
propType: PropType;
|
|
31
|
+
/** JSDoc description */
|
|
32
|
+
description: string;
|
|
33
|
+
/** Whether the prop is required */
|
|
34
|
+
required: boolean;
|
|
35
|
+
/** Default value if detected */
|
|
36
|
+
defaultValue?: unknown;
|
|
37
|
+
/** For enum/union types, the specific values */
|
|
38
|
+
enumValues?: string[];
|
|
39
|
+
/** Deprecation notice if present */
|
|
40
|
+
deprecated?: string;
|
|
41
|
+
/** Additional JSDoc tags */
|
|
42
|
+
tags?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Result of props extraction from a component file
|
|
47
|
+
*/
|
|
48
|
+
export interface PropsExtractionResult {
|
|
49
|
+
/** Path to the source file */
|
|
50
|
+
filePath: string;
|
|
51
|
+
/** Component name */
|
|
52
|
+
componentName: string;
|
|
53
|
+
/** All extracted props */
|
|
54
|
+
props: ExtractedProp[];
|
|
55
|
+
/** Name of the props interface/type found */
|
|
56
|
+
propsTypeName?: string;
|
|
57
|
+
/** Whether extraction was successful */
|
|
58
|
+
success: boolean;
|
|
59
|
+
/** Any warnings or errors encountered */
|
|
60
|
+
warnings: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Options for props extraction
|
|
65
|
+
*/
|
|
66
|
+
export interface PropsExtractionOptions {
|
|
67
|
+
/** Look for a specific props type name (e.g., "ButtonProps") */
|
|
68
|
+
propsTypeName?: string;
|
|
69
|
+
/** Include inherited props from extended interfaces */
|
|
70
|
+
includeInherited?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extract props from a TypeScript/TSX component file
|
|
75
|
+
*/
|
|
76
|
+
export async function extractPropsFromFile(
|
|
77
|
+
filePath: string,
|
|
78
|
+
options: PropsExtractionOptions = {}
|
|
79
|
+
): Promise<PropsExtractionResult> {
|
|
80
|
+
const absPath = resolve(filePath);
|
|
81
|
+
|
|
82
|
+
if (!existsSync(absPath)) {
|
|
83
|
+
return {
|
|
84
|
+
filePath: absPath,
|
|
85
|
+
componentName: inferComponentName(absPath),
|
|
86
|
+
props: [],
|
|
87
|
+
success: false,
|
|
88
|
+
warnings: [`File not found: ${absPath}`],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const content = await readFile(absPath, "utf-8");
|
|
93
|
+
return extractPropsFromSource(content, absPath, options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Extract props from TypeScript source code
|
|
98
|
+
*/
|
|
99
|
+
export function extractPropsFromSource(
|
|
100
|
+
source: string,
|
|
101
|
+
filePath: string,
|
|
102
|
+
options: PropsExtractionOptions = {}
|
|
103
|
+
): PropsExtractionResult {
|
|
104
|
+
const { propsTypeName } = options;
|
|
105
|
+
|
|
106
|
+
const componentName = inferComponentName(filePath);
|
|
107
|
+
const result: PropsExtractionResult = {
|
|
108
|
+
filePath,
|
|
109
|
+
componentName,
|
|
110
|
+
props: [],
|
|
111
|
+
success: false,
|
|
112
|
+
warnings: [],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Create a source file
|
|
116
|
+
const sourceFile = ts.createSourceFile(
|
|
117
|
+
filePath,
|
|
118
|
+
source,
|
|
119
|
+
ts.ScriptTarget.ESNext,
|
|
120
|
+
true,
|
|
121
|
+
filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Find all interfaces and type aliases that could be props
|
|
125
|
+
const propsDeclarations: Array<{
|
|
126
|
+
name: string;
|
|
127
|
+
node: ts.InterfaceDeclaration | ts.TypeAliasDeclaration;
|
|
128
|
+
}> = [];
|
|
129
|
+
|
|
130
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
131
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
132
|
+
propsDeclarations.push({ name: node.name.text, node });
|
|
133
|
+
}
|
|
134
|
+
if (ts.isTypeAliasDeclaration(node)) {
|
|
135
|
+
propsDeclarations.push({ name: node.name.text, node });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Find the best matching props type
|
|
140
|
+
const targetName = propsTypeName || `${componentName}Props`;
|
|
141
|
+
let propsDecl = propsDeclarations.find((d) => d.name === targetName);
|
|
142
|
+
|
|
143
|
+
// Fallback to common names
|
|
144
|
+
if (!propsDecl) {
|
|
145
|
+
const fallbackNames = ["Props", `${componentName}Properties`];
|
|
146
|
+
for (const name of fallbackNames) {
|
|
147
|
+
propsDecl = propsDeclarations.find((d) => d.name === name);
|
|
148
|
+
if (propsDecl) break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Fallback to any *Props or *Properties
|
|
153
|
+
if (!propsDecl) {
|
|
154
|
+
propsDecl = propsDeclarations.find(
|
|
155
|
+
(d) => d.name.endsWith("Props") || d.name.endsWith("Properties")
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!propsDecl) {
|
|
160
|
+
result.warnings.push(
|
|
161
|
+
`No props type found for ${componentName}. Looked for: ${targetName}`
|
|
162
|
+
);
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
result.propsTypeName = propsDecl.name;
|
|
167
|
+
|
|
168
|
+
// Extract props from the declaration
|
|
169
|
+
if (ts.isInterfaceDeclaration(propsDecl.node)) {
|
|
170
|
+
extractPropsFromInterface(propsDecl.node, sourceFile, result);
|
|
171
|
+
} else if (ts.isTypeAliasDeclaration(propsDecl.node)) {
|
|
172
|
+
extractPropsFromTypeAlias(propsDecl.node, sourceFile, result);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
result.success = result.props.length > 0;
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Extract props from an interface declaration
|
|
181
|
+
*/
|
|
182
|
+
function extractPropsFromInterface(
|
|
183
|
+
node: ts.InterfaceDeclaration,
|
|
184
|
+
sourceFile: ts.SourceFile,
|
|
185
|
+
result: PropsExtractionResult
|
|
186
|
+
): void {
|
|
187
|
+
for (const member of node.members) {
|
|
188
|
+
if (ts.isPropertySignature(member)) {
|
|
189
|
+
const prop = extractPropFromSignature(member, sourceFile);
|
|
190
|
+
if (prop) {
|
|
191
|
+
result.props.push(prop);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Extract props from a type alias declaration
|
|
199
|
+
*/
|
|
200
|
+
function extractPropsFromTypeAlias(
|
|
201
|
+
node: ts.TypeAliasDeclaration,
|
|
202
|
+
sourceFile: ts.SourceFile,
|
|
203
|
+
result: PropsExtractionResult
|
|
204
|
+
): void {
|
|
205
|
+
if (ts.isTypeLiteralNode(node.type)) {
|
|
206
|
+
for (const member of node.type.members) {
|
|
207
|
+
if (ts.isPropertySignature(member)) {
|
|
208
|
+
const prop = extractPropFromSignature(member, sourceFile);
|
|
209
|
+
if (prop) {
|
|
210
|
+
result.props.push(prop);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Extract a single prop from a property signature
|
|
219
|
+
*/
|
|
220
|
+
function extractPropFromSignature(
|
|
221
|
+
member: ts.PropertySignature,
|
|
222
|
+
sourceFile: ts.SourceFile
|
|
223
|
+
): ExtractedProp | null {
|
|
224
|
+
// Get prop name
|
|
225
|
+
const name = ts.isIdentifier(member.name)
|
|
226
|
+
? member.name.text
|
|
227
|
+
: member.name.getText(sourceFile);
|
|
228
|
+
|
|
229
|
+
// Skip internal props
|
|
230
|
+
if (name.startsWith("_") || name.startsWith("$")) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Get type
|
|
235
|
+
const typeNode = member.type;
|
|
236
|
+
const typeString = typeNode ? typeNode.getText(sourceFile) : "unknown";
|
|
237
|
+
|
|
238
|
+
// Determine if optional
|
|
239
|
+
const isOptional = member.questionToken !== undefined;
|
|
240
|
+
|
|
241
|
+
// Get JSDoc information
|
|
242
|
+
const jsDoc = getJSDocInfo(member, sourceFile);
|
|
243
|
+
|
|
244
|
+
// Parse the type into our PropType structure
|
|
245
|
+
const typeInfo = typeNode
|
|
246
|
+
? parseTypeNode(typeNode, sourceFile)
|
|
247
|
+
: { propType: { type: "custom" as const, typescript: "unknown" } };
|
|
248
|
+
|
|
249
|
+
const prop: ExtractedProp = {
|
|
250
|
+
name,
|
|
251
|
+
type: typeString,
|
|
252
|
+
propType: typeInfo.propType,
|
|
253
|
+
description: jsDoc.description,
|
|
254
|
+
required: !isOptional,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Add enum values if available
|
|
258
|
+
if (typeInfo.enumValues && typeInfo.enumValues.length > 0) {
|
|
259
|
+
prop.enumValues = typeInfo.enumValues;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Add default value if detected from JSDoc
|
|
263
|
+
if (jsDoc.defaultValue !== undefined) {
|
|
264
|
+
prop.defaultValue = jsDoc.defaultValue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Add deprecation notice
|
|
268
|
+
if (jsDoc.deprecated) {
|
|
269
|
+
prop.deprecated = jsDoc.deprecated;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Add other tags
|
|
273
|
+
if (Object.keys(jsDoc.tags).length > 0) {
|
|
274
|
+
prop.tags = jsDoc.tags;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return prop;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get JSDoc information from a node
|
|
282
|
+
*/
|
|
283
|
+
function getJSDocInfo(
|
|
284
|
+
node: ts.Node,
|
|
285
|
+
sourceFile: ts.SourceFile
|
|
286
|
+
): {
|
|
287
|
+
description: string;
|
|
288
|
+
defaultValue?: unknown;
|
|
289
|
+
deprecated?: string;
|
|
290
|
+
tags: Record<string, string>;
|
|
291
|
+
} {
|
|
292
|
+
const result = {
|
|
293
|
+
description: "",
|
|
294
|
+
defaultValue: undefined as unknown,
|
|
295
|
+
deprecated: undefined as string | undefined,
|
|
296
|
+
tags: {} as Record<string, string>,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Get full text including comments
|
|
300
|
+
const fullText = sourceFile.getFullText();
|
|
301
|
+
const nodeStart = node.getFullStart();
|
|
302
|
+
const nodePos = node.getStart(sourceFile);
|
|
303
|
+
const leadingText = fullText.slice(nodeStart, nodePos);
|
|
304
|
+
|
|
305
|
+
// Find JSDoc comments (/** ... */)
|
|
306
|
+
const jsDocMatch = leadingText.match(/\/\*\*([\s\S]*?)\*\//);
|
|
307
|
+
if (jsDocMatch) {
|
|
308
|
+
const content = jsDocMatch[1];
|
|
309
|
+
parseJSDocContent(content, result);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Also check line comment above (// ...)
|
|
313
|
+
const lineCommentMatch = leadingText.match(/\/\/\s*(.+)$/m);
|
|
314
|
+
if (lineCommentMatch && !result.description) {
|
|
315
|
+
result.description = lineCommentMatch[1].trim();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Parse JSDoc content into structured data
|
|
323
|
+
*/
|
|
324
|
+
function parseJSDocContent(
|
|
325
|
+
content: string,
|
|
326
|
+
result: {
|
|
327
|
+
description: string;
|
|
328
|
+
defaultValue?: unknown;
|
|
329
|
+
deprecated?: string;
|
|
330
|
+
tags: Record<string, string>;
|
|
331
|
+
}
|
|
332
|
+
): void {
|
|
333
|
+
const lines = content.split("\n");
|
|
334
|
+
let currentDescription: string[] = [];
|
|
335
|
+
let currentTag: string | null = null;
|
|
336
|
+
let currentTagContent: string[] = [];
|
|
337
|
+
|
|
338
|
+
for (const line of lines) {
|
|
339
|
+
// Remove leading * and whitespace
|
|
340
|
+
const trimmed = line.replace(/^\s*\*?\s?/, "").trim();
|
|
341
|
+
|
|
342
|
+
// Check for JSDoc tag
|
|
343
|
+
const tagMatch = trimmed.match(/^@(\w+)(?:\s+(.*))?$/);
|
|
344
|
+
|
|
345
|
+
if (tagMatch) {
|
|
346
|
+
// Save previous tag if exists
|
|
347
|
+
if (currentTag) {
|
|
348
|
+
processJSDocTag(result, currentTag, currentTagContent.join("\n").trim());
|
|
349
|
+
} else if (currentDescription.length > 0) {
|
|
350
|
+
result.description = currentDescription.join(" ").trim();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
currentTag = tagMatch[1];
|
|
354
|
+
currentTagContent = tagMatch[2] ? [tagMatch[2]] : [];
|
|
355
|
+
} else if (currentTag) {
|
|
356
|
+
if (trimmed) {
|
|
357
|
+
currentTagContent.push(trimmed);
|
|
358
|
+
}
|
|
359
|
+
} else if (trimmed) {
|
|
360
|
+
currentDescription.push(trimmed);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Process final tag
|
|
365
|
+
if (currentTag) {
|
|
366
|
+
processJSDocTag(result, currentTag, currentTagContent.join("\n").trim());
|
|
367
|
+
} else if (currentDescription.length > 0 && !result.description) {
|
|
368
|
+
result.description = currentDescription.join(" ").trim();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Process a single JSDoc tag
|
|
374
|
+
*/
|
|
375
|
+
function processJSDocTag(
|
|
376
|
+
result: {
|
|
377
|
+
description: string;
|
|
378
|
+
defaultValue?: unknown;
|
|
379
|
+
deprecated?: string;
|
|
380
|
+
tags: Record<string, string>;
|
|
381
|
+
},
|
|
382
|
+
tag: string,
|
|
383
|
+
content: string
|
|
384
|
+
): void {
|
|
385
|
+
switch (tag) {
|
|
386
|
+
case "default":
|
|
387
|
+
case "defaultValue":
|
|
388
|
+
result.defaultValue = parseDefaultValue(content);
|
|
389
|
+
break;
|
|
390
|
+
case "deprecated":
|
|
391
|
+
result.deprecated = content || "This prop is deprecated";
|
|
392
|
+
break;
|
|
393
|
+
case "description":
|
|
394
|
+
result.description = content;
|
|
395
|
+
break;
|
|
396
|
+
default:
|
|
397
|
+
if (content) {
|
|
398
|
+
result.tags[tag] = content;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Parse a default value from JSDoc string
|
|
405
|
+
*/
|
|
406
|
+
function parseDefaultValue(value: string): unknown {
|
|
407
|
+
const trimmed = value.trim();
|
|
408
|
+
if (trimmed === "true") return true;
|
|
409
|
+
if (trimmed === "false") return false;
|
|
410
|
+
if (trimmed === "null") return null;
|
|
411
|
+
if (trimmed === "undefined") return undefined;
|
|
412
|
+
if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10);
|
|
413
|
+
if (/^-?\d+\.\d+$/.test(trimmed)) return parseFloat(trimmed);
|
|
414
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
415
|
+
return trimmed.slice(1, -1);
|
|
416
|
+
}
|
|
417
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
418
|
+
return trimmed.slice(1, -1);
|
|
419
|
+
}
|
|
420
|
+
return trimmed;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Parse a type node into our PropType structure
|
|
425
|
+
*/
|
|
426
|
+
function parseTypeNode(
|
|
427
|
+
node: ts.TypeNode,
|
|
428
|
+
sourceFile: ts.SourceFile
|
|
429
|
+
): { propType: PropType; enumValues?: string[] } {
|
|
430
|
+
// Boolean keyword
|
|
431
|
+
if (node.kind === ts.SyntaxKind.BooleanKeyword) {
|
|
432
|
+
return { propType: { type: "boolean" } };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// String keyword
|
|
436
|
+
if (node.kind === ts.SyntaxKind.StringKeyword) {
|
|
437
|
+
return { propType: { type: "string" } };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Number keyword
|
|
441
|
+
if (node.kind === ts.SyntaxKind.NumberKeyword) {
|
|
442
|
+
return { propType: { type: "number" } };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Union type (including string literal unions -> enum)
|
|
446
|
+
if (ts.isUnionTypeNode(node)) {
|
|
447
|
+
const stringLiterals: string[] = [];
|
|
448
|
+
const otherTypes: PropType[] = [];
|
|
449
|
+
|
|
450
|
+
for (const type of node.types) {
|
|
451
|
+
if (ts.isLiteralTypeNode(type) && ts.isStringLiteral(type.literal)) {
|
|
452
|
+
stringLiterals.push(type.literal.text);
|
|
453
|
+
} else if (
|
|
454
|
+
type.kind !== ts.SyntaxKind.NullKeyword &&
|
|
455
|
+
type.kind !== ts.SyntaxKind.UndefinedKeyword
|
|
456
|
+
) {
|
|
457
|
+
const parsed = parseTypeNode(type, sourceFile);
|
|
458
|
+
otherTypes.push(parsed.propType);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// If all non-null types are string literals, it's an enum
|
|
463
|
+
if (stringLiterals.length > 0 && otherTypes.length === 0) {
|
|
464
|
+
return {
|
|
465
|
+
propType: { type: "enum", values: stringLiterals },
|
|
466
|
+
enumValues: stringLiterals,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// If we have string literals plus other types, still treat as enum for the values
|
|
471
|
+
if (stringLiterals.length > 0) {
|
|
472
|
+
return {
|
|
473
|
+
propType: { type: "enum", values: stringLiterals },
|
|
474
|
+
enumValues: stringLiterals,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Generic union
|
|
479
|
+
if (otherTypes.length > 0) {
|
|
480
|
+
return {
|
|
481
|
+
propType: { type: "union", types: otherTypes },
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Array type
|
|
487
|
+
if (ts.isArrayTypeNode(node)) {
|
|
488
|
+
const elementType = parseTypeNode(node.elementType, sourceFile);
|
|
489
|
+
return {
|
|
490
|
+
propType: { type: "array", items: elementType.propType },
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Function type
|
|
495
|
+
if (ts.isFunctionTypeNode(node)) {
|
|
496
|
+
const params = node.parameters
|
|
497
|
+
.map((p) => {
|
|
498
|
+
const name = p.name.getText(sourceFile);
|
|
499
|
+
const type = p.type ? p.type.getText(sourceFile) : "any";
|
|
500
|
+
return `${name}: ${type}`;
|
|
501
|
+
})
|
|
502
|
+
.join(", ");
|
|
503
|
+
const returnType = node.type ? node.type.getText(sourceFile) : "void";
|
|
504
|
+
return {
|
|
505
|
+
propType: { type: "function", signature: `(${params}) => ${returnType}` },
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Type reference (like ReactNode, ReactElement, etc.)
|
|
510
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
511
|
+
const typeName = node.typeName.getText(sourceFile);
|
|
512
|
+
|
|
513
|
+
// Check for React node types
|
|
514
|
+
if (
|
|
515
|
+
typeName === "ReactNode" ||
|
|
516
|
+
typeName === "React.ReactNode" ||
|
|
517
|
+
typeName.endsWith(".ReactNode")
|
|
518
|
+
) {
|
|
519
|
+
return { propType: { type: "node" } };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (
|
|
523
|
+
typeName === "ReactElement" ||
|
|
524
|
+
typeName === "React.ReactElement" ||
|
|
525
|
+
typeName === "JSX.Element"
|
|
526
|
+
) {
|
|
527
|
+
return { propType: { type: "element" } };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Generic type reference
|
|
531
|
+
return {
|
|
532
|
+
propType: { type: "custom", typescript: node.getText(sourceFile) },
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Type literal (inline object type)
|
|
537
|
+
if (ts.isTypeLiteralNode(node)) {
|
|
538
|
+
return { propType: { type: "object" } };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Default: custom type
|
|
542
|
+
return {
|
|
543
|
+
propType: { type: "custom", typescript: node.getText(sourceFile) },
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Infer component name from file path
|
|
549
|
+
*/
|
|
550
|
+
function inferComponentName(filePath: string): string {
|
|
551
|
+
const fileName = basename(filePath);
|
|
552
|
+
// Remove extension
|
|
553
|
+
let name = fileName.replace(/\.(tsx?|jsx?)$/, "");
|
|
554
|
+
// Handle index files
|
|
555
|
+
if (name === "index") {
|
|
556
|
+
name = basename(dirname(filePath));
|
|
557
|
+
}
|
|
558
|
+
return name;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Convert extracted props to PropDefinition format for segments.json
|
|
563
|
+
*/
|
|
564
|
+
export function convertToSegmentProps(
|
|
565
|
+
props: ExtractedProp[]
|
|
566
|
+
): Record<string, PropDefinition> {
|
|
567
|
+
const result: Record<string, PropDefinition> = {};
|
|
568
|
+
|
|
569
|
+
for (const prop of props) {
|
|
570
|
+
const definition: PropDefinition = {
|
|
571
|
+
type: prop.propType.type,
|
|
572
|
+
description: prop.description,
|
|
573
|
+
required: prop.required,
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// Add values for enum types
|
|
577
|
+
if (prop.enumValues && prop.enumValues.length > 0) {
|
|
578
|
+
definition.values = prop.enumValues;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Add default value
|
|
582
|
+
if (prop.defaultValue !== undefined) {
|
|
583
|
+
definition.default = prop.defaultValue;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Add type details for complex types
|
|
587
|
+
if (prop.propType.type === "object" && "shape" in prop.propType) {
|
|
588
|
+
definition.typeDetails = { shape: prop.propType.shape };
|
|
589
|
+
}
|
|
590
|
+
if (prop.propType.type === "array" && "items" in prop.propType) {
|
|
591
|
+
definition.typeDetails = { items: prop.propType.items };
|
|
592
|
+
}
|
|
593
|
+
if (prop.propType.type === "function" && "signature" in prop.propType) {
|
|
594
|
+
definition.typeDetails = { signature: prop.propType.signature };
|
|
595
|
+
}
|
|
596
|
+
if (prop.propType.type === "union" && "types" in prop.propType) {
|
|
597
|
+
definition.typeDetails = { types: prop.propType.types };
|
|
598
|
+
}
|
|
599
|
+
if (prop.propType.type === "custom" && "typescript" in prop.propType) {
|
|
600
|
+
definition.typeDetails = { typescript: prop.propType.typescript };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
result[prop.name] = definition;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return result;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Find component source file and extract props
|
|
611
|
+
*/
|
|
612
|
+
export async function extractPropsForComponent(
|
|
613
|
+
componentName: string,
|
|
614
|
+
searchDirs: string[]
|
|
615
|
+
): Promise<PropsExtractionResult | null> {
|
|
616
|
+
const patterns = [
|
|
617
|
+
`${componentName}.tsx`,
|
|
618
|
+
`${componentName}.ts`,
|
|
619
|
+
`${componentName}/index.tsx`,
|
|
620
|
+
`${componentName}/index.ts`,
|
|
621
|
+
`${componentName}/${componentName}.tsx`,
|
|
622
|
+
`${componentName}/${componentName}.ts`,
|
|
623
|
+
];
|
|
624
|
+
|
|
625
|
+
for (const dir of searchDirs) {
|
|
626
|
+
for (const pattern of patterns) {
|
|
627
|
+
const fullPath = join(dir, pattern);
|
|
628
|
+
if (existsSync(fullPath)) {
|
|
629
|
+
return extractPropsFromFile(fullPath, {
|
|
630
|
+
propsTypeName: `${componentName}Props`,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Extract props from multiple component files
|
|
641
|
+
*/
|
|
642
|
+
export async function extractAllComponentProps(
|
|
643
|
+
componentFiles: Map<string, string>
|
|
644
|
+
): Promise<Map<string, PropsExtractionResult>> {
|
|
645
|
+
const results = new Map<string, PropsExtractionResult>();
|
|
646
|
+
|
|
647
|
+
for (const [componentName, filePath] of componentFiles) {
|
|
648
|
+
try {
|
|
649
|
+
const extraction = await extractPropsFromFile(filePath, {
|
|
650
|
+
propsTypeName: `${componentName}Props`,
|
|
651
|
+
});
|
|
652
|
+
results.set(componentName, extraction);
|
|
653
|
+
} catch (error) {
|
|
654
|
+
results.set(componentName, {
|
|
655
|
+
filePath,
|
|
656
|
+
componentName,
|
|
657
|
+
props: [],
|
|
658
|
+
success: false,
|
|
659
|
+
warnings: [`Extraction failed: ${(error as Error).message}`],
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return results;
|
|
665
|
+
}
|