Skip to content

Commit 0cc5a5a

Browse files
tlgimenespieh
andauthored
fix(gatsby): Wrong route resolved by findPageByPath function (#34070)
* use best matching route * add more comments * add tests * add matchPath specifity tests to findPageByPath * add note about possible optimalization for matchPath handling Co-authored-by: Michal Piechowiak <[email protected]>
1 parent 33049c8 commit 0cc5a5a

File tree

6 files changed

+199
-8
lines changed

6 files changed

+199
-8
lines changed

e2e-tests/production-runtime/cypress/integration/ssr.js

+27
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const staticPath = `/ssr/static-path/`
22
const paramPath = `/ssr/param-path/`
33
const wildcardPath = `/ssr/wildcard-path/`
4+
const pathRaking = `/ssr/path-ranking/`
45

56
describe(`Static path ('${staticPath}')`, () => {
67
it(`Direct visit no query params`, () => {
@@ -72,6 +73,32 @@ describe(`Param path ('${paramPath}:param')`, () => {
7273
})
7374
})
7475

76+
describe(`Path ranking resolution ('${pathRaking}*')`, () => {
77+
it(`Resolves to [...].js template at ${pathRaking}p1`, () => {
78+
cy.visit(pathRaking + `p1/`).waitForRouteChange()
79+
cy.getTestElement(`query`).contains(`{}`)
80+
cy.getTestElement(`params`).contains(`{"*":"p1"}`)
81+
})
82+
83+
it(`Resolves to [p1]/[p2].js template at ${pathRaking}p1/p2`, () => {
84+
cy.visit(pathRaking + `p1/p2/`).waitForRouteChange()
85+
cy.getTestElement(`query`).contains(`{}`)
86+
cy.getTestElement(`params`).contains(`{"p1":"p1","p2":"p2"}`)
87+
})
88+
89+
it(`Resolves to [p1]/page.js template at ${pathRaking}p1/page`, () => {
90+
cy.visit(pathRaking + `p1/page/`).waitForRouteChange()
91+
cy.getTestElement(`query`).contains(`{}`)
92+
cy.getTestElement(`params`).contains(`{"p1":"p1"}`)
93+
})
94+
95+
it(`Resolves to [...].js template at ${pathRaking}p1/p2/p3`, () => {
96+
cy.visit(pathRaking + `p1/p2/p3/`).waitForRouteChange()
97+
cy.getTestElement(`query`).contains(`{}`)
98+
cy.getTestElement(`params`).contains(`{"*":"p1/p2/p3"}`)
99+
})
100+
})
101+
75102
describe(`Wildcard path ('${wildcardPath}*')`, () => {
76103
it(`Direct visit no query params`, () => {
77104
cy.visit(wildcardPath + `foo/nested/`).waitForRouteChange()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from "react"
2+
3+
export default function StaticPath({ serverData }) {
4+
return (
5+
<div>
6+
<h2>Query</h2>
7+
<pre data-testid="query">{JSON.stringify(serverData?.arg?.query)}</pre>
8+
<h2>Params</h2>
9+
<pre data-testid="params">{JSON.stringify(serverData?.arg?.params)}</pre>
10+
<h2>Debug</h2>
11+
<pre>{JSON.stringify({ serverData }, null, 2)}</pre>
12+
</div>
13+
)
14+
}
15+
16+
export function getServerData(arg) {
17+
return {
18+
props: {
19+
arg,
20+
},
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from "react"
2+
3+
export default function StaticPath({ serverData }) {
4+
return (
5+
<div>
6+
<h2>Query</h2>
7+
<pre data-testid="query">{JSON.stringify(serverData?.arg?.query)}</pre>
8+
<h2>Params</h2>
9+
<pre data-testid="params">{JSON.stringify(serverData?.arg?.params)}</pre>
10+
<h2>Debug</h2>
11+
<pre>{JSON.stringify({ serverData }, null, 2)}</pre>
12+
</div>
13+
)
14+
}
15+
16+
export function getServerData(arg) {
17+
return {
18+
props: {
19+
arg,
20+
},
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from "react"
2+
3+
export default function StaticPath({ serverData }) {
4+
return (
5+
<div>
6+
<h2>Query</h2>
7+
<pre data-testid="query">{JSON.stringify(serverData?.arg?.query)}</pre>
8+
<h2>Params</h2>
9+
<pre data-testid="params">{JSON.stringify(serverData?.arg?.params)}</pre>
10+
<h2>Debug</h2>
11+
<pre>{JSON.stringify({ serverData }, null, 2)}</pre>
12+
</div>
13+
)
14+
}
15+
16+
export function getServerData(arg) {
17+
return {
18+
props: {
19+
arg,
20+
},
21+
}
22+
}

packages/gatsby/src/utils/__tests__/find-page-by-path.ts

+58-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,22 @@ const commonPages = [
4141
path: `/app/`,
4242
matchPath: `/app/*`,
4343
},
44+
{
45+
path: `/app/p1/page/`,
46+
matchPath: `/app/:p1/page`,
47+
},
48+
{
49+
path: `/app/p1/p2/`,
50+
matchPath: `/app/:p1/:p2`,
51+
},
52+
{
53+
path: `/app/p1/page2/`,
54+
// this is very similar to `/app/:p1/page`, point of adding 2 of those is to make sure order of pages in state
55+
// doesn't impact deterministic page selection
56+
matchPath: `/app/:p1/page2`,
57+
},
4458
`/app/static/`,
59+
`/app/static/page/`,
4560
]
4661

4762
const state = generatePagesState([...commonPages])
@@ -121,16 +136,56 @@ describe(`findPageByPath`, () => {
121136
expect(page?.path).toEqual(`/app/`)
122137
})
123138

139+
it(`Picks most specific matchPath`, () => {
140+
{
141+
const page = findPageByPath(state, `/app/foo`)
142+
expect(page).toBeDefined()
143+
expect(page?.path).toEqual(`/app/`)
144+
}
145+
146+
{
147+
const page = findPageByPath(state, `/app/foo/bar/baz`)
148+
expect(page).toBeDefined()
149+
expect(page?.path).toEqual(`/app/`)
150+
}
151+
152+
{
153+
const page = findPageByPath(state, `/app/foo/bar`)
154+
expect(page).toBeDefined()
155+
expect(page?.path).toEqual(`/app/p1/p2/`)
156+
}
157+
158+
{
159+
const page = findPageByPath(state, `/app/foo/page`)
160+
expect(page).toBeDefined()
161+
expect(page?.path).toEqual(`/app/p1/page/`)
162+
}
163+
164+
{
165+
const page = findPageByPath(state, `/app/foo/page2`)
166+
expect(page).toBeDefined()
167+
expect(page?.path).toEqual(`/app/p1/page2/`)
168+
}
169+
})
170+
124171
it(`Can match client-only path by static`, () => {
125172
const page = findPageByPath(state, `/app`)
126173
expect(page).toBeDefined()
127174
expect(page?.path).toEqual(`/app/`)
128175
})
129176

130177
it(`Will prefer static page over client-only in case both match`, () => {
131-
const page = findPageByPath(state, `/app/static`)
132-
expect(page).toBeDefined()
133-
expect(page?.path).toEqual(`/app/static/`)
178+
{
179+
const page = findPageByPath(state, `/app/static`)
180+
expect(page).toBeDefined()
181+
expect(page?.path).toEqual(`/app/static/`)
182+
}
183+
184+
{
185+
const page = findPageByPath(state, `/app/static/page`)
186+
expect(page).toBeDefined()
187+
expect(page?.path).toEqual(`/app/static/page/`)
188+
}
134189
})
135190
})
136191

packages/gatsby/src/utils/find-page-by-path.ts

+48-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,44 @@
11
import { IGatsbyPage, IGatsbyState } from "../redux/types"
2-
import { match } from "@gatsbyjs/reach-router/lib/utils"
2+
import { pick } from "@gatsbyjs/reach-router/lib/utils"
3+
4+
// Ranks and picks the best page to match. Each segment gets the highest
5+
// amount of points, then the type of segment gets an additional amount of
6+
// points where
7+
//
8+
// static > dynamic > splat > root
9+
//
10+
// This way we don't have to worry about the order of our pages, let the
11+
// computers do it.
12+
//
13+
// In the future, we could move this pagesByMatchPath computation outside this
14+
// function and save some processing power
15+
const findBestMatchingPage = (
16+
pages: Map<string, IGatsbyPage>,
17+
path: string
18+
): IGatsbyPage | null => {
19+
// Pick only routes with matchPath for better performance.
20+
// Exact match should have already been checked
21+
const pagesByMatchPath: Record<string, IGatsbyPage> = {}
22+
for (const page of pages.values()) {
23+
const matchPath = page.matchPath
24+
if (matchPath) {
25+
pagesByMatchPath[matchPath] = page
26+
}
27+
}
28+
29+
const routes = Object.keys(pagesByMatchPath).map(path => {
30+
return { path }
31+
})
32+
33+
// picks best matching route with reach router's algorithm
34+
const picked = pick(routes, path)
35+
36+
if (picked) {
37+
return pagesByMatchPath[picked.route.path]
38+
}
39+
40+
return null
41+
}
342

443
export function findPageByPath(
544
state: IGatsbyState,
@@ -46,10 +85,14 @@ export function findPageByPath(
4685
}
4786

4887
// we didn't find exact static page, time to check matchPaths
49-
for (const page of pages.values()) {
50-
if (page.matchPath && match(page.matchPath, path)) {
51-
return page
52-
}
88+
// TODO: consider using `match-paths.json` generated by `requires-writer`
89+
// to avoid looping through all pages again. Ideally generate smaller `match-paths.json`
90+
// variant that doesn't including overlapping static pages in `requires-writer` as well
91+
// as this function already checked static paths at this point
92+
const matchingPage = findBestMatchingPage(pages, path)
93+
94+
if (matchingPage) {
95+
return matchingPage
5396
}
5497

5598
if (fallbackTo404) {

0 commit comments

Comments
 (0)