@vercel/config 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -0
- package/dist/router.d.ts +76 -5
- package/dist/router.js +18 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,6 +147,194 @@ Set the path to a bulk redirects JSON file.
|
|
|
147
147
|
router.bulkRedirectsPath = './bulkRedirectsDemo.json';
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
## Conditional Routing
|
|
151
|
+
|
|
152
|
+
The SDK supports powerful conditional routing using `has` and `missing` conditions. These conditions can be added to rewrites, redirects, headers, and cache control rules.
|
|
153
|
+
|
|
154
|
+
### Condition Types
|
|
155
|
+
|
|
156
|
+
- `header`: Match HTTP headers
|
|
157
|
+
- `cookie`: Match cookies
|
|
158
|
+
- `host`: Match the request host
|
|
159
|
+
- `query`: Match query parameters
|
|
160
|
+
- `path`: Match the request path pattern
|
|
161
|
+
|
|
162
|
+
### Simple Presence Check
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// Only rewrite if x-api-key header exists
|
|
166
|
+
router.rewrite('/api/(.*)', 'https://backend.com/$1', {
|
|
167
|
+
has: [
|
|
168
|
+
{ type: 'header', key: 'x-api-key' }
|
|
169
|
+
]
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Redirect if auth-token cookie is missing
|
|
173
|
+
router.redirect('/dashboard', '/login', {
|
|
174
|
+
missing: [
|
|
175
|
+
{ type: 'cookie', key: 'auth-token' }
|
|
176
|
+
]
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Conditional Operators
|
|
181
|
+
|
|
182
|
+
The SDK supports advanced matching operators for more complex conditions:
|
|
183
|
+
|
|
184
|
+
#### Equality Operators
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// Exact match using 'eq'
|
|
188
|
+
router.rewrite('/api/(.*)', 'https://backend.com/$1', {
|
|
189
|
+
has: [
|
|
190
|
+
{ type: 'header', key: 'x-api-version', eq: 'v2' }
|
|
191
|
+
]
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Not equal using 'neq'
|
|
195
|
+
router.redirect('/beta/(.*)', '/stable/$1', {
|
|
196
|
+
has: [
|
|
197
|
+
{ type: 'cookie', key: 'beta-access', neq: 'granted' }
|
|
198
|
+
]
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### Inclusion Operators
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// Must be one of (inclusion)
|
|
206
|
+
router.rewrite('/admin/(.*)', 'https://admin.backend.com/$1', {
|
|
207
|
+
has: [
|
|
208
|
+
{ type: 'header', key: 'x-user-role', inc: ['admin', 'moderator', 'superuser'] }
|
|
209
|
+
]
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Must NOT be one of (non-inclusion)
|
|
213
|
+
router.redirect('/public/(.*)', '/private/$1', {
|
|
214
|
+
has: [
|
|
215
|
+
{ type: 'header', key: 'x-user-role', ninc: ['guest', 'anonymous'] }
|
|
216
|
+
]
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### String Pattern Operators
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// Starts with (prefix)
|
|
224
|
+
router.rewrite('/staging/(.*)', 'https://staging.backend.com/$1', {
|
|
225
|
+
has: [
|
|
226
|
+
{ type: 'cookie', key: 'session', pre: 'staging-' }
|
|
227
|
+
]
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Ends with (suffix)
|
|
231
|
+
router.redirect('/dev/(.*)', '/development/$1', {
|
|
232
|
+
has: [
|
|
233
|
+
{ type: 'header', key: 'x-environment', suf: '-dev' }
|
|
234
|
+
]
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### Numeric Comparison Operators
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
// Greater than
|
|
242
|
+
router.rewrite('/api/v3/(.*)', 'https://api-v3.backend.com/$1', {
|
|
243
|
+
has: [
|
|
244
|
+
{ type: 'query', key: 'version', gt: 2 }
|
|
245
|
+
]
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Greater than or equal
|
|
249
|
+
router.rewrite('/premium/(.*)', '/premium-content/$1', {
|
|
250
|
+
has: [
|
|
251
|
+
{ type: 'header', key: 'x-subscription-tier', gte: 3 }
|
|
252
|
+
]
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Less than
|
|
256
|
+
router.redirect('/legacy/(.*)', '/upgrade/$1', {
|
|
257
|
+
has: [
|
|
258
|
+
{ type: 'query', key: 'api-version', lt: 2 }
|
|
259
|
+
]
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Less than or equal
|
|
263
|
+
router.rewrite('/free/(.*)', '/free-tier/$1', {
|
|
264
|
+
has: [
|
|
265
|
+
{ type: 'header', key: 'x-plan', lte: 1 }
|
|
266
|
+
]
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Host and Path Matching
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// Host matching (no key required)
|
|
274
|
+
router.redirect('/(.*)', 'https://www.example.com/$1', {
|
|
275
|
+
has: [
|
|
276
|
+
{ type: 'host', value: 'example.com' }
|
|
277
|
+
]
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Path pattern matching (no key required)
|
|
281
|
+
router.rewrite('/(.*)', '/internal/$1', {
|
|
282
|
+
has: [
|
|
283
|
+
{ type: 'path', value: '^/api/v[0-9]+/.*' }
|
|
284
|
+
]
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Multiple Conditions
|
|
289
|
+
|
|
290
|
+
All conditions in a `has` or `missing` array must match (AND logic):
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
router.rewrite('/secure/(.*)', 'https://secure.backend.com/$1', {
|
|
294
|
+
has: [
|
|
295
|
+
{ type: 'header', key: 'x-api-key' },
|
|
296
|
+
{ type: 'header', key: 'x-user-role', inc: ['admin', 'superuser'] },
|
|
297
|
+
{ type: 'cookie', key: 'session', pre: 'secure-' },
|
|
298
|
+
{ type: 'query', key: 'version', gte: 2 }
|
|
299
|
+
]
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Combining with Transforms
|
|
304
|
+
|
|
305
|
+
You can combine conditions with transforms for powerful routing logic:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
router.rewrite('/api/users/:userId', 'https://backend.com/users/$1', ({ userId, env }) => ({
|
|
309
|
+
has: [
|
|
310
|
+
{ type: 'header', key: 'authorization', pre: 'Bearer ' },
|
|
311
|
+
{ type: 'header', key: 'x-api-version', gte: 2 }
|
|
312
|
+
],
|
|
313
|
+
missing: [
|
|
314
|
+
{ type: 'header', key: 'x-deprecated-header' }
|
|
315
|
+
],
|
|
316
|
+
requestHeaders: {
|
|
317
|
+
'x-user-id': userId,
|
|
318
|
+
'x-internal-key': env.INTERNAL_API_KEY
|
|
319
|
+
}
|
|
320
|
+
}));
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Available Operators
|
|
324
|
+
|
|
325
|
+
| Operator | Type | Description | Example |
|
|
326
|
+
|----------|------|-------------|---------|
|
|
327
|
+
| `eq` | string \| number | Exact equality match | `{ eq: 'v2' }` |
|
|
328
|
+
| `neq` | string | Not equal | `{ neq: 'guest' }` |
|
|
329
|
+
| `inc` | string[] | Value is one of | `{ inc: ['admin', 'mod'] }` |
|
|
330
|
+
| `ninc` | string[] | Value is not one of | `{ ninc: ['guest', 'banned'] }` |
|
|
331
|
+
| `pre` | string | Starts with prefix | `{ pre: 'Bearer ' }` |
|
|
332
|
+
| `suf` | string | Ends with suffix | `{ suf: '-dev' }` |
|
|
333
|
+
| `gt` | number | Greater than | `{ gt: 2 }` |
|
|
334
|
+
| `gte` | number | Greater than or equal | `{ gte: 3 }` |
|
|
335
|
+
| `lt` | number | Less than | `{ lt: 5 }` |
|
|
336
|
+
| `lte` | number | Less than or equal | `{ lte: 10 }` |
|
|
337
|
+
|
|
150
338
|
## Important Notes
|
|
151
339
|
|
|
152
340
|
- **One config file only**: You cannot have both `vercel.ts` and `vercel.json`. The build will fail if both exist.
|
package/dist/router.d.ts
CHANGED
|
@@ -87,14 +87,75 @@ export interface CacheOptions {
|
|
|
87
87
|
* - 'cookie': Match if a specific cookie is present (or missing).
|
|
88
88
|
* - 'host': Match if the incoming host matches a given pattern.
|
|
89
89
|
* - 'query': Match if a query parameter is present (or missing).
|
|
90
|
+
* - 'path': Match if the path matches a given pattern.
|
|
90
91
|
*/
|
|
91
|
-
export type ConditionType = 'header' | 'cookie' | 'host' | 'query';
|
|
92
|
+
export type ConditionType = 'header' | 'cookie' | 'host' | 'query' | 'path';
|
|
92
93
|
/**
|
|
93
|
-
*
|
|
94
|
+
* Conditional matching operators for has/missing conditions.
|
|
95
|
+
* These can be used with the value field to perform advanced matching.
|
|
94
96
|
*/
|
|
95
|
-
export interface
|
|
97
|
+
export interface ConditionOperators {
|
|
98
|
+
/** Check equality on a value (exact match) */
|
|
99
|
+
eq?: string | number;
|
|
100
|
+
/** Check inequality on a value (not equal) */
|
|
101
|
+
neq?: string;
|
|
102
|
+
/** Check inclusion in an array of values (value is one of) */
|
|
103
|
+
inc?: string[];
|
|
104
|
+
/** Check non-inclusion in an array of values (value is not one of) */
|
|
105
|
+
ninc?: string[];
|
|
106
|
+
/** Check if value starts with a prefix */
|
|
107
|
+
pre?: string;
|
|
108
|
+
/** Check if value ends with a suffix */
|
|
109
|
+
suf?: string;
|
|
110
|
+
/** Check if value is greater than (numeric comparison) */
|
|
111
|
+
gt?: number;
|
|
112
|
+
/** Check if value is greater than or equal to */
|
|
113
|
+
gte?: number;
|
|
114
|
+
/** Check if value is less than (numeric comparison) */
|
|
115
|
+
lt?: number;
|
|
116
|
+
/** Check if value is less than or equal to */
|
|
117
|
+
lte?: number;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Used to define "has" or "missing" conditions with advanced matching operators.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* // Simple header presence check
|
|
124
|
+
* { type: 'header', key: 'x-api-key' }
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* // Header with exact value match
|
|
128
|
+
* { type: 'header', key: 'x-api-version', value: 'v2' }
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* // Header with conditional operators
|
|
132
|
+
* { type: 'header', key: 'x-user-role', inc: ['admin', 'moderator'] }
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* // Cookie with prefix matching
|
|
136
|
+
* { type: 'cookie', key: 'session', pre: 'prod-' }
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* // Host matching
|
|
140
|
+
* { type: 'host', value: 'api.example.com' }
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* // Query parameter with numeric comparison
|
|
144
|
+
* { type: 'query', key: 'version', gte: 2 }
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* // Path pattern matching
|
|
148
|
+
* { type: 'path', value: '^/api/v[0-9]+/.*' }
|
|
149
|
+
*/
|
|
150
|
+
export interface Condition extends ConditionOperators {
|
|
96
151
|
type: ConditionType;
|
|
97
|
-
key
|
|
152
|
+
/** The key to match. Not used for 'host' or 'path' types. */
|
|
153
|
+
key?: string;
|
|
154
|
+
/**
|
|
155
|
+
* Simple string/regex pattern to match against.
|
|
156
|
+
* For 'host' and 'path' types, this is the only matching option.
|
|
157
|
+
* For other types, you can use value OR the conditional operators (eq, neq, etc).
|
|
158
|
+
*/
|
|
98
159
|
value?: string;
|
|
99
160
|
}
|
|
100
161
|
/**
|
|
@@ -188,6 +249,8 @@ export interface Route {
|
|
|
188
249
|
src: string;
|
|
189
250
|
/** Optional destination for rewrite/redirect */
|
|
190
251
|
dest?: string;
|
|
252
|
+
/** Array of HTTP methods to match. If not provided, matches all methods */
|
|
253
|
+
methods?: string[];
|
|
191
254
|
/** Array of transforms to apply */
|
|
192
255
|
transforms?: Transform[];
|
|
193
256
|
/** Optional conditions that must be present */
|
|
@@ -196,7 +259,7 @@ export interface Route {
|
|
|
196
259
|
missing?: Condition[];
|
|
197
260
|
/** If true, this is a redirect (status defaults to 308 or specified) */
|
|
198
261
|
redirect?: boolean;
|
|
199
|
-
/** Status code for
|
|
262
|
+
/** Status code for the response */
|
|
200
263
|
status?: number;
|
|
201
264
|
/** Headers to set (alternative to using transforms) */
|
|
202
265
|
headers?: Record<string, string>;
|
|
@@ -359,6 +422,10 @@ export interface RewriteRule {
|
|
|
359
422
|
*/
|
|
360
423
|
source: string;
|
|
361
424
|
destination: string;
|
|
425
|
+
/** Array of HTTP methods to match. If not provided, matches all methods */
|
|
426
|
+
methods?: string[];
|
|
427
|
+
/** Status code for the response */
|
|
428
|
+
status?: number;
|
|
362
429
|
has?: Condition[];
|
|
363
430
|
missing?: Condition[];
|
|
364
431
|
/** Internal field: transforms generated from requestHeaders/responseHeaders/requestQuery */
|
|
@@ -512,6 +579,8 @@ export declare class Router {
|
|
|
512
579
|
* });
|
|
513
580
|
*/
|
|
514
581
|
rewrite(source: string, destination: string, optionsOrCallback?: {
|
|
582
|
+
methods?: string[];
|
|
583
|
+
status?: number;
|
|
515
584
|
has?: Condition[];
|
|
516
585
|
missing?: Condition[];
|
|
517
586
|
requestHeaders?: Record<string, string | string[]>;
|
|
@@ -526,6 +595,8 @@ export declare class Router {
|
|
|
526
595
|
} | ((params: Record<string, string> & {
|
|
527
596
|
env: any;
|
|
528
597
|
}) => {
|
|
598
|
+
methods?: string[];
|
|
599
|
+
status?: number;
|
|
529
600
|
has?: Condition[];
|
|
530
601
|
missing?: Condition[];
|
|
531
602
|
requestHeaders?: Record<string, string | string[]>;
|
package/dist/router.js
CHANGED
|
@@ -305,7 +305,7 @@ class Router {
|
|
|
305
305
|
options = optionsOrCallback;
|
|
306
306
|
}
|
|
307
307
|
// Extract transform options
|
|
308
|
-
const { requestHeaders, responseHeaders, requestQuery, appendRequestHeaders, appendResponseHeaders, appendRequestQuery, deleteRequestHeaders, deleteResponseHeaders, deleteRequestQuery, has, missing } = options || {};
|
|
308
|
+
const { methods, status, requestHeaders, responseHeaders, requestQuery, appendRequestHeaders, appendResponseHeaders, appendRequestQuery, deleteRequestHeaders, deleteResponseHeaders, deleteRequestQuery, has, missing } = options || {};
|
|
309
309
|
const transformOpts = {
|
|
310
310
|
requestHeaders,
|
|
311
311
|
responseHeaders,
|
|
@@ -327,6 +327,8 @@ class Router {
|
|
|
327
327
|
this.rewriteRules.push({
|
|
328
328
|
source,
|
|
329
329
|
destination,
|
|
330
|
+
...(methods && { methods }),
|
|
331
|
+
...(status && { status }),
|
|
330
332
|
has,
|
|
331
333
|
missing,
|
|
332
334
|
transforms,
|
|
@@ -410,7 +412,7 @@ class Router {
|
|
|
410
412
|
options = optionsOrCallback;
|
|
411
413
|
}
|
|
412
414
|
// Extract transform options
|
|
413
|
-
const { requestHeaders, responseHeaders, requestQuery, appendRequestHeaders, appendResponseHeaders, appendRequestQuery, deleteRequestHeaders, deleteResponseHeaders, deleteRequestQuery, permanent, statusCode, has, missing, } = options || {};
|
|
415
|
+
const { methods, requestHeaders, responseHeaders, requestQuery, appendRequestHeaders, appendResponseHeaders, appendRequestQuery, deleteRequestHeaders, deleteResponseHeaders, deleteRequestQuery, permanent, statusCode, has, missing, } = options || {};
|
|
414
416
|
// If transforms are provided, create a route instead of a redirect
|
|
415
417
|
const hasTransforms = requestHeaders || responseHeaders || requestQuery ||
|
|
416
418
|
appendRequestHeaders || appendResponseHeaders || appendRequestQuery ||
|
|
@@ -431,6 +433,7 @@ class Router {
|
|
|
431
433
|
this.routeRules.push({
|
|
432
434
|
src: source,
|
|
433
435
|
dest: destination,
|
|
436
|
+
...(methods && { methods }),
|
|
434
437
|
transforms,
|
|
435
438
|
redirect: true,
|
|
436
439
|
status: statusCode || (permanent ? 308 : 307),
|
|
@@ -580,16 +583,22 @@ class Router {
|
|
|
580
583
|
* so that Vercel can pick it up.
|
|
581
584
|
*/
|
|
582
585
|
getConfig() {
|
|
583
|
-
// Separate rewrites into those
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
586
|
+
// Separate rewrites into those that need to be routes vs. legacy rewrites
|
|
587
|
+
// Routes are needed for: transforms, methods, or custom status
|
|
588
|
+
const rewritesNeedingRoutes = this.rewriteRules.filter((r) => r.transforms || r.methods || r.status);
|
|
589
|
+
const legacyRewrites = this.rewriteRules.filter((r) => !r.transforms && !r.methods && !r.status);
|
|
590
|
+
// Convert rewrites to routes
|
|
591
|
+
const routesFromRewrites = rewritesNeedingRoutes.map((rewrite) => {
|
|
588
592
|
const route = {
|
|
589
593
|
src: rewrite.source,
|
|
590
594
|
dest: rewrite.destination,
|
|
591
|
-
transforms: rewrite.transforms,
|
|
592
595
|
};
|
|
596
|
+
if (rewrite.transforms)
|
|
597
|
+
route.transforms = rewrite.transforms;
|
|
598
|
+
if (rewrite.methods)
|
|
599
|
+
route.methods = rewrite.methods;
|
|
600
|
+
if (rewrite.status)
|
|
601
|
+
route.status = rewrite.status;
|
|
593
602
|
if (rewrite.has)
|
|
594
603
|
route.has = rewrite.has;
|
|
595
604
|
if (rewrite.missing)
|
|
@@ -619,7 +628,7 @@ class Router {
|
|
|
619
628
|
const config = {
|
|
620
629
|
redirects: this.redirectRules,
|
|
621
630
|
headers: this.headerRules,
|
|
622
|
-
rewrites:
|
|
631
|
+
rewrites: legacyRewrites,
|
|
623
632
|
cleanUrls: this.cleanUrlsConfig,
|
|
624
633
|
trailingSlash: this.trailingSlashConfig,
|
|
625
634
|
crons: this.cronRules,
|