@idealyst/theme 1.2.105 → 1.2.107
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 +2 -2
- package/src/babel/plugin.js +244 -10
- package/src/styleBuilder.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/theme",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.107",
|
|
4
4
|
"description": "Theming system for Idealyst Framework",
|
|
5
5
|
"readme": "README.md",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"publish:npm": "npm publish"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@idealyst/tooling": "^1.2.
|
|
66
|
+
"@idealyst/tooling": "^1.2.107"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"react-native-unistyles": ">=3.0.0"
|
package/src/babel/plugin.js
CHANGED
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
* }))
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const nodePath = require('path');
|
|
34
|
+
const crypto = require('crypto');
|
|
35
|
+
|
|
32
36
|
// Theme analysis is provided by @idealyst/tooling (single source of truth)
|
|
33
37
|
// This uses the TypeScript Compiler API for accurate theme extraction
|
|
34
38
|
let loadThemeKeys;
|
|
@@ -48,6 +52,7 @@ try {
|
|
|
48
52
|
// ============================================================================
|
|
49
53
|
|
|
50
54
|
// Map of componentName -> { base: AST | null, extensions: AST[], overrides: AST | null }
|
|
55
|
+
// In-memory registry still used as fast path within a single worker
|
|
51
56
|
const styleRegistry = {};
|
|
52
57
|
|
|
53
58
|
function getOrCreateEntry(componentName) {
|
|
@@ -62,6 +67,212 @@ function getOrCreateEntry(componentName) {
|
|
|
62
67
|
return styleRegistry[componentName];
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// File-Based Extension Cache - Cross-worker persistence
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// When bundlers use multiple workers (Metro, Vite), each worker has its own
|
|
74
|
+
// copy of the in-memory styleRegistry. The file-based cache ensures extensions
|
|
75
|
+
// registered in any worker are visible to all workers processing defineStyle.
|
|
76
|
+
|
|
77
|
+
let _cacheDir = null;
|
|
78
|
+
let _cacheInitialized = false;
|
|
79
|
+
// Track hashes we've already written in this worker to avoid re-reading our own
|
|
80
|
+
const _writtenHashes = new Set();
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get or create the cache directory for style extensions.
|
|
84
|
+
* Uses node_modules/.cache/idealyst-styles/ by convention.
|
|
85
|
+
*/
|
|
86
|
+
function getCacheDir(rootDir) {
|
|
87
|
+
if (_cacheDir) return _cacheDir;
|
|
88
|
+
|
|
89
|
+
// Walk up from rootDir to find the nearest node_modules
|
|
90
|
+
let dir = rootDir;
|
|
91
|
+
while (dir !== nodePath.dirname(dir)) {
|
|
92
|
+
const nmPath = nodePath.join(dir, 'node_modules');
|
|
93
|
+
if (fs.existsSync(nmPath)) {
|
|
94
|
+
_cacheDir = nodePath.join(nmPath, '.cache', 'idealyst-styles');
|
|
95
|
+
return _cacheDir;
|
|
96
|
+
}
|
|
97
|
+
dir = nodePath.dirname(dir);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Fallback: use rootDir directly
|
|
101
|
+
_cacheDir = nodePath.join(rootDir, 'node_modules', '.cache', 'idealyst-styles');
|
|
102
|
+
return _cacheDir;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Initialize the cache directory. Clears stale data from previous builds.
|
|
107
|
+
* Uses a build marker (PID + timestamp) to detect new build processes.
|
|
108
|
+
*/
|
|
109
|
+
function initCache(rootDir) {
|
|
110
|
+
if (_cacheInitialized) return;
|
|
111
|
+
_cacheInitialized = true;
|
|
112
|
+
|
|
113
|
+
const cacheDir = getCacheDir(rootDir);
|
|
114
|
+
const markerPath = nodePath.join(cacheDir, '.build-marker');
|
|
115
|
+
|
|
116
|
+
// Create cache dir if needed
|
|
117
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
118
|
+
|
|
119
|
+
// Check if this is a new build
|
|
120
|
+
const currentMarker = `${process.pid}`;
|
|
121
|
+
let existingMarker = null;
|
|
122
|
+
try {
|
|
123
|
+
existingMarker = fs.readFileSync(markerPath, 'utf8');
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// No marker — fresh cache
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (existingMarker !== currentMarker) {
|
|
129
|
+
// New build process — clear old extensions
|
|
130
|
+
try {
|
|
131
|
+
const files = fs.readdirSync(cacheDir);
|
|
132
|
+
for (const file of files) {
|
|
133
|
+
if (file !== '.build-marker') {
|
|
134
|
+
fs.unlinkSync(nodePath.join(cacheDir, file));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch (e) {
|
|
138
|
+
// Ignore cleanup errors
|
|
139
|
+
}
|
|
140
|
+
fs.writeFileSync(markerPath, currentMarker);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Write an expanded extension callback to the cache.
|
|
146
|
+
* Uses @babel/generator to convert AST to source, then writes atomically.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} rootDir - Project root directory
|
|
149
|
+
* @param {string} componentName - Component name (e.g., 'Text')
|
|
150
|
+
* @param {object} expandedCallback - Expanded AST node (ArrowFunctionExpression)
|
|
151
|
+
* @param {'extension'|'override'} type - Type of style modification
|
|
152
|
+
*/
|
|
153
|
+
function writeExtensionToCache(rootDir, componentName, expandedCallback, type) {
|
|
154
|
+
const cacheDir = getCacheDir(rootDir);
|
|
155
|
+
initCache(rootDir);
|
|
156
|
+
|
|
157
|
+
// Generate source code from AST
|
|
158
|
+
let generate;
|
|
159
|
+
try {
|
|
160
|
+
generate = require('@babel/generator').default || require('@babel/generator');
|
|
161
|
+
} catch (e) {
|
|
162
|
+
// If generator not available, skip file caching (single-worker fallback)
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const source = generate(expandedCallback).code;
|
|
167
|
+
const hash = crypto.createHash('md5').update(source).digest('hex').substring(0, 12);
|
|
168
|
+
|
|
169
|
+
// Track this hash so we don't re-read our own extensions in this worker
|
|
170
|
+
_writtenHashes.add(`${componentName}-${type}-${hash}`);
|
|
171
|
+
|
|
172
|
+
const filename = `${componentName}-${type}-${hash}.js`;
|
|
173
|
+
const filepath = nodePath.join(cacheDir, filename);
|
|
174
|
+
|
|
175
|
+
// Atomic write: write to temp file, then rename
|
|
176
|
+
const tmpPath = filepath + '.tmp.' + process.pid;
|
|
177
|
+
try {
|
|
178
|
+
fs.writeFileSync(tmpPath, source, 'utf8');
|
|
179
|
+
fs.renameSync(tmpPath, filepath);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
// Clean up temp file on error
|
|
182
|
+
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Read all cached extensions for a component.
|
|
188
|
+
* Returns parsed AST nodes ready for merging.
|
|
189
|
+
*
|
|
190
|
+
* @param {object} t - Babel types
|
|
191
|
+
* @param {string} rootDir - Project root directory
|
|
192
|
+
* @param {string} componentName - Component name
|
|
193
|
+
* @returns {{ extensions: object[], override: object|null }}
|
|
194
|
+
*/
|
|
195
|
+
function readExtensionsFromCache(t, rootDir, componentName) {
|
|
196
|
+
const cacheDir = getCacheDir(rootDir);
|
|
197
|
+
const result = { extensions: [], override: null };
|
|
198
|
+
|
|
199
|
+
let files;
|
|
200
|
+
try {
|
|
201
|
+
files = fs.readdirSync(cacheDir);
|
|
202
|
+
} catch (e) {
|
|
203
|
+
return result; // Cache dir doesn't exist yet
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let parse;
|
|
207
|
+
try {
|
|
208
|
+
parse = require('@babel/parser').parse;
|
|
209
|
+
} catch (e) {
|
|
210
|
+
return result; // Parser not available
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Find all extension/override files for this component
|
|
214
|
+
const prefix = `${componentName}-`;
|
|
215
|
+
const relevantFiles = files.filter(f => f.startsWith(prefix) && f.endsWith('.js'))
|
|
216
|
+
.sort(); // Sort for deterministic order
|
|
217
|
+
|
|
218
|
+
for (const file of relevantFiles) {
|
|
219
|
+
// Extract type and hash from filename: ComponentName-type-hash.js
|
|
220
|
+
const match = file.match(/^.+-(extension|override)-([a-f0-9]+)\.js$/);
|
|
221
|
+
if (!match) continue;
|
|
222
|
+
|
|
223
|
+
const type = match[1];
|
|
224
|
+
const hash = match[2];
|
|
225
|
+
const key = `${componentName}-${type}-${hash}`;
|
|
226
|
+
|
|
227
|
+
// Skip extensions we wrote in this worker — they're already in the in-memory registry
|
|
228
|
+
if (_writtenHashes.has(key)) continue;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const source = fs.readFileSync(nodePath.join(cacheDir, file), 'utf8');
|
|
232
|
+
|
|
233
|
+
// Parse source back into AST
|
|
234
|
+
const ast = parse(source, {
|
|
235
|
+
sourceType: 'module',
|
|
236
|
+
plugins: ['typescript', 'jsx'],
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// The parsed file contains a single expression statement (the arrow function)
|
|
240
|
+
// Extract it from the program body
|
|
241
|
+
if (ast.program.body.length > 0) {
|
|
242
|
+
const stmt = ast.program.body[0];
|
|
243
|
+
let expr = null;
|
|
244
|
+
if (t.isExpressionStatement(stmt)) {
|
|
245
|
+
expr = stmt.expression;
|
|
246
|
+
}
|
|
247
|
+
if (expr) {
|
|
248
|
+
if (type === 'override') {
|
|
249
|
+
result.override = expr;
|
|
250
|
+
} else {
|
|
251
|
+
result.extensions.push(expr);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} catch (e) {
|
|
256
|
+
// Skip unreadable/unparseable files
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Reset the file-based cache. Used for testing.
|
|
265
|
+
*/
|
|
266
|
+
function resetStyleCache() {
|
|
267
|
+
_cacheDir = null;
|
|
268
|
+
_cacheInitialized = false;
|
|
269
|
+
_writtenHashes.clear();
|
|
270
|
+
// Also clear in-memory registry
|
|
271
|
+
for (const key of Object.keys(styleRegistry)) {
|
|
272
|
+
delete styleRegistry[key];
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
65
276
|
// ============================================================================
|
|
66
277
|
// AST Deep Merge - Merges style object ASTs at build time
|
|
67
278
|
// ============================================================================
|
|
@@ -116,9 +327,15 @@ function mergeObjectExpressions(t, target, source) {
|
|
|
116
327
|
|
|
117
328
|
if (existingProp) {
|
|
118
329
|
// Both have this property - need to merge or replace
|
|
119
|
-
|
|
330
|
+
let existingValue = existingProp.value;
|
|
120
331
|
const newValue = prop.value;
|
|
121
332
|
|
|
333
|
+
// Unwrap TSAsExpression (e.g., from "as const") to get at the inner value
|
|
334
|
+
const existingHasTSAs = t.isTSAsExpression(existingValue);
|
|
335
|
+
if (existingHasTSAs) {
|
|
336
|
+
existingValue = existingValue.expression;
|
|
337
|
+
}
|
|
338
|
+
|
|
122
339
|
// If both are objects, deep merge
|
|
123
340
|
if (t.isObjectExpression(existingValue) && t.isObjectExpression(newValue)) {
|
|
124
341
|
const mergedValue = mergeObjectExpressions(t, existingValue, newValue);
|
|
@@ -805,7 +1022,7 @@ function buildMemberExpression(t, base, chain) {
|
|
|
805
1022
|
// Babel Plugin
|
|
806
1023
|
// ============================================================================
|
|
807
1024
|
|
|
808
|
-
|
|
1025
|
+
function idealystStylesPlugin({ types: t }) {
|
|
809
1026
|
// Store babel types for use in extractThemeKeysFromAST
|
|
810
1027
|
babelTypes = t;
|
|
811
1028
|
|
|
@@ -981,11 +1198,14 @@ module.exports = function idealystStylesPlugin({ types: t }) {
|
|
|
981
1198
|
const expandedVariants = [];
|
|
982
1199
|
const expandedCallback = expandIterators(t, stylesCallback, themeParam, keys, verbose, expandedVariants);
|
|
983
1200
|
|
|
984
|
-
// Store in registry for
|
|
1201
|
+
// Store in in-memory registry (fast path for same-worker)
|
|
985
1202
|
const entry = getOrCreateEntry(componentName);
|
|
986
1203
|
entry.extensions.push(expandedCallback);
|
|
987
1204
|
entry.themeParam = themeParam;
|
|
988
1205
|
|
|
1206
|
+
// Write to file cache (cross-worker persistence)
|
|
1207
|
+
writeExtensionToCache(rootDir, componentName, expandedCallback, 'extension');
|
|
1208
|
+
|
|
989
1209
|
debug(` -> Stored extension for '${componentName}' (${entry.extensions.length} total)`);
|
|
990
1210
|
|
|
991
1211
|
// Remove the extendStyle call (it's been captured)
|
|
@@ -1033,11 +1253,14 @@ module.exports = function idealystStylesPlugin({ types: t }) {
|
|
|
1033
1253
|
const expandedVariants = [];
|
|
1034
1254
|
const expandedCallback = expandIterators(t, stylesCallback, themeParam, keys, verbose, expandedVariants);
|
|
1035
1255
|
|
|
1036
|
-
// Store as override (
|
|
1256
|
+
// Store as override in in-memory registry (fast path for same-worker)
|
|
1037
1257
|
const entry = getOrCreateEntry(componentName);
|
|
1038
1258
|
entry.override = expandedCallback;
|
|
1039
1259
|
entry.themeParam = themeParam;
|
|
1040
1260
|
|
|
1261
|
+
// Write to file cache (cross-worker persistence)
|
|
1262
|
+
writeExtensionToCache(rootDir, componentName, expandedCallback, 'override');
|
|
1263
|
+
|
|
1041
1264
|
debug(` -> Stored override for '${componentName}'`);
|
|
1042
1265
|
|
|
1043
1266
|
// Remove the overrideStyle call
|
|
@@ -1094,16 +1317,23 @@ module.exports = function idealystStylesPlugin({ types: t }) {
|
|
|
1094
1317
|
let expandedCallback = expandIterators(t, stylesCallback, themeParam, keys, verbose, expandedVariants);
|
|
1095
1318
|
|
|
1096
1319
|
// Check for registered override or extensions
|
|
1320
|
+
// Combine in-memory registry (same worker) with file cache (cross-worker)
|
|
1097
1321
|
const entry = getOrCreateEntry(componentName);
|
|
1322
|
+
const cached = readExtensionsFromCache(t, rootDir, componentName);
|
|
1323
|
+
|
|
1324
|
+
// Determine override: in-memory takes precedence, then file cache
|
|
1325
|
+
const override = entry.override || cached.override;
|
|
1326
|
+
// Combine extensions: in-memory first, then file cache (already deduplicated)
|
|
1327
|
+
const allExtensions = [...entry.extensions, ...cached.extensions];
|
|
1098
1328
|
|
|
1099
|
-
if (
|
|
1329
|
+
if (override) {
|
|
1100
1330
|
// Override completely replaces base
|
|
1101
1331
|
debug(` -> Using override for '${componentName}'`);
|
|
1102
|
-
expandedCallback =
|
|
1103
|
-
} else if (
|
|
1332
|
+
expandedCallback = override;
|
|
1333
|
+
} else if (allExtensions.length > 0) {
|
|
1104
1334
|
// Merge extensions into base
|
|
1105
|
-
debug(` -> Merging ${
|
|
1106
|
-
for (const ext of
|
|
1335
|
+
debug(` -> Merging ${allExtensions.length} extensions for '${componentName}' (${entry.extensions.length} in-memory, ${cached.extensions.length} from cache)`);
|
|
1336
|
+
for (const ext of allExtensions) {
|
|
1107
1337
|
expandedCallback = mergeCallbackBodies(t, expandedCallback, ext);
|
|
1108
1338
|
}
|
|
1109
1339
|
}
|
|
@@ -1177,4 +1407,8 @@ module.exports = function idealystStylesPlugin({ types: t }) {
|
|
|
1177
1407
|
},
|
|
1178
1408
|
},
|
|
1179
1409
|
};
|
|
1180
|
-
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// Export plugin as default (Babel convention) with test utilities
|
|
1413
|
+
module.exports = idealystStylesPlugin;
|
|
1414
|
+
module.exports.resetStyleCache = resetStyleCache;
|