@vercel/config 0.0.10 → 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/cli.js +32 -3
- package/dist/router.d.ts +162 -10
- package/dist/router.js +137 -22
- 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/cli.js
CHANGED
|
@@ -28,7 +28,22 @@ const fs_1 = require("fs");
|
|
|
28
28
|
const path_1 = require("path");
|
|
29
29
|
const fs_2 = require("fs");
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* Named exports that should NOT be auto-converted to config
|
|
32
|
+
* (these are route-based features that compile into the routes array, or internal module properties)
|
|
33
|
+
*/
|
|
34
|
+
const ROUTE_BASED_EXPORTS = new Set([
|
|
35
|
+
'default',
|
|
36
|
+
'routes',
|
|
37
|
+
'redirects',
|
|
38
|
+
'rewrites',
|
|
39
|
+
'headers',
|
|
40
|
+
'crons',
|
|
41
|
+
'env',
|
|
42
|
+
'cacheControl',
|
|
43
|
+
'__esModule' // ES module metadata
|
|
44
|
+
]);
|
|
45
|
+
/**
|
|
46
|
+
* Read the user's vercel.ts file and collect both default export and export const declarations
|
|
32
47
|
*/
|
|
33
48
|
async function configureRouter() {
|
|
34
49
|
var _a;
|
|
@@ -36,8 +51,22 @@ async function configureRouter() {
|
|
|
36
51
|
const routerConfigPath = (0, path_1.resolve)(process.cwd(), "router.config.ts");
|
|
37
52
|
// Prefer vercel.ts, fallback to router.config.ts
|
|
38
53
|
const configPath = (0, fs_2.existsSync)(vercelTsPath) ? vercelTsPath : routerConfigPath;
|
|
39
|
-
|
|
40
|
-
|
|
54
|
+
// Import the entire module to get both default and named exports
|
|
55
|
+
const module = await (_a = configPath, Promise.resolve().then(() => __importStar(require(_a))));
|
|
56
|
+
// Start with the default export (router.getConfig())
|
|
57
|
+
const routerConfig = module.default || {};
|
|
58
|
+
// Auto-collect all export const declarations (except route-based ones)
|
|
59
|
+
const exportedConstants = {};
|
|
60
|
+
for (const [key, value] of Object.entries(module)) {
|
|
61
|
+
if (!ROUTE_BASED_EXPORTS.has(key)) {
|
|
62
|
+
exportedConstants[key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Merge: export const declarations take precedence over default export
|
|
66
|
+
return {
|
|
67
|
+
...routerConfig,
|
|
68
|
+
...exportedConstants
|
|
69
|
+
};
|
|
41
70
|
}
|
|
42
71
|
/**
|
|
43
72
|
* Read existing vercel.json and extract fields to preserve
|
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>;
|
|
@@ -215,27 +278,28 @@ export interface Header {
|
|
|
215
278
|
export interface TransformOptions {
|
|
216
279
|
/**
|
|
217
280
|
* Headers to set/modify on the incoming request.
|
|
218
|
-
*
|
|
281
|
+
* Sets the key and value if missing.
|
|
219
282
|
*
|
|
220
283
|
* @example
|
|
221
284
|
* requestHeaders: {
|
|
222
|
-
* 'x-user-id':
|
|
223
|
-
* 'authorization': `Bearer ${
|
|
285
|
+
* 'x-user-id': userId,
|
|
286
|
+
* 'authorization': `Bearer ${env.API_TOKEN}`
|
|
224
287
|
* }
|
|
225
288
|
*/
|
|
226
289
|
requestHeaders?: Record<string, string | string[]>;
|
|
227
290
|
/**
|
|
228
291
|
* Headers to set/modify on the outgoing response.
|
|
229
|
-
*
|
|
292
|
+
* Sets the key and value if missing.
|
|
230
293
|
*
|
|
231
294
|
* @example
|
|
232
295
|
* responseHeaders: {
|
|
233
|
-
* 'x-post-id':
|
|
296
|
+
* 'x-post-id': postId
|
|
234
297
|
* }
|
|
235
298
|
*/
|
|
236
299
|
responseHeaders?: Record<string, string | string[]>;
|
|
237
300
|
/**
|
|
238
301
|
* Query parameters to set/modify on the request.
|
|
302
|
+
* Sets the key and value if missing.
|
|
239
303
|
*
|
|
240
304
|
* @example
|
|
241
305
|
* requestQuery: {
|
|
@@ -243,6 +307,60 @@ export interface TransformOptions {
|
|
|
243
307
|
* }
|
|
244
308
|
*/
|
|
245
309
|
requestQuery?: Record<string, string | string[]>;
|
|
310
|
+
/**
|
|
311
|
+
* Headers to append to the incoming request.
|
|
312
|
+
* Appends args to the value of the key, and will set if missing.
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* appendRequestHeaders: {
|
|
316
|
+
* 'x-custom': 'value'
|
|
317
|
+
* }
|
|
318
|
+
*/
|
|
319
|
+
appendRequestHeaders?: Record<string, string | string[]>;
|
|
320
|
+
/**
|
|
321
|
+
* Headers to append to the outgoing response.
|
|
322
|
+
* Appends args to the value of the key, and will set if missing.
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* appendResponseHeaders: {
|
|
326
|
+
* 'x-custom': 'value'
|
|
327
|
+
* }
|
|
328
|
+
*/
|
|
329
|
+
appendResponseHeaders?: Record<string, string | string[]>;
|
|
330
|
+
/**
|
|
331
|
+
* Query parameters to append to the request.
|
|
332
|
+
* Appends args to the value of the key, and will set if missing.
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* appendRequestQuery: {
|
|
336
|
+
* 'tag': 'value'
|
|
337
|
+
* }
|
|
338
|
+
*/
|
|
339
|
+
appendRequestQuery?: Record<string, string | string[]>;
|
|
340
|
+
/**
|
|
341
|
+
* Headers to delete from the incoming request.
|
|
342
|
+
* Deletes the key entirely if args is not provided; otherwise, it will delete the value of args from the matching key.
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* deleteRequestHeaders: ['x-remove-this', 'x-remove-that']
|
|
346
|
+
*/
|
|
347
|
+
deleteRequestHeaders?: string[];
|
|
348
|
+
/**
|
|
349
|
+
* Headers to delete from the outgoing response.
|
|
350
|
+
* Deletes the key entirely if args is not provided; otherwise, it will delete the value of args from the matching key.
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* deleteResponseHeaders: ['x-powered-by']
|
|
354
|
+
*/
|
|
355
|
+
deleteResponseHeaders?: string[];
|
|
356
|
+
/**
|
|
357
|
+
* Query parameters to delete from the request.
|
|
358
|
+
* Deletes the key entirely if args is not provided; otherwise, it will delete the value of args from the matching key.
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* deleteRequestQuery: ['debug', 'trace']
|
|
362
|
+
*/
|
|
363
|
+
deleteRequestQuery?: string[];
|
|
246
364
|
}
|
|
247
365
|
/**
|
|
248
366
|
* HeaderRule defines one or more headers to set for requests
|
|
@@ -304,6 +422,10 @@ export interface RewriteRule {
|
|
|
304
422
|
*/
|
|
305
423
|
source: string;
|
|
306
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;
|
|
307
429
|
has?: Condition[];
|
|
308
430
|
missing?: Condition[];
|
|
309
431
|
/** Internal field: transforms generated from requestHeaders/responseHeaders/requestQuery */
|
|
@@ -429,6 +551,8 @@ export declare class Router {
|
|
|
429
551
|
private extractEnvVars;
|
|
430
552
|
/**
|
|
431
553
|
* Internal helper to convert TransformOptions to Transform array
|
|
554
|
+
* @param options Transform options to convert
|
|
555
|
+
* @param trackedEnvVars Optional set of environment variables that were accessed via the env proxy
|
|
432
556
|
*/
|
|
433
557
|
private transformOptionsToTransforms;
|
|
434
558
|
/**
|
|
@@ -455,19 +579,35 @@ export declare class Router {
|
|
|
455
579
|
* });
|
|
456
580
|
*/
|
|
457
581
|
rewrite(source: string, destination: string, optionsOrCallback?: {
|
|
582
|
+
methods?: string[];
|
|
583
|
+
status?: number;
|
|
458
584
|
has?: Condition[];
|
|
459
585
|
missing?: Condition[];
|
|
460
586
|
requestHeaders?: Record<string, string | string[]>;
|
|
461
587
|
responseHeaders?: Record<string, string | string[]>;
|
|
462
588
|
requestQuery?: Record<string, string | string[]>;
|
|
589
|
+
appendRequestHeaders?: Record<string, string | string[]>;
|
|
590
|
+
appendResponseHeaders?: Record<string, string | string[]>;
|
|
591
|
+
appendRequestQuery?: Record<string, string | string[]>;
|
|
592
|
+
deleteRequestHeaders?: string[];
|
|
593
|
+
deleteResponseHeaders?: string[];
|
|
594
|
+
deleteRequestQuery?: string[];
|
|
463
595
|
} | ((params: Record<string, string> & {
|
|
464
596
|
env: any;
|
|
465
597
|
}) => {
|
|
598
|
+
methods?: string[];
|
|
599
|
+
status?: number;
|
|
466
600
|
has?: Condition[];
|
|
467
601
|
missing?: Condition[];
|
|
468
602
|
requestHeaders?: Record<string, string | string[]>;
|
|
469
603
|
responseHeaders?: Record<string, string | string[]>;
|
|
470
604
|
requestQuery?: Record<string, string | string[]>;
|
|
605
|
+
appendRequestHeaders?: Record<string, string | string[]>;
|
|
606
|
+
appendResponseHeaders?: Record<string, string | string[]>;
|
|
607
|
+
appendRequestQuery?: Record<string, string | string[]>;
|
|
608
|
+
deleteRequestHeaders?: string[];
|
|
609
|
+
deleteResponseHeaders?: string[];
|
|
610
|
+
deleteRequestQuery?: string[];
|
|
471
611
|
})): this;
|
|
472
612
|
/**
|
|
473
613
|
* Loads rewrite rules asynchronously and appends them.
|
|
@@ -509,6 +649,12 @@ export declare class Router {
|
|
|
509
649
|
requestHeaders?: Record<string, string | string[]>;
|
|
510
650
|
responseHeaders?: Record<string, string | string[]>;
|
|
511
651
|
requestQuery?: Record<string, string | string[]>;
|
|
652
|
+
appendRequestHeaders?: Record<string, string | string[]>;
|
|
653
|
+
appendResponseHeaders?: Record<string, string | string[]>;
|
|
654
|
+
appendRequestQuery?: Record<string, string | string[]>;
|
|
655
|
+
deleteRequestHeaders?: string[];
|
|
656
|
+
deleteResponseHeaders?: string[];
|
|
657
|
+
deleteRequestQuery?: string[];
|
|
512
658
|
} | ((params: Record<string, string> & {
|
|
513
659
|
env: any;
|
|
514
660
|
}) => {
|
|
@@ -519,6 +665,12 @@ export declare class Router {
|
|
|
519
665
|
requestHeaders?: Record<string, string | string[]>;
|
|
520
666
|
responseHeaders?: Record<string, string | string[]>;
|
|
521
667
|
requestQuery?: Record<string, string | string[]>;
|
|
668
|
+
appendRequestHeaders?: Record<string, string | string[]>;
|
|
669
|
+
appendResponseHeaders?: Record<string, string | string[]>;
|
|
670
|
+
appendRequestQuery?: Record<string, string | string[]>;
|
|
671
|
+
deleteRequestHeaders?: string[];
|
|
672
|
+
deleteResponseHeaders?: string[];
|
|
673
|
+
deleteRequestQuery?: string[];
|
|
522
674
|
})): this;
|
|
523
675
|
/**
|
|
524
676
|
* Loads redirect rules asynchronously and appends them.
|
package/dist/router.js
CHANGED
|
@@ -131,13 +131,26 @@ class Router {
|
|
|
131
131
|
}
|
|
132
132
|
/**
|
|
133
133
|
* Internal helper to convert TransformOptions to Transform array
|
|
134
|
+
* @param options Transform options to convert
|
|
135
|
+
* @param trackedEnvVars Optional set of environment variables that were accessed via the env proxy
|
|
134
136
|
*/
|
|
135
|
-
transformOptionsToTransforms(options) {
|
|
137
|
+
transformOptionsToTransforms(options, trackedEnvVars) {
|
|
136
138
|
const transforms = [];
|
|
137
|
-
//
|
|
139
|
+
// Helper to get env vars for a value
|
|
140
|
+
const getEnvVars = (value) => {
|
|
141
|
+
if (trackedEnvVars) {
|
|
142
|
+
return Array.from(trackedEnvVars).filter(envVar => {
|
|
143
|
+
const valueStr = Array.isArray(value) ? value.join(' ') : value;
|
|
144
|
+
return valueStr.includes(`$${envVar}`);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return this.extractEnvVars(value);
|
|
148
|
+
};
|
|
149
|
+
// SET operations
|
|
150
|
+
// Convert requestHeaders (set)
|
|
138
151
|
if (options.requestHeaders) {
|
|
139
152
|
for (const [key, value] of Object.entries(options.requestHeaders)) {
|
|
140
|
-
const envVars =
|
|
153
|
+
const envVars = getEnvVars(value);
|
|
141
154
|
transforms.push({
|
|
142
155
|
type: 'request.headers',
|
|
143
156
|
op: 'set',
|
|
@@ -147,10 +160,10 @@ class Router {
|
|
|
147
160
|
});
|
|
148
161
|
}
|
|
149
162
|
}
|
|
150
|
-
// Convert responseHeaders
|
|
163
|
+
// Convert responseHeaders (set)
|
|
151
164
|
if (options.responseHeaders) {
|
|
152
165
|
for (const [key, value] of Object.entries(options.responseHeaders)) {
|
|
153
|
-
const envVars =
|
|
166
|
+
const envVars = getEnvVars(value);
|
|
154
167
|
transforms.push({
|
|
155
168
|
type: 'response.headers',
|
|
156
169
|
op: 'set',
|
|
@@ -160,10 +173,10 @@ class Router {
|
|
|
160
173
|
});
|
|
161
174
|
}
|
|
162
175
|
}
|
|
163
|
-
// Convert requestQuery
|
|
176
|
+
// Convert requestQuery (set)
|
|
164
177
|
if (options.requestQuery) {
|
|
165
178
|
for (const [key, value] of Object.entries(options.requestQuery)) {
|
|
166
|
-
const envVars =
|
|
179
|
+
const envVars = getEnvVars(value);
|
|
167
180
|
transforms.push({
|
|
168
181
|
type: 'request.query',
|
|
169
182
|
op: 'set',
|
|
@@ -173,6 +186,77 @@ class Router {
|
|
|
173
186
|
});
|
|
174
187
|
}
|
|
175
188
|
}
|
|
189
|
+
// APPEND operations
|
|
190
|
+
// Convert appendRequestHeaders
|
|
191
|
+
if (options.appendRequestHeaders) {
|
|
192
|
+
for (const [key, value] of Object.entries(options.appendRequestHeaders)) {
|
|
193
|
+
const envVars = getEnvVars(value);
|
|
194
|
+
transforms.push({
|
|
195
|
+
type: 'request.headers',
|
|
196
|
+
op: 'append',
|
|
197
|
+
target: { key },
|
|
198
|
+
args: value,
|
|
199
|
+
...(envVars.length > 0 && { env: envVars }),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Convert appendResponseHeaders
|
|
204
|
+
if (options.appendResponseHeaders) {
|
|
205
|
+
for (const [key, value] of Object.entries(options.appendResponseHeaders)) {
|
|
206
|
+
const envVars = getEnvVars(value);
|
|
207
|
+
transforms.push({
|
|
208
|
+
type: 'response.headers',
|
|
209
|
+
op: 'append',
|
|
210
|
+
target: { key },
|
|
211
|
+
args: value,
|
|
212
|
+
...(envVars.length > 0 && { env: envVars }),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Convert appendRequestQuery
|
|
217
|
+
if (options.appendRequestQuery) {
|
|
218
|
+
for (const [key, value] of Object.entries(options.appendRequestQuery)) {
|
|
219
|
+
const envVars = getEnvVars(value);
|
|
220
|
+
transforms.push({
|
|
221
|
+
type: 'request.query',
|
|
222
|
+
op: 'append',
|
|
223
|
+
target: { key },
|
|
224
|
+
args: value,
|
|
225
|
+
...(envVars.length > 0 && { env: envVars }),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// DELETE operations
|
|
230
|
+
// Convert deleteRequestHeaders
|
|
231
|
+
if (options.deleteRequestHeaders) {
|
|
232
|
+
for (const key of options.deleteRequestHeaders) {
|
|
233
|
+
transforms.push({
|
|
234
|
+
type: 'request.headers',
|
|
235
|
+
op: 'delete',
|
|
236
|
+
target: { key },
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Convert deleteResponseHeaders
|
|
241
|
+
if (options.deleteResponseHeaders) {
|
|
242
|
+
for (const key of options.deleteResponseHeaders) {
|
|
243
|
+
transforms.push({
|
|
244
|
+
type: 'response.headers',
|
|
245
|
+
op: 'delete',
|
|
246
|
+
target: { key },
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Convert deleteRequestQuery
|
|
251
|
+
if (options.deleteRequestQuery) {
|
|
252
|
+
for (const key of options.deleteRequestQuery) {
|
|
253
|
+
transforms.push({
|
|
254
|
+
type: 'request.query',
|
|
255
|
+
op: 'delete',
|
|
256
|
+
target: { key },
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
176
260
|
return transforms;
|
|
177
261
|
}
|
|
178
262
|
/**
|
|
@@ -202,10 +286,12 @@ class Router {
|
|
|
202
286
|
this.validateSourcePattern(source);
|
|
203
287
|
(0, validation_1.validateCaptureGroupReferences)(source, destination);
|
|
204
288
|
let options;
|
|
289
|
+
let trackedEnvVars;
|
|
205
290
|
// Handle callback syntax
|
|
206
291
|
if (typeof optionsOrCallback === 'function') {
|
|
207
292
|
const pathParams = this.extractPathParams(source);
|
|
208
|
-
const { proxy: envProxy } = this.createEnvProxy();
|
|
293
|
+
const { proxy: envProxy, accessedVars } = this.createEnvProxy();
|
|
294
|
+
trackedEnvVars = accessedVars;
|
|
209
295
|
// Create params object with path parameters as $paramName
|
|
210
296
|
const paramsObj = {};
|
|
211
297
|
for (const param of pathParams) {
|
|
@@ -219,19 +305,30 @@ class Router {
|
|
|
219
305
|
options = optionsOrCallback;
|
|
220
306
|
}
|
|
221
307
|
// Extract transform options
|
|
222
|
-
const { requestHeaders, responseHeaders, requestQuery, has, missing } = options || {};
|
|
308
|
+
const { methods, status, requestHeaders, responseHeaders, requestQuery, appendRequestHeaders, appendResponseHeaders, appendRequestQuery, deleteRequestHeaders, deleteResponseHeaders, deleteRequestQuery, has, missing } = options || {};
|
|
223
309
|
const transformOpts = {
|
|
224
310
|
requestHeaders,
|
|
225
311
|
responseHeaders,
|
|
226
312
|
requestQuery,
|
|
313
|
+
appendRequestHeaders,
|
|
314
|
+
appendResponseHeaders,
|
|
315
|
+
appendRequestQuery,
|
|
316
|
+
deleteRequestHeaders,
|
|
317
|
+
deleteResponseHeaders,
|
|
318
|
+
deleteRequestQuery,
|
|
227
319
|
};
|
|
228
320
|
// Convert to transforms if any transform options provided
|
|
229
|
-
const
|
|
230
|
-
|
|
321
|
+
const hasTransforms = requestHeaders || responseHeaders || requestQuery ||
|
|
322
|
+
appendRequestHeaders || appendResponseHeaders || appendRequestQuery ||
|
|
323
|
+
deleteRequestHeaders || deleteResponseHeaders || deleteRequestQuery;
|
|
324
|
+
const transforms = hasTransforms
|
|
325
|
+
? this.transformOptionsToTransforms(transformOpts, trackedEnvVars)
|
|
231
326
|
: undefined;
|
|
232
327
|
this.rewriteRules.push({
|
|
233
328
|
source,
|
|
234
329
|
destination,
|
|
330
|
+
...(methods && { methods }),
|
|
331
|
+
...(status && { status }),
|
|
235
332
|
has,
|
|
236
333
|
missing,
|
|
237
334
|
transforms,
|
|
@@ -296,10 +393,12 @@ class Router {
|
|
|
296
393
|
this.validateSourcePattern(source);
|
|
297
394
|
(0, validation_1.validateCaptureGroupReferences)(source, destination);
|
|
298
395
|
let options;
|
|
396
|
+
let trackedEnvVars;
|
|
299
397
|
// Handle callback syntax
|
|
300
398
|
if (typeof optionsOrCallback === 'function') {
|
|
301
399
|
const pathParams = this.extractPathParams(source);
|
|
302
|
-
const { proxy: envProxy } = this.createEnvProxy();
|
|
400
|
+
const { proxy: envProxy, accessedVars } = this.createEnvProxy();
|
|
401
|
+
trackedEnvVars = accessedVars;
|
|
303
402
|
// Create params object with path parameters as $paramName
|
|
304
403
|
const paramsObj = {};
|
|
305
404
|
for (const param of pathParams) {
|
|
@@ -313,18 +412,28 @@ class Router {
|
|
|
313
412
|
options = optionsOrCallback;
|
|
314
413
|
}
|
|
315
414
|
// Extract transform options
|
|
316
|
-
const { requestHeaders, responseHeaders, requestQuery, permanent, statusCode, has, missing, } = options || {};
|
|
415
|
+
const { methods, requestHeaders, responseHeaders, requestQuery, appendRequestHeaders, appendResponseHeaders, appendRequestQuery, deleteRequestHeaders, deleteResponseHeaders, deleteRequestQuery, permanent, statusCode, has, missing, } = options || {};
|
|
317
416
|
// If transforms are provided, create a route instead of a redirect
|
|
318
|
-
|
|
417
|
+
const hasTransforms = requestHeaders || responseHeaders || requestQuery ||
|
|
418
|
+
appendRequestHeaders || appendResponseHeaders || appendRequestQuery ||
|
|
419
|
+
deleteRequestHeaders || deleteResponseHeaders || deleteRequestQuery;
|
|
420
|
+
if (hasTransforms) {
|
|
319
421
|
const transformOpts = {
|
|
320
422
|
requestHeaders,
|
|
321
423
|
responseHeaders,
|
|
322
424
|
requestQuery,
|
|
425
|
+
appendRequestHeaders,
|
|
426
|
+
appendResponseHeaders,
|
|
427
|
+
appendRequestQuery,
|
|
428
|
+
deleteRequestHeaders,
|
|
429
|
+
deleteResponseHeaders,
|
|
430
|
+
deleteRequestQuery,
|
|
323
431
|
};
|
|
324
|
-
const transforms = this.transformOptionsToTransforms(transformOpts);
|
|
432
|
+
const transforms = this.transformOptionsToTransforms(transformOpts, trackedEnvVars);
|
|
325
433
|
this.routeRules.push({
|
|
326
434
|
src: source,
|
|
327
435
|
dest: destination,
|
|
436
|
+
...(methods && { methods }),
|
|
328
437
|
transforms,
|
|
329
438
|
redirect: true,
|
|
330
439
|
status: statusCode || (permanent ? 308 : 307),
|
|
@@ -474,16 +583,22 @@ class Router {
|
|
|
474
583
|
* so that Vercel can pick it up.
|
|
475
584
|
*/
|
|
476
585
|
getConfig() {
|
|
477
|
-
// Separate rewrites into those
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
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) => {
|
|
482
592
|
const route = {
|
|
483
593
|
src: rewrite.source,
|
|
484
594
|
dest: rewrite.destination,
|
|
485
|
-
transforms: rewrite.transforms,
|
|
486
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;
|
|
487
602
|
if (rewrite.has)
|
|
488
603
|
route.has = rewrite.has;
|
|
489
604
|
if (rewrite.missing)
|
|
@@ -513,7 +628,7 @@ class Router {
|
|
|
513
628
|
const config = {
|
|
514
629
|
redirects: this.redirectRules,
|
|
515
630
|
headers: this.headerRules,
|
|
516
|
-
rewrites:
|
|
631
|
+
rewrites: legacyRewrites,
|
|
517
632
|
cleanUrls: this.cleanUrlsConfig,
|
|
518
633
|
trailingSlash: this.trailingSlashConfig,
|
|
519
634
|
crons: this.cronRules,
|