@idealyst/tooling 1.2.23 → 1.2.25

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.
@@ -1,554 +0,0 @@
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
- SampleProps,
22
- } from './types';
23
- import { analyzeTheme } from './theme-analyzer';
24
-
25
- /**
26
- * Analyze components and generate a registry.
27
- */
28
- export function analyzeComponents(options: ComponentAnalyzerOptions): ComponentRegistry {
29
- const { componentPaths, themePath, include, exclude, includeInternal = false } = options;
30
-
31
- const registry: ComponentRegistry = {};
32
-
33
- // First, analyze the theme to get valid values
34
- const themeValues = analyzeTheme(themePath, false);
35
-
36
- // Scan each component path
37
- for (const componentPath of componentPaths) {
38
- const resolvedPath = path.resolve(componentPath);
39
-
40
- if (!fs.existsSync(resolvedPath)) {
41
- console.warn(`[component-analyzer] Path not found: ${resolvedPath}`);
42
- continue;
43
- }
44
-
45
- // Find all component directories (those with index.ts or types.ts)
46
- const componentDirs = findComponentDirs(resolvedPath);
47
-
48
- for (const dir of componentDirs) {
49
- const componentName = path.basename(dir);
50
-
51
- // Apply include/exclude filters
52
- if (include && !include.includes(componentName)) continue;
53
- if (exclude && exclude.includes(componentName)) continue;
54
- if (!includeInternal && componentName.startsWith('_')) continue;
55
-
56
- const definition = analyzeComponentDir(dir, componentName, themeValues);
57
- if (definition) {
58
- registry[componentName] = definition;
59
- }
60
- }
61
- }
62
-
63
- return registry;
64
- }
65
-
66
- /**
67
- * Find all component directories in a path.
68
- */
69
- function findComponentDirs(basePath: string): string[] {
70
- const dirs: string[] = [];
71
-
72
- const entries = fs.readdirSync(basePath, { withFileTypes: true });
73
- for (const entry of entries) {
74
- if (!entry.isDirectory()) continue;
75
-
76
- const dirPath = path.join(basePath, entry.name);
77
-
78
- // Check if it's a component directory (has index.ts or types.ts)
79
- const hasIndex = fs.existsSync(path.join(dirPath, 'index.ts'));
80
- const hasTypes = fs.existsSync(path.join(dirPath, 'types.ts'));
81
-
82
- if (hasIndex || hasTypes) {
83
- dirs.push(dirPath);
84
- }
85
- }
86
-
87
- return dirs;
88
- }
89
-
90
- /**
91
- * Analyze a single component directory.
92
- */
93
- function analyzeComponentDir(
94
- dir: string,
95
- componentName: string,
96
- themeValues: ThemeValues
97
- ): ComponentDefinition | null {
98
- // Find all TypeScript files in the component directory
99
- const tsFiles = fs.readdirSync(dir)
100
- .filter(f => f.endsWith('.ts') || f.endsWith('.tsx'))
101
- .map(f => path.join(dir, f));
102
-
103
- if (tsFiles.length === 0) {
104
- return null;
105
- }
106
-
107
- // Create TypeScript program with all files
108
- const program = ts.createProgram(tsFiles, {
109
- target: ts.ScriptTarget.ES2020,
110
- module: ts.ModuleKind.ESNext,
111
- jsx: ts.JsxEmit.React,
112
- strict: true,
113
- esModuleInterop: true,
114
- skipLibCheck: true,
115
- });
116
-
117
- const typeChecker = program.getTypeChecker();
118
-
119
- // Search all files for the props interface
120
- const propsInterfaceName = `${componentName}Props`;
121
- const altNames = [`${componentName}ComponentProps`, 'Props'];
122
- let propsInterface: ts.InterfaceDeclaration | ts.TypeAliasDeclaration | null = null;
123
- let interfaceDescription: string | undefined;
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
- }
136
- if (ts.isTypeAliasDeclaration(node) && node.name.text === propsInterfaceName) {
137
- propsInterface = node;
138
- interfaceDescription = getJSDocDescription(node);
139
- }
140
- });
141
-
142
- if (propsInterface) break;
143
- }
144
-
145
- // If not found, try alternate naming conventions
146
- if (!propsInterface) {
147
- for (const altName of altNames) {
148
- for (const filePath of tsFiles) {
149
- const sourceFile = program.getSourceFile(filePath);
150
- if (!sourceFile) continue;
151
-
152
- ts.forEachChild(sourceFile, (node) => {
153
- if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) && node.name.text === altName) {
154
- propsInterface = node;
155
- interfaceDescription = getJSDocDescription(node);
156
- }
157
- });
158
-
159
- if (propsInterface) break;
160
- }
161
- if (propsInterface) break;
162
- }
163
- }
164
-
165
- // If we couldn't find a props interface, skip this component
166
- if (!propsInterface) {
167
- return null;
168
- }
169
-
170
- // Extract props
171
- const props: Record<string, PropDefinition> = {};
172
-
173
- if (propsInterface) {
174
- const type = typeChecker.getTypeAtLocation(propsInterface);
175
- const properties = type.getProperties();
176
-
177
- for (const prop of properties) {
178
- const propDef = analyzeProperty(prop, typeChecker, themeValues);
179
- if (propDef && !isInternalProp(propDef.name)) {
180
- props[propDef.name] = propDef;
181
- }
182
- }
183
- }
184
-
185
- // Get description from the props interface JSDoc (single source of truth in types.ts)
186
- const description = interfaceDescription;
187
-
188
- // Determine category
189
- const category = inferCategory(componentName);
190
-
191
- // Look for docs.ts to extract sample props
192
- const sampleProps = extractSampleProps(dir);
193
-
194
- return {
195
- name: componentName,
196
- description,
197
- props,
198
- category,
199
- filePath: path.relative(process.cwd(), dir),
200
- sampleProps,
201
- };
202
- }
203
-
204
- /**
205
- * Extract sample props from docs.ts file if it exists.
206
- * The docs.ts file should export a `sampleProps` object.
207
- */
208
- function extractSampleProps(dir: string): SampleProps | undefined {
209
- const docsPath = path.join(dir, 'docs.ts');
210
-
211
- if (!fs.existsSync(docsPath)) {
212
- return undefined;
213
- }
214
-
215
- try {
216
- const content = fs.readFileSync(docsPath, 'utf-8');
217
-
218
- // Create a simple TypeScript program to extract the sampleProps export
219
- const sourceFile = ts.createSourceFile(
220
- 'docs.ts',
221
- content,
222
- ts.ScriptTarget.ES2020,
223
- true,
224
- ts.ScriptKind.TS
225
- );
226
-
227
- let samplePropsNode: ts.ObjectLiteralExpression | null = null;
228
-
229
- // Find the sampleProps export
230
- ts.forEachChild(sourceFile, (node) => {
231
- // Handle: export const sampleProps = { ... }
232
- if (ts.isVariableStatement(node)) {
233
- const isExported = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
234
- if (isExported) {
235
- for (const decl of node.declarationList.declarations) {
236
- if (ts.isIdentifier(decl.name) && decl.name.text === 'sampleProps' && decl.initializer) {
237
- if (ts.isObjectLiteralExpression(decl.initializer)) {
238
- samplePropsNode = decl.initializer;
239
- }
240
- }
241
- }
242
- }
243
- }
244
- });
245
-
246
- if (!samplePropsNode) {
247
- return undefined;
248
- }
249
-
250
- // Extract the object literal as JSON-compatible structure
251
- // This is a simplified extraction - it handles basic literals
252
- const result: SampleProps = {};
253
- const propsNode = samplePropsNode as ts.ObjectLiteralExpression;
254
-
255
- for (const prop of propsNode.properties) {
256
- if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
257
- const propName = prop.name.text;
258
-
259
- if (propName === 'props' && ts.isObjectLiteralExpression(prop.initializer)) {
260
- result.props = extractObjectLiteral(prop.initializer, content);
261
- } else if (propName === 'children') {
262
- // For children, we store the raw source text
263
- result.children = prop.initializer.getText(sourceFile);
264
- } else if (propName === 'state' && ts.isObjectLiteralExpression(prop.initializer)) {
265
- // Extract state configuration for controlled components
266
- result.state = extractObjectLiteral(prop.initializer, content);
267
- }
268
- }
269
- }
270
-
271
- return Object.keys(result).length > 0 ? result : undefined;
272
- } catch (e) {
273
- console.warn(`[component-analyzer] Error reading docs.ts in ${dir}:`, e);
274
- return undefined;
275
- }
276
- }
277
-
278
- /**
279
- * Extract an object literal to a plain object (for simple literal values).
280
- */
281
- function extractObjectLiteral(node: ts.ObjectLiteralExpression, sourceContent: string): Record<string, any> {
282
- const result: Record<string, any> = {};
283
-
284
- for (const prop of node.properties) {
285
- if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
286
- const key = prop.name.text;
287
- const init = prop.initializer;
288
-
289
- if (ts.isStringLiteral(init)) {
290
- result[key] = init.text;
291
- } else if (ts.isNumericLiteral(init)) {
292
- result[key] = Number(init.text);
293
- } else if (init.kind === ts.SyntaxKind.TrueKeyword) {
294
- result[key] = true;
295
- } else if (init.kind === ts.SyntaxKind.FalseKeyword) {
296
- result[key] = false;
297
- } else if (ts.isArrayLiteralExpression(init)) {
298
- result[key] = extractArrayLiteral(init, sourceContent);
299
- } else if (ts.isObjectLiteralExpression(init)) {
300
- result[key] = extractObjectLiteral(init, sourceContent);
301
- } else {
302
- // For complex expressions (JSX, functions), store the raw source
303
- result[key] = init.getText();
304
- }
305
- }
306
- }
307
-
308
- return result;
309
- }
310
-
311
- /**
312
- * Extract an array literal to a plain array.
313
- */
314
- function extractArrayLiteral(node: ts.ArrayLiteralExpression, sourceContent: string): any[] {
315
- const result: any[] = [];
316
-
317
- for (const element of node.elements) {
318
- if (ts.isStringLiteral(element)) {
319
- result.push(element.text);
320
- } else if (ts.isNumericLiteral(element)) {
321
- result.push(Number(element.text));
322
- } else if (element.kind === ts.SyntaxKind.TrueKeyword) {
323
- result.push(true);
324
- } else if (element.kind === ts.SyntaxKind.FalseKeyword) {
325
- result.push(false);
326
- } else if (ts.isObjectLiteralExpression(element)) {
327
- result.push(extractObjectLiteral(element, sourceContent));
328
- } else if (ts.isArrayLiteralExpression(element)) {
329
- result.push(extractArrayLiteral(element, sourceContent));
330
- } else {
331
- // For complex expressions, store raw source
332
- result.push(element.getText());
333
- }
334
- }
335
-
336
- return result;
337
- }
338
-
339
- /**
340
- * Analyze a single property symbol.
341
- */
342
- function analyzeProperty(
343
- symbol: ts.Symbol,
344
- typeChecker: ts.TypeChecker,
345
- themeValues: ThemeValues
346
- ): PropDefinition | null {
347
- const name = symbol.getName();
348
- const declarations = symbol.getDeclarations();
349
-
350
- if (!declarations || declarations.length === 0) return null;
351
-
352
- const declaration = declarations[0];
353
- const type = typeChecker.getTypeOfSymbolAtLocation(symbol, declaration);
354
- const typeString = typeChecker.typeToString(type);
355
-
356
- // Get JSDoc description
357
- const description = ts.displayPartsToString(symbol.getDocumentationComment(typeChecker)) || undefined;
358
-
359
- // Check if required
360
- const required = !(symbol.flags & ts.SymbolFlags.Optional);
361
-
362
- // Extract values for union types / theme types
363
- const values = extractPropValues(type, typeString, typeChecker, themeValues);
364
-
365
- // Extract default value (from JSDoc @default tag)
366
- const defaultValue = extractDefaultValue(symbol);
367
-
368
- return {
369
- name,
370
- type: simplifyTypeName(typeString),
371
- values: values.length > 0 ? values : undefined,
372
- default: defaultValue,
373
- description,
374
- required,
375
- };
376
- }
377
-
378
- /**
379
- * Extract valid values for a prop type.
380
- */
381
- function extractPropValues(
382
- type: ts.Type,
383
- typeString: string,
384
- _typeChecker: ts.TypeChecker,
385
- themeValues: ThemeValues
386
- ): string[] {
387
- // Handle theme-derived types
388
- if (typeString === 'Intent' || typeString.includes('Intent')) {
389
- return themeValues.intents;
390
- }
391
- if (typeString === 'Size' || typeString.includes('Size')) {
392
- // Return generic sizes - most components use the same keys
393
- return ['xs', 'sm', 'md', 'lg', 'xl'];
394
- }
395
-
396
- // Handle union types
397
- if (type.isUnion()) {
398
- const values: string[] = [];
399
- for (const unionType of type.types) {
400
- if (unionType.isStringLiteral()) {
401
- values.push(unionType.value);
402
- } else if ((unionType as any).intrinsicName === 'true') {
403
- values.push('true');
404
- } else if ((unionType as any).intrinsicName === 'false') {
405
- values.push('false');
406
- }
407
- }
408
- if (values.length > 0) return values;
409
- }
410
-
411
- // Handle boolean
412
- if (typeString === 'boolean') {
413
- return ['true', 'false'];
414
- }
415
-
416
- return [];
417
- }
418
-
419
- /**
420
- * Extract default value from JSDoc @default tag.
421
- */
422
- function extractDefaultValue(symbol: ts.Symbol): string | number | boolean | undefined {
423
- const tags = symbol.getJsDocTags();
424
- for (const tag of tags) {
425
- if (tag.name === 'default' && tag.text) {
426
- const value = ts.displayPartsToString(tag.text);
427
- // Try to parse as JSON
428
- try {
429
- return JSON.parse(value);
430
- } catch {
431
- return value;
432
- }
433
- }
434
- }
435
- return undefined;
436
- }
437
-
438
- /**
439
- * Get JSDoc description from a node.
440
- */
441
- function getJSDocDescription(node: ts.Node): string | undefined {
442
- const jsDocs = (node as any).jsDoc as ts.JSDoc[] | undefined;
443
- if (!jsDocs || jsDocs.length === 0) return undefined;
444
-
445
- const firstDoc = jsDocs[0];
446
- if (firstDoc.comment) {
447
- if (typeof firstDoc.comment === 'string') {
448
- return firstDoc.comment;
449
- }
450
- // Handle NodeArray of JSDocComment
451
- return (firstDoc.comment as ts.NodeArray<ts.JSDocComment>)
452
- .map(c => (c as any).text || '')
453
- .join('');
454
- }
455
- return undefined;
456
- }
457
-
458
- /**
459
- * Simplify type names for display.
460
- */
461
- function simplifyTypeName(typeString: string): string {
462
- // Remove import paths
463
- typeString = typeString.replace(/import\([^)]+\)\./g, '');
464
-
465
- // Simplify common complex types
466
- if (typeString.includes('ReactNode')) return 'ReactNode';
467
- if (typeString.includes('StyleProp')) return 'Style';
468
-
469
- return typeString;
470
- }
471
-
472
- /**
473
- * Check if a prop should be excluded (internal/inherited).
474
- */
475
- function isInternalProp(name: string): boolean {
476
- const internalProps = [
477
- 'ref',
478
- 'key',
479
- 'children',
480
- 'style',
481
- 'testID',
482
- 'nativeID',
483
- 'accessible',
484
- 'accessibilityActions',
485
- 'accessibilityComponentType',
486
- 'accessibilityElementsHidden',
487
- 'accessibilityHint',
488
- 'accessibilityIgnoresInvertColors',
489
- 'accessibilityLabel',
490
- 'accessibilityLabelledBy',
491
- 'accessibilityLanguage',
492
- 'accessibilityLiveRegion',
493
- 'accessibilityRole',
494
- 'accessibilityState',
495
- 'accessibilityTraits',
496
- 'accessibilityValue',
497
- 'accessibilityViewIsModal',
498
- 'collapsable',
499
- 'focusable',
500
- 'hasTVPreferredFocus',
501
- 'hitSlop',
502
- 'importantForAccessibility',
503
- 'needsOffscreenAlphaCompositing',
504
- 'onAccessibilityAction',
505
- 'onAccessibilityEscape',
506
- 'onAccessibilityTap',
507
- 'onLayout',
508
- 'onMagicTap',
509
- 'onMoveShouldSetResponder',
510
- 'onMoveShouldSetResponderCapture',
511
- 'onResponderEnd',
512
- 'onResponderGrant',
513
- 'onResponderMove',
514
- 'onResponderReject',
515
- 'onResponderRelease',
516
- 'onResponderStart',
517
- 'onResponderTerminate',
518
- 'onResponderTerminationRequest',
519
- 'onStartShouldSetResponder',
520
- 'onStartShouldSetResponderCapture',
521
- 'pointerEvents',
522
- 'removeClippedSubviews',
523
- 'renderToHardwareTextureAndroid',
524
- 'shouldRasterizeIOS',
525
- 'tvParallaxMagnification',
526
- 'tvParallaxProperties',
527
- 'tvParallaxShiftDistanceX',
528
- 'tvParallaxShiftDistanceY',
529
- 'tvParallaxTiltAngle',
530
- ];
531
-
532
- return internalProps.includes(name) || name.startsWith('accessibility');
533
- }
534
-
535
- /**
536
- * Infer component category from name.
537
- */
538
- function inferCategory(componentName: string): ComponentCategory {
539
- const formComponents = ['Button', 'Input', 'Checkbox', 'Select', 'Switch', 'RadioButton', 'Slider', 'TextArea'];
540
- const displayComponents = ['Text', 'Card', 'Badge', 'Chip', 'Avatar', 'Icon', 'Skeleton', 'Alert', 'Tooltip'];
541
- const layoutComponents = ['View', 'Screen', 'Divider'];
542
- const navigationComponents = ['TabBar', 'Breadcrumb', 'Menu', 'List', 'Link'];
543
- const overlayComponents = ['Dialog', 'Popover', 'Modal'];
544
- const dataComponents = ['Table', 'Progress', 'Accordion'];
545
-
546
- if (formComponents.includes(componentName)) return 'form';
547
- if (displayComponents.includes(componentName)) return 'display';
548
- if (layoutComponents.includes(componentName)) return 'layout';
549
- if (navigationComponents.includes(componentName)) return 'navigation';
550
- if (overlayComponents.includes(componentName)) return 'overlay';
551
- if (dataComponents.includes(componentName)) return 'data';
552
-
553
- return 'display'; // Default
554
- }
@@ -1,16 +0,0 @@
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';