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.
- package/dist/compiler/accessibility-engine.d.ts +57 -0
- package/dist/compiler/constraint-solver.d.ts +85 -0
- package/dist/compiler/css-if-transpiler.d.ts +33 -0
- package/dist/compiler/design-orchestrator.d.ts +119 -0
- package/dist/compiler/intent-api.d.ts +73 -0
- package/dist/compiler/intent-engine.d.ts +19 -1
- 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/scroll-timeline.d.ts +91 -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 +23 -0
- package/dist/index.js +4126 -2
- package/package.json +1 -1
- package/src/compiler/accessibility-engine.ts +502 -0
- package/src/compiler/constraint-solver.ts +407 -0
- package/src/compiler/css-if-transpiler.ts +117 -0
- package/src/compiler/design-orchestrator.ts +322 -0
- package/src/compiler/intent-api.ts +505 -0
- package/src/compiler/intent-engine.ts +291 -1
- 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/scroll-timeline.ts +284 -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 +209 -0
- package/ROADMAP.md +0 -31
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
// src/compiler/pass-manager.ts
|
|
2
|
+
/**
|
|
3
|
+
* Multi-Pass Optimization Pipeline
|
|
4
|
+
*
|
|
5
|
+
* Coordinates all compiler passes in a defined, deterministic order.
|
|
6
|
+
* Each pass is a pure function: StyleIR → StyleIR.
|
|
7
|
+
*
|
|
8
|
+
* Architecture inspired by: LLVM, Babel, SWC, Rust compiler
|
|
9
|
+
*
|
|
10
|
+
* Pipeline order (optimized for maximum information gain per pass):
|
|
11
|
+
* 1. Intent Recovery — fix typos, add defaults
|
|
12
|
+
* 2. Unit Resolution — resolve units, constant fold
|
|
13
|
+
* 3. Validation — contrast checks, conflict detection
|
|
14
|
+
* 4. Specificity Sorting — order rules by specificity
|
|
15
|
+
* 5. Dead Elimination — remove unused selectors
|
|
16
|
+
* 6. Atomic Extraction — extract shared properties
|
|
17
|
+
* 7. Media Query Packing — group same-query rules
|
|
18
|
+
* 8. CSS if() Transpiling — emit native if() + fallback
|
|
19
|
+
* 9. CSS Compression — minify output
|
|
20
|
+
* 10. Diagnostics Export — collect all pass diagnostics
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { StyleIR, IRPass, IRRule, IRDeclaration } from './style-ir.js';
|
|
24
|
+
import { applyPass, countNodes, debugIR } from './style-ir.js';
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Types
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
export type PassName =
|
|
31
|
+
| 'intent-recovery'
|
|
32
|
+
| 'unit-resolution'
|
|
33
|
+
| 'validation'
|
|
34
|
+
| 'specificity-sort'
|
|
35
|
+
| 'dead-elimination'
|
|
36
|
+
| 'atomic-extraction'
|
|
37
|
+
| 'media-query-packing'
|
|
38
|
+
| 'css-if-transpile'
|
|
39
|
+
| 'css-compression'
|
|
40
|
+
| 'diagnostics-export';
|
|
41
|
+
|
|
42
|
+
export type PassPriority = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
|
|
43
|
+
|
|
44
|
+
export interface PassDefinition {
|
|
45
|
+
name: PassName;
|
|
46
|
+
priority: PassPriority;
|
|
47
|
+
description: string;
|
|
48
|
+
/** The actual transform function */
|
|
49
|
+
pass: IRPass;
|
|
50
|
+
/** Dependencies — these passes must run first */
|
|
51
|
+
requires: PassName[];
|
|
52
|
+
/** Whether this pass is enabled */
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface PassResult {
|
|
57
|
+
name: PassName;
|
|
58
|
+
duration: number;
|
|
59
|
+
nodesBefore: number;
|
|
60
|
+
nodesAfter: number;
|
|
61
|
+
changes: number;
|
|
62
|
+
errors: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface PipelineResult {
|
|
66
|
+
ir: StyleIR;
|
|
67
|
+
css: string;
|
|
68
|
+
results: PassResult[];
|
|
69
|
+
totalDuration: number;
|
|
70
|
+
summary: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Built-in Passes
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Pass 1: Intent Recovery
|
|
79
|
+
* Corrects typos, adds defaults for common patterns.
|
|
80
|
+
* Runs FIRST so all downstream passes see clean data.
|
|
81
|
+
*/
|
|
82
|
+
export const intentRecoveryPass: IRPass = (ir: StyleIR): StyleIR => {
|
|
83
|
+
for (const rule of ir.rules) {
|
|
84
|
+
for (const decl of rule.declarations) {
|
|
85
|
+
// Fix common value mistakes
|
|
86
|
+
if (decl.property === 'display' && decl.value === 'flexbox') {
|
|
87
|
+
decl.value = 'flex';
|
|
88
|
+
decl.history.push({
|
|
89
|
+
pass: 'intent-recovery',
|
|
90
|
+
action: 'corrected-value',
|
|
91
|
+
timestamp: Date.now(),
|
|
92
|
+
previous: 'flexbox',
|
|
93
|
+
reason: 'flexbox → flex',
|
|
94
|
+
});
|
|
95
|
+
// Add centering defaults
|
|
96
|
+
const hasJustify = rule.declarations.some(d => d.property === 'justifyContent');
|
|
97
|
+
const hasAlign = rule.declarations.some(d => d.property === 'alignItems');
|
|
98
|
+
if (!hasJustify) {
|
|
99
|
+
rule.declarations.push({
|
|
100
|
+
id: 'ir-auto-' + Date.now(),
|
|
101
|
+
property: 'justifyContent',
|
|
102
|
+
value: 'center',
|
|
103
|
+
history: [{
|
|
104
|
+
pass: 'intent-recovery',
|
|
105
|
+
action: 'added-default',
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
reason: 'Added flexbox centering default',
|
|
108
|
+
}],
|
|
109
|
+
meta: {},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (!hasAlign) {
|
|
113
|
+
rule.declarations.push({
|
|
114
|
+
id: 'ir-auto-' + Date.now() + 1,
|
|
115
|
+
property: 'alignItems',
|
|
116
|
+
value: 'center',
|
|
117
|
+
history: [{
|
|
118
|
+
pass: 'intent-recovery',
|
|
119
|
+
action: 'added-default',
|
|
120
|
+
timestamp: Date.now(),
|
|
121
|
+
reason: 'Added flexbox centering default',
|
|
122
|
+
}],
|
|
123
|
+
meta: {},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (decl.property === 'position' && decl.value === 'abs') {
|
|
128
|
+
decl.value = 'absolute';
|
|
129
|
+
decl.history.push({
|
|
130
|
+
pass: 'intent-recovery',
|
|
131
|
+
action: 'corrected-value',
|
|
132
|
+
timestamp: Date.now(),
|
|
133
|
+
previous: 'abs',
|
|
134
|
+
reason: 'abs → absolute',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return ir;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Pass 2: Unit Resolution
|
|
144
|
+
* Resolves math expressions, converts units where possible.
|
|
145
|
+
* Runs early so later passes see resolved values.
|
|
146
|
+
*/
|
|
147
|
+
export const unitResolutionPass: IRPass = (ir: StyleIR): StyleIR => {
|
|
148
|
+
// Resolve common unit patterns: if value is a number, it stays as-is
|
|
149
|
+
// (actual math resolution happens via math-engine, which can be plugged in)
|
|
150
|
+
for (const rule of ir.rules) {
|
|
151
|
+
for (const decl of rule.declarations) {
|
|
152
|
+
// Normalize number values
|
|
153
|
+
if (typeof decl.value === 'number') {
|
|
154
|
+
const unitless = ['opacity', 'zIndex', 'flex', 'fontWeight', 'lineHeight', 'order'];
|
|
155
|
+
if (!unitless.includes(decl.property)) {
|
|
156
|
+
decl.value = decl.value + 'px';
|
|
157
|
+
decl.history.push({
|
|
158
|
+
pass: 'unit-resolution',
|
|
159
|
+
action: 'added-unit',
|
|
160
|
+
timestamp: Date.now(),
|
|
161
|
+
previous: decl.value,
|
|
162
|
+
reason: 'Added px unit to number value',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return ir;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Pass 3: Validation
|
|
173
|
+
* Runs contrast checks, detects conflicts.
|
|
174
|
+
*/
|
|
175
|
+
export const validationPass: IRPass = (ir: StyleIR): StyleIR => {
|
|
176
|
+
for (const rule of ir.rules) {
|
|
177
|
+
const position = rule.declarations.find(d => d.property === 'position');
|
|
178
|
+
const zIndex = rule.declarations.find(d => d.property === 'zIndex' || d.property === 'z-index');
|
|
179
|
+
|
|
180
|
+
if (position && position.value === 'static' && zIndex) {
|
|
181
|
+
ir.diagnostics.push({
|
|
182
|
+
id: 'diag-' + Date.now(),
|
|
183
|
+
nodeId: rule.id,
|
|
184
|
+
severity: 'warning',
|
|
185
|
+
message: 'z-index has no effect on static positioned elements',
|
|
186
|
+
suggestion: 'Change position to relative, absolute, or fixed',
|
|
187
|
+
pass: 'validation',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check for flex properties on non-flex containers
|
|
192
|
+
const display = rule.declarations.find(d => d.property === 'display');
|
|
193
|
+
const hasFlexProps = rule.declarations.some(d =>
|
|
194
|
+
['justifyContent', 'alignItems', 'flexDirection', 'flexWrap'].includes(d.property)
|
|
195
|
+
);
|
|
196
|
+
if (hasFlexProps && (!display || (display.value !== 'flex' && display.value !== 'inline-flex'))) {
|
|
197
|
+
ir.diagnostics.push({
|
|
198
|
+
id: 'diag-' + Date.now() + 1,
|
|
199
|
+
nodeId: rule.id,
|
|
200
|
+
severity: 'warning',
|
|
201
|
+
message: 'Flex properties require display: flex or display: inline-flex',
|
|
202
|
+
pass: 'validation',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return ir;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Pass 4: Specificity Sorting
|
|
211
|
+
* Orders rules by specificity so the cascade is predictable.
|
|
212
|
+
*/
|
|
213
|
+
export const specificitySortPass: IRPass = (ir: StyleIR): StyleIR => {
|
|
214
|
+
// Calculate specificity for each rule
|
|
215
|
+
for (const rule of ir.rules) {
|
|
216
|
+
let a = 0, b = 0, c = 0;
|
|
217
|
+
const idMatches = rule.selector.match(/#[a-zA-Z0-9_-]+/g);
|
|
218
|
+
if (idMatches) a += idMatches.length;
|
|
219
|
+
const classMatches = rule.selector.match(/\.[a-zA-Z0-9_-]+/g);
|
|
220
|
+
if (classMatches) b += classMatches.length;
|
|
221
|
+
const pseudoMatches = rule.selector.match(/:[a-zA-Z-]+/g);
|
|
222
|
+
if (pseudoMatches) b += pseudoMatches.length;
|
|
223
|
+
const elemMatches = rule.selector.match(/^[a-zA-Z]+|[a-zA-Z]+(?=[.#[:])/g);
|
|
224
|
+
if (elemMatches) c += elemMatches.length;
|
|
225
|
+
|
|
226
|
+
rule.specificity = a * 10000 + b * 100 + c;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Sort by specificity (lowest first for proper cascade)
|
|
230
|
+
ir.rules.sort((a, b) => a.specificity - b.specificity);
|
|
231
|
+
return ir;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Pass 5: Dead Elimination
|
|
236
|
+
* Removes rules marked as dead.
|
|
237
|
+
*/
|
|
238
|
+
export const deadEliminationPass: IRPass = (ir: StyleIR): StyleIR => {
|
|
239
|
+
const before = ir.rules.length;
|
|
240
|
+
ir.rules = ir.rules.filter(r => !r.isDead);
|
|
241
|
+
const eliminated = before - ir.rules.length;
|
|
242
|
+
|
|
243
|
+
if (eliminated > 0) {
|
|
244
|
+
ir.diagnostics.push({
|
|
245
|
+
id: 'diag-dead-' + Date.now(),
|
|
246
|
+
nodeId: ir.id,
|
|
247
|
+
severity: 'info',
|
|
248
|
+
message: 'Eliminated ' + eliminated + ' dead rules',
|
|
249
|
+
pass: 'dead-elimination',
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return ir;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Pass 6: Atomic Extraction
|
|
257
|
+
* Identifies identical declarations across rules and marks them for atomic CSS.
|
|
258
|
+
*/
|
|
259
|
+
export const atomicExtractionPass: IRPass = (ir: StyleIR): StyleIR => {
|
|
260
|
+
const usageMap = new Map<string, number>();
|
|
261
|
+
|
|
262
|
+
// Count declaration occurrences
|
|
263
|
+
for (const rule of ir.rules) {
|
|
264
|
+
for (const decl of rule.declarations) {
|
|
265
|
+
const key = decl.property + ':' + decl.value;
|
|
266
|
+
usageMap.set(key, (usageMap.get(key) || 0) + 1);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Mark frequently used declarations as atomic candidates
|
|
271
|
+
for (const rule of ir.rules) {
|
|
272
|
+
for (const decl of rule.declarations) {
|
|
273
|
+
const key = decl.property + ':' + decl.value;
|
|
274
|
+
const usage = usageMap.get(key) || 0;
|
|
275
|
+
decl.meta.atomic = usage >= 3; // Threshold: used 3+ times
|
|
276
|
+
decl.meta.usageCount = usage;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return ir;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Pass 7: Media Query Packing
|
|
285
|
+
* Groups rules with the same media query together.
|
|
286
|
+
*/
|
|
287
|
+
export const mediaQueryPackingPass: IRPass = (ir: StyleIR): StyleIR => {
|
|
288
|
+
// Collect all media queries
|
|
289
|
+
const queryMap = new Map<string, IRRule[]>();
|
|
290
|
+
|
|
291
|
+
for (const rule of ir.rules) {
|
|
292
|
+
for (const atRule of rule.atRules) {
|
|
293
|
+
if (atRule.type === 'media' && atRule.query) {
|
|
294
|
+
const existing = queryMap.get(atRule.query) || [];
|
|
295
|
+
existing.push(rule);
|
|
296
|
+
queryMap.set(atRule.query, existing);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Groups found — no structural change needed for now
|
|
302
|
+
// Future: restructure IR to group same-query rules
|
|
303
|
+
return ir;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Pass 8: CSS if() Transpile
|
|
308
|
+
* Detects conditional patterns and emits native CSS if().
|
|
309
|
+
*/
|
|
310
|
+
export const cssIfTranspilePass: IRPass = (ir: StyleIR): StyleIR => {
|
|
311
|
+
for (const rule of ir.rules) {
|
|
312
|
+
if (rule.conditions.length > 0) {
|
|
313
|
+
rule.meta.hasCSSIf = true;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return ir;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Pass 9: CSS Compression
|
|
321
|
+
* Minifies the IR — shortens values, removes unnecessary data.
|
|
322
|
+
*/
|
|
323
|
+
export const cssCompressionPass: IRPass = (ir: StyleIR): StyleIR => {
|
|
324
|
+
for (const rule of ir.rules) {
|
|
325
|
+
for (const decl of rule.declarations) {
|
|
326
|
+
// Shorten hex colors
|
|
327
|
+
if (typeof decl.value === 'string' && /^#[0-9a-fA-F]{6}$/.test(decl.value)) {
|
|
328
|
+
const hex = decl.value;
|
|
329
|
+
if (hex[1] === hex[2] && hex[3] === hex[4] && hex[5] === hex[6]) {
|
|
330
|
+
decl.value = '#' + hex[1] + hex[3] + hex[5];
|
|
331
|
+
decl.history.push({
|
|
332
|
+
pass: 'css-compression',
|
|
333
|
+
action: 'shortened-hex',
|
|
334
|
+
timestamp: Date.now(),
|
|
335
|
+
previous: hex,
|
|
336
|
+
reason: 'Shortened hex color',
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Remove leading zeros from decimals
|
|
341
|
+
if (typeof decl.value === 'string' && /^0\.\d+/.test(decl.value)) {
|
|
342
|
+
const shortened = decl.value.replace(/^0\./, '.');
|
|
343
|
+
decl.value = shortened;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return ir;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Pass 10: Diagnostics Export
|
|
352
|
+
* Collects all diagnostics from passes for reporting.
|
|
353
|
+
*/
|
|
354
|
+
export const diagnosticsExportPass: IRPass = (ir: StyleIR): StyleIR => {
|
|
355
|
+
// Diagnostics are already collected in ir.diagnostics by other passes
|
|
356
|
+
// This pass ensures they're organized and deduplicated
|
|
357
|
+
const seen = new Set<string>();
|
|
358
|
+
const unique = [];
|
|
359
|
+
for (const diag of ir.diagnostics) {
|
|
360
|
+
const key = diag.nodeId + ':' + diag.message;
|
|
361
|
+
if (!seen.has(key)) {
|
|
362
|
+
seen.add(key);
|
|
363
|
+
unique.push(diag);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
ir.diagnostics = unique;
|
|
367
|
+
return ir;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// ============================================================================
|
|
371
|
+
// Default Pipeline Configuration
|
|
372
|
+
// ============================================================================
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The default pass pipeline — runs all passes in optimal order.
|
|
376
|
+
*/
|
|
377
|
+
export const DEFAULT_PIPELINE: PassDefinition[] = [
|
|
378
|
+
{
|
|
379
|
+
name: 'intent-recovery',
|
|
380
|
+
priority: 1,
|
|
381
|
+
description: 'Fix typos and add defaults for common patterns',
|
|
382
|
+
pass: intentRecoveryPass,
|
|
383
|
+
requires: [],
|
|
384
|
+
enabled: true,
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: 'unit-resolution',
|
|
388
|
+
priority: 2,
|
|
389
|
+
description: 'Resolve units and normalize values',
|
|
390
|
+
pass: unitResolutionPass,
|
|
391
|
+
requires: [],
|
|
392
|
+
enabled: true,
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: 'validation',
|
|
396
|
+
priority: 3,
|
|
397
|
+
description: 'Run contrast checks and conflict detection',
|
|
398
|
+
pass: validationPass,
|
|
399
|
+
requires: ['intent-recovery'],
|
|
400
|
+
enabled: true,
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: 'specificity-sort',
|
|
404
|
+
priority: 4,
|
|
405
|
+
description: 'Order rules by specificity',
|
|
406
|
+
pass: specificitySortPass,
|
|
407
|
+
requires: [],
|
|
408
|
+
enabled: true,
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
name: 'dead-elimination',
|
|
412
|
+
priority: 5,
|
|
413
|
+
description: 'Remove unused selectors',
|
|
414
|
+
pass: deadEliminationPass,
|
|
415
|
+
requires: ['specificity-sort'],
|
|
416
|
+
enabled: true,
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: 'atomic-extraction',
|
|
420
|
+
priority: 6,
|
|
421
|
+
description: 'Extract shared properties into atomic classes',
|
|
422
|
+
pass: atomicExtractionPass,
|
|
423
|
+
requires: ['unit-resolution'],
|
|
424
|
+
enabled: true,
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: 'media-query-packing',
|
|
428
|
+
priority: 7,
|
|
429
|
+
description: 'Group same-query rules together',
|
|
430
|
+
pass: mediaQueryPackingPass,
|
|
431
|
+
requires: ['specificity-sort'],
|
|
432
|
+
enabled: true,
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
name: 'css-if-transpile',
|
|
436
|
+
priority: 8,
|
|
437
|
+
description: 'Transpile conditional patterns to native CSS if()',
|
|
438
|
+
pass: cssIfTranspilePass,
|
|
439
|
+
requires: ['intent-recovery'],
|
|
440
|
+
enabled: true,
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
name: 'css-compression',
|
|
444
|
+
priority: 9,
|
|
445
|
+
description: 'Minify CSS output',
|
|
446
|
+
pass: cssCompressionPass,
|
|
447
|
+
requires: [],
|
|
448
|
+
enabled: true,
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: 'diagnostics-export',
|
|
452
|
+
priority: 10,
|
|
453
|
+
description: 'Collect and organize diagnostics',
|
|
454
|
+
pass: diagnosticsExportPass,
|
|
455
|
+
requires: ['validation'],
|
|
456
|
+
enabled: true,
|
|
457
|
+
},
|
|
458
|
+
];
|
|
459
|
+
|
|
460
|
+
// ============================================================================
|
|
461
|
+
// Pass Manager
|
|
462
|
+
// ============================================================================
|
|
463
|
+
|
|
464
|
+
export class PassManager {
|
|
465
|
+
private passes: PassDefinition[] = [];
|
|
466
|
+
private results: PassResult[] = [];
|
|
467
|
+
|
|
468
|
+
constructor(passes: PassDefinition[] = DEFAULT_PIPELINE) {
|
|
469
|
+
this.passes = passes.filter(p => p.enabled);
|
|
470
|
+
this.validateDependencies();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Validate that all pass dependencies are satisfied.
|
|
475
|
+
*/
|
|
476
|
+
private validateDependencies(): void {
|
|
477
|
+
const passNames = new Set(this.passes.map(p => p.name));
|
|
478
|
+
for (const pass of this.passes) {
|
|
479
|
+
for (const req of pass.requires) {
|
|
480
|
+
if (!passNames.has(req)) {
|
|
481
|
+
throw new Error(
|
|
482
|
+
'Pass "' + pass.name + '" requires "' + req + '" but it is not in the pipeline'
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Topological sort passes by dependencies.
|
|
491
|
+
* Passes with no dependencies run first.
|
|
492
|
+
*/
|
|
493
|
+
private sortByDependencies(): PassDefinition[] {
|
|
494
|
+
const sorted: PassDefinition[] = [];
|
|
495
|
+
const remaining = [...this.passes];
|
|
496
|
+
const satisfied = new Set<string>();
|
|
497
|
+
|
|
498
|
+
while (remaining.length > 0) {
|
|
499
|
+
const ready = remaining.findIndex(p =>
|
|
500
|
+
p.requires.every(req => satisfied.has(req))
|
|
501
|
+
);
|
|
502
|
+
if (ready === -1) {
|
|
503
|
+
throw new Error('Circular dependency detected in pass pipeline');
|
|
504
|
+
}
|
|
505
|
+
const pass = remaining.splice(ready, 1)[0];
|
|
506
|
+
sorted.push(pass);
|
|
507
|
+
satisfied.add(pass.name);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return sorted;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Run the full pipeline on an IR.
|
|
515
|
+
*/
|
|
516
|
+
run(ir: StyleIR): PipelineResult {
|
|
517
|
+
const startTime = Date.now();
|
|
518
|
+
const sorted = this.sortByDependencies();
|
|
519
|
+
this.results = [];
|
|
520
|
+
|
|
521
|
+
let current = ir;
|
|
522
|
+
const from = countNodes(ir);
|
|
523
|
+
|
|
524
|
+
for (const pass of sorted) {
|
|
525
|
+
const passStart = Date.now();
|
|
526
|
+
const before = countNodes(current);
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
current = pass.pass(current);
|
|
530
|
+
} catch (err) {
|
|
531
|
+
this.results.push({
|
|
532
|
+
name: pass.name,
|
|
533
|
+
duration: Date.now() - passStart,
|
|
534
|
+
nodesBefore: before.rules + before.declarations,
|
|
535
|
+
nodesAfter: before.rules + before.declarations,
|
|
536
|
+
changes: 0,
|
|
537
|
+
errors: [(err as Error).message],
|
|
538
|
+
});
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const after = countNodes(current);
|
|
543
|
+
this.results.push({
|
|
544
|
+
name: pass.name,
|
|
545
|
+
duration: Date.now() - passStart,
|
|
546
|
+
nodesBefore: before.rules + before.declarations,
|
|
547
|
+
nodesAfter: after.rules + after.declarations,
|
|
548
|
+
changes: Math.abs((after.rules + after.declarations) - (before.rules + before.declarations)),
|
|
549
|
+
errors: [],
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const totalDuration = Date.now() - startTime;
|
|
554
|
+
const to = countNodes(current);
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
ir: current,
|
|
558
|
+
css: '', // Will be generated separately
|
|
559
|
+
results: this.results,
|
|
560
|
+
totalDuration,
|
|
561
|
+
summary: 'Pipeline complete: ' + this.results.length + ' passes in ' + totalDuration + 'ms. ' +
|
|
562
|
+
'Nodes: ' + (from.rules + from.declarations) + ' → ' + (to.rules + to.declarations),
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Get results from the last run.
|
|
568
|
+
*/
|
|
569
|
+
getResults(): PassResult[] {
|
|
570
|
+
return this.results;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Print a human-readable report of pass results.
|
|
575
|
+
*/
|
|
576
|
+
report(): string {
|
|
577
|
+
const lines = [
|
|
578
|
+
'═══════════════════════════════════════════',
|
|
579
|
+
' ChainCSS Multi-Pass Pipeline Report',
|
|
580
|
+
'═══════════════════════════════════════════',
|
|
581
|
+
];
|
|
582
|
+
|
|
583
|
+
for (const result of this.results) {
|
|
584
|
+
const status = result.errors.length > 0 ? '❌' : '✓';
|
|
585
|
+
lines.push(
|
|
586
|
+
' ' + status + ' ' + result.name.padEnd(22) +
|
|
587
|
+
' ' + result.duration.toString().padStart(4) + 'ms' +
|
|
588
|
+
' nodes: ' + result.nodesBefore + ' → ' + result.nodesAfter
|
|
589
|
+
);
|
|
590
|
+
if (result.errors.length > 0) {
|
|
591
|
+
for (const err of result.errors) {
|
|
592
|
+
lines.push(' ⚠ ' + err);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
lines.push('═══════════════════════════════════════════');
|
|
598
|
+
return lines.join('\n');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Add a custom pass to the pipeline.
|
|
603
|
+
*/
|
|
604
|
+
addPass(pass: PassDefinition): this {
|
|
605
|
+
this.passes.push(pass);
|
|
606
|
+
return this;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Remove a pass by name.
|
|
611
|
+
*/
|
|
612
|
+
removePass(name: PassName): this {
|
|
613
|
+
this.passes = this.passes.filter(p => p.name !== name);
|
|
614
|
+
return this;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Enable/disable a pass.
|
|
619
|
+
*/
|
|
620
|
+
setPassEnabled(name: PassName, enabled: boolean): this {
|
|
621
|
+
const pass = this.passes.find(p => p.name === name);
|
|
622
|
+
if (pass) pass.enabled = enabled;
|
|
623
|
+
this.passes = this.passes.filter(p => p.enabled);
|
|
624
|
+
return this;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Get the list of pass names in execution order.
|
|
629
|
+
*/
|
|
630
|
+
getPassOrder(): PassName[] {
|
|
631
|
+
return this.sortByDependencies().map(p => p.name);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ============================================================================
|
|
636
|
+
// Quick API
|
|
637
|
+
// ============================================================================
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Run the default pipeline on an IR.
|
|
641
|
+
*/
|
|
642
|
+
export function runDefaultPipeline(ir: StyleIR): PipelineResult {
|
|
643
|
+
const manager = new PassManager(DEFAULT_PIPELINE.map(p => ({ ...p })));
|
|
644
|
+
return manager.run(ir);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ============================================================================
|
|
648
|
+
// Exports
|
|
649
|
+
// ============================================================================
|
|
650
|
+
|
|
651
|
+
export const passManager = {
|
|
652
|
+
PassManager,
|
|
653
|
+
runDefaultPipeline,
|
|
654
|
+
DEFAULT_PIPELINE,
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
export default passManager;
|