From 803cd0f37007aa0749fcbe98548accdd6bc58908 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Mon, 23 Jan 2023 09:47:33 -0500 Subject: [PATCH] Support prerender in Netlify redirects (#5904) * Support prerender in Netlify redirects * Updated sorting algorithm * Update packages/integrations/netlify/src/shared.ts Co-authored-by: Bjorn Lu Co-authored-by: Bjorn Lu --- .changeset/real-rules-occur.md | 5 + .../netlify/src/integration-edge-functions.ts | 2 +- .../netlify/src/integration-functions.ts | 2 +- packages/integrations/netlify/src/shared.ts | 116 +++++++++++++++--- .../test/functions/dynamic-route.test.js | 9 +- .../dynamic-route/src/pages/pets/[cat].astro | 27 ++++ .../dynamic-route/src/pages/pets/[dog].astro | 27 ++++ .../dynamic-route/src/pages/pets/index.astro | 12 ++ .../fixtures/prerender/src/pages/404.astro | 8 ++ .../fixtures/prerender/src/pages/index.astro | 8 ++ .../fixtures/prerender/src/pages/one.astro | 11 ++ .../netlify/test/functions/prerender.test.js | 30 +++++ 12 files changed, 240 insertions(+), 17 deletions(-) create mode 100644 .changeset/real-rules-occur.md create mode 100644 packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/[cat].astro create mode 100644 packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/[dog].astro create mode 100644 packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/index.astro create mode 100644 packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/404.astro create mode 100644 packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/index.astro create mode 100644 packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/one.astro create mode 100644 packages/integrations/netlify/test/functions/prerender.test.js diff --git a/.changeset/real-rules-occur.md b/.changeset/real-rules-occur.md new file mode 100644 index 000000000000..10133a0592eb --- /dev/null +++ b/.changeset/real-rules-occur.md @@ -0,0 +1,5 @@ +--- +'@astrojs/netlify': patch +--- + +Support prerender in \_redirects diff --git a/packages/integrations/netlify/src/integration-edge-functions.ts b/packages/integrations/netlify/src/integration-edge-functions.ts index d443e1c0ba69..0d036be9394e 100644 --- a/packages/integrations/netlify/src/integration-edge-functions.ts +++ b/packages/integrations/netlify/src/integration-edge-functions.ts @@ -163,7 +163,7 @@ export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {}) 'astro:build:done': async ({ routes, dir }) => { await bundleServerEntry(_buildConfig, _vite); await createEdgeManifest(routes, entryFile, _config.root); - await createRedirects(routes, dir, entryFile, true); + await createRedirects(_config, routes, dir, entryFile, true); }, }, }; diff --git a/packages/integrations/netlify/src/integration-functions.ts b/packages/integrations/netlify/src/integration-functions.ts index e8ff4bd1f899..f75b6d1f85b3 100644 --- a/packages/integrations/netlify/src/integration-functions.ts +++ b/packages/integrations/netlify/src/integration-functions.ts @@ -48,7 +48,7 @@ function netlifyFunctions({ } }, 'astro:build:done': async ({ routes, dir }) => { - await createRedirects(routes, dir, entryFile, false); + await createRedirects(_config, routes, dir, entryFile, false); }, }, }; diff --git a/packages/integrations/netlify/src/shared.ts b/packages/integrations/netlify/src/shared.ts index 2c648984a2b6..c87d946f56db 100644 --- a/packages/integrations/netlify/src/shared.ts +++ b/packages/integrations/netlify/src/shared.ts @@ -1,7 +1,16 @@ -import type { RouteData } from 'astro'; +import type { AstroConfig, RouteData } from 'astro'; import fs from 'fs'; +type RedirectDefinition = { + dynamic: boolean; + input: string; + target: string; + weight: 0 | 1; + status: 200 | 404; +}; + export async function createRedirects( + config: AstroConfig, routes: RouteData[], dir: URL, entryFile: string, @@ -10,37 +19,116 @@ export async function createRedirects( const _redirectsURL = new URL('./_redirects', dir); const kind = edge ? 'edge-functions' : 'functions'; - // Create the redirects file that is used for routing. - let _redirects = ''; + const definitions: RedirectDefinition[] = []; + for (const route of routes) { if (route.pathname) { if (route.distURL) { - _redirects += ` - ${route.pathname} /${route.distURL.toString().replace(dir.toString(), '')} 200`; + definitions.push({ + dynamic: false, + input: route.pathname, + target: prependForwardSlash(route.distURL.toString().replace(dir.toString(), '')), + status: 200, + weight: 1 + }); } else { - _redirects += ` - ${route.pathname} /.netlify/${kind}/${entryFile} 200`; + definitions.push({ + dynamic: false, + input: route.pathname, + target: `/.netlify/${kind}/${entryFile}`, + status: 200, + weight: 1, + }); if (route.route === '/404') { - _redirects += ` - /* /.netlify/${kind}/${entryFile} 404`; + definitions.push({ + dynamic: true, + input: '/*', + target: `/.netlify/${kind}/${entryFile}`, + status: 404, + weight: 0 + }); } } } else { const pattern = - '/' + route.segments.map(([part]) => (part.dynamic ? '*' : part.content)).join('/'); + '/' + route.segments.map(([part]) => { + //(part.dynamic ? '*' : part.content) + if(part.dynamic) { + if(part.spread) { + return '*'; + } else { + return ':' + part.content; + } + } else { + return part.content; + } + }).join('/'); + if (route.distURL) { - _redirects += ` - ${pattern} /${route.distURL.toString().replace(dir.toString(), '')} 200`; + const target = `${pattern}` + (config.build.format === 'directory' ? '/index.html' : '.html'); + definitions.push({ + dynamic: true, + input: pattern, + target, + status: 200, + weight: 1 + }); } else { - _redirects += ` - ${pattern} /.netlify/${kind}/${entryFile} 200`; + definitions.push({ + dynamic: true, + input: pattern, + target: `/.netlify/${kind}/${entryFile}`, + status: 200, + weight: 1 + }); } } } + let _redirects = prettify(definitions); + // 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 prettify(definitions: RedirectDefinition[]) { + let minInputLength = 0, minTargetLength = 0; + 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; +} diff --git a/packages/integrations/netlify/test/functions/dynamic-route.test.js b/packages/integrations/netlify/test/functions/dynamic-route.test.js index 0cfb5359b45a..6bb68eab88ad 100644 --- a/packages/integrations/netlify/test/functions/dynamic-route.test.js +++ b/packages/integrations/netlify/test/functions/dynamic-route.test.js @@ -21,6 +21,13 @@ describe('Dynamic pages', () => { it('Dynamic pages are included in the redirects file', async () => { const redir = await fixture.readFile('/_redirects'); - expect(redir).to.match(/\/products\/\*/); + expect(redir).to.match(/\/products\/:id/); + }); + + it('Prerendered routes are also included using placeholder syntax', async () => { + const redir = await fixture.readFile('/_redirects'); + expect(redir).to.include('/pets/:cat /pets/:cat/index.html 200'); + expect(redir).to.include('/pets/:dog /pets/:dog/index.html 200'); + expect(redir).to.include('/pets /.netlify/functions/entry 200'); }); }); diff --git a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/[cat].astro b/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/[cat].astro new file mode 100644 index 000000000000..f86ee6ca939a --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/[cat].astro @@ -0,0 +1,27 @@ +--- +export const prerender = true + +export function getStaticPaths() { + return [ + { + params: {cat: 'cat1'}, + props: {cat: 'cat1'} + }, + { + params: {cat: 'cat2'}, + props: {cat: 'cat2'} + }, + { + params: {cat: 'cat3'}, + props: {cat: 'cat3'} + }, + ]; +} + +const { cat } = Astro.props; + +--- + +
Good cat, {cat}!
+ +back diff --git a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/[dog].astro b/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/[dog].astro new file mode 100644 index 000000000000..0f3300f04529 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/[dog].astro @@ -0,0 +1,27 @@ +--- +export const prerender = true + +export function getStaticPaths() { + return [ + { + params: {dog: 'dog1'}, + props: {dog: 'dog1'} + }, + { + params: {dog: 'dog2'}, + props: {dog: 'dog2'} + }, + { + params: {dog: 'dog3'}, + props: {dog: 'dog3'} + }, + ]; +} + +const { dog } = Astro.props; + +--- + +
Good dog, {dog}!
+ +back diff --git a/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/index.astro b/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/index.astro new file mode 100644 index 000000000000..d1423f8ef2ee --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/dynamic-route/src/pages/pets/index.astro @@ -0,0 +1,12 @@ + + + + + + + Astro + + +

Astro

+ + diff --git a/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/404.astro b/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/404.astro new file mode 100644 index 000000000000..ad5d44aa213b --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/404.astro @@ -0,0 +1,8 @@ + + + Testing + + +

testing

+ + diff --git a/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/index.astro new file mode 100644 index 000000000000..ad5d44aa213b --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/index.astro @@ -0,0 +1,8 @@ + + + Testing + + +

testing

+ + diff --git a/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/one.astro b/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/one.astro new file mode 100644 index 000000000000..12146450e41c --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/one.astro @@ -0,0 +1,11 @@ +--- +export const prerender = true; +--- + + + Testing + + +

testing

+ + diff --git a/packages/integrations/netlify/test/functions/prerender.test.js b/packages/integrations/netlify/test/functions/prerender.test.js new file mode 100644 index 000000000000..324ebc5c5a10 --- /dev/null +++ b/packages/integrations/netlify/test/functions/prerender.test.js @@ -0,0 +1,30 @@ +import { expect } from 'chai'; +import netlifyAdapter from '../../dist/index.js'; +import { loadFixture, testIntegration } from './test-utils.js'; + +describe('Mixed Prerendering with SSR', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: new URL('./fixtures/prerender/', import.meta.url).toString(), + output: 'server', + adapter: netlifyAdapter({ + dist: new URL('./fixtures/prerender/dist/', import.meta.url), + }), + site: `http://example.com`, + integrations: [testIntegration()], + }); + await fixture.build(); + }); + it('Wildcard 404 is sorted last', async () => { + const redir = await fixture.readFile('/_redirects'); + const baseRouteIndex = redir.indexOf('/ /.netlify/functions/entry 200'); + const oneRouteIndex = redir.indexOf('/one /one/index.html 200'); + const fourOhFourWildCardIndex = redir.indexOf('/* /.netlify/functions/entry 404'); + + expect(fourOhFourWildCardIndex).to.be.greaterThan(baseRouteIndex); + expect(fourOhFourWildCardIndex).to.be.greaterThan(oneRouteIndex); + }); +});