@melcanz85/chaincss 1.11.5 → 1.12.1

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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Rommel Caneos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+ EOF
@@ -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
- this.usageCount = new Map();
15
- this.atomicClasses = new Map();
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
- this.cache = null;
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
- const data = fs.readFileSync(this.options.cachePath, 'utf8');
31
- const cache = JSON.parse(data);
32
- if (cache.version === '1.0.0') {
33
- this.atomicClasses = new Map(cache.atomicClasses || []);
34
- this.stats = cache.stats || this.stats;
35
- const cacheTime = new Date(cache.timestamp).toLocaleString();
36
- console.log(`--Loaded ${this.atomicClasses.size} atomic classes from cache (${cacheTime})\n`);
37
- if (cache.config) {
38
- if (cache.config.threshold !== this.options.threshold) {
39
- console.log(`Cache threshold (${cache.config.threshold}) differs from current (${this.options.threshold})`);
40
- }
41
- }
42
- } else {
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
- files.slice(4).forEach(f => {
68
- fs.unlinkSync(path.join(cacheDir, f.name));
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).forEach(style => {
78
- Object.entries(style).forEach(([prop, value]) => {
79
- if (prop === 'selectors') return;
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
- const criticalProps = ['position', 'display', 'flex', 'grid', 'z-index'];
91
- const isCritical = criticalProps.some(p => prop.includes(p));
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
- let hash = 0;
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
- const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
108
- const safeValue = value.replace(/[^a-zA-Z0-9-]/g, '-');
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
- findKeyByClassName(className) {
126
- for (let [key, value] of this.atomicClasses.entries()) {
127
- if (value.className === className) return key;
128
- }
129
- return null;
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
- sortedClasses.forEach(atomic => {
136
- const kebabProp = atomic.prop.replace(/([A-Z])/g, '-$1').toLowerCase();
137
- css += `.${atomic.className} { ${kebabProp}: ${atomic.value}; }\n`;
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
- Object.entries(style).forEach(([prop, value]) => {
145
- if (prop === 'selectors') return;
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 (this.shouldBeAtomic(prop, value)) {
148
- const className = this.getOrCreateAtomic(prop, value);
149
- atomicClasses.push(className);
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 += `${selectors.join(', ')} {\n`;
157
- atomicClasses.forEach(className => {
158
- const key = this.findKeyByClassName(className);
159
- if (key) {
160
- const atomic = this.atomicClasses.get(key);
161
- const kebabProp = atomic.prop.replace(/([A-Z])/g, '-$1').toLowerCase();
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
- Object.entries(standardStyles).forEach(([prop, value]) => {
166
- const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
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
- Object.values(originalStyles).forEach(style => {
177
- Object.keys(style).forEach(prop => {
178
- if (prop !== 'selectors') originalProps.add(prop);
179
- });
180
- });
181
- this.atomicClasses.forEach(atomic => {
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
- optimize(styles) {
190
- this.trackStyles(styles);
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
- Object.entries(styles).forEach(([name, style]) => {
194
- const selectors = style.selectors || [`.${name}`];
195
- componentCSS += this.generateComponentCSS(name, style, selectors);
196
- });
197
- const savings = ((this.stats.totalStyles - this.atomicClasses.size) / this.stats.totalStyles * 100).toFixed(1);
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
- return atomicCSS + componentCSS;
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 };
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ChainCSS Benchmark Comparison
5
+ * Run with: npm run benchmark:compare
6
+ */
7
+
8
+ console.log('📊 ChainCSS Benchmark Comparison\n');
9
+ console.log('This will compare ChainCSS with other CSS-in-JS libraries.\n');
10
+ console.log('To run full benchmarks: npm run benchmark');
11
+ console.log('Check results at: https://chaincss.dev/benchmarks\n');
12
+
13
+ // Simple version info
14
+ const { version } = require('../../package.json');
15
+ console.log(`ChainCSS v${version} is ready for benchmarking!\n`);
16
+ console.log('For detailed benchmarks, please visit our documentation site.');
17
+ EOF