@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/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
+ }