Support spread parameters for server endpoints (#5106)

This commit is contained in:
Bjorn Lu 2022-10-18 13:16:24 +08:00 committed by GitHub
parent 0edfdd3259
commit ef0c543163
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 133 additions and 42 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Support spread parameters for server endpoints

View file

@ -63,23 +63,30 @@ function getParts(part: string, file: string) {
function getPattern(segments: RoutePart[][], addTrailingSlash: AstroConfig['trailingSlash']) { function getPattern(segments: RoutePart[][], addTrailingSlash: AstroConfig['trailingSlash']) {
const pathname = segments const pathname = segments
.map((segment) => { .map((segment) => {
return segment[0].spread if (segment.length === 1 && segment[0].spread) {
? '(?:\\/(.*?))?' return '(?:\\/(.*?))?';
: '\\/' + } else {
segment return (
.map((part) => { '\\/' +
if (part) segment
return part.dynamic .map((part) => {
? '([^/]+?)' if (part.spread) {
: part.content return '(.*?)';
.normalize() } else if (part.dynamic) {
.replace(/\?/g, '%3F') return '([^/]+?)';
.replace(/#/g, '%23') } else {
.replace(/%5B/g, '[') return part.content
.replace(/%5D/g, ']') .normalize()
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); .replace(/\?/g, '%3F')
}) .replace(/#/g, '%23')
.join(''); .replace(/%5B/g, '[')
.replace(/%5D/g, ']')
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
})
.join('')
);
}
}) })
.join(''); .join('');
@ -117,7 +124,10 @@ function validateSegment(segment: string, file = '') {
if (countOccurrences('[', segment) !== countOccurrences(']', segment)) { if (countOccurrences('[', segment) !== countOccurrences(']', segment)) {
throw new Error(`Invalid route ${file} \u2014 brackets are unbalanced`); 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`); throw new Error(`Invalid route ${file} \u2014 rest parameter must be a standalone segment`);
} }
} }

View file

@ -8,23 +8,26 @@ export function getRouteGenerator(
) { ) {
const template = segments const template = segments
.map((segment) => { .map((segment) => {
return segment[0].spread return (
? `/:${segment[0].content.slice(3)}(.*)?` '/' +
: '/' + segment
segment .map((part) => {
.map((part) => { if (part.spread) {
if (part) return `:${part.content.slice(3)}(.*)?`;
return part.dynamic } else if (part.dynamic) {
? `:${part.content}` return `:${part.content}`;
: part.content } else {
.normalize() return part.content
.replace(/\?/g, '%3F') .normalize()
.replace(/#/g, '%23') .replace(/\?/g, '%3F')
.replace(/%5B/g, '[') .replace(/#/g, '%23')
.replace(/%5D/g, ']') .replace(/%5B/g, '[')
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); .replace(/%5D/g, ']')
}) .replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
.join(''); }
})
.join('')
);
}) })
.join(''); .join('');

View file

@ -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' } }];
}

View file

@ -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' } }];
}

View file

@ -106,6 +106,21 @@ const routes = [
url: '/empty-slug/undefined', url: '/empty-slug/undefined',
fourOhFour: true, 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) { function appendForwardSlash(path) {
@ -123,9 +138,11 @@ describe('Routing priority', () => {
await fixture.build(); 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 () => { it(description, async () => {
const htmlFile = `${appendForwardSlash(url)}index.html`; const htmlFile = isEndpoint ? url : `${appendForwardSlash(url)}index.html`;
if (fourOhFour) { if (fourOhFour) {
expect(fixture.pathExists(htmlFile)).to.be.false; expect(fixture.pathExists(htmlFile)).to.be.false;
@ -135,11 +152,17 @@ describe('Routing priority', () => {
const html = await fixture.readFile(htmlFile); const html = await fixture.readFile(htmlFile);
const $ = cheerioLoad(html); const $ = cheerioLoad(html);
expect($('h1').text()).to.equal(h1); if (h1) {
expect($('h1').text()).to.equal(h1);
}
if (p) { if (p) {
expect($('p').text()).to.equal(p); expect($('p').text()).to.equal(p);
} }
if (htmlMatch) {
expect(html).to.equal(htmlMatch);
}
}); });
}); });
}); });
@ -160,7 +183,9 @@ describe('Routing priority', () => {
await devServer.stop(); 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 // checks URLs as written above
it(description, async () => { it(description, async () => {
const html = await fixture.fetch(url).then((res) => res.text()); const html = await fixture.fetch(url).then((res) => res.text());
@ -171,13 +196,22 @@ describe('Routing priority', () => {
return; return;
} }
expect($('h1').text()).to.equal(h1); if (h1) {
expect($('h1').text()).to.equal(h1);
}
if (p) { if (p) {
expect($('p').text()).to.equal(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' // checks with trailing slashes, ex: '/de/' instead of '/de'
it(`${description} (trailing slash)`, async () => { it(`${description} (trailing slash)`, async () => {
const html = await fixture.fetch(appendForwardSlash(url)).then((res) => res.text()); const html = await fixture.fetch(appendForwardSlash(url)).then((res) => res.text());
@ -188,11 +222,17 @@ describe('Routing priority', () => {
return; return;
} }
expect($('h1').text()).to.equal(h1); if (h1) {
expect($('h1').text()).to.equal(h1);
}
if (p) { if (p) {
expect($('p').text()).to.equal(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' // checks with index.html, ex: '/de/index.html' instead of '/de'
@ -207,11 +247,17 @@ describe('Routing priority', () => {
return; return;
} }
expect($('h1').text()).to.equal(h1); if (h1) {
expect($('h1').text()).to.equal(h1);
}
if (p) { if (p) {
expect($('p').text()).to.equal(p); expect($('p').text()).to.equal(p);
} }
if (htmlMatch) {
expect(html).to.equal(htmlMatch);
}
}); });
}); });
}); });