chaincss 2.1.38 โ†’ 2.1.39

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.
@@ -0,0 +1,660 @@
1
+ // ============================================================================
2
+ // FILE: src/compiler/style-graph.ts
3
+ // Style Graph Compiler โ€” Dependency Graph, Dead Elimination, Rule Merging
4
+ // ============================================================================
5
+
6
+ import crypto from 'crypto';
7
+ import type {
8
+ StyleDefinition,
9
+ StyleGraph,
10
+ StyleGraphNode,
11
+ StyleGraphEdge,
12
+ GraphCompileOptions,
13
+ GraphCompileResult,
14
+ CompileResult,
15
+ AtomicClass,
16
+ CompileStats,
17
+ } from '../core/types.js';
18
+
19
+ // ============================================================================
20
+ // Types
21
+ // ============================================================================
22
+
23
+ export type { StyleGraph, StyleGraphNode, StyleGraphEdge, GraphCompileOptions, GraphCompileResult };
24
+
25
+ interface StyleEntry {
26
+ selector: string;
27
+ properties: Record<string, string | number>;
28
+ sourceComponent: string;
29
+ sourceOrder: number;
30
+ mediaQuery?: string;
31
+ }
32
+
33
+ // ============================================================================
34
+ // Specificity Calculator
35
+ // ============================================================================
36
+
37
+ function calculateSpecificity(selector: string): number {
38
+ let a = 0; // IDs
39
+ let b = 0; // Classes, attributes, pseudo-classes
40
+ let c = 0; // Elements, pseudo-elements
41
+
42
+ // Count IDs
43
+ const idMatches = selector.match(/#[a-zA-Z0-9_-]+/g);
44
+ if (idMatches) a += idMatches.length;
45
+
46
+ // Count classes, attributes, pseudo-classes
47
+ const classMatches = selector.match(/\.[a-zA-Z0-9_-]+/g);
48
+ if (classMatches) b += classMatches.length;
49
+
50
+ const attrMatches = selector.match(/\[[^\]]+\]/g);
51
+ if (attrMatches) b += attrMatches.length;
52
+
53
+ const pseudoClassMatches = selector.match(/:[a-zA-Z-]+(?:\([^)]*\))?/g);
54
+ if (pseudoClassMatches) {
55
+ // :not() doesn't add specificity, but its argument does
56
+ const notMatches = selector.match(/:not\(([^)]+)\)/g);
57
+ const regularPseudoClasses = pseudoClassMatches.length - (notMatches?.length || 0);
58
+ b += Math.max(0, regularPseudoClasses);
59
+ }
60
+
61
+ // Count element selectors
62
+ const elementMatches = selector.match(/^[a-zA-Z]+|[a-zA-Z]+(?=[.#[:])/g);
63
+ if (elementMatches) c += elementMatches.length;
64
+
65
+ // Combine into single number (a*10000 + b*100 + c)
66
+ return a * 10000 + b * 100 + c;
67
+ }
68
+
69
+ // ============================================================================
70
+ // Hash Generator
71
+ // ============================================================================
72
+
73
+ function hashProperties(properties: Record<string, string | number>): string {
74
+ const sorted = Object.entries(properties)
75
+ .sort(([a], [b]) => a.localeCompare(b))
76
+ .map(([k, v]) => `${k}:${v}`)
77
+ .join(';');
78
+
79
+ return crypto.createHash('md5').update(sorted).digest('hex').slice(0, 8);
80
+ }
81
+
82
+ function hashNode(node: StyleGraphNode): string {
83
+ return crypto
84
+ .createHash('md5')
85
+ .update(`${node.selector}:${node.mediaQuery || ''}:${JSON.stringify(node.properties)}`)
86
+ .digest('hex')
87
+ .slice(0, 8);
88
+ }
89
+
90
+
91
+ function kebab(prop: string): string {
92
+ return prop.replace(/([A-Z])/g, '-$1').toLowerCase();
93
+ }
94
+
95
+ // ============================================================================
96
+ // Graph Builder
97
+ // ============================================================================
98
+
99
+ class StyleGraphBuilder {
100
+ private entries: StyleEntry[] = [];
101
+ private nodes: Map<string, StyleGraphNode> = new Map();
102
+ private edges: StyleGraphEdge[] = [];
103
+ private orderCounter = 0;
104
+
105
+ addEntry(entry: StyleEntry): void {
106
+ entry.sourceOrder = this.orderCounter++;
107
+ this.entries.push(entry);
108
+ }
109
+
110
+ build(): StyleGraph {
111
+ this.nodes.clear();
112
+ this.edges = [];
113
+
114
+ // Create nodes
115
+ for (const entry of this.entries) {
116
+ const id = `node-${this.nodes.size}`;
117
+ const node: StyleGraphNode = {
118
+ id,
119
+ selector: entry.selector,
120
+ properties: entry.properties,
121
+ specificity: calculateSpecificity(entry.selector),
122
+ dependencies: [],
123
+ dependents: [],
124
+ mediaQuery: entry.mediaQuery,
125
+ isDead: false,
126
+ hash: hashProperties(entry.properties),
127
+ sourceComponent: entry.sourceComponent,
128
+ };
129
+ this.nodes.set(id, node);
130
+ }
131
+
132
+ // Build dependency edges
133
+ const nodeArray = Array.from(this.nodes.values());
134
+ for (let i = 0; i < nodeArray.length; i++) {
135
+ for (let j = i + 1; j < nodeArray.length; j++) {
136
+ const a = nodeArray[i];
137
+ const b = nodeArray[j];
138
+
139
+ // Check if they target overlapping selectors
140
+ if (this.selectorsOverlap(a.selector, b.selector)) {
141
+ if (a.specificity <= b.specificity) {
142
+ // b overrides a
143
+ this.edges.push({ from: a.id, to: b.id, type: 'overrides' });
144
+ a.dependents.push(b.id);
145
+ b.dependencies.push(a.id);
146
+ }
147
+ if (b.specificity <= a.specificity) {
148
+ // a overrides b
149
+ this.edges.push({ from: b.id, to: a.id, type: 'overrides' });
150
+ b.dependents.push(a.id);
151
+ a.dependencies.push(b.id);
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ const rootNodes = nodeArray
158
+ .filter(n => n.dependencies.length === 0)
159
+ .map(n => n.id);
160
+
161
+ const leafNodes = nodeArray
162
+ .filter(n => n.dependents.length === 0)
163
+ .map(n => n.id);
164
+
165
+ return {
166
+ nodes: this.nodes,
167
+ edges: this.edges,
168
+ rootNodes,
169
+ leafNodes,
170
+ };
171
+ }
172
+
173
+ private selectorsOverlap(a: string, b: string): boolean {
174
+ // Simple heuristic: if one selector is a substring of the other, or they share
175
+ // the same base element/class prefix
176
+ const partsA = a.split(/[\s>+~]+/).filter(Boolean);
177
+ const partsB = b.split(/[\s>+~]+/).filter(Boolean);
178
+
179
+ for (const pa of partsA) {
180
+ for (const pb of partsB) {
181
+ if (pa === pb) return true;
182
+ if (pa.startsWith('.') && pb.startsWith('.') && pa === pb) return true;
183
+ }
184
+ }
185
+ return false;
186
+ }
187
+ }
188
+
189
+ // ============================================================================
190
+ // Dead Style Elimination
191
+ // ============================================================================
192
+
193
+ function eliminateDeadStyles(
194
+ graph: StyleGraph,
195
+ knownSelectors: string[]
196
+ ): { eliminated: number; graph: StyleGraph } {
197
+ if (knownSelectors.length === 0) {
198
+ return { eliminated: 0, graph };
199
+ }
200
+
201
+ const reachable = new Set<string>();
202
+ const queue: string[] = [];
203
+
204
+ // Mark root nodes that match known selectors as reachable
205
+ for (const [id, node] of graph.nodes) {
206
+ if (knownSelectors.some(ks => node.selector.includes(ks) || ks.includes(node.selector))) {
207
+ reachable.add(id);
208
+ queue.push(id);
209
+ }
210
+ }
211
+
212
+ // BFS to find all reachable nodes
213
+ while (queue.length > 0) {
214
+ const current = queue.shift()!;
215
+ const node = graph.nodes.get(current);
216
+ if (!node) continue;
217
+
218
+ for (const depId of node.dependents) {
219
+ if (!reachable.has(depId)) {
220
+ reachable.add(depId);
221
+ queue.push(depId);
222
+ }
223
+ }
224
+ }
225
+
226
+ // Mark unreachable nodes as dead
227
+ let eliminated = 0;
228
+ for (const [id, node] of graph.nodes) {
229
+ if (!reachable.has(id)) {
230
+ node.isDead = true;
231
+ eliminated++;
232
+ }
233
+ }
234
+
235
+ return { eliminated, graph };
236
+ }
237
+
238
+ // ============================================================================
239
+ // Identical Rule Merging
240
+ // ============================================================================
241
+
242
+ function mergeIdenticalRules(
243
+ graph: StyleGraph,
244
+ threshold: number
245
+ ): { merged: number; graph: StyleGraph } {
246
+ const hashGroups = new Map<string, StyleGraphNode[]>();
247
+
248
+ // Group by property hash
249
+ for (const [, node] of graph.nodes) {
250
+ if (node.isDead) continue;
251
+ if (Object.keys(node.properties).length < threshold) continue;
252
+
253
+ const existing = hashGroups.get(node.hash) || [];
254
+ existing.push(node);
255
+ hashGroups.set(node.hash, existing);
256
+ }
257
+
258
+ let merged = 0;
259
+
260
+ for (const [, group] of hashGroups) {
261
+ if (group.length < 2) continue;
262
+
263
+ // Merge selectors
264
+ const mergedSelector = group.map(n => n.selector).join(', ');
265
+ const primary = group[0];
266
+
267
+ // Update primary node
268
+ primary.selector = mergedSelector;
269
+
270
+ // Mark others as dead
271
+ for (let i = 1; i < group.length; i++) {
272
+ group[i].isDead = true;
273
+ merged++;
274
+ }
275
+ }
276
+
277
+ return { merged, graph };
278
+ }
279
+
280
+ // ============================================================================
281
+ // Topological Sort
282
+ // ============================================================================
283
+
284
+ function topologicalSort(graph: StyleGraph): StyleGraphNode[] {
285
+ const visited = new Set<string>();
286
+ const sorted: StyleGraphNode[] = [];
287
+ const visiting = new Set<string>();
288
+
289
+ function visit(id: string): boolean {
290
+ if (visited.has(id)) return true;
291
+ if (visiting.has(id)) return false; // Cycle detected
292
+
293
+ visiting.add(id);
294
+ const node = graph.nodes.get(id);
295
+ if (node) {
296
+ for (const depId of node.dependencies) {
297
+ if (!visit(depId)) return false;
298
+ }
299
+ }
300
+
301
+ visiting.delete(id);
302
+ visited.add(id);
303
+ if (node && !node.isDead) {
304
+ sorted.push(node);
305
+ }
306
+ return true;
307
+ }
308
+
309
+ for (const id of graph.rootNodes) {
310
+ if (!visit(id)) {
311
+ // Cycle detected, fall back to source order
312
+ return Array.from(graph.nodes.values())
313
+ .filter(n => !n.isDead)
314
+ .sort((a, b) => a.sourceComponent?.localeCompare(b.sourceComponent || '') || 0);
315
+ }
316
+ }
317
+
318
+ // Visit any remaining nodes
319
+ for (const [id] of graph.nodes) {
320
+ if (!visited.has(id)) {
321
+ visit(id);
322
+ }
323
+ }
324
+
325
+ return sorted;
326
+ }
327
+
328
+ // ============================================================================
329
+ // CSS Generation from Graph
330
+ // ============================================================================
331
+
332
+ function generateCSSFromGraph(
333
+ graph: StyleGraph,
334
+ sortOutput: GraphCompileOptions['sortOutput'] = 'specificity'
335
+ ): string {
336
+ let nodes: StyleGraphNode[];
337
+
338
+ switch (sortOutput) {
339
+ case 'specificity':
340
+ nodes = Array.from(graph.nodes.values())
341
+ .filter(n => !n.isDead)
342
+ .sort((a, b) => a.specificity - b.specificity);
343
+ break;
344
+ case 'topological':
345
+ nodes = topologicalSort(graph);
346
+ break;
347
+ case 'source-order':
348
+ default:
349
+ nodes = Array.from(graph.nodes.values())
350
+ .filter(n => !n.isDead);
351
+ break;
352
+ }
353
+
354
+ let css = '';
355
+ let currentMediaQuery: string | undefined;
356
+
357
+ for (const node of nodes) {
358
+ if (node.isDead) continue;
359
+
360
+ // Handle media query transitions
361
+ if (node.mediaQuery !== currentMediaQuery) {
362
+ if (currentMediaQuery) {
363
+ css += '}\n\n';
364
+ }
365
+ if (node.mediaQuery) {
366
+ css += `@media ${node.mediaQuery} {\n`;
367
+ }
368
+ currentMediaQuery = node.mediaQuery;
369
+ }
370
+
371
+ const rules = Object.entries(node.properties)
372
+ .map(([prop, value]) => ` ${kebab(prop)}: ${value};`)
373
+ .join('\n');
374
+
375
+ if (rules) {
376
+ css += `${node.selector} {\n${rules}\n}\n`;
377
+ }
378
+ }
379
+
380
+ if (currentMediaQuery) {
381
+ css += '}\n';
382
+ }
383
+
384
+ return css;
385
+ }
386
+
387
+ // ============================================================================
388
+ // Public API
389
+ // ============================================================================
390
+
391
+ export class StyleGraphCompiler {
392
+ private options: Required<GraphCompileOptions>;
393
+
394
+ constructor(options: GraphCompileOptions = {}) {
395
+ this.options = {
396
+ eliminateDead: options.eliminateDead ?? false,
397
+ knownSelectors: options.knownSelectors ?? [],
398
+ mergeIdentical: options.mergeIdentical ?? false,
399
+ mergeThreshold: options.mergeThreshold ?? 3,
400
+ sortOutput: options.sortOutput ?? 'specificity',
401
+ verbose: options.verbose ?? false,
402
+ };
403
+ }
404
+
405
+ /**
406
+ * Compile a set of style definitions through the graph compiler.
407
+ */
408
+ compile(styles: Record<string, StyleDefinition>): GraphCompileResult {
409
+ const startTime = Date.now();
410
+ const builder = new StyleGraphBuilder();
411
+
412
+ // Phase 1: Extract entries from style definitions
413
+ let preOptimizationSize = 0;
414
+
415
+ for (const [componentName, styleDef] of Object.entries(styles)) {
416
+ if (!styleDef || !styleDef.selectors) continue;
417
+
418
+ for (const selector of styleDef.selectors) {
419
+ const properties: Record<string, string | number> = {};
420
+
421
+ for (const [prop, value] of Object.entries(styleDef)) {
422
+ if (
423
+ prop === 'selectors' ||
424
+ prop === 'atRules' ||
425
+ prop === 'nestedRules' ||
426
+ prop === 'hover' ||
427
+ prop === 'themes' ||
428
+ prop.startsWith('_')
429
+ ) {
430
+ continue;
431
+ }
432
+
433
+ if (typeof value === 'string' || typeof value === 'number') {
434
+ properties[prop] = String(value);
435
+ preOptimizationSize += String(value).length + prop.length;
436
+ }
437
+ }
438
+
439
+ if (Object.keys(properties).length > 0) {
440
+ builder.addEntry({
441
+ selector,
442
+ properties,
443
+ sourceComponent: componentName,
444
+ sourceOrder: 0,
445
+ });
446
+ }
447
+
448
+ // Handle hover states
449
+ if (styleDef.hover && typeof styleDef.hover === 'object') {
450
+ const hoverProperties: Record<string, string | number> = {};
451
+ for (const [prop, value] of Object.entries(styleDef.hover)) {
452
+ if (typeof value === 'string' || typeof value === 'number') {
453
+ hoverProperties[prop] = String(value);
454
+ }
455
+ }
456
+ if (Object.keys(hoverProperties).length > 0) {
457
+ builder.addEntry({
458
+ selector: `${selector}:hover`,
459
+ properties: hoverProperties,
460
+ sourceComponent: componentName,
461
+ sourceOrder: 0,
462
+ });
463
+ }
464
+ }
465
+
466
+ // Handle atRules (media queries)
467
+ if (styleDef.atRules) {
468
+ for (const rule of styleDef.atRules) {
469
+ if (rule.type === 'media' && rule.styles && rule.query) {
470
+ const mediaProperties: Record<string, string | number> = {};
471
+ for (const [prop, value] of Object.entries(rule.styles)) {
472
+ if (typeof value === 'string' || typeof value === 'number') {
473
+ mediaProperties[prop] = String(value);
474
+ }
475
+ }
476
+ if (Object.keys(mediaProperties).length > 0) {
477
+ builder.addEntry({
478
+ selector,
479
+ properties: mediaProperties,
480
+ sourceComponent: componentName,
481
+ sourceOrder: 0,
482
+ mediaQuery: rule.query,
483
+ });
484
+ }
485
+ }
486
+ }
487
+ }
488
+ }
489
+ }
490
+
491
+ // Phase 2: Build graph
492
+ let graph = builder.build();
493
+
494
+ // Phase 3: Dead style elimination
495
+ let eliminatedDead = 0;
496
+ if (this.options.eliminateDead && this.options.knownSelectors!.length > 0) {
497
+ const result = eliminateDeadStyles(graph, this.options.knownSelectors!);
498
+ eliminatedDead = result.eliminated;
499
+ graph = result.graph;
500
+ }
501
+
502
+ // Phase 4: Merge identical rules
503
+ let mergedRules = 0;
504
+ if (this.options.mergeIdentical) {
505
+ const result = mergeIdenticalRules(graph, this.options.mergeThreshold);
506
+ mergedRules = result.merged;
507
+ graph = result.graph;
508
+ }
509
+
510
+ // Phase 5: Generate CSS
511
+ const css = generateCSSFromGraph(graph, this.options.sortOutput);
512
+
513
+ let postOptimizationSize = css.length;
514
+ if (postOptimizationSize === 0) {
515
+ postOptimizationSize = preOptimizationSize; // No output means no savings to report
516
+ }
517
+
518
+ // Build classMap from non-dead nodes
519
+ const classMap: Record<string, string> = {};
520
+ for (const [, node] of graph.nodes) {
521
+ if (!node.isDead && node.sourceComponent) {
522
+ if (classMap[node.sourceComponent]) {
523
+ classMap[node.sourceComponent] += ` ${node.selector.replace(/^\./, '')}`;
524
+ } else {
525
+ classMap[node.sourceComponent] = node.selector.replace(/^\./, '');
526
+ }
527
+ }
528
+ }
529
+
530
+ const totalNodes = graph.nodes.size;
531
+ const aliveNodes = totalNodes - eliminatedDead;
532
+ const savingsPercent =
533
+ preOptimizationSize > 0
534
+ ? `${(((preOptimizationSize - postOptimizationSize) / preOptimizationSize) * 100).toFixed(1)}%`
535
+ : '0%';
536
+
537
+ const stats: CompileStats = {
538
+ totalStyles: totalNodes,
539
+ atomicStyles: 0,
540
+ uniqueProperties: new Set(
541
+ Array.from(graph.nodes.values())
542
+ .filter(n => !n.isDead)
543
+ .flatMap(n => Object.keys(n.properties))
544
+ ).size,
545
+ savings: savingsPercent,
546
+ compileTime: Date.now() - startTime,
547
+ };
548
+
549
+ return {
550
+ css,
551
+ classMap,
552
+ atomicClasses: [] as AtomicClass[],
553
+ stats,
554
+ graph,
555
+ eliminatedDead,
556
+ mergedRules,
557
+ optimizationTime: Date.now() - startTime,
558
+ preOptimizationSize,
559
+ postOptimizationSize,
560
+ };
561
+ }
562
+
563
+ /**
564
+ * Analyze a style graph without generating CSS.
565
+ */
566
+ analyze(styles: Record<string, StyleDefinition>): StyleGraph {
567
+ const builder = new StyleGraphBuilder();
568
+
569
+ for (const [componentName, styleDef] of Object.entries(styles)) {
570
+ if (!styleDef || !styleDef.selectors) continue;
571
+
572
+ for (const selector of styleDef.selectors) {
573
+ const properties: Record<string, string | number> = {};
574
+ for (const [prop, value] of Object.entries(styleDef)) {
575
+ if (prop === 'selectors' || prop.startsWith('_')) continue;
576
+ if (typeof value === 'string' || typeof value === 'number') {
577
+ properties[prop] = String(value);
578
+ }
579
+ }
580
+ if (Object.keys(properties).length > 0) {
581
+ builder.addEntry({ selector, properties, sourceComponent: componentName, sourceOrder: 0 });
582
+ }
583
+ }
584
+ }
585
+
586
+ return builder.build();
587
+ }
588
+
589
+ /**
590
+ * Get optimization statistics for a graph.
591
+ */
592
+ getStats(graph: StyleGraph): {
593
+ totalNodes: number;
594
+ deadNodes: number;
595
+ mergedGroups: number;
596
+ averageSpecificity: number;
597
+ deepestDependencyChain: number;
598
+ } {
599
+ const nodes = Array.from(graph.nodes.values());
600
+ const deadNodes = nodes.filter(n => n.isDead).length;
601
+ const averageSpecificity =
602
+ nodes.length > 0
603
+ ? nodes.reduce((sum, n) => sum + n.specificity, 0) / nodes.length
604
+ : 0;
605
+
606
+ // Find deepest dependency chain
607
+ let maxDepth = 0;
608
+ const depths = new Map<string, number>();
609
+
610
+ function getDepth(id: string): number {
611
+ if (depths.has(id)) return depths.get(id)!;
612
+ const node = graph.nodes.get(id);
613
+ if (!node || node.dependencies.length === 0) {
614
+ depths.set(id, 0);
615
+ return 0;
616
+ }
617
+ const max = Math.max(...node.dependencies.map(d => getDepth(d)));
618
+ const depth = max + 1;
619
+ depths.set(id, depth);
620
+ return depth;
621
+ }
622
+
623
+ for (const [id] of graph.nodes) {
624
+ maxDepth = Math.max(maxDepth, getDepth(id));
625
+ }
626
+
627
+ return {
628
+ totalNodes: nodes.length,
629
+ deadNodes,
630
+ mergedGroups: 0,
631
+ averageSpecificity: Math.round(averageSpecificity * 100) / 100,
632
+ deepestDependencyChain: maxDepth,
633
+ };
634
+ }
635
+
636
+ /**
637
+ * Update options.
638
+ */
639
+ configure(options: Partial<GraphCompileOptions>): void {
640
+ this.options = { ...this.options, ...options };
641
+ }
642
+ }
643
+
644
+ // ============================================================================
645
+ // Convenience function
646
+ // ============================================================================
647
+
648
+ export function compileGraph(
649
+ styles: Record<string, StyleDefinition>,
650
+ options?: GraphCompileOptions
651
+ ): GraphCompileResult {
652
+ const compiler = new StyleGraphCompiler(options);
653
+ return compiler.compile(styles);
654
+ }
655
+
656
+ // ============================================================================
657
+ // Default export
658
+ // ============================================================================
659
+
660
+ export default StyleGraphCompiler;
@@ -25,6 +25,10 @@ import { PersistentCache } from '../compiler/content-addressable-cache.js';
25
25
  import { shorthandMap, macros } from '../compiler/shorthands.js';
26
26
  import type { AtomicClass } from '../compiler/atomic-optimizer.js';
27
27
 
28
+ import { StyleGraphCompiler } from '../compiler/style-graph.js';
29
+ import type { GraphCompileOptions } from '../compiler/style-graph.js';
30
+
31
+
28
32
  const __filename = typeof import.meta !== 'undefined' ? (() => { try { return fileURLToPath(import.meta.url); } catch { return ''; } })() : '';
29
33
  const __dirname = __filename ? path.dirname(__filename) : '';
30
34
 
@@ -91,6 +95,42 @@ export class ChainCSSCompiler {
91
95
  this.initPrefixer();
92
96
  }
93
97
 
98
+ /**
99
+ * Compile using the style graph compiler for advanced optimizations.
100
+ *
101
+ * @example
102
+ * const result = compiler.compileWithGraph(styles, {
103
+ * eliminateDead: true,
104
+ * knownSelectors: ['.header', '.footer'],
105
+ * mergeIdentical: true
106
+ * });
107
+ */
108
+ public compileWithGraph(
109
+ styles: Record<string, import('./types.js').StyleDefinition>,
110
+ options?: GraphCompileOptions
111
+ ): import('./types.js').GraphCompileResult {
112
+ const graphCompiler = new StyleGraphCompiler({
113
+ ...options,
114
+ verbose: this.config.verbose,
115
+ });
116
+
117
+ const result = graphCompiler.compile(styles);
118
+
119
+ if (this.config.verbose) {
120
+ if (result.eliminatedDead > 0) {
121
+ console.log(` ๐Ÿงน Eliminated ${result.eliminatedDead} dead styles`);
122
+ }
123
+ if (result.mergedRules > 0) {
124
+ console.log(` ๐Ÿ”— Merged ${result.mergedRules} identical rules`);
125
+ }
126
+ if (result.optimizationTime > 0) {
127
+ console.log(` โšก Graph compilation: ${result.optimizationTime}ms`);
128
+ }
129
+ }
130
+
131
+ return result;
132
+ }
133
+
94
134
  public hasStyles(): boolean {
95
135
  const combined = this.getCombinedCSS();
96
136
  return !!(combined && combined.trim().length > 0);