summaryrefslogtreecommitdiff
path: root/packages/integrations/vercel/src
diff options
context:
space:
mode:
authorGravatar Matt Kane <m@mk.gg> 2025-01-30 11:06:59 +0000
committerGravatar GitHub <noreply@github.com> 2025-01-30 11:06:59 +0000
commitf1133a197337d5fbbf114fdc82e2fb02b8e36db4 (patch)
tree7abb60a00731b0cb4bea981bb65258031931e319 /packages/integrations/vercel/src
parent9e209c5c90e3ff66d6f2e56142493e4fc8fb55a1 (diff)
downloadastro-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.ts65
-rw-r--r--packages/integrations/vercel/src/lib/redirects.ts93
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;
}