@idealyst/theme 1.1.7 → 1.1.9
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/package.json +30 -1
- package/src/babel/index.ts +9 -0
- package/src/babel/plugin.js +883 -0
- package/src/babel/plugin.ts +187 -0
- package/src/babel/runtime.ts +94 -0
- package/src/babel/theme-analyzer.js +357 -0
- package/src/breakpoints.ts +112 -0
- package/src/builder.ts +90 -18
- package/src/componentStyles.ts +93 -0
- package/src/config/cli.ts +95 -0
- package/src/config/generator.ts +817 -0
- package/src/config/index.ts +10 -0
- package/src/config/types.ts +112 -0
- package/src/darkTheme.ts +27 -18
- package/src/extensions.ts +110 -0
- package/src/index.ts +21 -4
- package/src/lightTheme.ts +14 -5
- package/src/responsive.ts +123 -0
- package/src/styleBuilder.ts +112 -0
- package/src/theme/breakpoint.ts +30 -0
- package/src/theme/extensions.ts +13 -0
- package/src/theme/index.ts +2 -0
- package/src/theme/structures.ts +7 -0
- package/src/theme/surface.ts +1 -1
- package/src/unistyles.ts +11 -15
- package/src/useResponsiveStyle.ts +282 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Babel plugin that transforms applyExtensions calls into Unistyles-compatible code.
|
|
3
|
+
*
|
|
4
|
+
* This plugin runs BEFORE Unistyles' Babel plugin to transform:
|
|
5
|
+
*
|
|
6
|
+
* ```typescript
|
|
7
|
+
* // INPUT:
|
|
8
|
+
* StyleSheet.create((theme) => {
|
|
9
|
+
* return applyExtensions('View', theme, {
|
|
10
|
+
* view: createViewStyles(theme),
|
|
11
|
+
* });
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* // OUTPUT:
|
|
15
|
+
* StyleSheet.create((theme) => ({
|
|
16
|
+
* view: __withExtension('View', 'view', theme, createViewStyles(theme)),
|
|
17
|
+
* }));
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* This transformation allows Unistyles to:
|
|
21
|
+
* 1. See an ObjectExpression return (required for analysis)
|
|
22
|
+
* 2. Track theme dependencies through the __withExtension call
|
|
23
|
+
* 3. Update styles reactively when theme changes
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { PluginObj, NodePath, types as BabelTypes } from '@babel/core';
|
|
27
|
+
|
|
28
|
+
interface PluginState {
|
|
29
|
+
file: {
|
|
30
|
+
path: NodePath;
|
|
31
|
+
};
|
|
32
|
+
needsImport: boolean;
|
|
33
|
+
importAdded: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function idealystExtensionsPlugin(
|
|
37
|
+
{ types: t }: { types: typeof BabelTypes }
|
|
38
|
+
): PluginObj<PluginState> {
|
|
39
|
+
return {
|
|
40
|
+
name: 'idealyst-extensions',
|
|
41
|
+
|
|
42
|
+
pre() {
|
|
43
|
+
this.needsImport = false;
|
|
44
|
+
this.importAdded = false;
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
visitor: {
|
|
48
|
+
// Transform applyExtensions calls
|
|
49
|
+
CallExpression(path, state) {
|
|
50
|
+
const { node } = path;
|
|
51
|
+
|
|
52
|
+
// Check if this is applyExtensions(...)
|
|
53
|
+
if (!t.isIdentifier(node.callee, { name: 'applyExtensions' })) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Get arguments: applyExtensions(componentName, theme, styleCreators)
|
|
58
|
+
const [componentNameNode, themeNode, styleCreatorsNode] = node.arguments;
|
|
59
|
+
|
|
60
|
+
// Validate argument types
|
|
61
|
+
if (!t.isStringLiteral(componentNameNode)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!t.isIdentifier(themeNode)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!t.isObjectExpression(styleCreatorsNode)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const componentName = componentNameNode.value;
|
|
74
|
+
const themeName = themeNode.name;
|
|
75
|
+
|
|
76
|
+
// Transform each property in styleCreators
|
|
77
|
+
const transformedProperties = styleCreatorsNode.properties.map((prop) => {
|
|
78
|
+
// Skip spread elements and methods
|
|
79
|
+
if (!t.isObjectProperty(prop)) {
|
|
80
|
+
return prop;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Get the key name
|
|
84
|
+
let elementName: string;
|
|
85
|
+
if (t.isIdentifier(prop.key)) {
|
|
86
|
+
elementName = prop.key.name;
|
|
87
|
+
} else if (t.isStringLiteral(prop.key)) {
|
|
88
|
+
elementName = prop.key.value;
|
|
89
|
+
} else {
|
|
90
|
+
return prop; // Can't handle computed keys
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Wrap value: __withExtension('Component', 'element', theme, originalValue)
|
|
94
|
+
const wrappedValue = t.callExpression(
|
|
95
|
+
t.identifier('__withExtension'),
|
|
96
|
+
[
|
|
97
|
+
t.stringLiteral(componentName),
|
|
98
|
+
t.stringLiteral(elementName),
|
|
99
|
+
t.identifier(themeName),
|
|
100
|
+
prop.value as BabelTypes.Expression,
|
|
101
|
+
]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return t.objectProperty(prop.key, wrappedValue);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Replace applyExtensions(...) with the transformed object
|
|
108
|
+
path.replaceWith(t.objectExpression(transformedProperties));
|
|
109
|
+
|
|
110
|
+
// Mark that we need to add the import
|
|
111
|
+
state.needsImport = true;
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
// Add import at the end of the program
|
|
115
|
+
Program: {
|
|
116
|
+
exit(path, state) {
|
|
117
|
+
if (!state.needsImport || state.importAdded) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check if import already exists
|
|
122
|
+
const hasImport = path.node.body.some((node) => {
|
|
123
|
+
if (!t.isImportDeclaration(node)) return false;
|
|
124
|
+
return (
|
|
125
|
+
node.source.value === '@idealyst/theme/extensions' ||
|
|
126
|
+
node.source.value === '@idealyst/theme'
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (hasImport) {
|
|
131
|
+
// Check if __withExtension is already imported
|
|
132
|
+
const existingImport = path.node.body.find((node) => {
|
|
133
|
+
if (!t.isImportDeclaration(node)) return false;
|
|
134
|
+
return node.source.value === '@idealyst/theme/extensions';
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (existingImport && t.isImportDeclaration(existingImport)) {
|
|
138
|
+
const hasWithExtension = existingImport.specifiers.some(
|
|
139
|
+
(spec) =>
|
|
140
|
+
t.isImportSpecifier(spec) &&
|
|
141
|
+
t.isIdentifier(spec.imported, { name: '__withExtension' })
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (!hasWithExtension) {
|
|
145
|
+
existingImport.specifiers.push(
|
|
146
|
+
t.importSpecifier(
|
|
147
|
+
t.identifier('__withExtension'),
|
|
148
|
+
t.identifier('__withExtension')
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
state.importAdded = true;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Add new import
|
|
158
|
+
const importDecl = t.importDeclaration(
|
|
159
|
+
[
|
|
160
|
+
t.importSpecifier(
|
|
161
|
+
t.identifier('__withExtension'),
|
|
162
|
+
t.identifier('__withExtension')
|
|
163
|
+
),
|
|
164
|
+
],
|
|
165
|
+
t.stringLiteral('@idealyst/theme/extensions')
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Insert after other imports
|
|
169
|
+
let insertIndex = 0;
|
|
170
|
+
for (let i = 0; i < path.node.body.length; i++) {
|
|
171
|
+
if (t.isImportDeclaration(path.node.body[i])) {
|
|
172
|
+
insertIndex = i + 1;
|
|
173
|
+
} else {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
path.node.body.splice(insertIndex, 0, importDecl);
|
|
179
|
+
state.importAdded = true;
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Also export as module.exports for CommonJS compatibility
|
|
187
|
+
module.exports = idealystExtensionsPlugin;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime helper for the idealyst-extensions Babel plugin.
|
|
3
|
+
*
|
|
4
|
+
* This function wraps style creator functions to merge extensions from the theme.
|
|
5
|
+
* It's called by code transformed by the Babel plugin - DO NOT call directly.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // Babel transforms this:
|
|
9
|
+
* applyExtensions('Button', theme, { button: createButtonStyles(theme) })
|
|
10
|
+
*
|
|
11
|
+
* // Into this:
|
|
12
|
+
* { button: __withExtension('Button', 'button', theme, createButtonStyles(theme)) }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Deep merge two objects, with source values taking priority.
|
|
17
|
+
* Handles nested objects recursively.
|
|
18
|
+
*/
|
|
19
|
+
function deepMerge<T extends object>(target: T, source: Partial<T>): T {
|
|
20
|
+
const result = { ...target } as T;
|
|
21
|
+
|
|
22
|
+
for (const key in source) {
|
|
23
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
24
|
+
const sourceValue = source[key];
|
|
25
|
+
const targetValue = (target as any)[key];
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
sourceValue !== null &&
|
|
29
|
+
typeof sourceValue === 'object' &&
|
|
30
|
+
!Array.isArray(sourceValue) &&
|
|
31
|
+
targetValue !== null &&
|
|
32
|
+
typeof targetValue === 'object' &&
|
|
33
|
+
!Array.isArray(targetValue)
|
|
34
|
+
) {
|
|
35
|
+
(result as any)[key] = deepMerge(targetValue, sourceValue);
|
|
36
|
+
} else if (sourceValue !== undefined) {
|
|
37
|
+
(result as any)[key] = sourceValue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Theme type with extensions support.
|
|
47
|
+
*/
|
|
48
|
+
interface ThemeWithExtensions {
|
|
49
|
+
__extensions?: Record<string, Record<string, any>>;
|
|
50
|
+
[key: string]: any;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Wrap a style function with extension support.
|
|
55
|
+
*
|
|
56
|
+
* @param component - Component name (e.g., 'View', 'Button')
|
|
57
|
+
* @param element - Style element name (e.g., 'view', 'button', 'text')
|
|
58
|
+
* @param theme - Theme object (may contain __extensions)
|
|
59
|
+
* @param baseStyleFn - Base style creator function
|
|
60
|
+
* @returns Wrapped function that merges extensions
|
|
61
|
+
*/
|
|
62
|
+
export function __withExtension<TProps, TResult>(
|
|
63
|
+
component: string,
|
|
64
|
+
element: string,
|
|
65
|
+
theme: ThemeWithExtensions,
|
|
66
|
+
baseStyleFn: ((props: TProps) => TResult) | TResult
|
|
67
|
+
): ((props: TProps) => TResult) | TResult {
|
|
68
|
+
const ext = theme.__extensions?.[component]?.[element];
|
|
69
|
+
|
|
70
|
+
// If no extension, return base as-is
|
|
71
|
+
if (!ext) {
|
|
72
|
+
return baseStyleFn;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// If baseStyleFn is not a function (static style object), merge directly
|
|
76
|
+
if (typeof baseStyleFn !== 'function') {
|
|
77
|
+
return deepMerge(baseStyleFn as object, ext) as TResult;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Cast to function type after the typeof check
|
|
81
|
+
const styleFn = baseStyleFn as (props: TProps) => TResult;
|
|
82
|
+
|
|
83
|
+
// Return wrapped function that merges extension at call time
|
|
84
|
+
return ((props: TProps): TResult => {
|
|
85
|
+
const base = styleFn(props);
|
|
86
|
+
|
|
87
|
+
// If base is not an object, can't merge
|
|
88
|
+
if (typeof base !== 'object' || base === null) {
|
|
89
|
+
return base;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return deepMerge(base as object, ext) as TResult;
|
|
93
|
+
}) as (props: TProps) => TResult;
|
|
94
|
+
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Analyzer - Extracts theme keys by statically analyzing theme files.
|
|
3
|
+
*
|
|
4
|
+
* Traces 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
|
+
const nodePath = require('path');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
|
|
16
|
+
let themeKeys = null;
|
|
17
|
+
let themeLoadAttempted = false;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract theme keys by statically analyzing the theme file's AST.
|
|
21
|
+
*/
|
|
22
|
+
function extractThemeKeysFromAST(themeFilePath, babelTypes, verboseMode) {
|
|
23
|
+
const { parseSync } = require('@babel/core');
|
|
24
|
+
const traverse = require('@babel/traverse').default;
|
|
25
|
+
const t = babelTypes;
|
|
26
|
+
|
|
27
|
+
const log = (...args) => {
|
|
28
|
+
if (verboseMode) console.log('[idealyst-plugin]', ...args);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const keys = {
|
|
32
|
+
intents: [],
|
|
33
|
+
sizes: {},
|
|
34
|
+
radii: [],
|
|
35
|
+
shadows: [],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
log('Reading theme file:', themeFilePath);
|
|
39
|
+
|
|
40
|
+
// Read and parse the theme file
|
|
41
|
+
const code = fs.readFileSync(themeFilePath, 'utf-8');
|
|
42
|
+
const ast = parseSync(code, {
|
|
43
|
+
filename: themeFilePath,
|
|
44
|
+
presets: [
|
|
45
|
+
['@babel/preset-typescript', { isTSX: true, allExtensions: true }]
|
|
46
|
+
],
|
|
47
|
+
parserOpts: {
|
|
48
|
+
plugins: ['typescript', 'jsx']
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!ast) {
|
|
53
|
+
throw new Error(`[idealyst-plugin] Failed to parse theme file: ${themeFilePath}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Track imports to resolve base themes
|
|
57
|
+
const imports = new Map(); // varName -> { source, imported }
|
|
58
|
+
|
|
59
|
+
// First pass: collect imports
|
|
60
|
+
traverse(ast, {
|
|
61
|
+
ImportDeclaration(path) {
|
|
62
|
+
const source = path.node.source.value;
|
|
63
|
+
for (const spec of path.node.specifiers) {
|
|
64
|
+
if (t.isImportSpecifier(spec)) {
|
|
65
|
+
const localName = spec.local.name;
|
|
66
|
+
const importedName = t.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
|
|
67
|
+
imports.set(localName, { source, imported: importedName });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Recursively trace a builder chain to extract all method calls.
|
|
75
|
+
* Returns { calls: Array, baseThemeVar: string | null }
|
|
76
|
+
*/
|
|
77
|
+
function traceBuilderChain(node, calls = []) {
|
|
78
|
+
if (!node) return { calls, baseThemeVar: null };
|
|
79
|
+
|
|
80
|
+
if (t.isCallExpression(node)) {
|
|
81
|
+
if (t.isIdentifier(node.callee, { name: 'createTheme' })) {
|
|
82
|
+
return { calls, baseThemeVar: null };
|
|
83
|
+
}
|
|
84
|
+
if (t.isIdentifier(node.callee, { name: 'fromTheme' })) {
|
|
85
|
+
const arg = node.arguments[0];
|
|
86
|
+
if (t.isIdentifier(arg)) {
|
|
87
|
+
return { calls, baseThemeVar: arg.name };
|
|
88
|
+
}
|
|
89
|
+
return { calls, baseThemeVar: null };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (t.isMemberExpression(node.callee)) {
|
|
93
|
+
const methodName = node.callee.property.name;
|
|
94
|
+
calls.unshift({ method: methodName, args: node.arguments });
|
|
95
|
+
return traceBuilderChain(node.callee.object, calls);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { calls, baseThemeVar: null };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve and analyze a base theme from an import.
|
|
104
|
+
*/
|
|
105
|
+
function analyzeBaseTheme(varName) {
|
|
106
|
+
const importInfo = imports.get(varName);
|
|
107
|
+
if (!importInfo) {
|
|
108
|
+
log('Could not find import for base theme:', varName);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
log('Base theme', varName, 'imported from', importInfo.source);
|
|
113
|
+
|
|
114
|
+
let baseThemePath;
|
|
115
|
+
try {
|
|
116
|
+
if (importInfo.source.startsWith('.')) {
|
|
117
|
+
baseThemePath = nodePath.resolve(nodePath.dirname(themeFilePath), importInfo.source);
|
|
118
|
+
if (!baseThemePath.endsWith('.ts') && !baseThemePath.endsWith('.tsx')) {
|
|
119
|
+
if (fs.existsSync(baseThemePath + '.ts')) baseThemePath += '.ts';
|
|
120
|
+
else if (fs.existsSync(baseThemePath + '.tsx')) baseThemePath += '.tsx';
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
const packageDir = nodePath.dirname(themeFilePath);
|
|
124
|
+
|
|
125
|
+
// Determine which theme file to look for based on variable name
|
|
126
|
+
const themeFileName = varName.includes('dark') ? 'darkTheme.ts' : 'lightTheme.ts';
|
|
127
|
+
let possiblePaths = [];
|
|
128
|
+
|
|
129
|
+
if (importInfo.source === '@idealyst/theme') {
|
|
130
|
+
possiblePaths = [
|
|
131
|
+
// Symlinked packages at root level
|
|
132
|
+
`/idealyst-packages/theme/src/${themeFileName}`,
|
|
133
|
+
// Standard node_modules
|
|
134
|
+
nodePath.resolve(packageDir, `node_modules/@idealyst/theme/src/${themeFileName}`),
|
|
135
|
+
// Monorepo structure - walk up to find packages dir
|
|
136
|
+
nodePath.resolve(packageDir, `../theme/src/${themeFileName}`),
|
|
137
|
+
nodePath.resolve(packageDir, `../../theme/src/${themeFileName}`),
|
|
138
|
+
nodePath.resolve(packageDir, `../../../theme/src/${themeFileName}`),
|
|
139
|
+
nodePath.resolve(packageDir, `../../packages/theme/src/${themeFileName}`),
|
|
140
|
+
nodePath.resolve(packageDir, `../../../packages/theme/src/${themeFileName}`),
|
|
141
|
+
// This plugin's own package location
|
|
142
|
+
nodePath.resolve(__dirname, `../${themeFileName}`),
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
log('Looking for base theme in:', possiblePaths);
|
|
146
|
+
|
|
147
|
+
for (const p of possiblePaths) {
|
|
148
|
+
if (fs.existsSync(p)) {
|
|
149
|
+
baseThemePath = p;
|
|
150
|
+
log('Found base theme at:', p);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!baseThemePath) {
|
|
157
|
+
log('Could not resolve base theme path for:', importInfo.source);
|
|
158
|
+
if (possiblePaths.length > 0) {
|
|
159
|
+
log('Searched paths:', possiblePaths);
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
log('Analyzing base theme file:', baseThemePath);
|
|
166
|
+
|
|
167
|
+
const baseKeys = extractThemeKeysFromAST(baseThemePath, babelTypes, verboseMode);
|
|
168
|
+
|
|
169
|
+
// Merge base keys
|
|
170
|
+
keys.intents.push(...baseKeys.intents.filter(k => !keys.intents.includes(k)));
|
|
171
|
+
keys.radii.push(...baseKeys.radii.filter(k => !keys.radii.includes(k)));
|
|
172
|
+
keys.shadows.push(...baseKeys.shadows.filter(k => !keys.shadows.includes(k)));
|
|
173
|
+
for (const [comp, sizes] of Object.entries(baseKeys.sizes)) {
|
|
174
|
+
if (!keys.sizes[comp]) {
|
|
175
|
+
keys.sizes[comp] = sizes;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
log('Merged base theme keys');
|
|
180
|
+
} catch (err) {
|
|
181
|
+
log('Error analyzing base theme:', err.message);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getStringValue(node) {
|
|
186
|
+
if (t.isStringLiteral(node)) return node.value;
|
|
187
|
+
if (t.isIdentifier(node)) return node.name;
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getObjectKeys(node) {
|
|
192
|
+
if (!t.isObjectExpression(node)) return [];
|
|
193
|
+
return node.properties
|
|
194
|
+
.filter(prop => t.isObjectProperty(prop))
|
|
195
|
+
.map(prop => {
|
|
196
|
+
if (t.isIdentifier(prop.key)) return prop.key.name;
|
|
197
|
+
if (t.isStringLiteral(prop.key)) return prop.key.value;
|
|
198
|
+
return null;
|
|
199
|
+
})
|
|
200
|
+
.filter(Boolean);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function processBuilderCalls(calls) {
|
|
204
|
+
log('Processing builder chain with', calls.length, 'method calls');
|
|
205
|
+
|
|
206
|
+
for (const { method, args } of calls) {
|
|
207
|
+
switch (method) {
|
|
208
|
+
case 'addIntent': {
|
|
209
|
+
const name = getStringValue(args[0]);
|
|
210
|
+
if (name && !keys.intents.includes(name)) {
|
|
211
|
+
keys.intents.push(name);
|
|
212
|
+
log(' Found intent:', name);
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case 'addRadius': {
|
|
217
|
+
const name = getStringValue(args[0]);
|
|
218
|
+
if (name && !keys.radii.includes(name)) {
|
|
219
|
+
keys.radii.push(name);
|
|
220
|
+
log(' Found radius:', name);
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case 'addShadow': {
|
|
225
|
+
const name = getStringValue(args[0]);
|
|
226
|
+
if (name && !keys.shadows.includes(name)) {
|
|
227
|
+
keys.shadows.push(name);
|
|
228
|
+
log(' Found shadow:', name);
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
case 'setSizes': {
|
|
233
|
+
const sizesObj = args[0];
|
|
234
|
+
if (t.isObjectExpression(sizesObj)) {
|
|
235
|
+
for (const prop of sizesObj.properties) {
|
|
236
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
237
|
+
const componentName = t.isIdentifier(prop.key) ? prop.key.name :
|
|
238
|
+
t.isStringLiteral(prop.key) ? prop.key.value : null;
|
|
239
|
+
if (componentName && t.isObjectExpression(prop.value)) {
|
|
240
|
+
keys.sizes[componentName] = getObjectKeys(prop.value);
|
|
241
|
+
log(' Found sizes for', componentName + ':', keys.sizes[componentName]);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case 'build':
|
|
248
|
+
break;
|
|
249
|
+
default:
|
|
250
|
+
log(' Skipping unknown method:', method);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Second pass: find theme builder chains
|
|
256
|
+
traverse(ast, {
|
|
257
|
+
VariableDeclarator(path) {
|
|
258
|
+
const init = path.node.init;
|
|
259
|
+
if (!init) return;
|
|
260
|
+
|
|
261
|
+
if (t.isCallExpression(init) &&
|
|
262
|
+
t.isMemberExpression(init.callee) &&
|
|
263
|
+
t.isIdentifier(init.callee.property, { name: 'build' })) {
|
|
264
|
+
|
|
265
|
+
const { calls, baseThemeVar } = traceBuilderChain(init);
|
|
266
|
+
|
|
267
|
+
if (baseThemeVar) {
|
|
268
|
+
log('Found fromTheme with base:', baseThemeVar);
|
|
269
|
+
analyzeBaseTheme(baseThemeVar);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
processBuilderCalls(calls);
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
ExportNamedDeclaration(path) {
|
|
277
|
+
if (!path.node.declaration) return;
|
|
278
|
+
if (!t.isVariableDeclaration(path.node.declaration)) return;
|
|
279
|
+
|
|
280
|
+
for (const decl of path.node.declaration.declarations) {
|
|
281
|
+
const init = decl.init;
|
|
282
|
+
if (!init) continue;
|
|
283
|
+
|
|
284
|
+
if (t.isCallExpression(init) &&
|
|
285
|
+
t.isMemberExpression(init.callee) &&
|
|
286
|
+
t.isIdentifier(init.callee.property, { name: 'build' })) {
|
|
287
|
+
|
|
288
|
+
const { calls, baseThemeVar } = traceBuilderChain(init);
|
|
289
|
+
|
|
290
|
+
if (baseThemeVar) {
|
|
291
|
+
log('Found fromTheme with base:', baseThemeVar);
|
|
292
|
+
analyzeBaseTheme(baseThemeVar);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
processBuilderCalls(calls);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
return keys;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Load theme keys by statically analyzing the theme file.
|
|
306
|
+
*
|
|
307
|
+
* REQUIRED Options:
|
|
308
|
+
* - themePath: Path to the consumer's theme file (e.g., './src/theme/styles.ts')
|
|
309
|
+
*/
|
|
310
|
+
function loadThemeKeys(opts, rootDir, babelTypes, verboseMode) {
|
|
311
|
+
if (themeLoadAttempted) return themeKeys;
|
|
312
|
+
themeLoadAttempted = true;
|
|
313
|
+
|
|
314
|
+
const themePath = opts.themePath;
|
|
315
|
+
|
|
316
|
+
if (!themePath) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
'[idealyst-plugin] themePath is required!\n' +
|
|
319
|
+
'Add it to your babel config:\n' +
|
|
320
|
+
' ["@idealyst/theme/plugin", { themePath: "./src/theme/styles.ts" }]'
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const resolvedPath = themePath.startsWith('.')
|
|
325
|
+
? nodePath.resolve(rootDir, themePath)
|
|
326
|
+
: require.resolve(themePath, { paths: [rootDir] });
|
|
327
|
+
|
|
328
|
+
console.log('[idealyst-plugin] Analyzing theme file:', resolvedPath);
|
|
329
|
+
|
|
330
|
+
themeKeys = extractThemeKeysFromAST(resolvedPath, babelTypes, verboseMode);
|
|
331
|
+
|
|
332
|
+
// Always log the extracted keys
|
|
333
|
+
console.log('[idealyst-plugin] Extracted theme keys:');
|
|
334
|
+
console.log(' intents:', themeKeys.intents);
|
|
335
|
+
console.log(' radii:', themeKeys.radii);
|
|
336
|
+
console.log(' shadows:', themeKeys.shadows);
|
|
337
|
+
console.log(' sizes:');
|
|
338
|
+
for (const [component, sizes] of Object.entries(themeKeys.sizes)) {
|
|
339
|
+
console.log(` ${component}:`, sizes);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return themeKeys;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Reset the theme cache (useful for testing or hot reload).
|
|
347
|
+
*/
|
|
348
|
+
function resetThemeCache() {
|
|
349
|
+
themeKeys = null;
|
|
350
|
+
themeLoadAttempted = false;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = {
|
|
354
|
+
extractThemeKeysFromAST,
|
|
355
|
+
loadThemeKeys,
|
|
356
|
+
resetThemeCache,
|
|
357
|
+
};
|