@vercel/config
Advanced tools
| /** | ||
| * @vercel/config/v1 - Main API entry point | ||
| * | ||
| * Usage: | ||
| * import { createRouter, VercelConfig } from '@vercel/config/v1'; | ||
| */ | ||
| export { createRouter, Router } from "../router"; | ||
| export * from "../router"; | ||
| export type { VercelConfig, Redirect, Rewrite, HeaderRule, Condition, RouteType } from "../types"; |
| "use strict"; | ||
| /** | ||
| * @vercel/config/v1 - Main API entry point | ||
| * | ||
| * Usage: | ||
| * import { createRouter, VercelConfig } from '@vercel/config/v1'; | ||
| */ | ||
| var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
| if (k2 === undefined) k2 = k; | ||
| var desc = Object.getOwnPropertyDescriptor(m, k); | ||
| if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
| desc = { enumerable: true, get: function() { return m[k]; } }; | ||
| } | ||
| Object.defineProperty(o, k2, desc); | ||
| }) : (function(o, m, k, k2) { | ||
| if (k2 === undefined) k2 = k; | ||
| o[k2] = m[k]; | ||
| })); | ||
| var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
| for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
| }; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.Router = exports.createRouter = void 0; | ||
| var router_1 = require("../router"); | ||
| Object.defineProperty(exports, "createRouter", { enumerable: true, get: function () { return router_1.createRouter; } }); | ||
| Object.defineProperty(exports, "Router", { enumerable: true, get: function () { return router_1.Router; } }); | ||
| __exportStar(require("../router"), exports); |
+57
-28
@@ -30,2 +30,3 @@ #!/usr/bin/env node | ||
| const fs_2 = require("fs"); | ||
| const validation_1 = require("./utils/validation"); | ||
| /** | ||
@@ -72,29 +73,40 @@ * Named exports that should NOT be auto-converted to config | ||
| /** | ||
| * Read existing vercel.json and extract fields to preserve | ||
| * Compile vercel.ts to JSON and output to stdout | ||
| */ | ||
| function readExistingVercelConfig() { | ||
| const vercelJsonPath = (0, path_1.resolve)(process.cwd(), "vercel.json"); | ||
| if (!(0, fs_2.existsSync)(vercelJsonPath)) { | ||
| return {}; | ||
| async function compileConfig() { | ||
| try { | ||
| const config = await configureRouter(); | ||
| const json = JSON.stringify(config, null, 2); | ||
| console.log(json); | ||
| } | ||
| catch (error) { | ||
| console.error("Failed to compile config:", error); | ||
| process.exit(1); | ||
| } | ||
| } | ||
| /** | ||
| * Validate the vercel.ts config | ||
| */ | ||
| async function validateConfig() { | ||
| var _a, _b, _c, _d; | ||
| try { | ||
| const content = (0, fs_1.readFileSync)(vercelJsonPath, "utf-8"); | ||
| const existing = JSON.parse(content); | ||
| // Extract fields we want to preserve | ||
| const preserved = {}; | ||
| if (existing.buildCommand) { | ||
| preserved.buildCommand = existing.buildCommand; | ||
| } | ||
| if (existing.installCommand) { | ||
| preserved.installCommand = existing.installCommand; | ||
| } | ||
| return preserved; | ||
| const config = await configureRouter(); | ||
| // Validate static fields | ||
| (0, validation_1.validateStaticFields)(config); | ||
| console.log("✓ Config is valid"); | ||
| console.log(` - buildCommand: ${config.buildCommand || '(not set)'}`); | ||
| console.log(` - framework: ${config.framework || '(not set)'}`); | ||
| console.log(` - routes: ${((_a = config.routes) === null || _a === void 0 ? void 0 : _a.length) || 0} route(s)`); | ||
| console.log(` - redirects: ${((_b = config.redirects) === null || _b === void 0 ? void 0 : _b.length) || 0} redirect(s)`); | ||
| console.log(` - rewrites: ${((_c = config.rewrites) === null || _c === void 0 ? void 0 : _c.length) || 0} rewrite(s)`); | ||
| console.log(` - headers: ${((_d = config.headers) === null || _d === void 0 ? void 0 : _d.length) || 0} header(s)`); | ||
| } | ||
| catch (error) { | ||
| console.warn("Could not read existing vercel.json:", error); | ||
| return {}; | ||
| console.error("✗ Config validation failed:"); | ||
| console.error(` ${error}`); | ||
| process.exit(1); | ||
| } | ||
| } | ||
| /** | ||
| * Generates the vercel.json file | ||
| * Generate vercel.json file (for backwards compatibility / development) | ||
| */ | ||
@@ -104,11 +116,5 @@ async function generateVercelConfig() { | ||
| const config = await configureRouter(); | ||
| const existingFields = readExistingVercelConfig(); | ||
| // Merge: generated config takes precedence, but preserve existing build/install commands if not set | ||
| const mergedConfig = { | ||
| ...existingFields, | ||
| ...config, | ||
| }; | ||
| const vercelConfig = JSON.stringify(mergedConfig, null, 2); | ||
| const json = JSON.stringify(config, null, 2); | ||
| const outputPath = (0, path_1.resolve)(process.cwd(), "vercel.json"); | ||
| (0, fs_1.writeFileSync)(outputPath, vercelConfig); | ||
| (0, fs_1.writeFileSync)(outputPath, json); | ||
| console.log("Successfully generated vercel.json"); | ||
@@ -121,5 +127,28 @@ } | ||
| } | ||
| /** | ||
| * CLI entry point | ||
| */ | ||
| async function main() { | ||
| const command = process.argv[2]; | ||
| switch (command) { | ||
| case 'compile': | ||
| await compileConfig(); | ||
| break; | ||
| case 'validate': | ||
| await validateConfig(); | ||
| break; | ||
| case 'generate': | ||
| case undefined: | ||
| // Default to generate for backwards compatibility | ||
| await generateVercelConfig(); | ||
| break; | ||
| default: | ||
| console.error(`Unknown command: ${command}`); | ||
| console.error('Available commands: compile, validate, generate'); | ||
| process.exit(1); | ||
| } | ||
| } | ||
| // Run if this file is executed directly | ||
| if (require.main === module) { | ||
| generateVercelConfig(); | ||
| main(); | ||
| } |
+2
-1
| export { createRouter, Router } from "./router"; | ||
| export * from "./router"; | ||
| export type { VercelConfig } from "./types"; | ||
| export type { VercelConfig, Redirect, Rewrite, HeaderRule, Condition, RouteType } from "./types"; | ||
| export { validateStaticString, validateStaticBoolean, validateStaticObject, validateStaticStringArray, validateStaticFields } from "./utils/validation"; |
+7
-1
@@ -17,3 +17,3 @@ "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.Router = exports.createRouter = void 0; | ||
| exports.validateStaticFields = exports.validateStaticStringArray = exports.validateStaticObject = exports.validateStaticBoolean = exports.validateStaticString = exports.Router = exports.createRouter = void 0; | ||
| var router_1 = require("./router"); | ||
@@ -23,1 +23,7 @@ Object.defineProperty(exports, "createRouter", { enumerable: true, get: function () { return router_1.createRouter; } }); | ||
| __exportStar(require("./router"), exports); | ||
| var validation_1 = require("./utils/validation"); | ||
| Object.defineProperty(exports, "validateStaticString", { enumerable: true, get: function () { return validation_1.validateStaticString; } }); | ||
| Object.defineProperty(exports, "validateStaticBoolean", { enumerable: true, get: function () { return validation_1.validateStaticBoolean; } }); | ||
| Object.defineProperty(exports, "validateStaticObject", { enumerable: true, get: function () { return validation_1.validateStaticObject; } }); | ||
| Object.defineProperty(exports, "validateStaticStringArray", { enumerable: true, get: function () { return validation_1.validateStaticStringArray; } }); | ||
| Object.defineProperty(exports, "validateStaticFields", { enumerable: true, get: function () { return validation_1.validateStaticFields; } }); |
+34
-96
@@ -0,1 +1,2 @@ | ||
| import type { Redirect, Rewrite } from './types'; | ||
| /** | ||
@@ -547,8 +548,3 @@ * Helper function to reference a path parameter in transforms. | ||
| /** | ||
| * Helper to extract environment variable names from a string or string array. | ||
| * Environment variables are identified by the pattern $VAR_NAME where VAR_NAME | ||
| * is typically uppercase with underscores (e.g., $API_KEY, $BEARER_TOKEN). | ||
| */ | ||
| private extractEnvVars; | ||
| /** | ||
| * @deprecated No longer used after refactor to return schema objects directly | ||
| * Internal helper to convert TransformOptions to Transform array | ||
@@ -558,6 +554,4 @@ * @param options Transform options to convert | ||
| */ | ||
| private transformOptionsToTransforms; | ||
| /** | ||
| * Adds a single rewrite rule (synchronous). | ||
| * Automatically enables rewrite caching by adding the x-vercel-enable-rewrite-caching header. | ||
| * Creates a rewrite rule. Returns either a Rewrite object (simple case) or Route with transforms. | ||
| * | ||
@@ -568,20 +562,9 @@ * @example | ||
| * | ||
| * // With transforms using callback | ||
| * // With transforms | ||
| * router.rewrite('/users/:userId', 'https://api.example.com/users/$1', ({userId, env}) => ({ | ||
| * requestHeaders: { | ||
| * 'x-user-id': userId, | ||
| * 'authorization': `Bearer ${env.API_TOKEN}` | ||
| * } | ||
| * })); | ||
| * | ||
| * // With transforms using object (legacy) | ||
| * router.rewrite('/users/:userId', 'https://api.example.com/users/$1', { | ||
| * requestHeaders: { | ||
| * 'x-user-id': param('userId') | ||
| * } | ||
| * }); | ||
| * requestHeaders: { 'x-user-id': userId } | ||
| * })) | ||
| * @internal Can return Route with transforms internally | ||
| */ | ||
| rewrite(source: string, destination: string, optionsOrCallback?: { | ||
| methods?: string[]; | ||
| status?: number; | ||
| has?: Condition[]; | ||
@@ -592,13 +575,5 @@ missing?: Condition[]; | ||
| requestQuery?: Record<string, string | string[]>; | ||
| appendRequestHeaders?: Record<string, string | string[]>; | ||
| appendResponseHeaders?: Record<string, string | string[]>; | ||
| appendRequestQuery?: Record<string, string | string[]>; | ||
| deleteRequestHeaders?: string[]; | ||
| deleteResponseHeaders?: string[]; | ||
| deleteRequestQuery?: string[]; | ||
| } | ((params: Record<string, string> & { | ||
| env: any; | ||
| }) => { | ||
| methods?: string[]; | ||
| status?: number; | ||
| has?: Condition[]; | ||
@@ -609,40 +584,16 @@ missing?: Condition[]; | ||
| requestQuery?: Record<string, string | string[]>; | ||
| appendRequestHeaders?: Record<string, string | string[]>; | ||
| appendResponseHeaders?: Record<string, string | string[]>; | ||
| appendRequestQuery?: Record<string, string | string[]>; | ||
| deleteRequestHeaders?: string[]; | ||
| deleteResponseHeaders?: string[]; | ||
| deleteRequestQuery?: string[]; | ||
| })): this; | ||
| })): Rewrite | Route; | ||
| /** | ||
| * Loads rewrite rules asynchronously and appends them. | ||
| * Automatically enables rewrite caching for all loaded rules by adding the x-vercel-enable-rewrite-caching header. | ||
| * Creates a redirect rule. Returns either a Redirect object (simple case) or Route with transforms. | ||
| * | ||
| * @example | ||
| * // This will automatically enable caching for all rewrites | ||
| * await router.rewrites(() => fetchRewriteRulesFromDB()); | ||
| */ | ||
| rewrites(provider: RewriteProvider): Promise<this>; | ||
| /** | ||
| * Adds a single redirect rule (synchronous). | ||
| * @example | ||
| * // Simple redirect | ||
| * router.redirect('/old-path', '/new-path', { permanent: true }) | ||
| * | ||
| * // With transforms using callback | ||
| * // With transforms | ||
| * router.redirect('/users/:userId', '/new-users/$1', ({userId, env}) => ({ | ||
| * permanent: true, | ||
| * requestHeaders: { | ||
| * 'x-user-id': userId, | ||
| * 'x-api-key': env.API_KEY | ||
| * } | ||
| * requestHeaders: { 'x-user-id': userId } | ||
| * })) | ||
| * | ||
| * // With transforms using object (legacy) | ||
| * router.redirect('/users/:userId', '/new-users/$1', { | ||
| * permanent: true, | ||
| * requestHeaders: { | ||
| * 'x-user-id': param('userId') | ||
| * } | ||
| * }) | ||
| * @internal Can return Route with transforms internally | ||
| */ | ||
@@ -655,10 +606,2 @@ redirect(source: string, destination: string, optionsOrCallback?: { | ||
| requestHeaders?: Record<string, string | string[]>; | ||
| responseHeaders?: Record<string, string | string[]>; | ||
| requestQuery?: Record<string, string | string[]>; | ||
| appendRequestHeaders?: Record<string, string | string[]>; | ||
| appendResponseHeaders?: Record<string, string | string[]>; | ||
| appendRequestQuery?: Record<string, string | string[]>; | ||
| deleteRequestHeaders?: string[]; | ||
| deleteResponseHeaders?: string[]; | ||
| deleteRequestQuery?: string[]; | ||
| } | ((params: Record<string, string> & { | ||
@@ -672,17 +615,5 @@ env: any; | ||
| requestHeaders?: Record<string, string | string[]>; | ||
| responseHeaders?: Record<string, string | string[]>; | ||
| requestQuery?: Record<string, string | string[]>; | ||
| appendRequestHeaders?: Record<string, string | string[]>; | ||
| appendResponseHeaders?: Record<string, string | string[]>; | ||
| appendRequestQuery?: Record<string, string | string[]>; | ||
| deleteRequestHeaders?: string[]; | ||
| deleteResponseHeaders?: string[]; | ||
| deleteRequestQuery?: string[]; | ||
| })): this; | ||
| })): Redirect | Route; | ||
| /** | ||
| * Loads redirect rules asynchronously and appends them. | ||
| */ | ||
| redirects(provider: RedirectProvider): Promise<this>; | ||
| /** | ||
| * Adds a single header rule (synchronous). | ||
| * Creates a header rule matching the vercel.json schema. | ||
| * @example | ||
@@ -694,16 +625,18 @@ * router.header('/api/(.*)', [{ key: 'X-Custom', value: 'HelloWorld' }]) | ||
| missing?: Condition[]; | ||
| }): this; | ||
| }): { | ||
| source: string; | ||
| headers: Header[]; | ||
| has?: Condition[]; | ||
| missing?: Condition[]; | ||
| }; | ||
| /** | ||
| * Loads header rules asynchronously and appends them. | ||
| */ | ||
| headers(provider: HeaderProvider): Promise<this>; | ||
| /** | ||
| * Adds a typed "Cache-Control" header, leveraging `pretty-cache-header`. | ||
| * This method is purely for convenience, so you can do: | ||
| * Creates a Cache-Control header rule, leveraging `pretty-cache-header`. | ||
| * Returns a HeaderRule matching the vercel.json schema. | ||
| * | ||
| * router.cacheControl('/my-page', { | ||
| * public: true, | ||
| * maxAge: '1week', | ||
| * staleWhileRevalidate: '1year' | ||
| * }); | ||
| * @example | ||
| * router.cacheControl('/my-page', { | ||
| * public: true, | ||
| * maxAge: '1week', | ||
| * staleWhileRevalidate: '1year' | ||
| * }) | ||
| */ | ||
@@ -713,3 +646,8 @@ cacheControl(source: string, cacheOptions: CacheOptions, options?: { | ||
| missing?: Condition[]; | ||
| }): this; | ||
| }): { | ||
| source: string; | ||
| headers: Header[]; | ||
| has?: Condition[]; | ||
| missing?: Condition[]; | ||
| }; | ||
| /** | ||
@@ -716,0 +654,0 @@ * Adds a route with transforms support. |
+117
-301
@@ -114,21 +114,5 @@ "use strict"; | ||
| } | ||
| // Deprecated: extractEnvVars method no longer needed after refactor | ||
| /** | ||
| * Helper to extract environment variable names from a string or string array. | ||
| * Environment variables are identified by the pattern $VAR_NAME where VAR_NAME | ||
| * is typically uppercase with underscores (e.g., $API_KEY, $BEARER_TOKEN). | ||
| */ | ||
| extractEnvVars(args) { | ||
| const envVars = new Set(); | ||
| const values = Array.isArray(args) ? args : [args]; | ||
| for (const value of values) { | ||
| const matches = value.match(/\$([A-Z][A-Z0-9_]*)/g); | ||
| if (matches) { | ||
| for (const match of matches) { | ||
| envVars.add(match.substring(1)); | ||
| } | ||
| } | ||
| } | ||
| return Array.from(envVars); | ||
| } | ||
| /** | ||
| * @deprecated No longer used after refactor to return schema objects directly | ||
| * Internal helper to convert TransformOptions to Transform array | ||
@@ -138,130 +122,5 @@ * @param options Transform options to convert | ||
| */ | ||
| transformOptionsToTransforms(options, trackedEnvVars) { | ||
| const transforms = []; | ||
| // Helper to get env vars for a value | ||
| const getEnvVars = (value) => { | ||
| if (trackedEnvVars) { | ||
| return Array.from(trackedEnvVars).filter(envVar => { | ||
| const valueStr = Array.isArray(value) ? value.join(' ') : value; | ||
| return valueStr.includes(`$${envVar}`); | ||
| }); | ||
| } | ||
| return this.extractEnvVars(value); | ||
| }; | ||
| // SET operations | ||
| // Convert requestHeaders (set) | ||
| if (options.requestHeaders) { | ||
| for (const [key, value] of Object.entries(options.requestHeaders)) { | ||
| const envVars = getEnvVars(value); | ||
| transforms.push({ | ||
| type: 'request.headers', | ||
| op: 'set', | ||
| target: { key }, | ||
| args: value, | ||
| ...(envVars.length > 0 && { env: envVars }), | ||
| }); | ||
| } | ||
| } | ||
| // Convert responseHeaders (set) | ||
| if (options.responseHeaders) { | ||
| for (const [key, value] of Object.entries(options.responseHeaders)) { | ||
| const envVars = getEnvVars(value); | ||
| transforms.push({ | ||
| type: 'response.headers', | ||
| op: 'set', | ||
| target: { key }, | ||
| args: value, | ||
| ...(envVars.length > 0 && { env: envVars }), | ||
| }); | ||
| } | ||
| } | ||
| // Convert requestQuery (set) | ||
| if (options.requestQuery) { | ||
| for (const [key, value] of Object.entries(options.requestQuery)) { | ||
| const envVars = getEnvVars(value); | ||
| transforms.push({ | ||
| type: 'request.query', | ||
| op: 'set', | ||
| target: { key }, | ||
| args: value, | ||
| ...(envVars.length > 0 && { env: envVars }), | ||
| }); | ||
| } | ||
| } | ||
| // APPEND operations | ||
| // Convert appendRequestHeaders | ||
| if (options.appendRequestHeaders) { | ||
| for (const [key, value] of Object.entries(options.appendRequestHeaders)) { | ||
| const envVars = getEnvVars(value); | ||
| transforms.push({ | ||
| type: 'request.headers', | ||
| op: 'append', | ||
| target: { key }, | ||
| args: value, | ||
| ...(envVars.length > 0 && { env: envVars }), | ||
| }); | ||
| } | ||
| } | ||
| // Convert appendResponseHeaders | ||
| if (options.appendResponseHeaders) { | ||
| for (const [key, value] of Object.entries(options.appendResponseHeaders)) { | ||
| const envVars = getEnvVars(value); | ||
| transforms.push({ | ||
| type: 'response.headers', | ||
| op: 'append', | ||
| target: { key }, | ||
| args: value, | ||
| ...(envVars.length > 0 && { env: envVars }), | ||
| }); | ||
| } | ||
| } | ||
| // Convert appendRequestQuery | ||
| if (options.appendRequestQuery) { | ||
| for (const [key, value] of Object.entries(options.appendRequestQuery)) { | ||
| const envVars = getEnvVars(value); | ||
| transforms.push({ | ||
| type: 'request.query', | ||
| op: 'append', | ||
| target: { key }, | ||
| args: value, | ||
| ...(envVars.length > 0 && { env: envVars }), | ||
| }); | ||
| } | ||
| } | ||
| // DELETE operations | ||
| // Convert deleteRequestHeaders | ||
| if (options.deleteRequestHeaders) { | ||
| for (const key of options.deleteRequestHeaders) { | ||
| transforms.push({ | ||
| type: 'request.headers', | ||
| op: 'delete', | ||
| target: { key }, | ||
| }); | ||
| } | ||
| } | ||
| // Convert deleteResponseHeaders | ||
| if (options.deleteResponseHeaders) { | ||
| for (const key of options.deleteResponseHeaders) { | ||
| transforms.push({ | ||
| type: 'response.headers', | ||
| op: 'delete', | ||
| target: { key }, | ||
| }); | ||
| } | ||
| } | ||
| // Convert deleteRequestQuery | ||
| if (options.deleteRequestQuery) { | ||
| for (const key of options.deleteRequestQuery) { | ||
| transforms.push({ | ||
| type: 'request.query', | ||
| op: 'delete', | ||
| target: { key }, | ||
| }); | ||
| } | ||
| } | ||
| return transforms; | ||
| } | ||
| // Deprecated: transformOptionsToTransforms method removed after refactor | ||
| /** | ||
| * Adds a single rewrite rule (synchronous). | ||
| * Automatically enables rewrite caching by adding the x-vercel-enable-rewrite-caching header. | ||
| * Creates a rewrite rule. Returns either a Rewrite object (simple case) or Route with transforms. | ||
| * | ||
@@ -272,16 +131,7 @@ * @example | ||
| * | ||
| * // With transforms using callback | ||
| * // With transforms | ||
| * router.rewrite('/users/:userId', 'https://api.example.com/users/$1', ({userId, env}) => ({ | ||
| * requestHeaders: { | ||
| * 'x-user-id': userId, | ||
| * 'authorization': `Bearer ${env.API_TOKEN}` | ||
| * } | ||
| * })); | ||
| * | ||
| * // With transforms using object (legacy) | ||
| * router.rewrite('/users/:userId', 'https://api.example.com/users/$1', { | ||
| * requestHeaders: { | ||
| * 'x-user-id': param('userId') | ||
| * } | ||
| * }); | ||
| * requestHeaders: { 'x-user-id': userId } | ||
| * })) | ||
| * @internal Can return Route with transforms internally | ||
| */ | ||
@@ -292,8 +142,6 @@ rewrite(source, destination, optionsOrCallback) { | ||
| let options; | ||
| let trackedEnvVars; | ||
| // Handle callback syntax | ||
| if (typeof optionsOrCallback === 'function') { | ||
| const pathParams = this.extractPathParams(source); | ||
| const { proxy: envProxy, accessedVars } = this.createEnvProxy(); | ||
| trackedEnvVars = accessedVars; | ||
| const { proxy: envProxy } = this.createEnvProxy(); | ||
| // Create params object with path parameters as $paramName | ||
@@ -311,86 +159,73 @@ const paramsObj = {}; | ||
| } | ||
| // Extract transform options | ||
| const { methods, status, requestHeaders, responseHeaders, requestQuery, appendRequestHeaders, appendResponseHeaders, appendRequestQuery, deleteRequestHeaders, deleteResponseHeaders, deleteRequestQuery, has, missing } = options || {}; | ||
| const transformOpts = { | ||
| requestHeaders, | ||
| responseHeaders, | ||
| requestQuery, | ||
| appendRequestHeaders, | ||
| appendResponseHeaders, | ||
| appendRequestQuery, | ||
| deleteRequestHeaders, | ||
| deleteResponseHeaders, | ||
| deleteRequestQuery, | ||
| }; | ||
| // Convert to transforms if any transform options provided | ||
| const hasTransforms = requestHeaders || responseHeaders || requestQuery || | ||
| appendRequestHeaders || appendResponseHeaders || appendRequestQuery || | ||
| deleteRequestHeaders || deleteResponseHeaders || deleteRequestQuery; | ||
| const transforms = hasTransforms | ||
| ? this.transformOptionsToTransforms(transformOpts, trackedEnvVars) | ||
| : undefined; | ||
| this.rewriteRules.push({ | ||
| const { has, missing, requestHeaders, responseHeaders, requestQuery } = options || {}; | ||
| // Check if any transforms were provided | ||
| const hasTransforms = requestHeaders || responseHeaders || requestQuery; | ||
| if (hasTransforms) { | ||
| // Build a Route object with transforms | ||
| const transforms = []; | ||
| if (requestHeaders) { | ||
| for (const [key, value] of Object.entries(requestHeaders)) { | ||
| transforms.push({ | ||
| type: 'request.headers', | ||
| op: 'set', | ||
| target: { key }, | ||
| args: value, | ||
| }); | ||
| } | ||
| } | ||
| if (responseHeaders) { | ||
| for (const [key, value] of Object.entries(responseHeaders)) { | ||
| transforms.push({ | ||
| type: 'response.headers', | ||
| op: 'set', | ||
| target: { key }, | ||
| args: value, | ||
| }); | ||
| } | ||
| } | ||
| if (requestQuery) { | ||
| for (const [key, value] of Object.entries(requestQuery)) { | ||
| transforms.push({ | ||
| type: 'request.query', | ||
| op: 'set', | ||
| target: { key }, | ||
| args: value, | ||
| }); | ||
| } | ||
| } | ||
| const route = { | ||
| src: source, | ||
| dest: destination, | ||
| transforms, | ||
| }; | ||
| if (has) | ||
| route.has = has; | ||
| if (missing) | ||
| route.missing = missing; | ||
| return route; | ||
| } | ||
| // Simple rewrite without transforms | ||
| const rewrite = { | ||
| source, | ||
| destination, | ||
| ...(methods && { methods }), | ||
| ...(status && { status }), | ||
| has, | ||
| missing, | ||
| transforms, | ||
| }); | ||
| // Only enable rewrite caching for rewrites without transforms | ||
| // (transforms convert to routes, which don't need the caching header) | ||
| if (!transforms) { | ||
| this.headerRules.push({ | ||
| source, | ||
| headers: [{ key: 'x-vercel-enable-rewrite-caching', value: '1' }], | ||
| has, | ||
| missing, | ||
| }); | ||
| } | ||
| return this; | ||
| }; | ||
| if (has) | ||
| rewrite.has = has; | ||
| if (missing) | ||
| rewrite.missing = missing; | ||
| return rewrite; | ||
| } | ||
| /** | ||
| * Loads rewrite rules asynchronously and appends them. | ||
| * Automatically enables rewrite caching for all loaded rules by adding the x-vercel-enable-rewrite-caching header. | ||
| * Creates a redirect rule. Returns either a Redirect object (simple case) or Route with transforms. | ||
| * | ||
| * @example | ||
| * // This will automatically enable caching for all rewrites | ||
| * await router.rewrites(() => fetchRewriteRulesFromDB()); | ||
| */ | ||
| async rewrites(provider) { | ||
| const rules = await provider(); | ||
| this.rewriteRules.push(...rules); | ||
| // Automatically enable rewrite caching for all rules | ||
| const headerRules = rules.map((rule) => ({ | ||
| source: rule.source, | ||
| headers: [{ key: 'x-vercel-enable-rewrite-caching', value: '1' }], | ||
| has: rule.has, | ||
| missing: rule.missing, | ||
| })); | ||
| this.headerRules.push(...headerRules); | ||
| return this; | ||
| } | ||
| /** | ||
| * Adds a single redirect rule (synchronous). | ||
| * @example | ||
| * // Simple redirect | ||
| * router.redirect('/old-path', '/new-path', { permanent: true }) | ||
| * | ||
| * // With transforms using callback | ||
| * // With transforms | ||
| * router.redirect('/users/:userId', '/new-users/$1', ({userId, env}) => ({ | ||
| * permanent: true, | ||
| * requestHeaders: { | ||
| * 'x-user-id': userId, | ||
| * 'x-api-key': env.API_KEY | ||
| * } | ||
| * requestHeaders: { 'x-user-id': userId } | ||
| * })) | ||
| * | ||
| * // With transforms using object (legacy) | ||
| * router.redirect('/users/:userId', '/new-users/$1', { | ||
| * permanent: true, | ||
| * requestHeaders: { | ||
| * 'x-user-id': param('userId') | ||
| * } | ||
| * }) | ||
| * @internal Can return Route with transforms internally | ||
| */ | ||
@@ -401,8 +236,6 @@ redirect(source, destination, optionsOrCallback) { | ||
| let options; | ||
| let trackedEnvVars; | ||
| // Handle callback syntax | ||
| if (typeof optionsOrCallback === 'function') { | ||
| const pathParams = this.extractPathParams(source); | ||
| const { proxy: envProxy, accessedVars } = this.createEnvProxy(); | ||
| trackedEnvVars = accessedVars; | ||
| const { proxy: envProxy } = this.createEnvProxy(); | ||
| // Create params object with path parameters as $paramName | ||
@@ -420,54 +253,45 @@ const paramsObj = {}; | ||
| } | ||
| // Extract transform options | ||
| const { methods, requestHeaders, responseHeaders, requestQuery, appendRequestHeaders, appendResponseHeaders, appendRequestQuery, deleteRequestHeaders, deleteResponseHeaders, deleteRequestQuery, permanent, statusCode, has, missing, } = options || {}; | ||
| // If transforms are provided, create a route instead of a redirect | ||
| const hasTransforms = requestHeaders || responseHeaders || requestQuery || | ||
| appendRequestHeaders || appendResponseHeaders || appendRequestQuery || | ||
| deleteRequestHeaders || deleteResponseHeaders || deleteRequestQuery; | ||
| if (hasTransforms) { | ||
| const transformOpts = { | ||
| requestHeaders, | ||
| responseHeaders, | ||
| requestQuery, | ||
| appendRequestHeaders, | ||
| appendResponseHeaders, | ||
| appendRequestQuery, | ||
| deleteRequestHeaders, | ||
| deleteResponseHeaders, | ||
| deleteRequestQuery, | ||
| }; | ||
| const transforms = this.transformOptionsToTransforms(transformOpts, trackedEnvVars); | ||
| this.routeRules.push({ | ||
| const { permanent, statusCode, has, missing, requestHeaders } = options || {}; | ||
| // Check if transforms were provided | ||
| if (requestHeaders) { | ||
| // Build a Route object with transforms | ||
| const transforms = []; | ||
| for (const [key, value] of Object.entries(requestHeaders)) { | ||
| transforms.push({ | ||
| type: 'request.headers', | ||
| op: 'set', | ||
| target: { key }, | ||
| args: value, | ||
| }); | ||
| } | ||
| const route = { | ||
| src: source, | ||
| dest: destination, | ||
| ...(methods && { methods }), | ||
| transforms, | ||
| redirect: true, | ||
| status: statusCode || (permanent ? 308 : 307), | ||
| has, | ||
| missing, | ||
| }); | ||
| transforms, | ||
| }; | ||
| if (has) | ||
| route.has = has; | ||
| if (missing) | ||
| route.missing = missing; | ||
| return route; | ||
| } | ||
| else { | ||
| this.redirectRules.push({ | ||
| source, | ||
| destination, | ||
| permanent, | ||
| statusCode, | ||
| has, | ||
| missing, | ||
| }); | ||
| } | ||
| return this; | ||
| // Simple redirect without transforms | ||
| const redirect = { | ||
| source, | ||
| destination, | ||
| }; | ||
| if (permanent !== undefined) | ||
| redirect.permanent = permanent; | ||
| if (statusCode !== undefined) | ||
| redirect.statusCode = statusCode; | ||
| if (has) | ||
| redirect.has = has; | ||
| if (missing) | ||
| redirect.missing = missing; | ||
| return redirect; | ||
| } | ||
| /** | ||
| * Loads redirect rules asynchronously and appends them. | ||
| */ | ||
| async redirects(provider) { | ||
| const rules = await provider(); | ||
| this.redirectRules.push(...rules); | ||
| return this; | ||
| } | ||
| /** | ||
| * Adds a single header rule (synchronous). | ||
| * Creates a header rule matching the vercel.json schema. | ||
| * @example | ||
@@ -478,31 +302,23 @@ * router.header('/api/(.*)', [{ key: 'X-Custom', value: 'HelloWorld' }]) | ||
| this.validateSourcePattern(source); | ||
| this.headerRules.push({ source, headers, ...options }); | ||
| return this; | ||
| return { source, headers, ...options }; | ||
| } | ||
| /** | ||
| * Loads header rules asynchronously and appends them. | ||
| */ | ||
| async headers(provider) { | ||
| const rules = await provider(); | ||
| this.headerRules.push(...rules); | ||
| return this; | ||
| } | ||
| /** | ||
| * Adds a typed "Cache-Control" header, leveraging `pretty-cache-header`. | ||
| * This method is purely for convenience, so you can do: | ||
| * Creates a Cache-Control header rule, leveraging `pretty-cache-header`. | ||
| * Returns a HeaderRule matching the vercel.json schema. | ||
| * | ||
| * router.cacheControl('/my-page', { | ||
| * public: true, | ||
| * maxAge: '1week', | ||
| * staleWhileRevalidate: '1year' | ||
| * }); | ||
| * @example | ||
| * router.cacheControl('/my-page', { | ||
| * public: true, | ||
| * maxAge: '1week', | ||
| * staleWhileRevalidate: '1year' | ||
| * }) | ||
| */ | ||
| cacheControl(source, cacheOptions, options) { | ||
| this.validateSourcePattern(source); | ||
| const value = (0, pretty_cache_header_1.cacheHeader)(cacheOptions); | ||
| this.headerRules.push({ | ||
| return { | ||
| source, | ||
| headers: [{ key: 'Cache-Control', value }], | ||
| ...options, | ||
| }); | ||
| return this; | ||
| }; | ||
| } | ||
@@ -509,0 +325,0 @@ /** |
+57
-3
@@ -37,2 +37,5 @@ /** | ||
| } | ||
| /** | ||
| * HTTP header key/value pair | ||
| */ | ||
| export interface Header { | ||
@@ -42,11 +45,56 @@ key: string; | ||
| } | ||
| /** | ||
| * Condition for matching in redirects, rewrites, and headers | ||
| */ | ||
| export interface Condition { | ||
| type: 'header' | 'cookie' | 'host' | 'query' | 'path'; | ||
| key?: string; | ||
| value?: string | number; | ||
| inc?: string[]; | ||
| pre?: string; | ||
| eq?: string | number; | ||
| neq?: string; | ||
| gt?: number; | ||
| gte?: number; | ||
| lt?: number; | ||
| lte?: number; | ||
| } | ||
| /** | ||
| * Redirect matching vercel.json schema | ||
| * Returned by router.redirect() | ||
| */ | ||
| export interface Redirect { | ||
| source: string; | ||
| destination: string; | ||
| permanent?: boolean; | ||
| statusCode?: number; | ||
| has?: Condition[]; | ||
| missing?: Condition[]; | ||
| } | ||
| /** | ||
| * Rewrite matching vercel.json schema | ||
| * Returned by router.rewrite() | ||
| */ | ||
| export interface Rewrite { | ||
| source: string; | ||
| destination: string; | ||
| has?: Condition[]; | ||
| missing?: Condition[]; | ||
| } | ||
| /** | ||
| * Header rule matching vercel.json schema | ||
| * Returned by router.header() and router.cacheControl() | ||
| */ | ||
| export interface HeaderRule { | ||
| source: string; | ||
| headers: Header[]; | ||
| has?: Condition[]; | ||
| missing?: Condition[]; | ||
| } | ||
| /** | ||
| * Route represents a single routing rule that can be a redirect, rewrite, or header rule | ||
| * Matches the output of router.redirect(), router.rewrite(), router.header(), etc. | ||
| * Union type for all router helper outputs | ||
| * Can be simple schema objects (Redirect, Rewrite, HeaderRule) or Routes with transforms | ||
| * Note: Route type is defined in router.ts (uses src/dest instead of source/destination) | ||
| */ | ||
| export type RouteType = any; | ||
| export type RouteType = Redirect | Rewrite | HeaderRule | any; | ||
| export interface WildcardDomain { | ||
@@ -109,2 +157,8 @@ domain: string; | ||
| /** | ||
| * Routes configuration using the lower-level routes primitive. | ||
| * Use this if you need transforms or want everything in one place. | ||
| * Cannot be mixed with headers, redirects, or rewrites. | ||
| */ | ||
| routes?: RouteType[]; | ||
| /** | ||
| * Wildcard domain configuration. | ||
@@ -111,0 +165,0 @@ */ |
@@ -44,1 +44,28 @@ /** | ||
| export declare function validateCaptureGroupReferences(source: string, destination: string): void; | ||
| /** | ||
| * Validates that a value is a static string literal (not computed, not a function call, etc.) | ||
| * Used for static fields that must be extracted before build execution. | ||
| */ | ||
| export declare function validateStaticString(value: any, fieldName: string): void; | ||
| /** | ||
| * Validates that a value is a static boolean literal | ||
| */ | ||
| export declare function validateStaticBoolean(value: any, fieldName: string): void; | ||
| /** | ||
| * Validates that a value is a static object with primitive values | ||
| * Used for git.deploymentEnabled and similar objects that need to be static | ||
| */ | ||
| export declare function validateStaticObject(value: any, fieldName: string): void; | ||
| /** | ||
| * Validates that a value is a static array of strings | ||
| */ | ||
| export declare function validateStaticStringArray(value: any, fieldName: string): void; | ||
| /** | ||
| * Validates static fields in VercelConfig that must be extracted before build execution. | ||
| * These fields include: | ||
| * - buildCommand, devCommand, installCommand, framework, nodeVersion, outputDirectory | ||
| * - github.enabled, github.autoAlias, github.autoJobCancelation | ||
| * - git.deploymentEnabled | ||
| * - relatedProjects | ||
| */ | ||
| export declare function validateStaticFields(config: Record<string, any>): void; |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.validateCaptureGroupReferences = exports.countCaptureGroups = exports.createCronExpression = exports.parseCronExpression = exports.validateRegexPattern = void 0; | ||
| exports.validateStaticFields = exports.validateStaticStringArray = exports.validateStaticObject = exports.validateStaticBoolean = exports.validateStaticString = exports.validateCaptureGroupReferences = exports.countCaptureGroups = exports.createCronExpression = exports.parseCronExpression = exports.validateRegexPattern = void 0; | ||
| const zod_1 = require("zod"); | ||
@@ -125,1 +125,96 @@ /** | ||
| exports.validateCaptureGroupReferences = validateCaptureGroupReferences; | ||
| /** | ||
| * Validates that a value is a static string literal (not computed, not a function call, etc.) | ||
| * Used for static fields that must be extracted before build execution. | ||
| */ | ||
| function validateStaticString(value, fieldName) { | ||
| if (typeof value !== 'string') { | ||
| throw new Error(`Field "${fieldName}" must be a static string literal. ` + | ||
| `Got ${typeof value}. Function calls, variables, and expressions are not allowed.`); | ||
| } | ||
| } | ||
| exports.validateStaticString = validateStaticString; | ||
| /** | ||
| * Validates that a value is a static boolean literal | ||
| */ | ||
| function validateStaticBoolean(value, fieldName) { | ||
| if (typeof value !== 'boolean') { | ||
| throw new Error(`Field "${fieldName}" must be a static boolean literal. ` + | ||
| `Got ${typeof value}. Only true or false are allowed.`); | ||
| } | ||
| } | ||
| exports.validateStaticBoolean = validateStaticBoolean; | ||
| /** | ||
| * Validates that a value is a static object with primitive values | ||
| * Used for git.deploymentEnabled and similar objects that need to be static | ||
| */ | ||
| function validateStaticObject(value, fieldName) { | ||
| if (typeof value !== 'object' || value === null) { | ||
| throw new Error(`Field "${fieldName}" must be a static object with primitive values. ` + | ||
| `Got ${typeof value}.`); | ||
| } | ||
| for (const [key, val] of Object.entries(value)) { | ||
| if (typeof val !== 'boolean' && typeof val !== 'string' && typeof val !== 'number') { | ||
| throw new Error(`Field "${fieldName}.${key}" must contain only static primitive values (string, number, boolean). ` + | ||
| `Got ${typeof val}.`); | ||
| } | ||
| } | ||
| } | ||
| exports.validateStaticObject = validateStaticObject; | ||
| /** | ||
| * Validates that a value is a static array of strings | ||
| */ | ||
| function validateStaticStringArray(value, fieldName) { | ||
| if (!Array.isArray(value)) { | ||
| throw new Error(`Field "${fieldName}" must be a static array of strings. ` + | ||
| `Got ${typeof value}.`); | ||
| } | ||
| for (let i = 0; i < value.length; i++) { | ||
| if (typeof value[i] !== 'string') { | ||
| throw new Error(`Field "${fieldName}[${i}]" must be a static string. ` + | ||
| `Got ${typeof value[i]}.`); | ||
| } | ||
| } | ||
| } | ||
| exports.validateStaticStringArray = validateStaticStringArray; | ||
| /** | ||
| * Validates static fields in VercelConfig that must be extracted before build execution. | ||
| * These fields include: | ||
| * - buildCommand, devCommand, installCommand, framework, nodeVersion, outputDirectory | ||
| * - github.enabled, github.autoAlias, github.autoJobCancelation | ||
| * - git.deploymentEnabled | ||
| * - relatedProjects | ||
| */ | ||
| function validateStaticFields(config) { | ||
| // Validate string fields | ||
| const stringFields = ['buildCommand', 'devCommand', 'installCommand', 'framework', 'nodeVersion', 'outputDirectory']; | ||
| for (const field of stringFields) { | ||
| if (config[field] !== undefined && config[field] !== null) { | ||
| validateStaticString(config[field], field); | ||
| } | ||
| } | ||
| // Validate relatedProjects (array of strings) | ||
| if (config.relatedProjects !== undefined) { | ||
| validateStaticStringArray(config.relatedProjects, 'relatedProjects'); | ||
| } | ||
| // Validate git.deploymentEnabled (boolean or object with branch booleans) | ||
| if (config.git !== undefined && config.git.deploymentEnabled !== undefined) { | ||
| const deploymentEnabled = config.git.deploymentEnabled; | ||
| if (typeof deploymentEnabled === 'boolean') { | ||
| validateStaticBoolean(deploymentEnabled, 'git.deploymentEnabled'); | ||
| } | ||
| else { | ||
| validateStaticObject(deploymentEnabled, 'git.deploymentEnabled'); | ||
| } | ||
| } | ||
| // Validate github fields (booleans) | ||
| const githubBooleanFields = ['enabled', 'autoAlias', 'autoJobCancelation']; | ||
| if (config.github !== undefined) { | ||
| for (const field of githubBooleanFields) { | ||
| if (config.github[field] !== undefined) { | ||
| validateStaticBoolean(config.github[field], `github.${field}`); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| exports.validateStaticFields = validateStaticFields; |
+15
-1
| { | ||
| "name": "@vercel/config", | ||
| "version": "0.0.14", | ||
| "version": "0.0.16", | ||
| "description": "A TypeScript SDK for programmatically generating Vercel configuration files", | ||
@@ -16,2 +16,16 @@ "bugs": { | ||
| "types": "dist/index.d.ts", | ||
| "exports": { | ||
| ".": "./dist/index.js", | ||
| "./v1": "./dist/v1/index.js" | ||
| }, | ||
| "typesVersions": { | ||
| "*": { | ||
| ".": [ | ||
| "dist/index.d.ts" | ||
| ], | ||
| "v1": [ | ||
| "dist/v1/index.d.ts" | ||
| ] | ||
| } | ||
| }, | ||
| "bin": { | ||
@@ -18,0 +32,0 @@ "@vercel/config": "./dist/cli.js" |
+75
-298
| # @vercel/config | ||
| TypeScript SDK for defining Vercel configuration programmatically. | ||
| TypeScript SDK for programmatically defining Vercel configuration. Write type-safe routing rules and build configuration in TypeScript instead of JSON. | ||
@@ -11,3 +11,3 @@ ## Installation | ||
| ## Usage | ||
| ## Quick Start | ||
@@ -17,330 +17,107 @@ Create a `vercel.ts` file in your project root: | ||
| ```typescript | ||
| import { createRouter } from '@vercel/config'; | ||
| import { createRouter, VercelConfig } from '@vercel/config/v1'; | ||
| const router = createRouter(); | ||
| // Basic rewrite | ||
| router.rewrite('/api/(.*)', 'https://backend.com/$1'); | ||
| export const config: VercelConfig = { | ||
| buildCommand: 'npm run build', | ||
| framework: 'nextjs', | ||
| routes: [ | ||
| // Headers | ||
| router.cacheControl('/static/(.*)', { | ||
| public: true, | ||
| maxAge: '1 week', | ||
| immutable: true | ||
| }), | ||
| // Rewrites with transforms | ||
| router.rewrite('/users/:userId', 'https://api.example.com/users/$1', | ||
| ({ userId, env }) => ({ | ||
| requestHeaders: { | ||
| 'x-user-id': userId, | ||
| 'authorization': `Bearer ${env.API_TOKEN}` | ||
| } | ||
| }) | ||
| ), | ||
| // Simple redirects | ||
| router.redirect('/old-docs', '/docs', { permanent: true }) | ||
| ], | ||
| // Rewrite with transforms | ||
| router.rewrite('/users/:userId', 'https://api.example.com/users/$1', ({userId, env}) => ({ | ||
| requestHeaders: { | ||
| 'x-user-id': userId, | ||
| 'authorization': `Bearer ${env.API_TOKEN}` | ||
| } | ||
| })); | ||
| // Redirects | ||
| router.redirect('/old-docs', '/docs', { permanent: true }); | ||
| // Cache control | ||
| router.cacheControl('/static/(.*)', { | ||
| public: true, | ||
| maxAge: '1 week', | ||
| immutable: true | ||
| }); | ||
| // Global settings | ||
| router.cleanUrls = true; | ||
| router.trailingSlash = true; | ||
| // Bulk redirects | ||
| router.bulkRedirectsPath = './bulkRedirectsDemo.json'; | ||
| // Cron jobs | ||
| router.cron('/api/cleanup', '0 0 * * *'); | ||
| export default router.getConfig(); | ||
| ``` | ||
| ## Automatic Compilation | ||
| Your `vercel.ts` file is automatically compiled when you run: | ||
| ```bash | ||
| vercel build # For local builds | ||
| vercel dev # For local development | ||
| vercel deploy # For deployment | ||
| ``` | ||
| The Vercel CLI automatically compiles `vercel.ts` to `.vercel/vercel.json` before building or deploying. | ||
| ## API Reference | ||
| ### `createRouter()` | ||
| Creates a new router instance. | ||
| ### `router.rewrite(source, destination, options?)` | ||
| Add a rewrite rule. Options can be an object or a callback function that receives path parameters and environment variables. | ||
| ```typescript | ||
| // With callback for transforms | ||
| router.rewrite('/users/:userId', 'https://api.example.com/users/$1', ({userId, env}) => ({ | ||
| requestHeaders: { | ||
| 'x-user-id': userId, | ||
| 'authorization': `Bearer ${env.API_TOKEN}` | ||
| }, | ||
| responseHeaders: { | ||
| 'x-powered-by': 'My API' | ||
| }, | ||
| requestQuery: { | ||
| 'version': '2.0' | ||
| } | ||
| })); | ||
| // Simple rewrite without transforms | ||
| router.rewrite('/api/(.*)', 'https://backend.com/$1'); | ||
| ``` | ||
| ### `router.redirect(source, destination, options?)` | ||
| Add a redirect rule. Options include `permanent` and `statusCode`. | ||
| ```typescript | ||
| router.redirect('/old-page', '/new-page', { permanent: true }); | ||
| router.redirect('/temp', '/elsewhere', { statusCode: 302 }); | ||
| ``` | ||
| ### `router.header(source, headers, options?)` | ||
| Add custom headers for a path pattern. | ||
| ```typescript | ||
| router.header('/api/(.*)', [ | ||
| { key: 'X-Custom-Header', value: 'value' } | ||
| ]); | ||
| ``` | ||
| ### `router.cacheControl(source, options)` | ||
| Set cache control headers. Options include `public`, `private`, `maxAge`, `sMaxAge`, `immutable`, etc. | ||
| ```typescript | ||
| router.cacheControl('/static/(.*)', { | ||
| public: true, | ||
| maxAge: '1 week', | ||
| immutable: true | ||
| }); | ||
| ``` | ||
| ### `router.cleanUrls` | ||
| Set whether to enable clean URLs (removes file extensions). | ||
| ```typescript | ||
| router.cleanUrls = true; | ||
| ``` | ||
| ### `router.trailingSlash` | ||
| Set whether to normalize paths to include trailing slashes. | ||
| ```typescript | ||
| router.trailingSlash = true; | ||
| ``` | ||
| ### `router.bulkRedirectsPath` | ||
| Set the path to a bulk redirects JSON file. | ||
| ```typescript | ||
| router.bulkRedirectsPath = './bulkRedirectsDemo.json'; | ||
| ``` | ||
| ## Conditional Routing | ||
| The SDK supports powerful conditional routing using `has` and `missing` conditions. These conditions can be added to rewrites, redirects, headers, and cache control rules. | ||
| ### Condition Types | ||
| - `header`: Match HTTP headers | ||
| - `cookie`: Match cookies | ||
| - `host`: Match the request host | ||
| - `query`: Match query parameters | ||
| - `path`: Match the request path pattern | ||
| ### Simple Presence Check | ||
| ```typescript | ||
| // Only rewrite if x-api-key header exists | ||
| router.rewrite('/api/(.*)', 'https://backend.com/$1', { | ||
| has: [ | ||
| { type: 'header', key: 'x-api-key' } | ||
| crons: [ | ||
| { path: '/api/cleanup', schedule: '0 0 * * *' } | ||
| ] | ||
| }); | ||
| // Redirect if auth-token cookie is missing | ||
| router.redirect('/dashboard', '/login', { | ||
| missing: [ | ||
| { type: 'cookie', key: 'auth-token' } | ||
| ] | ||
| }); | ||
| }; | ||
| ``` | ||
| ### Conditional Operators | ||
| ## Features | ||
| The SDK supports advanced matching operators for more complex conditions: | ||
| - **Type-safe configuration** - Full TypeScript support with IDE autocomplete | ||
| - **Readable syntax** - Helper methods like `router.redirect()`, `router.rewrite()`, `router.header()` | ||
| - **Static validation** - Catch configuration errors at development time | ||
| - **Transforms** - Modify request/response headers and query parameters on the fly | ||
| - **Conditions** - Advanced routing with `has` and `missing` conditions | ||
| - **CLI tools** - `compile` and `validate` commands for development | ||
| #### Equality Operators | ||
| ## Build-Time Compilation | ||
| ```typescript | ||
| // Exact match using 'eq' | ||
| router.rewrite('/api/(.*)', 'https://backend.com/$1', { | ||
| has: [ | ||
| { type: 'header', key: 'x-api-version', eq: 'v2' } | ||
| ] | ||
| }); | ||
| // Not equal using 'neq' | ||
| router.redirect('/beta/(.*)', '/stable/$1', { | ||
| has: [ | ||
| { type: 'cookie', key: 'beta-access', neq: 'granted' } | ||
| ] | ||
| }); | ||
| Your `vercel.ts` is automatically compiled to `vercel.json` during: | ||
| ```bash | ||
| vercel build | ||
| vercel dev | ||
| vercel deploy | ||
| ``` | ||
| #### Inclusion Operators | ||
| No manual build step needed - the Vercel CLI handles compilation automatically. | ||
| ```typescript | ||
| // Must be one of (inclusion) | ||
| router.rewrite('/admin/(.*)', 'https://admin.backend.com/$1', { | ||
| has: [ | ||
| { type: 'header', key: 'x-user-role', inc: ['admin', 'moderator', 'superuser'] } | ||
| ] | ||
| }); | ||
| ## CLI Commands | ||
| // Must NOT be one of (non-inclusion) | ||
| router.redirect('/public/(.*)', '/private/$1', { | ||
| has: [ | ||
| { type: 'header', key: 'x-user-role', ninc: ['guest', 'anonymous'] } | ||
| ] | ||
| }); | ||
| ``` | ||
| For development and validation: | ||
| #### String Pattern Operators | ||
| ```bash | ||
| # Compile vercel.ts to JSON (output to stdout) | ||
| npx @vercel/config compile | ||
| ```typescript | ||
| // Starts with (prefix) | ||
| router.rewrite('/staging/(.*)', 'https://staging.backend.com/$1', { | ||
| has: [ | ||
| { type: 'cookie', key: 'session', pre: 'staging-' } | ||
| ] | ||
| }); | ||
| # Validate config for errors and show summary | ||
| npx @vercel/config validate | ||
| // Ends with (suffix) | ||
| router.redirect('/dev/(.*)', '/development/$1', { | ||
| has: [ | ||
| { type: 'header', key: 'x-environment', suf: '-dev' } | ||
| ] | ||
| }); | ||
| # Generate vercel.json locally (for development) | ||
| npx @vercel/config generate | ||
| ``` | ||
| #### Numeric Comparison Operators | ||
| ## Validation | ||
| ```typescript | ||
| // Greater than | ||
| router.rewrite('/api/v3/(.*)', 'https://api-v3.backend.com/$1', { | ||
| has: [ | ||
| { type: 'query', key: 'version', gt: 2 } | ||
| ] | ||
| }); | ||
| Static fields are validated to ensure they only contain literal values (strings, booleans, objects with primitives): | ||
| // Greater than or equal | ||
| router.rewrite('/premium/(.*)', '/premium-content/$1', { | ||
| has: [ | ||
| { type: 'header', key: 'x-subscription-tier', gte: 3 } | ||
| ] | ||
| }); | ||
| // Less than | ||
| router.redirect('/legacy/(.*)', '/upgrade/$1', { | ||
| has: [ | ||
| { type: 'query', key: 'api-version', lt: 2 } | ||
| ] | ||
| }); | ||
| // Less than or equal | ||
| router.rewrite('/free/(.*)', '/free-tier/$1', { | ||
| has: [ | ||
| { type: 'header', key: 'x-plan', lte: 1 } | ||
| ] | ||
| }); | ||
| ``` | ||
| ### Host and Path Matching | ||
| ```typescript | ||
| // Host matching (no key required) | ||
| router.redirect('/(.*)', 'https://www.example.com/$1', { | ||
| has: [ | ||
| { type: 'host', value: 'example.com' } | ||
| ] | ||
| }); | ||
| // ✅ Valid - static string | ||
| export const buildCommand = 'npm run build'; | ||
| // Path pattern matching (no key required) | ||
| router.rewrite('/(.*)', '/internal/$1', { | ||
| has: [ | ||
| { type: 'path', value: '^/api/v[0-9]+/.*' } | ||
| ] | ||
| }); | ||
| ``` | ||
| // ❌ Invalid - computed value | ||
| export const buildCommand = process.env.BUILD_CMD; | ||
| ### Multiple Conditions | ||
| All conditions in a `has` or `missing` array must match (AND logic): | ||
| ```typescript | ||
| router.rewrite('/secure/(.*)', 'https://secure.backend.com/$1', { | ||
| has: [ | ||
| { type: 'header', key: 'x-api-key' }, | ||
| { type: 'header', key: 'x-user-role', inc: ['admin', 'superuser'] }, | ||
| { type: 'cookie', key: 'session', pre: 'secure-' }, | ||
| { type: 'query', key: 'version', gte: 2 } | ||
| ] | ||
| }); | ||
| ``` | ||
| ### Combining with Transforms | ||
| You can combine conditions with transforms for powerful routing logic: | ||
| ```typescript | ||
| router.rewrite('/api/users/:userId', 'https://backend.com/users/$1', ({ userId, env }) => ({ | ||
| has: [ | ||
| { type: 'header', key: 'authorization', pre: 'Bearer ' }, | ||
| { type: 'header', key: 'x-api-version', gte: 2 } | ||
| ], | ||
| missing: [ | ||
| { type: 'header', key: 'x-deprecated-header' } | ||
| ], | ||
| requestHeaders: { | ||
| 'x-user-id': userId, | ||
| 'x-internal-key': env.INTERNAL_API_KEY | ||
| // ✅ Valid - static object | ||
| export const git = { | ||
| deploymentEnabled: { | ||
| 'main': true, | ||
| 'dev': false | ||
| } | ||
| })); | ||
| }; | ||
| ``` | ||
| ### Available Operators | ||
| Static fields that are validated: | ||
| - `buildCommand`, `devCommand`, `installCommand` | ||
| - `framework`, `nodeVersion`, `outputDirectory` | ||
| - `github.enabled`, `github.autoAlias`, `github.autoJobCancelation` | ||
| - `git.deploymentEnabled` | ||
| - `relatedProjects` | ||
| | Operator | Type | Description | Example | | ||
| |----------|------|-------------|---------| | ||
| | `eq` | string \| number | Exact equality match | `{ eq: 'v2' }` | | ||
| | `neq` | string | Not equal | `{ neq: 'guest' }` | | ||
| | `inc` | string[] | Value is one of | `{ inc: ['admin', 'mod'] }` | | ||
| | `ninc` | string[] | Value is not one of | `{ ninc: ['guest', 'banned'] }` | | ||
| | `pre` | string | Starts with prefix | `{ pre: 'Bearer ' }` | | ||
| | `suf` | string | Ends with suffix | `{ suf: '-dev' }` | | ||
| | `gt` | number | Greater than | `{ gt: 2 }` | | ||
| | `gte` | number | Greater than or equal | `{ gte: 3 }` | | ||
| | `lt` | number | Less than | `{ lt: 5 }` | | ||
| | `lte` | number | Less than or equal | `{ lte: 10 }` | | ||
| ## Important Notes | ||
| - **One config file only**: You cannot have both `vercel.ts` and `vercel.json`. The build will fail if both exist. | ||
| - **Automatic gitignore**: The generated `.vercel/vercel.json` file is automatically ignored by git (in the `.vercel/` directory). | ||
| - **No manual compilation needed**: The Vercel CLI handles compilation automatically - no need to run a separate command. | ||
| - **Versioned API**: Use `@vercel/config/v1` for imports to enable future versioning. | ||
| ## Learn More | ||
@@ -347,0 +124,0 @@ |
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
14
16.67%2039
0.15%77845
-5.31%125
-64.08%14
7.69%