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
This commit is contained in:
parent
11a517b1f1
commit
ffc771e746
15 changed files with 288 additions and 98 deletions
|
@ -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",
|
||||
|
|
|
@ -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<any>({ default: () => {} });
|
||||
}
|
||||
}
|
||||
if (!pageModulePromise) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
9
packages/integrations/vercel/test/fixtures/redirects/astro.config.mjs
vendored
Normal file
9
packages/integrations/vercel/test/fixtures/redirects/astro.config.mjs
vendored
Normal file
|
@ -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
|
||||
}
|
||||
});
|
9
packages/integrations/vercel/test/fixtures/redirects/package.json
vendored
Normal file
9
packages/integrations/vercel/test/fixtures/redirects/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@test/astro-vercel-redirects",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/vercel": "workspace:*",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
8
packages/integrations/vercel/test/fixtures/redirects/src/pages/index.astro
vendored
Normal file
8
packages/integrations/vercel/test/fixtures/redirects/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Testing</h1>
|
||||
</body>
|
||||
</html>
|
25
packages/integrations/vercel/test/fixtures/redirects/src/pages/team/articles/[...slug].astro
vendored
Normal file
25
packages/integrations/vercel/test/fixtures/redirects/src/pages/team/articles/[...slug].astro
vendored
Normal file
|
@ -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;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>{ title }</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{ title }</h1>
|
||||
</body>
|
||||
</html>
|
48
packages/integrations/vercel/test/redirects.test.js
Normal file
48
packages/integrations/vercel/test/redirects.test.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
41
packages/internal-helpers/package.json
Normal file
41
packages/internal-helpers/package.json
Normal file
|
@ -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"
|
||||
]
|
||||
}
|
3
packages/internal-helpers/readme.md
Normal file
3
packages/internal-helpers/readme.md
Normal file
|
@ -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.
|
81
packages/internal-helpers/src/path.ts
Normal file
81
packages/internal-helpers/src/path.ts
Normal file
|
@ -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:');
|
||||
}
|
10
packages/internal-helpers/tsconfig.json
Normal file
10
packages/internal-helpers/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"target": "ES2021",
|
||||
"module": "ES2022",
|
||||
"outDir": "./dist"
|
||||
}
|
||||
}
|
|
@ -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':
|
||||
|
|
Loading…
Reference in a new issue