chaincss 2.2.0 → 2.3.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/README.md +240 -372
- package/dist/compiler/accessibility-engine.d.ts +57 -0
- package/dist/compiler/constraint-solver.d.ts +85 -0
- package/dist/compiler/intent-api.d.ts +73 -0
- package/dist/compiler/layout-intelligence.d.ts +71 -0
- package/dist/compiler/pass-manager.d.ts +157 -0
- package/dist/compiler/pattern-learner.d.ts +112 -0
- package/dist/compiler/responsive-inference.d.ts +63 -0
- package/dist/compiler/semantic-tokens.d.ts +57 -0
- package/dist/compiler/source-optimizer.d.ts +109 -0
- package/dist/compiler/style-ir.d.ts +183 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +3475 -0
- package/package.json +1 -1
- package/src/compiler/accessibility-engine.ts +502 -0
- package/src/compiler/constraint-solver.ts +407 -0
- package/src/compiler/intent-api.ts +505 -0
- package/src/compiler/layout-intelligence.ts +697 -0
- package/src/compiler/pass-manager.ts +657 -0
- package/src/compiler/pattern-learner.ts +398 -0
- package/src/compiler/responsive-inference.ts +415 -0
- package/src/compiler/semantic-tokens.ts +468 -0
- package/src/compiler/source-optimizer.ts +541 -0
- package/src/compiler/style-ir.ts +495 -0
- package/src/index.ts +175 -0
- package/ROADMAP.md +0 -31
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
// src/compiler/source-optimizer.ts
|
|
2
|
+
/**
|
|
3
|
+
* Source-Aware Optimization Engine
|
|
4
|
+
*
|
|
5
|
+
* Unifies all analysis modules into an enterprise-grade optimization report.
|
|
6
|
+
* Tracks where every style originates and detects:
|
|
7
|
+
* - Duplicate styles across files
|
|
8
|
+
* - Dead/unreachable rules
|
|
9
|
+
* - Specificity wars
|
|
10
|
+
* - Conflicting animations
|
|
11
|
+
* - Redundant media queries
|
|
12
|
+
* - Unused variants and recipes
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { StyleIR, IRRule, IRPass } from './style-ir.js';
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
export interface DuplicateGroup {
|
|
22
|
+
/** The shared signature */
|
|
23
|
+
signature: string;
|
|
24
|
+
/** All occurrences with source locations */
|
|
25
|
+
occurrences: Array<{
|
|
26
|
+
selector: string;
|
|
27
|
+
file?: string;
|
|
28
|
+
line?: number;
|
|
29
|
+
component?: string;
|
|
30
|
+
}>;
|
|
31
|
+
/** How many times it appears */
|
|
32
|
+
count: number;
|
|
33
|
+
/** Suggested extraction */
|
|
34
|
+
suggestion: string;
|
|
35
|
+
/** Estimated savings in bytes */
|
|
36
|
+
savingsBytes: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DeadRule {
|
|
40
|
+
ruleId: string;
|
|
41
|
+
selector: string;
|
|
42
|
+
file?: string;
|
|
43
|
+
line?: number;
|
|
44
|
+
reason: string;
|
|
45
|
+
bytesWasted: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SpecificityConflict {
|
|
49
|
+
higher: {
|
|
50
|
+
selector: string;
|
|
51
|
+
specificity: number;
|
|
52
|
+
file?: string;
|
|
53
|
+
line?: number;
|
|
54
|
+
};
|
|
55
|
+
lower: {
|
|
56
|
+
selector: string;
|
|
57
|
+
specificity: number;
|
|
58
|
+
file?: string;
|
|
59
|
+
line?: number;
|
|
60
|
+
};
|
|
61
|
+
property?: string;
|
|
62
|
+
severity: 'warning' | 'info';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AnimationConflict {
|
|
66
|
+
name: string;
|
|
67
|
+
locations: Array<{
|
|
68
|
+
file?: string;
|
|
69
|
+
line?: number;
|
|
70
|
+
selector: string;
|
|
71
|
+
}>;
|
|
72
|
+
count: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface MediaQueryRedundancy {
|
|
76
|
+
query: string;
|
|
77
|
+
count: number;
|
|
78
|
+
files: string[];
|
|
79
|
+
suggestion: string;
|
|
80
|
+
savingsBytes: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface OptimizationReport {
|
|
84
|
+
duplicates: DuplicateGroup[];
|
|
85
|
+
deadRules: DeadRule[];
|
|
86
|
+
specificityConflicts: SpecificityConflict[];
|
|
87
|
+
animationConflicts: AnimationConflict[];
|
|
88
|
+
mediaQueryRedundancies: MediaQueryRedundancy[];
|
|
89
|
+
summary: {
|
|
90
|
+
totalIssues: number;
|
|
91
|
+
duplicatesCount: number;
|
|
92
|
+
deadCount: number;
|
|
93
|
+
specificityCount: number;
|
|
94
|
+
animationCount: number;
|
|
95
|
+
mediaQueryCount: number;
|
|
96
|
+
totalSavingsBytes: number;
|
|
97
|
+
totalSavingsKB: string;
|
|
98
|
+
};
|
|
99
|
+
formattedReport: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Duplicate Detection
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
function findDuplicates(rules: IRRule[]): DuplicateGroup[] {
|
|
107
|
+
const signatureMap = new Map<string, DuplicateGroup>();
|
|
108
|
+
|
|
109
|
+
for (const rule of rules) {
|
|
110
|
+
if (rule.isDead) continue;
|
|
111
|
+
if (rule.declarations.length < 3) continue; // Only flag substantial duplicates
|
|
112
|
+
|
|
113
|
+
// Create a signature from declarations only (not selectors)
|
|
114
|
+
const sorted = [...rule.declarations]
|
|
115
|
+
.sort((a, b) => a.property.localeCompare(b.property));
|
|
116
|
+
const signature = sorted.map(d => d.property + ':' + d.value).join(';');
|
|
117
|
+
|
|
118
|
+
const existing = signatureMap.get(signature);
|
|
119
|
+
if (existing) {
|
|
120
|
+
existing.occurrences.push({
|
|
121
|
+
selector: rule.selector,
|
|
122
|
+
file: rule.source.file,
|
|
123
|
+
line: rule.source.line,
|
|
124
|
+
component: rule.source.component,
|
|
125
|
+
});
|
|
126
|
+
existing.count++;
|
|
127
|
+
existing.savingsBytes += estimateRuleBytes(rule);
|
|
128
|
+
} else {
|
|
129
|
+
signatureMap.set(signature, {
|
|
130
|
+
signature,
|
|
131
|
+
occurrences: [{
|
|
132
|
+
selector: rule.selector,
|
|
133
|
+
file: rule.source.file,
|
|
134
|
+
line: rule.source.line,
|
|
135
|
+
component: rule.source.component,
|
|
136
|
+
}],
|
|
137
|
+
count: 1,
|
|
138
|
+
suggestion: '',
|
|
139
|
+
savingsBytes: 0,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Filter to actual duplicates and generate suggestions
|
|
145
|
+
const duplicates: DuplicateGroup[] = [];
|
|
146
|
+
for (const [, group] of signatureMap) {
|
|
147
|
+
if (group.count >= 2) {
|
|
148
|
+
group.suggestion = generateExtractSuggestion(group);
|
|
149
|
+
duplicates.push(group);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return duplicates.sort((a, b) => b.savingsBytes - a.savingsBytes);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function estimateRuleBytes(rule: IRRule): number {
|
|
157
|
+
let bytes = rule.selector.length + 3; // selector + {}
|
|
158
|
+
|
|
159
|
+
for (const decl of rule.declarations) {
|
|
160
|
+
bytes += decl.property.length + String(decl.value).length + 6; // prop: value;
|
|
161
|
+
|
|
162
|
+
}
|
|
163
|
+
return bytes;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function generateExtractSuggestion(group: DuplicateGroup): string {
|
|
167
|
+
const selectors = group.occurrences.map(o => o.selector).join(', ');
|
|
168
|
+
const files = [...new Set(group.occurrences.map(o => o.file).filter(Boolean))];
|
|
169
|
+
return 'Extract as shared recipe or component. Found in: ' +
|
|
170
|
+
(files.length > 0 ? files.join(', ') : selectors);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// Dead Rule Detection
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
function findDeadRules(rules: IRRule[]): DeadRule[] {
|
|
178
|
+
const dead: DeadRule[] = [];
|
|
179
|
+
|
|
180
|
+
for (const rule of rules) {
|
|
181
|
+
if (!rule.isDead) continue;
|
|
182
|
+
|
|
183
|
+
dead.push({
|
|
184
|
+
ruleId: rule.id,
|
|
185
|
+
selector: rule.selector,
|
|
186
|
+
file: rule.source.file,
|
|
187
|
+
line: rule.source.line,
|
|
188
|
+
reason: rule.meta.deathReason || 'Marked as dead by optimization pass',
|
|
189
|
+
bytesWasted: estimateRuleBytes(rule),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return dead;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// Specificity Conflict Detection
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
function findSpecificityConflicts(rules: IRRule[]): SpecificityConflict[] {
|
|
201
|
+
const conflicts: SpecificityConflict[] = [];
|
|
202
|
+
const alive = rules.filter(r => !r.isDead);
|
|
203
|
+
|
|
204
|
+
// Compare every pair of rules that target overlapping selectors
|
|
205
|
+
for (let i = 0; i < alive.length; i++) {
|
|
206
|
+
for (let j = i + 1; j < alive.length; j++) {
|
|
207
|
+
const a = alive[i];
|
|
208
|
+
const b = alive[j];
|
|
209
|
+
|
|
210
|
+
// Check selector overlap
|
|
211
|
+
if (!selectorsOverlap(a.selector, b.selector)) continue;
|
|
212
|
+
|
|
213
|
+
// Check if they set the same property
|
|
214
|
+
const aProps = new Set(a.declarations.map(d => d.property));
|
|
215
|
+
const bProps = new Set(b.declarations.map(d => d.property));
|
|
216
|
+
const overlap = [...aProps].filter(p => bProps.has(p));
|
|
217
|
+
|
|
218
|
+
if (overlap.length === 0) continue;
|
|
219
|
+
|
|
220
|
+
// Significant specificity difference?
|
|
221
|
+
const diff = Math.abs(a.specificity - b.specificity);
|
|
222
|
+
if (diff >= 100) {
|
|
223
|
+
const higher = a.specificity > b.specificity ? a : b;
|
|
224
|
+
const lower = a.specificity > b.specificity ? b : a;
|
|
225
|
+
|
|
226
|
+
conflicts.push({
|
|
227
|
+
higher: {
|
|
228
|
+
selector: higher.selector,
|
|
229
|
+
specificity: higher.specificity,
|
|
230
|
+
file: higher.source.file,
|
|
231
|
+
line: higher.source.line,
|
|
232
|
+
},
|
|
233
|
+
lower: {
|
|
234
|
+
selector: lower.selector,
|
|
235
|
+
specificity: lower.specificity,
|
|
236
|
+
file: lower.source.file,
|
|
237
|
+
line: lower.source.line,
|
|
238
|
+
},
|
|
239
|
+
property: overlap[0],
|
|
240
|
+
severity: diff >= 10000 ? 'warning' : 'info',
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return conflicts;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function selectorsOverlap(a: string, b: string): boolean {
|
|
250
|
+
// Simple: if they share common class names or element names
|
|
251
|
+
const partsA = a.split(/[\s>+~]+/).filter(Boolean);
|
|
252
|
+
const partsB = b.split(/[\s>+~]+/).filter(Boolean);
|
|
253
|
+
|
|
254
|
+
for (const pa of partsA) {
|
|
255
|
+
for (const pb of partsB) {
|
|
256
|
+
// Same class
|
|
257
|
+
if (pa.startsWith('.') && pb.startsWith('.') && pa === pb) return true;
|
|
258
|
+
// Same element
|
|
259
|
+
if (pa === pb && !pa.startsWith('.') && !pa.startsWith('#')) return true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// Animation Conflict Detection
|
|
267
|
+
// ============================================================================
|
|
268
|
+
|
|
269
|
+
function findAnimationConflicts(rules: IRRule[]): AnimationConflict[] {
|
|
270
|
+
const animationMap = new Map<string, AnimationConflict>();
|
|
271
|
+
|
|
272
|
+
for (const rule of rules) {
|
|
273
|
+
if (rule.isDead) continue;
|
|
274
|
+
|
|
275
|
+
for (const atRule of rule.atRules) {
|
|
276
|
+
if (atRule.type === 'keyframes' && atRule.name) {
|
|
277
|
+
const existing = animationMap.get(atRule.name);
|
|
278
|
+
if (existing) {
|
|
279
|
+
existing.locations.push({
|
|
280
|
+
file: rule.source.file,
|
|
281
|
+
line: rule.source.line,
|
|
282
|
+
selector: rule.selector,
|
|
283
|
+
});
|
|
284
|
+
existing.count++;
|
|
285
|
+
} else {
|
|
286
|
+
animationMap.set(atRule.name, {
|
|
287
|
+
name: atRule.name,
|
|
288
|
+
locations: [{
|
|
289
|
+
file: rule.source.file,
|
|
290
|
+
line: rule.source.line,
|
|
291
|
+
selector: rule.selector,
|
|
292
|
+
}],
|
|
293
|
+
count: 1,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return [...animationMap.values()].filter(a => a.count >= 2);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ============================================================================
|
|
304
|
+
// Media Query Redundancy Detection
|
|
305
|
+
// ============================================================================
|
|
306
|
+
|
|
307
|
+
function findMediaQueryRedundancies(rules: IRRule[]): MediaQueryRedundancy[] {
|
|
308
|
+
const queryMap = new Map<string, { count: number; files: Set<string> }>();
|
|
309
|
+
|
|
310
|
+
for (const rule of rules) {
|
|
311
|
+
if (rule.isDead) continue;
|
|
312
|
+
|
|
313
|
+
for (const atRule of rule.atRules) {
|
|
314
|
+
if (atRule.type === 'media' && atRule.query) {
|
|
315
|
+
const normalized = atRule.query.replace(/\s+/g, ' ').trim();
|
|
316
|
+
const existing = queryMap.get(normalized);
|
|
317
|
+
if (existing) {
|
|
318
|
+
existing.count++;
|
|
319
|
+
if (rule.source.file) existing.files.add(rule.source.file);
|
|
320
|
+
} else {
|
|
321
|
+
queryMap.set(normalized, {
|
|
322
|
+
count: 1,
|
|
323
|
+
files: new Set(rule.source.file ? [rule.source.file] : []),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const redundancies: MediaQueryRedundancy[] = [];
|
|
331
|
+
for (const [query, data] of queryMap) {
|
|
332
|
+
if (data.count >= 3) {
|
|
333
|
+
redundancies.push({
|
|
334
|
+
query,
|
|
335
|
+
count: data.count,
|
|
336
|
+
files: [...data.files],
|
|
337
|
+
suggestion: 'Extract as shared breakpoint: $breakpoints.' + generateBreakpointName(query),
|
|
338
|
+
savingsBytes: estimateMQSavings(query, data.count),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return redundancies.sort((a, b) => b.savingsBytes - a.savingsBytes);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function generateBreakpointName(query: string): string {
|
|
347
|
+
if (query.includes('768')) return 'md';
|
|
348
|
+
if (query.includes('1024')) return 'lg';
|
|
349
|
+
if (query.includes('1280')) return 'xl';
|
|
350
|
+
if (query.includes('640')) return 'sm';
|
|
351
|
+
return 'custom';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function estimateMQSavings(query: string, count: number): number {
|
|
355
|
+
const queryBytes = query.length + 12; // @media {} wrapper
|
|
356
|
+
return (count - 1) * queryBytes; // All but one could be eliminated
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ============================================================================
|
|
360
|
+
// Report Generator
|
|
361
|
+
// ============================================================================
|
|
362
|
+
|
|
363
|
+
function formatReport(report: OptimizationReport): string {
|
|
364
|
+
const lines: string[] = [
|
|
365
|
+
'═══════════════════════════════════════════',
|
|
366
|
+
' ChainCSS Source-Aware Optimization Report',
|
|
367
|
+
'═══════════════════════════════════════════',
|
|
368
|
+
'',
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
// Duplicates
|
|
372
|
+
if (report.duplicates.length > 0) {
|
|
373
|
+
lines.push('🔁 DUPLICATES (' + report.duplicates.length + ' found)');
|
|
374
|
+
for (const dup of report.duplicates.slice(0, 5)) {
|
|
375
|
+
const selectors = dup.occurrences.map(o => o.selector).join(' = ');
|
|
376
|
+
lines.push(' • ' + selectors);
|
|
377
|
+
lines.push(' → ' + dup.suggestion);
|
|
378
|
+
lines.push(' → Savings: ~' + dup.savingsBytes + 'B');
|
|
379
|
+
}
|
|
380
|
+
lines.push('');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Dead rules
|
|
384
|
+
if (report.deadRules.length > 0) {
|
|
385
|
+
lines.push('💀 DEAD CODE (' + report.deadRules.length + ' rules, ~' +
|
|
386
|
+
report.deadRules.reduce((s, d) => s + d.bytesWasted, 0) + 'B)');
|
|
387
|
+
for (const dead of report.deadRules.slice(0, 5)) {
|
|
388
|
+
const location = dead.file ? dead.file + (dead.line ? ':' + dead.line : '') : 'unknown';
|
|
389
|
+
lines.push(' • ' + dead.selector + ' (' + location + ') — ' + dead.reason);
|
|
390
|
+
}
|
|
391
|
+
lines.push('');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Specificity
|
|
395
|
+
if (report.specificityConflicts.length > 0) {
|
|
396
|
+
lines.push('⚔️ SPECIFICITY WARS (' + report.specificityConflicts.length + ' conflicts)');
|
|
397
|
+
for (const conflict of report.specificityConflicts.slice(0, 5)) {
|
|
398
|
+
lines.push(' • ' + conflict.higher.selector + ' (' + conflict.higher.specificity + ')');
|
|
399
|
+
lines.push(' overrides: ' + conflict.lower.selector + ' (' + conflict.lower.specificity + ')');
|
|
400
|
+
if (conflict.property) lines.push(' Property: ' + conflict.property);
|
|
401
|
+
}
|
|
402
|
+
lines.push('');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Animation conflicts
|
|
406
|
+
if (report.animationConflicts.length > 0) {
|
|
407
|
+
lines.push('🎬 ANIMATION CONFLICTS (' + report.animationConflicts.length + ' found)');
|
|
408
|
+
for (const ac of report.animationConflicts.slice(0, 5)) {
|
|
409
|
+
lines.push(' • @keyframes ' + ac.name + ' — defined ' + ac.count + ' times');
|
|
410
|
+
for (const loc of ac.locations) {
|
|
411
|
+
lines.push(' ' + (loc.file || 'unknown') + ' → ' + loc.selector);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
lines.push('');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Media queries
|
|
418
|
+
if (report.mediaQueryRedundancies.length > 0) {
|
|
419
|
+
lines.push('📱 MEDIA QUERY CONSOLIDATION (' + report.mediaQueryRedundancies.length + ' redundant)');
|
|
420
|
+
for (const mq of report.mediaQueryRedundancies.slice(0, 5)) {
|
|
421
|
+
lines.push(' • ' + mq.query + ' — used ' + mq.count + ' times');
|
|
422
|
+
lines.push(' → ' + mq.suggestion);
|
|
423
|
+
lines.push(' → Savings: ~' + mq.savingsBytes + 'B');
|
|
424
|
+
}
|
|
425
|
+
lines.push('');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Summary
|
|
429
|
+
const s = report.summary;
|
|
430
|
+
lines.push('📊 SUMMARY');
|
|
431
|
+
lines.push(' • ' + s.duplicatesCount + ' duplicates → extract recipes');
|
|
432
|
+
lines.push(' • ' + s.deadCount + ' dead rules → remove');
|
|
433
|
+
lines.push(' • ' + s.specificityCount + ' specificity issues → fix cascade');
|
|
434
|
+
lines.push(' • ' + s.animationCount + ' animation conflicts → scope names');
|
|
435
|
+
lines.push(' • ' + s.mediaQueryCount + ' redundant media queries → consolidate');
|
|
436
|
+
lines.push(' • Total potential savings: ' + s.totalSavingsKB);
|
|
437
|
+
lines.push('');
|
|
438
|
+
lines.push('═══════════════════════════════════════════');
|
|
439
|
+
|
|
440
|
+
return lines.join('\n');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ============================================================================
|
|
444
|
+
// Full Report Generator
|
|
445
|
+
// ============================================================================
|
|
446
|
+
|
|
447
|
+
function generateOptimizationReport(rules: IRRule[]): OptimizationReport {
|
|
448
|
+
const duplicates = findDuplicates(rules);
|
|
449
|
+
const deadRules = findDeadRules(rules);
|
|
450
|
+
const specificityConflicts = findSpecificityConflicts(rules);
|
|
451
|
+
const animationConflicts = findAnimationConflicts(rules);
|
|
452
|
+
const mediaQueryRedundancies = findMediaQueryRedundancies(rules);
|
|
453
|
+
|
|
454
|
+
const totalSavingsBytes =
|
|
455
|
+
duplicates.reduce((s, d) => s + d.savingsBytes, 0) +
|
|
456
|
+
deadRules.reduce((s, d) => s + d.bytesWasted, 0) +
|
|
457
|
+
mediaQueryRedundancies.reduce((s, m) => s + m.savingsBytes, 0);
|
|
458
|
+
|
|
459
|
+
const report: OptimizationReport = {
|
|
460
|
+
duplicates,
|
|
461
|
+
deadRules,
|
|
462
|
+
specificityConflicts,
|
|
463
|
+
animationConflicts,
|
|
464
|
+
mediaQueryRedundancies,
|
|
465
|
+
summary: {
|
|
466
|
+
totalIssues: duplicates.length + deadRules.length + specificityConflicts.length +
|
|
467
|
+
animationConflicts.length + mediaQueryRedundancies.length,
|
|
468
|
+
duplicatesCount: duplicates.length,
|
|
469
|
+
deadCount: deadRules.length,
|
|
470
|
+
specificityCount: specificityConflicts.length,
|
|
471
|
+
animationCount: animationConflicts.length,
|
|
472
|
+
mediaQueryCount: mediaQueryRedundancies.length,
|
|
473
|
+
totalSavingsBytes,
|
|
474
|
+
totalSavingsKB: totalSavingsBytes > 1000
|
|
475
|
+
? (totalSavingsBytes / 1000).toFixed(1) + 'KB'
|
|
476
|
+
: totalSavingsBytes + 'B',
|
|
477
|
+
},
|
|
478
|
+
formattedReport: '',
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
report.formattedReport = formatReport(report);
|
|
482
|
+
return report;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ============================================================================
|
|
486
|
+
// IR Pass
|
|
487
|
+
// ============================================================================
|
|
488
|
+
|
|
489
|
+
export const sourceOptimizerPass: IRPass = (ir: StyleIR): StyleIR => {
|
|
490
|
+
const report = generateOptimizationReport(ir.rules);
|
|
491
|
+
|
|
492
|
+
// Add top findings as diagnostics
|
|
493
|
+
for (const dup of report.duplicates.slice(0, 3)) {
|
|
494
|
+
ir.diagnostics.push({
|
|
495
|
+
id: 'source-dup-' + Date.now(),
|
|
496
|
+
nodeId: ir.rules[0]?.id || ir.id,
|
|
497
|
+
severity: 'warning',
|
|
498
|
+
message: 'Duplicate: ' + dup.occurrences.map(o => o.selector).join(' = ') + ' (' + dup.count + '×)',
|
|
499
|
+
suggestion: dup.suggestion,
|
|
500
|
+
pass: 'source-optimizer',
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
for (const dead of report.deadRules.slice(0, 3)) {
|
|
505
|
+
ir.diagnostics.push({
|
|
506
|
+
id: 'source-dead-' + Date.now(),
|
|
507
|
+
nodeId: dead.ruleId,
|
|
508
|
+
severity: 'info',
|
|
509
|
+
message: 'Dead rule: ' + dead.selector + ' — ' + dead.reason,
|
|
510
|
+
pass: 'source-optimizer',
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Store full report
|
|
515
|
+
ir.meta = ir.meta || {};
|
|
516
|
+
(ir.meta as any).optimizationReport = report;
|
|
517
|
+
|
|
518
|
+
return ir;
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// ============================================================================
|
|
522
|
+
// Standalone API
|
|
523
|
+
// ============================================================================
|
|
524
|
+
|
|
525
|
+
export function optimizeSource(rules: IRRule[]): OptimizationReport {
|
|
526
|
+
return generateOptimizationReport(rules);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export const sourceOptimizer = {
|
|
530
|
+
optimize: optimizeSource,
|
|
531
|
+
findDuplicates,
|
|
532
|
+
findDeadRules,
|
|
533
|
+
findSpecificityConflicts,
|
|
534
|
+
findAnimationConflicts,
|
|
535
|
+
findMediaQueryRedundancies,
|
|
536
|
+
report: generateOptimizationReport,
|
|
537
|
+
format: formatReport,
|
|
538
|
+
pass: sourceOptimizerPass,
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
export default sourceOptimizer;
|