Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix regression on dynamic sibling trees and index inside rest parameter folders #9786

Merged
merged 9 commits into from
Jan 24, 2024
30 changes: 30 additions & 0 deletions .changeset/six-shrimps-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
"astro": patch
---

Fix regression in routing priority for index pages in rest parameter folders and dynamic sybling trees.

Considering the following tree:
```
src/pages/
├── index.astro
├── static.astro
├── [dynamic]/
│ ├── index.astro
│ ├── [...rest].astro
│ └── static.astro
└── [...rest]/
├── index.astro
└── static.astro
```

The routes are sorted in this order:
```
/src/pages/static.astro
/src/pages/index.astro
/src/pages/[dynamic]/static.astro
/src/pages/[dynamic]/index.astro
/src/pages/[dynamic]/[...rest].astro
/src/pages/[...rest]/static.astro
/src/pages/[...rest]/index.astro
```
75 changes: 47 additions & 28 deletions packages/astro/src/core/routing/manifest/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,47 +194,66 @@ function isSemanticallyEqualSegment(segmentA: RoutePart[], segmentB: RoutePart[]
* The definition of "alphabetically" is dependent on the default locale of the running system.
*/
function routeComparator(a: ManifestRouteData, b: ManifestRouteData) {
// 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 bLength = b.isIndex ? b.segments.length + 1 : b.segments.length;

// Sort more specific routes before less specific routes
if (aLength !== bLength) {
return aLength > bLength ? -1 : 1;
}
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);

// Sort static routes before dynamic routes
if (aIsStatic !== bIsStatic) {
return aIsStatic ? -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)
);
const aHasSpread = aSegment.some((part) => part.spread);
const bHasSpread = bSegment.some((part) => part.spread);

// Sort static routes before dynamic routes
if (aIsStatic !== bIsStatic) {
return aIsStatic ? -1 : 1;
// 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;
}
}

// 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 bLength = b.isIndex ? b.segments.length + 1 : b.segments.length;

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 aHasSpread = a.segments.some((segment) => segment.some((part) => part.spread));
const bHasSpread = b.segments.some((segment) => segment.some((part) => part.spread));
const followingASegment = a.segments.at(b.segments.length);
const followingASegmentIsStatic = followingASegment?.every((part) => !part.dynamic && !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;
return followingASegmentIsStatic ? -1 : 1;
}

// Sort prerendered routes before non-prerendered routes
if (a.prerender !== b.prerender) {
return a.prerender ? -1 : 1;
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;
}

// Sort endpoints before pages
if ((a.type === 'endpoint') !== (b.type === 'endpoint')) {
return a.type === 'endpoint' ? -1 : 1;
}

// Sort alphabetically
// Both routes have segments with the same properties
return a.route.localeCompare(b.route);
}

Expand Down
68 changes: 64 additions & 4 deletions packages/astro/test/units/routing/manifest.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,11 @@ describe('routing - createRouteManifest', () => {

expect(getManifestRoutes(manifest)).to.deep.equal([
{
route: '/',
route: '/static',
type: 'page',
},
{
route: '/static',
route: '/',
type: 'page',
},
{
Expand All @@ -148,6 +148,66 @@ describe('routing - createRouteManifest', () => {
]);
});

it('route sorting respects the file tree', async () => {
const fs = createFs(
{
'/src/pages/[dynamic]/static.astro': `<h1>test</h1>`,
'/src/pages/[dynamic]/index.astro': `<h1>test</h1>`,
'/src/pages/[dynamic]/[...rest].astro': `<h1>test</h1>`,
'/src/pages/[...rest]/static.astro': `<h1>test</h1>`,
'/src/pages/[...rest]/index.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": "/static",
"type": "page",
},
{
"route": "/",
"type": "page",
},
{
"route": "/[dynamic]/static",
"type": "page",
},
{
"route": "/[dynamic]",
"type": "page",
},
{
"route": "/[dynamic]/[...rest]",
"type": "page",
},
{
"route": "/[...rest]/static",
"type": "page",
},
{
"route": "/[...rest]",
"type": "page",
},
]);
});

it('injected routes are sorted in legacy mode above filesystem routes', async () => {
const fs = createFs(
{
Expand Down Expand Up @@ -242,11 +302,11 @@ describe('routing - createRouteManifest', () => {
type: 'page',
},
{
route: '/',
route: '/contributing',
type: 'page',
},
{
route: '/contributing',
route: '/',
type: 'page',
},
{
Expand Down