chaincss 1.13.2 → 1.13.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.
@@ -12,23 +12,41 @@ function kebab(s) {
12
12
 
13
13
  class AtomicOptimizer {
14
14
  constructor(options = {}) {
15
+ // Ensure arrays are arrays (fix for config merging issues)
16
+ if (options.alwaysAtomic && !Array.isArray(options.alwaysAtomic)) {
17
+ options.alwaysAtomic = Object.values(options.alwaysAtomic);
18
+ }
19
+ if (options.neverAtomic && !Array.isArray(options.neverAtomic)) {
20
+ options.neverAtomic = Object.values(options.neverAtomic);
21
+ }
22
+
15
23
  this.options = {
16
24
  enabled: true,
17
- threshold: 3, // Default threshold
18
- naming: 'hash', // 'hash' | 'readable'
25
+ threshold: 3,
26
+ naming: 'hash',
19
27
  cache: true,
20
28
  cachePath: './.chaincss-cache',
21
29
  minify: true,
22
- alwaysAtomic: [], // Force these props to be atomic
23
- neverAtomic: [ // Never make these atomic
30
+ mode: 'hybrid',
31
+ outputStrategy: 'component-first',
32
+ alwaysAtomic: [],
33
+ neverAtomic: [
24
34
  'content', 'animation', 'transition', 'keyframes',
25
35
  'counterIncrement', 'counterReset'
26
36
  ],
37
+ frameworkOutput: {
38
+ react: false,
39
+ vue: false,
40
+ vanilla: true
41
+ },
42
+ preserveSelectors: false,
43
+ verbose: false,
27
44
  ...options
28
45
  };
29
46
 
30
- this.usageCount = new Map(); // prop:value -> count
31
- this.atomicClasses = new Map(); // prop:value -> { className, prop, value, usageCount }
47
+ this.usageCount = new Map();
48
+ this.atomicClasses = new Map();
49
+ this.componentClassMap = new Map();
32
50
  this.stats = {
33
51
  totalStyles: 0,
34
52
  atomicStyles: 0,
@@ -52,23 +70,21 @@ class AtomicOptimizer {
52
70
 
53
71
  const data = JSON.parse(fs.readFileSync(this.options.cachePath, 'utf8'));
54
72
 
55
- // Version check
56
73
  if (data.version !== '1.0.0') {
57
- console.log('Cache version mismatch, creating new cache');
74
+ //if (this.options.verbose) console.log('Cache version mismatch, creating new cache');
58
75
  return;
59
76
  }
60
77
 
61
- // Check if config changed
62
78
  if (data.config?.threshold !== this.options.threshold) {
63
- console.log(`Cache threshold (${data.config?.threshold}) differs from current (${this.options.threshold})`);
79
+ //if (this.options.verbose) console.log(`Cache threshold (${data.config?.threshold}) differs from current (${this.options.threshold})`);
64
80
  return;
65
81
  }
66
82
 
67
83
  this.atomicClasses = new Map(data.atomicClasses || []);
84
+ this.componentClassMap = new Map(data.componentClassMap || []);
68
85
  this.stats = data.stats || this.stats;
69
86
 
70
87
  const cacheTime = new Date(data.timestamp).toLocaleString();
71
- console.log(`✅ Loaded ${this.atomicClasses.size} atomic classes from cache (${cacheTime})`);
72
88
 
73
89
  } catch (err) {
74
90
  console.log('Could not load cache:', err.message);
@@ -84,30 +100,17 @@ class AtomicOptimizer {
84
100
  fs.mkdirSync(cacheDir, { recursive: true });
85
101
  }
86
102
 
87
- // Clean up old cache files (keep last 5)
88
- if (fs.existsSync(cacheDir)) {
89
- const files = fs.readdirSync(cacheDir)
90
- .filter(f => f.startsWith('.chaincss-cache'))
91
- .map(f => ({
92
- name: f,
93
- time: fs.statSync(path.join(cacheDir, f)).mtime.getTime()
94
- }))
95
- .sort((a, b) => b.time - a.time);
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 {}
100
- });
101
- }
102
-
103
103
  const cache = {
104
104
  version: '1.0.0',
105
105
  timestamp: Date.now(),
106
106
  atomicClasses: Array.from(this.atomicClasses.entries()),
107
+ componentClassMap: Array.from(this.componentClassMap.entries()),
107
108
  stats: this.stats,
108
109
  config: {
109
110
  threshold: this.options.threshold,
110
- naming: this.options.naming
111
+ naming: this.options.naming,
112
+ mode: this.options.mode,
113
+ outputStrategy: this.options.outputStrategy
111
114
  }
112
115
  };
113
116
 
@@ -128,36 +131,46 @@ class AtomicOptimizer {
128
131
  if (!style || !style.selectors) continue;
129
132
 
130
133
  for (const [prop, value] of Object.entries(style)) {
131
- if (prop === 'selectors' || prop === 'atRules' || prop === 'hover') continue;
134
+ if (prop === 'selectors' || prop === 'atRules') continue;
132
135
 
133
- const key = `${prop}:${value}`;
134
- this.usageCount.set(key, (this.usageCount.get(key) || 0) + 1);
135
- this.stats.totalStyles++;
136
+ if (prop === 'hover' && typeof value === 'object') {
137
+ for (const [hoverProp, hoverValue] of Object.entries(value)) {
138
+ this.incrementUsage(hoverProp, hoverValue);
139
+ }
140
+ } else {
141
+ this.incrementUsage(prop, value);
142
+ }
136
143
  }
137
144
  }
138
145
 
139
146
  this.stats.uniqueProperties = this.usageCount.size;
140
147
  }
141
148
 
149
+ incrementUsage(prop, value) {
150
+ const key = `${prop}:${value}`;
151
+ const count = (this.usageCount.get(key) || 0) + 1;
152
+ this.usageCount.set(key, count);
153
+ this.stats.totalStyles++;
154
+
155
+ if (this.atomicClasses.has(key)) {
156
+ this.atomicClasses.get(key).usageCount = count;
157
+ }
158
+ }
159
+
142
160
  shouldBeAtomic(prop, value) {
143
- // Never atomic
144
- if (this.options.neverAtomic.includes(prop)) return false;
161
+ if (this.options.mode === 'standard') return false;
162
+ if (this.options.mode === 'atomic') return true;
145
163
 
146
- // Always atomic
164
+ if (this.options.neverAtomic.includes(prop)) return false;
147
165
  if (this.options.alwaysAtomic.includes(prop)) return true;
148
166
 
149
- // Critical props that need higher threshold
150
167
  const criticalProps = ['position', 'display', 'flex', 'grid', 'zIndex', 'top', 'left', 'right', 'bottom'];
151
168
  const isCritical = criticalProps.includes(prop);
152
169
 
153
170
  const key = `${prop}:${value}`;
154
171
  const usage = this.usageCount.get(key) || 0;
155
172
 
156
- // Critical props need double threshold to be atomic
157
- if (isCritical && usage < this.options.threshold * 2) {
158
- return false;
159
- }
160
-
173
+ if (isCritical && usage < this.options.threshold * 2) return false;
161
174
  return usage >= this.options.threshold;
162
175
  }
163
176
 
@@ -168,7 +181,6 @@ class AtomicOptimizer {
168
181
  return `c_${hashKey(key)}`;
169
182
  }
170
183
 
171
- // Readable naming
172
184
  const kebabProp = kebab(prop);
173
185
  const safeValue = String(value).replace(/[^a-z0-9_-]/gi, '-').slice(0, 30);
174
186
  return `${kebabProp}-${safeValue}`;
@@ -190,6 +202,13 @@ class AtomicOptimizer {
190
202
 
191
203
  return this.atomicClasses.get(key).className;
192
204
  }
205
+
206
+ getKeyFromClassName(className) {
207
+ for (const [key, atomic] of this.atomicClasses) {
208
+ if (atomic.className === className) return key;
209
+ }
210
+ return null;
211
+ }
193
212
 
194
213
  // ============================================================================
195
214
  // CSS Generation
@@ -202,117 +221,164 @@ class AtomicOptimizer {
202
221
 
203
222
  for (const atomic of sortedClasses) {
204
223
  const kebabProp = kebab(atomic.prop);
205
- css += `.${atomic.className}{${kebabProp}:${atomic.value}${this.options.minify ? '' : ';'}}\n`;
224
+ if (this.options.minify) {
225
+ css += `.${atomic.className}{${kebabProp}:${atomic.value}}`;
226
+ } else {
227
+ css += `.${atomic.className} {\n ${kebabProp}: ${atomic.value};\n}\n`;
228
+ }
229
+ }
230
+
231
+ // Debug: Log atomic CSS generation
232
+ if (this.options.verbose && css) {
233
+ //console.log(` Generated ${this.atomicClasses.size} atomic classes (${css.length} bytes)`);
234
+ if (css.length > 0) {
235
+ //console.log(`First atomic class: ${css.substring(0, 50)}...`);
236
+ }
206
237
  }
207
238
 
208
239
  return css;
209
240
  }
210
241
 
211
- generateComponentCSS(componentName, style, selectors) {
242
+ generateComponentCSS(style, selectors) {
212
243
  const atomicClasses = [];
213
- const standardStyles = {};
214
- const hoverStyles = {};
244
+ const hoverAtomicClasses = [];
215
245
 
216
- // Separate styles
246
+ // Track which properties become atomic
217
247
  for (const [prop, value] of Object.entries(style)) {
218
248
  if (prop === 'selectors' || prop === 'atRules') continue;
219
249
 
220
250
  if (prop === 'hover' && typeof value === 'object') {
221
- // Handle hover styles
222
251
  for (const [hoverProp, hoverValue] of Object.entries(value)) {
223
252
  if (this.shouldBeAtomic(hoverProp, hoverValue)) {
224
- atomicClasses.push(this.getOrCreateAtomic(hoverProp, hoverValue));
225
- } else {
226
- hoverStyles[hoverProp] = hoverValue;
253
+ hoverAtomicClasses.push(this.getOrCreateAtomic(hoverProp, hoverValue));
227
254
  }
228
255
  }
229
256
  } else if (this.shouldBeAtomic(prop, value)) {
230
257
  atomicClasses.push(this.getOrCreateAtomic(prop, value));
231
- } else {
232
- standardStyles[prop] = value;
233
258
  }
234
259
  }
235
260
 
236
- // Generate CSS
237
261
  let componentCSS = '';
238
262
  const selectorStr = selectors.join(', ');
239
263
 
240
- if (atomicClasses.length > 0 || Object.keys(standardStyles).length > 0) {
241
- componentCSS += `${selectorStr} {\n`;
264
+ // COMPONENT-FIRST STRATEGY (Default)
265
+ if (this.options.outputStrategy === 'component-first') {
266
+ // Include ALL properties in component CSS
267
+ const allStyles = {};
242
268
 
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);
248
- componentCSS += ` ${kebabProp}: ${atomic.value};\n`;
269
+ // Add all properties (both atomic and non-atomic)
270
+ for (const [prop, value] of Object.entries(style)) {
271
+ if (prop !== 'selectors' && prop !== 'atRules' && prop !== 'hover') {
272
+ allStyles[prop] = value;
249
273
  }
250
274
  }
251
275
 
252
- // Standard styles
253
- for (const [prop, value] of Object.entries(standardStyles)) {
254
- const kebabProp = kebab(prop);
255
- componentCSS += ` ${kebabProp}: ${value};\n`;
276
+ // Generate CSS with ALL properties
277
+ if (Object.keys(allStyles).length > 0) {
278
+ if (this.options.minify) {
279
+ componentCSS += `${selectorStr}{`;
280
+ for (const [prop, value] of Object.entries(allStyles)) {
281
+ const kebabProp = kebab(prop);
282
+ componentCSS += `${kebabProp}:${value};`;
283
+ }
284
+ componentCSS += `}`;
285
+ } else {
286
+ componentCSS += `${selectorStr} {\n`;
287
+ for (const [prop, value] of Object.entries(allStyles)) {
288
+ const kebabProp = kebab(prop);
289
+ componentCSS += ` ${kebabProp}: ${value};\n`;
290
+ }
291
+ componentCSS += `}\n`;
292
+ }
256
293
  }
257
294
 
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`;
295
+ // Add hover styles (all hover properties)
296
+ if (style.hover && typeof style.hover === 'object') {
297
+ const allHoverStyles = {};
298
+ for (const [prop, value] of Object.entries(style.hover)) {
299
+ allHoverStyles[prop] = value;
300
+ }
301
+
302
+ if (Object.keys(allHoverStyles).length > 0) {
303
+ if (this.options.minify) {
304
+ componentCSS += `${selectorStr}:hover{`;
305
+ for (const [prop, value] of Object.entries(allHoverStyles)) {
306
+ const kebabProp = kebab(prop);
307
+ componentCSS += `${kebabProp}:${value};`;
308
+ }
309
+ componentCSS += `}`;
310
+ } else {
311
+ componentCSS += `${selectorStr}:hover {\n`;
312
+ for (const [prop, value] of Object.entries(allHoverStyles)) {
313
+ const kebabProp = kebab(prop);
314
+ componentCSS += ` ${kebabProp}: ${value};\n`;
315
+ }
316
+ componentCSS += `}\n`;
317
+ }
318
+ }
267
319
  }
268
- componentCSS += `}\n`;
269
320
  }
270
321
 
271
- return componentCSS;
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
-
281
- validateStyleOrder(originalStyles, atomicStyles) {
282
- const originalProps = new Set();
283
- const atomicProps = new Set();
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);
322
+ // UTILITY-FIRST STRATEGY (Advanced)
323
+ else {
324
+ const standardStyles = {};
325
+ const hoverStandardStyles = {};
326
+
327
+ for (const [prop, value] of Object.entries(style)) {
328
+ if (prop === 'selectors' || prop === 'atRules') continue;
329
+
330
+ if (prop === 'hover' && typeof value === 'object') {
331
+ for (const [hoverProp, hoverValue] of Object.entries(value)) {
332
+ if (!this.shouldBeAtomic(hoverProp, hoverValue)) {
333
+ hoverStandardStyles[hoverProp] = hoverValue;
334
+ }
335
+ }
336
+ } else if (!this.shouldBeAtomic(prop, value)) {
337
+ standardStyles[prop] = value;
338
+ }
339
+ }
340
+
341
+ if (Object.keys(standardStyles).length > 0) {
342
+ if (this.options.minify) {
343
+ componentCSS += `${selectorStr}{`;
344
+ for (const [prop, value] of Object.entries(standardStyles)) {
345
+ const kebabProp = kebab(prop);
346
+ componentCSS += `${kebabProp}:${value};`;
347
+ }
348
+ componentCSS += `}`;
349
+ } else {
350
+ componentCSS += `${selectorStr} {\n`;
351
+ for (const [prop, value] of Object.entries(standardStyles)) {
352
+ const kebabProp = kebab(prop);
353
+ componentCSS += ` ${kebabProp}: ${value};\n`;
354
+ }
355
+ componentCSS += `}\n`;
356
+ }
357
+ }
358
+
359
+ if (Object.keys(hoverStandardStyles).length > 0) {
360
+ if (this.options.minify) {
361
+ componentCSS += `${selectorStr}:hover{`;
362
+ for (const [prop, value] of Object.entries(hoverStandardStyles)) {
363
+ const kebabProp = kebab(prop);
364
+ componentCSS += `${kebabProp}:${value};`;
365
+ }
366
+ componentCSS += `}`;
367
+ } else {
368
+ componentCSS += `${selectorStr}:hover {\n`;
369
+ for (const [prop, value] of Object.entries(hoverStandardStyles)) {
370
+ const kebabProp = kebab(prop);
371
+ componentCSS += ` ${kebabProp}: ${value};\n`;
372
+ }
373
+ componentCSS += `}\n`;
291
374
  }
292
375
  }
293
376
  }
294
377
 
295
- for (const atomic of this.atomicClasses.values()) {
296
- atomicProps.add(atomic.prop);
297
- }
298
-
299
- const missingProps = [...originalProps].filter(p => !atomicProps.has(p));
300
- if (missingProps.length > 0) {
301
- console.warn('⚠️ Missing atomic classes for:', missingProps.slice(0, 10));
302
- }
303
- }
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
378
  return {
311
- totalStyles: this.stats.totalStyles,
312
- atomicStyles: this.stats.atomicStyles,
313
- standardStyles: this.stats.standardStyles,
314
- uniqueProperties: this.stats.uniqueProperties,
315
- savings: `${savings}%`
379
+ css: componentCSS,
380
+ atomicClasses,
381
+ hoverAtomicClasses
316
382
  };
317
383
  }
318
384
 
@@ -330,18 +396,33 @@ class AtomicOptimizer {
330
396
  };
331
397
  }
332
398
 
333
- // Normalize input to array
334
- const styleArray = Array.isArray(stylesInput)
335
- ? stylesInput
336
- : typeof stylesInput === 'object'
337
- ? Object.values(stylesInput)
338
- : [];
399
+ // Reset stats
400
+ this.stats = {
401
+ totalStyles: 0,
402
+ atomicStyles: 0,
403
+ standardStyles: 0,
404
+ uniqueProperties: 0,
405
+ savings: 0
406
+ };
407
+ this.usageCount.clear();
408
+ this.componentClassMap.clear();
409
+
410
+ // Normalize input
411
+ let styleArray = [];
412
+ if (Array.isArray(stylesInput)) {
413
+ styleArray = stylesInput;
414
+ } else if (typeof stylesInput === 'object') {
415
+ styleArray = Object.values(stylesInput).filter(v => v && typeof v === 'object');
416
+ }
339
417
 
340
- // Track usage counts
418
+ if (styleArray.length === 0) {
419
+ return { css: '', map: {}, stats: this.getStats(), atomicCSS: '', componentCSS: '' };
420
+ }
421
+
422
+ // FIRST: Track usage counts
341
423
  this.trackStyles(styleArray);
342
424
 
343
- // Generate CSS
344
- let atomicCSS = this.generateAtomicCSS();
425
+ // SECOND: Generate component CSS (this populates atomicClasses via getOrCreateAtomic)
345
426
  let componentCSS = '';
346
427
  const classMap = {};
347
428
 
@@ -350,42 +431,96 @@ class AtomicOptimizer {
350
431
 
351
432
  const selectors = style.selectors;
352
433
  const selectorKey = selectors.join(', ');
434
+ const { css, atomicClasses, hoverAtomicClasses } = this.generateComponentCSS(style, selectors);
435
+ componentCSS += css;
353
436
 
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));
437
+ if (this.options.outputStrategy === 'utility-first') {
438
+ if (atomicClasses.length > 0) {
439
+ classMap[selectorKey] = atomicClasses.join(' ');
440
+ }
441
+ if (hoverAtomicClasses.length > 0) {
442
+ classMap[`${selectorKey}:hover`] = hoverAtomicClasses.join(' ');
363
443
  }
364
444
  }
365
- classMap[selectorKey] = atomicClassesForSelector.join(' ');
445
+
446
+ this.componentClassMap.set(selectorKey, {
447
+ atomicClasses,
448
+ hoverAtomicClasses,
449
+ selectors
450
+ });
366
451
  }
367
452
 
368
- // Validation
369
- this.validateStyleOrder(styleArray);
453
+ // THIRD: Generate atomic CSS (now atomicClasses is populated!)
454
+ const atomicCSS = this.generateAtomicCSS();
455
+
456
+ // Combine CSS
457
+ const finalCSS = atomicCSS + componentCSS;
458
+
459
+ // Log stats if verbose
460
+ if (this.options.verbose) {
461
+ const stats = this.getStats();
462
+ //console.log(` Atomic Optimization Stats:`);
463
+ //console.log(` Output strategy: ${this.options.outputStrategy}`);
464
+ //console.log(` Total styles tracked: ${stats.totalStyles}`);
465
+ //console.log(` Atomic classes created: ${this.atomicClasses.size}`);
466
+ //console.log(` Atomic CSS length: ${atomicCSS.length} bytes`);
467
+ //console.log(` Component CSS length: ${componentCSS.length} bytes`);
468
+ //console.log(` Total CSS length: ${finalCSS.length} bytes`);
469
+ //console.log(` Savings: ${stats.savings}`);
470
+
471
+ if (atomicCSS.length === 0 && this.atomicClasses.size > 0) {
472
+ //console.log(` WARNING: ${this.atomicClasses.size} atomic classes exist but generated CSS is empty!`);
473
+ }
474
+ }
370
475
 
371
476
  // Save cache
372
477
  if (this.options.cache) {
373
478
  this.saveCache();
374
479
  }
375
480
 
376
- // Calculate savings
481
+ return {
482
+ css: finalCSS,
483
+ map: classMap,
484
+ stats: this.getStats(),
485
+ atomicCSS: atomicCSS,
486
+ componentCSS: componentCSS,
487
+ componentMap: this.componentClassMap
488
+ };
489
+ }
490
+
491
+ getStats() {
377
492
  const savings = this.stats.totalStyles > 0
378
- ? ((this.stats.totalStyles - this.atomicClasses.size) / this.stats.totalStyles * 100).toFixed(1)
493
+ ? ((this.stats.totalStyles - this.stats.atomicStyles) / this.stats.totalStyles * 100).toFixed(1)
379
494
  : 0;
380
495
 
381
496
  return {
382
- css: (atomicCSS + componentCSS).trim(),
383
- map: classMap,
384
- stats: this.getStats(),
385
- atomicCSS: atomicCSS.trim(),
386
- componentCSS: componentCSS.trim()
497
+ totalStyles: this.stats.totalStyles,
498
+ atomicStyles: this.stats.atomicStyles,
499
+ standardStyles: this.stats.standardStyles,
500
+ uniqueProperties: this.stats.uniqueProperties,
501
+ savings: `${savings}%`
387
502
  };
388
503
  }
504
+
505
+ getAtomicClass(prop, value) {
506
+ const key = `${prop}:${value}`;
507
+ const atomic = this.atomicClasses.get(key);
508
+ return atomic ? atomic.className : null;
509
+ }
510
+
511
+ getAllAtomicClasses() {
512
+ return Array.from(this.atomicClasses.values());
513
+ }
514
+
515
+ clearCache() {
516
+ this.atomicClasses.clear();
517
+ this.componentClassMap.clear();
518
+ this.usageCount.clear();
519
+ if (this.options.cache && fs.existsSync(this.options.cachePath)) {
520
+ fs.unlinkSync(this.options.cachePath);
521
+ }
522
+ //console.log('Atomic optimizer cache cleared');
523
+ }
389
524
  }
390
525
 
391
526
  module.exports = { AtomicOptimizer };