Skip to content

Commit a4b696d

Browse files
Fryunimatthewpflorian-lefebvre
authoredJan 18, 2024
Fix regression in the routing priority of index routes (#9726)
* fix: Fix regression in the routing priority of index routes * chore: Add changeset * Update .changeset/smart-rules-train.md Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev> --------- Co-authored-by: Matthew Phillips <matthew@matthewphillips.info> Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
1 parent 259c30e commit a4b696d

File tree

3 files changed

+111
-46
lines changed

3 files changed

+111
-46
lines changed
 

‎.changeset/smart-rules-train.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"astro": patch
3+
---
4+
5+
Fixes a regression in routing priority between `index.astro` and dynamic routes with rest parameters

‎packages/astro/src/core/routing/manifest/create.ts

+58-43
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ interface Item {
3333
routeSuffix: string;
3434
}
3535

36+
interface ManifestRouteData extends RouteData {
37+
isIndex: boolean;
38+
}
39+
3640
function countOccurrences(needle: string, haystack: string) {
3741
let count = 0;
3842
for (const hay of haystack) {
@@ -134,6 +138,40 @@ function validateSegment(segment: string, file = '') {
134138
}
135139
}
136140

141+
/**
142+
* Checks whether two route segments are semantically equivalent.
143+
*
144+
* Two segments are equivalent if they would match the same paths. This happens when:
145+
* - They have the same length.
146+
* - Each part in the same position is either:
147+
* - Both static and with the same content (e.g. `/foo` and `/foo`).
148+
* - Both dynamic, regardless of the content (e.g. `/[bar]` and `/[baz]`).
149+
* - Both rest parameters, regardless of the content (e.g. `/[...bar]` and `/[...baz]`).
150+
*/
151+
function isSemanticallyEqualSegment(segmentA: RoutePart[], segmentB: RoutePart[]) {
152+
if (segmentA.length !== segmentB.length) {
153+
return false;
154+
}
155+
156+
for (const [index, partA] of segmentA.entries()) {
157+
// Safe to use the index of one segment for the other because the segments have the same length
158+
const partB = segmentB[index];
159+
160+
if (partA.dynamic !== partB.dynamic || partA.spread !== partB.spread) {
161+
return false;
162+
}
163+
164+
// Only compare the content on non-dynamic segments
165+
// `/[bar]` and `/[baz]` are effectively the same route,
166+
// only bound to a different path parameter.
167+
if (!partA.dynamic && partA.content !== partB.content) {
168+
return false;
169+
}
170+
}
171+
172+
return true;
173+
}
174+
137175
/**
138176
* Comparator for sorting routes in resolution order.
139177
*
@@ -142,6 +180,8 @@ function validateSegment(segment: string, file = '') {
142180
* - More specific routes are sorted before less specific routes. Here, "specific" means
143181
* the number of segments in the route, so a parent route is always sorted after its children.
144182
* For example, `/foo/bar` is sorted before `/foo`.
183+
* Index routes, originating from a file named `index.astro`, are considered to have one more
184+
* segment than the URL they represent.
145185
* - Static routes are sorted before dynamic routes.
146186
* For example, `/foo/bar` is sorted before `/foo/[bar]`.
147187
* - Dynamic routes with single parameters are sorted before dynamic routes with rest parameters.
@@ -153,10 +193,14 @@ function validateSegment(segment: string, file = '') {
153193
* For example, `/bar` is sorted before `/foo`.
154194
* The definition of "alphabetically" is dependent on the default locale of the running system.
155195
*/
156-
function routeComparator(a: RouteData, b: RouteData) {
196+
function routeComparator(a: ManifestRouteData, b: ManifestRouteData) {
197+
// For sorting purposes, an index route is considered to have one more segment than the URL it represents.
198+
const aLength = a.isIndex ? a.segments.length + 1 : a.segments.length;
199+
const bLength = b.isIndex ? b.segments.length + 1 : b.segments.length;
200+
157201
// Sort more specific routes before less specific routes
158-
if (a.segments.length !== b.segments.length) {
159-
return a.segments.length > b.segments.length ? -1 : 1;
202+
if (aLength !== bLength) {
203+
return aLength > bLength ? -1 : 1;
160204
}
161205

162206
const aIsStatic = a.segments.every((segment) =>
@@ -206,9 +250,9 @@ export interface CreateRouteManifestParams {
206250
function createFileBasedRoutes(
207251
{ settings, cwd, fsMod }: CreateRouteManifestParams,
208252
logger: Logger
209-
): RouteData[] {
253+
): ManifestRouteData[] {
210254
const components: string[] = [];
211-
const routes: RouteData[] = [];
255+
const routes: ManifestRouteData[] = [];
212256
const validPageExtensions = new Set<string>([
213257
'.astro',
214258
...SUPPORTED_MARKDOWN_FILE_EXTENSIONS,
@@ -321,6 +365,7 @@ function createFileBasedRoutes(
321365
.join('/')}`.toLowerCase();
322366
routes.push({
323367
route,
368+
isIndex: item.isIndex,
324369
type: item.isPage ? 'page' : 'endpoint',
325370
pattern,
326371
segments,
@@ -348,7 +393,7 @@ function createFileBasedRoutes(
348393
return routes;
349394
}
350395

351-
type PrioritizedRoutesData = Record<RoutePriorityOverride, RouteData[]>;
396+
type PrioritizedRoutesData = Record<RoutePriorityOverride, ManifestRouteData[]>;
352397

353398
function createInjectedRoutes({ settings, cwd }: CreateRouteManifestParams): PrioritizedRoutesData {
354399
const { config } = settings;
@@ -398,6 +443,8 @@ function createInjectedRoutes({ settings, cwd }: CreateRouteManifestParams): Pri
398443

399444
routes[priority].push({
400445
type,
446+
// For backwards compatibility, an injected route is never considered an index route.
447+
isIndex: false,
401448
route,
402449
pattern,
403450
segments,
@@ -468,6 +515,8 @@ function createRedirectRoutes(
468515

469516
routes[priority].push({
470517
type: 'redirect',
518+
// For backwards compatibility, a redirect is never considered an index route.
519+
isIndex: false,
471520
route,
472521
pattern,
473522
segments,
@@ -492,40 +541,6 @@ function isStaticSegment(segment: RoutePart[]) {
492541
return segment.every((part) => !part.dynamic && !part.spread);
493542
}
494543

495-
/**
496-
* Checks whether two route segments are semantically equivalent.
497-
*
498-
* Two segments are equivalent if they would match the same paths. This happens when:
499-
* - They have the same length.
500-
* - Each part in the same position is either:
501-
* - Both static and with the same content (e.g. `/foo` and `/foo`).
502-
* - Both dynamic, regardless of the content (e.g. `/[bar]` and `/[baz]`).
503-
* - Both rest parameters, regardless of the content (e.g. `/[...bar]` and `/[...baz]`).
504-
*/
505-
function isSemanticallyEqualSegment(segmentA: RoutePart[], segmentB: RoutePart[]) {
506-
if (segmentA.length !== segmentB.length) {
507-
return false;
508-
}
509-
510-
for (const [index, partA] of segmentA.entries()) {
511-
// Safe to use the index of one segment for the other because the segments have the same length
512-
const partB = segmentB[index];
513-
514-
if (partA.dynamic !== partB.dynamic || partA.spread !== partB.spread) {
515-
return false;
516-
}
517-
518-
// Only compare the content on non-dynamic segments
519-
// `/[bar]` and `/[baz]` are effectively the same route,
520-
// only bound to a different path parameter.
521-
if (!partA.dynamic && partA.content !== partB.content) {
522-
return false;
523-
}
524-
}
525-
526-
return true;
527-
}
528-
529544
/**
530545
* Check whether two are sure to collide in clearly unintended ways report appropriately.
531546
*
@@ -624,7 +639,7 @@ export function createRouteManifest(
624639

625640
const redirectRoutes = createRedirectRoutes(params, routeMap, logger);
626641

627-
const routes: RouteData[] = [
642+
const routes: ManifestRouteData[] = [
628643
...injectedRoutes['legacy'].sort(routeComparator),
629644
...[...fileBasedRoutes, ...injectedRoutes['normal'], ...redirectRoutes['normal']].sort(
630645
routeComparator
@@ -660,8 +675,8 @@ export function createRouteManifest(
660675

661676
// In this block of code we group routes based on their locale
662677

663-
// A map like: locale => RouteData[]
664-
const routesByLocale = new Map<string, RouteData[]>();
678+
// A map like: locale => ManifestRouteData[]
679+
const routesByLocale = new Map<string, ManifestRouteData[]>();
665680
// This type is here only as a helper. We copy the routes and make them unique, so we don't "process" the same route twice.
666681
// The assumption is that a route in the file system belongs to only one locale.
667682
const setRoutes = new Set(routes.filter((route) => route.type === 'page'));

‎packages/astro/test/units/routing/manifest.test.js

+48-3
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,51 @@ describe('routing - createRouteManifest', () => {
103103
]);
104104
});
105105

106+
it('static routes are sorted before dynamic and rest routes', async () => {
107+
const fs = createFs(
108+
{
109+
'/src/pages/[dynamic].astro': `<h1>test</h1>`,
110+
'/src/pages/[...rest].astro': `<h1>test</h1>`,
111+
'/src/pages/static.astro': `<h1>test</h1>`,
112+
'/src/pages/index.astro': `<h1>test</h1>`,
113+
},
114+
root
115+
);
116+
const settings = await createBasicSettings({
117+
root: fileURLToPath(root),
118+
base: '/search',
119+
trailingSlash: 'never',
120+
experimental: {
121+
globalRoutePriority: true,
122+
},
123+
});
124+
125+
const manifest = createRouteManifest({
126+
cwd: fileURLToPath(root),
127+
settings,
128+
fsMod: fs,
129+
});
130+
131+
expect(getManifestRoutes(manifest)).to.deep.equal([
132+
{
133+
"route": "/",
134+
"type": "page",
135+
},
136+
{
137+
"route": "/static",
138+
"type": "page",
139+
},
140+
{
141+
"route": "/[dynamic]",
142+
"type": "page",
143+
},
144+
{
145+
"route": "/[...rest]",
146+
"type": "page",
147+
},
148+
]);
149+
});
150+
106151
it('injected routes are sorted in legacy mode above filesystem routes', async () => {
107152
const fs = createFs(
108153
{
@@ -197,15 +242,15 @@ describe('routing - createRouteManifest', () => {
197242
type: 'page',
198243
},
199244
{
200-
route: '/contributing',
245+
route: '/',
201246
type: 'page',
202247
},
203248
{
204-
route: '/[...slug]',
249+
route: '/contributing',
205250
type: 'page',
206251
},
207252
{
208-
route: '/',
253+
route: '/[...slug]',
209254
type: 'page',
210255
},
211256
]);

0 commit comments

Comments
 (0)
Please sign in to comment.