xvcl 2.6.0__py3-none-any.whl
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.
- xvcl/__init__.py +15 -0
- xvcl/compiler.py +1541 -0
- xvcl/py.typed +0 -0
- xvcl-2.6.0.dist-info/METADATA +1579 -0
- xvcl-2.6.0.dist-info/RECORD +8 -0
- xvcl-2.6.0.dist-info/WHEEL +4 -0
- xvcl-2.6.0.dist-info/entry_points.txt +2 -0
- xvcl-2.6.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1579 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xvcl
|
|
3
|
+
Version: 2.6.0
|
|
4
|
+
Summary: Extended VCL compiler with metaprogramming features for Fastly VCL
|
|
5
|
+
Author-email: Frank Denis <github@pureftpd.org>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: compiler,fastly,preprocessor,varnish,vcl
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src=".media/logo.png" alt="xvcl logo" width="300">
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
# xvcl
|
|
30
|
+
|
|
31
|
+
Supercharge your Fastly VCL with programming constructs like loops, functions, constants, and more.
|
|
32
|
+
|
|
33
|
+
**[Quick Reference Guide](xvcl-quick-reference.md)** - One-page syntax reference for all xvcl features
|
|
34
|
+
|
|
35
|
+
## Table of Contents
|
|
36
|
+
|
|
37
|
+
- [Introduction](#introduction)
|
|
38
|
+
- [Why Use xvcl?](#why-use-xvcl)
|
|
39
|
+
- [Installation](#installation)
|
|
40
|
+
- [Quick Start](#quick-start)
|
|
41
|
+
- [Core Features](#core-features)
|
|
42
|
+
- [Constants](#constants)
|
|
43
|
+
- [Template Expressions](#template-expressions)
|
|
44
|
+
- [For Loops](#for-loops)
|
|
45
|
+
- [Conditionals](#conditionals)
|
|
46
|
+
- [Variables](#variables)
|
|
47
|
+
- [File Includes](#file-includes)
|
|
48
|
+
- [Advanced Features](#advanced-features)
|
|
49
|
+
- [Inline Macros](#inline-macros)
|
|
50
|
+
- [Functions](#functions)
|
|
51
|
+
- [Command-Line Usage](#command-line-usage)
|
|
52
|
+
- [Integration with Falco](#integration-with-falco)
|
|
53
|
+
- [Best Practices](#best-practices)
|
|
54
|
+
- [Troubleshooting](#troubleshooting)
|
|
55
|
+
|
|
56
|
+
## Introduction
|
|
57
|
+
|
|
58
|
+
xvcl is a VCL transpiler that extends Fastly VCL with programming constructs that generate standard VCL code.
|
|
59
|
+
|
|
60
|
+
Think of it as a build step for your VCL: write enhanced VCL source files, run xvcl, and get clean, valid VCL output.
|
|
61
|
+
|
|
62
|
+
> **Tip:** For a quick syntax reference, see the [xvcl Quick Reference Guide](xvcl-quick-reference.md).
|
|
63
|
+
|
|
64
|
+
**What you can do:**
|
|
65
|
+
|
|
66
|
+
- Define constants once, use them everywhere
|
|
67
|
+
- Generate repetitive code with for loops
|
|
68
|
+
- Create reusable functions with return values
|
|
69
|
+
- Build zero-overhead macros for common patterns
|
|
70
|
+
- Conditionally compile code for different environments
|
|
71
|
+
- Split large VCL files into modular includes
|
|
72
|
+
|
|
73
|
+
**What you get:**
|
|
74
|
+
|
|
75
|
+
- Standard VCL output that works with Fastly
|
|
76
|
+
- Compile-time safety and error checking
|
|
77
|
+
- Reduced code duplication
|
|
78
|
+
- Better maintainability
|
|
79
|
+
|
|
80
|
+
## Why Use xvcl?
|
|
81
|
+
|
|
82
|
+
VCL is powerful but limited by design. You can't define functions with return values, you can't use loops, and managing constants means find-and-replace. This leads to:
|
|
83
|
+
|
|
84
|
+
- **Copy-paste errors:** Similar backends? Copy, paste, modify, repeat, make mistakes
|
|
85
|
+
- **Magic numbers:** Hardcoded values scattered throughout your code
|
|
86
|
+
- **Duplication:** Same logic repeated in multiple subroutines
|
|
87
|
+
- **Poor maintainability:** Change one thing, update it in 20 places
|
|
88
|
+
|
|
89
|
+
xvcl solves these problems by adding programming constructs that compile down to clean VCL.
|
|
90
|
+
|
|
91
|
+
### Real-World Example
|
|
92
|
+
|
|
93
|
+
**Without xvcl (manual, error-prone):**
|
|
94
|
+
|
|
95
|
+
```vcl
|
|
96
|
+
backend web1 {
|
|
97
|
+
.host = "web1.example.com";
|
|
98
|
+
.port = "80";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
backend web2 {
|
|
102
|
+
.host = "web2.example.com";
|
|
103
|
+
.port = "80";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
backend web3 {
|
|
107
|
+
.host = "web3.example.com";
|
|
108
|
+
.port = "80";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
sub vcl_recv {
|
|
112
|
+
if (req.http.Host == "web1.example.com") {
|
|
113
|
+
set req.backend = web1;
|
|
114
|
+
}
|
|
115
|
+
if (req.http.Host == "web2.example.com") {
|
|
116
|
+
set req.backend = web2;
|
|
117
|
+
}
|
|
118
|
+
if (req.http.Host == "web3.example.com") {
|
|
119
|
+
set req.backend = web3;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**With xvcl (clean, maintainable):**
|
|
125
|
+
|
|
126
|
+
```vcl
|
|
127
|
+
#const BACKENDS = ["web1", "web2", "web3"]
|
|
128
|
+
|
|
129
|
+
#for backend in BACKENDS
|
|
130
|
+
backend {{backend}} {
|
|
131
|
+
.host = "{{backend}}.example.com";
|
|
132
|
+
.port = "80";
|
|
133
|
+
}
|
|
134
|
+
#endfor
|
|
135
|
+
|
|
136
|
+
sub vcl_recv {
|
|
137
|
+
#for backend in BACKENDS
|
|
138
|
+
if (req.http.Host == "{{backend}}.example.com") {
|
|
139
|
+
set req.backend = {{backend}};
|
|
140
|
+
}
|
|
141
|
+
#endfor
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Adding a new backend? Just update the list. xvcl generates all the code.
|
|
146
|
+
|
|
147
|
+
## Installation
|
|
148
|
+
|
|
149
|
+
xvcl is a Python package. You need Python 3.9 or later.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Using pip
|
|
153
|
+
pip install xvcl
|
|
154
|
+
|
|
155
|
+
# Or install from source
|
|
156
|
+
pip install .
|
|
157
|
+
|
|
158
|
+
# Development installation with dev dependencies
|
|
159
|
+
pip install -e ".[dev]"
|
|
160
|
+
|
|
161
|
+
# Using uv (recommended for faster installation)
|
|
162
|
+
uv pip install xvcl
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
After installation, the `xvcl` command is available globally.
|
|
166
|
+
|
|
167
|
+
**No external dependencies** — uses only the Python standard library.
|
|
168
|
+
|
|
169
|
+
### Running without installation
|
|
170
|
+
|
|
171
|
+
You can run xvcl without installing it using `uvx`:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Run xvcl directly (uv will handle dependencies automatically)
|
|
175
|
+
uvx xvcl input.xvcl -o output.vcl
|
|
176
|
+
|
|
177
|
+
# With include paths
|
|
178
|
+
uvx xvcl input.xvcl -o output.vcl -I ./includes
|
|
179
|
+
|
|
180
|
+
# With debug mode
|
|
181
|
+
uvx xvcl input.xvcl -o output.vcl --debug
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
This is perfect for CI/CD pipelines or one-off usage without polluting your environment.
|
|
185
|
+
|
|
186
|
+
## Quick Start
|
|
187
|
+
|
|
188
|
+
Create an xvcl source file (use the `.xvcl` extension by convention):
|
|
189
|
+
|
|
190
|
+
**`hello.xvcl`:**
|
|
191
|
+
|
|
192
|
+
```vcl
|
|
193
|
+
#const MESSAGE = "Hello from xvcl!"
|
|
194
|
+
|
|
195
|
+
sub vcl_recv {
|
|
196
|
+
set req.http.X-Message = "{{MESSAGE}}";
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Run xvcl:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# If installed
|
|
204
|
+
xvcl hello.xvcl -o hello.vcl
|
|
205
|
+
|
|
206
|
+
# Or run without installing using uvx
|
|
207
|
+
uvx xvcl hello.xvcl -o hello.vcl
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Output **`hello.vcl`:**
|
|
211
|
+
|
|
212
|
+
```vcl
|
|
213
|
+
sub vcl_recv {
|
|
214
|
+
set req.http.X-Message = "Hello from xvcl!";
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Validate with Falco:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
falco lint hello.vcl
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Core Features
|
|
225
|
+
|
|
226
|
+
### Constants
|
|
227
|
+
|
|
228
|
+
Define named constants with type checking. Constants are evaluated at preprocessing time and substituted into your code.
|
|
229
|
+
|
|
230
|
+
**Syntax:**
|
|
231
|
+
|
|
232
|
+
```vcl
|
|
233
|
+
#const NAME TYPE = value
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Supported types:**
|
|
237
|
+
- `INTEGER` - Whole numbers
|
|
238
|
+
- `STRING` - Text strings
|
|
239
|
+
- `FLOAT` - Decimal numbers
|
|
240
|
+
- `BOOL` - True/False
|
|
241
|
+
|
|
242
|
+
**Example:**
|
|
243
|
+
|
|
244
|
+
```vcl
|
|
245
|
+
#const MAX_AGE INTEGER = 3600
|
|
246
|
+
#const ORIGIN STRING = "origin.example.com"
|
|
247
|
+
#const PRODUCTION BOOL = True
|
|
248
|
+
#const CACHE_VERSION FLOAT = 1.5
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Using constants in templates:**
|
|
252
|
+
|
|
253
|
+
```vcl
|
|
254
|
+
#const TTL = 300
|
|
255
|
+
#const BACKEND_HOST = "api.example.com"
|
|
256
|
+
|
|
257
|
+
backend F_api {
|
|
258
|
+
.host = "{{BACKEND_HOST}}";
|
|
259
|
+
.port = "443";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
sub vcl_fetch {
|
|
263
|
+
set beresp.ttl = {{TTL}}s;
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Why use constants?**
|
|
268
|
+
|
|
269
|
+
- Single source of truth for configuration values
|
|
270
|
+
- Easy to update across entire VCL
|
|
271
|
+
- Type safety prevents mistakes
|
|
272
|
+
- Self-documenting code
|
|
273
|
+
|
|
274
|
+
### Template Expressions
|
|
275
|
+
|
|
276
|
+
Embed Python expressions in your VCL using `{{expression}}` syntax. Expressions are evaluated at preprocessing time.
|
|
277
|
+
|
|
278
|
+
**Example:**
|
|
279
|
+
|
|
280
|
+
```vcl
|
|
281
|
+
#const PORT = 8080
|
|
282
|
+
|
|
283
|
+
sub vcl_recv {
|
|
284
|
+
set req.http.X-Port = "{{PORT}}";
|
|
285
|
+
set req.http.X-Double = "{{PORT * 2}}";
|
|
286
|
+
set req.http.X-Hex = "{{hex(PORT)}}";
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Output:**
|
|
291
|
+
|
|
292
|
+
```vcl
|
|
293
|
+
sub vcl_recv {
|
|
294
|
+
set req.http.X-Port = "8080";
|
|
295
|
+
set req.http.X-Double = "16160";
|
|
296
|
+
set req.http.X-Hex = "0x1f90";
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Available functions:**
|
|
301
|
+
|
|
302
|
+
- `range(n)` - Generate number sequences
|
|
303
|
+
- `len(list)` - Get list length
|
|
304
|
+
- `str(x)`, `int(x)` - Type conversions
|
|
305
|
+
- `hex(n)` - Hexadecimal conversion
|
|
306
|
+
- `format(x, fmt)` - Format values
|
|
307
|
+
- `enumerate(iterable)` - Enumerate with indices
|
|
308
|
+
- `min(...)`, `max(...)` - Min/max values
|
|
309
|
+
- `abs(n)` - Absolute value
|
|
310
|
+
|
|
311
|
+
**String formatting:**
|
|
312
|
+
|
|
313
|
+
```vcl
|
|
314
|
+
#const REGION = "us-east"
|
|
315
|
+
#const INDEX = 1
|
|
316
|
+
|
|
317
|
+
set req.backend = F_backend_{{REGION}}_{{INDEX}};
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### For Loops
|
|
321
|
+
|
|
322
|
+
Generate repetitive VCL code by iterating over ranges or lists.
|
|
323
|
+
|
|
324
|
+
**Syntax:**
|
|
325
|
+
|
|
326
|
+
```vcl
|
|
327
|
+
#for variable in iterable
|
|
328
|
+
// Code to repeat
|
|
329
|
+
#endfor
|
|
330
|
+
|
|
331
|
+
#for var1, var2 in iterable
|
|
332
|
+
// Tuple unpacking
|
|
333
|
+
#endfor
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Example 1: Range-based loop**
|
|
337
|
+
|
|
338
|
+
```vcl
|
|
339
|
+
#for i in range(5)
|
|
340
|
+
backend web{{i}} {
|
|
341
|
+
.host = "web{{i}}.example.com";
|
|
342
|
+
.port = "80";
|
|
343
|
+
}
|
|
344
|
+
#endfor
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Output:**
|
|
348
|
+
|
|
349
|
+
```vcl
|
|
350
|
+
backend web0 {
|
|
351
|
+
.host = "web0.example.com";
|
|
352
|
+
.port = "80";
|
|
353
|
+
}
|
|
354
|
+
backend web1 {
|
|
355
|
+
.host = "web1.example.com";
|
|
356
|
+
.port = "80";
|
|
357
|
+
}
|
|
358
|
+
// ... continues through web4
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Example 2: List iteration**
|
|
362
|
+
|
|
363
|
+
```vcl
|
|
364
|
+
#const REGIONS = ["us-east", "us-west", "eu-west"]
|
|
365
|
+
|
|
366
|
+
#for region in REGIONS
|
|
367
|
+
backend F_{{region}} {
|
|
368
|
+
.host = "{{region}}.example.com";
|
|
369
|
+
.port = "443";
|
|
370
|
+
.ssl = true;
|
|
371
|
+
}
|
|
372
|
+
#endfor
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Example 3: Nested loops**
|
|
376
|
+
|
|
377
|
+
```vcl
|
|
378
|
+
#const REGIONS = ["us", "eu"]
|
|
379
|
+
#const ENVS = ["prod", "staging"]
|
|
380
|
+
|
|
381
|
+
#for region in REGIONS
|
|
382
|
+
#for env in ENVS
|
|
383
|
+
backend {{region}}_{{env}} {
|
|
384
|
+
.host = "{{env}}.{{region}}.example.com";
|
|
385
|
+
.port = "443";
|
|
386
|
+
}
|
|
387
|
+
#endfor
|
|
388
|
+
#endfor
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Example 4: Tuple unpacking**
|
|
392
|
+
|
|
393
|
+
```vcl
|
|
394
|
+
#const BACKENDS = [("web", 8080), ("api", 9000), ("admin", 9001)]
|
|
395
|
+
|
|
396
|
+
#for name, port in BACKENDS
|
|
397
|
+
backend F_{{name}} {
|
|
398
|
+
.host = "{{name}}.example.com";
|
|
399
|
+
.port = "{{port}}";
|
|
400
|
+
}
|
|
401
|
+
#endfor
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Example 5: With enumerate**
|
|
405
|
+
|
|
406
|
+
```vcl
|
|
407
|
+
#const REGIONS = ["us-east", "us-west", "eu-west"]
|
|
408
|
+
|
|
409
|
+
#for idx, region in enumerate(REGIONS)
|
|
410
|
+
set req.http.X-Region-{{idx}} = "{{region}}";
|
|
411
|
+
#endfor
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Why use for loops?**
|
|
415
|
+
|
|
416
|
+
- Generate multiple similar backends
|
|
417
|
+
- Create ACL entries from lists
|
|
418
|
+
- Build routing logic programmatically
|
|
419
|
+
- Reduce copy-paste errors
|
|
420
|
+
|
|
421
|
+
### Conditionals
|
|
422
|
+
|
|
423
|
+
Conditionally include or exclude code based on compile-time conditions.
|
|
424
|
+
|
|
425
|
+
**Syntax:**
|
|
426
|
+
|
|
427
|
+
```vcl
|
|
428
|
+
#if condition
|
|
429
|
+
// Code when true
|
|
430
|
+
#else
|
|
431
|
+
// Code when false (optional)
|
|
432
|
+
#endif
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Example 1: Environment-specific configuration**
|
|
436
|
+
|
|
437
|
+
```vcl
|
|
438
|
+
#const PRODUCTION = True
|
|
439
|
+
#const DEBUG = False
|
|
440
|
+
|
|
441
|
+
sub vcl_recv {
|
|
442
|
+
#if PRODUCTION
|
|
443
|
+
set req.http.X-Environment = "production";
|
|
444
|
+
unset req.http.X-Debug-Info;
|
|
445
|
+
#else
|
|
446
|
+
set req.http.X-Environment = "development";
|
|
447
|
+
set req.http.X-Debug-Info = "Enabled";
|
|
448
|
+
#endif
|
|
449
|
+
|
|
450
|
+
#if DEBUG
|
|
451
|
+
set req.http.X-Request-ID = randomstr(16, "0123456789abcdef");
|
|
452
|
+
#endif
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**Example 2: Feature flags**
|
|
457
|
+
|
|
458
|
+
```vcl
|
|
459
|
+
#const ENABLE_NEW_ROUTING = True
|
|
460
|
+
#const ENABLE_RATE_LIMITING = False
|
|
461
|
+
|
|
462
|
+
sub vcl_recv {
|
|
463
|
+
#if ENABLE_NEW_ROUTING
|
|
464
|
+
call new_routing_logic;
|
|
465
|
+
#else
|
|
466
|
+
call legacy_routing_logic;
|
|
467
|
+
#endif
|
|
468
|
+
|
|
469
|
+
#if ENABLE_RATE_LIMITING
|
|
470
|
+
if (ratelimit.check_rate("client_" + client.ip, 1, 100, 60s, 1000s)) {
|
|
471
|
+
error 429 "Too Many Requests";
|
|
472
|
+
}
|
|
473
|
+
#endif
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**Why use conditionals?**
|
|
478
|
+
|
|
479
|
+
- Single codebase for multiple environments
|
|
480
|
+
- Easy feature flag management
|
|
481
|
+
- Dead code elimination (code in false branches isn't generated)
|
|
482
|
+
- Compile-time optimization
|
|
483
|
+
|
|
484
|
+
### Variables
|
|
485
|
+
|
|
486
|
+
Declare and initialize local variables in one step.
|
|
487
|
+
|
|
488
|
+
**Syntax:**
|
|
489
|
+
|
|
490
|
+
```vcl
|
|
491
|
+
#let name TYPE = expression;
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Example:**
|
|
495
|
+
|
|
496
|
+
```vcl
|
|
497
|
+
sub vcl_recv {
|
|
498
|
+
#let timestamp STRING = std.time(now, now);
|
|
499
|
+
#let cache_key STRING = req.url.path + req.http.Host;
|
|
500
|
+
|
|
501
|
+
set req.http.X-Timestamp = var.timestamp;
|
|
502
|
+
set req.hash = var.cache_key;
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
**Expands to:**
|
|
507
|
+
|
|
508
|
+
```vcl
|
|
509
|
+
sub vcl_recv {
|
|
510
|
+
declare local var.timestamp STRING;
|
|
511
|
+
set var.timestamp = std.time(now, now);
|
|
512
|
+
declare local var.cache_key STRING;
|
|
513
|
+
set var.cache_key = req.url.path + req.http.Host;
|
|
514
|
+
|
|
515
|
+
set req.http.X-Timestamp = var.timestamp;
|
|
516
|
+
set req.hash = var.cache_key;
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
**Why use #let?**
|
|
521
|
+
|
|
522
|
+
- Shorter syntax than separate declare + set
|
|
523
|
+
- Clear initialization point
|
|
524
|
+
- Reduces boilerplate
|
|
525
|
+
|
|
526
|
+
### File Includes
|
|
527
|
+
|
|
528
|
+
Split large VCL files into modular, reusable components.
|
|
529
|
+
|
|
530
|
+
**Syntax:**
|
|
531
|
+
|
|
532
|
+
```vcl
|
|
533
|
+
#include "path/to/file.xvcl"
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
**Example project structure:**
|
|
537
|
+
|
|
538
|
+
```
|
|
539
|
+
vcl/
|
|
540
|
+
├── main.xvcl
|
|
541
|
+
├── includes/
|
|
542
|
+
│ ├── backends.xvcl
|
|
543
|
+
│ ├── security.xvcl
|
|
544
|
+
│ └── routing.xvcl
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**`main.xvcl`:**
|
|
548
|
+
|
|
549
|
+
```vcl
|
|
550
|
+
#include "includes/backends.xvcl"
|
|
551
|
+
#include "includes/security.xvcl"
|
|
552
|
+
#include "includes/routing.xvcl"
|
|
553
|
+
|
|
554
|
+
sub vcl_recv {
|
|
555
|
+
call security_checks;
|
|
556
|
+
call routing_logic;
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**`includes/backends.xvcl`:**
|
|
561
|
+
|
|
562
|
+
```vcl
|
|
563
|
+
#const BACKENDS = ["web1", "web2", "web3"]
|
|
564
|
+
|
|
565
|
+
#for backend in BACKENDS
|
|
566
|
+
backend F_{{backend}} {
|
|
567
|
+
.host = "{{backend}}.example.com";
|
|
568
|
+
.port = "443";
|
|
569
|
+
}
|
|
570
|
+
#endfor
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**Include path resolution:**
|
|
574
|
+
|
|
575
|
+
1. Relative to the current file
|
|
576
|
+
2. Relative to include paths specified with `-I`
|
|
577
|
+
|
|
578
|
+
**Run with include paths:**
|
|
579
|
+
|
|
580
|
+
```bash
|
|
581
|
+
xvcl main.xvcl -o main.vcl -I ./vcl/includes
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
**Features:**
|
|
585
|
+
|
|
586
|
+
- **Include-once semantics:** Files are only included once even if referenced multiple times
|
|
587
|
+
- **Cycle detection:** Prevents circular includes
|
|
588
|
+
- **Shared constants:** Constants defined in included files are available to the parent
|
|
589
|
+
|
|
590
|
+
**Why use includes?**
|
|
591
|
+
|
|
592
|
+
- Organize large VCL projects
|
|
593
|
+
- Share common configurations across multiple VCL files
|
|
594
|
+
- Team collaboration (different files for different concerns)
|
|
595
|
+
- Reusable components library
|
|
596
|
+
|
|
597
|
+
## Advanced Features
|
|
598
|
+
|
|
599
|
+
### Inline Macros
|
|
600
|
+
|
|
601
|
+
Create zero-overhead text substitution macros. Unlike functions, macros are expanded inline at compile time with no runtime cost.
|
|
602
|
+
|
|
603
|
+
**Syntax:**
|
|
604
|
+
|
|
605
|
+
```vcl
|
|
606
|
+
#inline macro_name(param1, param2, ...)
|
|
607
|
+
expression
|
|
608
|
+
#endinline
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
**Example 1: String concatenation**
|
|
612
|
+
|
|
613
|
+
```vcl
|
|
614
|
+
#inline add_prefix(s)
|
|
615
|
+
"prefix-" + s
|
|
616
|
+
#endinline
|
|
617
|
+
|
|
618
|
+
#inline add_suffix(s)
|
|
619
|
+
s + "-suffix"
|
|
620
|
+
#endinline
|
|
621
|
+
|
|
622
|
+
sub vcl_recv {
|
|
623
|
+
set req.http.X-Modified = add_prefix("test");
|
|
624
|
+
set req.http.X-Both = add_prefix(add_suffix("middle"));
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
**Output:**
|
|
629
|
+
|
|
630
|
+
```vcl
|
|
631
|
+
sub vcl_recv {
|
|
632
|
+
set req.http.X-Modified = "prefix-" + "test";
|
|
633
|
+
set req.http.X-Both = "prefix-" + "middle" + "-suffix";
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**Example 2: Common patterns**
|
|
638
|
+
|
|
639
|
+
```vcl
|
|
640
|
+
#inline normalize_host(host)
|
|
641
|
+
std.tolower(regsub(host, "^www\.", ""))
|
|
642
|
+
#endinline
|
|
643
|
+
|
|
644
|
+
#inline cache_key(url, host)
|
|
645
|
+
digest.hash_md5(url + "|" + host)
|
|
646
|
+
#endinline
|
|
647
|
+
|
|
648
|
+
sub vcl_recv {
|
|
649
|
+
set req.http.X-Normalized = normalize_host(req.http.Host);
|
|
650
|
+
set req.hash = cache_key(req.url, req.http.Host);
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
**Output:**
|
|
655
|
+
|
|
656
|
+
```vcl
|
|
657
|
+
sub vcl_recv {
|
|
658
|
+
set req.http.X-Normalized = std.tolower(regsub(req.http.Host, "^www\.", ""));
|
|
659
|
+
set req.hash = digest.hash_md5(req.url + "|" + req.http.Host);
|
|
660
|
+
}
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
**Example 3: Operator precedence handling**
|
|
664
|
+
|
|
665
|
+
xvcl automatically handles operator precedence:
|
|
666
|
+
|
|
667
|
+
```vcl
|
|
668
|
+
#inline double(x)
|
|
669
|
+
x + x
|
|
670
|
+
#endinline
|
|
671
|
+
|
|
672
|
+
sub vcl_recv {
|
|
673
|
+
declare local var.result INTEGER;
|
|
674
|
+
set var.result = double(5) * 10; // Correctly expands to (5 + 5) * 10
|
|
675
|
+
}
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
**Macros vs Functions:**
|
|
679
|
+
|
|
680
|
+
| Feature | Macros | Functions |
|
|
681
|
+
| ------------- | ------------------- | ----------------------------- |
|
|
682
|
+
| Expansion | Compile-time inline | Runtime subroutine call |
|
|
683
|
+
| Overhead | None | Subroutine call + global vars |
|
|
684
|
+
| Return values | Expression only | Single or tuple |
|
|
685
|
+
| Use case | Simple expressions | Complex logic |
|
|
686
|
+
|
|
687
|
+
**When to use macros:**
|
|
688
|
+
|
|
689
|
+
- String manipulation patterns
|
|
690
|
+
- Simple calculations
|
|
691
|
+
- Common expressions repeated throughout code
|
|
692
|
+
- When you need zero runtime overhead
|
|
693
|
+
|
|
694
|
+
**When to use functions:**
|
|
695
|
+
|
|
696
|
+
- Complex logic with multiple statements
|
|
697
|
+
- Need to return multiple values
|
|
698
|
+
- Conditional logic or loops inside the reusable code
|
|
699
|
+
|
|
700
|
+
### Functions
|
|
701
|
+
|
|
702
|
+
Define reusable functions with parameters and return values. Functions are compiled into VCL subroutines.
|
|
703
|
+
|
|
704
|
+
**Syntax:**
|
|
705
|
+
|
|
706
|
+
```vcl
|
|
707
|
+
#def function_name(param1 TYPE, param2 TYPE, ...) -> RETURN_TYPE
|
|
708
|
+
// Function body
|
|
709
|
+
return value;
|
|
710
|
+
#enddef
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
**Example 1: Simple function**
|
|
714
|
+
|
|
715
|
+
```vcl
|
|
716
|
+
#def add(a INTEGER, b INTEGER) -> INTEGER
|
|
717
|
+
declare local var.sum INTEGER;
|
|
718
|
+
set var.sum = a + b;
|
|
719
|
+
return var.sum;
|
|
720
|
+
#enddef
|
|
721
|
+
|
|
722
|
+
sub vcl_recv {
|
|
723
|
+
declare local var.result INTEGER;
|
|
724
|
+
set var.result = add(5, 10);
|
|
725
|
+
set req.http.X-Sum = var.result;
|
|
726
|
+
}
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
**Example 2: String processing**
|
|
730
|
+
|
|
731
|
+
```vcl
|
|
732
|
+
#def normalize_path(path STRING) -> STRING
|
|
733
|
+
declare local var.result STRING;
|
|
734
|
+
set var.result = std.tolower(path);
|
|
735
|
+
set var.result = regsub(var.result, "/$", "");
|
|
736
|
+
return var.result;
|
|
737
|
+
#enddef
|
|
738
|
+
|
|
739
|
+
sub vcl_recv {
|
|
740
|
+
declare local var.clean_path STRING;
|
|
741
|
+
set var.clean_path = normalize_path(req.url.path);
|
|
742
|
+
set req.url = var.clean_path;
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
**Example 3: Functions with conditionals**
|
|
747
|
+
|
|
748
|
+
```vcl
|
|
749
|
+
#def should_cache(url STRING) -> BOOL
|
|
750
|
+
declare local var.cacheable BOOL;
|
|
751
|
+
|
|
752
|
+
if (url ~ "^/api/") {
|
|
753
|
+
set var.cacheable = false;
|
|
754
|
+
} else if (url ~ "\.(jpg|png|css|js)$") {
|
|
755
|
+
set var.cacheable = true;
|
|
756
|
+
} else {
|
|
757
|
+
set var.cacheable = false;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return var.cacheable;
|
|
761
|
+
#enddef
|
|
762
|
+
|
|
763
|
+
sub vcl_recv {
|
|
764
|
+
declare local var.can_cache BOOL;
|
|
765
|
+
set var.can_cache = should_cache(req.url.path);
|
|
766
|
+
|
|
767
|
+
if (var.can_cache) {
|
|
768
|
+
return(lookup);
|
|
769
|
+
} else {
|
|
770
|
+
return(pass);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
**Example 4: Tuple returns (multiple values)**
|
|
776
|
+
|
|
777
|
+
```vcl
|
|
778
|
+
#def parse_user_agent(ua STRING) -> (STRING, STRING)
|
|
779
|
+
declare local var.browser STRING;
|
|
780
|
+
declare local var.os STRING;
|
|
781
|
+
|
|
782
|
+
if (ua ~ "Chrome") {
|
|
783
|
+
set var.browser = "chrome";
|
|
784
|
+
} else if (ua ~ "Firefox") {
|
|
785
|
+
set var.browser = "firefox";
|
|
786
|
+
} else {
|
|
787
|
+
set var.browser = "other";
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (ua ~ "Windows") {
|
|
791
|
+
set var.os = "windows";
|
|
792
|
+
} else if (ua ~ "Mac") {
|
|
793
|
+
set var.os = "macos";
|
|
794
|
+
} else {
|
|
795
|
+
set var.os = "other";
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return var.browser, var.os;
|
|
799
|
+
#enddef
|
|
800
|
+
|
|
801
|
+
sub vcl_recv {
|
|
802
|
+
declare local var.browser STRING;
|
|
803
|
+
declare local var.os STRING;
|
|
804
|
+
|
|
805
|
+
set var.browser, var.os = parse_user_agent(req.http.User-Agent);
|
|
806
|
+
|
|
807
|
+
set req.http.X-Browser = var.browser;
|
|
808
|
+
set req.http.X-OS = var.os;
|
|
809
|
+
}
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
**Behind the scenes:**
|
|
813
|
+
|
|
814
|
+
Functions are compiled into VCL subroutines using global headers for parameter passing:
|
|
815
|
+
|
|
816
|
+
```vcl
|
|
817
|
+
// Your code:
|
|
818
|
+
set var.result = add(5, 10);
|
|
819
|
+
|
|
820
|
+
// Becomes:
|
|
821
|
+
set req.http.X-Func-add-a = std.itoa(5);
|
|
822
|
+
set req.http.X-Func-add-b = std.itoa(10);
|
|
823
|
+
call add;
|
|
824
|
+
set var.result = std.atoi(req.http.X-Func-add-Return);
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
xvcl generates the `sub add { ... }` implementation and handles all type conversions automatically.
|
|
828
|
+
|
|
829
|
+
**Function features:**
|
|
830
|
+
|
|
831
|
+
- **Type safety:** Parameters and returns are type-checked
|
|
832
|
+
- **Multiple returns:** Use tuple syntax to return multiple values
|
|
833
|
+
- **Automatic conversions:** INTEGER/FLOAT/BOOL are converted to/from STRING automatically
|
|
834
|
+
- **Scope annotations:** Generated subroutines work in all VCL scopes
|
|
835
|
+
|
|
836
|
+
**Why use functions?**
|
|
837
|
+
|
|
838
|
+
- Reusable complex logic
|
|
839
|
+
- Reduce code duplication
|
|
840
|
+
- Easier testing (test the function once)
|
|
841
|
+
- Better code organization
|
|
842
|
+
|
|
843
|
+
## Command-Line Usage
|
|
844
|
+
|
|
845
|
+
**Basic usage:**
|
|
846
|
+
|
|
847
|
+
```bash
|
|
848
|
+
xvcl input.xvcl -o output.vcl
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
**Options:**
|
|
852
|
+
|
|
853
|
+
| Option | Description |
|
|
854
|
+
| --------------- | --------------------------------------------------- |
|
|
855
|
+
| `input` | Input xvcl source file (required) |
|
|
856
|
+
| `-o, --output` | Output VCL file (default: replaces .xvcl with .vcl) |
|
|
857
|
+
| `-I, --include` | Add an include search path (repeatable) |
|
|
858
|
+
| `--debug` | Enable debug output with expansion traces |
|
|
859
|
+
| `--source-maps` | Add source map comments to output |
|
|
860
|
+
| `-v, --verbose` | Verbose output (alias for --debug) |
|
|
861
|
+
|
|
862
|
+
**Examples:**
|
|
863
|
+
|
|
864
|
+
```bash
|
|
865
|
+
# Basic compilation
|
|
866
|
+
xvcl main.xvcl -o main.vcl
|
|
867
|
+
|
|
868
|
+
# With include paths
|
|
869
|
+
xvcl main.xvcl -o main.vcl \
|
|
870
|
+
-I ./includes \
|
|
871
|
+
-I ./shared
|
|
872
|
+
|
|
873
|
+
# Debug mode (see expansion traces)
|
|
874
|
+
xvcl main.xvcl -o main.vcl --debug
|
|
875
|
+
|
|
876
|
+
# With source maps (track generated code origin)
|
|
877
|
+
xvcl main.xvcl -o main.vcl --source-maps
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
**Automatic output naming:**
|
|
881
|
+
|
|
882
|
+
If you don't specify `-o`, xvcl replaces `.xvcl` with `.vcl`:
|
|
883
|
+
|
|
884
|
+
```bash
|
|
885
|
+
# These are equivalent:
|
|
886
|
+
xvcl main.xvcl
|
|
887
|
+
xvcl main.xvcl -o main.vcl
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
**Debug mode output:**
|
|
891
|
+
|
|
892
|
+
```bash
|
|
893
|
+
$ xvcl example.xvcl --debug
|
|
894
|
+
|
|
895
|
+
[DEBUG] Processing file: example.xvcl
|
|
896
|
+
[DEBUG] Pass 1: Extracting constants
|
|
897
|
+
[DEBUG] Defined constant: MAX_AGE = 3600
|
|
898
|
+
[DEBUG] Pass 2: Processing includes
|
|
899
|
+
[DEBUG] Pass 3: Extracting inline macros
|
|
900
|
+
[DEBUG] Defined macro: add_prefix(s)
|
|
901
|
+
[DEBUG] Pass 4: Extracting functions
|
|
902
|
+
[DEBUG] Pass 5: Processing directives and generating code
|
|
903
|
+
[DEBUG] Processing #for at line 10
|
|
904
|
+
[DEBUG] Loop iterating 3 times
|
|
905
|
+
[DEBUG] Iteration 0: backend = web1
|
|
906
|
+
[DEBUG] Iteration 1: backend = web2
|
|
907
|
+
[DEBUG] Iteration 2: backend = web3
|
|
908
|
+
[DEBUG] Pass 6: Generating function subroutines
|
|
909
|
+
✓ Compiled example.xvcl -> example.vcl
|
|
910
|
+
Constants: 1
|
|
911
|
+
Macros: 1 (add_prefix)
|
|
912
|
+
Functions: 0
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
## Integration with Falco
|
|
916
|
+
|
|
917
|
+
xvcl generates standard VCL that you can use with Falco's full toolset.
|
|
918
|
+
|
|
919
|
+
**Recommended workflow:**
|
|
920
|
+
|
|
921
|
+
```bash
|
|
922
|
+
# 1. Write your xvcl source
|
|
923
|
+
vim main.xvcl
|
|
924
|
+
|
|
925
|
+
# 2. Compile with xvcl
|
|
926
|
+
xvcl main.xvcl -o main.vcl
|
|
927
|
+
|
|
928
|
+
# 3. Lint with Falco
|
|
929
|
+
falco lint main.vcl
|
|
930
|
+
|
|
931
|
+
# 4. Test with Falco
|
|
932
|
+
falco test main.vcl
|
|
933
|
+
|
|
934
|
+
# 5. Simulate with Falco
|
|
935
|
+
falco simulate main.vcl
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
**Makefile integration:**
|
|
939
|
+
|
|
940
|
+
```makefile
|
|
941
|
+
# Makefile
|
|
942
|
+
.PHONY: build lint test clean
|
|
943
|
+
|
|
944
|
+
XVCL = xvcl
|
|
945
|
+
SOURCES = $(wildcard *.xvcl)
|
|
946
|
+
OUTPUTS = $(SOURCES:.xvcl=.vcl)
|
|
947
|
+
|
|
948
|
+
build: $(OUTPUTS)
|
|
949
|
+
|
|
950
|
+
%.vcl: %.xvcl
|
|
951
|
+
$(XVCL) $< -o $@ -I ./includes
|
|
952
|
+
|
|
953
|
+
lint: build
|
|
954
|
+
falco lint *.vcl
|
|
955
|
+
|
|
956
|
+
test: build
|
|
957
|
+
falco test *.vcl
|
|
958
|
+
|
|
959
|
+
clean:
|
|
960
|
+
rm -f $(OUTPUTS)
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
**CI/CD integration:**
|
|
964
|
+
|
|
965
|
+
```yaml
|
|
966
|
+
# .github/workflows/vcl.yml
|
|
967
|
+
name: VCL CI
|
|
968
|
+
|
|
969
|
+
on: [push, pull_request]
|
|
970
|
+
|
|
971
|
+
jobs:
|
|
972
|
+
test:
|
|
973
|
+
runs-on: ubuntu-latest
|
|
974
|
+
steps:
|
|
975
|
+
- uses: actions/checkout@v3
|
|
976
|
+
|
|
977
|
+
- name: Install uv
|
|
978
|
+
uses: astral-sh/setup-uv@v2
|
|
979
|
+
|
|
980
|
+
- name: Install Falco
|
|
981
|
+
run: |
|
|
982
|
+
wget https://github.com/ysugimoto/falco/releases/latest/download/falco_linux_amd64
|
|
983
|
+
chmod +x falco_linux_amd64
|
|
984
|
+
sudo mv falco_linux_amd64 /usr/local/bin/falco
|
|
985
|
+
|
|
986
|
+
- name: Compile xvcl
|
|
987
|
+
run: |
|
|
988
|
+
uvx xvcl main.xvcl -o main.vcl
|
|
989
|
+
|
|
990
|
+
- name: Lint VCL
|
|
991
|
+
run: falco lint main.vcl
|
|
992
|
+
|
|
993
|
+
- name: Test VCL
|
|
994
|
+
run: falco test main.vcl
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
Using `uvx` eliminates the need to set up Python and install xvcl separately — it's handled automatically.
|
|
998
|
+
|
|
999
|
+
**Testing compiled VCL:**
|
|
1000
|
+
|
|
1001
|
+
You can write Falco tests for your generated VCL:
|
|
1002
|
+
|
|
1003
|
+
**`main.test.vcl`:**
|
|
1004
|
+
|
|
1005
|
+
```vcl
|
|
1006
|
+
// @suite: Backend routing tests
|
|
1007
|
+
|
|
1008
|
+
// @test: Should route to correct backend
|
|
1009
|
+
sub test_backend_routing {
|
|
1010
|
+
set req.http.Host = "web1.example.com";
|
|
1011
|
+
call vcl_recv;
|
|
1012
|
+
|
|
1013
|
+
assert.equal(req.backend, "web1");
|
|
1014
|
+
}
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
Run tests after compilation:
|
|
1018
|
+
|
|
1019
|
+
```bash
|
|
1020
|
+
xvcl main.xvcl -o main.vcl
|
|
1021
|
+
falco test main.vcl
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
## Best Practices
|
|
1025
|
+
|
|
1026
|
+
### 1. Use the `.xvcl` extension
|
|
1027
|
+
|
|
1028
|
+
Makes it clear which files are xvcl source files:
|
|
1029
|
+
|
|
1030
|
+
```
|
|
1031
|
+
✓ main.xvcl → main.vcl
|
|
1032
|
+
✗ main.vcl → main.vcl.processed
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
### 2. Keep constants at the top
|
|
1036
|
+
|
|
1037
|
+
```vcl
|
|
1038
|
+
// Good: Constants first, easy to find
|
|
1039
|
+
#const MAX_BACKENDS = 10
|
|
1040
|
+
#const PRODUCTION = True
|
|
1041
|
+
|
|
1042
|
+
#for i in range(MAX_BACKENDS)
|
|
1043
|
+
// ... use constant
|
|
1044
|
+
#endfor
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
### 3. Use descriptive constant names
|
|
1048
|
+
|
|
1049
|
+
```vcl
|
|
1050
|
+
// Good
|
|
1051
|
+
#const CACHE_TTL_SECONDS = 3600
|
|
1052
|
+
#const API_BACKEND_HOST = "api.example.com"
|
|
1053
|
+
|
|
1054
|
+
// Bad
|
|
1055
|
+
#const X = 3600
|
|
1056
|
+
#const B = "api.example.com"
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
### 4. Comment your macros and functions
|
|
1060
|
+
|
|
1061
|
+
```vcl
|
|
1062
|
+
// Normalizes a hostname by removing www prefix and converting to lowercase
|
|
1063
|
+
#inline normalize_host(host)
|
|
1064
|
+
std.tolower(regsub(host, "^www\.", ""))
|
|
1065
|
+
#endinline
|
|
1066
|
+
|
|
1067
|
+
// Parses User-Agent and returns (browser, os) tuple
|
|
1068
|
+
#def parse_user_agent(ua STRING) -> (STRING, STRING)
|
|
1069
|
+
// ...
|
|
1070
|
+
#enddef
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
### 5. Prefer macros for simple expressions, functions for complex logic
|
|
1074
|
+
|
|
1075
|
+
```vcl
|
|
1076
|
+
// Good: Simple expression = macro
|
|
1077
|
+
#inline cache_key(url, host)
|
|
1078
|
+
digest.hash_md5(url + "|" + host)
|
|
1079
|
+
#endinline
|
|
1080
|
+
|
|
1081
|
+
// Good: Complex logic = function
|
|
1082
|
+
#def should_cache(url STRING, method STRING) -> BOOL
|
|
1083
|
+
declare local var.result BOOL;
|
|
1084
|
+
if (method != "GET" && method != "HEAD") {
|
|
1085
|
+
set var.result = false;
|
|
1086
|
+
} else if (url ~ "^/api/") {
|
|
1087
|
+
set var.result = false;
|
|
1088
|
+
} else {
|
|
1089
|
+
set var.result = true;
|
|
1090
|
+
}
|
|
1091
|
+
return var.result;
|
|
1092
|
+
#enddef
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
### 6. Use includes for organization
|
|
1096
|
+
|
|
1097
|
+
```
|
|
1098
|
+
vcl/
|
|
1099
|
+
├── main.xvcl # Main entry point
|
|
1100
|
+
├── config.xvcl # Constants and configuration
|
|
1101
|
+
├── includes/
|
|
1102
|
+
│ ├── backends.xvcl
|
|
1103
|
+
│ ├── security.xvcl
|
|
1104
|
+
│ ├── routing.xvcl
|
|
1105
|
+
│ └── caching.xvcl
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
### 7. Version control both source and output
|
|
1109
|
+
|
|
1110
|
+
```gitignore
|
|
1111
|
+
# Include both in git
|
|
1112
|
+
*.xvcl
|
|
1113
|
+
*.vcl
|
|
1114
|
+
|
|
1115
|
+
# But gitignore generated files in CI
|
|
1116
|
+
# (if you regenerate on deploy)
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
**Or** only version control source files and regenerate on deployment:
|
|
1120
|
+
|
|
1121
|
+
```gitignore
|
|
1122
|
+
# Version control xvcl source only
|
|
1123
|
+
*.xvcl
|
|
1124
|
+
|
|
1125
|
+
# Ignore generated VCL
|
|
1126
|
+
*.vcl
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
Choose based on your deployment process.
|
|
1130
|
+
|
|
1131
|
+
### 8. Add source maps in development
|
|
1132
|
+
|
|
1133
|
+
```bash
|
|
1134
|
+
# Development: easier debugging
|
|
1135
|
+
xvcl main.xvcl -o main.vcl --source-maps
|
|
1136
|
+
|
|
1137
|
+
# Production: cleaner output
|
|
1138
|
+
xvcl main.xvcl -o main.vcl
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
Source maps add comments like:
|
|
1142
|
+
|
|
1143
|
+
```vcl
|
|
1144
|
+
// BEGIN INCLUDE: includes/backends.xvcl
|
|
1145
|
+
backend F_web1 { ... }
|
|
1146
|
+
// END INCLUDE: includes/backends.xvcl
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
### 9. Test incrementally
|
|
1150
|
+
|
|
1151
|
+
Don't write a massive source file and compile once. Test as you go:
|
|
1152
|
+
|
|
1153
|
+
```bash
|
|
1154
|
+
# Write a bit
|
|
1155
|
+
vim main.xvcl
|
|
1156
|
+
|
|
1157
|
+
# Compile
|
|
1158
|
+
xvcl main.xvcl
|
|
1159
|
+
|
|
1160
|
+
# Check output
|
|
1161
|
+
cat main.vcl
|
|
1162
|
+
|
|
1163
|
+
# Lint
|
|
1164
|
+
falco lint main.vcl
|
|
1165
|
+
|
|
1166
|
+
# Repeat
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
### 10. Use debug mode when things go wrong
|
|
1170
|
+
|
|
1171
|
+
```bash
|
|
1172
|
+
xvcl main.xvcl --debug
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
Shows exactly what xvcl is doing.
|
|
1176
|
+
|
|
1177
|
+
## Troubleshooting
|
|
1178
|
+
|
|
1179
|
+
### Error: "Name 'X' is not defined"
|
|
1180
|
+
|
|
1181
|
+
**Problem:** You're using a variable or constant that doesn't exist.
|
|
1182
|
+
|
|
1183
|
+
```vcl
|
|
1184
|
+
#const PORT = 8080
|
|
1185
|
+
|
|
1186
|
+
set req.http.X-Value = "{{PROT}}"; // Typo!
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
**Error:**
|
|
1190
|
+
|
|
1191
|
+
```
|
|
1192
|
+
Error at main.xvcl:3:
|
|
1193
|
+
Name 'PROT' is not defined
|
|
1194
|
+
Did you mean: PORT?
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
**Solution:** Check spelling. xvcl suggests similar names.
|
|
1198
|
+
|
|
1199
|
+
### Error: "Invalid #const syntax"
|
|
1200
|
+
|
|
1201
|
+
**Problem:** Malformed constant declaration.
|
|
1202
|
+
|
|
1203
|
+
```vcl
|
|
1204
|
+
#const PORT 8080 // Missing = sign
|
|
1205
|
+
#const = 8080 // Missing name
|
|
1206
|
+
#const PORT = STRING // Missing value
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
**Solution:** Use correct syntax:
|
|
1210
|
+
|
|
1211
|
+
```vcl
|
|
1212
|
+
#const PORT INTEGER = 8080
|
|
1213
|
+
```
|
|
1214
|
+
|
|
1215
|
+
### Error: "No matching #endfor for #for"
|
|
1216
|
+
|
|
1217
|
+
**Problem:** Missing closing keyword.
|
|
1218
|
+
|
|
1219
|
+
```vcl
|
|
1220
|
+
#for i in range(10)
|
|
1221
|
+
backend web{{i}} { ... }
|
|
1222
|
+
// Missing #endfor
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1225
|
+
**Solution:** Add the closing keyword:
|
|
1226
|
+
|
|
1227
|
+
```vcl
|
|
1228
|
+
#for i in range(10)
|
|
1229
|
+
backend web{{i}} { ... }
|
|
1230
|
+
#endfor
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
### Error: "Circular include detected"
|
|
1234
|
+
|
|
1235
|
+
**Problem:** File A includes file B which includes file A.
|
|
1236
|
+
|
|
1237
|
+
```
|
|
1238
|
+
main.xvcl includes util.xvcl
|
|
1239
|
+
util.xvcl includes main.xvcl
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
**Solution:** Restructure your includes. Create a shared file:
|
|
1243
|
+
|
|
1244
|
+
```
|
|
1245
|
+
main.xvcl includes shared.xvcl
|
|
1246
|
+
util.xvcl includes shared.xvcl
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
### Error: "Cannot find included file"
|
|
1250
|
+
|
|
1251
|
+
**Problem:** Include path is wrong or file doesn't exist.
|
|
1252
|
+
|
|
1253
|
+
```vcl
|
|
1254
|
+
#include "includes/backends.xvcl"
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
**Solution:** Check path and use `-I` flag:
|
|
1258
|
+
|
|
1259
|
+
```bash
|
|
1260
|
+
xvcl main.xvcl -o main.vcl -I ./includes
|
|
1261
|
+
```
|
|
1262
|
+
|
|
1263
|
+
### Generated VCL has syntax errors
|
|
1264
|
+
|
|
1265
|
+
**Problem:** xvcl generated invalid VCL.
|
|
1266
|
+
|
|
1267
|
+
**Solution:**
|
|
1268
|
+
|
|
1269
|
+
1. Check the generated output:
|
|
1270
|
+
```bash
|
|
1271
|
+
cat main.vcl
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
2. Find the problematic section
|
|
1275
|
+
|
|
1276
|
+
3. Trace back to source with `--source-maps`:
|
|
1277
|
+
|
|
1278
|
+
```bash
|
|
1279
|
+
xvcl main.xvcl -o main.vcl --source-maps
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
4. Fix the source file
|
|
1283
|
+
|
|
1284
|
+
### Macro expansion issues
|
|
1285
|
+
|
|
1286
|
+
**Problem:** Macro expands incorrectly.
|
|
1287
|
+
|
|
1288
|
+
```vcl
|
|
1289
|
+
#inline double(x)
|
|
1290
|
+
x + x
|
|
1291
|
+
#endinline
|
|
1292
|
+
|
|
1293
|
+
set var.result = double(1 + 2);
|
|
1294
|
+
// Expands to: (1 + 2) + (1 + 2) ✓ Correct
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
xvcl automatically adds parentheses when needed.
|
|
1298
|
+
|
|
1299
|
+
**If you see issues:** Check operator precedence in your macro definition.
|
|
1300
|
+
|
|
1301
|
+
### Function calls not working
|
|
1302
|
+
|
|
1303
|
+
**Problem:** Function call doesn't get replaced.
|
|
1304
|
+
|
|
1305
|
+
```vcl
|
|
1306
|
+
#def add(a INTEGER, b INTEGER) -> INTEGER
|
|
1307
|
+
return a + b;
|
|
1308
|
+
#enddef
|
|
1309
|
+
|
|
1310
|
+
set var.result = add(5, 10);
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
**Common causes:**
|
|
1314
|
+
|
|
1315
|
+
1. **Missing semicolon:** Function calls must end with `;`
|
|
1316
|
+
```vcl
|
|
1317
|
+
set var.result = add(5, 10); // ✓ Correct
|
|
1318
|
+
set var.result = add(5, 10) // ✗ Won't match
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
2. **Wrong number of arguments:**
|
|
1322
|
+
```vcl
|
|
1323
|
+
set var.result = add(5); // ✗ Expects 2 args
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
3. **Typo in function name:**
|
|
1327
|
+
```vcl
|
|
1328
|
+
set var.result = addr(5, 10); // ✗ Function 'addr' not defined
|
|
1329
|
+
```
|
|
1330
|
+
|
|
1331
|
+
### Performance issues
|
|
1332
|
+
|
|
1333
|
+
**Problem:** Compilation is slow.
|
|
1334
|
+
|
|
1335
|
+
**Common causes:**
|
|
1336
|
+
|
|
1337
|
+
1. **Large loops:** `#for i in range(10000)` generates 10,000 copies
|
|
1338
|
+
2. **Deep nesting:** Multiple nested loops or includes
|
|
1339
|
+
3. **Complex macros:** Heavily nested macro expansions
|
|
1340
|
+
|
|
1341
|
+
**Solutions:**
|
|
1342
|
+
|
|
1343
|
+
1. Reduce loop iterations if possible
|
|
1344
|
+
2. Use functions instead of generating everything inline
|
|
1345
|
+
3. Split into multiple source files
|
|
1346
|
+
4. Profile with `--debug` to see what's slow
|
|
1347
|
+
|
|
1348
|
+
### Getting help
|
|
1349
|
+
|
|
1350
|
+
**Check the error context:**
|
|
1351
|
+
|
|
1352
|
+
Errors show surrounding lines:
|
|
1353
|
+
|
|
1354
|
+
```
|
|
1355
|
+
Error at main.xvcl:15:
|
|
1356
|
+
Invalid #for syntax: #for in range(10)
|
|
1357
|
+
|
|
1358
|
+
Context:
|
|
1359
|
+
13: sub vcl_recv {
|
|
1360
|
+
14: // Generate backends
|
|
1361
|
+
→ 15: #for in range(10)
|
|
1362
|
+
16: backend web{{i}} { ... }
|
|
1363
|
+
17: #endfor
|
|
1364
|
+
18: }
|
|
1365
|
+
```
|
|
1366
|
+
|
|
1367
|
+
**Enable debug mode:**
|
|
1368
|
+
|
|
1369
|
+
```bash
|
|
1370
|
+
xvcl main.xvcl --debug
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
**Validate generated VCL:**
|
|
1374
|
+
|
|
1375
|
+
```bash
|
|
1376
|
+
falco lint main.vcl -vv
|
|
1377
|
+
```
|
|
1378
|
+
|
|
1379
|
+
The `-vv` flag shows detailed Falco errors.
|
|
1380
|
+
|
|
1381
|
+
---
|
|
1382
|
+
|
|
1383
|
+
## Examples Gallery
|
|
1384
|
+
|
|
1385
|
+
Here are complete, working examples you can use as starting points.
|
|
1386
|
+
|
|
1387
|
+
### Example 1: Multi-region backends
|
|
1388
|
+
|
|
1389
|
+
**`multi-region.xvcl`:**
|
|
1390
|
+
|
|
1391
|
+
```vcl
|
|
1392
|
+
#const REGIONS = ["us-east", "us-west", "eu-west", "ap-south"]
|
|
1393
|
+
#const DEFAULT_REGION = "us-east"
|
|
1394
|
+
|
|
1395
|
+
#for region in REGIONS
|
|
1396
|
+
backend F_origin_{{region}} {
|
|
1397
|
+
.host = "origin-{{region}}.example.com";
|
|
1398
|
+
.port = "443";
|
|
1399
|
+
.ssl = true;
|
|
1400
|
+
.connect_timeout = 5s;
|
|
1401
|
+
.first_byte_timeout = 30s;
|
|
1402
|
+
.between_bytes_timeout = 10s;
|
|
1403
|
+
}
|
|
1404
|
+
#endfor
|
|
1405
|
+
|
|
1406
|
+
sub vcl_recv {
|
|
1407
|
+
declare local var.region STRING;
|
|
1408
|
+
|
|
1409
|
+
// Detect region from client IP or header
|
|
1410
|
+
if (req.http.X-Region) {
|
|
1411
|
+
set var.region = req.http.X-Region;
|
|
1412
|
+
} else {
|
|
1413
|
+
set var.region = "{{DEFAULT_REGION}}";
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// Route to appropriate backend
|
|
1417
|
+
#for region in REGIONS
|
|
1418
|
+
if (var.region == "{{region}}") {
|
|
1419
|
+
set req.backend = F_origin_{{region}};
|
|
1420
|
+
}
|
|
1421
|
+
#endfor
|
|
1422
|
+
}
|
|
1423
|
+
```
|
|
1424
|
+
|
|
1425
|
+
### Example 2: Feature flag system
|
|
1426
|
+
|
|
1427
|
+
**`feature-flags.xvcl`:**
|
|
1428
|
+
|
|
1429
|
+
```vcl
|
|
1430
|
+
#const ENABLE_NEW_CACHE_POLICY = True
|
|
1431
|
+
#const ENABLE_WEBP_CONVERSION = True
|
|
1432
|
+
#const ENABLE_ANALYTICS = False
|
|
1433
|
+
#const ENABLE_DEBUG_HEADERS = False
|
|
1434
|
+
|
|
1435
|
+
sub vcl_recv {
|
|
1436
|
+
#if ENABLE_NEW_CACHE_POLICY
|
|
1437
|
+
// New cache policy with fine-grained control
|
|
1438
|
+
if (req.url.path ~ "\.(jpg|png|gif|css|js)$") {
|
|
1439
|
+
set req.http.X-Cache-Policy = "static";
|
|
1440
|
+
} else {
|
|
1441
|
+
set req.http.X-Cache-Policy = "dynamic";
|
|
1442
|
+
}
|
|
1443
|
+
#else
|
|
1444
|
+
// Legacy cache policy
|
|
1445
|
+
set req.http.X-Cache-Policy = "default";
|
|
1446
|
+
#endif
|
|
1447
|
+
|
|
1448
|
+
#if ENABLE_WEBP_CONVERSION
|
|
1449
|
+
if (req.http.Accept ~ "image/webp") {
|
|
1450
|
+
set req.http.X-Image-Format = "webp";
|
|
1451
|
+
}
|
|
1452
|
+
#endif
|
|
1453
|
+
|
|
1454
|
+
#if ENABLE_ANALYTICS
|
|
1455
|
+
set req.http.X-Analytics-ID = uuid.generate();
|
|
1456
|
+
#endif
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
sub vcl_deliver {
|
|
1460
|
+
#if ENABLE_DEBUG_HEADERS
|
|
1461
|
+
set resp.http.X-Cache-Status = resp.http.X-Cache;
|
|
1462
|
+
set resp.http.X-Backend = req.backend;
|
|
1463
|
+
set resp.http.X-Region = req.http.X-Region;
|
|
1464
|
+
#endif
|
|
1465
|
+
}
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
### Example 3: URL normalization library
|
|
1469
|
+
|
|
1470
|
+
**`url-utils.xvcl`:**
|
|
1471
|
+
|
|
1472
|
+
```vcl
|
|
1473
|
+
// Inline macros for common URL operations
|
|
1474
|
+
#inline strip_www(host)
|
|
1475
|
+
regsub(host, "^www\.", "")
|
|
1476
|
+
#endinline
|
|
1477
|
+
|
|
1478
|
+
#inline lowercase_host(host)
|
|
1479
|
+
std.tolower(host)
|
|
1480
|
+
#endinline
|
|
1481
|
+
|
|
1482
|
+
#inline normalize_host(host)
|
|
1483
|
+
lowercase_host(strip_www(host))
|
|
1484
|
+
#endinline
|
|
1485
|
+
|
|
1486
|
+
#inline remove_trailing_slash(path)
|
|
1487
|
+
regsub(path, "/$", "")
|
|
1488
|
+
#endinline
|
|
1489
|
+
|
|
1490
|
+
#inline remove_query_string(url)
|
|
1491
|
+
regsub(url, "\?.*$", "")
|
|
1492
|
+
#endinline
|
|
1493
|
+
|
|
1494
|
+
// Function for complex normalization
|
|
1495
|
+
#def normalize_url(url STRING, host STRING) -> STRING
|
|
1496
|
+
declare local var.result STRING;
|
|
1497
|
+
declare local var.clean_host STRING;
|
|
1498
|
+
declare local var.clean_path STRING;
|
|
1499
|
+
|
|
1500
|
+
set var.clean_host = normalize_host(host);
|
|
1501
|
+
set var.clean_path = remove_trailing_slash(url);
|
|
1502
|
+
set var.result = "https://" + var.clean_host + var.clean_path;
|
|
1503
|
+
|
|
1504
|
+
return var.result;
|
|
1505
|
+
#enddef
|
|
1506
|
+
|
|
1507
|
+
sub vcl_recv {
|
|
1508
|
+
declare local var.canonical_url STRING;
|
|
1509
|
+
set var.canonical_url = normalize_url(req.url.path, req.http.Host);
|
|
1510
|
+
set req.http.X-Canonical-URL = var.canonical_url;
|
|
1511
|
+
}
|
|
1512
|
+
```
|
|
1513
|
+
|
|
1514
|
+
### Example 4: Lookup table generation
|
|
1515
|
+
|
|
1516
|
+
**`lookup-tables.xvcl`:**
|
|
1517
|
+
|
|
1518
|
+
```vcl
|
|
1519
|
+
#const HEX_DIGITS = "0123456789abcdef"
|
|
1520
|
+
|
|
1521
|
+
// Generate a byte-to-hex lookup table
|
|
1522
|
+
table byte_to_hex STRING {
|
|
1523
|
+
#for i in range(256)
|
|
1524
|
+
"{{i}}": "{{format(i, '02x')}}"{{", " if i < 255 else ""}}
|
|
1525
|
+
#endfor
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// Generate HTTP status code descriptions
|
|
1529
|
+
#const STATUS_CODES = [200, 201, 301, 302, 400, 401, 403, 404, 500, 502, 503]
|
|
1530
|
+
#const STATUS_MESSAGES = ["OK", "Created", "Moved Permanently", "Found", "Bad Request", "Unauthorized", "Forbidden", "Not Found", "Internal Server Error", "Bad Gateway", "Service Unavailable"]
|
|
1531
|
+
|
|
1532
|
+
table status_messages STRING {
|
|
1533
|
+
#for i in range(len(STATUS_CODES))
|
|
1534
|
+
"{{STATUS_CODES[i]}}": "{{STATUS_MESSAGES[i]}}"{{", " if i < len(STATUS_CODES) - 1 else ""}}
|
|
1535
|
+
#endfor
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
sub vcl_deliver {
|
|
1539
|
+
// Use generated tables
|
|
1540
|
+
set resp.http.X-Status-Message = table.lookup(status_messages, resp.status);
|
|
1541
|
+
}
|
|
1542
|
+
```
|
|
1543
|
+
|
|
1544
|
+
---
|
|
1545
|
+
|
|
1546
|
+
## Summary
|
|
1547
|
+
|
|
1548
|
+
xvcl extends Fastly VCL with powerful programming constructs:
|
|
1549
|
+
|
|
1550
|
+
**Core features:**
|
|
1551
|
+
|
|
1552
|
+
- Constants - Single source of truth for configuration
|
|
1553
|
+
- Template expressions - Dynamic value substitution
|
|
1554
|
+
- For loops - Generate repetitive code
|
|
1555
|
+
- Conditionals - Environment-specific builds
|
|
1556
|
+
- Variables - Cleaner local variable syntax
|
|
1557
|
+
- Includes - Modular code organization
|
|
1558
|
+
|
|
1559
|
+
**Advanced features:**
|
|
1560
|
+
|
|
1561
|
+
- Inline macros - Zero-overhead text substitution
|
|
1562
|
+
- Functions - Reusable logic with return values
|
|
1563
|
+
|
|
1564
|
+
**Benefits:**
|
|
1565
|
+
|
|
1566
|
+
- Less code duplication
|
|
1567
|
+
- Fewer copy-paste errors
|
|
1568
|
+
- Better maintainability
|
|
1569
|
+
- Easier testing
|
|
1570
|
+
- Faster development
|
|
1571
|
+
|
|
1572
|
+
**Integration:**
|
|
1573
|
+
|
|
1574
|
+
- Works with Falco's full toolset
|
|
1575
|
+
- Standard VCL output
|
|
1576
|
+
- No runtime overhead
|
|
1577
|
+
- Easy CI/CD integration
|
|
1578
|
+
|
|
1579
|
+
Start simple, add complexity as needed. xvcl grows with your VCL projects.
|