From 21a0fdc3909d0306fdcdafb461bbff094716bafe Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Mon, 5 Jun 2023 09:03:20 -0400 Subject: [PATCH] Redirects (#7067) * Redirects spike * Allow redirects in static mode * Support in Netlify as well * Adding a changeset * Rename file * Fix build problem * Refactor to be more modular * Fix location ref * Late test should only run in SSR * Support redirects in Netlify SSR configuration (#7167) * Implement support for dynamic routes in redirects (#7173) * Implement support for dynamic routes in redirects * Remove the .only * No need to special-case redirects in static build * Implement support for redirects config in the Vercel adapter (#7182) * Implement support for redirects config in the Vercel adapter * Remove unused condition * Move to a internal helper package * Add support for the object notation in redirects * Use status 308 for non-GET redirects (#7186) * Implement redirects in Cloudflare (#7198) * Implement redirects in Cloudflare * Fix build * Update tests b/c of new ordering * Debug issue * Use posix.join * Update packages/underscore-redirects/package.json Co-authored-by: Emanuele Stoppa * Update based on review comments * Update broken test --------- Co-authored-by: Emanuele Stoppa * Test that redirects can come from middleware (#7213) * Test that redirects can come from middleware * Allow non-promise returns for middleware * Implement priority (#7210) * Refactor * Fix netlify test ordering * Fix ordering again * Redirects: Allow preventing the output of the static HTML file (#7245) * Do a simple push for priority * Adding changesets * Put the implementation behind a flag. * Self review * Update .changeset/chatty-actors-stare.md Co-authored-by: Chris Swithinbank * Update packages/astro/src/@types/astro.ts Co-authored-by: Chris Swithinbank * Update packages/astro/src/@types/astro.ts Co-authored-by: Chris Swithinbank * Update packages/astro/src/@types/astro.ts Co-authored-by: Chris Swithinbank * Update packages/astro/src/@types/astro.ts Co-authored-by: Chris Swithinbank * Update docs on dynamic restrictions. * Update packages/astro/src/@types/astro.ts Co-authored-by: Sarah Rainsberger * Update packages/astro/src/@types/astro.ts Co-authored-by: Sarah Rainsberger * Code review changes * Document netlify static adapter * Update packages/astro/src/@types/astro.ts Co-authored-by: Sarah Rainsberger * Slight reword * Update .changeset/twenty-suns-vanish.md Co-authored-by: Sarah Rainsberger * Add a note about public/_redirects file * Update packages/astro/src/@types/astro.ts Co-authored-by: Sarah Rainsberger --------- Co-authored-by: Emanuele Stoppa Co-authored-by: Chris Swithinbank Co-authored-by: Sarah Rainsberger Co-authored-by: Nate Moore --- packages/integrations/vercel/package.json | 1 + .../integrations/vercel/src/lib/redirects.ts | 58 +++++++++++++----- .../integrations/vercel/src/static/adapter.ts | 1 + .../test/fixtures/redirects/astro.config.mjs | 9 +++ .../test/fixtures/redirects/package.json | 9 +++ .../fixtures/redirects/src/pages/index.astro | 8 +++ .../src/pages/team/articles/[...slug].astro | 25 ++++++++ .../vercel/test/redirects.test.js | 59 +++++++++++++++++++ 8 files changed, 155 insertions(+), 15 deletions(-) create mode 100644 packages/integrations/vercel/test/fixtures/redirects/astro.config.mjs create mode 100644 packages/integrations/vercel/test/fixtures/redirects/package.json create mode 100644 packages/integrations/vercel/test/fixtures/redirects/src/pages/index.astro create mode 100644 packages/integrations/vercel/test/fixtures/redirects/src/pages/team/articles/[...slug].astro create mode 100644 packages/integrations/vercel/test/redirects.test.js diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json index d57b882c1c..cd2df20d7c 100644 --- a/packages/integrations/vercel/package.json +++ b/packages/integrations/vercel/package.json @@ -50,6 +50,7 @@ "test": "mocha --exit --timeout 20000 test/" }, "dependencies": { + "@astrojs/internal-helpers": "^0.1.0", "@astrojs/webapi": "^2.2.0", "@vercel/analytics": "^0.1.8", "@vercel/nft": "^0.22.1", diff --git a/packages/integrations/vercel/src/lib/redirects.ts b/packages/integrations/vercel/src/lib/redirects.ts index c11d748024..1ec19bfac3 100644 --- a/packages/integrations/vercel/src/lib/redirects.ts +++ b/packages/integrations/vercel/src/lib/redirects.ts @@ -1,4 +1,9 @@ import type { AstroConfig, RouteData, RoutePart } from 'astro'; +import { appendForwardSlash } from '@astrojs/internal-helpers/path'; +import nodePath from 'node:path'; + +const pathJoin = nodePath.posix.join; + // https://vercel.com/docs/project-configuration#legacy/routes interface VercelRoute { @@ -54,28 +59,51 @@ function getReplacePattern(segments: RoutePart[][]) { return result; } +function getRedirectLocation(route: RouteData, 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); + } else if(typeof route.redirect === 'object') { + return pathJoin(config.base, route.redirect.destination); + } else { + 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[] = []; - if (config.trailingSlash === 'always') { - for (const route of routes) { - if (route.type !== 'page' || route.segments.length === 0) continue; + + for(const route of routes) { + if(route.type === 'redirect') { redirects.push({ src: config.base + getMatchPattern(route.segments), - headers: { Location: config.base + getReplacePattern(route.segments) + '/' }, - status: 308, - }); - } - } else if (config.trailingSlash === 'never') { - for (const route of routes) { - if (route.type !== 'page' || route.segments.length === 0) continue; - - redirects.push({ - src: config.base + getMatchPattern(route.segments) + '/', - headers: { Location: config.base + getReplacePattern(route.segments) }, - status: 308, + headers: { Location: getRedirectLocation(route, config) }, + status: getRedirectStatus(route) }); + } else if (route.type === 'page') { + if (config.trailingSlash === 'always') { + redirects.push({ + src: config.base + getMatchPattern(route.segments), + headers: { Location: config.base + getReplacePattern(route.segments) + '/' }, + status: 308, + }); + } else if (config.trailingSlash === 'never') { + redirects.push({ + src: config.base + getMatchPattern(route.segments) + '/', + headers: { Location: config.base + getReplacePattern(route.segments) }, + status: 308, + }); + } } } diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts index 0b3579cdd1..e0cc14322f 100644 --- a/packages/integrations/vercel/src/static/adapter.ts +++ b/packages/integrations/vercel/src/static/adapter.ts @@ -43,6 +43,7 @@ export default function vercelStatic({ outDir, build: { format: 'directory', + redirects: false, }, vite: { define: viteDefine, diff --git a/packages/integrations/vercel/test/fixtures/redirects/astro.config.mjs b/packages/integrations/vercel/test/fixtures/redirects/astro.config.mjs new file mode 100644 index 0000000000..a38be5065f --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/redirects/astro.config.mjs @@ -0,0 +1,9 @@ +import vercel from '@astrojs/vercel/static'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + adapter: vercel({imageService: true}), + experimental: { + assets: true + } +}); diff --git a/packages/integrations/vercel/test/fixtures/redirects/package.json b/packages/integrations/vercel/test/fixtures/redirects/package.json new file mode 100644 index 0000000000..d7dcc54718 --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/redirects/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/astro-vercel-redirects", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/vercel": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/vercel/test/fixtures/redirects/src/pages/index.astro b/packages/integrations/vercel/test/fixtures/redirects/src/pages/index.astro new file mode 100644 index 0000000000..9c077e2a38 --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/redirects/src/pages/index.astro @@ -0,0 +1,8 @@ + + + Testing + + +

Testing

+ + diff --git a/packages/integrations/vercel/test/fixtures/redirects/src/pages/team/articles/[...slug].astro b/packages/integrations/vercel/test/fixtures/redirects/src/pages/team/articles/[...slug].astro new file mode 100644 index 0000000000..716d3bd5d4 --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/redirects/src/pages/team/articles/[...slug].astro @@ -0,0 +1,25 @@ +--- +export const getStaticPaths = (async () => { + const posts = [ + { slug: 'one', data: {draft: false, title: 'One'} }, + { slug: 'two', data: {draft: false, title: 'Two'} } + ]; + return posts.map((post) => { + return { + params: { slug: post.slug }, + props: { draft: post.data.draft, title: post.data.title }, + }; + }); +}) + +const { slug } = Astro.params; +const { title } = Astro.props; +--- + + + { title } + + +

{ title }

+ + diff --git a/packages/integrations/vercel/test/redirects.test.js b/packages/integrations/vercel/test/redirects.test.js new file mode 100644 index 0000000000..0d54589fc5 --- /dev/null +++ b/packages/integrations/vercel/test/redirects.test.js @@ -0,0 +1,59 @@ +import { expect } from 'chai'; +import * as cheerio from 'cheerio'; +import { loadFixture } from './test-utils.js'; + +describe('Redirects', () => { + /** @type {import('../../../astro/test/test-utils.js').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/redirects/', + redirects: { + '/one': '/', + '/two': '/', + '/three': { + status: 302, + destination: '/' + }, + '/blog/[...slug]': '/team/articles/[...slug]', + }, + experimental: { + redirects: true, + }, + }); + await fixture.build(); + }); + + async function getConfig() { + const json = await fixture.readFile('../.vercel/output/config.json'); + const config = JSON.parse(json); + + return config; + } + + it('define static routes', async () => { + const config = await getConfig(); + + const oneRoute = config.routes.find(r => r.src === '/\\/one'); + expect(oneRoute.headers.Location).to.equal('/'); + expect(oneRoute.status).to.equal(301); + + 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 () => { + const config = await getConfig(); + + const blogRoute = config.routes.find(r => r.src.startsWith('/\\/blog')); + expect(blogRoute).to.not.be.undefined; + expect(blogRoute.headers.Location.startsWith('/team/articles')).to.equal(true); + expect(blogRoute.status).to.equal(301); + }); +});