chaincss 2.1.39 → 2.3.0

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.
Files changed (33) hide show
  1. package/dist/compiler/accessibility-engine.d.ts +57 -0
  2. package/dist/compiler/constraint-solver.d.ts +85 -0
  3. package/dist/compiler/css-if-transpiler.d.ts +33 -0
  4. package/dist/compiler/design-orchestrator.d.ts +119 -0
  5. package/dist/compiler/intent-api.d.ts +73 -0
  6. package/dist/compiler/intent-engine.d.ts +19 -1
  7. package/dist/compiler/layout-intelligence.d.ts +71 -0
  8. package/dist/compiler/pass-manager.d.ts +157 -0
  9. package/dist/compiler/pattern-learner.d.ts +112 -0
  10. package/dist/compiler/responsive-inference.d.ts +63 -0
  11. package/dist/compiler/scroll-timeline.d.ts +91 -0
  12. package/dist/compiler/semantic-tokens.d.ts +57 -0
  13. package/dist/compiler/source-optimizer.d.ts +109 -0
  14. package/dist/compiler/style-ir.d.ts +183 -0
  15. package/dist/index.d.ts +23 -0
  16. package/dist/index.js +4126 -2
  17. package/package.json +1 -1
  18. package/src/compiler/accessibility-engine.ts +502 -0
  19. package/src/compiler/constraint-solver.ts +407 -0
  20. package/src/compiler/css-if-transpiler.ts +117 -0
  21. package/src/compiler/design-orchestrator.ts +322 -0
  22. package/src/compiler/intent-api.ts +505 -0
  23. package/src/compiler/intent-engine.ts +291 -1
  24. package/src/compiler/layout-intelligence.ts +697 -0
  25. package/src/compiler/pass-manager.ts +657 -0
  26. package/src/compiler/pattern-learner.ts +398 -0
  27. package/src/compiler/responsive-inference.ts +415 -0
  28. package/src/compiler/scroll-timeline.ts +284 -0
  29. package/src/compiler/semantic-tokens.ts +468 -0
  30. package/src/compiler/source-optimizer.ts +541 -0
  31. package/src/compiler/style-ir.ts +495 -0
  32. package/src/index.ts +209 -0
  33. 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;