@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
|
@@ -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';
|