@melcanz85/chaincss 1.11.5 → 1.12.0
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/node/atomic-optimizer.js +275 -88
- package/node/btt.js +587 -150
- package/node/chaincss.js +189 -65
- package/node/strVal.js +2 -2
- package/package.json +69 -35
- package/types.d.ts +175 -61
package/node/atomic-optimizer.js
CHANGED
|
@@ -1,61 +1,90 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
1
2
|
const path = require('path');
|
|
2
3
|
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
function hashKey(key) {
|
|
6
|
+
return crypto.createHash('sha1').update(key).digest('hex').slice(0, 6);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function kebab(s) {
|
|
10
|
+
return s.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
class AtomicOptimizer {
|
|
4
14
|
constructor(options = {}) {
|
|
5
15
|
this.options = {
|
|
6
16
|
enabled: true,
|
|
7
|
-
threshold: 3,
|
|
8
|
-
naming: 'hash',
|
|
17
|
+
threshold: 3, // Default threshold
|
|
18
|
+
naming: 'hash', // 'hash' | 'readable'
|
|
9
19
|
cache: true,
|
|
10
20
|
cachePath: './.chaincss-cache',
|
|
11
21
|
minify: true,
|
|
22
|
+
alwaysAtomic: [], // Force these props to be atomic
|
|
23
|
+
neverAtomic: [ // Never make these atomic
|
|
24
|
+
'content', 'animation', 'transition', 'keyframes',
|
|
25
|
+
'counterIncrement', 'counterReset'
|
|
26
|
+
],
|
|
12
27
|
...options
|
|
13
28
|
};
|
|
14
|
-
|
|
15
|
-
this.
|
|
29
|
+
|
|
30
|
+
this.usageCount = new Map(); // prop:value -> count
|
|
31
|
+
this.atomicClasses = new Map(); // prop:value -> { className, prop, value, usageCount }
|
|
16
32
|
this.stats = {
|
|
17
33
|
totalStyles: 0,
|
|
18
34
|
atomicStyles: 0,
|
|
19
35
|
standardStyles: 0,
|
|
20
|
-
uniqueProperties: 0
|
|
36
|
+
uniqueProperties: 0,
|
|
37
|
+
savings: 0
|
|
21
38
|
};
|
|
22
|
-
|
|
39
|
+
|
|
23
40
|
if (this.options.cache) {
|
|
24
41
|
this.loadCache();
|
|
25
42
|
}
|
|
26
43
|
}
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Cache Management
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
27
49
|
loadCache() {
|
|
28
50
|
try {
|
|
29
|
-
if (fs.existsSync(this.options.cachePath))
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
console.log('Cache version mismatch, creating new cache');
|
|
44
|
-
}
|
|
51
|
+
if (!fs.existsSync(this.options.cachePath)) return;
|
|
52
|
+
|
|
53
|
+
const data = JSON.parse(fs.readFileSync(this.options.cachePath, 'utf8'));
|
|
54
|
+
|
|
55
|
+
// Version check
|
|
56
|
+
if (data.version !== '1.0.0') {
|
|
57
|
+
console.log('Cache version mismatch, creating new cache');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check if config changed
|
|
62
|
+
if (data.config?.threshold !== this.options.threshold) {
|
|
63
|
+
console.log(`Cache threshold (${data.config?.threshold}) differs from current (${this.options.threshold})`);
|
|
64
|
+
return;
|
|
45
65
|
}
|
|
66
|
+
|
|
67
|
+
this.atomicClasses = new Map(data.atomicClasses || []);
|
|
68
|
+
this.stats = data.stats || this.stats;
|
|
69
|
+
|
|
70
|
+
const cacheTime = new Date(data.timestamp).toLocaleString();
|
|
71
|
+
console.log(`✅ Loaded ${this.atomicClasses.size} atomic classes from cache (${cacheTime})`);
|
|
72
|
+
|
|
46
73
|
} catch (err) {
|
|
47
74
|
console.log('Could not load cache:', err.message);
|
|
48
75
|
}
|
|
49
76
|
}
|
|
77
|
+
|
|
50
78
|
saveCache() {
|
|
79
|
+
if (!this.options.cache) return;
|
|
80
|
+
|
|
51
81
|
try {
|
|
52
|
-
const cache = {
|
|
53
|
-
version: '1.0.0',
|
|
54
|
-
timestamp: Date.now(),
|
|
55
|
-
atomicClasses: Array.from(this.atomicClasses.entries()),
|
|
56
|
-
stats: this.stats
|
|
57
|
-
};
|
|
58
82
|
const cacheDir = path.dirname(this.options.cachePath);
|
|
83
|
+
if (!fs.existsSync(cacheDir)) {
|
|
84
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Clean up old cache files (keep last 5)
|
|
59
88
|
if (fs.existsSync(cacheDir)) {
|
|
60
89
|
const files = fs.readdirSync(cacheDir)
|
|
61
90
|
.filter(f => f.startsWith('.chaincss-cache'))
|
|
@@ -64,52 +93,90 @@ class AtomicOptimizer {
|
|
|
64
93
|
time: fs.statSync(path.join(cacheDir, f)).mtime.getTime()
|
|
65
94
|
}))
|
|
66
95
|
.sort((a, b) => b.time - a.time);
|
|
67
|
-
|
|
68
|
-
|
|
96
|
+
|
|
97
|
+
// Keep only the 5 most recent cache files
|
|
98
|
+
files.slice(5).forEach(f => {
|
|
99
|
+
try { fs.unlinkSync(path.join(cacheDir, f.name)); } catch {}
|
|
69
100
|
});
|
|
70
101
|
}
|
|
102
|
+
|
|
103
|
+
const cache = {
|
|
104
|
+
version: '1.0.0',
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
atomicClasses: Array.from(this.atomicClasses.entries()),
|
|
107
|
+
stats: this.stats,
|
|
108
|
+
config: {
|
|
109
|
+
threshold: this.options.threshold,
|
|
110
|
+
naming: this.options.naming
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
71
114
|
fs.writeFileSync(this.options.cachePath, JSON.stringify(cache, null, 2), 'utf8');
|
|
72
115
|
} catch (err) {
|
|
73
116
|
console.log('Could not save cache:', err.message);
|
|
74
117
|
}
|
|
75
118
|
}
|
|
119
|
+
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// Style Tracking
|
|
122
|
+
// ============================================================================
|
|
123
|
+
|
|
76
124
|
trackStyles(styles) {
|
|
77
|
-
Object.values(styles)
|
|
78
|
-
|
|
79
|
-
|
|
125
|
+
const styleArray = Array.isArray(styles) ? styles : Object.values(styles);
|
|
126
|
+
|
|
127
|
+
for (const style of styleArray) {
|
|
128
|
+
if (!style || !style.selectors) continue;
|
|
129
|
+
|
|
130
|
+
for (const [prop, value] of Object.entries(style)) {
|
|
131
|
+
if (prop === 'selectors' || prop === 'atRules' || prop === 'hover') continue;
|
|
132
|
+
|
|
80
133
|
const key = `${prop}:${value}`;
|
|
81
134
|
this.usageCount.set(key, (this.usageCount.get(key) || 0) + 1);
|
|
82
135
|
this.stats.totalStyles++;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
85
139
|
this.stats.uniqueProperties = this.usageCount.size;
|
|
86
140
|
}
|
|
141
|
+
|
|
87
142
|
shouldBeAtomic(prop, value) {
|
|
143
|
+
// Never atomic
|
|
144
|
+
if (this.options.neverAtomic.includes(prop)) return false;
|
|
145
|
+
|
|
146
|
+
// Always atomic
|
|
147
|
+
if (this.options.alwaysAtomic.includes(prop)) return true;
|
|
148
|
+
|
|
149
|
+
// Critical props that need higher threshold
|
|
150
|
+
const criticalProps = ['position', 'display', 'flex', 'grid', 'zIndex', 'top', 'left', 'right', 'bottom'];
|
|
151
|
+
const isCritical = criticalProps.includes(prop);
|
|
152
|
+
|
|
88
153
|
const key = `${prop}:${value}`;
|
|
89
154
|
const usage = this.usageCount.get(key) || 0;
|
|
90
|
-
|
|
91
|
-
|
|
155
|
+
|
|
156
|
+
// Critical props need double threshold to be atomic
|
|
92
157
|
if (isCritical && usage < this.options.threshold * 2) {
|
|
93
158
|
return false;
|
|
94
159
|
}
|
|
160
|
+
|
|
95
161
|
return usage >= this.options.threshold;
|
|
96
162
|
}
|
|
163
|
+
|
|
97
164
|
generateClassName(prop, value) {
|
|
98
165
|
const key = `${prop}:${value}`;
|
|
166
|
+
|
|
99
167
|
if (this.options.naming === 'hash') {
|
|
100
|
-
|
|
101
|
-
for (let i = 0; i < key.length; i++) {
|
|
102
|
-
hash = ((hash << 5) - hash) + key.charCodeAt(i);
|
|
103
|
-
hash |= 0;
|
|
104
|
-
}
|
|
105
|
-
return `_${Math.abs(hash).toString(36).substring(0, 6)}`;
|
|
168
|
+
return `c_${hashKey(key)}`;
|
|
106
169
|
}
|
|
107
|
-
|
|
108
|
-
|
|
170
|
+
|
|
171
|
+
// Readable naming
|
|
172
|
+
const kebabProp = kebab(prop);
|
|
173
|
+
const safeValue = String(value).replace(/[^a-z0-9_-]/gi, '-').slice(0, 30);
|
|
109
174
|
return `${kebabProp}-${safeValue}`;
|
|
110
175
|
}
|
|
176
|
+
|
|
111
177
|
getOrCreateAtomic(prop, value) {
|
|
112
178
|
const key = `${prop}:${value}`;
|
|
179
|
+
|
|
113
180
|
if (!this.atomicClasses.has(key)) {
|
|
114
181
|
const className = this.generateClassName(prop, value);
|
|
115
182
|
this.atomicClasses.set(key, {
|
|
@@ -120,85 +187,205 @@ class AtomicOptimizer {
|
|
|
120
187
|
});
|
|
121
188
|
this.stats.atomicStyles++;
|
|
122
189
|
}
|
|
190
|
+
|
|
123
191
|
return this.atomicClasses.get(key).className;
|
|
124
192
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
193
|
+
|
|
194
|
+
// ============================================================================
|
|
195
|
+
// CSS Generation
|
|
196
|
+
// ============================================================================
|
|
197
|
+
|
|
131
198
|
generateAtomicCSS() {
|
|
132
|
-
let css = '';
|
|
199
|
+
let css = '';
|
|
133
200
|
const sortedClasses = Array.from(this.atomicClasses.values())
|
|
134
201
|
.sort((a, b) => b.usageCount - a.usageCount);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
202
|
+
|
|
203
|
+
for (const atomic of sortedClasses) {
|
|
204
|
+
const kebabProp = kebab(atomic.prop);
|
|
205
|
+
css += `.${atomic.className}{${kebabProp}:${atomic.value}${this.options.minify ? '' : ';'}}\n`;
|
|
206
|
+
}
|
|
207
|
+
|
|
139
208
|
return css;
|
|
140
209
|
}
|
|
210
|
+
|
|
141
211
|
generateComponentCSS(componentName, style, selectors) {
|
|
142
212
|
const atomicClasses = [];
|
|
143
213
|
const standardStyles = {};
|
|
144
|
-
|
|
145
|
-
|
|
214
|
+
const hoverStyles = {};
|
|
215
|
+
|
|
216
|
+
// Separate styles
|
|
217
|
+
for (const [prop, value] of Object.entries(style)) {
|
|
218
|
+
if (prop === 'selectors' || prop === 'atRules') continue;
|
|
146
219
|
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
220
|
+
if (prop === 'hover' && typeof value === 'object') {
|
|
221
|
+
// Handle hover styles
|
|
222
|
+
for (const [hoverProp, hoverValue] of Object.entries(value)) {
|
|
223
|
+
if (this.shouldBeAtomic(hoverProp, hoverValue)) {
|
|
224
|
+
atomicClasses.push(this.getOrCreateAtomic(hoverProp, hoverValue));
|
|
225
|
+
} else {
|
|
226
|
+
hoverStyles[hoverProp] = hoverValue;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} else if (this.shouldBeAtomic(prop, value)) {
|
|
230
|
+
atomicClasses.push(this.getOrCreateAtomic(prop, value));
|
|
150
231
|
} else {
|
|
151
232
|
standardStyles[prop] = value;
|
|
152
233
|
}
|
|
153
|
-
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Generate CSS
|
|
154
237
|
let componentCSS = '';
|
|
238
|
+
const selectorStr = selectors.join(', ');
|
|
239
|
+
|
|
155
240
|
if (atomicClasses.length > 0 || Object.keys(standardStyles).length > 0) {
|
|
156
|
-
componentCSS += `${
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
241
|
+
componentCSS += `${selectorStr} {\n`;
|
|
242
|
+
|
|
243
|
+
// Atomic classes (inlined for specificity)
|
|
244
|
+
for (const className of atomicClasses) {
|
|
245
|
+
const atomic = this.findAtomicByClassName(className);
|
|
246
|
+
if (atomic) {
|
|
247
|
+
const kebabProp = kebab(atomic.prop);
|
|
162
248
|
componentCSS += ` ${kebabProp}: ${atomic.value};\n`;
|
|
163
249
|
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Standard styles
|
|
253
|
+
for (const [prop, value] of Object.entries(standardStyles)) {
|
|
254
|
+
const kebabProp = kebab(prop);
|
|
167
255
|
componentCSS += ` ${kebabProp}: ${value};\n`;
|
|
168
|
-
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
componentCSS += `}\n`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Hover styles
|
|
262
|
+
if (Object.keys(hoverStyles).length > 0) {
|
|
263
|
+
componentCSS += `${selectorStr}:hover {\n`;
|
|
264
|
+
for (const [prop, value] of Object.entries(hoverStyles)) {
|
|
265
|
+
const kebabProp = kebab(prop);
|
|
266
|
+
componentCSS += ` ${kebabProp}: ${value};\n`;
|
|
267
|
+
}
|
|
169
268
|
componentCSS += `}\n`;
|
|
170
269
|
}
|
|
270
|
+
|
|
171
271
|
return componentCSS;
|
|
172
272
|
}
|
|
273
|
+
|
|
274
|
+
findAtomicByClassName(className) {
|
|
275
|
+
for (const atomic of this.atomicClasses.values()) {
|
|
276
|
+
if (atomic.className === className) return atomic;
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
173
281
|
validateStyleOrder(originalStyles, atomicStyles) {
|
|
174
282
|
const originalProps = new Set();
|
|
175
283
|
const atomicProps = new Set();
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
284
|
+
|
|
285
|
+
const styleArray = Array.isArray(originalStyles) ? originalStyles : Object.values(originalStyles);
|
|
286
|
+
for (const style of styleArray) {
|
|
287
|
+
if (!style) continue;
|
|
288
|
+
for (const prop of Object.keys(style)) {
|
|
289
|
+
if (prop !== 'selectors' && prop !== 'atRules' && prop !== 'hover') {
|
|
290
|
+
originalProps.add(prop);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (const atomic of this.atomicClasses.values()) {
|
|
182
296
|
atomicProps.add(atomic.prop);
|
|
183
|
-
}
|
|
297
|
+
}
|
|
298
|
+
|
|
184
299
|
const missingProps = [...originalProps].filter(p => !atomicProps.has(p));
|
|
185
300
|
if (missingProps.length > 0) {
|
|
186
|
-
console.warn('Missing atomic classes for:', missingProps);
|
|
301
|
+
console.warn('⚠️ Missing atomic classes for:', missingProps.slice(0, 10));
|
|
187
302
|
}
|
|
188
303
|
}
|
|
189
|
-
|
|
190
|
-
|
|
304
|
+
|
|
305
|
+
getStats() {
|
|
306
|
+
const savings = this.stats.totalStyles > 0
|
|
307
|
+
? ((this.stats.totalStyles - this.stats.atomicStyles) / this.stats.totalStyles * 100).toFixed(1)
|
|
308
|
+
: 0;
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
totalStyles: this.stats.totalStyles,
|
|
312
|
+
atomicStyles: this.stats.atomicStyles,
|
|
313
|
+
standardStyles: this.stats.standardStyles,
|
|
314
|
+
uniqueProperties: this.stats.uniqueProperties,
|
|
315
|
+
savings: `${savings}%`
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ============================================================================
|
|
320
|
+
// Main Optimize Method
|
|
321
|
+
// ============================================================================
|
|
322
|
+
|
|
323
|
+
optimize(stylesInput) {
|
|
324
|
+
if (!this.options.enabled) {
|
|
325
|
+
return {
|
|
326
|
+
css: '',
|
|
327
|
+
map: {},
|
|
328
|
+
stats: this.getStats(),
|
|
329
|
+
atomicCSS: ''
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Normalize input to array
|
|
334
|
+
const styleArray = Array.isArray(stylesInput)
|
|
335
|
+
? stylesInput
|
|
336
|
+
: typeof stylesInput === 'object'
|
|
337
|
+
? Object.values(stylesInput)
|
|
338
|
+
: [];
|
|
339
|
+
|
|
340
|
+
// Track usage counts
|
|
341
|
+
this.trackStyles(styleArray);
|
|
342
|
+
|
|
343
|
+
// Generate CSS
|
|
191
344
|
let atomicCSS = this.generateAtomicCSS();
|
|
192
345
|
let componentCSS = '';
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
346
|
+
const classMap = {};
|
|
347
|
+
|
|
348
|
+
for (const style of styleArray) {
|
|
349
|
+
if (!style || !style.selectors) continue;
|
|
350
|
+
|
|
351
|
+
const selectors = style.selectors;
|
|
352
|
+
const selectorKey = selectors.join(', ');
|
|
353
|
+
|
|
354
|
+
// Generate component CSS
|
|
355
|
+
componentCSS += this.generateComponentCSS(style.name || 'component', style, selectors);
|
|
356
|
+
|
|
357
|
+
// Build class map for users
|
|
358
|
+
const atomicClassesForSelector = [];
|
|
359
|
+
for (const [prop, value] of Object.entries(style)) {
|
|
360
|
+
if (prop === 'selectors' || prop === 'atRules' || prop === 'hover') continue;
|
|
361
|
+
if (this.shouldBeAtomic(prop, value)) {
|
|
362
|
+
atomicClassesForSelector.push(this.getOrCreateAtomic(prop, value));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
classMap[selectorKey] = atomicClassesForSelector.join(' ');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Validation
|
|
369
|
+
this.validateStyleOrder(styleArray);
|
|
370
|
+
|
|
371
|
+
// Save cache
|
|
198
372
|
if (this.options.cache) {
|
|
199
373
|
this.saveCache();
|
|
200
374
|
}
|
|
201
|
-
|
|
375
|
+
|
|
376
|
+
// Calculate savings
|
|
377
|
+
const savings = this.stats.totalStyles > 0
|
|
378
|
+
? ((this.stats.totalStyles - this.atomicClasses.size) / this.stats.totalStyles * 100).toFixed(1)
|
|
379
|
+
: 0;
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
css: (atomicCSS + componentCSS).trim(),
|
|
383
|
+
map: classMap,
|
|
384
|
+
stats: this.getStats(),
|
|
385
|
+
atomicCSS: atomicCSS.trim(),
|
|
386
|
+
componentCSS: componentCSS.trim()
|
|
387
|
+
};
|
|
202
388
|
}
|
|
203
389
|
}
|
|
390
|
+
|
|
204
391
|
module.exports = { AtomicOptimizer };
|