mirror of
https://github.com/withastro/astro.git
synced 2025-01-27 22:19:04 -05:00
Fixes regression on routing priority for multi-layer index pages (#10096)
* Reproduce regression * Simplify sorting algorithm * Add changeset * Fix changeset typo * Rename assertion utility function * Fix index detection * Add changeset for index fix --------- Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
This commit is contained in:
parent
e24db1d8a6
commit
227cd83a51
4 changed files with 151 additions and 104 deletions
23
.changeset/early-windows-accept.md
Normal file
23
.changeset/early-windows-accept.md
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
"astro": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixes regression on routing priority for multi-layer index pages
|
||||||
|
|
||||||
|
The sorting algorithm positions more specific routes before less specific routes, and considers index pages to be more specific than a dynamic route with a rest parameter inside of it.
|
||||||
|
This means that `/blog` is considered more specific than `/blog/[...slug]`.
|
||||||
|
|
||||||
|
But this special case was being applied incorrectly to indexes, which could cause a problem in scenarios like the following:
|
||||||
|
- `/`
|
||||||
|
- `/blog`
|
||||||
|
- `/blog/[...slug]`
|
||||||
|
|
||||||
|
The algorithm would make the following comparisons:
|
||||||
|
- `/` is more specific than `/blog` (incorrect)
|
||||||
|
- `/blog/[...slug]` is more specific than `/` (correct)
|
||||||
|
- `/blog` is more specific than `/blog/[...slug]` (correct)
|
||||||
|
|
||||||
|
Although the incorrect first comparison is not a problem by itself, it could cause the algorithm to make the wrong decision.
|
||||||
|
Depending on the other routes in the project, the sorting could perform just the last two comparisons and by transitivity infer the inverse of the third (`/blog/[...slug` > `/` > `/blog`), which is incorrect.
|
||||||
|
|
||||||
|
Now the algorithm doesn't have a special case for index pages and instead does the comparison soleley for rest parameter segments and their immediate parents, which is consistent with the transitivity property.
|
8
.changeset/lemon-cobras-swim.md
Normal file
8
.changeset/lemon-cobras-swim.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
"astro": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixes edge case on i18n fallback routes
|
||||||
|
|
||||||
|
Previously index routes deeply nested in the default locale, like `/some/nested/index.astro` could be mistaked as the root index for the default locale, resulting in an incorrect redirect on `/`.
|
||||||
|
|
|
@ -227,55 +227,33 @@ function routeComparator(a: RouteData, b: RouteData) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case to have `/[foo].astro` be equivalent to `/[foo]/index.astro`
|
const aLength = a.segments.length;
|
||||||
// when compared against `/[foo]/[...rest].astro`.
|
const bLength = b.segments.length;
|
||||||
if (Math.abs(a.segments.length - b.segments.length) === 1) {
|
|
||||||
|
if (aLength !== bLength) {
|
||||||
const aEndsInRest = a.segments.at(-1)?.some((part) => part.spread);
|
const aEndsInRest = a.segments.at(-1)?.some((part) => part.spread);
|
||||||
const bEndsInRest = b.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.
|
if (aEndsInRest !== bEndsInRest && Math.abs(aLength - bLength) === 1) {
|
||||||
// For example, `/foo/[...bar]` is sorted after `/foo`.
|
// If only one of the routes ends in a rest parameter
|
||||||
|
// and the difference in length is exactly 1
|
||||||
|
// and the shorter route is the one that ends in a rest parameter
|
||||||
|
// the shorter route is considered more specific.
|
||||||
|
// I.e. `/foo` is considered more specific than `/foo/[...bar]`
|
||||||
|
if (aLength > bLength && aEndsInRest) {
|
||||||
|
// b: /foo
|
||||||
|
// a: /foo/[...bar]
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (a.segments.length > b.segments.length && !bEndsInRest) {
|
if (bLength > aLength && bEndsInRest) {
|
||||||
return 1;
|
// a: /foo
|
||||||
|
// b: /foo/[...bar]
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (b.segments.length > a.segments.length && !aEndsInRest) {
|
// Sort routes by length
|
||||||
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.
|
|
||||||
const aLength = a.isIndex ? a.segments.length + 1 : a.segments.length;
|
|
||||||
const bLength = b.isIndex ? b.segments.length + 1 : b.segments.length;
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -781,10 +759,12 @@ export function createRouteManifest(
|
||||||
// we attempt to retrieve the index page of the default locale
|
// we attempt to retrieve the index page of the default locale
|
||||||
const defaultLocaleRoutes = routesByLocale.get(i18n.defaultLocale);
|
const defaultLocaleRoutes = routesByLocale.get(i18n.defaultLocale);
|
||||||
if (defaultLocaleRoutes) {
|
if (defaultLocaleRoutes) {
|
||||||
const indexDefaultRoute = defaultLocaleRoutes.find((routeData) => {
|
// The index for the default locale will be either already at the root path
|
||||||
// it should be safe to assume that an index page has "index" in their name
|
// or at the root of the locale.
|
||||||
return routeData.component.includes('index');
|
const indexDefaultRoute = defaultLocaleRoutes
|
||||||
});
|
.find(({route}) => route === '/')
|
||||||
|
?? defaultLocaleRoutes.find(({route}) => route === `/${i18n.defaultLocale}`);
|
||||||
|
|
||||||
if (indexDefaultRoute) {
|
if (indexDefaultRoute) {
|
||||||
// we found the index of the default locale, now we create a root index that will redirect to the index of the default locale
|
// we found the index of the default locale, now we create a root index that will redirect to the index of the default locale
|
||||||
const pathname = '/';
|
const pathname = '/';
|
||||||
|
|
|
@ -26,6 +26,19 @@ function getLogger() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertRouteRelations(routes, relations) {
|
||||||
|
const routePaths = routes.map((route) => route.route);
|
||||||
|
|
||||||
|
for (const [before, after] of relations) {
|
||||||
|
const beforeIndex = routePaths.indexOf(before);
|
||||||
|
const afterIndex = routePaths.indexOf(after);
|
||||||
|
|
||||||
|
if (beforeIndex > afterIndex) {
|
||||||
|
assert.fail(`${before} should be higher priority than ${after}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe('routing - createRouteManifest', () => {
|
describe('routing - createRouteManifest', () => {
|
||||||
it('using trailingSlash: "never" does not match the index route when it contains a trailing slash', async () => {
|
it('using trailingSlash: "never" does not match the index route when it contains a trailing slash', async () => {
|
||||||
const fs = createFs(
|
const fs = createFs(
|
||||||
|
@ -128,23 +141,58 @@ describe('routing - createRouteManifest', () => {
|
||||||
fsMod: fs,
|
fsMod: fs,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(getManifestRoutes(manifest), [
|
assertRouteRelations(getManifestRoutes(manifest), [
|
||||||
|
['/', '/[...rest]'],
|
||||||
|
['/static', '/[dynamic]'],
|
||||||
|
['/static', '/[...rest]'],
|
||||||
|
['/[dynamic]', '/[...rest]'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('route sorting with multi-layer index page conflict', async () => {
|
||||||
|
// Reproducing regression from https://github.com/withastro/astro/issues/10071
|
||||||
|
const fs = createFs(
|
||||||
{
|
{
|
||||||
route: '/',
|
'/src/pages/a/1.astro': `<h1>test</h1>`,
|
||||||
type: 'page',
|
'/src/pages/a/2.astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/a/3.astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/modules/[...slug].astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/modules/index.astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/test/[...slug].astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/test/index.astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/index.astro': `<h1>test</h1>`,
|
||||||
},
|
},
|
||||||
{
|
root
|
||||||
route: '/static',
|
);
|
||||||
type: 'page',
|
const settings = await createBasicSettings({
|
||||||
},
|
root: fileURLToPath(root),
|
||||||
{
|
base: '/search',
|
||||||
route: '/[dynamic]',
|
trailingSlash: 'never',
|
||||||
type: 'page',
|
experimental: {
|
||||||
},
|
globalRoutePriority: true,
|
||||||
{
|
|
||||||
route: '/[...rest]',
|
|
||||||
type: 'page',
|
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const manifest = createRouteManifest({
|
||||||
|
cwd: fileURLToPath(root),
|
||||||
|
settings,
|
||||||
|
fsMod: fs,
|
||||||
|
});
|
||||||
|
|
||||||
|
assertRouteRelations(getManifestRoutes(manifest), [
|
||||||
|
// Parent route should come before rest parameters
|
||||||
|
['/test', '/test/[...slug]'],
|
||||||
|
['/modules', '/modules/[...slug]'],
|
||||||
|
|
||||||
|
// More specific routes should come before less specific routes
|
||||||
|
['/a/1', '/'],
|
||||||
|
['/a/2', '/'],
|
||||||
|
['/a/3', '/'],
|
||||||
|
['/test', '/'],
|
||||||
|
['/modules', '/'],
|
||||||
|
|
||||||
|
// Alphabetical order
|
||||||
|
['/modules', '/test'],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -157,6 +205,7 @@ describe('routing - createRouteManifest', () => {
|
||||||
'/src/pages/[...rest]/static.astro': `<h1>test</h1>`,
|
'/src/pages/[...rest]/static.astro': `<h1>test</h1>`,
|
||||||
'/src/pages/[...rest]/index.astro': `<h1>test</h1>`,
|
'/src/pages/[...rest]/index.astro': `<h1>test</h1>`,
|
||||||
'/src/pages/blog/index.astro': `<h1>test</h1>`,
|
'/src/pages/blog/index.astro': `<h1>test</h1>`,
|
||||||
|
'/src/pages/blog/[...slug].astro': `<h1>test</h1>`,
|
||||||
'/src/pages/[dynamic_file].astro': `<h1>test</h1>`,
|
'/src/pages/[dynamic_file].astro': `<h1>test</h1>`,
|
||||||
'/src/pages/[...other].astro': `<h1>test</h1>`,
|
'/src/pages/[...other].astro': `<h1>test</h1>`,
|
||||||
'/src/pages/static.astro': `<h1>test</h1>`,
|
'/src/pages/static.astro': `<h1>test</h1>`,
|
||||||
|
@ -179,47 +228,34 @@ describe('routing - createRouteManifest', () => {
|
||||||
fsMod: fs,
|
fsMod: fs,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(getManifestRoutes(manifest), [
|
assertRouteRelations(getManifestRoutes(manifest), [
|
||||||
{
|
// Parent route should come before rest parameters
|
||||||
route: '/',
|
['/', '/[...rest]'],
|
||||||
type: 'page',
|
['/', '/[...other]'],
|
||||||
},
|
['/blog', '/blog/[...slug]'],
|
||||||
{
|
['/[dynamic_file]', '/[dynamic_folder]/[...rest]'],
|
||||||
route: '/blog',
|
['/[dynamic_folder]', '/[dynamic_folder]/[...rest]'],
|
||||||
type: 'page',
|
|
||||||
},
|
// Static should come before dynamic
|
||||||
{
|
['/static', '/[dynamic_folder]'],
|
||||||
route: '/static',
|
['/static', '/[dynamic_file]'],
|
||||||
type: 'page',
|
|
||||||
},
|
// Static should come before rest parameters
|
||||||
{
|
['/blog', '/[...rest]'],
|
||||||
route: '/[dynamic_folder]',
|
['/blog', '/[...other]'],
|
||||||
type: 'page',
|
['/static', '/[...rest]'],
|
||||||
},
|
['/static', '/[...other]'],
|
||||||
{
|
['/static', '/[...rest]/static'],
|
||||||
route: '/[dynamic_file]',
|
['/[dynamic_folder]/static', '/[dynamic_folder]/[...rest]'],
|
||||||
type: 'page',
|
|
||||||
},
|
// Dynamic should come before rest parameters
|
||||||
{
|
['/[dynamic_file]', '/[dynamic_folder]/[...rest]'],
|
||||||
route: '/[dynamic_folder]/static',
|
|
||||||
type: 'page',
|
// More specific routes should come before less specific routes
|
||||||
},
|
['/[dynamic_folder]/[...rest]', '/[...rest]'],
|
||||||
{
|
['/[dynamic_folder]/[...rest]', '/[...other]'],
|
||||||
route: '/[dynamic_folder]/[...rest]',
|
['/blog/[...slug]', '/[...rest]'],
|
||||||
type: 'page',
|
['/blog/[...slug]', '/[...other]'],
|
||||||
},
|
|
||||||
{
|
|
||||||
route: '/[...rest]/static',
|
|
||||||
type: 'page',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
route: '/[...rest]',
|
|
||||||
type: 'page',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
route: '/[...other]',
|
|
||||||
type: 'page',
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -317,11 +353,11 @@ describe('routing - createRouteManifest', () => {
|
||||||
type: 'page',
|
type: 'page',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
route: '/',
|
route: '/contributing',
|
||||||
type: 'page',
|
type: 'page',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
route: '/contributing',
|
route: '/',
|
||||||
type: 'page',
|
type: 'page',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Reference in a new issue