@vercel/config 0.0.9
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 +46 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +93 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +21 -0
- package/dist/router.d.ts +555 -0
- package/dist/router.js +484 -0
- package/dist/utils/validation.d.ts +43 -0
- package/dist/utils/validation.js +124 -0
- package/package.json +47 -0
package/dist/router.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRouter = exports.Router = exports.runtimeEnv = exports.param = void 0;
|
|
4
|
+
const pretty_cache_header_1 = require("pretty-cache-header");
|
|
5
|
+
const validation_1 = require("./utils/validation");
|
|
6
|
+
/**
|
|
7
|
+
* Helper function to reference a path parameter in transforms.
|
|
8
|
+
* Path parameters are extracted from the route pattern (e.g., :userId).
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* param('userId') // Returns '$userId'
|
|
12
|
+
*/
|
|
13
|
+
function param(name) {
|
|
14
|
+
return `$${name}`;
|
|
15
|
+
}
|
|
16
|
+
exports.param = param;
|
|
17
|
+
/**
|
|
18
|
+
* Helper function to reference a runtime environment variable in transforms.
|
|
19
|
+
* These are environment variables that get resolved at request time by Vercel's routing layer,
|
|
20
|
+
* not at build time.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* runtimeEnv('BEARER_TOKEN') // Returns '$BEARER_TOKEN'
|
|
24
|
+
*/
|
|
25
|
+
function runtimeEnv(name) {
|
|
26
|
+
return `$${name}`;
|
|
27
|
+
}
|
|
28
|
+
exports.runtimeEnv = runtimeEnv;
|
|
29
|
+
/**
|
|
30
|
+
* The main Router class for building a Vercel configuration object in code.
|
|
31
|
+
* Supports synchronous or asynchronous addition of rewrites, redirects, headers,
|
|
32
|
+
* plus convenience methods for crons, caching, and more.
|
|
33
|
+
*/
|
|
34
|
+
class Router {
|
|
35
|
+
constructor() {
|
|
36
|
+
this.redirectRules = [];
|
|
37
|
+
this.headerRules = [];
|
|
38
|
+
this.rewriteRules = [];
|
|
39
|
+
this.routeRules = [];
|
|
40
|
+
this.cronRules = [];
|
|
41
|
+
this.cleanUrlsConfig = undefined;
|
|
42
|
+
this.trailingSlashConfig = undefined;
|
|
43
|
+
this.buildCommandConfig = undefined;
|
|
44
|
+
this.installCommandConfig = undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Helper to extract environment variable names from a string or string array.
|
|
48
|
+
* Environment variables are identified by the pattern $VAR_NAME where VAR_NAME
|
|
49
|
+
* is typically uppercase with underscores (e.g., $API_KEY, $BEARER_TOKEN).
|
|
50
|
+
*/
|
|
51
|
+
extractEnvVars(args) {
|
|
52
|
+
const envVars = new Set();
|
|
53
|
+
const values = Array.isArray(args) ? args : [args];
|
|
54
|
+
for (const value of values) {
|
|
55
|
+
const matches = value.match(/\$([A-Z][A-Z0-9_]*)/g);
|
|
56
|
+
if (matches) {
|
|
57
|
+
for (const match of matches) {
|
|
58
|
+
envVars.add(match.substring(1));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return Array.from(envVars);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Internal helper to convert TransformOptions to Transform array
|
|
66
|
+
*/
|
|
67
|
+
transformOptionsToTransforms(options) {
|
|
68
|
+
const transforms = [];
|
|
69
|
+
// Convert requestHeaders
|
|
70
|
+
if (options.requestHeaders) {
|
|
71
|
+
for (const [key, value] of Object.entries(options.requestHeaders)) {
|
|
72
|
+
const envVars = this.extractEnvVars(value);
|
|
73
|
+
transforms.push({
|
|
74
|
+
type: 'request.headers',
|
|
75
|
+
op: 'set',
|
|
76
|
+
target: { key },
|
|
77
|
+
args: value,
|
|
78
|
+
...(envVars.length > 0 && { env: envVars }),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Convert responseHeaders
|
|
83
|
+
if (options.responseHeaders) {
|
|
84
|
+
for (const [key, value] of Object.entries(options.responseHeaders)) {
|
|
85
|
+
const envVars = this.extractEnvVars(value);
|
|
86
|
+
transforms.push({
|
|
87
|
+
type: 'response.headers',
|
|
88
|
+
op: 'set',
|
|
89
|
+
target: { key },
|
|
90
|
+
args: value,
|
|
91
|
+
...(envVars.length > 0 && { env: envVars }),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Convert requestQuery
|
|
96
|
+
if (options.requestQuery) {
|
|
97
|
+
for (const [key, value] of Object.entries(options.requestQuery)) {
|
|
98
|
+
const envVars = this.extractEnvVars(value);
|
|
99
|
+
transforms.push({
|
|
100
|
+
type: 'request.query',
|
|
101
|
+
op: 'set',
|
|
102
|
+
target: { key },
|
|
103
|
+
args: value,
|
|
104
|
+
...(envVars.length > 0 && { env: envVars }),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return transforms;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Adds a single rewrite rule (synchronous).
|
|
112
|
+
* Automatically enables rewrite caching by adding the x-vercel-enable-rewrite-caching header.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* // This will automatically enable caching for the rewrite
|
|
116
|
+
* import { createRouter, param, runtimeEnv } from '@vercel/router-sdk';
|
|
117
|
+
* const router = createRouter();
|
|
118
|
+
* router.rewrite('/api/(.*)', 'https://old-on-prem.com/$1')
|
|
119
|
+
*
|
|
120
|
+
* // With transforms
|
|
121
|
+
* router.rewrite('/users/:userId', 'https://api.example.com/users/$1', {
|
|
122
|
+
* requestHeaders: {
|
|
123
|
+
* 'x-user-id': param('userId'),
|
|
124
|
+
* 'authorization': `Bearer ${runtimeEnv('API_TOKEN')}`
|
|
125
|
+
* }
|
|
126
|
+
* });
|
|
127
|
+
*/
|
|
128
|
+
rewrite(source, destination, options) {
|
|
129
|
+
this.validateSourcePattern(source);
|
|
130
|
+
(0, validation_1.validateCaptureGroupReferences)(source, destination);
|
|
131
|
+
// Extract transform options
|
|
132
|
+
const { requestHeaders, responseHeaders, requestQuery, has, missing } = options || {};
|
|
133
|
+
const transformOpts = {
|
|
134
|
+
requestHeaders,
|
|
135
|
+
responseHeaders,
|
|
136
|
+
requestQuery,
|
|
137
|
+
};
|
|
138
|
+
// Convert to transforms if any transform options provided
|
|
139
|
+
const transforms = requestHeaders || responseHeaders || requestQuery
|
|
140
|
+
? this.transformOptionsToTransforms(transformOpts)
|
|
141
|
+
: undefined;
|
|
142
|
+
this.rewriteRules.push({
|
|
143
|
+
source,
|
|
144
|
+
destination,
|
|
145
|
+
has,
|
|
146
|
+
missing,
|
|
147
|
+
transforms,
|
|
148
|
+
});
|
|
149
|
+
// Only enable rewrite caching for rewrites without transforms
|
|
150
|
+
// (transforms convert to routes, which don't need the caching header)
|
|
151
|
+
if (!transforms) {
|
|
152
|
+
this.headerRules.push({
|
|
153
|
+
source,
|
|
154
|
+
headers: [{ key: 'x-vercel-enable-rewrite-caching', value: '1' }],
|
|
155
|
+
has,
|
|
156
|
+
missing,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Loads rewrite rules asynchronously and appends them.
|
|
163
|
+
* Automatically enables rewrite caching for all loaded rules by adding the x-vercel-enable-rewrite-caching header.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* // This will automatically enable caching for all rewrites
|
|
167
|
+
* await router.rewrites(() => fetchRewriteRulesFromDB());
|
|
168
|
+
*/
|
|
169
|
+
async rewrites(provider) {
|
|
170
|
+
const rules = await provider();
|
|
171
|
+
this.rewriteRules.push(...rules);
|
|
172
|
+
// Automatically enable rewrite caching for all rules
|
|
173
|
+
const headerRules = rules.map((rule) => ({
|
|
174
|
+
source: rule.source,
|
|
175
|
+
headers: [{ key: 'x-vercel-enable-rewrite-caching', value: '1' }],
|
|
176
|
+
has: rule.has,
|
|
177
|
+
missing: rule.missing,
|
|
178
|
+
}));
|
|
179
|
+
this.headerRules.push(...headerRules);
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Adds a single redirect rule (synchronous).
|
|
184
|
+
* @example
|
|
185
|
+
* router.redirect('/old-path', '/new-path', { permanent: true })
|
|
186
|
+
*
|
|
187
|
+
* // With transforms
|
|
188
|
+
* router.redirect('/users/:userId', '/new-users/$1', {
|
|
189
|
+
* permanent: true,
|
|
190
|
+
* requestHeaders: {
|
|
191
|
+
* 'x-user-id': param('userId')
|
|
192
|
+
* }
|
|
193
|
+
* })
|
|
194
|
+
*/
|
|
195
|
+
redirect(source, destination, options) {
|
|
196
|
+
this.validateSourcePattern(source);
|
|
197
|
+
(0, validation_1.validateCaptureGroupReferences)(source, destination);
|
|
198
|
+
// Extract transform options
|
|
199
|
+
const { requestHeaders, responseHeaders, requestQuery, permanent, statusCode, has, missing, } = options || {};
|
|
200
|
+
// If transforms are provided, create a route instead of a redirect
|
|
201
|
+
if (requestHeaders || responseHeaders || requestQuery) {
|
|
202
|
+
const transformOpts = {
|
|
203
|
+
requestHeaders,
|
|
204
|
+
responseHeaders,
|
|
205
|
+
requestQuery,
|
|
206
|
+
};
|
|
207
|
+
const transforms = this.transformOptionsToTransforms(transformOpts);
|
|
208
|
+
this.routeRules.push({
|
|
209
|
+
src: source,
|
|
210
|
+
dest: destination,
|
|
211
|
+
transforms,
|
|
212
|
+
redirect: true,
|
|
213
|
+
status: statusCode || (permanent ? 308 : 307),
|
|
214
|
+
has,
|
|
215
|
+
missing,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
this.redirectRules.push({
|
|
220
|
+
source,
|
|
221
|
+
destination,
|
|
222
|
+
permanent,
|
|
223
|
+
statusCode,
|
|
224
|
+
has,
|
|
225
|
+
missing,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Loads redirect rules asynchronously and appends them.
|
|
232
|
+
*/
|
|
233
|
+
async redirects(provider) {
|
|
234
|
+
const rules = await provider();
|
|
235
|
+
this.redirectRules.push(...rules);
|
|
236
|
+
return this;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Adds a single header rule (synchronous).
|
|
240
|
+
* @example
|
|
241
|
+
* router.header('/api/(.*)', [{ key: 'X-Custom', value: 'HelloWorld' }])
|
|
242
|
+
*/
|
|
243
|
+
header(source, headers, options) {
|
|
244
|
+
this.validateSourcePattern(source);
|
|
245
|
+
this.headerRules.push({ source, headers, ...options });
|
|
246
|
+
return this;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Loads header rules asynchronously and appends them.
|
|
250
|
+
*/
|
|
251
|
+
async headers(provider) {
|
|
252
|
+
const rules = await provider();
|
|
253
|
+
this.headerRules.push(...rules);
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Adds a typed "Cache-Control" header, leveraging `pretty-cache-header`.
|
|
258
|
+
* This method is purely for convenience, so you can do:
|
|
259
|
+
*
|
|
260
|
+
* router.cacheControl('/my-page', {
|
|
261
|
+
* public: true,
|
|
262
|
+
* maxAge: '1week',
|
|
263
|
+
* staleWhileRevalidate: '1year'
|
|
264
|
+
* });
|
|
265
|
+
*/
|
|
266
|
+
cacheControl(source, cacheOptions, options) {
|
|
267
|
+
const value = (0, pretty_cache_header_1.cacheHeader)(cacheOptions);
|
|
268
|
+
this.headerRules.push({
|
|
269
|
+
source,
|
|
270
|
+
headers: [{ key: 'Cache-Control', value }],
|
|
271
|
+
...options,
|
|
272
|
+
});
|
|
273
|
+
return this;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Adds a route with transforms support.
|
|
277
|
+
* This is the newer, more powerful routing format that supports transforms.
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* // Add a route with transforms for path parameters and environment variables
|
|
281
|
+
* router.route({
|
|
282
|
+
* src: '/users/:userId/posts/:postId',
|
|
283
|
+
* dest: 'https://api.example.com/users/$userId/posts/$postId',
|
|
284
|
+
* transforms: [
|
|
285
|
+
* {
|
|
286
|
+
* type: 'request.headers',
|
|
287
|
+
* op: 'set',
|
|
288
|
+
* target: { key: 'x-user-id' },
|
|
289
|
+
* args: '$userId'
|
|
290
|
+
* },
|
|
291
|
+
* {
|
|
292
|
+
* type: 'request.headers',
|
|
293
|
+
* op: 'set',
|
|
294
|
+
* target: { key: 'authorization' },
|
|
295
|
+
* args: 'Bearer $BEARER_TOKEN'
|
|
296
|
+
* }
|
|
297
|
+
* ]
|
|
298
|
+
* });
|
|
299
|
+
*/
|
|
300
|
+
route(config) {
|
|
301
|
+
this.validateSourcePattern(config.src);
|
|
302
|
+
this.routeRules.push(config);
|
|
303
|
+
return this;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* When true, automatically serve HTML files and serverless functions
|
|
307
|
+
* without the .html or .js extension. This is a built-in Vercel feature.
|
|
308
|
+
*/
|
|
309
|
+
setCleanUrls(value) {
|
|
310
|
+
this.cleanUrlsConfig = value;
|
|
311
|
+
return this;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* When true, automatically normalize paths to include a trailing slash.
|
|
315
|
+
*/
|
|
316
|
+
setTrailingSlash(value) {
|
|
317
|
+
this.trailingSlashConfig = value;
|
|
318
|
+
return this;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Sets a custom build command to run during deployment.
|
|
322
|
+
* @example
|
|
323
|
+
* router.setBuildCommand('pnpm run generate-config')
|
|
324
|
+
*/
|
|
325
|
+
setBuildCommand(value) {
|
|
326
|
+
this.buildCommandConfig = value;
|
|
327
|
+
return this;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Sets a custom install command to run during deployment.
|
|
331
|
+
* @example
|
|
332
|
+
* router.setInstallCommand('pnpm install --no-frozen-lockfile')
|
|
333
|
+
*/
|
|
334
|
+
setInstallCommand(value) {
|
|
335
|
+
this.installCommandConfig = value;
|
|
336
|
+
return this;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Adds a single cron rule (synchronous).
|
|
340
|
+
*/
|
|
341
|
+
cron(path, schedule) {
|
|
342
|
+
this.validateCronExpression(schedule);
|
|
343
|
+
this.cronRules.push({ path, schedule });
|
|
344
|
+
return this;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Loads cron rules asynchronously and appends them.
|
|
348
|
+
*/
|
|
349
|
+
async crons(provider) {
|
|
350
|
+
const rules = await provider();
|
|
351
|
+
this.cronRules.push(...rules);
|
|
352
|
+
return this;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Returns the complete router configuration.
|
|
356
|
+
* Typically, you'll export or return this in your build scripts,
|
|
357
|
+
* so that Vercel can pick it up.
|
|
358
|
+
*/
|
|
359
|
+
getConfig() {
|
|
360
|
+
// Separate rewrites into those with and without transforms
|
|
361
|
+
const rewritesWithoutTransforms = this.rewriteRules.filter((r) => !r.transforms);
|
|
362
|
+
const rewritesWithTransforms = this.rewriteRules.filter((r) => r.transforms);
|
|
363
|
+
// Convert rewrites with transforms to routes
|
|
364
|
+
const routesFromRewrites = rewritesWithTransforms.map((rewrite) => {
|
|
365
|
+
const route = {
|
|
366
|
+
src: rewrite.source,
|
|
367
|
+
dest: rewrite.destination,
|
|
368
|
+
transforms: rewrite.transforms,
|
|
369
|
+
};
|
|
370
|
+
if (rewrite.has)
|
|
371
|
+
route.has = rewrite.has;
|
|
372
|
+
if (rewrite.missing)
|
|
373
|
+
route.missing = rewrite.missing;
|
|
374
|
+
return route;
|
|
375
|
+
});
|
|
376
|
+
// Combine with existing routes
|
|
377
|
+
const allRoutes = [...routesFromRewrites, ...this.routeRules];
|
|
378
|
+
// If routes exist, only return routes (not the legacy fields)
|
|
379
|
+
if (allRoutes.length > 0) {
|
|
380
|
+
const config = {
|
|
381
|
+
routes: allRoutes,
|
|
382
|
+
};
|
|
383
|
+
// Only include buildCommand/installCommand if they're explicitly set
|
|
384
|
+
if (this.buildCommandConfig !== undefined) {
|
|
385
|
+
config.buildCommand = this.buildCommandConfig;
|
|
386
|
+
}
|
|
387
|
+
if (this.installCommandConfig !== undefined) {
|
|
388
|
+
config.installCommand = this.installCommandConfig;
|
|
389
|
+
}
|
|
390
|
+
return config;
|
|
391
|
+
}
|
|
392
|
+
// Otherwise, return the legacy format
|
|
393
|
+
const config = {
|
|
394
|
+
redirects: this.redirectRules,
|
|
395
|
+
headers: this.headerRules,
|
|
396
|
+
rewrites: rewritesWithoutTransforms,
|
|
397
|
+
cleanUrls: this.cleanUrlsConfig,
|
|
398
|
+
trailingSlash: this.trailingSlashConfig,
|
|
399
|
+
crons: this.cronRules,
|
|
400
|
+
};
|
|
401
|
+
// Only include buildCommand/installCommand if they're explicitly set
|
|
402
|
+
if (this.buildCommandConfig !== undefined) {
|
|
403
|
+
config.buildCommand = this.buildCommandConfig;
|
|
404
|
+
}
|
|
405
|
+
if (this.installCommandConfig !== undefined) {
|
|
406
|
+
config.installCommand = this.installCommandConfig;
|
|
407
|
+
}
|
|
408
|
+
return config;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Visualizes the routing tree in the order that Vercel applies routes.
|
|
412
|
+
* Returns a formatted string showing the routing hierarchy.
|
|
413
|
+
*/
|
|
414
|
+
visualize() {
|
|
415
|
+
const tree = [
|
|
416
|
+
{
|
|
417
|
+
type: 'Headers',
|
|
418
|
+
rules: this.headerRules,
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
type: 'Redirects',
|
|
422
|
+
rules: this.redirectRules,
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
type: 'Before Filesystem Rewrites',
|
|
426
|
+
rules: this.rewriteRules.filter((rewrite) => rewrite.source.startsWith('/api/') ||
|
|
427
|
+
rewrite.source.startsWith('/_next/')),
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
type: 'Filesystem',
|
|
431
|
+
rules: [], // This would be populated by Vercel's filesystem routing
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
type: 'After Filesystem Rewrites',
|
|
435
|
+
rules: this.rewriteRules.filter((rewrite) => !rewrite.source.startsWith('/api/') &&
|
|
436
|
+
!rewrite.source.startsWith('/_next/') &&
|
|
437
|
+
rewrite.source !== '/(.*)'),
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
type: 'Fallback Rewrites',
|
|
441
|
+
rules: this.rewriteRules.filter((rewrite) => rewrite.source === '/(.*)'),
|
|
442
|
+
},
|
|
443
|
+
];
|
|
444
|
+
return tree
|
|
445
|
+
.map((node) => {
|
|
446
|
+
const rules = node.rules.length > 0
|
|
447
|
+
? node.rules
|
|
448
|
+
.map((rule) => {
|
|
449
|
+
if ('headers' in rule) {
|
|
450
|
+
const headersStr = rule.headers
|
|
451
|
+
.map((h) => `${h.key}: ${h.value}`)
|
|
452
|
+
.join(', ');
|
|
453
|
+
return ` ${rule.source} [${headersStr}]`;
|
|
454
|
+
}
|
|
455
|
+
if ('destination' in rule) {
|
|
456
|
+
return ` ${rule.source} → ${rule.destination}`;
|
|
457
|
+
}
|
|
458
|
+
// @ts-ignore
|
|
459
|
+
return ` ${rule.source}`;
|
|
460
|
+
})
|
|
461
|
+
.join('\n')
|
|
462
|
+
: ' (empty)';
|
|
463
|
+
return `${node.type}:\n${rules}`;
|
|
464
|
+
})
|
|
465
|
+
.join('\n\n');
|
|
466
|
+
}
|
|
467
|
+
validateSourcePattern(source) {
|
|
468
|
+
(0, validation_1.validateRegexPattern)(source);
|
|
469
|
+
}
|
|
470
|
+
validateCronExpression(schedule) {
|
|
471
|
+
(0, validation_1.parseCronExpression)(schedule);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
exports.Router = Router;
|
|
475
|
+
/**
|
|
476
|
+
* A simple factory function for creating a new Router instance.
|
|
477
|
+
* @example
|
|
478
|
+
* import { createRouter } from '@vercel/router-sdk';
|
|
479
|
+
* const router = createRouter();
|
|
480
|
+
*/
|
|
481
|
+
function createRouter() {
|
|
482
|
+
return new Router();
|
|
483
|
+
}
|
|
484
|
+
exports.createRouter = createRouter;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates and type-checks regex patterns for Vercel's path-to-regexp syntax.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* // Valid patterns:
|
|
6
|
+
* "/api/(.*)" // Basic capture group
|
|
7
|
+
* "/blog/:slug" // Named parameter
|
|
8
|
+
* "/feedback/((?!general).*)" // Negative lookahead in a group
|
|
9
|
+
*
|
|
10
|
+
* // Invalid patterns:
|
|
11
|
+
* "/feedback/(?!general)" // Negative lookahead without group
|
|
12
|
+
* "[unclosed" // Invalid regex syntax
|
|
13
|
+
* "/*" // Invalid wildcard pattern
|
|
14
|
+
*/
|
|
15
|
+
export declare function validateRegexPattern(pattern: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Type for cron expression parts
|
|
18
|
+
*/
|
|
19
|
+
export type CronPart = {
|
|
20
|
+
minute: string;
|
|
21
|
+
hour: string;
|
|
22
|
+
dayOfMonth: string;
|
|
23
|
+
month: string;
|
|
24
|
+
dayOfWeek: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Parses a cron expression into its parts
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseCronExpression(expression: string): CronPart;
|
|
30
|
+
/**
|
|
31
|
+
* Creates a type-safe cron expression builder
|
|
32
|
+
*/
|
|
33
|
+
export declare function createCronExpression(parts: CronPart): string;
|
|
34
|
+
/**
|
|
35
|
+
* Counts the number of capture groups in a regex pattern.
|
|
36
|
+
* This includes both numbered groups (.*) and named parameters (:name).
|
|
37
|
+
*/
|
|
38
|
+
export declare function countCaptureGroups(pattern: string): number;
|
|
39
|
+
/**
|
|
40
|
+
* Validates that a destination string doesn't reference capture groups
|
|
41
|
+
* that don't exist in the source pattern.
|
|
42
|
+
*/
|
|
43
|
+
export declare function validateCaptureGroupReferences(source: string, destination: string): void;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateCaptureGroupReferences = exports.countCaptureGroups = exports.createCronExpression = exports.parseCronExpression = exports.validateRegexPattern = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
/**
|
|
6
|
+
* Validates and type-checks regex patterns for Vercel's path-to-regexp syntax.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // Valid patterns:
|
|
10
|
+
* "/api/(.*)" // Basic capture group
|
|
11
|
+
* "/blog/:slug" // Named parameter
|
|
12
|
+
* "/feedback/((?!general).*)" // Negative lookahead in a group
|
|
13
|
+
*
|
|
14
|
+
* // Invalid patterns:
|
|
15
|
+
* "/feedback/(?!general)" // Negative lookahead without group
|
|
16
|
+
* "[unclosed" // Invalid regex syntax
|
|
17
|
+
* "/*" // Invalid wildcard pattern
|
|
18
|
+
*/
|
|
19
|
+
function validateRegexPattern(pattern) {
|
|
20
|
+
// Check for common path-to-regexp syntax errors
|
|
21
|
+
if (pattern.includes("(?!") && !pattern.includes("((?!")) {
|
|
22
|
+
throw new Error(`Invalid path-to-regexp pattern: Negative lookaheads must be wrapped in a group. ` +
|
|
23
|
+
`Use "((?!pattern).*)" instead of "(?!pattern)". Pattern: ${pattern}`);
|
|
24
|
+
}
|
|
25
|
+
// Check for invalid wildcard patterns
|
|
26
|
+
if (pattern.includes("/*") || pattern.includes("/**")) {
|
|
27
|
+
throw new Error(`Invalid path-to-regexp pattern: Use '(.*)' instead of '*' for wildcards. ` +
|
|
28
|
+
`Pattern: ${pattern}`);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
// Test if it's a valid regex pattern
|
|
32
|
+
new RegExp(pattern);
|
|
33
|
+
return pattern;
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
throw new Error(`Invalid regex pattern: ${pattern}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.validateRegexPattern = validateRegexPattern;
|
|
40
|
+
/**
|
|
41
|
+
* Zod schema for validating cron expressions
|
|
42
|
+
*/
|
|
43
|
+
const cronPartSchema = zod_1.z.object({
|
|
44
|
+
minute: zod_1.z
|
|
45
|
+
.string()
|
|
46
|
+
.regex(/^(\*|[0-5]?[0-9]|\*\/[0-9]+|[0-5]?[0-9]-[0-5]?[0-9](,[0-5]?[0-9]-[0-5]?[0-9])*)$/),
|
|
47
|
+
hour: zod_1.z
|
|
48
|
+
.string()
|
|
49
|
+
.regex(/^(\*|1?[0-9]|2[0-3]|\*\/[0-9]+|1?[0-9]-1?[0-9]|2[0-3]-2[0-3](,1?[0-9]-1?[0-9]|,2[0-3]-2[0-3])*)$/),
|
|
50
|
+
dayOfMonth: zod_1.z
|
|
51
|
+
.string()
|
|
52
|
+
.regex(/^(\*|[1-2]?[0-9]|3[0-1]|\*\/[0-9]+|[1-2]?[0-9]-[1-2]?[0-9]|3[0-1]-3[0-1](,[1-2]?[0-9]-[1-2]?[0-9]|,3[0-1]-3[0-1])*)$/),
|
|
53
|
+
month: zod_1.z
|
|
54
|
+
.string()
|
|
55
|
+
.regex(/^(\*|[1-9]|1[0-2]|\*\/[0-9]+|[1-9]-[1-9]|1[0-2]-1[0-2](,[1-9]-[1-9]|,1[0-2]-1[0-2])*)$/),
|
|
56
|
+
dayOfWeek: zod_1.z
|
|
57
|
+
.string()
|
|
58
|
+
.regex(/^(\*|[0-6]|\*\/[0-9]+|[0-6]-[0-6](,[0-6]-[0-6])*)$/)
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* Parses a cron expression into its parts
|
|
62
|
+
*/
|
|
63
|
+
function parseCronExpression(expression) {
|
|
64
|
+
const parts = expression.split(" ");
|
|
65
|
+
if (parts.length !== 5) {
|
|
66
|
+
throw new Error("Invalid cron expression: must have 5 parts");
|
|
67
|
+
}
|
|
68
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
69
|
+
return cronPartSchema.parse({
|
|
70
|
+
minute,
|
|
71
|
+
hour,
|
|
72
|
+
dayOfMonth,
|
|
73
|
+
month,
|
|
74
|
+
dayOfWeek
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
exports.parseCronExpression = parseCronExpression;
|
|
78
|
+
/**
|
|
79
|
+
* Creates a type-safe cron expression builder
|
|
80
|
+
*/
|
|
81
|
+
function createCronExpression(parts) {
|
|
82
|
+
const validated = cronPartSchema.parse(parts);
|
|
83
|
+
return `${validated.minute} ${validated.hour} ${validated.dayOfMonth} ${validated.month} ${validated.dayOfWeek}`;
|
|
84
|
+
}
|
|
85
|
+
exports.createCronExpression = createCronExpression;
|
|
86
|
+
/**
|
|
87
|
+
* Counts the number of capture groups in a regex pattern.
|
|
88
|
+
* This includes both numbered groups (.*) and named parameters (:name).
|
|
89
|
+
*/
|
|
90
|
+
function countCaptureGroups(pattern) {
|
|
91
|
+
let count = 0;
|
|
92
|
+
// Count regex capture groups (parentheses that aren't non-capturing)
|
|
93
|
+
const regex = /\((?!\?:)/g;
|
|
94
|
+
const matches = pattern.match(regex);
|
|
95
|
+
if (matches) {
|
|
96
|
+
count += matches.length;
|
|
97
|
+
}
|
|
98
|
+
// Count named parameters (:name)
|
|
99
|
+
const namedParams = pattern.match(/:[a-zA-Z_][a-zA-Z0-9_]*/g);
|
|
100
|
+
if (namedParams) {
|
|
101
|
+
count += namedParams.length;
|
|
102
|
+
}
|
|
103
|
+
return count;
|
|
104
|
+
}
|
|
105
|
+
exports.countCaptureGroups = countCaptureGroups;
|
|
106
|
+
/**
|
|
107
|
+
* Validates that a destination string doesn't reference capture groups
|
|
108
|
+
* that don't exist in the source pattern.
|
|
109
|
+
*/
|
|
110
|
+
function validateCaptureGroupReferences(source, destination) {
|
|
111
|
+
const captureGroupCount = countCaptureGroups(source);
|
|
112
|
+
const references = destination.match(/\$(\d+)/g);
|
|
113
|
+
if (!references)
|
|
114
|
+
return;
|
|
115
|
+
for (const ref of references) {
|
|
116
|
+
const groupNum = parseInt(ref.substring(1), 10);
|
|
117
|
+
if (groupNum > captureGroupCount) {
|
|
118
|
+
throw new Error(`Invalid capture group reference: ${ref} used in destination "${destination}", ` +
|
|
119
|
+
`but source pattern "${source}" only has ${captureGroupCount} capture group(s). ` +
|
|
120
|
+
`Valid references are: ${Array.from({ length: captureGroupCount }, (_, i) => `$${i + 1}`).join(', ') || 'none'}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.validateCaptureGroupReferences = validateCaptureGroupReferences;
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vercel/config",
|
|
3
|
+
"version": "0.0.9",
|
|
4
|
+
"description": "A TypeScript SDK for programmatically generating Vercel configuration files",
|
|
5
|
+
"bugs": {
|
|
6
|
+
"url": "https://github.com/vercel/router-sdk/issues"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/vercel/router-sdk"
|
|
11
|
+
},
|
|
12
|
+
"author": "Vercel",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"types": "dist/index.d.ts",
|
|
16
|
+
"bin": {
|
|
17
|
+
"@vercel/config": "./dist/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"pretty-cache-header": "^1.0.0",
|
|
24
|
+
"zod": "^3.22.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@changesets/cli": "^2.27.12",
|
|
28
|
+
"@types/node": "^18.0.0",
|
|
29
|
+
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
|
30
|
+
"@typescript-eslint/parser": "^5.0.0",
|
|
31
|
+
"eslint": "^8.0.0",
|
|
32
|
+
"prettier": "^2.8.0",
|
|
33
|
+
"typescript": "^4.9.0",
|
|
34
|
+
"vitest": "^1.0.0"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "restricted"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc",
|
|
41
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
42
|
+
"lint": "eslint . --ext .ts",
|
|
43
|
+
"release": "npm run build && changeset publish",
|
|
44
|
+
"test": "vitest",
|
|
45
|
+
"version-packages": "changeset version"
|
|
46
|
+
}
|
|
47
|
+
}
|