0
# AST Visitors
1
2
Extensible AST visitor pattern for custom CSS transformations and analysis with type-safe interfaces, enabling JavaScript-based CSS processing and manipulation.
3
4
## Capabilities
5
6
### Compose Visitors Function
7
8
Combine multiple visitor objects into a single visitor for modular transformation pipelines.
9
10
```typescript { .api }
11
/**
12
* Composes multiple visitor objects into a single one
13
* @param visitors - Array of visitor objects to combine
14
* @returns Single composed visitor with all transformations
15
*/
16
function composeVisitors<C extends CustomAtRules>(
17
visitors: Visitor<C>[]
18
): Visitor<C>;
19
```
20
21
**Usage Examples:**
22
23
```typescript
24
import { composeVisitors, transform } from "lightningcss";
25
26
// Create individual visitors
27
const colorVisitor = {
28
Color(color) {
29
// Convert all red colors to brand color
30
if (color.type === 'rgb' && color.r === 255 && color.g === 0 && color.b === 0) {
31
return { type: 'rgb', r: 42, g: 86, b: 153 }; // Brand blue
32
}
33
return color;
34
}
35
};
36
37
const urlVisitor = {
38
Url(url) {
39
// Rewrite relative URLs to absolute
40
if (!url.url.startsWith('http') && !url.url.startsWith('data:')) {
41
return { ...url, url: `https://cdn.example.com/${url.url}` };
42
}
43
return url;
44
}
45
};
46
47
// Compose visitors
48
const combinedVisitor = composeVisitors([colorVisitor, urlVisitor]);
49
50
const result = transform({
51
filename: "styles.css",
52
code: new TextEncoder().encode(`
53
.logo { background: url('./logo.png'); color: red; }
54
.button { background: blue; color: red; }
55
`),
56
visitor: combinedVisitor,
57
minify: true
58
});
59
```
60
61
### Visitor Interface
62
63
Comprehensive visitor interface for intercepting and transforming all CSS AST nodes.
64
65
```typescript { .api }
66
interface Visitor<C extends CustomAtRules> {
67
/** Visit stylesheet before processing rules */
68
StyleSheet?(stylesheet: StyleSheet): StyleSheet<ReturnedDeclaration, ReturnedMediaQuery> | void;
69
/** Visit stylesheet after processing rules */
70
StyleSheetExit?(stylesheet: StyleSheet): StyleSheet<ReturnedDeclaration, ReturnedMediaQuery> | void;
71
72
/** Visit CSS rules (can be function or object with rule-type-specific visitors) */
73
Rule?: RuleVisitor | RuleVisitors<C>;
74
/** Visit CSS rules after processing contents */
75
RuleExit?: RuleVisitor | RuleVisitors<C>;
76
77
/** Visit CSS declarations (can be function or object with property-specific visitors) */
78
Declaration?: DeclarationVisitor | DeclarationVisitors;
79
/** Visit CSS declarations after processing values */
80
DeclarationExit?: DeclarationVisitor | DeclarationVisitors;
81
82
/** Visit URL values */
83
Url?(url: Url): Url | void;
84
/** Visit color values */
85
Color?(color: CssColor): CssColor | void;
86
/** Visit image values */
87
Image?(image: Image): Image | void;
88
/** Visit image values after processing */
89
ImageExit?(image: Image): Image | void;
90
91
/** Visit length values */
92
Length?(length: LengthValue): LengthValue | void;
93
/** Visit angle values */
94
Angle?(angle: Angle): Angle | void;
95
/** Visit ratio values */
96
Ratio?(ratio: Ratio): Ratio | void;
97
/** Visit resolution values */
98
Resolution?(resolution: Resolution): Resolution | void;
99
/** Visit time values */
100
Time?(time: Time): Time | void;
101
102
/** Visit custom identifier values */
103
CustomIdent?(ident: string): string | void;
104
/** Visit dashed identifier values */
105
DashedIdent?(ident: string): string | void;
106
107
/** Visit media queries */
108
MediaQuery?(query: MediaQuery): ReturnedMediaQuery | ReturnedMediaQuery[] | void;
109
/** Visit media queries after processing */
110
MediaQueryExit?(query: MediaQuery): ReturnedMediaQuery | ReturnedMediaQuery[] | void;
111
112
/** Visit @supports conditions */
113
SupportsCondition?(condition: SupportsCondition): SupportsCondition;
114
/** Visit @supports conditions after processing */
115
SupportsConditionExit?(condition: SupportsCondition): SupportsCondition;
116
117
/** Visit selectors */
118
Selector?(selector: Selector): Selector | Selector[] | void;
119
120
/** Visit CSS tokens (can be function or object with token-type-specific visitors) */
121
Token?: TokenVisitor | TokenVisitors;
122
/** Visit CSS functions (can be function or object with function-name-specific visitors) */
123
Function?: FunctionVisitor | { [name: string]: FunctionVisitor };
124
/** Visit CSS functions after processing arguments */
125
FunctionExit?: FunctionVisitor | { [name: string]: FunctionVisitor };
126
127
/** Visit CSS variables */
128
Variable?(variable: Variable): TokenReturnValue;
129
/** Visit CSS variables after processing */
130
VariableExit?(variable: Variable): TokenReturnValue;
131
132
/** Visit environment variables (can be function or object with env-name-specific visitors) */
133
EnvironmentVariable?: EnvironmentVariableVisitor | EnvironmentVariableVisitors;
134
/** Visit environment variables after processing */
135
EnvironmentVariableExit?: EnvironmentVariableVisitor | EnvironmentVariableVisitors;
136
}
137
```
138
139
### Rule-Specific Visitors
140
141
Visit specific types of CSS rules with type-safe interfaces.
142
143
```typescript { .api }
144
type RuleVisitors<C extends CustomAtRules> = {
145
/** Visit @media rules */
146
media?: RuleVisitor<MediaRule>;
147
/** Visit @import rules */
148
import?: RuleVisitor<ImportRule>;
149
/** Visit style rules (selectors + declarations) */
150
style?: RuleVisitor<StyleRule>;
151
/** Visit @keyframes rules */
152
keyframes?: RuleVisitor<KeyframesRule>;
153
/** Visit @font-face rules */
154
'font-face'?: RuleVisitor<FontFaceRule>;
155
/** Visit @page rules */
156
page?: RuleVisitor<PageRule>;
157
/** Visit @supports rules */
158
supports?: RuleVisitor<SupportsRule>;
159
/** Visit @counter-style rules */
160
'counter-style'?: RuleVisitor<CounterStyleRule>;
161
/** Visit @namespace rules */
162
namespace?: RuleVisitor<NamespaceRule>;
163
/** Visit @layer rules */
164
layer?: RuleVisitor<LayerRule>;
165
/** Visit @container rules */
166
container?: RuleVisitor<ContainerRule>;
167
/** Visit unknown at-rules */
168
unknown?: UnknownVisitors<UnknownAtRule>;
169
/** Visit custom at-rules */
170
custom?: CustomVisitors<C>;
171
};
172
173
type RuleVisitor<R> = (rule: R) => ReturnedRule | ReturnedRule[] | void;
174
```
175
176
**Usage Examples:**
177
178
```typescript
179
// Rule-specific transformation
180
const ruleVisitor = {
181
Rule: {
182
// Transform media queries
183
media(rule) {
184
// Convert old max-width syntax to modern range syntax
185
if (rule.value.query.mediaType === 'screen') {
186
// Transform query...
187
return rule;
188
}
189
return rule;
190
},
191
192
// Transform style rules
193
style(rule) {
194
// Add vendor prefixes to flex properties
195
const hasFlexDisplay = rule.value.declarations.declarations.some(
196
decl => decl.property === 'display' && decl.value === 'flex'
197
);
198
199
if (hasFlexDisplay) {
200
// Add -webkit-box, -moz-box, etc.
201
return {
202
...rule,
203
value: {
204
...rule.value,
205
declarations: {
206
...rule.value.declarations,
207
declarations: [
208
{ property: 'display', value: '-webkit-box' },
209
{ property: 'display', value: '-moz-box' },
210
...rule.value.declarations.declarations
211
]
212
}
213
}
214
};
215
}
216
return rule;
217
},
218
219
// Remove @import rules
220
import(rule) {
221
console.log(`Removing import: ${rule.value.url}`);
222
return void 0; // Remove rule
223
}
224
}
225
};
226
```
227
228
### Declaration-Specific Visitors
229
230
Visit specific CSS properties with type-safe value access.
231
232
```typescript { .api }
233
type DeclarationVisitors = {
234
/** Visit background properties */
235
background?: DeclarationVisitor<BackgroundDeclaration>;
236
/** Visit color properties */
237
color?: DeclarationVisitor<ColorDeclaration>;
238
/** Visit display properties */
239
display?: DeclarationVisitor<DisplayDeclaration>;
240
/** Visit margin properties */
241
margin?: DeclarationVisitor<MarginDeclaration>;
242
/** Visit padding properties */
243
padding?: DeclarationVisitor<PaddingDeclaration>;
244
/** Visit transform properties */
245
transform?: DeclarationVisitor<TransformDeclaration>;
246
/** Visit custom properties */
247
custom?: CustomPropertyVisitors | DeclarationVisitor<CustomProperty>;
248
// ... many more property-specific visitors
249
};
250
251
type DeclarationVisitor<P> = (property: P) => ReturnedDeclaration | ReturnedDeclaration[] | void;
252
```
253
254
**Usage Examples:**
255
256
```typescript
257
// Property-specific transformations
258
const declarationVisitor = {
259
Declaration: {
260
// Transform background properties
261
background(decl) {
262
// Convert background shorthand to individual properties for IE support
263
if (decl.property === 'background' && typeof decl.value === 'object') {
264
const individual = [];
265
if (decl.value.color) {
266
individual.push({ property: 'background-color', value: decl.value.color });
267
}
268
if (decl.value.image) {
269
individual.push({ property: 'background-image', value: decl.value.image });
270
}
271
return individual;
272
}
273
return decl;
274
},
275
276
// Transform custom properties
277
custom: {
278
'--primary-color'(decl) {
279
// Replace CSS variable with computed value
280
return {
281
property: 'color',
282
value: { type: 'rgb', r: 42, g: 86, b: 153 }
283
};
284
}
285
},
286
287
// Transform display properties
288
display(decl) {
289
// Add fallbacks for CSS Grid
290
if (decl.value === 'grid') {
291
return [
292
{ property: 'display', value: 'block' }, // Fallback
293
decl // Original
294
];
295
}
296
return decl;
297
}
298
}
299
};
300
```
301
302
### Token and Value Visitors
303
304
Visit individual CSS tokens and values for fine-grained transformations.
305
306
```typescript { .api }
307
type TokenVisitors = {
308
/** Visit identifier tokens */
309
ident?: (token: IdentToken) => TokenReturnValue;
310
/** Visit at-keyword tokens */
311
'at-keyword'?: (token: AtKeywordToken) => TokenReturnValue;
312
/** Visit hash tokens */
313
hash?: (token: HashToken) => TokenReturnValue;
314
/** Visit string tokens */
315
string?: (token: StringToken) => TokenReturnValue;
316
/** Visit number tokens */
317
number?: (token: NumberToken) => TokenReturnValue;
318
/** Visit percentage tokens */
319
percentage?: (token: PercentageToken) => TokenReturnValue;
320
/** Visit dimension tokens */
321
dimension?: (token: DimensionToken) => TokenReturnValue;
322
};
323
324
type TokenReturnValue = TokenOrValue | TokenOrValue[] | RawValue | void;
325
326
interface RawValue {
327
/** A raw string value which will be parsed like CSS. */
328
raw: string;
329
}
330
```
331
332
**Usage Examples:**
333
334
```typescript
335
// Token-level transformations
336
const tokenVisitor = {
337
Token: {
338
// Transform dimension tokens
339
dimension(token) {
340
// Convert px to rem
341
if (token.unit === 'px' && typeof token.value === 'number') {
342
return {
343
type: 'dimension',
344
value: token.value / 16, // Assuming 16px = 1rem
345
unit: 'rem'
346
};
347
}
348
return token;
349
},
350
351
// Transform string tokens
352
string(token) {
353
// Replace font family names
354
if (token.value === 'Arial') {
355
return { type: 'string', value: 'system-ui' };
356
}
357
return token;
358
},
359
360
// Transform identifier tokens
361
ident(token) {
362
// Replace color names
363
const colorMap = {
364
'red': { raw: '#ff0000' },
365
'blue': { raw: '#0000ff' }
366
};
367
return colorMap[token.value] || token;
368
}
369
},
370
371
// Function-specific transformations
372
Function: {
373
// Transform calc() functions
374
calc(fn) {
375
// Simplify calc expressions
376
if (fn.arguments.length === 1) {
377
const arg = fn.arguments[0];
378
if (arg.type === 'token' && arg.value.type === 'dimension') {
379
return arg.value; // Remove unnecessary calc()
380
}
381
}
382
return fn;
383
},
384
385
// Transform url() functions
386
url(fn) {
387
// Rewrite URLs
388
if (fn.arguments[0]?.type === 'token' && fn.arguments[0].value.type === 'string') {
389
const url = fn.arguments[0].value.value;
390
if (url.startsWith('./')) {
391
return {
392
...fn,
393
arguments: [{
394
...fn.arguments[0],
395
value: {
396
...fn.arguments[0].value,
397
value: `https://cdn.example.com/${url.slice(2)}`
398
}
399
}]
400
};
401
}
402
}
403
return fn;
404
}
405
}
406
};
407
```
408
409
### Complete Visitor Example
410
411
Real-world example combining multiple visitor types for comprehensive CSS transformation.
412
413
```typescript
414
import { transform, composeVisitors } from "lightningcss";
415
416
// Asset optimization visitor
417
const assetVisitor = {
418
Url(url) {
419
// Convert relative URLs to CDN URLs
420
if (!url.url.startsWith('http') && !url.url.startsWith('data:')) {
421
return { ...url, url: `https://cdn.example.com/assets/${url.url}` };
422
}
423
return url;
424
}
425
};
426
427
// Color standardization visitor
428
const colorVisitor = {
429
Color(color) {
430
// Standardize brand colors
431
const brandColors = {
432
'#ff0000': { type: 'rgb', r: 42, g: 86, b: 153 }, // Brand blue
433
'#00ff00': { type: 'rgb', r: 40, g: 167, b: 69 } // Brand green
434
};
435
436
if (color.type === 'rgb') {
437
const hex = `#${color.r.toString(16).padStart(2, '0')}${color.g.toString(16).padStart(2, '0')}${color.b.toString(16).padStart(2, '0')}`;
438
return brandColors[hex] || color;
439
}
440
return color;
441
}
442
};
443
444
// Legacy support visitor
445
const legacyVisitor = {
446
Declaration: {
447
display(decl) {
448
// Add IE fallbacks for flexbox
449
if (decl.value === 'flex') {
450
return [
451
{ property: 'display', value: '-ms-flexbox' },
452
{ property: 'display', value: '-webkit-flex' },
453
decl
454
];
455
}
456
return decl;
457
}
458
},
459
460
Rule: {
461
style(rule) {
462
// Add -webkit- prefixes for flexbox properties
463
const needsPrefixing = rule.value.declarations.declarations.some(
464
decl => ['align-items', 'justify-content', 'flex-direction'].includes(decl.property)
465
);
466
467
if (needsPrefixing) {
468
const prefixedDeclarations = rule.value.declarations.declarations.flatMap(decl => {
469
if (['align-items', 'justify-content', 'flex-direction'].includes(decl.property)) {
470
return [
471
{ property: `-webkit-${decl.property}`, value: decl.value },
472
decl
473
];
474
}
475
return [decl];
476
});
477
478
return {
479
...rule,
480
value: {
481
...rule.value,
482
declarations: {
483
...rule.value.declarations,
484
declarations: prefixedDeclarations
485
}
486
}
487
};
488
}
489
return rule;
490
}
491
}
492
};
493
494
// Compose all visitors
495
const fullVisitor = composeVisitors([assetVisitor, colorVisitor, legacyVisitor]);
496
497
// Apply comprehensive transformations
498
const result = transform({
499
filename: "app.css",
500
code: new TextEncoder().encode(`
501
.hero {
502
display: flex;
503
align-items: center;
504
background: url('./hero-bg.jpg');
505
color: #ff0000;
506
}
507
508
.button {
509
background: #00ff00;
510
justify-content: center;
511
}
512
`),
513
visitor: fullVisitor,
514
targets: { ie: 11 << 16 },
515
minify: true
516
});
517
518
console.log(new TextDecoder().decode(result.code));
519
// Output includes CDN URLs, brand colors, and IE-compatible flexbox properties
520
```
521
522
### Environment Variable Visitors
523
524
Visit CSS environment variables like `env(safe-area-inset-top)` for custom processing and polyfills.
525
526
```typescript { .api }
527
type EnvironmentVariableVisitor = (env: EnvironmentVariable) => TokenReturnValue;
528
529
type EnvironmentVariableVisitors = {
530
[name: string]: EnvironmentVariableVisitor;
531
};
532
```
533
534
**Usage Examples:**
535
536
```typescript
537
// Environment variable transformations
538
const envVisitor = {
539
EnvironmentVariable: {
540
// Handle safe area insets for iOS
541
'safe-area-inset-top'(env) {
542
// Provide fallback value for browsers that don't support env()
543
return { raw: 'max(env(safe-area-inset-top), 20px)' };
544
},
545
546
'safe-area-inset-bottom'(env) {
547
return { raw: 'max(env(safe-area-inset-bottom), 20px)' };
548
},
549
550
// Handle custom environment variables
551
'keyboard-height'(env) {
552
// Polyfill custom env() variables
553
return { raw: 'var(--keyboard-height, 0px)' };
554
}
555
},
556
557
// Generic environment variable handler
558
EnvironmentVariable(env) {
559
console.log(`Processing env variable: ${env.name}`);
560
// Add debug information or logging
561
return env;
562
}
563
};
564
565
const result = transform({
566
filename: "mobile.css",
567
code: new TextEncoder().encode(`
568
.safe-area {
569
padding-top: env(safe-area-inset-top);
570
padding-bottom: env(safe-area-inset-bottom);
571
margin-bottom: env(keyboard-height);
572
}
573
`),
574
visitor: envVisitor,
575
minify: true
576
});
577
```
578
579
### Advanced Type Mapping Visitors
580
581
Lightning CSS provides detailed type mappings for rule and declaration visitors that enable precise targeting of specific CSS constructs.
582
583
```typescript { .api }
584
// Rule type mapping for maximum specificity
585
type MappedRuleVisitors = {
586
[Name in Exclude<Rule['type'], 'unknown' | 'custom'>]?: RuleVisitor<RequiredValue<FindByType<Rule, Name>>>;
587
}
588
589
// Declaration type mapping for property-specific handling
590
type MappedDeclarationVisitors = {
591
[Name in Exclude<Declaration['property'], 'unparsed' | 'custom'>]?: DeclarationVisitor<FindProperty<Declaration, Name> | FindProperty<Declaration, 'unparsed'>>;
592
}
593
594
// Unknown rule visitors for handling non-standard at-rules
595
type UnknownVisitors<T> = {
596
[name: string]: RuleVisitor<T>;
597
}
598
599
// Custom rule visitors for user-defined at-rules
600
type CustomVisitors<T extends CustomAtRules> = {
601
[Name in keyof T]?: RuleVisitor<CustomAtRule<Name, T[Name]>>;
602
};
603
```
604
605
**Usage Examples:**
606
607
```typescript
608
// Type-safe rule handling with comprehensive coverage
609
const advancedVisitor = {
610
Rule: {
611
// Type-safe media rule handling
612
media(rule) {
613
// rule is automatically typed as MediaRule
614
if (rule.value.query.mediaType === 'print') {
615
// Remove print-specific rules in web builds
616
return void 0;
617
}
618
return rule;
619
},
620
621
// Type-safe keyframes handling
622
keyframes(rule) {
623
// rule is automatically typed as KeyframesRule
624
const name = rule.value.name;
625
if (name.startsWith('legacy-')) {
626
// Rename legacy animations
627
return {
628
...rule,
629
value: {
630
...rule.value,
631
name: name.replace('legacy-', 'modern-')
632
}
633
};
634
}
635
return rule;
636
},
637
638
// Handle unknown at-rules
639
unknown: {
640
'custom-layout'(rule) {
641
// Convert custom at-rule to standard CSS
642
console.log('Converting custom layout rule');
643
return { raw: `/* Converted: ${rule.prelude} */` };
644
}
645
}
646
}
647
};
648
```