chaincss 2.1.37 โ 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.
- package/ROADMAP.md +31 -0
- package/dist/cli/index.js +458 -3
- package/dist/compiler/analyzer.d.ts +12 -0
- package/dist/compiler/intent-engine.d.ts +31 -0
- package/dist/compiler/math-engine.d.ts +89 -0
- package/dist/compiler/style-graph.d.ts +30 -0
- package/dist/core/compiler.d.ts +12 -0
- package/dist/core/types.d.ts +145 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1116 -9
- package/dist/plugins/vite.js +459 -4
- package/package.json +1 -1
- package/src/compiler/analyzer.ts +62 -0
- package/src/compiler/intent-engine.ts +112 -0
- package/src/compiler/math-engine.ts +511 -0
- package/src/compiler/style-graph.ts +660 -0
- package/src/core/compiler.ts +40 -0
- package/src/core/types.ts +206 -0
- package/src/index.ts +69 -1
- package/src/plugins/vite.ts +9 -1
- package/demo/demo/node_modules/caniuse-db/fulldata-json/data-2.0.json +0 -1
- package/demo/index.html +0 -16
- package/demo/package.json +0 -20
- package/demo/src/App.tsx +0 -117
- package/demo/src/chaincss-barrel.ts +0 -9
- package/demo/src/main.tsx +0 -8
- package/demo/src/styles.chain.ts +0 -300
- package/demo/vite.config.ts +0 -46
|
@@ -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;
|
package/src/core/compiler.ts
CHANGED
|
@@ -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);
|