From 4adedbaafe3c68a85b5df20f8f1e4c8efb2800e7 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 23 May 2023 14:46:00 -0400 Subject: [PATCH] Add support for the object notation in redirects --- packages/astro/src/@types/astro.ts | 11 +++++-- packages/astro/src/core/build/generate.ts | 10 +++--- packages/astro/src/core/redirects/helpers.ts | 19 +++++++++-- packages/astro/src/core/redirects/index.ts | 2 +- packages/astro/src/core/render/core.ts | 6 ++-- packages/astro/test/redirects.test.js | 10 +++++- packages/integrations/netlify/src/shared.ts | 15 ++++++--- .../netlify/test/static/redirects.test.js | 5 +++ .../integrations/vercel/src/lib/redirects.ts | 32 ++++++++++++------- .../vercel/test/redirects.test.js | 10 +++++- 10 files changed, 91 insertions(+), 29 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index cf9ced260..807e16ded 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1582,6 +1582,8 @@ export interface AstroAdapter { type Body = string; +export type ValidRedirectStatus = 300 | 301 | 302 | 303 | 304 | 307 | 308; + // Shared types between `Astro` global and API context object interface AstroSharedContext = Record> { /** @@ -1611,7 +1613,7 @@ interface AstroSharedContext = Record= 300 && response.status < 400): { const location = getRedirectLocationOrThrow(response.headers); body = ` Redirecting to: ${location} `; - pageData.route.redirect = location; + // A dynamic redirect, set the location so that integrations know about it. + if(pageData.route.type !== 'redirect') { + pageData.route.redirect = location; + } break; } default: { diff --git a/packages/astro/src/core/redirects/helpers.ts b/packages/astro/src/core/redirects/helpers.ts index b65e216e6..df82a61e5 100644 --- a/packages/astro/src/core/redirects/helpers.ts +++ b/packages/astro/src/core/redirects/helpers.ts @@ -1,4 +1,4 @@ -import type { RouteData, RedirectRouteData, Params } from '../../@types/astro'; +import type { RouteData, RedirectRouteData, Params, ValidRedirectStatus } from '../../@types/astro'; export function routeIsRedirect(route: RouteData | undefined): route is RedirectRouteData { return route?.type === 'redirect'; @@ -8,5 +8,20 @@ export function redirectRouteGenerate(redirectRoute: RouteData, data: Params): s const routeData = redirectRoute.redirectRoute; const route = redirectRoute.redirect; - return routeData?.generate(data) || routeData?.pathname || route || '/'; + if(typeof routeData !== 'undefined') { + return routeData?.generate(data) || routeData?.pathname || '/'; + } else if(typeof route === 'string') { + return route; + } else if(typeof route === 'undefined') { + return '/'; + } + return route.destination; +} + +export function redirectRouteStatus(redirectRoute: RouteData): ValidRedirectStatus { + const routeData = redirectRoute.redirectRoute; + if(typeof routeData?.redirect === 'object') { + return routeData.redirect.status; + } + return 301; } diff --git a/packages/astro/src/core/redirects/index.ts b/packages/astro/src/core/redirects/index.ts index 3a8326de6..aedb828eb 100644 --- a/packages/astro/src/core/redirects/index.ts +++ b/packages/astro/src/core/redirects/index.ts @@ -1,2 +1,2 @@ export { getRedirectLocationOrThrow } from './validate.js'; -export { routeIsRedirect, redirectRouteGenerate } from './helpers.js'; +export { routeIsRedirect, redirectRouteGenerate, redirectRouteStatus } from './helpers.js'; diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index 3eb085fda..57cd7949e 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -8,7 +8,7 @@ import type { RenderContext } from './context.js'; import type { Environment } from './environment.js'; import { createResult } from './result.js'; import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js'; -import { routeIsRedirect, redirectRouteGenerate } from '../redirects/index.js'; +import { routeIsRedirect, redirectRouteGenerate, redirectRouteStatus } from '../redirects/index.js'; interface GetParamsAndPropsOptions { mod: ComponentInstance; @@ -114,9 +114,9 @@ export type RenderPage = { export async function renderPage({ mod, renderContext, env, apiContext }: RenderPage) { if(routeIsRedirect(renderContext.route)) { return new Response(null, { - status: 301, + status: redirectRouteStatus(renderContext.route), headers: { - location: redirectRouteGenerate(renderContext.route!, renderContext.params) + location: redirectRouteGenerate(renderContext.route, renderContext.params) } }); } diff --git a/packages/astro/test/redirects.test.js b/packages/astro/test/redirects.test.js index b8fd723ff..f2531b466 100644 --- a/packages/astro/test/redirects.test.js +++ b/packages/astro/test/redirects.test.js @@ -48,7 +48,11 @@ describe('Astro.redirect', () => { redirects: { '/one': '/', '/two': '/', - '/blog/[...slug]': '/articles/[...slug]' + '/blog/[...slug]': '/articles/[...slug]', + '/three': { + status: 302, + destination: '/' + } } }); await fixture.build(); @@ -68,6 +72,10 @@ describe('Astro.redirect', () => { html = await fixture.readFile('/two/index.html'); expect(html).to.include('http-equiv="refresh'); expect(html).to.include('url=/'); + + html = await fixture.readFile('/three/index.html'); + expect(html).to.include('http-equiv="refresh'); + expect(html).to.include('url=/'); }); it('Generates page for dynamic routes', async () => { diff --git a/packages/integrations/netlify/src/shared.ts b/packages/integrations/netlify/src/shared.ts index 479c03907..4d1f890af 100644 --- a/packages/integrations/netlify/src/shared.ts +++ b/packages/integrations/netlify/src/shared.ts @@ -1,4 +1,4 @@ -import type { AstroConfig, RouteData } from 'astro'; +import type { AstroConfig, RouteData, ValidRedirectStatus } from 'astro'; import fs from 'fs'; export type RedirectDefinition = { @@ -6,9 +6,16 @@ export type RedirectDefinition = { input: string; target: string; weight: 0 | 1; - status: 200 | 404 | 301; + status: 200 | 404 | ValidRedirectStatus; }; +function getRedirectStatus(route: RouteData): ValidRedirectStatus { + if(typeof route.redirect === 'object') { + return route.redirect.status; + } + return 301; +} + export async function createRedirects( config: AstroConfig, routes: RouteData[], @@ -27,8 +34,8 @@ export async function createRedirects( definitions.push({ dynamic: false, input: route.pathname, - target: route.redirect, - status: 301, + target: typeof route.redirect === 'object' ? route.redirect.destination : route.redirect, + status: getRedirectStatus(route), weight: 1 }); continue; diff --git a/packages/integrations/netlify/test/static/redirects.test.js b/packages/integrations/netlify/test/static/redirects.test.js index 68fbc60f8..dcd904cab 100644 --- a/packages/integrations/netlify/test/static/redirects.test.js +++ b/packages/integrations/netlify/test/static/redirects.test.js @@ -15,6 +15,10 @@ describe('SSG - Redirects', () => { integrations: [testIntegration()], redirects: { '/other': '/', + '/two': { + status: 302, + destination: '/' + }, '/blog/[...slug]': '/team/articles/[...slug]' } }); @@ -26,6 +30,7 @@ describe('SSG - Redirects', () => { let parts = redirects.split(/\s+/); expect(parts).to.deep.equal([ '/blog/*', '/team/articles/*/index.html', '301', + '/two', '/', '302', '/other', '/', '301', '/nope', '/', '301', '/team/articles/*', '/team/articles/*/index.html', '200' diff --git a/packages/integrations/vercel/src/lib/redirects.ts b/packages/integrations/vercel/src/lib/redirects.ts index 4f0629172..015f59ec2 100644 --- a/packages/integrations/vercel/src/lib/redirects.ts +++ b/packages/integrations/vercel/src/lib/redirects.ts @@ -1,4 +1,7 @@ import type { AstroConfig, RouteData, RoutePart } from 'astro'; +import nodePath from 'node:path'; + +const pathJoin = nodePath.posix.join; // https://vercel.com/docs/project-configuration#legacy/routes interface VercelRoute { @@ -62,26 +65,33 @@ function getRedirectLocation(route: RouteData, config: AstroConfig): string { if(route.redirectRoute) { const pattern = getReplacePattern(route.redirectRoute.segments); const path = (config.trailingSlash === 'always' ? appendTrailingSlash(pattern) : pattern); - return config.base + path; + return pathJoin(config.base, path); + } else if(typeof route.redirect === 'object') { + return pathJoin(config.base, route.redirect.destination); } else { - return config.base + route.redirect; + return pathJoin(config.base, route.redirect || ''); } } +function getRedirectStatus(route: RouteData): number { + if(typeof route.redirect === 'object') { + return route.redirect.status; + } + return 301; +} + export function getRedirects(routes: RouteData[], config: AstroConfig): VercelRoute[] { let redirects: VercelRoute[] = []; + + for(const route of routes) { if(route.type === 'redirect') { - if(true || route.pathname) { - redirects.push({ - src: config.base + getMatchPattern(route.segments), - headers: { Location: getRedirectLocation(route, config) }, - status: 301 - }) - } else { - console.error(`Dynamic routes not yet supported`); - } + redirects.push({ + src: config.base + getMatchPattern(route.segments), + headers: { Location: getRedirectLocation(route, config) }, + status: getRedirectStatus(route) + }); } else if (route.type === 'page') { if (config.trailingSlash === 'always') { redirects.push({ diff --git a/packages/integrations/vercel/test/redirects.test.js b/packages/integrations/vercel/test/redirects.test.js index 33b0fe7b4..9cc04fb8c 100644 --- a/packages/integrations/vercel/test/redirects.test.js +++ b/packages/integrations/vercel/test/redirects.test.js @@ -12,6 +12,10 @@ describe('Redirects', () => { redirects: { '/one': '/', '/two': '/', + '/three': { + status: 302, + destination: '/' + }, '/blog/[...slug]': '/team/articles/[...slug]', } }); @@ -32,9 +36,13 @@ describe('Redirects', () => { expect(oneRoute.headers.Location).to.equal('/'); expect(oneRoute.status).to.equal(301); - const twoRoute = config.routes.find(r => r.src === '/\\/one'); + const twoRoute = config.routes.find(r => r.src === '/\\/two'); expect(twoRoute.headers.Location).to.equal('/'); expect(twoRoute.status).to.equal(301); + + const threeRoute = config.routes.find(r => r.src === '/\\/three'); + expect(threeRoute.headers.Location).to.equal('/'); + expect(threeRoute.status).to.equal(302); }); it('defines dynamic routes', async () => {