@vercel/routing-utils
Advanced tools
+12
-4
@@ -74,4 +74,6 @@ "use strict"; | ||
| } | ||
| route.dest = route.destination; | ||
| delete route.destination; | ||
| if (typeof route.destination === "string") { | ||
| route.dest = route.destination; | ||
| delete route.destination; | ||
| } | ||
| } | ||
@@ -136,2 +138,7 @@ if (route.statusCode !== void 0) { | ||
| } | ||
| if (route.destination && typeof route.destination === "object" && route.continue) { | ||
| errors.push( | ||
| `Route at index ${i} cannot define \`continue: true\` with a service \`destination\`. The service handoff is terminal.` | ||
| ); | ||
| } | ||
| const handleValue = handling[handling.length - 1]; | ||
@@ -203,5 +210,6 @@ if (handleValue === "hit") { | ||
| } | ||
| if (destination) { | ||
| const destinationString = typeof destination === "string" ? destination : typeof destination?.path === "string" ? destination.path : void 0; | ||
| if (destinationString !== void 0) { | ||
| try { | ||
| const { hostname, pathname, query } = (0, import_url.parse)(destination, true); | ||
| const { hostname, pathname, query } = (0, import_url.parse)(destinationString, true); | ||
| (0, import_superstatic.sourceToRegex)(hostname || "").segments.forEach( | ||
@@ -208,0 +216,0 @@ (name) => destinationSegments.add(name) |
+32
-5
@@ -43,2 +43,28 @@ "use strict"; | ||
| }; | ||
| const serviceNameSchema = { | ||
| description: "A service name identifier.", | ||
| type: "string", | ||
| minLength: 1, | ||
| maxLength: 64, | ||
| pattern: "^[a-zA-Z]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$" | ||
| }; | ||
| const serviceDestinationSchema = { | ||
| description: "A service-targeted destination that delegates routing into a named service from `services`.", | ||
| type: "object", | ||
| additionalProperties: false, | ||
| required: ["type", "service"], | ||
| properties: { | ||
| type: { | ||
| description: "Discriminator. Must be `service`.", | ||
| type: "string", | ||
| enum: ["service"] | ||
| }, | ||
| service: serviceNameSchema, | ||
| path: { | ||
| description: "Routing-only path used to select a route inside the target service. It does not mutate the URL observed by user code.", | ||
| type: "string", | ||
| maxLength: 4096 | ||
| } | ||
| } | ||
| }; | ||
| const matchableValueSchema = { | ||
@@ -392,4 +418,6 @@ description: "A value to match against. Can be a string (regex) or a condition operation object", | ||
| destination: { | ||
| type: "string", | ||
| maxLength: 4096 | ||
| anyOf: [ | ||
| { type: "string", maxLength: 4096 }, | ||
| serviceDestinationSchema | ||
| ] | ||
| }, | ||
@@ -543,5 +571,4 @@ headers: { | ||
| destination: { | ||
| description: "An absolute pathname to an existing resource or an external URL.", | ||
| type: "string", | ||
| maxLength: 4096 | ||
| description: "An absolute pathname to an existing resource, an external URL, or a service-targeted destination object.", | ||
| anyOf: [{ type: "string", maxLength: 4096 }, serviceDestinationSchema] | ||
| }, | ||
@@ -548,0 +575,0 @@ has: hasSchema, |
+12
-3
@@ -148,10 +148,19 @@ "use strict"; | ||
| try { | ||
| const dest = replaceSegments( | ||
| const interpolate = (value) => replaceSegments( | ||
| segments, | ||
| hasSegments, | ||
| r.destination, | ||
| value, | ||
| false, | ||
| internalParamNames | ||
| ); | ||
| const route = { src, dest, check: true }; | ||
| let route; | ||
| if (typeof r.destination === "string") { | ||
| route = { src, dest: interpolate(r.destination), check: true }; | ||
| } else { | ||
| const destination = { ...r.destination }; | ||
| if (typeof destination.path === "string") { | ||
| destination.path = interpolate(destination.path); | ||
| } | ||
| route = { src, destination }; | ||
| } | ||
| if (typeof r.env !== "undefined") { | ||
@@ -158,0 +167,0 @@ route.env = r.env; |
+15
-4
@@ -52,2 +52,8 @@ import { HandleValue } from './index'; | ||
| }; | ||
| export type ServiceDestination = { | ||
| type: 'service'; | ||
| service: string; | ||
| /** Routing-only path used to select a route inside the target service. */ | ||
| path?: string; | ||
| }; | ||
| export type RouteWithSrc = { | ||
@@ -82,7 +88,12 @@ src: string; | ||
| * `rewrites`, `redirects`, and `headers` fields which use `source`, `destination`, | ||
| * and `statusCode`. During normalization, these are converted to their canonical | ||
| * forms (`src`, `dest`, `status`) and stripped from the route object. | ||
| * and `statusCode`. During normalization, the string forms are converted to | ||
| * their canonical forms (`src`, `dest`, `status`) and stripped from the route | ||
| * object. | ||
| * | ||
| * `destination` may also be a service-targeted object, in which case routing | ||
| * is delegated into the named service's internal route table and the object | ||
| * is preserved as-is (not folded into `dest`). | ||
| */ | ||
| source?: string; | ||
| destination?: string; | ||
| destination?: string | ServiceDestination; | ||
| statusCode?: number; | ||
@@ -139,3 +150,3 @@ /** | ||
| source: string; | ||
| destination: string; | ||
| destination: string | ServiceDestination; | ||
| has?: HasField; | ||
@@ -142,0 +153,0 @@ missing?: HasField; |
+6
-5
| { | ||
| "name": "@vercel/routing-utils", | ||
| "version": "6.2.0", | ||
| "version": "6.3.0", | ||
| "description": "Vercel routing utilities", | ||
@@ -21,6 +21,5 @@ "main": "./dist/index.js", | ||
| "devDependencies": { | ||
| "@types/jest": "27.4.1", | ||
| "@types/node": "20.11.0", | ||
| "ajv": "^6.12.3", | ||
| "jest-junit": "16.0.0" | ||
| "vitest": "2.0.3" | ||
| }, | ||
@@ -32,6 +31,8 @@ "optionalDependencies": { | ||
| "build": "node ../../utils/build.mjs", | ||
| "test": "jest --reporters=default --reporters=jest-junit --env node --verbose --runInBand --bail", | ||
| "test": "vitest run --config ../../vitest.config.mts", | ||
| "test-unit": "pnpm test", | ||
| "type-check": "tsc --noEmit" | ||
| "type-check": "tsc --noEmit", | ||
| "vitest-run": "vitest -c ../../vitest.config.mts", | ||
| "vitest-unit": "glob --absolute 'test/**/*.test.js' 'test/**/*.test.ts' 'test/**/*.test.mjs' 'test/**/*.test.mts' 'tests/**/*.test.js' 'tests/**/*.test.ts'" | ||
| } | ||
| } |
-416
| "use strict"; | ||
| var __defProp = Object.defineProperty; | ||
| var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
| var __getOwnPropNames = Object.getOwnPropertyNames; | ||
| var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
| var __export = (target, all) => { | ||
| for (var name in all) | ||
| __defProp(target, name, { get: all[name], enumerable: true }); | ||
| }; | ||
| var __copyProps = (to, from, except, desc) => { | ||
| if (from && typeof from === "object" || typeof from === "function") { | ||
| for (let key of __getOwnPropNames(from)) | ||
| if (!__hasOwnProp.call(to, key) && key !== except) | ||
| __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | ||
| } | ||
| return to; | ||
| }; | ||
| var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default")); | ||
| var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||
| var src_exports = {}; | ||
| __export(src_exports, { | ||
| appendRoutesToPhase: () => import_append.appendRoutesToPhase, | ||
| getCleanUrls: () => import_superstatic2.getCleanUrls, | ||
| getOwnershipGuard: () => import_service_route_ownership.getOwnershipGuard, | ||
| getTransformedRoutes: () => getTransformedRoutes, | ||
| isHandler: () => isHandler, | ||
| isValidHandleValue: () => isValidHandleValue, | ||
| mergeRoutes: () => import_merge.mergeRoutes, | ||
| normalizeRoutePrefix: () => import_service_route_ownership.normalizeRoutePrefix, | ||
| normalizeRoutes: () => normalizeRoutes, | ||
| scopeRouteSourceToOwnership: () => import_service_route_ownership.scopeRouteSourceToOwnership, | ||
| sourceToRegex: () => import_superstatic2.sourceToRegex | ||
| }); | ||
| module.exports = __toCommonJS(src_exports); | ||
| var import_url = require("url"); | ||
| var import_superstatic = require("./superstatic"); | ||
| var import_append = require("./append"); | ||
| var import_merge = require("./merge"); | ||
| var import_service_route_ownership = require("./service-route-ownership"); | ||
| __reExport(src_exports, require("./schemas"), module.exports); | ||
| var import_superstatic2 = require("./superstatic"); | ||
| __reExport(src_exports, require("./types"), module.exports); | ||
| const VALID_HANDLE_VALUES = [ | ||
| "filesystem", | ||
| "hit", | ||
| "miss", | ||
| "rewrite", | ||
| "error", | ||
| "resource" | ||
| ]; | ||
| const validHandleValues = new Set(VALID_HANDLE_VALUES); | ||
| function isHandler(route) { | ||
| return typeof route.handle !== "undefined"; | ||
| } | ||
| function isValidHandleValue(handle) { | ||
| return validHandleValues.has(handle); | ||
| } | ||
| function convertRouteAliases(route, index) { | ||
| if (route.source !== void 0) { | ||
| if (route.src !== void 0) { | ||
| throw new Error( | ||
| `Route at index ${index} cannot define both \`src\` and \`source\`. Please use only one.` | ||
| ); | ||
| } | ||
| route.src = route.source; | ||
| delete route.source; | ||
| } | ||
| if (route.destination !== void 0) { | ||
| if (route.dest !== void 0) { | ||
| throw new Error( | ||
| `Route at index ${index} cannot define both \`dest\` and \`destination\`. Please use only one.` | ||
| ); | ||
| } | ||
| route.dest = route.destination; | ||
| delete route.destination; | ||
| } | ||
| if (route.statusCode !== void 0) { | ||
| if (route.status !== void 0) { | ||
| throw new Error( | ||
| `Route at index ${index} cannot define both \`status\` and \`statusCode\`. Please use only one.` | ||
| ); | ||
| } | ||
| route.status = route.statusCode; | ||
| delete route.statusCode; | ||
| } | ||
| } | ||
| function normalizeRoutes(inputRoutes) { | ||
| if (!inputRoutes || inputRoutes.length === 0) { | ||
| return { routes: inputRoutes, error: null }; | ||
| } | ||
| const routes = []; | ||
| const handling = []; | ||
| const errors = []; | ||
| inputRoutes.forEach((r, i) => { | ||
| const route = { ...r }; | ||
| routes.push(route); | ||
| if (!isHandler(route)) { | ||
| try { | ||
| convertRouteAliases(route, i); | ||
| } catch (err) { | ||
| errors.push(err.message); | ||
| } | ||
| } | ||
| const keys = Object.keys(route); | ||
| if (isHandler(route)) { | ||
| const { handle } = route; | ||
| if (keys.length !== 1) { | ||
| const unknownProp = keys.find((prop) => prop !== "handle"); | ||
| errors.push( | ||
| `Route at index ${i} has unknown property \`${unknownProp}\`.` | ||
| ); | ||
| } else if (!isValidHandleValue(handle)) { | ||
| errors.push( | ||
| `Route at index ${i} has unknown handle value \`handle: ${handle}\`.` | ||
| ); | ||
| } else if (handling.includes(handle)) { | ||
| errors.push( | ||
| `Route at index ${i} is a duplicate. Please use one \`handle: ${handle}\` at most.` | ||
| ); | ||
| } else { | ||
| handling.push(handle); | ||
| } | ||
| } else if (route.src) { | ||
| if (!route.src.startsWith("^")) { | ||
| route.src = `^${route.src}`; | ||
| } | ||
| if (!route.src.endsWith("$")) { | ||
| route.src = `${route.src}$`; | ||
| } | ||
| route.src = route.src.replace(/\\\//g, "/"); | ||
| const regError = checkRegexSyntax("Route", i, route.src); | ||
| if (regError) { | ||
| errors.push(regError); | ||
| } | ||
| const handleValue = handling[handling.length - 1]; | ||
| if (handleValue === "hit") { | ||
| if (route.dest) { | ||
| errors.push( | ||
| `Route at index ${i} cannot define \`dest\`/\`destination\` after \`handle: hit\`.` | ||
| ); | ||
| } | ||
| if (route.status) { | ||
| errors.push( | ||
| `Route at index ${i} cannot define \`status\`/\`statusCode\` after \`handle: hit\`.` | ||
| ); | ||
| } | ||
| if (!route.continue) { | ||
| errors.push( | ||
| `Route at index ${i} must define \`continue: true\` after \`handle: hit\`.` | ||
| ); | ||
| } | ||
| } else if (handleValue === "miss") { | ||
| if (route.dest && !route.check) { | ||
| errors.push( | ||
| `Route at index ${i} must define \`check: true\` after \`handle: miss\`.` | ||
| ); | ||
| } else if (!route.dest && !route.continue) { | ||
| errors.push( | ||
| `Route at index ${i} must define \`continue: true\` after \`handle: miss\`.` | ||
| ); | ||
| } | ||
| } | ||
| } else { | ||
| errors.push( | ||
| `Route at index ${i} must define either \`src\` or \`source\` property.` | ||
| ); | ||
| } | ||
| }); | ||
| const error = errors.length > 0 ? createError( | ||
| "invalid_route", | ||
| errors, | ||
| "https://vercel.link/routes-json", | ||
| "Learn More" | ||
| ) : null; | ||
| return { routes, error }; | ||
| } | ||
| function checkRegexSyntax(type, index, src) { | ||
| try { | ||
| new RegExp(src); | ||
| } catch (_err) { | ||
| const prop = type === "Route" ? "src`/`source" : "source"; | ||
| return `${type} at index ${index} has invalid \`${prop}\` regular expression "${src}".`; | ||
| } | ||
| return null; | ||
| } | ||
| function checkPatternSyntax(type, index, { | ||
| source, | ||
| destination, | ||
| has | ||
| }) { | ||
| let sourceSegments = /* @__PURE__ */ new Set(); | ||
| const destinationSegments = /* @__PURE__ */ new Set(); | ||
| try { | ||
| sourceSegments = new Set((0, import_superstatic.sourceToRegex)(source).segments); | ||
| } catch (_err) { | ||
| return { | ||
| message: `${type} at index ${index} has invalid \`source\` pattern "${source}".`, | ||
| link: "https://vercel.link/invalid-route-source-pattern" | ||
| }; | ||
| } | ||
| if (destination) { | ||
| try { | ||
| const { hostname, pathname, query } = (0, import_url.parse)(destination, true); | ||
| (0, import_superstatic.sourceToRegex)(hostname || "").segments.forEach( | ||
| (name) => destinationSegments.add(name) | ||
| ); | ||
| (0, import_superstatic.sourceToRegex)(pathname || "").segments.forEach( | ||
| (name) => destinationSegments.add(name) | ||
| ); | ||
| for (const strOrArray of Object.values(query)) { | ||
| const value = Array.isArray(strOrArray) ? strOrArray[0] : strOrArray; | ||
| (0, import_superstatic.sourceToRegex)(value || "").segments.forEach( | ||
| (name) => destinationSegments.add(name) | ||
| ); | ||
| } | ||
| } catch (_err) { | ||
| } | ||
| const hasSegments = (0, import_superstatic.collectHasSegments)(has); | ||
| for (const segment of destinationSegments) { | ||
| if (!sourceSegments.has(segment) && !hasSegments.includes(segment)) { | ||
| return { | ||
| message: `${type} at index ${index} has segment ":${segment}" in \`destination\` property but not in \`source\` or \`has\` property.`, | ||
| link: "https://vercel.link/invalid-route-destination-segment" | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| function checkRedirect(r, index) { | ||
| if (typeof r.permanent !== "undefined" && typeof r.statusCode !== "undefined") { | ||
| return `Redirect at index ${index} cannot define both \`permanent\` and \`statusCode\` properties.`; | ||
| } | ||
| return null; | ||
| } | ||
| function createError(code, allErrors, link, action) { | ||
| const errors = Array.isArray(allErrors) ? allErrors : [allErrors]; | ||
| const message = errors[0]; | ||
| const error = { | ||
| name: "RouteApiError", | ||
| code, | ||
| message, | ||
| link, | ||
| action, | ||
| errors | ||
| }; | ||
| return error; | ||
| } | ||
| function notEmpty(value) { | ||
| return value !== null && value !== void 0; | ||
| } | ||
| function getTransformedRoutes(vercelConfig) { | ||
| const { cleanUrls, rewrites, redirects, headers, trailingSlash } = vercelConfig; | ||
| const { routes: userRoutes = null } = vercelConfig; | ||
| let routes = null; | ||
| if (typeof cleanUrls !== "undefined") { | ||
| const normalized = normalizeRoutes( | ||
| (0, import_superstatic.convertCleanUrls)(cleanUrls, trailingSlash) | ||
| ); | ||
| if (normalized.error) { | ||
| normalized.error.code = "invalid_clean_urls"; | ||
| return { routes, error: normalized.error }; | ||
| } | ||
| routes = routes || []; | ||
| routes.push(...normalized.routes || []); | ||
| } | ||
| if (typeof trailingSlash !== "undefined") { | ||
| const normalized = normalizeRoutes((0, import_superstatic.convertTrailingSlash)(trailingSlash)); | ||
| if (normalized.error) { | ||
| normalized.error.code = "invalid_trailing_slash"; | ||
| return { routes, error: normalized.error }; | ||
| } | ||
| routes = routes || []; | ||
| routes.push(...normalized.routes || []); | ||
| } | ||
| if (userRoutes) { | ||
| const normalized = normalizeRoutes(userRoutes); | ||
| if (normalized.error) { | ||
| return { routes, error: normalized.error }; | ||
| } | ||
| routes = routes || []; | ||
| routes.push(...normalized.routes || []); | ||
| } | ||
| if (typeof redirects !== "undefined") { | ||
| const code = "invalid_redirect"; | ||
| const regexErrorMessage = redirects.map((r, i) => checkRegexSyntax("Redirect", i, r.source)).find(notEmpty); | ||
| if (regexErrorMessage) { | ||
| return { | ||
| routes, | ||
| error: createError( | ||
| "invalid_redirect", | ||
| regexErrorMessage, | ||
| "https://vercel.link/invalid-route-source-pattern", | ||
| "Learn More" | ||
| ) | ||
| }; | ||
| } | ||
| const patternError = redirects.map((r, i) => checkPatternSyntax("Redirect", i, r)).find(notEmpty); | ||
| if (patternError) { | ||
| return { | ||
| routes, | ||
| error: createError( | ||
| code, | ||
| patternError.message, | ||
| patternError.link, | ||
| "Learn More" | ||
| ) | ||
| }; | ||
| } | ||
| const redirectErrorMessage = redirects.map(checkRedirect).find(notEmpty); | ||
| if (redirectErrorMessage) { | ||
| return { | ||
| routes, | ||
| error: createError( | ||
| code, | ||
| redirectErrorMessage, | ||
| "https://vercel.link/redirects-json", | ||
| "Learn More" | ||
| ) | ||
| }; | ||
| } | ||
| const normalized = normalizeRoutes((0, import_superstatic.convertRedirects)(redirects)); | ||
| if (normalized.error) { | ||
| normalized.error.code = code; | ||
| return { routes, error: normalized.error }; | ||
| } | ||
| routes = routes || []; | ||
| routes.push(...normalized.routes || []); | ||
| } | ||
| if (typeof headers !== "undefined") { | ||
| const code = "invalid_header"; | ||
| const regexErrorMessage = headers.map((r, i) => checkRegexSyntax("Header", i, r.source)).find(notEmpty); | ||
| if (regexErrorMessage) { | ||
| return { | ||
| routes, | ||
| error: createError( | ||
| code, | ||
| regexErrorMessage, | ||
| "https://vercel.link/invalid-route-source-pattern", | ||
| "Learn More" | ||
| ) | ||
| }; | ||
| } | ||
| const patternError = headers.map((r, i) => checkPatternSyntax("Header", i, r)).find(notEmpty); | ||
| if (patternError) { | ||
| return { | ||
| routes, | ||
| error: createError( | ||
| code, | ||
| patternError.message, | ||
| patternError.link, | ||
| "Learn More" | ||
| ) | ||
| }; | ||
| } | ||
| const normalized = normalizeRoutes((0, import_superstatic.convertHeaders)(headers)); | ||
| if (normalized.error) { | ||
| normalized.error.code = code; | ||
| return { routes, error: normalized.error }; | ||
| } | ||
| routes = routes || []; | ||
| routes.push(...normalized.routes || []); | ||
| } | ||
| if (typeof rewrites !== "undefined") { | ||
| const code = "invalid_rewrite"; | ||
| const regexErrorMessage = rewrites.map((r, i) => checkRegexSyntax("Rewrite", i, r.source)).find(notEmpty); | ||
| if (regexErrorMessage) { | ||
| return { | ||
| routes, | ||
| error: createError( | ||
| code, | ||
| regexErrorMessage, | ||
| "https://vercel.link/invalid-route-source-pattern", | ||
| "Learn More" | ||
| ) | ||
| }; | ||
| } | ||
| const patternError = rewrites.map((r, i) => checkPatternSyntax("Rewrite", i, r)).find(notEmpty); | ||
| if (patternError) { | ||
| return { | ||
| routes, | ||
| error: createError( | ||
| code, | ||
| patternError.message, | ||
| patternError.link, | ||
| "Learn More" | ||
| ) | ||
| }; | ||
| } | ||
| const normalized = normalizeRoutes((0, import_superstatic.convertRewrites)(rewrites)); | ||
| if (normalized.error) { | ||
| normalized.error.code = code; | ||
| return { routes, error: normalized.error }; | ||
| } | ||
| routes = routes || []; | ||
| routes.push({ handle: "filesystem" }); | ||
| routes.push(...normalized.routes || []); | ||
| } | ||
| return { routes, error: null }; | ||
| } | ||
| // Annotate the CommonJS export names for ESM import in node: | ||
| 0 && (module.exports = { | ||
| appendRoutesToPhase, | ||
| getCleanUrls, | ||
| getOwnershipGuard, | ||
| getTransformedRoutes, | ||
| isHandler, | ||
| isValidHandleValue, | ||
| mergeRoutes, | ||
| normalizeRoutePrefix, | ||
| normalizeRoutes, | ||
| scopeRouteSourceToOwnership, | ||
| sourceToRegex, | ||
| ...require("./schemas"), | ||
| ...require("./types") | ||
| }); |
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
3
-25%207815
-3.8%17
-5.56%4284
-6.73%