diff --git a/.changeset/gold-roses-argue.md b/.changeset/gold-roses-argue.md new file mode 100644 index 000000000000..05c93789fc0f --- /dev/null +++ b/.changeset/gold-roses-argue.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Support spread parameters for server endpoints diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index c5b19bf1a5a3..6a28b33d2b7e 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -63,23 +63,30 @@ function getParts(part: string, file: string) { function getPattern(segments: RoutePart[][], addTrailingSlash: AstroConfig['trailingSlash']) { const pathname = segments .map((segment) => { - return segment[0].spread - ? '(?:\\/(.*?))?' - : '\\/' + - segment - .map((part) => { - if (part) - return part.dynamic - ? '([^/]+?)' - : part.content - .normalize() - .replace(/\?/g, '%3F') - .replace(/#/g, '%23') - .replace(/%5B/g, '[') - .replace(/%5D/g, ']') - .replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - }) - .join(''); + if (segment.length === 1 && segment[0].spread) { + return '(?:\\/(.*?))?'; + } else { + return ( + '\\/' + + segment + .map((part) => { + if (part.spread) { + return '(.*?)'; + } else if (part.dynamic) { + return '([^/]+?)'; + } else { + return part.content + .normalize() + .replace(/\?/g, '%3F') + .replace(/#/g, '%23') + .replace(/%5B/g, '[') + .replace(/%5D/g, ']') + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + }) + .join('') + ); + } }) .join(''); @@ -117,7 +124,10 @@ function validateSegment(segment: string, file = '') { if (countOccurrences('[', segment) !== countOccurrences(']', segment)) { throw new Error(`Invalid route ${file} \u2014 brackets are unbalanced`); } - if (/.+\[\.\.\.[^\]]+\]/.test(segment) || /\[\.\.\.[^\]]+\].+/.test(segment)) { + if ( + (/.+\[\.\.\.[^\]]+\]/.test(segment) || /\[\.\.\.[^\]]+\].+/.test(segment)) && + file.endsWith('.astro') + ) { throw new Error(`Invalid route ${file} \u2014 rest parameter must be a standalone segment`); } } diff --git a/packages/astro/src/core/routing/manifest/generator.ts b/packages/astro/src/core/routing/manifest/generator.ts index 6df4806fd6fa..4945ea9f13ae 100644 --- a/packages/astro/src/core/routing/manifest/generator.ts +++ b/packages/astro/src/core/routing/manifest/generator.ts @@ -8,23 +8,26 @@ export function getRouteGenerator( ) { const template = segments .map((segment) => { - return segment[0].spread - ? `/:${segment[0].content.slice(3)}(.*)?` - : '/' + - segment - .map((part) => { - if (part) - return part.dynamic - ? `:${part.content}` - : part.content - .normalize() - .replace(/\?/g, '%3F') - .replace(/#/g, '%23') - .replace(/%5B/g, '[') - .replace(/%5D/g, ']') - .replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - }) - .join(''); + return ( + '/' + + segment + .map((part) => { + if (part.spread) { + return `:${part.content.slice(3)}(.*)?`; + } else if (part.dynamic) { + return `:${part.content}`; + } else { + return part.content + .normalize() + .replace(/\?/g, '%3F') + .replace(/#/g, '%23') + .replace(/%5B/g, '[') + .replace(/%5D/g, ']') + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + }) + .join('') + ); }) .join(''); diff --git a/packages/astro/test/fixtures/routing-priority/src/pages/api/catch/[...slug].json.ts b/packages/astro/test/fixtures/routing-priority/src/pages/api/catch/[...slug].json.ts new file mode 100644 index 000000000000..142b11711c0b --- /dev/null +++ b/packages/astro/test/fixtures/routing-priority/src/pages/api/catch/[...slug].json.ts @@ -0,0 +1,13 @@ +import type { APIRoute } from 'astro'; + +export const get: APIRoute = async ({ params }) => { + return { + body: JSON.stringify({ + path: params.slug, + }), + }; +}; + +export function getStaticPaths() { + return [{ params: { slug: 'a' } }, { params: { slug: 'b/c' } }]; +} diff --git a/packages/astro/test/fixtures/routing-priority/src/pages/api/catch/[foo]-[bar].json.ts b/packages/astro/test/fixtures/routing-priority/src/pages/api/catch/[foo]-[bar].json.ts new file mode 100644 index 000000000000..2e66a22ae51a --- /dev/null +++ b/packages/astro/test/fixtures/routing-priority/src/pages/api/catch/[foo]-[bar].json.ts @@ -0,0 +1,14 @@ +import type { APIRoute } from 'astro'; + +export const get: APIRoute = async ({ params }) => { + return { + body: JSON.stringify({ + foo: params.foo, + bar: params.bar, + }), + }; +}; + +export function getStaticPaths() { + return [{ params: { foo: 'a', bar: 'b' } }]; +} diff --git a/packages/astro/test/routing-priority.test.js b/packages/astro/test/routing-priority.test.js index dba4094a22b6..586978d4f4cd 100644 --- a/packages/astro/test/routing-priority.test.js +++ b/packages/astro/test/routing-priority.test.js @@ -106,6 +106,21 @@ const routes = [ url: '/empty-slug/undefined', fourOhFour: true, }, + { + description: 'matches /api/catch/a.json to api/catch/[...slug].json.ts', + url: '/api/catch/a.json', + htmlMatch: JSON.stringify({ path: 'a' }), + }, + { + description: 'matches /api/catch/b/c.json to api/catch/[...slug].json.ts', + url: '/api/catch/b/c.json', + htmlMatch: JSON.stringify({ path: 'b/c' }), + }, + { + description: 'matches /api/catch/a-b.json to api/catch/[foo]-[bar].json.ts', + url: '/api/catch/a-b.json', + htmlMatch: JSON.stringify({ foo: 'a', bar: 'b' }), + }, ]; function appendForwardSlash(path) { @@ -123,9 +138,11 @@ describe('Routing priority', () => { await fixture.build(); }); - routes.forEach(({ description, url, fourOhFour, h1, p }) => { + routes.forEach(({ description, url, fourOhFour, h1, p, htmlMatch }) => { + const isEndpoint = htmlMatch && !h1 && !p; + it(description, async () => { - const htmlFile = `${appendForwardSlash(url)}index.html`; + const htmlFile = isEndpoint ? url : `${appendForwardSlash(url)}index.html`; if (fourOhFour) { expect(fixture.pathExists(htmlFile)).to.be.false; @@ -135,11 +152,17 @@ describe('Routing priority', () => { const html = await fixture.readFile(htmlFile); const $ = cheerioLoad(html); - expect($('h1').text()).to.equal(h1); + if (h1) { + expect($('h1').text()).to.equal(h1); + } if (p) { expect($('p').text()).to.equal(p); } + + if (htmlMatch) { + expect(html).to.equal(htmlMatch); + } }); }); }); @@ -160,7 +183,9 @@ describe('Routing priority', () => { await devServer.stop(); }); - routes.forEach(({ description, url, fourOhFour, h1, p }) => { + routes.forEach(({ description, url, fourOhFour, h1, p, htmlMatch }) => { + const isEndpoint = htmlMatch && !h1 && !p; + // checks URLs as written above it(description, async () => { const html = await fixture.fetch(url).then((res) => res.text()); @@ -171,13 +196,22 @@ describe('Routing priority', () => { return; } - expect($('h1').text()).to.equal(h1); + if (h1) { + expect($('h1').text()).to.equal(h1); + } if (p) { expect($('p').text()).to.equal(p); } + + if (htmlMatch) { + expect(html).to.equal(htmlMatch); + } }); + // skip for endpoint page test + if (isEndpoint) return; + // checks with trailing slashes, ex: '/de/' instead of '/de' it(`${description} (trailing slash)`, async () => { const html = await fixture.fetch(appendForwardSlash(url)).then((res) => res.text()); @@ -188,11 +222,17 @@ describe('Routing priority', () => { return; } - expect($('h1').text()).to.equal(h1); + if (h1) { + expect($('h1').text()).to.equal(h1); + } if (p) { expect($('p').text()).to.equal(p); } + + if (htmlMatch) { + expect(html).to.equal(htmlMatch); + } }); // checks with index.html, ex: '/de/index.html' instead of '/de' @@ -207,11 +247,17 @@ describe('Routing priority', () => { return; } - expect($('h1').text()).to.equal(h1); + if (h1) { + expect($('h1').text()).to.equal(h1); + } if (p) { expect($('p').text()).to.equal(p); } + + if (htmlMatch) { + expect(html).to.equal(htmlMatch); + } }); }); });