diff options
author | 2025-01-30 11:06:59 +0000 | |
---|---|---|
committer | 2025-01-30 11:06:59 +0000 | |
commit | f1133a197337d5fbbf114fdc82e2fb02b8e36db4 (patch) | |
tree | 7abb60a00731b0cb4bea981bb65258031931e319 /packages/integrations/vercel/src | |
parent | 9e209c5c90e3ff66d6f2e56142493e4fc8fb55a1 (diff) | |
download | astro-f1133a197337d5fbbf114fdc82e2fb02b8e36db4.tar.gz astro-f1133a197337d5fbbf114fdc82e2fb02b8e36db4.tar.zst astro-f1133a197337d5fbbf114fdc82e2fb02b8e36db4.zip |
fix: use vercel routing utils (#525)
* fix: use vercel routing utils
* changeset
* log error
* Format
* Update changeset
* Update tests
* Changes from review
* Format
Diffstat (limited to 'packages/integrations/vercel/src')
-rw-r--r-- | packages/integrations/vercel/src/index.ts | 65 | ||||
-rw-r--r-- | packages/integrations/vercel/src/lib/redirects.ts | 93 |
2 files changed, 91 insertions, 67 deletions
diff --git a/packages/integrations/vercel/src/index.ts b/packages/integrations/vercel/src/index.ts index 6b46512ec..3ee954202 100644 --- a/packages/integrations/vercel/src/index.ts +++ b/packages/integrations/vercel/src/index.ts @@ -2,6 +2,7 @@ import { cpSync, existsSync, mkdirSync, readFileSync } from 'node:fs'; import { basename } from 'node:path'; import { pathToFileURL } from 'node:url'; import { emptyDir, removeDir, writeJson } from '@astrojs/internal-helpers/fs'; +import { type Route, getTransformedRoutes, normalizeRoutes } from '@vercel/routing-utils'; import type { AstroAdapter, AstroConfig, @@ -10,6 +11,7 @@ import type { HookParameters, IntegrationResolvedRoute, } from 'astro'; +import { AstroError } from 'astro/errors'; import glob from 'fast-glob'; import { type DevImageService, @@ -261,16 +263,23 @@ export default function vercelAdapter({ ); } const vercelConfigPath = new URL('vercel.json', config.root); - if (existsSync(vercelConfigPath)) { + if ( + config.trailingSlash && + config.trailingSlash !== 'ignore' && + existsSync(vercelConfigPath) + ) { try { const vercelConfig = JSON.parse(readFileSync(vercelConfigPath, 'utf-8')); - if (vercelConfig.trailingSlash === true && config.trailingSlash === 'always') { - logger.warn( - '\n' + - `\tYour "vercel.json" \`trailingSlash\` configuration (set to \`true\`) will conflict with your Astro \`trailinglSlash\` configuration (set to \`"always"\`).\n` + - // biome-ignore lint/style/noUnusedTemplateLiteral: <explanation> - `\tThis would cause infinite redirects under certain conditions and throw an \`ERR_TOO_MANY_REDIRECTS\` error.\n` + - `\tTo prevent this, change your Astro configuration and update \`"trailingSlash"\` to \`"ignore"\`.\n` + if ( + (vercelConfig.trailingSlash === true && config.trailingSlash === 'never') || + (vercelConfig.trailingSlash === false && config.trailingSlash === 'always') + ) { + logger.error( + ` + Your "vercel.json" \`trailingSlash\` configuration (set to \`${vercelConfig.trailingSlash}\`) will conflict with your Astro \`trailingSlash\` configuration (set to \`${JSON.stringify(config.trailingSlash)}\`). + This would cause infinite redirects or duplicate content issues. + Please remove the \`trailingSlash\` configuration from your \`vercel.json\` file or Astro config. +` ); } } catch (_err) { @@ -435,14 +444,12 @@ export default function vercelAdapter({ } const fourOhFourRoute = routes.find((route) => route.pathname === '/404'); const destination = new URL('./.vercel/output/config.json', _config.root); - const finalRoutes = [ - ...getRedirects(routes, _config), + const finalRoutes: Route[] = [ { src: `^/${_config.build.assets}/(.*)$`, headers: { 'cache-control': 'public, max-age=31536000, immutable' }, continue: true, }, - { handle: 'filesystem' }, ]; if (_buildOutput === 'server') { finalRoutes.push(...routeDefinitions); @@ -467,6 +474,30 @@ export default function vercelAdapter({ }); } } + // The Vercel `trailingSlash` option + let trailingSlash: boolean | undefined; + // Vercel's `trailingSlash` option maps to Astro's like so: + // - `true` -> `"always"` + // - `false` -> `"never"` + // - `undefined` -> `"ignore"` + // If config is set to "ignore", we leave it as undefined. + if (_config.trailingSlash && _config.trailingSlash !== 'ignore') { + // Otherwise, map it accordingly. + trailingSlash = _config.trailingSlash === 'always'; + } + + const { routes: redirects = [], error } = getTransformedRoutes({ + trailingSlash, + rewrites: [], + redirects: getRedirects(routes, _config), + headers: [], + }); + if (error) { + throw new AstroError( + `Error generating redirects: ${error.message}`, + error.link ? `${error.action ?? 'More info'}: ${error.link}` : undefined + ); + } let images: VercelImageConfig | undefined; if (imageService || imagesConfig) { @@ -487,11 +518,21 @@ export default function vercelAdapter({ } } + const normalized = normalizeRoutes([...(redirects ?? []), ...finalRoutes]); + if (normalized.error) { + throw new AstroError( + `Error generating routes: ${normalized.error.message}`, + normalized.error.link + ? `${normalized.error.action ?? 'More info'}: ${normalized.error.link}` + : undefined + ); + } + // Output configuration // https://vercel.com/docs/build-output-api/v3#build-output-configuration await writeJson(destination, { version: 3, - routes: finalRoutes, + routes: normalized.routes, images, }); diff --git a/packages/integrations/vercel/src/lib/redirects.ts b/packages/integrations/vercel/src/lib/redirects.ts index a9a3a81b2..2b51c9007 100644 --- a/packages/integrations/vercel/src/lib/redirects.ts +++ b/packages/integrations/vercel/src/lib/redirects.ts @@ -1,7 +1,9 @@ import nodePath from 'node:path'; -import { appendForwardSlash, removeLeadingForwardSlash } from '@astrojs/internal-helpers/path'; +import { removeLeadingForwardSlash } from '@astrojs/internal-helpers/path'; import type { AstroConfig, IntegrationResolvedRoute, RoutePart } from 'astro'; +import type { Redirect } from '@vercel/routing-utils'; + const pathJoin = nodePath.posix.join; // https://vercel.com/docs/project-configuration#legacy/routes @@ -40,10 +42,32 @@ function getParts(part: string, file: string) { return result; } +/** + * Convert Astro routes into Vercel path-to-regexp syntax, which are the input for getTransformedRoutes + */ +function getMatchPattern(segments: RoutePart[][]) { + return segments + .map((segment) => { + return segment + .map((part) => { + if (part.spread) { + // Extract parameter name from spread syntax (e.g., "...slug" -> "slug") + const paramName = part.content.startsWith('...') ? part.content.slice(3) : part.content; + return `:${paramName}*`; + } + if (part.dynamic) { + return `:${part.content}`; + } + return part.content; + }) + .join(''); + }) + .join('/'); +} // Copied from /home/juanm04/dev/misc/astro/packages/astro/src/core/routing/manifest/create.ts // 2022-04-26 -function getMatchPattern(segments: RoutePart[][]) { +function getMatchRegex(segments: RoutePart[][]) { return segments .map((segment, segmentIndex) => { return segment.length === 1 && segment[0].spread @@ -72,37 +96,16 @@ function getMatchPattern(segments: RoutePart[][]) { .join(''); } -function getReplacePattern(segments: RoutePart[][]) { - let n = 0; - let result = ''; - - for (const segment of segments) { - for (const part of segment) { - // biome-ignore lint/style/useTemplate: <explanation> - if (part.dynamic) result += '$' + ++n; - else result += part.content; - } - result += '/'; - } - - // Remove trailing slash - result = result.slice(0, -1); - - return result; -} - function getRedirectLocation(route: IntegrationResolvedRoute, config: AstroConfig): string { if (route.redirectRoute) { - const pattern = getReplacePattern(route.redirectRoute.segments); - const path = config.trailingSlash === 'always' ? appendForwardSlash(pattern) : pattern; - return pathJoin(config.base, path); - // biome-ignore lint/style/noUselessElse: <explanation> - } else if (typeof route.redirect === 'object') { + const pattern = getMatchPattern(route.redirectRoute.segments); + return pathJoin(config.base, pattern); + } + + if (typeof route.redirect === 'object') { return pathJoin(config.base, route.redirect.destination); - // biome-ignore lint/style/noUselessElse: <explanation> - } else { - return pathJoin(config.base, route.redirect || ''); } + return pathJoin(config.base, route.redirect || ''); } function getRedirectStatus(route: IntegrationResolvedRoute): number { @@ -119,40 +122,20 @@ export function escapeRegex(content: string) { .map((s: string) => { return getParts(s, content); }); - return `^/${getMatchPattern(segments)}$`; + return `^/${getMatchRegex(segments)}$`; } -export function getRedirects( - routes: IntegrationResolvedRoute[], - config: AstroConfig -): VercelRoute[] { - const redirects: VercelRoute[] = []; +export function getRedirects(routes: IntegrationResolvedRoute[], config: AstroConfig): Redirect[] { + const redirects: Redirect[] = []; for (const route of routes) { if (route.type === 'redirect') { redirects.push({ - src: config.base + getMatchPattern(route.segments), - headers: { Location: getRedirectLocation(route, config) }, - status: getRedirectStatus(route), + source: config.base + getMatchPattern(route.segments), + destination: getRedirectLocation(route, config), + statusCode: getRedirectStatus(route), }); - } else if (route.type === 'page' && route.pattern !== '/') { - if (config.trailingSlash === 'always') { - redirects.push({ - src: config.base + getMatchPattern(route.segments), - // biome-ignore lint/style/useTemplate: <explanation> - headers: { Location: config.base + getReplacePattern(route.segments) + '/' }, - status: 308, - }); - } else if (config.trailingSlash === 'never') { - redirects.push({ - // biome-ignore lint/style/useTemplate: <explanation> - src: config.base + getMatchPattern(route.segments) + '/', - headers: { Location: config.base + getReplacePattern(route.segments) }, - status: 308, - }); - } } } - return redirects; } |