From ffc771e7464fee681f55848b340a2ec5ff6be2f7 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 23 May 2023 15:43:24 -0400 Subject: [PATCH] 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 --- packages/astro/package.json | 1 + packages/astro/src/core/build/generate.ts | 2 +- packages/astro/src/core/path.ts | 82 +------------------ packages/integrations/vercel/package.json | 1 + .../integrations/vercel/src/lib/redirects.ts | 45 ++++++---- .../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 | 48 +++++++++++ packages/internal-helpers/package.json | 41 ++++++++++ packages/internal-helpers/readme.md | 3 + packages/internal-helpers/src/path.ts | 81 ++++++++++++++++++ packages/internal-helpers/tsconfig.json | 10 +++ pnpm-lock.yaml | 21 +++++ 15 files changed, 288 insertions(+), 98 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 create mode 100644 packages/internal-helpers/package.json create mode 100644 packages/internal-helpers/readme.md create mode 100644 packages/internal-helpers/src/path.ts create mode 100644 packages/internal-helpers/tsconfig.json diff --git a/packages/astro/package.json b/packages/astro/package.json index 5094ec543..65af625c9 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -113,6 +113,7 @@ }, "dependencies": { "@astrojs/compiler": "^1.4.0", + "@astrojs/internal-helpers": "^0.1.0", "@astrojs/language-server": "^1.0.0", "@astrojs/markdown-remark": "^2.2.1", "@astrojs/telemetry": "^2.1.1", diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index e0a576542..dec42e78a 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -177,7 +177,7 @@ async function generatePage( if(pageData.route.redirectRoute) { pageModulePromise = ssrEntry.pageMap?.get(pageData.route.redirectRoute!.component); } else { - pageModulePromise = { default: () => {} } as any; + pageModulePromise = () => Promise.resolve({ default: () => {} }); } } if (!pageModulePromise) { diff --git a/packages/astro/src/core/path.ts b/packages/astro/src/core/path.ts index cbf959f69..cbc3b6900 100644 --- a/packages/astro/src/core/path.ts +++ b/packages/astro/src/core/path.ts @@ -1,81 +1 @@ -export function appendExtension(path: string, extension: string) { - return path + '.' + extension; -} - -export function appendForwardSlash(path: string) { - return path.endsWith('/') ? path : path + '/'; -} - -export function prependForwardSlash(path: string) { - return path[0] === '/' ? path : '/' + path; -} - -export function removeTrailingForwardSlash(path: string) { - return path.endsWith('/') ? path.slice(0, path.length - 1) : path; -} - -export function removeLeadingForwardSlash(path: string) { - return path.startsWith('/') ? path.substring(1) : path; -} - -export function removeLeadingForwardSlashWindows(path: string) { - return path.startsWith('/') && path[2] === ':' ? path.substring(1) : path; -} - -export function trimSlashes(path: string) { - return path.replace(/^\/|\/$/g, ''); -} - -export function startsWithForwardSlash(path: string) { - return path[0] === '/'; -} - -export function startsWithDotDotSlash(path: string) { - const c1 = path[0]; - const c2 = path[1]; - const c3 = path[2]; - return c1 === '.' && c2 === '.' && c3 === '/'; -} - -export function startsWithDotSlash(path: string) { - const c1 = path[0]; - const c2 = path[1]; - return c1 === '.' && c2 === '/'; -} - -export function isRelativePath(path: string) { - return startsWithDotDotSlash(path) || startsWithDotSlash(path); -} - -function isString(path: unknown): path is string { - return typeof path === 'string' || path instanceof String; -} - -export function joinPaths(...paths: (string | undefined)[]) { - return paths - .filter(isString) - .map((path, i) => { - if (i === 0) { - return removeTrailingForwardSlash(path); - } else if (i === paths.length - 1) { - return removeLeadingForwardSlash(path); - } else { - return trimSlashes(path); - } - }) - .join('/'); -} - -export function removeFileExtension(path: string) { - let idx = path.lastIndexOf('.'); - return idx === -1 ? path : path.slice(0, idx); -} - -export function removeQueryString(path: string) { - const index = path.lastIndexOf('?'); - return index > 0 ? path.substring(0, index) : path; -} - -export function isRemotePath(src: string) { - return /^(http|ftp|https):?\/\//.test(src) || src.startsWith('data:'); -} +export * from '@astrojs/internal-helpers/path'; diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json index 72bb5758f..7747b24d8 100644 --- a/packages/integrations/vercel/package.json +++ b/packages/integrations/vercel/package.json @@ -51,6 +51,7 @@ }, "dependencies": { "@astrojs/webapi": "^2.1.1", + "@astrojs/internal-helpers": "^0.1.0", "@vercel/analytics": "^0.1.8", "@vercel/nft": "^0.22.1", "esbuild": "^0.17.12", diff --git a/packages/integrations/vercel/src/lib/redirects.ts b/packages/integrations/vercel/src/lib/redirects.ts index c11d74802..9915ea9ea 100644 --- a/packages/integrations/vercel/src/lib/redirects.ts +++ b/packages/integrations/vercel/src/lib/redirects.ts @@ -1,4 +1,5 @@ import type { AstroConfig, RouteData, RoutePart } from 'astro'; +import { appendForwardSlash } from '@astrojs/internal-helpers/path'; // https://vercel.com/docs/project-configuration#legacy/routes interface VercelRoute { @@ -54,28 +55,40 @@ 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 config.base + path; + } else { + return config.base + route.redirect; + } +} + 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: 301 }); + } 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/test/fixtures/redirects/astro.config.mjs b/packages/integrations/vercel/test/fixtures/redirects/astro.config.mjs new file mode 100644 index 000000000..a38be5065 --- /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 000000000..d7dcc5471 --- /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 000000000..9c077e2a3 --- /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 000000000..716d3bd5d --- /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 000000000..33b0fe7b4 --- /dev/null +++ b/packages/integrations/vercel/test/redirects.test.js @@ -0,0 +1,48 @@ +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': '/', + '/blog/[...slug]': '/team/articles/[...slug]', + } + }); + 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 === '/\\/one'); + expect(twoRoute.headers.Location).to.equal('/'); + expect(twoRoute.status).to.equal(301); + }); + + 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); + }); +}); diff --git a/packages/internal-helpers/package.json b/packages/internal-helpers/package.json new file mode 100644 index 000000000..29df36726 --- /dev/null +++ b/packages/internal-helpers/package.json @@ -0,0 +1,41 @@ +{ + "name": "@astrojs/internal-helpers", + "description": "Internal helpers used by core Astro packages.", + "version": "0.1.0", + "type": "module", + "author": "withastro", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/withastro/astro.git", + "directory": "packages/internal-helpers" + }, + "bugs": "https://github.com/withastro/astro/issues", + "exports": { + "./path": "./dist/path.js" + }, + "typesVersions": { + "*": { + "path": [ + "./dist/path.d.ts" + ] + } + }, + "files": [ + "dist" + ], + "scripts": { + "prepublish": "pnpm build", + "build": "astro-scripts build \"src/**/*.ts\" && tsc -p tsconfig.json", + "build:ci": "astro-scripts build \"src/**/*.ts\"", + "postbuild": "astro-scripts copy \"src/**/*.js\"", + "dev": "astro-scripts dev \"src/**/*.ts\"" + }, + "devDependencies": { + "astro-scripts": "workspace:*" + }, + "keywords": [ + "astro", + "astro-component" + ] +} diff --git a/packages/internal-helpers/readme.md b/packages/internal-helpers/readme.md new file mode 100644 index 000000000..283913dc5 --- /dev/null +++ b/packages/internal-helpers/readme.md @@ -0,0 +1,3 @@ +# @astrojs/internal-helpers + +These are internal helpers used by core Astro packages. This package does not follow semver and should not be used externally. diff --git a/packages/internal-helpers/src/path.ts b/packages/internal-helpers/src/path.ts new file mode 100644 index 000000000..cbf959f69 --- /dev/null +++ b/packages/internal-helpers/src/path.ts @@ -0,0 +1,81 @@ +export function appendExtension(path: string, extension: string) { + return path + '.' + extension; +} + +export function appendForwardSlash(path: string) { + return path.endsWith('/') ? path : path + '/'; +} + +export function prependForwardSlash(path: string) { + return path[0] === '/' ? path : '/' + path; +} + +export function removeTrailingForwardSlash(path: string) { + return path.endsWith('/') ? path.slice(0, path.length - 1) : path; +} + +export function removeLeadingForwardSlash(path: string) { + return path.startsWith('/') ? path.substring(1) : path; +} + +export function removeLeadingForwardSlashWindows(path: string) { + return path.startsWith('/') && path[2] === ':' ? path.substring(1) : path; +} + +export function trimSlashes(path: string) { + return path.replace(/^\/|\/$/g, ''); +} + +export function startsWithForwardSlash(path: string) { + return path[0] === '/'; +} + +export function startsWithDotDotSlash(path: string) { + const c1 = path[0]; + const c2 = path[1]; + const c3 = path[2]; + return c1 === '.' && c2 === '.' && c3 === '/'; +} + +export function startsWithDotSlash(path: string) { + const c1 = path[0]; + const c2 = path[1]; + return c1 === '.' && c2 === '/'; +} + +export function isRelativePath(path: string) { + return startsWithDotDotSlash(path) || startsWithDotSlash(path); +} + +function isString(path: unknown): path is string { + return typeof path === 'string' || path instanceof String; +} + +export function joinPaths(...paths: (string | undefined)[]) { + return paths + .filter(isString) + .map((path, i) => { + if (i === 0) { + return removeTrailingForwardSlash(path); + } else if (i === paths.length - 1) { + return removeLeadingForwardSlash(path); + } else { + return trimSlashes(path); + } + }) + .join('/'); +} + +export function removeFileExtension(path: string) { + let idx = path.lastIndexOf('.'); + return idx === -1 ? path : path.slice(0, idx); +} + +export function removeQueryString(path: string) { + const index = path.lastIndexOf('?'); + return index > 0 ? path.substring(0, index) : path; +} + +export function isRemotePath(src: string) { + return /^(http|ftp|https):?\/\//.test(src) || src.startsWith('data:'); +} diff --git a/packages/internal-helpers/tsconfig.json b/packages/internal-helpers/tsconfig.json new file mode 100644 index 000000000..569016e9d --- /dev/null +++ b/packages/internal-helpers/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "allowJs": true, + "target": "ES2021", + "module": "ES2022", + "outDir": "./dist" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20ca7bcb5..48c6a3515 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -534,6 +534,9 @@ importers: '@astrojs/compiler': specifier: ^1.4.0 version: 1.4.0 + '@astrojs/internal-helpers': + specifier: ^0.1.0 + version: link:../internal-helpers '@astrojs/language-server': specifier: ^1.0.0 version: 1.0.0 @@ -4809,6 +4812,9 @@ importers: packages/integrations/vercel: dependencies: + '@astrojs/internal-helpers': + specifier: ^0.1.0 + version: link:../../internal-helpers '@astrojs/webapi': specifier: ^2.1.1 version: link:../../webapi @@ -4868,6 +4874,15 @@ importers: specifier: workspace:* version: link:../../../../../astro + packages/integrations/vercel/test/fixtures/redirects: + dependencies: + '@astrojs/vercel': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + packages/integrations/vercel/test/fixtures/serverless-prerender: dependencies: '@astrojs/vercel': @@ -4926,6 +4941,12 @@ importers: specifier: workspace:* version: link:../../../../../astro + packages/internal-helpers: + devDependencies: + astro-scripts: + specifier: workspace:* + version: link:../../scripts + packages/markdown/component: devDependencies: '@types/mocha':