From 46e726960fbebf16ae80f42295e4566fcb94037e Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Wed, 10 May 2023 14:47:14 -0400 Subject: [PATCH 01/40] Redirects spike --- packages/astro/src/core/build/generate.ts | 29 ++++++-- packages/astro/src/core/errors/errors-data.ts | 13 ++++ packages/astro/src/core/render/result.ts | 30 ++++---- .../astro/src/runtime/server/render/common.ts | 7 ++ packages/astro/test/ssr-redirect.test.js | 70 ++++++++++++------- 5 files changed, 102 insertions(+), 47 deletions(-) diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 8d195bab4536..c1adc7baf584 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -33,7 +33,7 @@ import { createAPIContext, throwIfRedirectNotAllowed, } from '../endpoint/index.js'; -import { AstroError } from '../errors/index.js'; +import { AstroError, AstroErrorData } from '../errors/index.js'; import { debug, info } from '../logger/core.js'; import { callMiddleware } from '../middleware/callMiddleware.js'; import { createEnvironment, createRenderContext, renderPage } from '../render/index.js'; @@ -72,6 +72,12 @@ export function rootRelativeFacadeId(facadeId: string, settings: AstroSettings): return facadeId.slice(fileURLToPath(settings.config.root).length); } +function redirectWithNoLocation() { + throw new AstroError({ + ...AstroErrorData.RedirectWithNoLocation + }); +} + // Determines of a Rollup chunk is an entrypoint page. export function chunkIsPage( settings: AstroSettings, @@ -510,10 +516,23 @@ async function generatePath( } throw err; } - throwIfRedirectNotAllowed(response, opts.settings.config); - // If there's no body, do nothing - if (!response.body) return; - body = await response.text(); + + switch(response.status) { + case 301: + case 302: { + const location = response.headers.get("location"); + if(!location) { + redirectWithNoLocation(); + } + body = `` + break; + } + default: { + // If there's no body, do nothing + if (!response.body) return; + body = await response.text(); + } + } } const outFolder = getOutFolder(settings.config, pathname, pageData.route.type); diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 27425aee539b..55be7ba4973f 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -45,6 +45,7 @@ export const AstroErrorData = { * To redirect on a static website, the [meta refresh attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) can be used. Certain hosts also provide config-based redirects (ex: [Netlify redirects](https://docs.netlify.com/routing/redirects/)). */ StaticRedirectNotAvailable: { + // TODO remove title: '`Astro.redirect` is not available in static mode.', code: 3001, message: @@ -717,6 +718,18 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati return `The information stored in \`Astro.locals\` for the path "${href}" is not serializable.\nMake sure you store only serializable data.`; }, }, + /** + * @docs + * @see + * - [Astro.redirect](https://docs.astro.build/en/guides/server-side-rendering/#astroredirect) + * @description + * A redirect must be given a location with the `Location` header. + */ + RedirectWithNoLocation: { + // TODO remove + title: 'A redirect must be given a location with the `Location` header.', + code: 3035, + }, // No headings here, that way Vite errors are merged with Astro ones in the docs, which makes more sense to users. // Vite Errors - 4xxx /** diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 598ec116f785..2fad5d9c1c33 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -204,23 +204,21 @@ export function createResult(args: CreateResultArgs): SSRResult { locals, request, url, - redirect: args.ssr - ? (path, status) => { - // If the response is already sent, error as we cannot proceed with the redirect. - if ((request as any)[responseSentSymbol]) { - throw new AstroError({ - ...AstroErrorData.ResponseSentError, - }); - } + redirect(path, status) { + // If the response is already sent, error as we cannot proceed with the redirect. + if ((request as any)[responseSentSymbol]) { + throw new AstroError({ + ...AstroErrorData.ResponseSentError, + }); + } - return new Response(null, { - status: status || 302, - headers: { - Location: path, - }, - }); - } - : onlyAvailableInSSR('Astro.redirect'), + return new Response(null, { + status: status || 302, + headers: { + Location: path, + }, + }); + }, response: response as AstroGlobal['response'], slots: astroSlots as unknown as AstroGlobal['slots'], }; diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts index e9e74f9fa03e..debdbdbe0b24 100644 --- a/packages/astro/src/runtime/server/render/common.ts +++ b/packages/astro/src/runtime/server/render/common.ts @@ -57,6 +57,12 @@ export function stringifyChunk( } return renderAllHeadContent(result); } + default: { + if(chunk instanceof Response) { + return ''; + } + throw new Error(`Unknown chunk type: ${(chunk as any).type}`); + } } } else { if (isSlotString(chunk as string)) { @@ -102,6 +108,7 @@ export function chunkToByteArray( if (chunk instanceof Uint8Array) { return chunk as Uint8Array; } + // stringify chunk might return a HTMLString let stringified = stringifyChunk(result, chunk); return encoder.encode(stringified.toString()); diff --git a/packages/astro/test/ssr-redirect.test.js b/packages/astro/test/ssr-redirect.test.js index bb4f747cdd62..922963e79450 100644 --- a/packages/astro/test/ssr-redirect.test.js +++ b/packages/astro/test/ssr-redirect.test.js @@ -6,34 +6,52 @@ describe('Astro.redirect', () => { /** @type {import('./test-utils').Fixture} */ let fixture; - before(async () => { - fixture = await loadFixture({ - root: './fixtures/ssr-redirect/', - output: 'server', - adapter: testAdapter(), + describe('output: "server"', () => { + before(async () => { + fixture = await loadFixture({ + root: './fixtures/ssr-redirect/', + output: 'server', + adapter: testAdapter(), + }); + await fixture.build(); + }); + + it('Returns a 302 status', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/secret'); + const response = await app.render(request); + expect(response.status).to.equal(302); + expect(response.headers.get('location')).to.equal('/login'); + }); + + it('Warns when used inside a component', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/late'); + const response = await app.render(request); + try { + const text = await response.text(); + expect(false).to.equal(true); + } catch (e) { + expect(e.message).to.equal( + 'The response has already been sent to the browser and cannot be altered.' + ); + } }); - await fixture.build(); - }); - - it('Returns a 302 status', async () => { - const app = await fixture.loadTestAdapterApp(); - const request = new Request('http://example.com/secret'); - const response = await app.render(request); - expect(response.status).to.equal(302); - expect(response.headers.get('location')).to.equal('/login'); }); - it('Warns when used inside a component', async () => { - const app = await fixture.loadTestAdapterApp(); - const request = new Request('http://example.com/late'); - const response = await app.render(request); - try { - const text = await response.text(); - expect(false).to.equal(true); - } catch (e) { - expect(e.message).to.equal( - 'The response has already been sent to the browser and cannot be altered.' - ); - } + describe('output: "static"', () => { + before(async () => { + fixture = await loadFixture({ + root: './fixtures/ssr-redirect/', + output: 'static', + }); + await fixture.build(); + }); + + it.only('Includes the meta refresh tag.', async () => { + const html = await fixture.readFile('/secret/index.html'); + expect(html).to.include('http-equiv="refresh'); + expect(html).to.include('url=/login'); + }); }); }); From ef3ea942cc699f7782d733e1fc4751327d76ad96 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 11 May 2023 15:19:38 -0400 Subject: [PATCH 02/40] Allow redirects in static mode --- packages/astro/src/@types/astro.ts | 1 + packages/astro/src/core/build/generate.ts | 5 ++- packages/integrations/netlify/src/index.ts | 1 + .../netlify/src/integration-static.ts | 28 ++++++++++++++ packages/integrations/netlify/src/shared.ts | 38 +++++++++++++++---- .../fixtures/redirects/src/pages/index.astro | 6 +++ .../fixtures/redirects/src/pages/nope.astro | 3 ++ .../netlify/test/static/redirects.test.js | 27 +++++++++++++ .../netlify/test/static/test-utils.js | 29 ++++++++++++++ 9 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 packages/integrations/netlify/src/integration-static.ts create mode 100644 packages/integrations/netlify/test/static/fixtures/redirects/src/pages/index.astro create mode 100644 packages/integrations/netlify/test/static/fixtures/redirects/src/pages/nope.astro create mode 100644 packages/integrations/netlify/test/static/redirects.test.js create mode 100644 packages/integrations/netlify/test/static/test-utils.js diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 0f8cf424017f..56d26415dba6 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1724,6 +1724,7 @@ export interface RouteData { segments: RoutePart[][]; type: RouteType; prerender: boolean; + redirect?: string; } export type SerializedRouteData = Omit & { diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index c1adc7baf584..6f12b927589d 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -522,9 +522,10 @@ async function generatePath( case 302: { const location = response.headers.get("location"); if(!location) { - redirectWithNoLocation(); + return void redirectWithNoLocation(); } - body = `` + body = ``; + pageData.route.redirect = location; break; } default: { diff --git a/packages/integrations/netlify/src/index.ts b/packages/integrations/netlify/src/index.ts index fd7fd5fed05d..510e560f1128 100644 --- a/packages/integrations/netlify/src/index.ts +++ b/packages/integrations/netlify/src/index.ts @@ -1,2 +1,3 @@ export { netlifyEdgeFunctions } from './integration-edge-functions.js'; export { netlifyFunctions as default, netlifyFunctions } from './integration-functions.js'; +export { netlifyStatic } from './integration-static.js'; diff --git a/packages/integrations/netlify/src/integration-static.ts b/packages/integrations/netlify/src/integration-static.ts new file mode 100644 index 000000000000..ca9cc7db293a --- /dev/null +++ b/packages/integrations/netlify/src/integration-static.ts @@ -0,0 +1,28 @@ +import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro'; +import type { Args } from './netlify-functions.js'; +import { createRedirects } from './shared.js'; + +export function netlifyStatic(): AstroIntegration { + let _config: any; + return { + name: '@astrojs/netlify', + hooks: { + 'astro:config:done': ({ config }) => { + _config = config; + }, + // 'astro:config:setup': ({ config, updateConfig }) => { + // const outDir = dist ?? new URL('./dist/', config.root); + // updateConfig({ + // outDir, + // build: { + // client: outDir, + // server: new URL('./.netlify/functions-internal/', config.root), + // }, + // }); + // }, + 'astro:build:done': async ({ dir, routes }) => { + await createRedirects(_config, routes, dir, '', 'static'); + } + } + }; +} diff --git a/packages/integrations/netlify/src/shared.ts b/packages/integrations/netlify/src/shared.ts index 78a61a800ab4..8aaa1a34f070 100644 --- a/packages/integrations/netlify/src/shared.ts +++ b/packages/integrations/netlify/src/shared.ts @@ -1,12 +1,12 @@ import type { AstroConfig, RouteData } from 'astro'; import fs from 'fs'; -type RedirectDefinition = { +export type RedirectDefinition = { dynamic: boolean; input: string; target: string; weight: 0 | 1; - status: 200 | 404; + status: 200 | 404 | 301; }; export async function createRedirects( @@ -14,7 +14,7 @@ export async function createRedirects( routes: RouteData[], dir: URL, entryFile: string, - type: 'functions' | 'edge-functions' | 'builders' + type: 'functions' | 'edge-functions' | 'builders' | 'static' ) { const _redirectsURL = new URL('./_redirects', dir); const kind = type ?? 'functions'; @@ -23,7 +23,19 @@ export async function createRedirects( for (const route of routes) { if (route.pathname) { - if (route.distURL) { + if( kind === 'static') { + if(route.redirect) { + definitions.push({ + dynamic: false, + input: route.pathname, + target: route.redirect, + status: 301, + weight: 1 + }); + } + continue; + } + else if (route.distURL) { definitions.push({ dynamic: false, input: route.pathname, @@ -68,7 +80,19 @@ export async function createRedirects( }) .join('/'); - if (route.distURL) { + if(kind === 'static') { + if(route.redirect) { + definitions.push({ + dynamic: true, + input: pattern, + target: route.redirect, + status: 301, + weight: 1 + }); + } + continue; + } + else if (route.distURL) { const target = `${pattern}` + (config.build.format === 'directory' ? '/index.html' : '.html'); definitions.push({ @@ -99,8 +123,8 @@ export async function createRedirects( } function prettify(definitions: RedirectDefinition[]) { - let minInputLength = 0, - minTargetLength = 0; + let minInputLength = 4, + minTargetLength = 4; definitions.sort((a, b) => { // Find the longest input, so we can format things nicely if (a.input.length > minInputLength) { diff --git a/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/index.astro b/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/index.astro new file mode 100644 index 000000000000..53e029f04983 --- /dev/null +++ b/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/index.astro @@ -0,0 +1,6 @@ + +Testing + +

Testing

+ + diff --git a/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/nope.astro b/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/nope.astro new file mode 100644 index 000000000000..f48d767ee180 --- /dev/null +++ b/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/nope.astro @@ -0,0 +1,3 @@ +--- +return Astro.redirect('/'); +--- diff --git a/packages/integrations/netlify/test/static/redirects.test.js b/packages/integrations/netlify/test/static/redirects.test.js new file mode 100644 index 000000000000..5e0dd12ba95a --- /dev/null +++ b/packages/integrations/netlify/test/static/redirects.test.js @@ -0,0 +1,27 @@ +import { expect } from 'chai'; +import { load as cheerioLoad } from 'cheerio'; +import { loadFixture, testIntegration } from './test-utils.js'; +import { netlifyStatic } from '../../dist/index.js'; +import { fileURLToPath } from 'url'; + +describe('SSG - Redirects', () => { + /** @type {import('../../../astro/test/test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: new URL('./fixtures/redirects/', import.meta.url).toString(), + output: 'static', + adapter: netlifyStatic(), + site: `http://example.com`, + integrations: [testIntegration()], + }); + await fixture.build(); + }); + + it('Creates a redirects file', async () => { + let redirects = await fixture.readFile('/_redirects'); + let parts = redirects.split(/\s+/); + expect(parts).to.deep.equal(['/nope', '/', '301']); + }); +}); diff --git a/packages/integrations/netlify/test/static/test-utils.js b/packages/integrations/netlify/test/static/test-utils.js new file mode 100644 index 000000000000..02b5d2ad90b7 --- /dev/null +++ b/packages/integrations/netlify/test/static/test-utils.js @@ -0,0 +1,29 @@ +// @ts-check +import { fileURLToPath } from 'url'; + +export * from '../../../../astro/test/test-utils.js'; + +/** + * + * @returns {import('../../../../astro/dist/types/@types/astro').AstroIntegration} + */ +export function testIntegration() { + return { + name: '@astrojs/netlify/test-integration', + hooks: { + 'astro:config:setup': ({ updateConfig }) => { + updateConfig({ + vite: { + resolve: { + alias: { + '@astrojs/netlify/netlify-functions.js': fileURLToPath( + new URL('../../dist/netlify-functions.js', import.meta.url) + ), + }, + }, + }, + }); + }, + }, + }; +} From d6b71047227376e3240cae9d0ab1177ba8c81fc1 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 11 May 2023 15:54:10 -0400 Subject: [PATCH 03/40] Support in Netlify as well --- packages/astro/src/@types/astro.ts | 7 +++- packages/astro/src/core/build/common.ts | 2 + packages/astro/src/core/build/generate.ts | 12 ++++-- .../src/core/build/plugins/plugin-pages.ts | 3 ++ packages/astro/src/core/config/schema.ts | 2 + packages/astro/src/core/render/core.ts | 9 +++++ .../astro/src/core/routing/manifest/create.ts | 39 +++++++++++++++++++ .../netlify/test/static/redirects.test.js | 8 +++- 8 files changed, 76 insertions(+), 6 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 56d26415dba6..da6b593cba22 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -447,6 +447,11 @@ export interface AstroUserConfig { */ cacheDir?: string; + /** + * TODO + */ + redirects?: Record; + /** * @docs * @name site @@ -1704,7 +1709,7 @@ export interface AstroPluginOptions { logging: LogOptions; } -export type RouteType = 'page' | 'endpoint'; +export type RouteType = 'page' | 'endpoint' | 'redirect'; export interface RoutePart { content: string; diff --git a/packages/astro/src/core/build/common.ts b/packages/astro/src/core/build/common.ts index 74be830f0356..ed0c08d5bc51 100644 --- a/packages/astro/src/core/build/common.ts +++ b/packages/astro/src/core/build/common.ts @@ -26,6 +26,7 @@ export function getOutFolder( case 'endpoint': return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot); case 'page': + case 'redirect': switch (astroConfig.build.format) { case 'directory': { if (STATUS_CODE_PAGES.has(pathname)) { @@ -51,6 +52,7 @@ export function getOutFile( case 'endpoint': return new URL(npath.basename(pathname), outFolder); case 'page': + case 'redirect': switch (astroConfig.build.format) { case 'directory': { if (STATUS_CODE_PAGES.has(pathname)) { diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 6f12b927589d..2741a19cdaff 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -178,13 +178,17 @@ async function generatePage( .map(({ sheet }) => sheet) .reduce(mergeInlineCss, []); - const pageModule = ssrEntry.pageMap?.get(pageData.component); + let pageModule = ssrEntry.pageMap?.get(pageData.component); const middleware = ssrEntry.middleware; if (!pageModule) { - throw new Error( - `Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.` - ); + if(pageData.route.type === 'redirect') { + pageModule = { 'default': Function.prototype as any }; + } else { + throw new Error( + `Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.` + ); + } } if (shouldSkipDraft(pageModule, opts.settings)) { diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts index 132d03cf80fd..4efcd8817c69 100644 --- a/packages/astro/src/core/build/plugins/plugin-pages.ts +++ b/packages/astro/src/core/build/plugins/plugin-pages.ts @@ -35,6 +35,9 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern let imports = []; let i = 0; for (const pageData of eachPageData(internals)) { + if(pageData.route.type === 'redirect') { + continue; + } const variable = `_page${i}`; imports.push(`import * as ${variable} from ${JSON.stringify(pageData.moduleSpecifier)};`); importMap += `[${JSON.stringify(pageData.component)}, ${variable}],`; diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index fd8d88c4df58..a42c0ba792b5 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -36,6 +36,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = { }, vite: {}, legacy: {}, + redirects: {}, experimental: { assets: false, inlineStylesheets: 'never', @@ -133,6 +134,7 @@ export const AstroConfigSchema = z.object({ .optional() .default({}) ), + redirects: z.record(z.string(), z.string()).default(ASTRO_CONFIG_DEFAULTS.redirects), image: z .object({ service: z.object({ diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index fd57ad8bc673..6f392d0f564c 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -111,6 +111,15 @@ export type RenderPage = { }; export async function renderPage({ mod, renderContext, env, apiContext }: RenderPage) { + if(renderContext.route?.type === 'redirect') { + return new Response(null, { + status: 301, + headers: { + 'location': renderContext.route.redirect! + } + }); + } + // Validate the page component before rendering the page const Component = mod.default; if (!Component) diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index 8c7514969e02..a4af1583f554 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -412,6 +412,45 @@ export function createRouteManifest( }); }); + Object.entries(settings.config.redirects).forEach(([from, to]) => { + const trailingSlash = config.trailingSlash; + + const segments = removeLeadingForwardSlash(from) + .split(path.posix.sep) + .filter(Boolean) + .map((s: string) => { + validateSegment(s); + return getParts(s, from); + }); + + const pattern = getPattern(segments, settings.config.base, trailingSlash); + const generate = getRouteGenerator(segments, trailingSlash); + const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) + ? `/${segments.map((segment) => segment[0].content).join('/')}` + : null; + const params = segments + .flat() + .filter((p) => p.dynamic) + .map((p) => p.content); + const route = `/${segments + .map(([{ dynamic, content }]) => (dynamic ? `[${content}]` : content)) + .join('/')}`.toLowerCase(); + + + routes.unshift({ + type: 'redirect', + route, + pattern, + segments, + params, + component: '', + generate, + pathname: pathname || void 0, + prerender: false, + redirect: to + }); + }); + return { routes, }; diff --git a/packages/integrations/netlify/test/static/redirects.test.js b/packages/integrations/netlify/test/static/redirects.test.js index 5e0dd12ba95a..cab238c39790 100644 --- a/packages/integrations/netlify/test/static/redirects.test.js +++ b/packages/integrations/netlify/test/static/redirects.test.js @@ -15,6 +15,9 @@ describe('SSG - Redirects', () => { adapter: netlifyStatic(), site: `http://example.com`, integrations: [testIntegration()], + redirects: { + '/other': '/' + } }); await fixture.build(); }); @@ -22,6 +25,9 @@ describe('SSG - Redirects', () => { it('Creates a redirects file', async () => { let redirects = await fixture.readFile('/_redirects'); let parts = redirects.split(/\s+/); - expect(parts).to.deep.equal(['/nope', '/', '301']); + expect(parts).to.deep.equal([ + '/other', '/', '301', + '/nope', '/', '301' + ]); }); }); From f52116ac0381909f85d20d468f6d6123e303dc83 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 11 May 2023 16:05:29 -0400 Subject: [PATCH 04/40] Adding a changeset --- .changeset/chatty-actors-stare.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/chatty-actors-stare.md diff --git a/.changeset/chatty-actors-stare.md b/.changeset/chatty-actors-stare.md new file mode 100644 index 000000000000..a162719968dd --- /dev/null +++ b/.changeset/chatty-actors-stare.md @@ -0,0 +1,6 @@ +--- +'@astrojs/netlify': minor +'astro': minor +--- + +Implements the redirects proposal From a70820be15d592c1b7dab57e8f19cffb699b978c Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 11 May 2023 16:25:02 -0400 Subject: [PATCH 05/40] Rename file --- packages/astro/test/{ssr-redirect.test.js => redirects.test.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename packages/astro/test/{ssr-redirect.test.js => redirects.test.js} (96%) diff --git a/packages/astro/test/ssr-redirect.test.js b/packages/astro/test/redirects.test.js similarity index 96% rename from packages/astro/test/ssr-redirect.test.js rename to packages/astro/test/redirects.test.js index 922963e79450..80c783fe2ec7 100644 --- a/packages/astro/test/ssr-redirect.test.js +++ b/packages/astro/test/redirects.test.js @@ -48,7 +48,7 @@ describe('Astro.redirect', () => { await fixture.build(); }); - it.only('Includes the meta refresh tag.', async () => { + it('Includes the meta refresh tag.', async () => { const html = await fixture.readFile('/secret/index.html'); expect(html).to.include('http-equiv="refresh'); expect(html).to.include('url=/login'); From eed6a72a2adf3f7adbd8b00bc47cc56c258e75cb Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 19 May 2023 14:48:21 -0400 Subject: [PATCH 06/40] Fix build problem --- packages/astro/src/core/build/generate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 3453106ba0c7..1673703a01b0 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -175,12 +175,12 @@ async function generatePage( .map(({ sheet }) => sheet) .reduce(mergeInlineCss, []); - const pageModulePromise = ssrEntry.pageMap?.get(pageData.component); + let pageModulePromise = ssrEntry.pageMap?.get(pageData.component); const middleware = ssrEntry.middleware; if (!pageModulePromise) { if(pageData.route.type === 'redirect') { - pageModule = { 'default': Function.prototype as any }; + pageModulePromise = () => Promise.resolve({ 'default': Function.prototype }) as any; } else { throw new Error( `Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.` From 1749ce5d089c33e5bc4fb3dd2060252928aa8553 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 19 May 2023 15:07:14 -0400 Subject: [PATCH 07/40] Refactor to be more modular --- packages/astro/src/@types/astro.ts | 4 ++++ packages/astro/src/core/build/generate.ts | 15 +++------------ .../astro/src/core/build/plugins/plugin-pages.ts | 3 ++- packages/astro/src/core/errors/errors-data.ts | 2 +- packages/astro/src/core/redirects/helpers.ts | 5 +++++ packages/astro/src/core/redirects/index.ts | 2 ++ packages/astro/src/core/redirects/validate.ts | 13 +++++++++++++ packages/astro/src/core/render/core.ts | 5 +++-- 8 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 packages/astro/src/core/redirects/helpers.ts create mode 100644 packages/astro/src/core/redirects/index.ts create mode 100644 packages/astro/src/core/redirects/validate.ts diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 311157645c0f..ec98229125a9 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1843,6 +1843,10 @@ export interface RouteData { redirect?: string; } +export type RedirectRouteData = RouteData & { + redirect: string; +} + export type SerializedRouteData = Omit & { generate: undefined; pattern: string; diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 1673703a01b0..11613545cf7b 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -30,11 +30,12 @@ import { runHookBuildGenerated } from '../../integrations/index.js'; import { isHybridOutput } from '../../prerender/utils.js'; import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; import { callEndpoint, createAPIContext, throwIfRedirectNotAllowed } from '../endpoint/index.js'; -import { AstroError, AstroErrorData } from '../errors/index.js'; +import { AstroError } from '../errors/index.js'; import { debug, info } from '../logger/core.js'; import { callMiddleware } from '../middleware/callMiddleware.js'; import { createEnvironment, createRenderContext, renderPage } from '../render/index.js'; import { callGetStaticPaths } from '../render/route-cache.js'; +import { getRedirectLocationOrThrow } from '../redirects/index.js'; import { createAssetLink, createModuleScriptsSet, @@ -69,12 +70,6 @@ export function rootRelativeFacadeId(facadeId: string, settings: AstroSettings): return facadeId.slice(fileURLToPath(settings.config.root).length); } -function redirectWithNoLocation() { - throw new AstroError({ - ...AstroErrorData.RedirectWithNoLocation - }); -} - // Determines of a Rollup chunk is an entrypoint page. export function chunkIsPage( settings: AstroSettings, @@ -523,12 +518,8 @@ async function generatePath( switch(response.status) { case 301: case 302: { - const location = response.headers.get("location"); - if(!location) { - return void redirectWithNoLocation(); - } body = ``; - pageData.route.redirect = location; + pageData.route.redirect = getRedirectLocationOrThrow(response.headers) break; } default: { diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts index 47d5dab5b82a..aab166372473 100644 --- a/packages/astro/src/core/build/plugins/plugin-pages.ts +++ b/packages/astro/src/core/build/plugins/plugin-pages.ts @@ -5,6 +5,7 @@ import { eachPageData, type BuildInternals } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin'; import type { StaticBuildOptions } from '../types'; import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js'; +import { routeIsRedirect } from '../../redirects/index.js'; function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin { return { @@ -28,7 +29,7 @@ function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): V let imports = []; let i = 0; for (const pageData of eachPageData(internals)) { - if(pageData.route.type === 'redirect') { + if(routeIsRedirect(pageData.route)) { continue; } const variable = `_page${i}`; diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 1a8112626e69..5c7d3a101992 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -43,9 +43,9 @@ export const AstroErrorData = { * The `Astro.redirect` function is only available when [Server-side rendering](/en/guides/server-side-rendering/) is enabled. * * To redirect on a static website, the [meta refresh attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) can be used. Certain hosts also provide config-based redirects (ex: [Netlify redirects](https://docs.netlify.com/routing/redirects/)). + * @deprecated since version 2.6 */ StaticRedirectNotAvailable: { - // TODO remove title: '`Astro.redirect` is not available in static mode.', code: 3001, message: diff --git a/packages/astro/src/core/redirects/helpers.ts b/packages/astro/src/core/redirects/helpers.ts new file mode 100644 index 000000000000..05e4e83a07cf --- /dev/null +++ b/packages/astro/src/core/redirects/helpers.ts @@ -0,0 +1,5 @@ +import type { RouteData, RedirectRouteData } from '../../@types/astro'; + +export function routeIsRedirect(route: RouteData | undefined): route is RedirectRouteData { + return route?.type === 'redirect'; +} diff --git a/packages/astro/src/core/redirects/index.ts b/packages/astro/src/core/redirects/index.ts new file mode 100644 index 000000000000..1f69960486f8 --- /dev/null +++ b/packages/astro/src/core/redirects/index.ts @@ -0,0 +1,2 @@ +export { getRedirectLocationOrThrow } from './validate.js'; +export { routeIsRedirect } from './helpers.js'; diff --git a/packages/astro/src/core/redirects/validate.ts b/packages/astro/src/core/redirects/validate.ts new file mode 100644 index 000000000000..523d9f5783e8 --- /dev/null +++ b/packages/astro/src/core/redirects/validate.ts @@ -0,0 +1,13 @@ +import { AstroError, AstroErrorData } from '../errors/index.js'; + +export function getRedirectLocationOrThrow(headers: Headers): string { + let location = headers.get('location'); + + if(!location) { + throw new AstroError({ + ...AstroErrorData.RedirectWithNoLocation + }); + } + + return location; +} diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index c4930ab9416c..ec25833e5f4a 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -8,6 +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 } from '../redirects/index.js'; interface GetParamsAndPropsOptions { mod: ComponentInstance; @@ -111,11 +112,11 @@ export type RenderPage = { }; export async function renderPage({ mod, renderContext, env, apiContext }: RenderPage) { - if(renderContext.route?.type === 'redirect') { + if(routeIsRedirect(renderContext.route)) { return new Response(null, { status: 301, headers: { - 'location': renderContext.route.redirect! + location: renderContext.route!.redirect } }); } From ab0539b9516a6c6bf94aaec7a0a6db9f229f7ee9 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 19 May 2023 15:18:44 -0400 Subject: [PATCH 08/40] Fix location ref --- packages/astro/src/core/build/generate.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 11613545cf7b..dcd55089674e 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -518,8 +518,9 @@ async function generatePath( switch(response.status) { case 301: case 302: { + const location = getRedirectLocationOrThrow(response.headers); body = ``; - pageData.route.redirect = getRedirectLocationOrThrow(response.headers) + pageData.route.redirect = location; break; } default: { From 83ed3669be4ae87b5d6819b8df2e9d282a0e912d Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 19 May 2023 15:42:18 -0400 Subject: [PATCH 09/40] Late test should only run in SSR --- .../astro/test/fixtures/ssr-redirect/src/pages/late.astro | 5 ++++- packages/astro/test/redirects.test.js | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/astro/test/fixtures/ssr-redirect/src/pages/late.astro b/packages/astro/test/fixtures/ssr-redirect/src/pages/late.astro index dcfedb8da6a1..62d35411927e 100644 --- a/packages/astro/test/fixtures/ssr-redirect/src/pages/late.astro +++ b/packages/astro/test/fixtures/ssr-redirect/src/pages/late.astro @@ -1,5 +1,6 @@ --- import Redirect from '../components/redirect.astro'; +const staticMode = import.meta.env.STATIC_MODE; --- @@ -7,6 +8,8 @@ import Redirect from '../components/redirect.astro';

Testing

- + { !staticMode ? ( + + ) :
} diff --git a/packages/astro/test/redirects.test.js b/packages/astro/test/redirects.test.js index 80c783fe2ec7..f3e6c1121b76 100644 --- a/packages/astro/test/redirects.test.js +++ b/packages/astro/test/redirects.test.js @@ -29,7 +29,7 @@ describe('Astro.redirect', () => { const request = new Request('http://example.com/late'); const response = await app.render(request); try { - const text = await response.text(); + await response.text(); expect(false).to.equal(true); } catch (e) { expect(e.message).to.equal( @@ -41,6 +41,7 @@ describe('Astro.redirect', () => { describe('output: "static"', () => { before(async () => { + process.env.STATIC_MODE = true; fixture = await loadFixture({ root: './fixtures/ssr-redirect/', output: 'static', From 4857c7d31716cd18b8382df3da24de48cffbc974 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 23 May 2023 08:14:18 -0400 Subject: [PATCH 10/40] Support redirects in Netlify SSR configuration (#7167) --- .../netlify/src/integration-static.ts | 10 ----- packages/integrations/netlify/src/shared.ts | 29 ++++++++------ .../netlify/test/functions/redirects.test.js | 39 +++++++++++++++++++ 3 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 packages/integrations/netlify/test/functions/redirects.test.js diff --git a/packages/integrations/netlify/src/integration-static.ts b/packages/integrations/netlify/src/integration-static.ts index ca9cc7db293a..bb989b532f35 100644 --- a/packages/integrations/netlify/src/integration-static.ts +++ b/packages/integrations/netlify/src/integration-static.ts @@ -10,16 +10,6 @@ export function netlifyStatic(): AstroIntegration { 'astro:config:done': ({ config }) => { _config = config; }, - // 'astro:config:setup': ({ config, updateConfig }) => { - // const outDir = dist ?? new URL('./dist/', config.root); - // updateConfig({ - // outDir, - // build: { - // client: outDir, - // server: new URL('./.netlify/functions-internal/', config.root), - // }, - // }); - // }, 'astro:build:done': async ({ dir, routes }) => { await createRedirects(_config, routes, dir, '', 'static'); } diff --git a/packages/integrations/netlify/src/shared.ts b/packages/integrations/netlify/src/shared.ts index 8aaa1a34f070..d7843ae06904 100644 --- a/packages/integrations/netlify/src/shared.ts +++ b/packages/integrations/netlify/src/shared.ts @@ -23,16 +23,18 @@ export async function createRedirects( for (const route of routes) { if (route.pathname) { - if( kind === 'static') { - if(route.redirect) { - definitions.push({ - dynamic: false, - input: route.pathname, - target: route.redirect, - status: 301, - weight: 1 - }); - } + if(route.redirect) { + definitions.push({ + dynamic: false, + input: route.pathname, + target: route.redirect, + status: 301, + weight: 1 + }); + continue; + } + + if(kind === 'static') { continue; } else if (route.distURL) { @@ -80,7 +82,7 @@ export async function createRedirects( }) .join('/'); - if(kind === 'static') { + //if(kind === 'static') { if(route.redirect) { definitions.push({ dynamic: true, @@ -89,8 +91,11 @@ export async function createRedirects( status: 301, weight: 1 }); + continue; } - continue; + + if(kind === 'static') { + continue; } else if (route.distURL) { const target = diff --git a/packages/integrations/netlify/test/functions/redirects.test.js b/packages/integrations/netlify/test/functions/redirects.test.js new file mode 100644 index 000000000000..f14b0dde7e55 --- /dev/null +++ b/packages/integrations/netlify/test/functions/redirects.test.js @@ -0,0 +1,39 @@ +import { expect } from 'chai'; +import { load as cheerioLoad } from 'cheerio'; +import { loadFixture, testIntegration } from './test-utils.js'; +import netlifyAdapter from '../../dist/index.js'; +import { fileURLToPath } from 'url'; + +describe('SSG - Redirects', () => { + /** @type {import('../../../astro/test/test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: new URL('../static/fixtures/redirects/', import.meta.url).toString(), + output: 'server', + adapter: netlifyAdapter({ + dist: new URL('../static/fixtures/redirects/dist/', import.meta.url), + }), + site: `http://example.com`, + integrations: [testIntegration()], + redirects: { + '/other': '/' + } + }); + await fixture.build(); + }); + + it('Creates a redirects file', async () => { + let redirects = await fixture.readFile('/_redirects'); + let parts = redirects.split(/\s+/); + expect(parts).to.deep.equal([ + '/other', '/', '301', + '/', '/.netlify/functions/entry', '200', + + // This uses the dynamic Astro.redirect, so we don't know that it's a redirect + // until runtime. This is correct! + '/nope', '/.netlify/functions/entry', '200' + ]); + }); +}); From 25d7d208ba7653e79d7b1b6046016c5e3a27eba6 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 23 May 2023 09:01:46 -0400 Subject: [PATCH 11/40] 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 --- packages/astro/src/@types/astro.ts | 1 + packages/astro/src/core/build/generate.ts | 23 ++++--- packages/astro/src/core/redirects/helpers.ts | 9 ++- packages/astro/src/core/redirects/index.ts | 2 +- packages/astro/src/core/render/core.ts | 4 +- .../astro/src/core/routing/manifest/create.ts | 6 +- .../src/pages/articles/[...slug].astro | 25 ++++++++ .../ssr-redirect/src/pages/index.astro | 10 ++++ packages/astro/test/redirects.test.js | 27 ++++++++- packages/integrations/netlify/src/shared.ts | 60 ++++++++----------- .../netlify/test/functions/redirects.test.js | 5 +- .../src/pages/team/articles/[...slug].astro | 25 ++++++++ .../netlify/test/static/redirects.test.js | 9 +-- 13 files changed, 151 insertions(+), 55 deletions(-) create mode 100644 packages/astro/test/fixtures/ssr-redirect/src/pages/articles/[...slug].astro create mode 100644 packages/astro/test/fixtures/ssr-redirect/src/pages/index.astro create mode 100644 packages/integrations/netlify/test/static/fixtures/redirects/src/pages/team/articles/[...slug].astro diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index ec98229125a9..9d09d4260137 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1841,6 +1841,7 @@ export interface RouteData { type: RouteType; prerender: boolean; redirect?: string; + redirectRoute?: RouteData; } export type RedirectRouteData = RouteData & { diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index dcd55089674e..e0a576542d79 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -35,8 +35,8 @@ import { debug, info } from '../logger/core.js'; import { callMiddleware } from '../middleware/callMiddleware.js'; import { createEnvironment, createRenderContext, renderPage } from '../render/index.js'; import { callGetStaticPaths } from '../render/route-cache.js'; -import { getRedirectLocationOrThrow } from '../redirects/index.js'; -import { +import { getRedirectLocationOrThrow, routeIsRedirect } from '../redirects/index.js'; +import { createAssetLink, createModuleScriptsSet, createStylesheetElementSet, @@ -173,15 +173,18 @@ async function generatePage( let pageModulePromise = ssrEntry.pageMap?.get(pageData.component); const middleware = ssrEntry.middleware; - if (!pageModulePromise) { - if(pageData.route.type === 'redirect') { - pageModulePromise = () => Promise.resolve({ 'default': Function.prototype }) as any; + if (!pageModulePromise && routeIsRedirect(pageData.route)) { + if(pageData.route.redirectRoute) { + pageModulePromise = ssrEntry.pageMap?.get(pageData.route.redirectRoute!.component); } else { - throw new Error( - `Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.` - ); + pageModulePromise = { default: () => {} } as any; } } + if (!pageModulePromise) { + throw new Error( + `Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.` + ); + } const pageModule = await pageModulePromise(); if (shouldSkipDraft(pageModule, opts.settings)) { info(opts.logging, null, `${magenta('⚠️')} Skipping draft ${pageData.route.component}`); @@ -519,7 +522,9 @@ async function generatePath( case 301: case 302: { const location = getRedirectLocationOrThrow(response.headers); - body = ``; + body = ` +Redirecting to: ${location} +`; pageData.route.redirect = location; break; } diff --git a/packages/astro/src/core/redirects/helpers.ts b/packages/astro/src/core/redirects/helpers.ts index 05e4e83a07cf..b65e216e6cd3 100644 --- a/packages/astro/src/core/redirects/helpers.ts +++ b/packages/astro/src/core/redirects/helpers.ts @@ -1,5 +1,12 @@ -import type { RouteData, RedirectRouteData } from '../../@types/astro'; +import type { RouteData, RedirectRouteData, Params } from '../../@types/astro'; export function routeIsRedirect(route: RouteData | undefined): route is RedirectRouteData { return route?.type === 'redirect'; } + +export function redirectRouteGenerate(redirectRoute: RouteData, data: Params): string { + const routeData = redirectRoute.redirectRoute; + const route = redirectRoute.redirect; + + return routeData?.generate(data) || routeData?.pathname || route || '/'; +} diff --git a/packages/astro/src/core/redirects/index.ts b/packages/astro/src/core/redirects/index.ts index 1f69960486f8..3a8326de6690 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 } from './helpers.js'; +export { routeIsRedirect, redirectRouteGenerate } from './helpers.js'; diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index eb86b3193a36..3eb085fda534 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 } from '../redirects/index.js'; +import { routeIsRedirect, redirectRouteGenerate } from '../redirects/index.js'; interface GetParamsAndPropsOptions { mod: ComponentInstance; @@ -116,7 +116,7 @@ export async function renderPage({ mod, renderContext, env, apiContext }: Render return new Response(null, { status: 301, headers: { - location: renderContext.route!.redirect + location: redirectRouteGenerate(renderContext.route!, renderContext.params) } }); } diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index 25258e3e559b..81ff18634909 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -444,6 +444,7 @@ export function createRouteManifest( .map(([{ dynamic, content }]) => (dynamic ? `[${content}]` : content)) .join('/')}`.toLowerCase(); + routes.unshift({ type: 'redirect', @@ -451,11 +452,12 @@ export function createRouteManifest( pattern, segments, params, - component: '', + component: from, generate, pathname: pathname || void 0, prerender: false, - redirect: to + redirect: to, + redirectRoute: routes.find(r => r.route === to) }); }); diff --git a/packages/astro/test/fixtures/ssr-redirect/src/pages/articles/[...slug].astro b/packages/astro/test/fixtures/ssr-redirect/src/pages/articles/[...slug].astro new file mode 100644 index 000000000000..716d3bd5d4af --- /dev/null +++ b/packages/astro/test/fixtures/ssr-redirect/src/pages/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/astro/test/fixtures/ssr-redirect/src/pages/index.astro b/packages/astro/test/fixtures/ssr-redirect/src/pages/index.astro new file mode 100644 index 000000000000..e06d49b853b1 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-redirect/src/pages/index.astro @@ -0,0 +1,10 @@ +--- +--- + + + Testing + + +

Testing

+ + diff --git a/packages/astro/test/redirects.test.js b/packages/astro/test/redirects.test.js index f3e6c1121b76..b8fd723ffe44 100644 --- a/packages/astro/test/redirects.test.js +++ b/packages/astro/test/redirects.test.js @@ -45,14 +45,39 @@ describe('Astro.redirect', () => { fixture = await loadFixture({ root: './fixtures/ssr-redirect/', output: 'static', + redirects: { + '/one': '/', + '/two': '/', + '/blog/[...slug]': '/articles/[...slug]' + } }); await fixture.build(); }); - it('Includes the meta refresh tag.', async () => { + it('Includes the meta refresh tag in Astro.redirect pages', async () => { const html = await fixture.readFile('/secret/index.html'); expect(html).to.include('http-equiv="refresh'); expect(html).to.include('url=/login'); }); + + it('Includes the meta refresh tag in `redirect` config pages', async () => { + let html = await fixture.readFile('/one/index.html'); + expect(html).to.include('http-equiv="refresh'); + expect(html).to.include('url=/'); + + html = await fixture.readFile('/two/index.html'); + expect(html).to.include('http-equiv="refresh'); + expect(html).to.include('url=/'); + }); + + it('Generates page for dynamic routes', async () => { + let html = await fixture.readFile('/blog/one/index.html'); + expect(html).to.include('http-equiv="refresh'); + expect(html).to.include('url=/articles/one'); + + html = await fixture.readFile('/blog/two/index.html'); + expect(html).to.include('http-equiv="refresh'); + expect(html).to.include('url=/articles/two'); + }); }); }); diff --git a/packages/integrations/netlify/src/shared.ts b/packages/integrations/netlify/src/shared.ts index d7843ae06904..479c03907dc4 100644 --- a/packages/integrations/netlify/src/shared.ts +++ b/packages/integrations/netlify/src/shared.ts @@ -65,46 +65,18 @@ export async function createRedirects( } } } else { - const pattern = - '/' + - route.segments - .map(([part]) => { - //(part.dynamic ? '*' : part.content) - if (part.dynamic) { - if (part.spread) { - return '*'; - } else { - return ':' + part.content; - } - } else { - return part.content; - } - }) - .join('/'); + const pattern = generateDynamicPattern(route); - //if(kind === 'static') { - if(route.redirect) { - definitions.push({ - dynamic: true, - input: pattern, - target: route.redirect, - status: 301, - weight: 1 - }); - continue; - } - - if(kind === 'static') { - continue; - } - else if (route.distURL) { + if (route.distURL) { + const targetRoute = route.redirectRoute ?? route; + const targetPattern = generateDynamicPattern(targetRoute); const target = - `${pattern}` + (config.build.format === 'directory' ? '/index.html' : '.html'); + `${targetPattern}` + (config.build.format === 'directory' ? '/index.html' : '.html'); definitions.push({ dynamic: true, input: pattern, target, - status: 200, + status: route.type === 'redirect' ? 301 : 200, weight: 1, }); } else { @@ -127,6 +99,26 @@ export async function createRedirects( await fs.promises.appendFile(_redirectsURL, _redirects, 'utf-8'); } +function generateDynamicPattern(route: RouteData) { + const pattern = + '/' + + route.segments + .map(([part]) => { + //(part.dynamic ? '*' : part.content) + if (part.dynamic) { + if (part.spread) { + return '*'; + } else { + return ':' + part.content; + } + } else { + return part.content; + } + }) + .join('/'); + return pattern; +} + function prettify(definitions: RedirectDefinition[]) { let minInputLength = 4, minTargetLength = 4; diff --git a/packages/integrations/netlify/test/functions/redirects.test.js b/packages/integrations/netlify/test/functions/redirects.test.js index f14b0dde7e55..8de4cbc9b9b1 100644 --- a/packages/integrations/netlify/test/functions/redirects.test.js +++ b/packages/integrations/netlify/test/functions/redirects.test.js @@ -33,7 +33,10 @@ describe('SSG - Redirects', () => { // This uses the dynamic Astro.redirect, so we don't know that it's a redirect // until runtime. This is correct! - '/nope', '/.netlify/functions/entry', '200' + '/nope', '/.netlify/functions/entry', '200', + + // A real route + '/team/articles/*', '/.netlify/functions/entry', '200', ]); }); }); diff --git a/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/team/articles/[...slug].astro b/packages/integrations/netlify/test/static/fixtures/redirects/src/pages/team/articles/[...slug].astro new file mode 100644 index 000000000000..716d3bd5d4af --- /dev/null +++ b/packages/integrations/netlify/test/static/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/netlify/test/static/redirects.test.js b/packages/integrations/netlify/test/static/redirects.test.js index cab238c39790..68fbc60f8640 100644 --- a/packages/integrations/netlify/test/static/redirects.test.js +++ b/packages/integrations/netlify/test/static/redirects.test.js @@ -1,8 +1,6 @@ import { expect } from 'chai'; -import { load as cheerioLoad } from 'cheerio'; import { loadFixture, testIntegration } from './test-utils.js'; import { netlifyStatic } from '../../dist/index.js'; -import { fileURLToPath } from 'url'; describe('SSG - Redirects', () => { /** @type {import('../../../astro/test/test-utils').Fixture} */ @@ -16,7 +14,8 @@ describe('SSG - Redirects', () => { site: `http://example.com`, integrations: [testIntegration()], redirects: { - '/other': '/' + '/other': '/', + '/blog/[...slug]': '/team/articles/[...slug]' } }); await fixture.build(); @@ -26,8 +25,10 @@ describe('SSG - Redirects', () => { let redirects = await fixture.readFile('/_redirects'); let parts = redirects.split(/\s+/); expect(parts).to.deep.equal([ + '/blog/*', '/team/articles/*/index.html', '301', '/other', '/', '301', - '/nope', '/', '301' + '/nope', '/', '301', + '/team/articles/*', '/team/articles/*/index.html', '200' ]); }); }); From ffc771e7464fee681f55848b340a2ec5ff6be2f7 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 23 May 2023 15:43:24 -0400 Subject: [PATCH 12/40] 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 5094ec543876..65af625c9a02 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 e0a576542d79..dec42e78a485 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 cbf959f69f57..cbc3b6900e29 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 72bb5758f9be..7747b24d8348 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 c11d748024fe..9915ea9ea4a2 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 000000000000..a38be5065f8e --- /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 000000000000..d7dcc54718c6 --- /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 000000000000..9c077e2a381b --- /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 000000000000..716d3bd5d4af --- /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 000000000000..33b0fe7b4eb8 --- /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 000000000000..29df367262cb --- /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 000000000000..283913dc5708 --- /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 000000000000..cbf959f69f57 --- /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 000000000000..569016e9d844 --- /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 20ca7bcb57b4..48c6a3515193 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': From f55e42222c01ab9517e70b8989b22f912901ef35 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 23 May 2023 14:46:00 -0400 Subject: [PATCH 13/40] 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 | 21 ++++++++++++++++--- .../vercel/test/redirects.test.js | 10 ++++++++- 10 files changed, 88 insertions(+), 21 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index cf9ced260b05..807e16ded861 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 b65e216e6cd3..df82a61e5a4f 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 3a8326de6690..aedb828eb80c 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 3eb085fda534..57cd7949e3b2 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 b8fd723ffe44..f2531b466fc1 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 479c03907dc4..4d1f890afe50 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 68fbc60f8640..dcd904cab257 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 9915ea9ea4a2..1ec19bfac36a 100644 --- a/packages/integrations/vercel/src/lib/redirects.ts +++ b/packages/integrations/vercel/src/lib/redirects.ts @@ -1,5 +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 { @@ -59,21 +63,32 @@ 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; + 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') { redirects.push({ src: config.base + getMatchPattern(route.segments), headers: { Location: getRedirectLocation(route, config) }, - status: 301 + status: getRedirectStatus(route) }); } else if (route.type === 'page') { if (config.trailingSlash === 'always') { diff --git a/packages/integrations/vercel/test/redirects.test.js b/packages/integrations/vercel/test/redirects.test.js index 33b0fe7b4eb8..9cc04fb8cd4b 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 () => { From c2f889bec627371775256185c60394bd748ac7a7 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Wed, 24 May 2023 08:39:23 -0400 Subject: [PATCH 14/40] Use status 308 for non-GET redirects (#7186) --- packages/astro/src/core/app/index.ts | 21 +++++++++++++----- packages/astro/src/core/build/generate.ts | 4 ++-- .../astro/src/core/redirects/component.ts | 10 +++++++++ packages/astro/src/core/redirects/helpers.ts | 4 +++- packages/astro/src/core/redirects/index.ts | 1 + packages/astro/src/core/render/core.ts | 2 +- packages/astro/test/redirects.test.js | 22 +++++++++++++++++++ 7 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 packages/astro/src/core/redirects/component.ts diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 1e2dd1d24164..4609ded1e0a2 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -28,6 +28,7 @@ import { createStylesheetElementSet, } from '../render/ssr-element.js'; import { matchRoute } from '../routing/match.js'; +import { RedirectComponentInstance } from '../redirects/index.js'; export { deserializeManifest } from './common.js'; const clientLocalsSymbol = Symbol.for('astro.locals'); @@ -139,20 +140,20 @@ export class App { defaultStatus = 404; } - let mod = await this.#manifest.pageMap.get(routeData.component)!(); + let mod = await this.#getModuleForRoute(routeData); - if (routeData.type === 'page') { + if (routeData.type === 'page' || routeData.type === 'redirect') { let response = await this.#renderPage(request, routeData, mod, defaultStatus); // If there was a known error code, try sending the according page (e.g. 404.astro / 500.astro). if (response.status === 500 || response.status === 404) { - const errorPageData = matchRoute('/' + response.status, this.#manifestData); - if (errorPageData && errorPageData.route !== routeData.route) { - mod = await this.#manifest.pageMap.get(errorPageData.component)!(); + const errorRouteData = matchRoute('/' + response.status, this.#manifestData); + if (errorRouteData && errorRouteData.route !== routeData.route) { + mod = await this.#getModuleForRoute(errorRouteData); try { let errorResponse = await this.#renderPage( request, - errorPageData, + errorRouteData, mod, response.status ); @@ -172,6 +173,14 @@ export class App { return getSetCookiesFromResponse(response); } + async #getModuleForRoute(route: RouteData): Promise { + if(route.type === 'redirect') { + return RedirectComponentInstance; + } else { + return await this.#manifest.pageMap.get(route.component)!(); + } + } + async #renderPage( request: Request, routeData: RouteData, diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 9c4dfb1eed43..d52acbe1831e 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -35,7 +35,7 @@ import { debug, info } from '../logger/core.js'; import { callMiddleware } from '../middleware/callMiddleware.js'; import { createEnvironment, createRenderContext, renderPage } from '../render/index.js'; import { callGetStaticPaths } from '../render/route-cache.js'; -import { getRedirectLocationOrThrow, routeIsRedirect } from '../redirects/index.js'; +import { getRedirectLocationOrThrow, routeIsRedirect, RedirectComponentInstance } from '../redirects/index.js'; import { createAssetLink, createModuleScriptsSet, @@ -177,7 +177,7 @@ async function generatePage( if(pageData.route.redirectRoute) { pageModulePromise = ssrEntry.pageMap?.get(pageData.route.redirectRoute!.component); } else { - pageModulePromise = () => Promise.resolve({ default: () => {} }); + pageModulePromise = () => Promise.resolve(RedirectComponentInstance); } } if (!pageModulePromise) { diff --git a/packages/astro/src/core/redirects/component.ts b/packages/astro/src/core/redirects/component.ts new file mode 100644 index 000000000000..1471af1f40fd --- /dev/null +++ b/packages/astro/src/core/redirects/component.ts @@ -0,0 +1,10 @@ +import type { ComponentInstance } from '../../@types/astro'; + +// A stub of a component instance for a given route +export const RedirectComponentInstance: ComponentInstance = { + default() { + return new Response(null, { + status: 301 + }); + } +}; diff --git a/packages/astro/src/core/redirects/helpers.ts b/packages/astro/src/core/redirects/helpers.ts index df82a61e5a4f..c5c54ee35d98 100644 --- a/packages/astro/src/core/redirects/helpers.ts +++ b/packages/astro/src/core/redirects/helpers.ts @@ -18,10 +18,12 @@ export function redirectRouteGenerate(redirectRoute: RouteData, data: Params): s return route.destination; } -export function redirectRouteStatus(redirectRoute: RouteData): ValidRedirectStatus { +export function redirectRouteStatus(redirectRoute: RouteData, method = 'GET'): ValidRedirectStatus { const routeData = redirectRoute.redirectRoute; if(typeof routeData?.redirect === 'object') { return routeData.redirect.status; + } else if(method !== 'GET') { + return 308; } return 301; } diff --git a/packages/astro/src/core/redirects/index.ts b/packages/astro/src/core/redirects/index.ts index aedb828eb80c..f494230f56d8 100644 --- a/packages/astro/src/core/redirects/index.ts +++ b/packages/astro/src/core/redirects/index.ts @@ -1,2 +1,3 @@ export { getRedirectLocationOrThrow } from './validate.js'; export { routeIsRedirect, redirectRouteGenerate, redirectRouteStatus } from './helpers.js'; +export { RedirectComponentInstance } from './component.js'; diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index 3581d3527a4f..505c391e5dcd 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -114,7 +114,7 @@ export type RenderPage = { export async function renderPage({ mod, renderContext, env, apiContext }: RenderPage) { if(routeIsRedirect(renderContext.route)) { return new Response(null, { - status: redirectRouteStatus(renderContext.route), + status: redirectRouteStatus(renderContext.route, renderContext.request.method), headers: { location: redirectRouteGenerate(renderContext.route, renderContext.params) } diff --git a/packages/astro/test/redirects.test.js b/packages/astro/test/redirects.test.js index f2531b466fc1..fde343bcf7ea 100644 --- a/packages/astro/test/redirects.test.js +++ b/packages/astro/test/redirects.test.js @@ -12,6 +12,9 @@ describe('Astro.redirect', () => { root: './fixtures/ssr-redirect/', output: 'server', adapter: testAdapter(), + redirects: { + '/api/redirect': '/' + } }); await fixture.build(); }); @@ -37,6 +40,25 @@ describe('Astro.redirect', () => { ); } }); + + describe('Redirects config', () => { + it('Returns the redirect', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/api/redirect'); + const response = await app.render(request); + expect(response.status).to.equal(301); + expect(response.headers.get('Location')).to.equal('/'); + }); + + it('Uses 308 for non-GET methods', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/api/redirect', { + method: 'POST' + }); + const response = await app.render(request); + expect(response.status).to.equal(308); + }); + }); }); describe('output: "static"', () => { From 8b4d248a36dc79e7c0cca1f579d3559a997cd332 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 25 May 2023 10:53:58 -0400 Subject: [PATCH 15/40] 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 --- packages/integrations/cloudflare/package.json | 1 + packages/integrations/cloudflare/src/index.ts | 16 +- .../cloudflare/test/directory.test.js | 15 ++ packages/integrations/netlify/package.json | 1 + packages/integrations/netlify/src/shared.ts | 168 ++---------------- .../netlify/test/functions/redirects.test.js | 5 +- .../netlify/test/static/redirects.test.js | 9 +- packages/underscore-redirects/package.json | 42 +++++ packages/underscore-redirects/readme.md | 3 + packages/underscore-redirects/src/astro.ts | 145 +++++++++++++++ packages/underscore-redirects/src/index.ts | 8 + packages/underscore-redirects/src/print.ts | 36 ++++ .../underscore-redirects/src/redirects.ts | 69 +++++++ .../underscore-redirects/test/astro.test.js | 25 +++ .../underscore-redirects/test/print.test.js | 44 +++++ .../underscore-redirects/test/weight.test.js | 32 ++++ packages/underscore-redirects/tsconfig.json | 10 ++ pnpm-lock.yaml | 30 +++- 18 files changed, 492 insertions(+), 167 deletions(-) create mode 100644 packages/underscore-redirects/package.json create mode 100644 packages/underscore-redirects/readme.md create mode 100644 packages/underscore-redirects/src/astro.ts create mode 100644 packages/underscore-redirects/src/index.ts create mode 100644 packages/underscore-redirects/src/print.ts create mode 100644 packages/underscore-redirects/src/redirects.ts create mode 100644 packages/underscore-redirects/test/astro.test.js create mode 100644 packages/underscore-redirects/test/print.test.js create mode 100644 packages/underscore-redirects/test/weight.test.js create mode 100644 packages/underscore-redirects/tsconfig.json diff --git a/packages/integrations/cloudflare/package.json b/packages/integrations/cloudflare/package.json index 2c298a270a76..cd8bd1dec88b 100644 --- a/packages/integrations/cloudflare/package.json +++ b/packages/integrations/cloudflare/package.json @@ -38,6 +38,7 @@ "test": "mocha --exit --timeout 30000 test/" }, "dependencies": { + "@astrojs/underscore-redirects": "^0.1.0", "esbuild": "^0.17.12", "tiny-glob": "^0.2.9" }, diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index 2f6b36e8718a..0b8b5f415b0a 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -1,4 +1,5 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; +import { createRedirectsFromAstroRoutes, type Redirects } from '@astrojs/underscore-redirects'; import esbuild from 'esbuild'; import * as fs from 'fs'; import * as os from 'os'; @@ -88,7 +89,7 @@ export default function createIntegration(args?: Options): AstroIntegration { vite.ssr.target = 'webworker'; } }, - 'astro:build:done': async ({ pages }) => { + 'astro:build:done': async ({ pages, routes, dir }) => { const entryPath = fileURLToPath(new URL(_buildConfig.serverEntry, _buildConfig.server)); const entryUrl = new URL(_buildConfig.serverEntry, _config.outDir); const buildPath = fileURLToPath(entryUrl); @@ -197,6 +198,19 @@ export default function createIntegration(args?: Options): AstroIntegration { } } + const redirectRoutes = routes.filter(r => r.type === 'redirect'); + const trueRedirects = createRedirectsFromAstroRoutes({ + config: _config, + routes: redirectRoutes, + dir, + }); + if(!trueRedirects.empty()) { + await fs.promises.appendFile( + new URL('./_redirects', _config.outDir), + trueRedirects.print() + ); + } + await fs.promises.writeFile( new URL('./_routes.json', _config.outDir), JSON.stringify( diff --git a/packages/integrations/cloudflare/test/directory.test.js b/packages/integrations/cloudflare/test/directory.test.js index 67693310a7e0..2217ad1ddde6 100644 --- a/packages/integrations/cloudflare/test/directory.test.js +++ b/packages/integrations/cloudflare/test/directory.test.js @@ -11,6 +11,9 @@ describe('mode: "directory"', () => { root: './fixtures/basics/', output: 'server', adapter: cloudflare({ mode: 'directory' }), + redirects: { + '/old': '/' + } }); await fixture.build(); }); @@ -19,4 +22,16 @@ describe('mode: "directory"', () => { expect(await fixture.pathExists('../functions')).to.be.true; expect(await fixture.pathExists('../functions/[[path]].js')).to.be.true; }); + + it('generates a redirects file', async () => { + try { + let _redirects = await fixture.readFile('/_redirects'); + let parts = _redirects.split(/\s+/); + expect(parts).to.deep.equal([ + '/old', '/', '301' + ]); + } catch { + expect(false).to.equal(true); + } + }); }); diff --git a/packages/integrations/netlify/package.json b/packages/integrations/netlify/package.json index c2d908812e9e..fd837865c1ef 100644 --- a/packages/integrations/netlify/package.json +++ b/packages/integrations/netlify/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@astrojs/webapi": "^2.1.1", + "@astrojs/underscore-redirects": "^0.1.0", "@netlify/functions": "^1.0.0", "esbuild": "^0.15.18" }, diff --git a/packages/integrations/netlify/src/shared.ts b/packages/integrations/netlify/src/shared.ts index 4d1f890afe50..d452ada10252 100644 --- a/packages/integrations/netlify/src/shared.ts +++ b/packages/integrations/netlify/src/shared.ts @@ -1,20 +1,6 @@ -import type { AstroConfig, RouteData, ValidRedirectStatus } from 'astro'; -import fs from 'fs'; - -export type RedirectDefinition = { - dynamic: boolean; - input: string; - target: string; - weight: 0 | 1; - status: 200 | 404 | ValidRedirectStatus; -}; - -function getRedirectStatus(route: RouteData): ValidRedirectStatus { - if(typeof route.redirect === 'object') { - return route.redirect.status; - } - return 301; -} +import type { AstroConfig, RouteData } from 'astro'; +import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects'; +import fs from 'node:fs'; export async function createRedirects( config: AstroConfig, @@ -23,151 +9,17 @@ export async function createRedirects( entryFile: string, type: 'functions' | 'edge-functions' | 'builders' | 'static' ) { - const _redirectsURL = new URL('./_redirects', dir); const kind = type ?? 'functions'; + const dynamicTarget = `/.netlify/${kind}/${entryFile}`; + const _redirectsURL = new URL('./_redirects', dir); - const definitions: RedirectDefinition[] = []; - - for (const route of routes) { - if (route.pathname) { - if(route.redirect) { - definitions.push({ - dynamic: false, - input: route.pathname, - target: typeof route.redirect === 'object' ? route.redirect.destination : route.redirect, - status: getRedirectStatus(route), - weight: 1 - }); - continue; - } - - if(kind === 'static') { - continue; - } - else if (route.distURL) { - definitions.push({ - dynamic: false, - input: route.pathname, - target: prependForwardSlash(route.distURL.toString().replace(dir.toString(), '')), - status: 200, - weight: 1, - }); - } else { - definitions.push({ - dynamic: false, - input: route.pathname, - target: `/.netlify/${kind}/${entryFile}`, - status: 200, - weight: 1, - }); - - if (route.route === '/404') { - definitions.push({ - dynamic: true, - input: '/*', - target: `/.netlify/${kind}/${entryFile}`, - status: 404, - weight: 0, - }); - } - } - } else { - const pattern = generateDynamicPattern(route); - - if (route.distURL) { - const targetRoute = route.redirectRoute ?? route; - const targetPattern = generateDynamicPattern(targetRoute); - const target = - `${targetPattern}` + (config.build.format === 'directory' ? '/index.html' : '.html'); - definitions.push({ - dynamic: true, - input: pattern, - target, - status: route.type === 'redirect' ? 301 : 200, - weight: 1, - }); - } else { - definitions.push({ - dynamic: true, - input: pattern, - target: `/.netlify/${kind}/${entryFile}`, - status: 200, - weight: 1, - }); - } - } - } - - let _redirects = prettify(definitions); + const _redirects = createRedirectsFromAstroRoutes({ + config, routes, dir, dynamicTarget + }); + const content = _redirects.print(); // Always use appendFile() because the redirects file could already exist, // e.g. due to a `/public/_redirects` file that got copied to the output dir. // If the file does not exist yet, appendFile() automatically creates it. - await fs.promises.appendFile(_redirectsURL, _redirects, 'utf-8'); -} - -function generateDynamicPattern(route: RouteData) { - const pattern = - '/' + - route.segments - .map(([part]) => { - //(part.dynamic ? '*' : part.content) - if (part.dynamic) { - if (part.spread) { - return '*'; - } else { - return ':' + part.content; - } - } else { - return part.content; - } - }) - .join('/'); - return pattern; -} - -function prettify(definitions: RedirectDefinition[]) { - let minInputLength = 4, - minTargetLength = 4; - definitions.sort((a, b) => { - // Find the longest input, so we can format things nicely - if (a.input.length > minInputLength) { - minInputLength = a.input.length; - } - if (b.input.length > minInputLength) { - minInputLength = b.input.length; - } - - // Same for the target - if (a.target.length > minTargetLength) { - minTargetLength = a.target.length; - } - if (b.target.length > minTargetLength) { - minTargetLength = b.target.length; - } - - // Sort dynamic routes on top - return b.weight - a.weight; - }); - - let _redirects = ''; - // Loop over the definitions - definitions.forEach((defn, i) => { - // Figure out the number of spaces to add. We want at least 4 spaces - // after the input. This ensure that all targets line up together. - let inputSpaces = minInputLength - defn.input.length + 4; - let targetSpaces = minTargetLength - defn.target.length + 4; - _redirects += - (i === 0 ? '' : '\n') + - defn.input + - ' '.repeat(inputSpaces) + - defn.target + - ' '.repeat(Math.abs(targetSpaces)) + - defn.status; - }); - return _redirects; -} - -function prependForwardSlash(str: string) { - return str[0] === '/' ? str : '/' + str; + await fs.promises.appendFile(_redirectsURL, content, 'utf-8'); } diff --git a/packages/integrations/netlify/test/functions/redirects.test.js b/packages/integrations/netlify/test/functions/redirects.test.js index 8de4cbc9b9b1..634843553646 100644 --- a/packages/integrations/netlify/test/functions/redirects.test.js +++ b/packages/integrations/netlify/test/functions/redirects.test.js @@ -28,12 +28,11 @@ describe('SSG - Redirects', () => { let redirects = await fixture.readFile('/_redirects'); let parts = redirects.split(/\s+/); expect(parts).to.deep.equal([ - '/other', '/', '301', - '/', '/.netlify/functions/entry', '200', - // This uses the dynamic Astro.redirect, so we don't know that it's a redirect // until runtime. This is correct! '/nope', '/.netlify/functions/entry', '200', + '/', '/.netlify/functions/entry', '200', + '/other', '/', '301', // A real route '/team/articles/*', '/.netlify/functions/entry', '200', diff --git a/packages/integrations/netlify/test/static/redirects.test.js b/packages/integrations/netlify/test/static/redirects.test.js index dcd904cab257..f6695f15b594 100644 --- a/packages/integrations/netlify/test/static/redirects.test.js +++ b/packages/integrations/netlify/test/static/redirects.test.js @@ -27,13 +27,14 @@ describe('SSG - Redirects', () => { it('Creates a redirects file', async () => { let redirects = await fixture.readFile('/_redirects'); + console.log(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' + '/other', '/', '301', + '/two', '/', '302', + '/team/articles/*', '/team/articles/*/index.html', '200', + '/blog/*', '/team/articles/*/index.html', '301', ]); }); }); diff --git a/packages/underscore-redirects/package.json b/packages/underscore-redirects/package.json new file mode 100644 index 000000000000..1c9643dde981 --- /dev/null +++ b/packages/underscore-redirects/package.json @@ -0,0 +1,42 @@ +{ + "name": "@astrojs/underscore-redirects", + "description": "Utilities to generate _redirects files in Astro projects", + "version": "0.1.0", + "type": "module", + "author": "withastro", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/withastro/astro.git", + "directory": "packages/underscore-redirects" + }, + "bugs": "https://github.com/withastro/astro/issues", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": "./dist/index.js" + }, + "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\"", + "test": "mocha --exit --timeout 20000" + }, + "devDependencies": { + "astro": "workspace:*", + "astro-scripts": "workspace:*", + "@types/chai": "^4.3.1", + "@types/mocha": "^9.1.1", + "chai": "^4.3.6", + "mocha": "^9.2.2" + }, + "keywords": [ + "astro", + "astro-component" + ] +} diff --git a/packages/underscore-redirects/readme.md b/packages/underscore-redirects/readme.md new file mode 100644 index 000000000000..8eb29603b00a --- /dev/null +++ b/packages/underscore-redirects/readme.md @@ -0,0 +1,3 @@ +# @astrojs/underscore-redirects + +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/underscore-redirects/src/astro.ts b/packages/underscore-redirects/src/astro.ts new file mode 100644 index 000000000000..db84bb6e7f3d --- /dev/null +++ b/packages/underscore-redirects/src/astro.ts @@ -0,0 +1,145 @@ +import type { AstroConfig, RouteData, ValidRedirectStatus } from 'astro'; +import { Redirects } from './redirects.js'; +import { posix } from 'node:path'; + +const pathJoin = posix.join; + +function getRedirectStatus(route: RouteData): ValidRedirectStatus { + if(typeof route.redirect === 'object') { + return route.redirect.status; + } + return 301; +} + +interface CreateRedirectsFromAstroRoutesParams { + config: Pick; + routes: RouteData[]; + dir: URL; + dynamicTarget?: string; +} + +/** + * Takes a set of routes and creates a Redirects object from them. + */ +export function createRedirectsFromAstroRoutes({ + config, + routes, + dir, + dynamicTarget = '', +}: CreateRedirectsFromAstroRoutesParams) { + const output = config.output; + const _redirects = new Redirects(); + + for (const route of routes) { + // A route with a `pathname` is as static route. + if (route.pathname) { + if(route.redirect) { + // A redirect route without dynamic parts. Get the redirect status + // from the user if provided. + _redirects.add({ + dynamic: false, + input: route.pathname, + target: typeof route.redirect === 'object' ? route.redirect.destination : route.redirect, + status: getRedirectStatus(route), + weight: 2 + }); + continue; + } + + // If this is a static build we don't want to add redirects to the HTML file. + if(output === 'static') { + continue; + } + + else if (route.distURL) { + _redirects.add({ + dynamic: false, + input: route.pathname, + target: prependForwardSlash(route.distURL.toString().replace(dir.toString(), '')), + status: 200, + weight: 2, + }); + } else { + _redirects.add({ + dynamic: false, + input: route.pathname, + target: dynamicTarget, + status: 200, + weight: 2, + }); + + if (route.route === '/404') { + _redirects.add({ + dynamic: true, + input: '/*', + target: dynamicTarget, + status: 404, + weight: 0, + }); + } + } + } else { + // This is the dynamic route code. This generates a pattern from a dynamic + // route formatted with *s in place of the Astro dynamic/spread syntax. + const pattern = generateDynamicPattern(route); + + // This route was prerendered and should be forwarded to the HTML file. + if (route.distURL) { + const targetRoute = route.redirectRoute ?? route; + const targetPattern = generateDynamicPattern(targetRoute); + let target = targetPattern; + if(config.build.format === 'directory') { + target = pathJoin(target, 'index.html'); + } else { + target += '.html'; + } + _redirects.add({ + dynamic: true, + input: pattern, + target, + status: route.type === 'redirect' ? 301 : 200, + weight: 1, + }); + } else { + _redirects.add({ + dynamic: true, + input: pattern, + target: dynamicTarget, + status: 200, + weight: 1, + }); + } + } + } + + return _redirects; +} + +/** + * Converts an Astro dynamic route into one formatted like: + * /team/articles/* + * With stars replacing spread and :id syntax replacing [id] + */ +function generateDynamicPattern(route: RouteData) { + const pattern = + '/' + + route.segments + .map(([part]) => { + //(part.dynamic ? '*' : part.content) + if (part.dynamic) { + if (part.spread) { + return '*'; + } else { + return ':' + part.content; + } + } else { + return part.content; + } + }) + .join('/'); + return pattern; +} + +function prependForwardSlash(str: string) { + return str[0] === '/' ? str : '/' + str; +} diff --git a/packages/underscore-redirects/src/index.ts b/packages/underscore-redirects/src/index.ts new file mode 100644 index 000000000000..07e240218a0a --- /dev/null +++ b/packages/underscore-redirects/src/index.ts @@ -0,0 +1,8 @@ +export { + Redirects, + type RedirectDefinition +} from './redirects.js'; + +export { + createRedirectsFromAstroRoutes +} from './astro.js'; diff --git a/packages/underscore-redirects/src/print.ts b/packages/underscore-redirects/src/print.ts new file mode 100644 index 000000000000..a0d9564b907a --- /dev/null +++ b/packages/underscore-redirects/src/print.ts @@ -0,0 +1,36 @@ +import type { RedirectDefinition } from './redirects'; + +/** + * Pretty print a list of definitions into the output format. Keeps + * things readable for humans. Ex: + * /nope / 301 + * /other / 301 + * /two / 302 + * /team/articles/* /team/articles/*\/index.html 200 + * /blog/* /team/articles/*\/index.html 301 + */ +export function print( + definitions: RedirectDefinition[], + minInputLength: number, + minTargetLength: number +) { + let _redirects = ''; + + // Loop over the definitions + for(let i = 0; i < definitions.length; i++) { + let definition = definitions[i]; + // Figure out the number of spaces to add. We want at least 4 spaces + // after the input. This ensure that all targets line up together. + let inputSpaces = minInputLength - definition.input.length + 4; + let targetSpaces = minTargetLength - definition.target.length + 4; + _redirects += + (i === 0 ? '' : '\n') + + definition.input + + ' '.repeat(inputSpaces) + + definition.target + + ' '.repeat(Math.abs(targetSpaces)) + + definition.status; + } + + return _redirects; +} diff --git a/packages/underscore-redirects/src/redirects.ts b/packages/underscore-redirects/src/redirects.ts new file mode 100644 index 000000000000..c46d41628251 --- /dev/null +++ b/packages/underscore-redirects/src/redirects.ts @@ -0,0 +1,69 @@ +import { print } from './print.js'; + +export type RedirectDefinition = { + dynamic: boolean; + input: string; + target: string; + // Allows specifying a weight to the definition. + // This allows insertion of definitions out of order but having + // a priority once inserted. + weight: number; + status: number; +}; + +export class Redirects { + public definitions: RedirectDefinition[] = []; + public minInputLength = 4; + public minTargetLength = 4; + + /** + * Adds a new definition by inserting it into the list of definitions + * prioritized by the given weight. This keeps higher priority definitions + * At the top of the list once printed. + */ + add(definition: RedirectDefinition) { + // Find the longest input, so we can format things nicely + if (definition.input.length > this.minInputLength) { + this.minInputLength = definition.input.length; + } + // Same for the target + if (definition.target.length > this.minTargetLength) { + this.minTargetLength = definition.target.length; + } + + binaryInsert(this.definitions, definition, (a, b) => { + return a.weight > b.weight; + }); + } + + print(): string { + return print(this.definitions, this.minInputLength, this.minTargetLength); + } + + empty(): boolean { + return this.definitions.length === 0; + } +} + +function binaryInsert(sorted: T[], item: T, comparator: (a: T, b: T) => boolean) { + if(sorted.length === 0) { + sorted.push(item); + return 0; + } + let low = 0, high = sorted.length - 1, mid = 0; + while (low <= high) { + mid = low + (high - low >> 1); + if(comparator(sorted[mid], item)) { + low = mid + 1; + } else { + high = mid -1; + } + } + + if(comparator(sorted[mid], item)) { + mid++; + } + + sorted.splice(mid, 0, item); + return mid; +} diff --git a/packages/underscore-redirects/test/astro.test.js b/packages/underscore-redirects/test/astro.test.js new file mode 100644 index 000000000000..15a8aa0584d3 --- /dev/null +++ b/packages/underscore-redirects/test/astro.test.js @@ -0,0 +1,25 @@ +import { createRedirectsFromAstroRoutes } from '../dist/index.js'; +import { expect } from 'chai'; + +describe('Astro', () => { + const serverConfig = { + output: 'server', + build: { format: 'directory' } + }; + + it('Creates a Redirects object from routes', () => { + const routes = [ + { pathname: '/', distURL: new URL('./index.html', import.meta.url), segments: [] }, + { pathname: '/one', distURL: new URL('./one/index.html', import.meta.url), segments: [] } + ]; + const dynamicTarget = './.adapter/dist/entry.mjs'; + const _redirects = createRedirectsFromAstroRoutes({ + config: serverConfig, + routes, + dir: new URL(import.meta.url), + dynamicTarget + }); + + expect(_redirects.definitions).to.have.a.lengthOf(2); + }); +}); diff --git a/packages/underscore-redirects/test/print.test.js b/packages/underscore-redirects/test/print.test.js new file mode 100644 index 000000000000..c04a8e9a9ebf --- /dev/null +++ b/packages/underscore-redirects/test/print.test.js @@ -0,0 +1,44 @@ +import { Redirects } from '../dist/index.js'; +import { expect } from 'chai'; + +describe('Printing', () => { + it('Formats long lines in a pretty way', () => { + const _redirects = new Redirects(); + _redirects.add({ + dynamic: false, + input: '/a', + target: '/b', + weight: 0, + status: 200 + }); + _redirects.add({ + dynamic: false, + input: '/some-pretty-long-input-line', + target: '/b', + weight: 0, + status: 200 + }); + let out = _redirects.print(); + + let [lineOne, lineTwo] = out.split('\n'); + + expect(lineOne.indexOf('/b')).to.equal(lineTwo.indexOf('/b'), 'destinations lined up'); + expect(lineOne.indexOf('200')).to.equal(lineTwo.indexOf('200'), 'statuses lined up'); + }); + + it('Properly prints dynamic routes', () => { + const _redirects = new Redirects(); + _redirects.add({ + dynamic: true, + input: '/pets/:cat', + target: '/pets/:cat/index.html', + status: 200, + weight: 1 + }); + let out = _redirects.print(); + let parts = out.split(/\s+/); + expect(parts).to.deep.equal([ + '/pets/:cat', '/pets/:cat/index.html', '200', + ]) + }); +}); diff --git a/packages/underscore-redirects/test/weight.test.js b/packages/underscore-redirects/test/weight.test.js new file mode 100644 index 000000000000..0c6014427e74 --- /dev/null +++ b/packages/underscore-redirects/test/weight.test.js @@ -0,0 +1,32 @@ +import { Redirects } from '../dist/index.js'; +import { expect } from 'chai'; + +describe('Weight', () => { + it('Puts higher weighted definitions on top', () => { + const _redirects = new Redirects(); + _redirects.add({ + dynamic: false, + input: '/a', + target: '/b', + weight: 0, + status: 200 + }); + _redirects.add({ + dynamic: false, + input: '/c', + target: '/d', + weight: 0, + status: 200 + }); + _redirects.add({ + dynamic: false, + input: '/e', + target: '/f', + weight: 1, + status: 200 + }); + const firstDefn = _redirects.definitions[0]; + expect(firstDefn.weight).to.equal(1); + expect(firstDefn.input).to.equal('/e'); + }); +}); diff --git a/packages/underscore-redirects/tsconfig.json b/packages/underscore-redirects/tsconfig.json new file mode 100644 index 000000000000..569016e9d844 --- /dev/null +++ b/packages/underscore-redirects/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 2d639e85d8cc..35bbdb696f6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3630,6 +3630,9 @@ importers: packages/integrations/cloudflare: dependencies: + '@astrojs/underscore-redirects': + specifier: ^0.1.0 + version: link:../../underscore-redirects esbuild: specifier: ^0.17.12 version: 0.17.12 @@ -4360,6 +4363,9 @@ importers: packages/integrations/netlify: dependencies: + '@astrojs/underscore-redirects': + specifier: ^0.1.0 + version: link:../../underscore-redirects '@astrojs/webapi': specifier: ^2.1.1 version: link:../../webapi @@ -5219,6 +5225,27 @@ importers: specifier: ^9.2.2 version: 9.2.2 + packages/underscore-redirects: + devDependencies: + '@types/chai': + specifier: ^4.3.1 + version: 4.3.3 + '@types/mocha': + specifier: ^9.1.1 + version: 9.1.1 + astro: + specifier: workspace:* + version: link:../astro + astro-scripts: + specifier: workspace:* + version: link:../../scripts + chai: + specifier: ^4.3.6 + version: 4.3.6 + mocha: + specifier: ^9.2.2 + version: 9.2.2 + packages/webapi: dependencies: undici: @@ -8783,11 +8810,12 @@ packages: /@types/chai-subset@1.3.3: resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: - '@types/chai': 4.3.3 + '@types/chai': 4.3.5 dev: false /@types/chai@4.3.3: resolution: {integrity: sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==} + dev: true /@types/chai@4.3.5: resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==} From ef9a456f25e353bc1987a14e39cf3cc078714be7 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 26 May 2023 06:49:10 -0400 Subject: [PATCH 16/40] Test that redirects can come from middleware (#7213) * Test that redirects can come from middleware * Allow non-promise returns for middleware --- packages/astro/src/@types/astro.ts | 2 +- .../test/fixtures/ssr-redirect/src/middleware.ts | 13 +++++++++++++ .../src/pages/middleware-redirect.astro | 10 ++++++++++ packages/astro/test/redirects.test.js | 11 ++++++++++- 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 packages/astro/test/fixtures/ssr-redirect/src/middleware.ts create mode 100644 packages/astro/test/fixtures/ssr-redirect/src/pages/middleware-redirect.astro diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 437cf4c385e7..fefdec2bfde2 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1805,7 +1805,7 @@ export type MiddlewareNext = () => Promise; export type MiddlewareHandler = ( context: APIContext, next: MiddlewareNext -) => Promise | Promise | void; +) => Promise | R | Promise | void; export type MiddlewareResponseHandler = MiddlewareHandler; export type MiddlewareEndpointHandler = MiddlewareHandler; diff --git a/packages/astro/test/fixtures/ssr-redirect/src/middleware.ts b/packages/astro/test/fixtures/ssr-redirect/src/middleware.ts new file mode 100644 index 000000000000..5f8243a43d09 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-redirect/src/middleware.ts @@ -0,0 +1,13 @@ +import { defineMiddleware } from 'astro/middleware'; + +export const onRequest = defineMiddleware(({ request }, next) => { + if(new URL(request.url).pathname === '/middleware-redirect/') { + return new Response(null, { + status: 301, + headers: { + 'Location': '/' + } + }); + } + return next(); +}); diff --git a/packages/astro/test/fixtures/ssr-redirect/src/pages/middleware-redirect.astro b/packages/astro/test/fixtures/ssr-redirect/src/pages/middleware-redirect.astro new file mode 100644 index 000000000000..04f62cd89d45 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-redirect/src/pages/middleware-redirect.astro @@ -0,0 +1,10 @@ +--- +--- + + + This page should have been redirected + + +

This page should have been redirected

+ + diff --git a/packages/astro/test/redirects.test.js b/packages/astro/test/redirects.test.js index fde343bcf7ea..df041baa2b07 100644 --- a/packages/astro/test/redirects.test.js +++ b/packages/astro/test/redirects.test.js @@ -14,7 +14,7 @@ describe('Astro.redirect', () => { adapter: testAdapter(), redirects: { '/api/redirect': '/' - } + }, }); await fixture.build(); }); @@ -67,6 +67,9 @@ describe('Astro.redirect', () => { fixture = await loadFixture({ root: './fixtures/ssr-redirect/', output: 'static', + experimental: { + middleware: true + }, redirects: { '/one': '/', '/two': '/', @@ -109,5 +112,11 @@ describe('Astro.redirect', () => { expect(html).to.include('http-equiv="refresh'); expect(html).to.include('url=/articles/two'); }); + + it('Generates redirect pages for redirects created by middleware', async () => { + let html = await fixture.readFile('/middleware-redirect/index.html'); + expect(html).to.include('http-equiv="refresh'); + expect(html).to.include('url=/'); + }); }); }); From fa03a41a7a9cad785049795cd93b42dcea48e725 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 30 May 2023 08:31:00 -0400 Subject: [PATCH 17/40] Implement priority (#7210) --- .../astro/src/core/routing/manifest/create.ts | 63 ++++++++++++++++++- .../astro/test/units/routing/manifest.test.js | 28 +++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index 81ff18634909..60090b498a2a 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -62,6 +62,10 @@ function getParts(part: string, file: string) { return result; } +function areSamePart(a: RoutePart, b: RoutePart) { + return a.content === b.content && a.dynamic === b.dynamic && a.spread === b.spread; +} + function getPattern( segments: RoutePart[][], base: string, @@ -204,6 +208,49 @@ function injectedRouteToItem( }; } +// Seeings if the two routes are siblings of each other, with `b` being the route +// in focus. If it is in the same parent folder as `a`, they are siblings. +function areSiblings(a: RouteData, b: RouteData) { + if(a.segments.length < b.segments.length) return false; + for(let i = 0; i < b.segments.length - 1; i++) { + let segment = b.segments[i]; + if(segment.length === a.segments[i].length) { + for(let j = 0; j < segment.length; j++) { + if(!areSamePart(segment[j], a.segments[i][j])) { + return false; + } + } + } else { + return false; + } + } + return true; +} + +// A fast insertion method based on binary search. +function binaryInsert(sorted: T[], item: T, insertComparator: (a: T, item: T) => boolean) { + if(sorted.length === 0) { + sorted.push(item); + return 0; + } + let low = 0, high = sorted.length - 1, mid = 0; + while (low <= high) { + mid = low + (high - low >> 1); + if(insertComparator(sorted[mid], item)) { + low = mid + 1; + } else { + high = mid -1; + } + } + + if(insertComparator(sorted[mid], item)) { + mid++; + } + + sorted.splice(mid, 0, item); + return mid; +} + export interface CreateRouteManifestParams { /** Astro Settings object */ settings: AstroSettings; @@ -444,9 +491,7 @@ export function createRouteManifest( .map(([{ dynamic, content }]) => (dynamic ? `[${content}]` : content)) .join('/')}`.toLowerCase(); - - - routes.unshift({ + const routeData: RouteData = { type: 'redirect', route, pattern, @@ -458,6 +503,17 @@ export function createRouteManifest( prerender: false, redirect: to, redirectRoute: routes.find(r => r.route === to) + }; + const isSpreadRoute = isSpread(route); + + binaryInsert(routes, routeData, (a, item) => { + // If the routes are siblings and the redirect route is a spread + // Then it should come after the sibling unless it is also a spread. + // This essentially means that redirects are prioritized when *exactly* the same. + if(isSpreadRoute && areSiblings(a, item)) { + return !isSpread(a.route); + } + return true; }); }); @@ -465,3 +521,4 @@ export function createRouteManifest( routes, }; } + diff --git a/packages/astro/test/units/routing/manifest.test.js b/packages/astro/test/units/routing/manifest.test.js index 05789d7526ff..5d7613e528f5 100644 --- a/packages/astro/test/units/routing/manifest.test.js +++ b/packages/astro/test/units/routing/manifest.test.js @@ -31,4 +31,32 @@ describe('routing - createRouteManifest', () => { expect(pattern.test('')).to.equal(true); expect(pattern.test('/')).to.equal(false); }); + + it('redirects are sorted alongside the filesystem routes', async () => { + const fs = createFs( + { + '/src/pages/index.astro': `

test

`, + '/src/pages/blog/contributing.astro': `

test

`, + }, + root + ); + const settings = await createDefaultDevSettings( + { + base: '/search', + trailingSlash: 'never', + redirects: { + '/blog/[...slug]': '/' + } + }, + root + ); + const manifest = createRouteManifest({ + cwd: fileURLToPath(root), + settings, + fsMod: fs, + }); + + expect(manifest.routes[1].route).to.equal('/blog/contributing'); + expect(manifest.routes[2].route).to.equal('/blog/[...slug]'); + }) }); From eb7617d719e11f11d8b9fe6b5e83ff36608e253c Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 30 May 2023 17:41:24 -0400 Subject: [PATCH 18/40] Refactor --- packages/astro/src/core/app/index.ts | 2 +- packages/astro/src/core/build/generate.ts | 18 +++--------------- .../astro/src/core/build/plugins/plugin-ssr.ts | 4 ++-- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 0d1b0c680679..5a849153de4a 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -177,7 +177,7 @@ export class App { } else { const importComponentInstance = this.#manifest.pageMap.get(route.component); if(!importComponentInstance) { - throw new Error(`Unexpected unable to find a component instance for route ${route.route}`); + throw new Error(`Unexpectedly unable to find a component instance for route ${route.route}`); } const built = await importComponentInstance(); return built.page(); diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index ff89db6410c9..6f22d5914db2 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -150,12 +150,8 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn } } for(const pageData of eachRedirectPageData(internals)) { - // TODO MOVE - await generatePage(opts, internals, pageData, { - page: () => Promise.resolve(RedirectComponentInstance), - middleware: StaticMiddlewareInstance, - renderers: [] - }, builtPaths); + const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder); + await generatePage(opts, internals, pageData, entry, builtPaths); } } else { for (const [pageData, filePath] of eachPageDataFromEntryPoint(internals)) { @@ -166,15 +162,7 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn } for(const pageData of eachRedirectPageData(internals)) { const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder); - if(pageData.route.redirectRoute) { - const filePath = getEntryFilePathFromComponentPath(internals, pageData.route.redirectRoute.component); - } - - await generatePage(opts, internals, pageData, { - page: () => Promise.resolve(RedirectComponentInstance), - middleware: StaticMiddlewareInstance, - renderers: [] - }, builtPaths); + await generatePage(opts, internals, pageData, entry, builtPaths); } } diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 28265530ccd2..50c08c642570 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -15,7 +15,7 @@ import type { AstroBuildPlugin } from '../plugin'; import type { StaticBuildOptions } from '../types'; import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js'; import { routeIsRedirect } from '../../redirects/index.js'; -import { getVirtualModulePageIdFromPath } from './plugin-pages.js'; +import { getVirtualModulePageNameFromPath } from './plugin-pages.js'; import { RENDERERS_MODULE_ID } from './plugin-renderers.js'; export const SSR_VIRTUAL_MODULE_ID = '@astrojs-ssr-virtual-entry'; @@ -60,7 +60,7 @@ function vitePluginSSR( if(routeIsRedirect(pageData.route)) { continue; } - const virtualModuleName = getVirtualModulePageIdFromPath(path); + const virtualModuleName = getVirtualModulePageNameFromPath(path); let module = await this.resolve(virtualModuleName); if (module) { const variable = `_page${i}`; From d7d0b22e966aea7770a729fcddc901f8a9401db0 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 30 May 2023 17:49:22 -0400 Subject: [PATCH 19/40] Fix netlify test ordering --- .../integrations/netlify/test/static/redirects.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/integrations/netlify/test/static/redirects.test.js b/packages/integrations/netlify/test/static/redirects.test.js index f6695f15b594..5e3ee6cbfd68 100644 --- a/packages/integrations/netlify/test/static/redirects.test.js +++ b/packages/integrations/netlify/test/static/redirects.test.js @@ -27,14 +27,14 @@ describe('SSG - Redirects', () => { it('Creates a redirects file', async () => { let redirects = await fixture.readFile('/_redirects'); - console.log(redirects) let parts = redirects.split(/\s+/); expect(parts).to.deep.equal([ - '/nope', '/', '301', - '/other', '/', '301', '/two', '/', '302', - '/team/articles/*', '/team/articles/*/index.html', '200', + '/other', '/', '301', + '/nope', '/', '301', + '/blog/*', '/team/articles/*/index.html', '301', + '/team/articles/*', '/team/articles/*/index.html', '200', ]); }); }); From a39eb51d4c207ed1d80e9767156299d2782dc7f5 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 30 May 2023 18:04:21 -0400 Subject: [PATCH 20/40] Fix ordering again --- packages/integrations/netlify/test/functions/redirects.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/netlify/test/functions/redirects.test.js b/packages/integrations/netlify/test/functions/redirects.test.js index 634843553646..198534ad4836 100644 --- a/packages/integrations/netlify/test/functions/redirects.test.js +++ b/packages/integrations/netlify/test/functions/redirects.test.js @@ -28,11 +28,11 @@ describe('SSG - Redirects', () => { let redirects = await fixture.readFile('/_redirects'); let parts = redirects.split(/\s+/); expect(parts).to.deep.equal([ + '/other', '/', '301', // This uses the dynamic Astro.redirect, so we don't know that it's a redirect // until runtime. This is correct! '/nope', '/.netlify/functions/entry', '200', '/', '/.netlify/functions/entry', '200', - '/other', '/', '301', // A real route '/team/articles/*', '/.netlify/functions/entry', '200', From d3895a2d71fc4ccc8f8377682fa8a5fee4b90237 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 30 May 2023 18:18:40 -0400 Subject: [PATCH 21/40] Redirects: Allow preventing the output of the static HTML file (#7245) --- packages/astro/src/@types/astro.ts | 22 ++++++++++++++++ packages/astro/src/core/build/generate.ts | 4 +++ packages/astro/src/core/config/schema.ts | 3 +++ packages/astro/test/redirects.test.js | 25 +++++++++++++++++++ packages/integrations/cloudflare/src/index.ts | 1 + .../netlify/src/integration-static.ts | 8 ++++++ .../integrations/vercel/src/static/adapter.ts | 1 + 7 files changed, 64 insertions(+) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index bb37c4989e01..f4988e043e6c 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -738,6 +738,28 @@ export interface AstroUserConfig { * ``` */ serverEntry?: string; + /** + * @docs + * @name build.redirects + * @type {boolean} + * @default `true` + * @description + * Specifies whether redirects will be output to HTML during the build. + * This option only applies to `output: 'static'` mode; in SSR redirects + * are treated the same as all responses. + * + * This option is mostly meant to be used by adapters that have special + * configuration files for redirects and do not need/want HTML based redirects. + * + * ```js + * { + * build: { + * redirects: false + * } + * } + * ``` + */ + redirects?: boolean; }; /** diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 6f22d5914db2..21da35d5b850 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -564,6 +564,10 @@ async function generatePath( switch(true) { case (response.status >= 300 && response.status < 400): { + // If redirects is set to false, don't output the HTML + if(!opts.settings.config.build.redirects) { + return; + } const location = getRedirectLocationOrThrow(response.headers); body = ` Redirecting to: ${location} diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 4573d88107f4..a6badbe2f19c 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -22,6 +22,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = { server: './dist/server/', assets: '_astro', serverEntry: 'entry.mjs', + redirects: true, }, compressHTML: false, server: { @@ -116,6 +117,7 @@ export const AstroConfigSchema = z.object({ assets: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.assets), assetsPrefix: z.string().optional(), serverEntry: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.serverEntry), + redirects: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.redirects), }) .optional() .default({}), @@ -279,6 +281,7 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) { assets: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.assets), assetsPrefix: z.string().optional(), serverEntry: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.serverEntry), + redirects: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.redirects), }) .optional() .default({}), diff --git a/packages/astro/test/redirects.test.js b/packages/astro/test/redirects.test.js index df041baa2b07..05357b449abf 100644 --- a/packages/astro/test/redirects.test.js +++ b/packages/astro/test/redirects.test.js @@ -119,4 +119,29 @@ describe('Astro.redirect', () => { expect(html).to.include('url=/'); }); }); + + describe('config.build.redirects = false', () => { + before(async () => { + process.env.STATIC_MODE = true; + fixture = await loadFixture({ + root: './fixtures/ssr-redirect/', + output: 'static', + redirects: { + '/one': '/' + }, + build: { + redirects: false + } + }); + await fixture.build(); + }); + + it('Does not output redirect HTML', async () => { + let oneHtml = undefined; + try { + oneHtml = await fixture.readFile('/one/index.html'); + } catch {} + expect(oneHtml).be.an('undefined'); + }) + }) }); diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index 0b8b5f415b0a..ca755432ee8b 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -51,6 +51,7 @@ export default function createIntegration(args?: Options): AstroIntegration { client: new URL(`.${config.base}`, config.outDir), server: new URL(`.${SERVER_BUILD_FOLDER}`, config.outDir), serverEntry: '_worker.mjs', + redirects: false, }, }); }, diff --git a/packages/integrations/netlify/src/integration-static.ts b/packages/integrations/netlify/src/integration-static.ts index bb989b532f35..8814f9d2af4d 100644 --- a/packages/integrations/netlify/src/integration-static.ts +++ b/packages/integrations/netlify/src/integration-static.ts @@ -7,6 +7,14 @@ export function netlifyStatic(): AstroIntegration { return { name: '@astrojs/netlify', hooks: { + 'astro:config:setup': ({ updateConfig }) => { + updateConfig({ + build: { + // Do not output HTML redirects because we are building a `_redirects` file. + redirects: false, + }, + }); + }, 'astro:config:done': ({ config }) => { _config = config; }, diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts index 0b3579cdd11d..e0cc14322fcb 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, From 2700c125c9bfebbd2c723451ece473944fdda56f Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 30 May 2023 18:33:43 -0400 Subject: [PATCH 22/40] Do a simple push for priority --- .../astro/src/core/routing/manifest/create.ts | 38 ++----------------- .../astro/test/units/routing/manifest.test.js | 4 +- 2 files changed, 6 insertions(+), 36 deletions(-) diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index 7ad6fd6d49de..a193621299ac 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -228,30 +228,6 @@ function areSiblings(a: RouteData, b: RouteData) { return true; } -// A fast insertion method based on binary search. -function binaryInsert(sorted: T[], item: T, insertComparator: (a: T, item: T) => boolean) { - if(sorted.length === 0) { - sorted.push(item); - return 0; - } - let low = 0, high = sorted.length - 1, mid = 0; - while (low <= high) { - mid = low + (high - low >> 1); - if(insertComparator(sorted[mid], item)) { - low = mid + 1; - } else { - high = mid -1; - } - } - - if(insertComparator(sorted[mid], item)) { - mid++; - } - - sorted.splice(mid, 0, item); - return mid; -} - export interface CreateRouteManifestParams { /** Astro Settings object */ settings: AstroSettings; @@ -505,17 +481,9 @@ export function createRouteManifest( redirect: to, redirectRoute: routes.find(r => r.route === to) }; - const isSpreadRoute = isSpread(route); - - binaryInsert(routes, routeData, (a, item) => { - // If the routes are siblings and the redirect route is a spread - // Then it should come after the sibling unless it is also a spread. - // This essentially means that redirects are prioritized when *exactly* the same. - if(isSpreadRoute && areSiblings(a, item)) { - return !isSpread(a.route); - } - return true; - }); + + // Push so that redirects are selected last. + routes.push(routeData); }); return { diff --git a/packages/astro/test/units/routing/manifest.test.js b/packages/astro/test/units/routing/manifest.test.js index 5d7613e528f5..4a8a96175d99 100644 --- a/packages/astro/test/units/routing/manifest.test.js +++ b/packages/astro/test/units/routing/manifest.test.js @@ -45,7 +45,8 @@ describe('routing - createRouteManifest', () => { base: '/search', trailingSlash: 'never', redirects: { - '/blog/[...slug]': '/' + '/blog/[...slug]': '/', + '/blog/contributing': '/another', } }, root @@ -57,6 +58,7 @@ describe('routing - createRouteManifest', () => { }); expect(manifest.routes[1].route).to.equal('/blog/contributing'); + expect(manifest.routes[1].type).to.equal('page'); expect(manifest.routes[2].route).to.equal('/blog/[...slug]'); }) }); From 60dcfb6d33c2afcfb0d63193c67c4e7fb2ecf336 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 30 May 2023 18:41:19 -0400 Subject: [PATCH 23/40] Adding changesets --- .changeset/chatty-actors-stare.md | 31 +++++++++++++++++++++++++++++-- .changeset/fuzzy-ladybugs-jump.md | 7 +++++++ .changeset/hip-news-clean.md | 7 +++++++ .changeset/twenty-suns-vanish.md | 7 +++++++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 .changeset/fuzzy-ladybugs-jump.md create mode 100644 .changeset/hip-news-clean.md create mode 100644 .changeset/twenty-suns-vanish.md diff --git a/.changeset/chatty-actors-stare.md b/.changeset/chatty-actors-stare.md index a162719968dd..cb8e0c31ac05 100644 --- a/.changeset/chatty-actors-stare.md +++ b/.changeset/chatty-actors-stare.md @@ -1,6 +1,33 @@ --- -'@astrojs/netlify': minor 'astro': minor --- -Implements the redirects proposal +Experimental redirects support + +This change adds support for the redirects RFC, currently in stage 3: https://github.com/withastro/roadmap/pull/587 + +Now you can specify redirects in your Astro config: + +```js +import { defineConfig } from 'astro/config'; + +export defineConfig({ + redirects: { + '/blog/old-post': '/blog/new-post' + } +}); +``` + +You can also specify spread routes using the same syntax as in file-based routing: + +```js +import { defineConfig } from 'astro/config'; + +export defineConfig({ + redirects: { + '/blog/[...slug]': '/articles/[...slog]' + } +}); +``` + +By default Astro will build HTML files that contain the `` tag. Adapters can also support redirect routes and create configuration for real HTTP-level redirects in production. diff --git a/.changeset/fuzzy-ladybugs-jump.md b/.changeset/fuzzy-ladybugs-jump.md new file mode 100644 index 000000000000..fecabbeaca53 --- /dev/null +++ b/.changeset/fuzzy-ladybugs-jump.md @@ -0,0 +1,7 @@ +--- +'@astrojs/cloudflare': minor +--- + +Support for experimental redirects + +This adds support for the redirects RFC in the Cloudflare adapter. No changes are necessary, simply use configured redirects and the adapter will update your `_redirects` file. diff --git a/.changeset/hip-news-clean.md b/.changeset/hip-news-clean.md new file mode 100644 index 000000000000..2b0dc1db10aa --- /dev/null +++ b/.changeset/hip-news-clean.md @@ -0,0 +1,7 @@ +--- +'@astrojs/vercel': minor +--- + +Support for experimental redirects + +This adds support for the redirects RFC in the Vercel adapter. No changes are necessary, simply use configured redirects and the adapter will output the vercel.json file with the configuration values. diff --git a/.changeset/twenty-suns-vanish.md b/.changeset/twenty-suns-vanish.md new file mode 100644 index 000000000000..1471d7af4ada --- /dev/null +++ b/.changeset/twenty-suns-vanish.md @@ -0,0 +1,7 @@ +--- +'@astrojs/netlify': minor +--- + +Support for experimental redirects + +This adds support for the redirects RFC in the Netlify adapter. No changes are necessary, simply use configured redirects and the adapter will update your `_redirects` file. From c04027948973b5751ace7e02737050f28e75e217 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 30 May 2023 20:08:42 -0400 Subject: [PATCH 24/40] Put the implementation behind a flag. --- packages/astro/src/@types/astro.ts | 42 ++++++++++++++++++- packages/astro/src/core/build/generate.ts | 8 +++- packages/astro/src/core/config/config.ts | 2 + packages/astro/src/core/config/schema.ts | 2 + .../src/vite-plugin-astro-server/route.ts | 7 ++++ packages/astro/test/redirects.test.js | 11 ++++- .../cloudflare/test/directory.test.js | 5 ++- .../netlify/test/functions/redirects.test.js | 5 ++- .../netlify/test/static/redirects.test.js | 3 ++ .../vercel/test/redirects.test.js | 5 ++- 10 files changed, 81 insertions(+), 9 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index f4988e043e6c..d39a52223048 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -109,6 +109,7 @@ export interface CLIFlags { open?: boolean; experimentalAssets?: boolean; experimentalMiddleware?: boolean; + experimentalRedirects?: boolean; } export interface BuildConfig { @@ -452,10 +453,26 @@ export interface AstroUserConfig { */ cacheDir?: string; + + /** - * TODO + * @docs + * @name redirects + * @type {RedirectConfig} + * @default `{}` + * @description Specify a mapping of redirects where the key is the route to match + * and the value is the path to redirect to. Either of these an also be a dynamic route + * following the same convention as in file-based routes. + * + * ```js + * { + * redirects: { + * '/old': '/new' + * } + * } + * ``` */ - redirects?: Record; + redirects?: RedirectConfig; /** * @docs @@ -1206,6 +1223,27 @@ export interface AstroUserConfig { * ``` */ hybridOutput?: boolean; + + /** + * @docs + * @name experimental.redirectgs + * @type {boolean} + * @default `false` + * @version 2.6.0 + * @description + * Enable experimental support for redirect configuration. With this enabled + * you can set redirects via the top-level redirects property. To enable + * this feature, set `experimental.redirects` to `true`. + * + * ```js + * { + * experimental: { + * redirects: true, + * }, + * } + * ``` + */ + redirects?: boolean; }; // Legacy options to be removed diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 21da35d5b850..6f56bfe58957 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -42,7 +42,7 @@ import { debug, info } from '../logger/core.js'; import { callMiddleware } from '../middleware/callMiddleware.js'; import { createEnvironment, createRenderContext, renderPage } from '../render/index.js'; import { callGetStaticPaths } from '../render/route-cache.js'; -import { getRedirectLocationOrThrow, RedirectComponentInstance } from '../redirects/index.js'; +import { getRedirectLocationOrThrow, RedirectComponentInstance, routeIsRedirect } from '../redirects/index.js'; import { createAssetLink, createModuleScriptsSet, @@ -155,7 +155,7 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn } } else { for (const [pageData, filePath] of eachPageDataFromEntryPoint(internals)) { - const ssrEntryURLPage =createEntryURL(filePath, outFolder); + const ssrEntryURLPage = createEntryURL(filePath, outFolder); const ssrEntryPage: SinglePageBuiltModule = await import(ssrEntryURLPage.toString()); await generatePage(opts, internals, pageData, ssrEntryPage, builtPaths); @@ -208,6 +208,10 @@ async function generatePage( ssrEntry: SinglePageBuiltModule, builtPaths: Set ) { + if(routeIsRedirect(pageData.route) &&!opts.settings.config.experimental.redirects) { + throw new Error(`To use redirects first set experimental.redirects to \`true\``); + } + let timeStart = performance.now(); const renderers = ssrEntry?.renderers; diff --git a/packages/astro/src/core/config/config.ts b/packages/astro/src/core/config/config.ts index 0ca13a220936..81ec93d9b395 100644 --- a/packages/astro/src/core/config/config.ts +++ b/packages/astro/src/core/config/config.ts @@ -106,6 +106,8 @@ export function resolveFlags(flags: Partial): CLIFlags { typeof flags.experimentalAssets === 'boolean' ? flags.experimentalAssets : undefined, experimentalMiddleware: typeof flags.experimentalMiddleware === 'boolean' ? flags.experimentalMiddleware : undefined, + experimentalRedirects: + typeof flags.experimentalRedirects === 'boolean' ? flags.experimentalRedirects : undefined }; } diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index a6badbe2f19c..cb92e297d47e 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -45,6 +45,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = { customClientDirectives: false, inlineStylesheets: 'never', middleware: false, + redirects: false, }, }; @@ -213,6 +214,7 @@ export const AstroConfigSchema = z.object({ .default(ASTRO_CONFIG_DEFAULTS.experimental.inlineStylesheets), middleware: z.oboolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.middleware), hybridOutput: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.hybridOutput), + redirects: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.redirects), }) .passthrough() .refine( diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index dae4162296f6..ecf9a0c35f26 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -129,6 +129,13 @@ export async function handleRoute( return handle404Response(origin, req, res); } + if(matchedRoute.route.type === 'redirect' && !settings.config.experimental.redirects) { + writeWebResponse(res, new Response(`To enable redirect set experimental.redirects to \`true\`.`, { + status: 400 + })); + return; + } + const { config } = settings; const filePath: URL | undefined = matchedRoute.filePath; const { route, preloadedComponent, mod } = matchedRoute; diff --git a/packages/astro/test/redirects.test.js b/packages/astro/test/redirects.test.js index 05357b449abf..d5b3e5663997 100644 --- a/packages/astro/test/redirects.test.js +++ b/packages/astro/test/redirects.test.js @@ -15,6 +15,9 @@ describe('Astro.redirect', () => { redirects: { '/api/redirect': '/' }, + experimental: { + redirects: true, + }, }); await fixture.build(); }); @@ -68,7 +71,8 @@ describe('Astro.redirect', () => { root: './fixtures/ssr-redirect/', output: 'static', experimental: { - middleware: true + middleware: true, + redirects: true, }, redirects: { '/one': '/', @@ -131,7 +135,10 @@ describe('Astro.redirect', () => { }, build: { redirects: false - } + }, + experimental: { + redirects: true, + }, }); await fixture.build(); }); diff --git a/packages/integrations/cloudflare/test/directory.test.js b/packages/integrations/cloudflare/test/directory.test.js index 2217ad1ddde6..e88019401dca 100644 --- a/packages/integrations/cloudflare/test/directory.test.js +++ b/packages/integrations/cloudflare/test/directory.test.js @@ -13,7 +13,10 @@ describe('mode: "directory"', () => { adapter: cloudflare({ mode: 'directory' }), redirects: { '/old': '/' - } + }, + experimental: { + redirects: true, + }, }); await fixture.build(); }); diff --git a/packages/integrations/netlify/test/functions/redirects.test.js b/packages/integrations/netlify/test/functions/redirects.test.js index 198534ad4836..8a6d36694894 100644 --- a/packages/integrations/netlify/test/functions/redirects.test.js +++ b/packages/integrations/netlify/test/functions/redirects.test.js @@ -19,7 +19,10 @@ describe('SSG - Redirects', () => { integrations: [testIntegration()], redirects: { '/other': '/' - } + }, + experimental: { + redirects: true, + }, }); await fixture.build(); }); diff --git a/packages/integrations/netlify/test/static/redirects.test.js b/packages/integrations/netlify/test/static/redirects.test.js index 5e3ee6cbfd68..0b153b31c0fd 100644 --- a/packages/integrations/netlify/test/static/redirects.test.js +++ b/packages/integrations/netlify/test/static/redirects.test.js @@ -11,6 +11,9 @@ describe('SSG - Redirects', () => { root: new URL('./fixtures/redirects/', import.meta.url).toString(), output: 'static', adapter: netlifyStatic(), + experimental: { + redirects: true, + }, site: `http://example.com`, integrations: [testIntegration()], redirects: { diff --git a/packages/integrations/vercel/test/redirects.test.js b/packages/integrations/vercel/test/redirects.test.js index 9cc04fb8cd4b..0d54589fc564 100644 --- a/packages/integrations/vercel/test/redirects.test.js +++ b/packages/integrations/vercel/test/redirects.test.js @@ -17,7 +17,10 @@ describe('Redirects', () => { destination: '/' }, '/blog/[...slug]': '/team/articles/[...slug]', - } + }, + experimental: { + redirects: true, + }, }); await fixture.build(); }); From 79263e343117f935830652a380c28b2fa66cdd0d Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 30 May 2023 20:11:18 -0400 Subject: [PATCH 25/40] Self review --- packages/astro/src/core/build/generate.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 6f56bfe58957..c7226118c886 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -213,7 +213,7 @@ async function generatePage( } let timeStart = performance.now(); - const renderers = ssrEntry?.renderers; + const renderers = ssrEntry.renderers; const pageInfo = getPageDataByComponent(internals, pageData.route.component); @@ -225,8 +225,8 @@ async function generatePage( .map(({ sheet }) => sheet) .reduce(mergeInlineCss, []); - let pageModulePromise = ssrEntry?.page; - const middleware = ssrEntry?.middleware; + const pageModulePromise = ssrEntry.page; + const middleware = ssrEntry.middleware; if (!pageModulePromise) { throw new Error( From 5198529d5bfb1ea0d0daea1d13f3d9d1ef7008fb Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Wed, 31 May 2023 08:53:22 -0400 Subject: [PATCH 26/40] Update .changeset/chatty-actors-stare.md Co-authored-by: Chris Swithinbank --- .changeset/chatty-actors-stare.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/chatty-actors-stare.md b/.changeset/chatty-actors-stare.md index cb8e0c31ac05..e8d42848a2b0 100644 --- a/.changeset/chatty-actors-stare.md +++ b/.changeset/chatty-actors-stare.md @@ -25,7 +25,7 @@ import { defineConfig } from 'astro/config'; export defineConfig({ redirects: { - '/blog/[...slug]': '/articles/[...slog]' + '/blog/[...slug]': '/articles/[...slug]' } }); ``` From 63b5cfab22f0e80cedf64ff768d7444056fc833b Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Wed, 31 May 2023 08:53:28 -0400 Subject: [PATCH 27/40] Update packages/astro/src/@types/astro.ts Co-authored-by: Chris Swithinbank --- packages/astro/src/@types/astro.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index d39a52223048..f3873cdb3751 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -460,6 +460,7 @@ export interface AstroUserConfig { * @name redirects * @type {RedirectConfig} * @default `{}` + * @version 2.6.0 * @description Specify a mapping of redirects where the key is the route to match * and the value is the path to redirect to. Either of these an also be a dynamic route * following the same convention as in file-based routes. From 17d05381592a07b62f4fff813f0ab800a5871e74 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Wed, 31 May 2023 08:53:35 -0400 Subject: [PATCH 28/40] Update packages/astro/src/@types/astro.ts Co-authored-by: Chris Swithinbank --- packages/astro/src/@types/astro.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index f3873cdb3751..2ea9b1ece6d3 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -761,6 +761,7 @@ export interface AstroUserConfig { * @name build.redirects * @type {boolean} * @default `true` + * @version 2.6.0 * @description * Specifies whether redirects will be output to HTML during the build. * This option only applies to `output: 'static'` mode; in SSR redirects From 7b64d65a635f8838212ce44212ef8271e92988c2 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Wed, 31 May 2023 08:53:49 -0400 Subject: [PATCH 29/40] Update packages/astro/src/@types/astro.ts Co-authored-by: Chris Swithinbank --- packages/astro/src/@types/astro.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 2ea9b1ece6d3..9792a182faba 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1228,7 +1228,7 @@ export interface AstroUserConfig { /** * @docs - * @name experimental.redirectgs + * @name experimental.redirects * @type {boolean} * @default `false` * @version 2.6.0 From cd8e703001949ef6ab5127f38cc1cd93098e97c9 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Wed, 31 May 2023 13:12:24 -0400 Subject: [PATCH 30/40] Update packages/astro/src/@types/astro.ts Co-authored-by: Chris Swithinbank --- packages/astro/src/@types/astro.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 9792a182faba..cb1e58ad81e1 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -462,16 +462,38 @@ export interface AstroUserConfig { * @default `{}` * @version 2.6.0 * @description Specify a mapping of redirects where the key is the route to match - * and the value is the path to redirect to. Either of these an also be a dynamic route + * and the value is the path to redirect to. Either of these can also be a dynamic route * following the same convention as in file-based routes. * * ```js * { * redirects: { - * '/old': '/new' + * '/old': '/new', + * '/blog/[...slug]': '/articles/[...slug]', * } * } * ``` + * + * Astro will serve redirected GET requests with a status of `301` + * and use a status of `308` for any other request method. + * You can customize the status code using an object in the redirect config: + * + * ```js + * { + * redirects: { + * '/other': { + * status: 302, + * destination: '/place', + * }, + * } + * } + * ``` + * + * Status codes are only set when using SSR or with a static adapter that + * supports it (Netlify, Cloudflare, and Vercel). + * Elsewhere, builds will fall back to a client redirect using a + * [`` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#http-equiv) + * and do not support status codes. */ redirects?: RedirectConfig; From 7fd2a0a677be296eb1cd9455b2544d525b9a8d2a Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Wed, 31 May 2023 13:58:20 -0400 Subject: [PATCH 31/40] Update docs on dynamic restrictions. --- packages/astro/src/@types/astro.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index cb1e58ad81e1..92d6abed69f8 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -457,14 +457,18 @@ export interface AstroUserConfig { /** * @docs - * @name redirects + * @name redirects (Experimental) * @type {RedirectConfig} * @default `{}` * @version 2.6.0 * @description Specify a mapping of redirects where the key is the route to match - * and the value is the path to redirect to. Either of these can also be a dynamic route + * and the value is the path to redirect to. These can also be dynamic routes, * following the same convention as in file-based routes. * + * > *Note*: If using dynamic routes, both *sides* of the declaration must + * have the same syntax. For example you cannot have a `'/article': '/blog/[...slug]'` redirect. + * + * * ```js * { * redirects: { From 42bfa65240dea94f834b0fe041997d2c103c1a9b Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 1 Jun 2023 09:53:01 -0400 Subject: [PATCH 32/40] Update packages/astro/src/@types/astro.ts Co-authored-by: Sarah Rainsberger --- packages/astro/src/@types/astro.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 92d6abed69f8..a5eb18a23e23 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -462,11 +462,10 @@ export interface AstroUserConfig { * @default `{}` * @version 2.6.0 * @description Specify a mapping of redirects where the key is the route to match - * and the value is the path to redirect to. These can also be dynamic routes, - * following the same convention as in file-based routes. - * - * > *Note*: If using dynamic routes, both *sides* of the declaration must - * have the same syntax. For example you cannot have a `'/article': '/blog/[...slug]'` redirect. + * and the value is the path to redirect to. + * + * You can redirect both static and dynamic routes, but only to the same kind of route. + * For example you cannot have a `'/article': '/blog/[...slug]'` redirect. * * * ```js From 4da0c7b62df8d629035bbdf9c4ef5f4d074112aa Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 1 Jun 2023 10:04:42 -0400 Subject: [PATCH 33/40] Update packages/astro/src/@types/astro.ts Co-authored-by: Sarah Rainsberger --- packages/astro/src/@types/astro.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index a5eb18a23e23..269b03a9e1c6 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1259,7 +1259,7 @@ export interface AstroUserConfig { * @version 2.6.0 * @description * Enable experimental support for redirect configuration. With this enabled - * you can set redirects via the top-level redirects property. To enable + * you can set redirects via the top-level `redirects` property. To enable * this feature, set `experimental.redirects` to `true`. * * ```js From 3eaf936b2edb4c8b905c3d1fde986529665deab5 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 1 Jun 2023 10:09:01 -0400 Subject: [PATCH 34/40] Code review changes --- packages/astro/src/core/build/generate.ts | 32 ++++++++++------------- packages/internal-helpers/src/path.ts | 7 ++++- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index c7226118c886..84355b341ff0 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -566,27 +566,23 @@ async function generatePath( throw err; } - switch(true) { - case (response.status >= 300 && response.status < 400): { - // If redirects is set to false, don't output the HTML - if(!opts.settings.config.build.redirects) { - return; - } - const location = getRedirectLocationOrThrow(response.headers); - body = ` + if(response.status >= 300 && response.status < 400) { + // If redirects is set to false, don't output the HTML + if(!opts.settings.config.build.redirects) { + return; + } + const location = getRedirectLocationOrThrow(response.headers); + body = ` Redirecting to: ${location} `; - // A dynamic redirect, set the location so that integrations know about it. - if(pageData.route.type !== 'redirect') { - pageData.route.redirect = location; - } - break; - } - default: { - // If there's no body, do nothing - if (!response.body) return; - body = await response.text(); + // A dynamic redirect, set the location so that integrations know about it. + if(pageData.route.type !== 'redirect') { + pageData.route.redirect = location; } + } else { + // If there's no body, do nothing + if (!response.body) return; + body = await response.text(); } } diff --git a/packages/internal-helpers/src/path.ts b/packages/internal-helpers/src/path.ts index cbf959f69f57..2f2a974c43f6 100644 --- a/packages/internal-helpers/src/path.ts +++ b/packages/internal-helpers/src/path.ts @@ -1,3 +1,8 @@ +/** + * A set of common path utilities commonly used through the Astro core and integration + * projects. These do things like ensure a forward slash prepends paths. + */ + export function appendExtension(path: string, extension: string) { return path + '.' + extension; } @@ -77,5 +82,5 @@ export function removeQueryString(path: string) { } export function isRemotePath(src: string) { - return /^(http|ftp|https):?\/\//.test(src) || src.startsWith('data:'); + return /^(http|ftp|https|ws):?\/\//.test(src) || src.startsWith('data:'); } From 7c0905bfecfd392b2426f7d95a641cbe7fc12ca5 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 1 Jun 2023 10:18:46 -0400 Subject: [PATCH 35/40] Document netlify static adapter --- packages/integrations/netlify/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/integrations/netlify/README.md b/packages/integrations/netlify/README.md index ec72f2a2cad6..ad9a6dcd70d8 100644 --- a/packages/integrations/netlify/README.md +++ b/packages/integrations/netlify/README.md @@ -74,6 +74,25 @@ export default defineConfig({ }); ``` +### Static sites + +For static sites you usually don't need an adapter. However, if you use `redirects` configuration in your Astro config, the Netlify adapter can be used to translate this to the proper `_redirects` format. + +```js +import { defineConfig } from 'astro/config'; +import netlify from '@astrojs/netlify/static'; + +export default defineConfig({ + adapter: netlify(), + + redirects: { + '/blog/old-post': '/blog/new-post' + } +}); +``` + +Once you run `astro build` there will be a `dist/_redirects` file. Netlify will use that to properly route pages in production. + ## Usage [Read the full deployment guide here.](https://docs.astro.build/en/guides/deploy/netlify/) From 63210fea91f59693b101a3963d5b4ef31bda8b33 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 1 Jun 2023 10:28:42 -0400 Subject: [PATCH 36/40] Update packages/astro/src/@types/astro.ts Co-authored-by: Sarah Rainsberger --- packages/astro/src/@types/astro.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 269b03a9e1c6..17b2071a672c 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -477,9 +477,15 @@ export interface AstroUserConfig { * } * ``` * + * + * For statically-generated sites with no adapter installed, this will produce a client redirect using a [`` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#http-equiv) and does not support status codes. + * + * When using SSR or with a `/static` adapter in `output: static` + * mode (Netlify, Cloudflare, and Vercel), status codes are supported. * Astro will serve redirected GET requests with a status of `301` * and use a status of `308` for any other request method. - * You can customize the status code using an object in the redirect config: + * + * You can customize the [redirection status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages) using an object in the redirect config: * * ```js * { @@ -491,12 +497,6 @@ export interface AstroUserConfig { * } * } * ``` - * - * Status codes are only set when using SSR or with a static adapter that - * supports it (Netlify, Cloudflare, and Vercel). - * Elsewhere, builds will fall back to a client redirect using a - * [`` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#http-equiv) - * and do not support status codes. */ redirects?: RedirectConfig; From 57e81054c13efb7b3cf43b2068f20553ff2c174d Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 1 Jun 2023 10:30:33 -0400 Subject: [PATCH 37/40] Slight reword --- packages/astro/src/@types/astro.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 17b2071a672c..43e0a09eed93 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -480,7 +480,7 @@ export interface AstroUserConfig { * * For statically-generated sites with no adapter installed, this will produce a client redirect using a [`` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#http-equiv) and does not support status codes. * - * When using SSR or with a `/static` adapter in `output: static` + * When using SSR or with a static adapter in `output: static` * mode (Netlify, Cloudflare, and Vercel), status codes are supported. * Astro will serve redirected GET requests with a status of `301` * and use a status of `308` for any other request method. From 131f9d24e405f6b09916253038c5b4dfdbfa88db Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 1 Jun 2023 10:46:45 -0400 Subject: [PATCH 38/40] Update .changeset/twenty-suns-vanish.md Co-authored-by: Sarah Rainsberger --- .changeset/twenty-suns-vanish.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/twenty-suns-vanish.md b/.changeset/twenty-suns-vanish.md index 1471d7af4ada..987876cc8200 100644 --- a/.changeset/twenty-suns-vanish.md +++ b/.changeset/twenty-suns-vanish.md @@ -4,4 +4,6 @@ Support for experimental redirects -This adds support for the redirects RFC in the Netlify adapter. No changes are necessary, simply use configured redirects and the adapter will update your `_redirects` file. +This adds support for the redirects RFC in the Netlify adapter, including a new `@astrojs/netlify/static` adapter for static sites. + +No changes are necessary when using SSR. Simply use configured redirects and the adapter will update your `_redirects` file. From afcc209796b28545729b1ee29cfd0a0c92e56a6d Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 1 Jun 2023 11:16:50 -0400 Subject: [PATCH 39/40] Add a note about public/_redirects file --- packages/integrations/netlify/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/integrations/netlify/README.md b/packages/integrations/netlify/README.md index ad9a6dcd70d8..cee5fa5c2962 100644 --- a/packages/integrations/netlify/README.md +++ b/packages/integrations/netlify/README.md @@ -76,7 +76,7 @@ export default defineConfig({ ### Static sites -For static sites you usually don't need an adapter. However, if you use `redirects` configuration in your Astro config, the Netlify adapter can be used to translate this to the proper `_redirects` format. +For static sites you usually don't need an adapter. However, if you use `redirects` configuration (experimental) in your Astro config, the Netlify adapter can be used to translate this to the proper `_redirects` format. ```js import { defineConfig } from 'astro/config'; @@ -87,12 +87,17 @@ export default defineConfig({ redirects: { '/blog/old-post': '/blog/new-post' + }, + experimental: { + redirects: true } }); ``` Once you run `astro build` there will be a `dist/_redirects` file. Netlify will use that to properly route pages in production. +> __Note__, you can still include a `public/_redirects` file for manual redirects. Any redirects you specify in the redirects config are appended to the end of your own. + ## Usage [Read the full deployment guide here.](https://docs.astro.build/en/guides/deploy/netlify/) From 303b79af30a12979ade4bd6cf1f6d04dc98cee9e Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 1 Jun 2023 17:09:56 -0400 Subject: [PATCH 40/40] Update packages/astro/src/@types/astro.ts Co-authored-by: Sarah Rainsberger --- packages/astro/src/@types/astro.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 36832eb36089..e38d33f86d5e 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -481,7 +481,7 @@ export interface AstroUserConfig { * For statically-generated sites with no adapter installed, this will produce a client redirect using a [`` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#http-equiv) and does not support status codes. * * When using SSR or with a static adapter in `output: static` - * mode (Netlify, Cloudflare, and Vercel), status codes are supported. + * mode, status codes are supported. * Astro will serve redirected GET requests with a status of `301` * and use a status of `308` for any other request method. *