mirror of
https://github.com/withastro/astro.git
synced 2024-12-16 21:46:22 -05:00
Fix regression on dynamic sibling trees and index inside rest parameter folders (#9786)
* fix: Fix regression on dynamic sibling trees and index inside rest parameter folders * Add extra test scenarios * Make `/[foo].astro` also win over `/[foo]/[...rest].astro` * Make `/[foo].astro` also win over `/[foo]/[...rest].astro` * Update tests * Remove commented out code * Update .changeset/six-shrimps-glow.md * Fix sorting cycle --------- Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
This commit is contained in:
parent
659087be1a
commit
5b29550996
4 changed files with 204 additions and 43 deletions
39
.changeset/six-shrimps-glow.md
Normal file
39
.changeset/six-shrimps-glow.md
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
---
|
||||||
|
"astro": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixes a regression in routing priority for index pages in rest parameter folders and dynamic sibling trees.
|
||||||
|
|
||||||
|
Considering the following tree:
|
||||||
|
```
|
||||||
|
src/pages/
|
||||||
|
├── index.astro
|
||||||
|
├── static.astro
|
||||||
|
├── [dynamic_file].astro
|
||||||
|
├── [...rest_file].astro
|
||||||
|
├── blog/
|
||||||
|
│ └── index.astro
|
||||||
|
├── [dynamic_folder]/
|
||||||
|
│ ├── index.astro
|
||||||
|
│ ├── static.astro
|
||||||
|
│ └── [...rest].astro
|
||||||
|
└── [...rest_folder]/
|
||||||
|
├── index.astro
|
||||||
|
└── static.astro
|
||||||
|
```
|
||||||
|
|
||||||
|
The routes are sorted in this order:
|
||||||
|
```
|
||||||
|
/src/pages/index.astro
|
||||||
|
/src/pages/blog/index.astro
|
||||||
|
/src/pages/static.astro
|
||||||
|
/src/pages/[dynamic_folder]/index.astro
|
||||||
|
/src/pages/[dynamic_file].astro
|
||||||
|
/src/pages/[dynamic_folder]/static.astro
|
||||||
|
/src/pages/[dynamic_folder]/[...rest].astro
|
||||||
|
/src/pages/[...rest_folder]/static.astro
|
||||||
|
/src/pages/[...rest_folder]/index.astro
|
||||||
|
/src/pages/[...rest_file]/index.astro
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows for index files to be used as overrides to rest parameter routes on SSR when the rest parameter matching `undefined` is not desired.
|
|
@ -194,47 +194,94 @@ function isSemanticallyEqualSegment(segmentA: RoutePart[], segmentB: RoutePart[]
|
||||||
* The definition of "alphabetically" is dependent on the default locale of the running system.
|
* The definition of "alphabetically" is dependent on the default locale of the running system.
|
||||||
*/
|
*/
|
||||||
function routeComparator(a: ManifestRouteData, b: ManifestRouteData) {
|
function routeComparator(a: ManifestRouteData, b: ManifestRouteData) {
|
||||||
|
const commonLength = Math.min(a.segments.length, b.segments.length);
|
||||||
|
|
||||||
|
for (let index = 0; index < commonLength; index++) {
|
||||||
|
const aSegment = a.segments[index];
|
||||||
|
const bSegment = b.segments[index];
|
||||||
|
|
||||||
|
const aIsStatic = aSegment.every((part) => !part.dynamic && !part.spread);
|
||||||
|
const bIsStatic = bSegment.every((part) => !part.dynamic && !part.spread);
|
||||||
|
|
||||||
|
if (aIsStatic && bIsStatic) {
|
||||||
|
// Both segments are static, they are sorted alphabetically if they are different
|
||||||
|
const aContent = aSegment.map((part) => part.content).join('');
|
||||||
|
const bContent = bSegment.map((part) => part.content).join('');
|
||||||
|
|
||||||
|
if (aContent !== bContent) {
|
||||||
|
return aContent.localeCompare(bContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort static routes before dynamic routes
|
||||||
|
if (aIsStatic !== bIsStatic) {
|
||||||
|
return aIsStatic ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const aHasSpread = aSegment.some((part) => part.spread);
|
||||||
|
const bHasSpread = bSegment.some((part) => part.spread);
|
||||||
|
|
||||||
|
// Sort dynamic routes with rest parameters after dynamic routes with single parameters
|
||||||
|
// (also after static, but that is already covered by the previous condition)
|
||||||
|
if (aHasSpread !== bHasSpread) {
|
||||||
|
return aHasSpread ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case to have `/[foo].astro` be equivalent to `/[foo]/index.astro`
|
||||||
|
// when compared against `/[foo]/[...rest].astro`.
|
||||||
|
if (Math.abs(a.segments.length - b.segments.length) === 1) {
|
||||||
|
const aEndsInRest = a.segments.at(-1)?.some((part) => part.spread);
|
||||||
|
const bEndsInRest = b.segments.at(-1)?.some((part) => part.spread);
|
||||||
|
|
||||||
|
// Routes with rest parameters are less specific than their parent route.
|
||||||
|
// For example, `/foo/[...bar]` is sorted after `/foo`.
|
||||||
|
|
||||||
|
if (a.segments.length > b.segments.length && !bEndsInRest) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.segments.length > a.segments.length && !aEndsInRest) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.isIndex !== b.isIndex) {
|
||||||
|
// Index pages are lower priority than other static segments in the same prefix.
|
||||||
|
// They match the path up to their parent, but are more specific than the parent.
|
||||||
|
// For example:
|
||||||
|
// - `/foo/index.astro` is sorted before `/foo`
|
||||||
|
// - `/foo/index.astro` is sorted before `/foo/[bar].astro`
|
||||||
|
// - `/[...foo]/index.astro` is sorted after `/[...foo]/bar.astro`
|
||||||
|
|
||||||
|
if (a.isIndex) {
|
||||||
|
const followingBSegment = b.segments.at(a.segments.length);
|
||||||
|
const followingBSegmentIsStatic = followingBSegment?.every((part) => !part.dynamic && !part.spread);
|
||||||
|
|
||||||
|
return followingBSegmentIsStatic ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const followingASegment = a.segments.at(b.segments.length);
|
||||||
|
const followingASegmentIsStatic = followingASegment?.every((part) => !part.dynamic && !part.spread);
|
||||||
|
|
||||||
|
return followingASegmentIsStatic ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
// For sorting purposes, an index route is considered to have one more segment than the URL it represents.
|
// For sorting purposes, an index route is considered to have one more segment than the URL it represents.
|
||||||
const aLength = a.isIndex ? a.segments.length + 1 : a.segments.length;
|
const aLength = a.isIndex ? a.segments.length + 1 : a.segments.length;
|
||||||
const bLength = b.isIndex ? b.segments.length + 1 : b.segments.length;
|
const bLength = b.isIndex ? b.segments.length + 1 : b.segments.length;
|
||||||
|
|
||||||
// Sort more specific routes before less specific routes
|
if (aLength !== bLength){
|
||||||
if (aLength !== bLength) {
|
// Routes are equal up to the smaller of the two lengths, so the longer route is more specific
|
||||||
return aLength > bLength ? -1 : 1;
|
return aLength > bLength ? -1 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const aIsStatic = a.segments.every((segment) =>
|
|
||||||
segment.every((part) => !part.dynamic && !part.spread)
|
|
||||||
);
|
|
||||||
const bIsStatic = b.segments.every((segment) =>
|
|
||||||
segment.every((part) => !part.dynamic && !part.spread)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sort static routes before dynamic routes
|
|
||||||
if (aIsStatic !== bIsStatic) {
|
|
||||||
return aIsStatic ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const aHasSpread = a.segments.some((segment) => segment.some((part) => part.spread));
|
|
||||||
const bHasSpread = b.segments.some((segment) => segment.some((part) => part.spread));
|
|
||||||
|
|
||||||
// Sort dynamic routes with rest parameters after dynamic routes with single parameters
|
|
||||||
// (also after static, but that is already covered by the previous condition)
|
|
||||||
if (aHasSpread !== bHasSpread) {
|
|
||||||
return aHasSpread ? 1 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort prerendered routes before non-prerendered routes
|
|
||||||
if (a.prerender !== b.prerender) {
|
|
||||||
return a.prerender ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort endpoints before pages
|
// Sort endpoints before pages
|
||||||
if ((a.type === 'endpoint') !== (b.type === 'endpoint')) {
|
if ((a.type === 'endpoint') !== (b.type === 'endpoint')) {
|
||||||
return a.type === 'endpoint' ? -1 : 1;
|
return a.type === 'endpoint' ? -1 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort alphabetically
|
// Both routes have segments with the same properties
|
||||||
return a.route.localeCompare(b.route);
|
return a.route.localeCompare(b.route);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ export function isServerLikeOutput(config: AstroConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPrerenderDefault(config: AstroConfig) {
|
export function getPrerenderDefault(config: AstroConfig) {
|
||||||
return config.output === 'hybrid';
|
return config.output !== 'server';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -52,8 +52,8 @@ describe('routing - createRouteManifest', () => {
|
||||||
it('endpoint routes are sorted before page routes', async () => {
|
it('endpoint routes are sorted before page routes', async () => {
|
||||||
const fs = createFs(
|
const fs = createFs(
|
||||||
{
|
{
|
||||||
'/src/pages/contact-me.astro': `<h1>test</h1>`,
|
'/src/pages/[contact].astro': `<h1>test</h1>`,
|
||||||
'/src/pages/sendContact.ts': `<h1>test</h1>`,
|
'/src/pages/[contact].ts': `<h1>test</h1>`,
|
||||||
},
|
},
|
||||||
root
|
root
|
||||||
);
|
);
|
||||||
|
@ -84,20 +84,20 @@ describe('routing - createRouteManifest', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getManifestRoutes(manifest)).to.deep.equal([
|
expect(getManifestRoutes(manifest)).to.deep.equal([
|
||||||
{
|
|
||||||
route: '/api',
|
|
||||||
type: 'endpoint',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
route: '/sendcontact',
|
|
||||||
type: 'endpoint',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
route: '/about',
|
route: '/about',
|
||||||
type: 'page',
|
type: 'page',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
route: '/contact-me',
|
route: '/api',
|
||||||
|
type: 'endpoint',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: '/[contact]',
|
||||||
|
type: 'endpoint',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: '/[contact]',
|
||||||
type: 'page',
|
type: 'page',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -148,6 +148,81 @@ describe('routing - createRouteManifest', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('route sorting respects the file tree', async () => {
|
||||||
|
const fs = createFs(
|
||||||
|
{
|
||||||
|
'/src/pages/[dynamic_folder]/static.astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/[dynamic_folder]/index.astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/[dynamic_folder]/[...rest].astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/[...rest]/static.astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/[...rest]/index.astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/blog/index.astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/[dynamic_file].astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/[...other].astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/static.astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/index.astro': `<h1>test</h1>`,
|
||||||
|
},
|
||||||
|
root
|
||||||
|
);
|
||||||
|
const settings = await createBasicSettings({
|
||||||
|
root: fileURLToPath(root),
|
||||||
|
base: '/search',
|
||||||
|
trailingSlash: 'never',
|
||||||
|
experimental: {
|
||||||
|
globalRoutePriority: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const manifest = createRouteManifest({
|
||||||
|
cwd: fileURLToPath(root),
|
||||||
|
settings,
|
||||||
|
fsMod: fs,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getManifestRoutes(manifest)).to.deep.equal([
|
||||||
|
{
|
||||||
|
"route": "/",
|
||||||
|
"type": "page",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "/blog",
|
||||||
|
"type": "page",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "/static",
|
||||||
|
"type": "page",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "/[dynamic_folder]",
|
||||||
|
"type": "page",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "/[dynamic_file]",
|
||||||
|
"type": "page",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "/[dynamic_folder]/static",
|
||||||
|
"type": "page",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "/[dynamic_folder]/[...rest]",
|
||||||
|
"type": "page",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "/[...rest]/static",
|
||||||
|
"type": "page",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "/[...rest]",
|
||||||
|
"type": "page",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "/[...other]",
|
||||||
|
"type": "page",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('injected routes are sorted in legacy mode above filesystem routes', async () => {
|
it('injected routes are sorted in legacy mode above filesystem routes', async () => {
|
||||||
const fs = createFs(
|
const fs = createFs(
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue