@idealyst/tooling 1.2.3
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/README.md +185 -0
- package/package.json +89 -0
- package/src/analyzer/component-analyzer.ts +418 -0
- package/src/analyzer/index.ts +16 -0
- package/src/analyzer/theme-analyzer.ts +473 -0
- package/src/analyzer/types.ts +132 -0
- package/src/analyzers/index.ts +1 -0
- package/src/analyzers/platformImports.ts +395 -0
- package/src/index.ts +142 -0
- package/src/rules/index.ts +2 -0
- package/src/rules/reactDomPrimitives.ts +217 -0
- package/src/rules/reactNativePrimitives.ts +363 -0
- package/src/types.ts +173 -0
- package/src/utils/fileClassifier.ts +135 -0
- package/src/utils/importParser.ts +235 -0
- package/src/utils/index.ts +2 -0
- package/src/vite-plugin.ts +264 -0
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# @idealyst/tooling
|
|
2
|
+
|
|
3
|
+
Code analysis and validation utilities for Idealyst Framework. Provides shared tools for babel plugins, CLI, and MCP to validate cross-platform React/React Native code.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn add @idealyst/tooling
|
|
9
|
+
# or
|
|
10
|
+
npm install @idealyst/tooling
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Platform Import Analyzer**: Validates that component files don't use platform-specific primitives unless appropriately suffixed
|
|
16
|
+
- **File Classifier**: Classifies files by their extension (.web.tsx, .native.tsx, etc.)
|
|
17
|
+
- **Import Parser**: Parses TypeScript/JavaScript imports using the TypeScript compiler API
|
|
18
|
+
- **Primitive Rules**: Built-in lists of React Native and React DOM primitives
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Platform Import Analysis
|
|
23
|
+
|
|
24
|
+
The main feature is validating that shared component files don't accidentally use platform-specific imports:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { analyzePlatformImports, analyzeFiles } from '@idealyst/tooling';
|
|
28
|
+
|
|
29
|
+
// Single file analysis
|
|
30
|
+
const result = analyzePlatformImports(
|
|
31
|
+
'src/components/Button.tsx',
|
|
32
|
+
sourceCode,
|
|
33
|
+
{ severity: 'error' }
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (!result.passed) {
|
|
37
|
+
for (const violation of result.violations) {
|
|
38
|
+
console.error(`${violation.filePath}:${violation.line}:${violation.column}`);
|
|
39
|
+
console.error(` ${violation.message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Batch analysis
|
|
44
|
+
const results = analyzeFiles(
|
|
45
|
+
[
|
|
46
|
+
{ path: 'Button.tsx', content: buttonSource },
|
|
47
|
+
{ path: 'Button.web.tsx', content: webSource },
|
|
48
|
+
{ path: 'Button.native.tsx', content: nativeSource },
|
|
49
|
+
],
|
|
50
|
+
{
|
|
51
|
+
severity: 'warning',
|
|
52
|
+
ignoredPatterns: ['**/*.test.tsx'],
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const failedFiles = results.filter(r => !r.passed);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### File Classification
|
|
60
|
+
|
|
61
|
+
Classify files based on their extension patterns:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { classifyFile, isSharedFile } from '@idealyst/tooling';
|
|
65
|
+
|
|
66
|
+
classifyFile('Button.tsx'); // 'shared'
|
|
67
|
+
classifyFile('Button.web.tsx'); // 'web'
|
|
68
|
+
classifyFile('Button.native.tsx'); // 'native'
|
|
69
|
+
classifyFile('Button.styles.tsx'); // 'styles'
|
|
70
|
+
classifyFile('types.ts'); // 'types'
|
|
71
|
+
|
|
72
|
+
// Check if a file should be validated for cross-platform imports
|
|
73
|
+
if (isSharedFile(filePath)) {
|
|
74
|
+
// This file should NOT have platform-specific imports
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Import Parsing
|
|
79
|
+
|
|
80
|
+
Parse imports from source code:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { parseImports } from '@idealyst/tooling';
|
|
84
|
+
|
|
85
|
+
const imports = parseImports(sourceCode, 'Button.tsx');
|
|
86
|
+
|
|
87
|
+
for (const imp of imports) {
|
|
88
|
+
console.log(`${imp.name} from '${imp.source}' (${imp.platform})`);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Primitive Rules
|
|
93
|
+
|
|
94
|
+
Access built-in primitive lists:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import {
|
|
98
|
+
REACT_NATIVE_PRIMITIVES,
|
|
99
|
+
REACT_DOM_PRIMITIVES,
|
|
100
|
+
isReactNativePrimitive,
|
|
101
|
+
isReactDomPrimitive,
|
|
102
|
+
} from '@idealyst/tooling';
|
|
103
|
+
|
|
104
|
+
// Check if a name is a known primitive
|
|
105
|
+
isReactNativePrimitive('View'); // true
|
|
106
|
+
isReactNativePrimitive('div'); // false
|
|
107
|
+
isReactDomPrimitive('createPortal'); // true
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## API Reference
|
|
111
|
+
|
|
112
|
+
### `analyzePlatformImports(filePath, sourceCode, options?)`
|
|
113
|
+
|
|
114
|
+
Analyze a single file for platform import violations.
|
|
115
|
+
|
|
116
|
+
**Options:**
|
|
117
|
+
- `severity`: Default severity level (`'error'` | `'warning'` | `'info'`)
|
|
118
|
+
- `additionalNativePrimitives`: Extra primitives to flag as React Native
|
|
119
|
+
- `additionalDomPrimitives`: Extra primitives to flag as React DOM
|
|
120
|
+
- `ignoredPrimitives`: Primitives to skip validation for
|
|
121
|
+
- `ignoredPatterns`: Glob patterns for files to skip
|
|
122
|
+
- `additionalNativeSources`: Extra module sources treated as React Native
|
|
123
|
+
- `additionalDomSources`: Extra module sources treated as React DOM
|
|
124
|
+
|
|
125
|
+
**Returns:** `AnalysisResult` with violations and metadata.
|
|
126
|
+
|
|
127
|
+
### `analyzeFiles(files, options?)`
|
|
128
|
+
|
|
129
|
+
Analyze multiple files at once.
|
|
130
|
+
|
|
131
|
+
### `classifyFile(filePath)`
|
|
132
|
+
|
|
133
|
+
Classify a file by its extension pattern.
|
|
134
|
+
|
|
135
|
+
**Returns:** `'shared'` | `'web'` | `'native'` | `'styles'` | `'types'` | `'other'`
|
|
136
|
+
|
|
137
|
+
### `parseImports(sourceCode, filePath?, options?)`
|
|
138
|
+
|
|
139
|
+
Parse all imports from source code.
|
|
140
|
+
|
|
141
|
+
**Returns:** Array of `ImportInfo` objects.
|
|
142
|
+
|
|
143
|
+
## Validation Rules
|
|
144
|
+
|
|
145
|
+
### Shared Files (`.tsx`, `.jsx`)
|
|
146
|
+
|
|
147
|
+
Should NOT import:
|
|
148
|
+
- React Native primitives (`View`, `Text`, `TouchableOpacity`, etc.)
|
|
149
|
+
- React DOM APIs (`createPortal`, `flushSync`, etc.)
|
|
150
|
+
|
|
151
|
+
### Web Files (`.web.tsx`, `.web.jsx`)
|
|
152
|
+
|
|
153
|
+
Should NOT import:
|
|
154
|
+
- React Native primitives
|
|
155
|
+
|
|
156
|
+
### Native Files (`.native.tsx`, `.native.jsx`)
|
|
157
|
+
|
|
158
|
+
Should NOT import:
|
|
159
|
+
- React DOM APIs
|
|
160
|
+
|
|
161
|
+
## Built-in Primitives
|
|
162
|
+
|
|
163
|
+
### React Native
|
|
164
|
+
|
|
165
|
+
Core: `View`, `Text`, `Image`, `ScrollView`, `FlatList`, `SectionList`
|
|
166
|
+
|
|
167
|
+
Interactive: `TouchableOpacity`, `TouchableHighlight`, `Pressable`, `Button`
|
|
168
|
+
|
|
169
|
+
Input: `TextInput`, `Switch`
|
|
170
|
+
|
|
171
|
+
Modal: `Modal`, `Alert`, `StatusBar`
|
|
172
|
+
|
|
173
|
+
Animation: `Animated`, `Easing`, `LayoutAnimation`
|
|
174
|
+
|
|
175
|
+
Platform: `Platform`, `Dimensions`, `BackHandler`, `Keyboard`
|
|
176
|
+
|
|
177
|
+
Safety: `SafeAreaView`, `KeyboardAvoidingView`
|
|
178
|
+
|
|
179
|
+
### React DOM
|
|
180
|
+
|
|
181
|
+
APIs: `createPortal`, `flushSync`, `createRoot`, `hydrateRoot`
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@idealyst/tooling",
|
|
3
|
+
"version": "1.2.3",
|
|
4
|
+
"description": "Code analysis and validation utilities for Idealyst Framework",
|
|
5
|
+
"readme": "README.md",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"module": "src/index.ts",
|
|
8
|
+
"types": "src/index.ts",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/IdealystIO/idealyst-framework.git",
|
|
12
|
+
"directory": "packages/tooling"
|
|
13
|
+
},
|
|
14
|
+
"author": "Your Name <your.email@example.com>",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./src/index.ts",
|
|
22
|
+
"require": "./src/index.ts",
|
|
23
|
+
"types": "./src/index.ts"
|
|
24
|
+
},
|
|
25
|
+
"./vite": {
|
|
26
|
+
"import": "./src/vite-plugin.ts",
|
|
27
|
+
"require": "./src/vite-plugin.ts",
|
|
28
|
+
"types": "./src/vite-plugin.ts"
|
|
29
|
+
},
|
|
30
|
+
"./docs": {
|
|
31
|
+
"import": "./src/analyzer/index.ts",
|
|
32
|
+
"require": "./src/analyzer/index.ts",
|
|
33
|
+
"types": "./src/analyzer/index.ts"
|
|
34
|
+
},
|
|
35
|
+
"./analyzers": {
|
|
36
|
+
"import": "./src/analyzers/index.ts",
|
|
37
|
+
"require": "./src/analyzers/index.ts",
|
|
38
|
+
"types": "./src/analyzers/index.ts"
|
|
39
|
+
},
|
|
40
|
+
"./rules": {
|
|
41
|
+
"import": "./src/rules/index.ts",
|
|
42
|
+
"require": "./src/rules/index.ts",
|
|
43
|
+
"types": "./src/rules/index.ts"
|
|
44
|
+
},
|
|
45
|
+
"./utils": {
|
|
46
|
+
"import": "./src/utils/index.ts",
|
|
47
|
+
"require": "./src/utils/index.ts",
|
|
48
|
+
"types": "./src/utils/index.ts"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"prepublishOnly": "echo 'Publishing TypeScript source directly'",
|
|
53
|
+
"publish:npm": "npm publish",
|
|
54
|
+
"test": "jest",
|
|
55
|
+
"typecheck": "tsc --noEmit"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"typescript": "^5.0.0"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"vite": ">=5.0.0"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"vite": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/jest": "^29.0.0",
|
|
70
|
+
"@types/node": "^20.0.0",
|
|
71
|
+
"jest": "^29.0.0",
|
|
72
|
+
"ts-jest": "^29.0.0",
|
|
73
|
+
"tsx": "^4.21.0",
|
|
74
|
+
"vite": "^7.3.1"
|
|
75
|
+
},
|
|
76
|
+
"files": [
|
|
77
|
+
"src",
|
|
78
|
+
"README.md"
|
|
79
|
+
],
|
|
80
|
+
"keywords": [
|
|
81
|
+
"tooling",
|
|
82
|
+
"code-analysis",
|
|
83
|
+
"validation",
|
|
84
|
+
"react",
|
|
85
|
+
"react-native",
|
|
86
|
+
"cross-platform",
|
|
87
|
+
"idealyst"
|
|
88
|
+
]
|
|
89
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Analyzer - Extracts component prop definitions using TypeScript Compiler API.
|
|
3
|
+
*
|
|
4
|
+
* Analyzes:
|
|
5
|
+
* - types.ts files for prop interfaces
|
|
6
|
+
* - JSDoc comments for descriptions
|
|
7
|
+
* - Static .description properties on components
|
|
8
|
+
* - Theme-derived types (Intent, Size) resolved to actual values
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as ts from 'typescript';
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import type {
|
|
15
|
+
ComponentRegistry,
|
|
16
|
+
ComponentDefinition,
|
|
17
|
+
PropDefinition,
|
|
18
|
+
ComponentAnalyzerOptions,
|
|
19
|
+
ThemeValues,
|
|
20
|
+
ComponentCategory,
|
|
21
|
+
} from './types';
|
|
22
|
+
import { analyzeTheme } from './theme-analyzer';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Analyze components and generate a registry.
|
|
26
|
+
*/
|
|
27
|
+
export function analyzeComponents(options: ComponentAnalyzerOptions): ComponentRegistry {
|
|
28
|
+
const { componentPaths, themePath, include, exclude, includeInternal = false } = options;
|
|
29
|
+
|
|
30
|
+
const registry: ComponentRegistry = {};
|
|
31
|
+
|
|
32
|
+
// First, analyze the theme to get valid values
|
|
33
|
+
const themeValues = analyzeTheme(themePath, false);
|
|
34
|
+
|
|
35
|
+
// Scan each component path
|
|
36
|
+
for (const componentPath of componentPaths) {
|
|
37
|
+
const resolvedPath = path.resolve(componentPath);
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
40
|
+
console.warn(`[component-analyzer] Path not found: ${resolvedPath}`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Find all component directories (those with index.ts or types.ts)
|
|
45
|
+
const componentDirs = findComponentDirs(resolvedPath);
|
|
46
|
+
|
|
47
|
+
for (const dir of componentDirs) {
|
|
48
|
+
const componentName = path.basename(dir);
|
|
49
|
+
|
|
50
|
+
// Apply include/exclude filters
|
|
51
|
+
if (include && !include.includes(componentName)) continue;
|
|
52
|
+
if (exclude && exclude.includes(componentName)) continue;
|
|
53
|
+
if (!includeInternal && componentName.startsWith('_')) continue;
|
|
54
|
+
|
|
55
|
+
const definition = analyzeComponentDir(dir, componentName, themeValues);
|
|
56
|
+
if (definition) {
|
|
57
|
+
registry[componentName] = definition;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return registry;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Find all component directories in a path.
|
|
67
|
+
*/
|
|
68
|
+
function findComponentDirs(basePath: string): string[] {
|
|
69
|
+
const dirs: string[] = [];
|
|
70
|
+
|
|
71
|
+
const entries = fs.readdirSync(basePath, { withFileTypes: true });
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (!entry.isDirectory()) continue;
|
|
74
|
+
|
|
75
|
+
const dirPath = path.join(basePath, entry.name);
|
|
76
|
+
|
|
77
|
+
// Check if it's a component directory (has index.ts or types.ts)
|
|
78
|
+
const hasIndex = fs.existsSync(path.join(dirPath, 'index.ts'));
|
|
79
|
+
const hasTypes = fs.existsSync(path.join(dirPath, 'types.ts'));
|
|
80
|
+
|
|
81
|
+
if (hasIndex || hasTypes) {
|
|
82
|
+
dirs.push(dirPath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return dirs;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Analyze a single component directory.
|
|
91
|
+
*/
|
|
92
|
+
function analyzeComponentDir(
|
|
93
|
+
dir: string,
|
|
94
|
+
componentName: string,
|
|
95
|
+
themeValues: ThemeValues
|
|
96
|
+
): ComponentDefinition | null {
|
|
97
|
+
// Find all TypeScript files in the component directory
|
|
98
|
+
const tsFiles = fs.readdirSync(dir)
|
|
99
|
+
.filter(f => f.endsWith('.ts') || f.endsWith('.tsx'))
|
|
100
|
+
.map(f => path.join(dir, f));
|
|
101
|
+
|
|
102
|
+
if (tsFiles.length === 0) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Create TypeScript program with all files
|
|
107
|
+
const program = ts.createProgram(tsFiles, {
|
|
108
|
+
target: ts.ScriptTarget.ES2020,
|
|
109
|
+
module: ts.ModuleKind.ESNext,
|
|
110
|
+
jsx: ts.JsxEmit.React,
|
|
111
|
+
strict: true,
|
|
112
|
+
esModuleInterop: true,
|
|
113
|
+
skipLibCheck: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const typeChecker = program.getTypeChecker();
|
|
117
|
+
|
|
118
|
+
// Search all files for the props interface
|
|
119
|
+
const propsInterfaceName = `${componentName}Props`;
|
|
120
|
+
const altNames = [`${componentName}ComponentProps`, 'Props'];
|
|
121
|
+
let propsInterface: ts.InterfaceDeclaration | ts.TypeAliasDeclaration | null = null;
|
|
122
|
+
let interfaceDescription: string | undefined;
|
|
123
|
+
let foundInFile: ts.SourceFile | null = null;
|
|
124
|
+
|
|
125
|
+
// Search each file for the props interface
|
|
126
|
+
for (const filePath of tsFiles) {
|
|
127
|
+
const sourceFile = program.getSourceFile(filePath);
|
|
128
|
+
if (!sourceFile) continue;
|
|
129
|
+
|
|
130
|
+
// First try the main props interface name
|
|
131
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
132
|
+
if (ts.isInterfaceDeclaration(node) && node.name.text === propsInterfaceName) {
|
|
133
|
+
propsInterface = node;
|
|
134
|
+
interfaceDescription = getJSDocDescription(node);
|
|
135
|
+
foundInFile = sourceFile;
|
|
136
|
+
}
|
|
137
|
+
if (ts.isTypeAliasDeclaration(node) && node.name.text === propsInterfaceName) {
|
|
138
|
+
propsInterface = node;
|
|
139
|
+
interfaceDescription = getJSDocDescription(node);
|
|
140
|
+
foundInFile = sourceFile;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (propsInterface) break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// If not found, try alternate naming conventions
|
|
148
|
+
if (!propsInterface) {
|
|
149
|
+
for (const altName of altNames) {
|
|
150
|
+
for (const filePath of tsFiles) {
|
|
151
|
+
const sourceFile = program.getSourceFile(filePath);
|
|
152
|
+
if (!sourceFile) continue;
|
|
153
|
+
|
|
154
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
155
|
+
if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) && node.name.text === altName) {
|
|
156
|
+
propsInterface = node;
|
|
157
|
+
interfaceDescription = getJSDocDescription(node);
|
|
158
|
+
foundInFile = sourceFile;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (propsInterface) break;
|
|
163
|
+
}
|
|
164
|
+
if (propsInterface) break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// If we couldn't find a props interface, skip this component
|
|
169
|
+
if (!propsInterface) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Extract props
|
|
174
|
+
const props: Record<string, PropDefinition> = {};
|
|
175
|
+
|
|
176
|
+
if (propsInterface) {
|
|
177
|
+
const type = typeChecker.getTypeAtLocation(propsInterface);
|
|
178
|
+
const properties = type.getProperties();
|
|
179
|
+
|
|
180
|
+
for (const prop of properties) {
|
|
181
|
+
const propDef = analyzeProperty(prop, typeChecker, themeValues);
|
|
182
|
+
if (propDef && !isInternalProp(propDef.name)) {
|
|
183
|
+
props[propDef.name] = propDef;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Get description from the props interface JSDoc (single source of truth in types.ts)
|
|
189
|
+
const description = interfaceDescription;
|
|
190
|
+
|
|
191
|
+
// Determine category
|
|
192
|
+
const category = inferCategory(componentName);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
name: componentName,
|
|
196
|
+
description,
|
|
197
|
+
props,
|
|
198
|
+
category,
|
|
199
|
+
filePath: path.relative(process.cwd(), dir),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Analyze a single property symbol.
|
|
205
|
+
*/
|
|
206
|
+
function analyzeProperty(
|
|
207
|
+
symbol: ts.Symbol,
|
|
208
|
+
typeChecker: ts.TypeChecker,
|
|
209
|
+
themeValues: ThemeValues
|
|
210
|
+
): PropDefinition | null {
|
|
211
|
+
const name = symbol.getName();
|
|
212
|
+
const declarations = symbol.getDeclarations();
|
|
213
|
+
|
|
214
|
+
if (!declarations || declarations.length === 0) return null;
|
|
215
|
+
|
|
216
|
+
const declaration = declarations[0];
|
|
217
|
+
const type = typeChecker.getTypeOfSymbolAtLocation(symbol, declaration);
|
|
218
|
+
const typeString = typeChecker.typeToString(type);
|
|
219
|
+
|
|
220
|
+
// Get JSDoc description
|
|
221
|
+
const description = ts.displayPartsToString(symbol.getDocumentationComment(typeChecker)) || undefined;
|
|
222
|
+
|
|
223
|
+
// Check if required
|
|
224
|
+
const required = !(symbol.flags & ts.SymbolFlags.Optional);
|
|
225
|
+
|
|
226
|
+
// Extract values for union types / theme types
|
|
227
|
+
const values = extractPropValues(type, typeString, typeChecker, themeValues);
|
|
228
|
+
|
|
229
|
+
// Extract default value (from JSDoc @default tag)
|
|
230
|
+
const defaultValue = extractDefaultValue(symbol);
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
name,
|
|
234
|
+
type: simplifyTypeName(typeString),
|
|
235
|
+
values: values.length > 0 ? values : undefined,
|
|
236
|
+
default: defaultValue,
|
|
237
|
+
description,
|
|
238
|
+
required,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Extract valid values for a prop type.
|
|
244
|
+
*/
|
|
245
|
+
function extractPropValues(
|
|
246
|
+
type: ts.Type,
|
|
247
|
+
typeString: string,
|
|
248
|
+
typeChecker: ts.TypeChecker,
|
|
249
|
+
themeValues: ThemeValues
|
|
250
|
+
): string[] {
|
|
251
|
+
// Handle theme-derived types
|
|
252
|
+
if (typeString === 'Intent' || typeString.includes('Intent')) {
|
|
253
|
+
return themeValues.intents;
|
|
254
|
+
}
|
|
255
|
+
if (typeString === 'Size' || typeString.includes('Size')) {
|
|
256
|
+
// Return generic sizes - most components use the same keys
|
|
257
|
+
return ['xs', 'sm', 'md', 'lg', 'xl'];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Handle union types
|
|
261
|
+
if (type.isUnion()) {
|
|
262
|
+
const values: string[] = [];
|
|
263
|
+
for (const unionType of type.types) {
|
|
264
|
+
if (unionType.isStringLiteral()) {
|
|
265
|
+
values.push(unionType.value);
|
|
266
|
+
} else if ((unionType as any).intrinsicName === 'true') {
|
|
267
|
+
values.push('true');
|
|
268
|
+
} else if ((unionType as any).intrinsicName === 'false') {
|
|
269
|
+
values.push('false');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (values.length > 0) return values;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Handle boolean
|
|
276
|
+
if (typeString === 'boolean') {
|
|
277
|
+
return ['true', 'false'];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Extract default value from JSDoc @default tag.
|
|
285
|
+
*/
|
|
286
|
+
function extractDefaultValue(symbol: ts.Symbol): string | number | boolean | undefined {
|
|
287
|
+
const tags = symbol.getJsDocTags();
|
|
288
|
+
for (const tag of tags) {
|
|
289
|
+
if (tag.name === 'default' && tag.text) {
|
|
290
|
+
const value = ts.displayPartsToString(tag.text);
|
|
291
|
+
// Try to parse as JSON
|
|
292
|
+
try {
|
|
293
|
+
return JSON.parse(value);
|
|
294
|
+
} catch {
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get JSDoc description from a node.
|
|
304
|
+
*/
|
|
305
|
+
function getJSDocDescription(node: ts.Node): string | undefined {
|
|
306
|
+
const jsDocs = (node as any).jsDoc as ts.JSDoc[] | undefined;
|
|
307
|
+
if (!jsDocs || jsDocs.length === 0) return undefined;
|
|
308
|
+
|
|
309
|
+
const firstDoc = jsDocs[0];
|
|
310
|
+
if (firstDoc.comment) {
|
|
311
|
+
if (typeof firstDoc.comment === 'string') {
|
|
312
|
+
return firstDoc.comment;
|
|
313
|
+
}
|
|
314
|
+
// Handle NodeArray of JSDocComment
|
|
315
|
+
return (firstDoc.comment as ts.NodeArray<ts.JSDocComment>)
|
|
316
|
+
.map(c => (c as any).text || '')
|
|
317
|
+
.join('');
|
|
318
|
+
}
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Simplify type names for display.
|
|
324
|
+
*/
|
|
325
|
+
function simplifyTypeName(typeString: string): string {
|
|
326
|
+
// Remove import paths
|
|
327
|
+
typeString = typeString.replace(/import\([^)]+\)\./g, '');
|
|
328
|
+
|
|
329
|
+
// Simplify common complex types
|
|
330
|
+
if (typeString.includes('ReactNode')) return 'ReactNode';
|
|
331
|
+
if (typeString.includes('StyleProp')) return 'Style';
|
|
332
|
+
|
|
333
|
+
return typeString;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Check if a prop should be excluded (internal/inherited).
|
|
338
|
+
*/
|
|
339
|
+
function isInternalProp(name: string): boolean {
|
|
340
|
+
const internalProps = [
|
|
341
|
+
'ref',
|
|
342
|
+
'key',
|
|
343
|
+
'children',
|
|
344
|
+
'style',
|
|
345
|
+
'testID',
|
|
346
|
+
'nativeID',
|
|
347
|
+
'accessible',
|
|
348
|
+
'accessibilityActions',
|
|
349
|
+
'accessibilityComponentType',
|
|
350
|
+
'accessibilityElementsHidden',
|
|
351
|
+
'accessibilityHint',
|
|
352
|
+
'accessibilityIgnoresInvertColors',
|
|
353
|
+
'accessibilityLabel',
|
|
354
|
+
'accessibilityLabelledBy',
|
|
355
|
+
'accessibilityLanguage',
|
|
356
|
+
'accessibilityLiveRegion',
|
|
357
|
+
'accessibilityRole',
|
|
358
|
+
'accessibilityState',
|
|
359
|
+
'accessibilityTraits',
|
|
360
|
+
'accessibilityValue',
|
|
361
|
+
'accessibilityViewIsModal',
|
|
362
|
+
'collapsable',
|
|
363
|
+
'focusable',
|
|
364
|
+
'hasTVPreferredFocus',
|
|
365
|
+
'hitSlop',
|
|
366
|
+
'importantForAccessibility',
|
|
367
|
+
'needsOffscreenAlphaCompositing',
|
|
368
|
+
'onAccessibilityAction',
|
|
369
|
+
'onAccessibilityEscape',
|
|
370
|
+
'onAccessibilityTap',
|
|
371
|
+
'onLayout',
|
|
372
|
+
'onMagicTap',
|
|
373
|
+
'onMoveShouldSetResponder',
|
|
374
|
+
'onMoveShouldSetResponderCapture',
|
|
375
|
+
'onResponderEnd',
|
|
376
|
+
'onResponderGrant',
|
|
377
|
+
'onResponderMove',
|
|
378
|
+
'onResponderReject',
|
|
379
|
+
'onResponderRelease',
|
|
380
|
+
'onResponderStart',
|
|
381
|
+
'onResponderTerminate',
|
|
382
|
+
'onResponderTerminationRequest',
|
|
383
|
+
'onStartShouldSetResponder',
|
|
384
|
+
'onStartShouldSetResponderCapture',
|
|
385
|
+
'pointerEvents',
|
|
386
|
+
'removeClippedSubviews',
|
|
387
|
+
'renderToHardwareTextureAndroid',
|
|
388
|
+
'shouldRasterizeIOS',
|
|
389
|
+
'tvParallaxMagnification',
|
|
390
|
+
'tvParallaxProperties',
|
|
391
|
+
'tvParallaxShiftDistanceX',
|
|
392
|
+
'tvParallaxShiftDistanceY',
|
|
393
|
+
'tvParallaxTiltAngle',
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
return internalProps.includes(name) || name.startsWith('accessibility');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Infer component category from name.
|
|
401
|
+
*/
|
|
402
|
+
function inferCategory(componentName: string): ComponentCategory {
|
|
403
|
+
const formComponents = ['Button', 'Input', 'Checkbox', 'Select', 'Switch', 'RadioButton', 'Slider', 'TextArea'];
|
|
404
|
+
const displayComponents = ['Text', 'Card', 'Badge', 'Chip', 'Avatar', 'Icon', 'Skeleton', 'Alert', 'Tooltip'];
|
|
405
|
+
const layoutComponents = ['View', 'Screen', 'Divider'];
|
|
406
|
+
const navigationComponents = ['TabBar', 'Breadcrumb', 'Menu', 'List', 'Link'];
|
|
407
|
+
const overlayComponents = ['Dialog', 'Popover', 'Modal'];
|
|
408
|
+
const dataComponents = ['Table', 'Progress', 'Accordion'];
|
|
409
|
+
|
|
410
|
+
if (formComponents.includes(componentName)) return 'form';
|
|
411
|
+
if (displayComponents.includes(componentName)) return 'display';
|
|
412
|
+
if (layoutComponents.includes(componentName)) return 'layout';
|
|
413
|
+
if (navigationComponents.includes(componentName)) return 'navigation';
|
|
414
|
+
if (overlayComponents.includes(componentName)) return 'overlay';
|
|
415
|
+
if (dataComponents.includes(componentName)) return 'data';
|
|
416
|
+
|
|
417
|
+
return 'display'; // Default
|
|
418
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component and Theme Analyzers
|
|
3
|
+
*
|
|
4
|
+
* Tools for extracting component metadata from TypeScript source code.
|
|
5
|
+
* Used by both the Vite plugin (for docs generation) and MCP server (for IDE assistance).
|
|
6
|
+
* Also used by the Babel plugin for $iterator expansion.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { analyzeComponents } from './component-analyzer';
|
|
10
|
+
export {
|
|
11
|
+
analyzeTheme,
|
|
12
|
+
loadThemeKeys,
|
|
13
|
+
resetThemeCache,
|
|
14
|
+
type BabelThemeKeys,
|
|
15
|
+
} from './theme-analyzer';
|
|
16
|
+
export * from './types';
|