@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.
@@ -0,0 +1,473 @@
1
+ /**
2
+ * Theme Analyzer - Extracts theme keys by statically analyzing theme files.
3
+ *
4
+ * Uses TypeScript Compiler API to trace the declarative builder API:
5
+ * - createTheme() / fromTheme(base)
6
+ * - .addIntent('name', {...})
7
+ * - .addRadius('name', value)
8
+ * - .addShadow('name', {...})
9
+ * - .setSizes({ button: { xs: {}, sm: {}, ... }, ... })
10
+ * - .build()
11
+ */
12
+
13
+ import * as ts from 'typescript';
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import type { ThemeValues } from './types';
17
+
18
+ interface AnalyzerContext {
19
+ program: ts.Program;
20
+ typeChecker: ts.TypeChecker;
21
+ verbose: boolean;
22
+ }
23
+
24
+ /**
25
+ * Extract theme values from a theme file.
26
+ */
27
+ export function analyzeTheme(themePath: string, verbose = false): ThemeValues {
28
+ const resolvedPath = path.resolve(themePath);
29
+
30
+ if (!fs.existsSync(resolvedPath)) {
31
+ throw new Error(`Theme file not found: ${resolvedPath}`);
32
+ }
33
+
34
+ const log = (...args: any[]) => {
35
+ if (verbose) console.log('[theme-analyzer]', ...args);
36
+ };
37
+
38
+ log('Analyzing theme file:', resolvedPath);
39
+
40
+ // Create a TypeScript program
41
+ const program = ts.createProgram([resolvedPath], {
42
+ target: ts.ScriptTarget.ES2020,
43
+ module: ts.ModuleKind.ESNext,
44
+ strict: true,
45
+ esModuleInterop: true,
46
+ skipLibCheck: true,
47
+ allowSyntheticDefaultImports: true,
48
+ });
49
+
50
+ const sourceFile = program.getSourceFile(resolvedPath);
51
+ if (!sourceFile) {
52
+ throw new Error(`Failed to parse theme file: ${resolvedPath}`);
53
+ }
54
+
55
+ const ctx: AnalyzerContext = {
56
+ program,
57
+ typeChecker: program.getTypeChecker(),
58
+ verbose,
59
+ };
60
+
61
+ const values: ThemeValues = {
62
+ intents: [],
63
+ sizes: {},
64
+ radii: [],
65
+ shadows: [],
66
+ breakpoints: [],
67
+ typography: [],
68
+ surfaceColors: [],
69
+ textColors: [],
70
+ borderColors: [],
71
+ };
72
+
73
+ // Track imports for base theme resolution
74
+ const imports = new Map<string, { source: string; imported: string }>();
75
+
76
+ // First pass: collect imports
77
+ ts.forEachChild(sourceFile, (node) => {
78
+ if (ts.isImportDeclaration(node)) {
79
+ const source = (node.moduleSpecifier as ts.StringLiteral).text;
80
+ const clause = node.importClause;
81
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
82
+ for (const element of clause.namedBindings.elements) {
83
+ const localName = element.name.text;
84
+ const importedName = element.propertyName?.text ?? localName;
85
+ imports.set(localName, { source, imported: importedName });
86
+ }
87
+ }
88
+ }
89
+ });
90
+
91
+ /**
92
+ * Process a builder method call chain.
93
+ */
94
+ function processBuilderChain(node: ts.Node): void {
95
+ if (!ts.isCallExpression(node)) return;
96
+
97
+ // Check if this is a .build() call
98
+ if (ts.isPropertyAccessExpression(node.expression)) {
99
+ const methodName = node.expression.name.text;
100
+
101
+ if (methodName === 'build') {
102
+ // Trace the full chain
103
+ const calls = traceBuilderCalls(node);
104
+ processCalls(calls);
105
+ return;
106
+ }
107
+ }
108
+
109
+ // Recurse into children
110
+ ts.forEachChild(node, processBuilderChain);
111
+ }
112
+
113
+ interface BuilderCall {
114
+ method: string;
115
+ args: ts.NodeArray<ts.Expression>;
116
+ }
117
+
118
+ /**
119
+ * Trace a builder chain backwards to collect all method calls.
120
+ */
121
+ function traceBuilderCalls(node: ts.CallExpression, calls: BuilderCall[] = []): BuilderCall[] {
122
+ if (!ts.isPropertyAccessExpression(node.expression)) {
123
+ // Check for createTheme() or fromTheme()
124
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
125
+ const fnName = node.expression.text;
126
+ if (fnName === 'fromTheme' && node.arguments.length > 0) {
127
+ const arg = node.arguments[0];
128
+ if (ts.isIdentifier(arg)) {
129
+ // Analyze base theme
130
+ analyzeBaseTheme(arg.text, imports, values, ctx);
131
+ }
132
+ }
133
+ }
134
+ return calls;
135
+ }
136
+
137
+ const methodName = node.expression.name.text;
138
+ calls.unshift({ method: methodName, args: node.arguments });
139
+
140
+ // Recurse into the object being called
141
+ const obj = node.expression.expression;
142
+ if (ts.isCallExpression(obj)) {
143
+ return traceBuilderCalls(obj, calls);
144
+ }
145
+
146
+ return calls;
147
+ }
148
+
149
+ /**
150
+ * Process the collected builder method calls.
151
+ */
152
+ function processCalls(calls: BuilderCall[]): void {
153
+ log('Processing', calls.length, 'builder method calls');
154
+
155
+ for (const { method, args } of calls) {
156
+ switch (method) {
157
+ case 'addIntent': {
158
+ const name = getStringValue(args[0]);
159
+ if (name && !values.intents.includes(name)) {
160
+ values.intents.push(name);
161
+ log(' Found intent:', name);
162
+ }
163
+ break;
164
+ }
165
+ case 'addRadius': {
166
+ const name = getStringValue(args[0]);
167
+ if (name && !values.radii.includes(name)) {
168
+ values.radii.push(name);
169
+ log(' Found radius:', name);
170
+ }
171
+ break;
172
+ }
173
+ case 'addShadow': {
174
+ const name = getStringValue(args[0]);
175
+ if (name && !values.shadows.includes(name)) {
176
+ values.shadows.push(name);
177
+ log(' Found shadow:', name);
178
+ }
179
+ break;
180
+ }
181
+ case 'setSizes': {
182
+ if (args[0] && ts.isObjectLiteralExpression(args[0])) {
183
+ for (const prop of args[0].properties) {
184
+ if (ts.isPropertyAssignment(prop)) {
185
+ const componentName = getPropertyName(prop.name);
186
+ if (componentName && ts.isObjectLiteralExpression(prop.initializer)) {
187
+ values.sizes[componentName] = getObjectKeys(prop.initializer);
188
+ log(' Found sizes for', componentName + ':', values.sizes[componentName]);
189
+ }
190
+ }
191
+ }
192
+ }
193
+ break;
194
+ }
195
+ case 'setBreakpoints': {
196
+ if (args[0] && ts.isObjectLiteralExpression(args[0])) {
197
+ values.breakpoints = getObjectKeys(args[0]);
198
+ log(' Found breakpoints:', values.breakpoints);
199
+ }
200
+ break;
201
+ }
202
+ case 'setColors': {
203
+ if (args[0] && ts.isObjectLiteralExpression(args[0])) {
204
+ for (const prop of args[0].properties) {
205
+ if (ts.isPropertyAssignment(prop)) {
206
+ const colorType = getPropertyName(prop.name);
207
+ if (ts.isObjectLiteralExpression(prop.initializer)) {
208
+ const keys = getObjectKeys(prop.initializer);
209
+ switch (colorType) {
210
+ case 'surface':
211
+ values.surfaceColors = keys;
212
+ log(' Found surface colors:', keys);
213
+ break;
214
+ case 'text':
215
+ values.textColors = keys;
216
+ log(' Found text colors:', keys);
217
+ break;
218
+ case 'border':
219
+ values.borderColors = keys;
220
+ log(' Found border colors:', keys);
221
+ break;
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ break;
228
+ }
229
+ case 'build':
230
+ // End of chain
231
+ break;
232
+ default:
233
+ log(' Skipping unknown method:', method);
234
+ }
235
+ }
236
+ }
237
+
238
+ // Second pass: find and process builder chains
239
+ ts.forEachChild(sourceFile, (node) => {
240
+ // Handle variable declarations
241
+ if (ts.isVariableStatement(node)) {
242
+ for (const decl of node.declarationList.declarations) {
243
+ if (decl.initializer) {
244
+ processBuilderChain(decl.initializer);
245
+ }
246
+ }
247
+ }
248
+ // Handle export statements
249
+ if (ts.isExportAssignment(node)) {
250
+ processBuilderChain(node.expression);
251
+ }
252
+ });
253
+
254
+ // Extract typography keys from sizes if present
255
+ if (values.sizes['typography']) {
256
+ values.typography = values.sizes['typography'];
257
+ }
258
+
259
+ log('Extracted theme values:', values);
260
+
261
+ return values;
262
+ }
263
+
264
+ /**
265
+ * Analyze a base theme file referenced by an import.
266
+ */
267
+ function analyzeBaseTheme(
268
+ varName: string,
269
+ imports: Map<string, { source: string; imported: string }>,
270
+ values: ThemeValues,
271
+ ctx: AnalyzerContext
272
+ ): void {
273
+ const log = (...args: any[]) => {
274
+ if (ctx.verbose) console.log('[theme-analyzer]', ...args);
275
+ };
276
+
277
+ const importInfo = imports.get(varName);
278
+ if (!importInfo) {
279
+ log('Could not find import for base theme:', varName);
280
+ return;
281
+ }
282
+
283
+ log('Base theme', varName, 'imported from', importInfo.source);
284
+
285
+ // For @idealyst/theme imports, we know the structure
286
+ if (importInfo.source === '@idealyst/theme' || importInfo.source.includes('@idealyst/theme')) {
287
+ // Use default light theme values
288
+ const defaultValues = getDefaultThemeValues();
289
+ mergeThemeValues(values, defaultValues);
290
+ log('Using default @idealyst/theme values');
291
+ return;
292
+ }
293
+
294
+ // For relative imports, try to resolve and analyze
295
+ // (This is simplified - full implementation would recursively analyze)
296
+ log('Skipping base theme analysis for:', importInfo.source);
297
+ }
298
+
299
+ /**
300
+ * Get default theme values from @idealyst/theme.
301
+ */
302
+ function getDefaultThemeValues(): ThemeValues {
303
+ return {
304
+ intents: ['primary', 'success', 'error', 'warning', 'neutral', 'info'],
305
+ sizes: {
306
+ button: ['xs', 'sm', 'md', 'lg', 'xl'],
307
+ chip: ['xs', 'sm', 'md', 'lg', 'xl'],
308
+ badge: ['xs', 'sm', 'md', 'lg', 'xl'],
309
+ icon: ['xs', 'sm', 'md', 'lg', 'xl'],
310
+ input: ['xs', 'sm', 'md', 'lg', 'xl'],
311
+ radioButton: ['xs', 'sm', 'md', 'lg', 'xl'],
312
+ select: ['xs', 'sm', 'md', 'lg', 'xl'],
313
+ slider: ['xs', 'sm', 'md', 'lg', 'xl'],
314
+ switch: ['xs', 'sm', 'md', 'lg', 'xl'],
315
+ textarea: ['xs', 'sm', 'md', 'lg', 'xl'],
316
+ avatar: ['xs', 'sm', 'md', 'lg', 'xl'],
317
+ progress: ['xs', 'sm', 'md', 'lg', 'xl'],
318
+ accordion: ['xs', 'sm', 'md', 'lg', 'xl'],
319
+ activityIndicator: ['xs', 'sm', 'md', 'lg', 'xl'],
320
+ breadcrumb: ['xs', 'sm', 'md', 'lg', 'xl'],
321
+ list: ['xs', 'sm', 'md', 'lg', 'xl'],
322
+ menu: ['xs', 'sm', 'md', 'lg', 'xl'],
323
+ text: ['xs', 'sm', 'md', 'lg', 'xl'],
324
+ tabBar: ['xs', 'sm', 'md', 'lg', 'xl'],
325
+ table: ['xs', 'sm', 'md', 'lg', 'xl'],
326
+ tooltip: ['xs', 'sm', 'md', 'lg', 'xl'],
327
+ view: ['xs', 'sm', 'md', 'lg', 'xl'],
328
+ },
329
+ radii: ['none', 'xs', 'sm', 'md', 'lg', 'xl'],
330
+ shadows: ['none', 'sm', 'md', 'lg', 'xl'],
331
+ breakpoints: ['xs', 'sm', 'md', 'lg', 'xl'],
332
+ typography: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'subtitle1', 'subtitle2', 'body1', 'body2', 'caption'],
333
+ surfaceColors: ['screen', 'primary', 'secondary', 'tertiary', 'inverse', 'inverse-secondary', 'inverse-tertiary'],
334
+ textColors: ['primary', 'secondary', 'tertiary', 'inverse', 'inverse-secondary', 'inverse-tertiary'],
335
+ borderColors: ['primary', 'secondary', 'tertiary', 'disabled'],
336
+ };
337
+ }
338
+
339
+ /**
340
+ * Merge theme values, avoiding duplicates.
341
+ */
342
+ function mergeThemeValues(target: ThemeValues, source: ThemeValues): void {
343
+ target.intents.push(...source.intents.filter(k => !target.intents.includes(k)));
344
+ target.radii.push(...source.radii.filter(k => !target.radii.includes(k)));
345
+ target.shadows.push(...source.shadows.filter(k => !target.shadows.includes(k)));
346
+ target.breakpoints.push(...source.breakpoints.filter(k => !target.breakpoints.includes(k)));
347
+ target.typography.push(...source.typography.filter(k => !target.typography.includes(k)));
348
+ target.surfaceColors.push(...source.surfaceColors.filter(k => !target.surfaceColors.includes(k)));
349
+ target.textColors.push(...source.textColors.filter(k => !target.textColors.includes(k)));
350
+ target.borderColors.push(...source.borderColors.filter(k => !target.borderColors.includes(k)));
351
+
352
+ for (const [comp, sizes] of Object.entries(source.sizes)) {
353
+ if (!target.sizes[comp]) {
354
+ target.sizes[comp] = sizes;
355
+ }
356
+ }
357
+ }
358
+
359
+ // Helper functions
360
+
361
+ function getStringValue(node?: ts.Expression): string | null {
362
+ if (!node) return null;
363
+ if (ts.isStringLiteral(node)) return node.text;
364
+ if (ts.isIdentifier(node)) return node.text;
365
+ return null;
366
+ }
367
+
368
+ function getPropertyName(node: ts.PropertyName): string | null {
369
+ if (ts.isIdentifier(node)) return node.text;
370
+ if (ts.isStringLiteral(node)) return node.text;
371
+ return null;
372
+ }
373
+
374
+ function getObjectKeys(node: ts.ObjectLiteralExpression): string[] {
375
+ return node.properties
376
+ .filter(ts.isPropertyAssignment)
377
+ .map(prop => getPropertyName(prop.name))
378
+ .filter((k): k is string => k !== null);
379
+ }
380
+
381
+ // ============================================================================
382
+ // Babel Plugin Compatibility Layer
383
+ // ============================================================================
384
+
385
+ /**
386
+ * Theme keys format expected by the Babel plugin.
387
+ * This is a subset of ThemeValues for backwards compatibility.
388
+ */
389
+ export interface BabelThemeKeys {
390
+ intents: string[];
391
+ sizes: Record<string, string[]>;
392
+ radii: string[];
393
+ shadows: string[];
394
+ }
395
+
396
+ // Cache for loadThemeKeys to avoid re-parsing
397
+ let themeKeysCache: BabelThemeKeys | null = null;
398
+ let themeLoadAttempted = false;
399
+
400
+ /**
401
+ * Load theme keys for the Babel plugin.
402
+ * This is a compatibility wrapper around analyzeTheme() that:
403
+ * - Provides caching (only parses once per build)
404
+ * - Returns the subset of keys needed by the Babel plugin
405
+ * - Handles path resolution based on babel opts
406
+ *
407
+ * @param opts - Babel plugin options (requires themePath)
408
+ * @param rootDir - Root directory for path resolution
409
+ * @param _babelTypes - Unused (kept for backwards compatibility)
410
+ * @param verboseMode - Enable verbose logging
411
+ */
412
+ export function loadThemeKeys(
413
+ opts: { themePath?: string },
414
+ rootDir: string,
415
+ _babelTypes?: unknown,
416
+ verboseMode = false
417
+ ): BabelThemeKeys {
418
+ if (themeLoadAttempted && themeKeysCache) {
419
+ return themeKeysCache;
420
+ }
421
+ themeLoadAttempted = true;
422
+
423
+ const themePath = opts.themePath;
424
+
425
+ if (!themePath) {
426
+ throw new Error(
427
+ '[idealyst-plugin] themePath is required!\n' +
428
+ 'Add it to your babel config:\n' +
429
+ ' ["@idealyst/theme/plugin", { themePath: "./src/theme/styles.ts" }]'
430
+ );
431
+ }
432
+
433
+ // Resolve the path
434
+ const resolvedPath = themePath.startsWith('.')
435
+ ? path.resolve(rootDir, themePath)
436
+ : themePath;
437
+
438
+ if (verboseMode) {
439
+ console.log('[idealyst-plugin] Analyzing theme file via @idealyst/tooling:', resolvedPath);
440
+ }
441
+
442
+ // Use the TypeScript-based analyzer
443
+ const themeValues = analyzeTheme(resolvedPath, verboseMode);
444
+
445
+ // Convert to Babel-compatible format (subset of ThemeValues)
446
+ themeKeysCache = {
447
+ intents: themeValues.intents,
448
+ sizes: themeValues.sizes,
449
+ radii: themeValues.radii,
450
+ shadows: themeValues.shadows,
451
+ };
452
+
453
+ if (verboseMode) {
454
+ console.log('[idealyst-plugin] Extracted theme keys:');
455
+ console.log(' intents:', themeKeysCache.intents);
456
+ console.log(' radii:', themeKeysCache.radii);
457
+ console.log(' shadows:', themeKeysCache.shadows);
458
+ console.log(' sizes:');
459
+ for (const [component, sizes] of Object.entries(themeKeysCache.sizes)) {
460
+ console.log(` ${component}:`, sizes);
461
+ }
462
+ }
463
+
464
+ return themeKeysCache;
465
+ }
466
+
467
+ /**
468
+ * Reset the theme cache. Useful for testing or hot reload.
469
+ */
470
+ export function resetThemeCache(): void {
471
+ themeKeysCache = null;
472
+ themeLoadAttempted = false;
473
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Component Registry Types
3
+ *
4
+ * These types define the structure of the auto-generated component registry
5
+ * that documents all components with their props, types, and valid values.
6
+ */
7
+
8
+ /**
9
+ * Definition of a single component prop
10
+ */
11
+ export interface PropDefinition {
12
+ /** The prop name */
13
+ name: string;
14
+
15
+ /** The TypeScript type as a string (e.g., 'Intent', 'Size', 'boolean', 'string') */
16
+ type: string;
17
+
18
+ /** Valid values for this prop (for unions, enums, theme-derived types) */
19
+ values?: string[];
20
+
21
+ /** Default value if specified in the component */
22
+ default?: string | number | boolean;
23
+
24
+ /** Description from JSDoc */
25
+ description?: string;
26
+
27
+ /** Whether the prop is required */
28
+ required: boolean;
29
+ }
30
+
31
+ /**
32
+ * Definition of a component in the registry
33
+ */
34
+ export interface ComponentDefinition {
35
+ /** Component name (e.g., 'Button', 'Card') */
36
+ name: string;
37
+
38
+ /** Component description from static property or JSDoc */
39
+ description?: string;
40
+
41
+ /** All props for this component */
42
+ props: Record<string, PropDefinition>;
43
+
44
+ /** Component category for grouping (e.g., 'form', 'display', 'layout') */
45
+ category?: ComponentCategory;
46
+
47
+ /** Path to the component file (relative) */
48
+ filePath?: string;
49
+ }
50
+
51
+ /**
52
+ * Component categories for organizing documentation
53
+ */
54
+ export type ComponentCategory =
55
+ | 'layout'
56
+ | 'form'
57
+ | 'display'
58
+ | 'navigation'
59
+ | 'overlay'
60
+ | 'data'
61
+ | 'feedback';
62
+
63
+ /**
64
+ * The complete component registry
65
+ */
66
+ export type ComponentRegistry = Record<string, ComponentDefinition>;
67
+
68
+ /**
69
+ * Theme values extracted from the theme configuration
70
+ */
71
+ export interface ThemeValues {
72
+ /** Intent names (e.g., ['primary', 'success', 'error', ...]) */
73
+ intents: string[];
74
+
75
+ /** Size keys per component (e.g., { button: ['xs', 'sm', 'md', ...], ... }) */
76
+ sizes: Record<string, string[]>;
77
+
78
+ /** Radius keys (e.g., ['none', 'xs', 'sm', 'md', ...]) */
79
+ radii: string[];
80
+
81
+ /** Shadow keys (e.g., ['none', 'sm', 'md', 'lg', 'xl']) */
82
+ shadows: string[];
83
+
84
+ /** Breakpoint keys (e.g., ['xs', 'sm', 'md', 'lg', 'xl']) */
85
+ breakpoints: string[];
86
+
87
+ /** Typography keys (e.g., ['h1', 'h2', 'body1', 'body2', ...]) */
88
+ typography: string[];
89
+
90
+ /** Surface color keys */
91
+ surfaceColors: string[];
92
+
93
+ /** Text color keys */
94
+ textColors: string[];
95
+
96
+ /** Border color keys */
97
+ borderColors: string[];
98
+ }
99
+
100
+ /**
101
+ * Options for the component analyzer
102
+ */
103
+ export interface ComponentAnalyzerOptions {
104
+ /** Paths to scan for components (e.g., ['packages/components/src']) */
105
+ componentPaths: string[];
106
+
107
+ /** Path to the theme file (e.g., 'packages/theme/src/lightTheme.ts') */
108
+ themePath: string;
109
+
110
+ /** Component names to include (default: all) */
111
+ include?: string[];
112
+
113
+ /** Component names to exclude */
114
+ exclude?: string[];
115
+
116
+ /** Whether to include internal/private components */
117
+ includeInternal?: boolean;
118
+ }
119
+
120
+ /**
121
+ * Options for the Vite plugin
122
+ */
123
+ export interface IdealystDocsPluginOptions extends ComponentAnalyzerOptions {
124
+ /** Output mode: 'virtual' for virtual module, 'file' for physical file */
125
+ output?: 'virtual' | 'file';
126
+
127
+ /** Path to write the registry file (if output is 'file') */
128
+ outputPath?: string;
129
+
130
+ /** Enable debug logging */
131
+ debug?: boolean;
132
+ }
@@ -0,0 +1 @@
1
+ export * from './platformImports';