From 363aab9b2faf6a3dfcb69cc2ea1678d9509890bf Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Tue, 23 Jan 2024 11:44:49 -0300 Subject: [PATCH 1/8] fix: Fix regression on dynamic sibling trees and index inside rest parameter folders --- .changeset/six-shrimps-glow.md | 30 ++++++++ .../astro/src/core/routing/manifest/create.ts | 75 ++++++++++++------- .../astro/test/units/routing/manifest.test.js | 68 ++++++++++++++++- 3 files changed, 141 insertions(+), 32 deletions(-) create mode 100644 .changeset/six-shrimps-glow.md diff --git a/.changeset/six-shrimps-glow.md b/.changeset/six-shrimps-glow.md new file mode 100644 index 000000000000..e9b67c57f3eb --- /dev/null +++ b/.changeset/six-shrimps-glow.md @@ -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 +``` diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index 6ab297a5ee22..84138258c764 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -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); } diff --git a/packages/astro/test/units/routing/manifest.test.js b/packages/astro/test/units/routing/manifest.test.js index 7bfdf6839150..f99372472b15 100644 --- a/packages/astro/test/units/routing/manifest.test.js +++ b/packages/astro/test/units/routing/manifest.test.js @@ -130,11 +130,11 @@ describe('routing - createRouteManifest', () => { expect(getManifestRoutes(manifest)).to.deep.equal([ { - route: '/', + route: '/static', type: 'page', }, { - route: '/static', + route: '/', type: 'page', }, { @@ -148,6 +148,66 @@ describe('routing - createRouteManifest', () => { ]); }); + it('route sorting respects the file tree', async () => { + const fs = createFs( + { + '/src/pages/[dynamic]/static.astro': `

test

`, + '/src/pages/[dynamic]/index.astro': `

test

`, + '/src/pages/[dynamic]/[...rest].astro': `

test

`, + '/src/pages/[...rest]/static.astro': `

test

`, + '/src/pages/[...rest]/index.astro': `

test

`, + '/src/pages/static.astro': `

test

`, + '/src/pages/index.astro': `

test

`, + }, + 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( { @@ -242,11 +302,11 @@ describe('routing - createRouteManifest', () => { type: 'page', }, { - route: '/', + route: '/contributing', type: 'page', }, { - route: '/contributing', + route: '/', type: 'page', }, { From 6a3711ad02227d12496d9507151877364cb574b8 Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Tue, 23 Jan 2024 13:53:34 -0300 Subject: [PATCH 2/8] Add extra test scenarios --- packages/astro/src/prerender/utils.ts | 2 +- .../astro/test/units/routing/manifest.test.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/prerender/utils.ts b/packages/astro/src/prerender/utils.ts index b444352135d5..5cc627343028 100644 --- a/packages/astro/src/prerender/utils.ts +++ b/packages/astro/src/prerender/utils.ts @@ -6,7 +6,7 @@ export function isServerLikeOutput(config: AstroConfig) { } export function getPrerenderDefault(config: AstroConfig) { - return config.output === 'hybrid'; + return config.output !== 'server'; } /** diff --git a/packages/astro/test/units/routing/manifest.test.js b/packages/astro/test/units/routing/manifest.test.js index f99372472b15..a4853f3f7b5a 100644 --- a/packages/astro/test/units/routing/manifest.test.js +++ b/packages/astro/test/units/routing/manifest.test.js @@ -156,6 +156,9 @@ describe('routing - createRouteManifest', () => { '/src/pages/[dynamic]/[...rest].astro': `

test

`, '/src/pages/[...rest]/static.astro': `

test

`, '/src/pages/[...rest]/index.astro': `

test

`, + '/src/pages/blog/index.astro': `

test

`, + '/src/pages/[other].astro': `

test

`, + '/src/pages/[...other].astro': `

test

`, '/src/pages/static.astro': `

test

`, '/src/pages/index.astro': `

test

`, }, @@ -177,6 +180,10 @@ describe('routing - createRouteManifest', () => { }); expect(getManifestRoutes(manifest)).to.deep.equal([ + { + "route": "/blog", + "type": "page", + }, { "route": "/static", "type": "page", @@ -197,6 +204,10 @@ describe('routing - createRouteManifest', () => { "route": "/[dynamic]/[...rest]", "type": "page", }, + { + "route": "/[other]", + "type": "page", + }, { "route": "/[...rest]/static", "type": "page", @@ -205,6 +216,10 @@ describe('routing - createRouteManifest', () => { "route": "/[...rest]", "type": "page", }, + { + "route": "/[...other]", + "type": "page", + }, ]); }); From d56b31e423c7e75f9293a5be49d7d9ae696dbf2c Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Tue, 23 Jan 2024 14:51:11 -0300 Subject: [PATCH 3/8] Make `/[foo].astro` also win over `/[foo]/[...rest].astro` --- .../astro/src/core/routing/manifest/create.ts | 40 +++++++++++++++---- .../astro/test/units/routing/manifest.test.js | 14 +++---- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index 84138258c764..9bc292b0cadd 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -203,6 +203,14 @@ function routeComparator(a: ManifestRouteData, b: ManifestRouteData) { const aIsStatic = aSegment.every((part) => !part.dynamic && !part.spread); const bIsStatic = bSegment.every((part) => !part.dynamic && !part.spread); + // Both segments are static, sort them alphabetically + // if (aIsStatic && bIsStatic) { + // const aContent = aSegment.map((part) => part.content).join(''); + // const bContent = bSegment.map((part) => part.content).join(''); + // + // return aContent.localeCompare(bContent); + // } + // Sort static routes before dynamic routes if (aIsStatic !== bIsStatic) { return aIsStatic ? -1 : 1; @@ -217,12 +225,26 @@ function routeComparator(a: ManifestRouteData, b: ManifestRouteData) { 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) { + + // 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: @@ -233,7 +255,7 @@ function routeComparator(a: ManifestRouteData, b: ManifestRouteData) { if (a.isIndex) { const followingBSegment = b.segments.at(a.segments.length); const followingBSegmentIsStatic = followingBSegment?.every((part) => !part.dynamic && !part.spread); - + return followingBSegmentIsStatic ? 1 : -1; } @@ -243,6 +265,10 @@ function routeComparator(a: ManifestRouteData, b: ManifestRouteData) { 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; diff --git a/packages/astro/test/units/routing/manifest.test.js b/packages/astro/test/units/routing/manifest.test.js index a4853f3f7b5a..c54642beccb2 100644 --- a/packages/astro/test/units/routing/manifest.test.js +++ b/packages/astro/test/units/routing/manifest.test.js @@ -181,31 +181,31 @@ describe('routing - createRouteManifest', () => { expect(getManifestRoutes(manifest)).to.deep.equal([ { - "route": "/blog", + "route": "/", "type": "page", }, { - "route": "/static", + "route": "/blog", "type": "page", }, { - "route": "/", + "route": "/static", "type": "page", }, { - "route": "/[dynamic]/static", + "route": "/[dynamic]", "type": "page", }, { - "route": "/[dynamic]", + "route": "/[other]", "type": "page", }, { - "route": "/[dynamic]/[...rest]", + "route": "/[dynamic]/static", "type": "page", }, { - "route": "/[other]", + "route": "/[dynamic]/[...rest]", "type": "page", }, { From fe3a7114ac43d51e2325fad28aa6043bf5d18867 Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Tue, 23 Jan 2024 14:57:04 -0300 Subject: [PATCH 4/8] Make `/[foo].astro` also win over `/[foo]/[...rest].astro` --- .changeset/six-shrimps-glow.md | 29 ++++++++++++------- .../astro/test/units/routing/manifest.test.js | 16 +++++----- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/.changeset/six-shrimps-glow.md b/.changeset/six-shrimps-glow.md index e9b67c57f3eb..0f10c6432eec 100644 --- a/.changeset/six-shrimps-glow.md +++ b/.changeset/six-shrimps-glow.md @@ -9,22 +9,31 @@ Considering the following tree: src/pages/ ├── index.astro ├── static.astro -├── [dynamic]/ +├── [dynamic_file].astro +├── [...rest_file].astro +├── blog/ +│ └── index.astro +├── [dynamic_folder]/ │ ├── index.astro -│ ├── [...rest].astro -│ └── static.astro -└── [...rest]/ +│ ├── static.astro +│ └── [...rest].astro +└── [...rest_folder]/ ├── 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 +/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. diff --git a/packages/astro/test/units/routing/manifest.test.js b/packages/astro/test/units/routing/manifest.test.js index c54642beccb2..1bc67b1486c7 100644 --- a/packages/astro/test/units/routing/manifest.test.js +++ b/packages/astro/test/units/routing/manifest.test.js @@ -151,13 +151,13 @@ describe('routing - createRouteManifest', () => { it('route sorting respects the file tree', async () => { const fs = createFs( { - '/src/pages/[dynamic]/static.astro': `

test

`, - '/src/pages/[dynamic]/index.astro': `

test

`, - '/src/pages/[dynamic]/[...rest].astro': `

test

`, + '/src/pages/[dynamic_folder]/static.astro': `

test

`, + '/src/pages/[dynamic_folder]/index.astro': `

test

`, + '/src/pages/[dynamic_folder]/[...rest].astro': `

test

`, '/src/pages/[...rest]/static.astro': `

test

`, '/src/pages/[...rest]/index.astro': `

test

`, '/src/pages/blog/index.astro': `

test

`, - '/src/pages/[other].astro': `

test

`, + '/src/pages/[dynamic_file].astro': `

test

`, '/src/pages/[...other].astro': `

test

`, '/src/pages/static.astro': `

test

`, '/src/pages/index.astro': `

test

`, @@ -193,19 +193,19 @@ describe('routing - createRouteManifest', () => { "type": "page", }, { - "route": "/[dynamic]", + "route": "/[dynamic_folder]", "type": "page", }, { - "route": "/[other]", + "route": "/[dynamic_file]", "type": "page", }, { - "route": "/[dynamic]/static", + "route": "/[dynamic_folder]/static", "type": "page", }, { - "route": "/[dynamic]/[...rest]", + "route": "/[dynamic_folder]/[...rest]", "type": "page", }, { From 044df1f3174389599e27745fafb075aadbb64579 Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Tue, 23 Jan 2024 15:19:02 -0300 Subject: [PATCH 5/8] Update tests --- packages/astro/test/units/routing/manifest.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/astro/test/units/routing/manifest.test.js b/packages/astro/test/units/routing/manifest.test.js index 1bc67b1486c7..027049697e2d 100644 --- a/packages/astro/test/units/routing/manifest.test.js +++ b/packages/astro/test/units/routing/manifest.test.js @@ -130,11 +130,11 @@ describe('routing - createRouteManifest', () => { expect(getManifestRoutes(manifest)).to.deep.equal([ { - route: '/static', + route: '/', type: 'page', }, { - route: '/', + route: '/static', type: 'page', }, { @@ -317,11 +317,11 @@ describe('routing - createRouteManifest', () => { type: 'page', }, { - route: '/contributing', + route: '/', type: 'page', }, { - route: '/', + route: '/contributing', type: 'page', }, { From e20b76d65d0f36848eee09ce93a132ab89e6ce40 Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Tue, 23 Jan 2024 15:28:15 -0300 Subject: [PATCH 6/8] Remove commented out code --- packages/astro/src/core/routing/manifest/create.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index 9bc292b0cadd..60c5c18acc1d 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -203,14 +203,6 @@ function routeComparator(a: ManifestRouteData, b: ManifestRouteData) { const aIsStatic = aSegment.every((part) => !part.dynamic && !part.spread); const bIsStatic = bSegment.every((part) => !part.dynamic && !part.spread); - // Both segments are static, sort them alphabetically - // if (aIsStatic && bIsStatic) { - // const aContent = aSegment.map((part) => part.content).join(''); - // const bContent = bSegment.map((part) => part.content).join(''); - // - // return aContent.localeCompare(bContent); - // } - // Sort static routes before dynamic routes if (aIsStatic !== bIsStatic) { return aIsStatic ? -1 : 1; From 4c27e10075d0c1270ea9a08b6443b8f32093beee Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 23 Jan 2024 12:39:04 -0600 Subject: [PATCH 7/8] Update .changeset/six-shrimps-glow.md --- .changeset/six-shrimps-glow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/six-shrimps-glow.md b/.changeset/six-shrimps-glow.md index 0f10c6432eec..2d925bb8c1ab 100644 --- a/.changeset/six-shrimps-glow.md +++ b/.changeset/six-shrimps-glow.md @@ -2,7 +2,7 @@ "astro": patch --- -Fix regression in routing priority for index pages in rest parameter folders and dynamic sybling trees. +Fixes a regression in routing priority for index pages in rest parameter folders and dynamic sibling trees. Considering the following tree: ``` From 4a1df8484264814d6745fb68bbc1c9f949b224fd Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Tue, 23 Jan 2024 21:04:16 -0300 Subject: [PATCH 8/8] Fix sorting cycle --- .../astro/src/core/routing/manifest/create.ts | 10 ++++++++++ .../astro/test/units/routing/manifest.test.js | 16 ++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index 60c5c18acc1d..1b4909a2e36a 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -202,6 +202,16 @@ function routeComparator(a: ManifestRouteData, b: ManifestRouteData) { 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) { diff --git a/packages/astro/test/units/routing/manifest.test.js b/packages/astro/test/units/routing/manifest.test.js index 027049697e2d..3ad1ab302e64 100644 --- a/packages/astro/test/units/routing/manifest.test.js +++ b/packages/astro/test/units/routing/manifest.test.js @@ -52,8 +52,8 @@ describe('routing - createRouteManifest', () => { it('endpoint routes are sorted before page routes', async () => { const fs = createFs( { - '/src/pages/contact-me.astro': `

test

`, - '/src/pages/sendContact.ts': `

test

`, + '/src/pages/[contact].astro': `

test

`, + '/src/pages/[contact].ts': `

test

`, }, root ); @@ -85,19 +85,19 @@ describe('routing - createRouteManifest', () => { expect(getManifestRoutes(manifest)).to.deep.equal([ { - route: '/api', - type: 'endpoint', + route: '/about', + type: 'page', }, { - route: '/sendcontact', + route: '/api', type: 'endpoint', }, { - route: '/about', - type: 'page', + route: '/[contact]', + type: 'endpoint', }, { - route: '/contact-me', + route: '/[contact]', type: 'page', }, ]);