Skip to content

Commit 4df59d4

Browse files
pengzhanboMister-Hopemeteorlxy
authoredMay 27, 2024··
fix: fix route resolving error with hash and queries (close #1561) (#1562)
Co-authored-by: Mister-Hope <mister-hope@outlook.com> Co-authored-by: meteorlxy <meteor.lxy@foxmail.com>
1 parent d3b3cc4 commit 4df59d4

21 files changed

+359
-102
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
<template>
22
<div class="e2e-theme-not-found">404 Not Found</div>
3+
<div class="e2e-theme-not-found-content"><Content /></div>
34
</template>

‎e2e/docs/404.md

+2
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
routeMeta:
33
foo: bar
44
---
5+
6+
## NotFound H2

‎e2e/docs/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
foo
2+
3+
## Home H2

‎e2e/docs/router/navigate-by-link.md

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
## Markdown Links
2+
3+
- [Home](/README.md)
4+
- [404](/404.md)
5+
- [Home with query](/README.md?home=true)
6+
- [Home with query and hash](/README.md?home=true#home)
7+
- [404 with hash](/404.md#404)
8+
- [404 with hash and query](/404.md#404?notFound=true)
9+
10+
## HTML Links
11+
12+
<a :href="$withBase('/')" class="home">Home</a>
13+
<a :href="$withBase('/404.html')" class="not-found">404</a>
14+
<a :href="$withBase('/?home=true')" class="home-with-query">Home</a>
15+
<a :href="$withBase('/?home=true#home')" class="home-with-query-and-hash">Home</a>
16+
<a :href="$withBase('/404.html#404')" class="not-found-with-hash">404</a>
17+
<a :href="$withBase('/404.html#404?notFound=true')" class="not-found-with-hash-and-query">404</a>
18+
19+
## Markdown Links with html paths
20+
21+
- [Home](/)
22+
- [404](/404.html)
23+
- [Home with query](/?home=true)
24+
- [Home with query and hash](/?home=true#home)
25+
- [404 with hash](/404.html#404)
26+
- [404 with hash and query](/404.html#404?notFound=true)
27+
28+
> Non-recommended usage. HTML paths could not be prepended with `base` correctly.

‎e2e/docs/router/navigate-by-router.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<button id="home" @click="goHome">Home</button>
2+
<button id="not-found" @click="go404">404</button>
3+
4+
<button id="home-with-query" @click="goHomeWithQuery">Home</button>
5+
<button id="home-with-query-and-hash" @click="goHomeWithQueryAndHash">Home</button>
6+
<button id="not-found-with-hash" @click="go404WithHash">404</button>
7+
<button id="not-found-with-hash-and-query" @click="go404WithHashAndQuery">404</button>
8+
9+
<script setup lang="ts">
10+
import { useRouter } from 'vuepress/client';
11+
12+
const router = useRouter();
13+
14+
const goHome = () => {
15+
router.push('/');
16+
}
17+
18+
const go404 = () => {
19+
router.push('/404.html');
20+
}
21+
22+
const goHomeWithQuery = () => {
23+
router.push('/?home=true');
24+
}
25+
26+
const goHomeWithQueryAndHash = () => {
27+
router.push('/?home=true#home');
28+
}
29+
30+
const go404WithHash = () => {
31+
router.push('/404.html#404');
32+
}
33+
34+
const go404WithHashAndQuery = () => {
35+
router.push('/404.html#404?notFound=true');
36+
}
37+
</script>

‎e2e/docs/router/navigation.md

-16
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Resolve Route FullPath
2+
3+
## Includes Query And Hash
4+
5+
- Search Query: {{ JSON.stringify(resolveRoute('/?query=1')) }}
6+
- Hash: {{ JSON.stringify(resolveRoute('/#hash')) }}
7+
- Search Query And Hash: {{ JSON.stringify(resolveRoute('/?query=1#hash')) }}
8+
- Permalink And Search Query: {{ JSON.stringify(resolveRoute('/routes/permalinks/ascii-non-ascii.md?query=1')) }}
9+
10+
<script setup>
11+
import { resolveRoute } from 'vuepress/client'
12+
</script>
+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { expect, test } from '@playwright/test'
2+
import { BASE } from '../../utils/env'
3+
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('router/navigate-by-link.html')
6+
})
7+
8+
test.describe('markdown links', () => {
9+
test('should navigate to home correctly', async ({ page }) => {
10+
await page.locator('#markdown-links + ul > li > a').nth(0).click()
11+
await expect(page).toHaveURL(`${BASE}`)
12+
await expect(page.locator('#home-h2')).toHaveText('Home H2')
13+
})
14+
15+
test('should navigate to 404 page correctly', async ({ page }) => {
16+
await page.locator('#markdown-links + ul > li > a').nth(1).click()
17+
await expect(page).toHaveURL(`${BASE}404.html`)
18+
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
19+
})
20+
21+
test('should preserve query', async ({ page }) => {
22+
await page.locator('#markdown-links + ul > li > a').nth(2).click()
23+
await expect(page).toHaveURL(`${BASE}?home=true`)
24+
await expect(page.locator('#home-h2')).toHaveText('Home H2')
25+
})
26+
27+
test('should preserve query and hash', async ({ page }) => {
28+
await page.locator('#markdown-links + ul > li > a').nth(3).click()
29+
await expect(page).toHaveURL(`${BASE}?home=true#home`)
30+
await expect(page.locator('#home-h2')).toHaveText('Home H2')
31+
})
32+
33+
test('should preserve hash', async ({ page }) => {
34+
await page.locator('#markdown-links + ul > li > a').nth(4).click()
35+
await expect(page).toHaveURL(`${BASE}404.html#404`)
36+
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
37+
})
38+
39+
test('should preserve hash and query', async ({ page }) => {
40+
await page.locator('#markdown-links + ul > li > a').nth(5).click()
41+
await expect(page).toHaveURL(`${BASE}404.html#404?notFound=true`)
42+
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
43+
})
44+
})
45+
46+
test.describe('html links', () => {
47+
test('should navigate to home correctly', async ({ page }) => {
48+
await page.locator('#html-links + p > a').nth(0).click()
49+
await expect(page).toHaveURL(`${BASE}`)
50+
await expect(page.locator('#home-h2')).toHaveText('Home H2')
51+
})
52+
53+
test('should navigate to 404 page correctly', async ({ page }) => {
54+
await page.locator('#html-links + p > a').nth(1).click()
55+
await expect(page).toHaveURL(`${BASE}404.html`)
56+
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
57+
})
58+
59+
test('should preserve query', async ({ page }) => {
60+
await page.locator('#html-links + p > a').nth(2).click()
61+
await expect(page).toHaveURL(`${BASE}?home=true`)
62+
await expect(page.locator('#home-h2')).toHaveText('Home H2')
63+
})
64+
65+
test('should preserve query and hash', async ({ page }) => {
66+
await page.locator('#html-links + p > a').nth(3).click()
67+
await expect(page).toHaveURL(`${BASE}?home=true#home`)
68+
await expect(page.locator('#home-h2')).toHaveText('Home H2')
69+
})
70+
71+
test('should preserve hash', async ({ page }) => {
72+
await page.locator('#html-links + p > a').nth(4).click()
73+
await expect(page).toHaveURL(`${BASE}404.html#404`)
74+
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
75+
})
76+
77+
test('should preserve hash and query', async ({ page }) => {
78+
await page.locator('#html-links + p > a').nth(5).click()
79+
await expect(page).toHaveURL(`${BASE}404.html#404?notFound=true`)
80+
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
81+
})
82+
})
83+
84+
test.describe('markdown links with html paths', () => {
85+
test('should navigate to home correctly', async ({ page }) => {
86+
const locator = page
87+
.locator('#markdown-links-with-html-paths + ul > li > a')
88+
.nth(0)
89+
if (BASE === '/') {
90+
await locator.click()
91+
await expect(page).toHaveURL('/')
92+
await expect(page.locator('#home-h2')).toHaveText('Home H2')
93+
} else {
94+
await expect(locator).toHaveAttribute('href', '/')
95+
await expect(locator).toHaveAttribute('target', '_blank')
96+
}
97+
})
98+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { expect, test } from '@playwright/test'
2+
import { BASE } from '../../utils/env'
3+
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('router/navigate-by-router.html')
6+
})
7+
8+
test('should navigate to home correctly', async ({ page }) => {
9+
await page.locator('#home').click()
10+
await expect(page).toHaveURL(`${BASE}`)
11+
await expect(page.locator('#home-h2')).toHaveText('Home H2')
12+
})
13+
14+
test('should navigate to 404 page correctly', async ({ page }) => {
15+
await page.locator('#not-found').click()
16+
await expect(page).toHaveURL(`${BASE}404.html`)
17+
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
18+
})
19+
20+
test('should preserve query', async ({ page }) => {
21+
await page.locator('#home-with-query').click()
22+
await expect(page).toHaveURL(`${BASE}?home=true`)
23+
await expect(page.locator('#home-h2')).toHaveText('Home H2')
24+
})
25+
26+
test('should preserve query and hash', async ({ page }) => {
27+
await page.locator('#home-with-query-and-hash').click()
28+
await expect(page).toHaveURL(`${BASE}?home=true#home`)
29+
await expect(page.locator('#home-h2')).toHaveText('Home H2')
30+
})
31+
32+
test('should preserve hash', async ({ page }) => {
33+
await page.locator('#not-found-with-hash').click()
34+
await expect(page).toHaveURL(`${BASE}404.html#404`)
35+
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
36+
})
37+
38+
test('should preserve hash and query', async ({ page }) => {
39+
await page.locator('#not-found-with-hash-and-query').click()
40+
await expect(page).toHaveURL(`${BASE}404.html#404?notFound=true`)
41+
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
42+
})

‎e2e/tests/router/navigation.spec.ts

-18
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { expect, test } from '@playwright/test'
2+
3+
const testCases = [
4+
{
5+
path: '/?query=1',
6+
notFound: false,
7+
},
8+
{
9+
path: '/#hash',
10+
notFound: false,
11+
},
12+
{
13+
path: '/?query=1#hash',
14+
notFound: false,
15+
},
16+
{
17+
path: encodeURI('/永久链接-ascii-中文/?query=1'),
18+
notFound: false,
19+
},
20+
]
21+
22+
test('should resolve routes when including both the query and hash', async ({
23+
page,
24+
}) => {
25+
const listItemsLocator = await page
26+
.locator('.e2e-theme-content #includes-query-and-hash + ul > li')
27+
.all()
28+
29+
for (const [index, li] of listItemsLocator.entries()) {
30+
const textContent = await li.textContent()
31+
const resolvedRoute = JSON.parse(/: (\{.*\})\s*$/.exec(textContent!)![1])
32+
expect(resolvedRoute.path).toEqual(testCases[index].path)
33+
expect(resolvedRoute.notFound).toEqual(testCases[index].notFound)
34+
}
35+
})

‎packages/client/src/components/RouteLink.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { computed, defineComponent, h } from 'vue'
22
import type { SlotsType, VNode } from 'vue'
33
import { useRoute, useRouter } from 'vue-router'
4-
import { resolveRoutePath } from '../router/index.js'
4+
import { resolveRouteFullPath } from '../router/index.js'
55

66
/**
77
* Forked from https://github.com/vuejs/router/blob/941b2131e80550009e5221d4db9f366b1fea3fd5/packages/router/src/RouterLink.ts#L293
@@ -91,7 +91,7 @@ export const RouteLink = defineComponent({
9191
const path = computed(() =>
9292
props.to.startsWith('#') || props.to.startsWith('?')
9393
? props.to
94-
: `${__VUEPRESS_BASE__}${resolveRoutePath(props.to, route.path).substring(1)}`,
94+
: `${__VUEPRESS_BASE__}${resolveRouteFullPath(props.to, route.path).substring(1)}`,
9595
)
9696

9797
return () =>

‎packages/client/src/router/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export type { Router, RouteLocationNormalizedLoaded } from 'vue-router'
22
export { useRoute, useRouter } from 'vue-router'
33

44
export * from './resolveRoute.js'
5+
export * from './resolveRouteFullPath.js'
56
export * from './resolveRoutePath.js'

‎packages/client/src/router/resolveRoute.ts

+17-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { splitPath } from '@vuepress/shared'
12
import { routes } from '../internal/routes.js'
23
import type { Route, RouteMeta } from '../types/index.js'
34
import { resolveRoutePath } from './resolveRoutePath.js'
@@ -15,15 +16,25 @@ export const resolveRoute = <T extends RouteMeta = RouteMeta>(
1516
path: string,
1617
currentPath?: string,
1718
): ResolvedRoute<T> => {
18-
const routePath = resolveRoutePath(path, currentPath)
19-
const route = routes.value[routePath] ?? {
20-
...routes.value['/404.html'],
21-
notFound: true,
19+
// get only the pathname from the path
20+
const { pathname, hashAndQueries } = splitPath(path)
21+
22+
// resolve the route path
23+
const routePath = resolveRoutePath(pathname, currentPath)
24+
const routeFullPath = routePath + hashAndQueries
25+
26+
// the route not found
27+
if (!routes.value[routePath]) {
28+
return {
29+
...routes.value['/404.html'],
30+
path: routeFullPath,
31+
notFound: true,
32+
} as ResolvedRoute<T>
2233
}
2334

2435
return {
25-
path: routePath,
36+
...routes.value[routePath],
37+
path: routeFullPath,
2638
notFound: false,
27-
...route,
2839
} as ResolvedRoute<T>
2940
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { splitPath } from '@vuepress/shared'
2+
import { resolveRoutePath } from './resolveRoutePath.js'
3+
4+
/**
5+
* Resolve route full path with given raw path
6+
*/
7+
export const resolveRouteFullPath = (
8+
path: string,
9+
currentPath?: string,
10+
): string => {
11+
const { pathname, hashAndQueries } = splitPath(path)
12+
return resolveRoutePath(pathname, currentPath) + hashAndQueries
13+
}

‎packages/client/src/router/resolveRoutePath.ts

+21-12
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,30 @@ import { redirects, routes } from '../internal/routes.js'
55
* Resolve route path with given raw path
66
*/
77
export const resolveRoutePath = (
8-
path: string,
8+
pathname: string,
99
currentPath?: string,
1010
): string => {
1111
// normalized path
12-
const normalizedPath = normalizeRoutePath(path, currentPath)
13-
if (routes.value[normalizedPath]) return normalizedPath
12+
const normalizedRoutePath = normalizeRoutePath(pathname, currentPath)
1413

15-
// encoded path
16-
const encodedPath = encodeURI(normalizedPath)
17-
if (routes.value[encodedPath]) return encodedPath
14+
// check if the normalized path is in routes
15+
if (routes.value[normalizedRoutePath]) return normalizedRoutePath
1816

19-
// redirected path or fallback to the normalized path
20-
return (
21-
redirects.value[normalizedPath] ||
22-
redirects.value[encodedPath] ||
23-
normalizedPath
24-
)
17+
// check encoded path
18+
const encodedRoutePath = encodeURI(normalizedRoutePath)
19+
20+
if (routes.value[encodedRoutePath]) {
21+
return encodedRoutePath
22+
}
23+
24+
// check redirected path with normalized path and encoded path
25+
const redirectedRoutePath =
26+
redirects.value[normalizedRoutePath] || redirects.value[encodedRoutePath]
27+
28+
if (redirectedRoutePath) {
29+
return redirectedRoutePath
30+
}
31+
32+
// default to normalized route path
33+
return normalizedRoutePath
2534
}

‎packages/shared/src/utils/routes/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './inferRoutePath'
22
export * from './normalizeRoutePath.js'
33
export * from './resolveLocalePath.js'
44
export * from './resolveRoutePathFromUrl.js'
5+
export * from './splitPath.js'

‎packages/shared/src/utils/routes/normalizeRoutePath.ts

+8-9
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,18 @@ import { inferRoutePath } from './inferRoutePath.js'
33
const FAKE_HOST = 'http://.'
44

55
/**
6-
* Normalize the given path to the final route path
6+
* Normalize the given pathname path to the final route path
77
*/
8-
export const normalizeRoutePath = (path: string, current?: string): string => {
9-
if (!path.startsWith('/') && current) {
8+
export const normalizeRoutePath = (
9+
pathname: string,
10+
current?: string,
11+
): string => {
12+
if (!pathname.startsWith('/') && current) {
1013
// the relative path should be resolved against the current path
1114
const loc = current.slice(0, current.lastIndexOf('/'))
1215

13-
const { pathname, search, hash } = new URL(`${loc}/${path}`, FAKE_HOST)
14-
15-
return inferRoutePath(pathname) + search + hash
16+
return inferRoutePath(new URL(`${loc}/${pathname}`, FAKE_HOST).pathname)
1617
}
1718

18-
const [pathname, ...queryAndHash] = path.split(/(\?|#)/)
19-
20-
return inferRoutePath(pathname) + queryAndHash.join('')
19+
return inferRoutePath(pathname)
2120
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const SPLIT_CHAR_REGEXP = /(#|\?)/
2+
3+
/**
4+
* Split a path into pathname and hashAndQueries
5+
*/
6+
export const splitPath = (
7+
path: string,
8+
): {
9+
pathname: string
10+
hashAndQueries: string
11+
} => {
12+
const [pathname, ...hashAndQueries] = path.split(SPLIT_CHAR_REGEXP)
13+
return {
14+
pathname,
15+
hashAndQueries: hashAndQueries.join(''),
16+
}
17+
}

‎packages/shared/tests/routes/normalizeRoutePath.spec.ts

-39
Original file line numberDiff line numberDiff line change
@@ -204,42 +204,3 @@ describe('should normalize clean paths correctly', () => {
204204
}),
205205
)
206206
})
207-
208-
describe('should normalize paths with query correctly', () => {
209-
testCases
210-
.map(([[path, current], expected]) => [
211-
[`${path}?foo=bar`, current],
212-
`${expected}?foo=bar`,
213-
])
214-
.forEach(([[path, current], expected]) =>
215-
it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
216-
expect(normalizeRoutePath(path, current)).toBe(expected)
217-
}),
218-
)
219-
})
220-
221-
describe('should normalize paths with hash correctly', () => {
222-
testCases
223-
.map(([[path, current], expected]) => [
224-
[`${path}#foobar`, current],
225-
`${expected}#foobar`,
226-
])
227-
.map(([[path, current], expected]) =>
228-
it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
229-
expect(normalizeRoutePath(path, current)).toBe(expected)
230-
}),
231-
)
232-
})
233-
234-
describe('should normalize paths with query and hash correctly', () => {
235-
testCases
236-
.map(([[path, current], expected]) => [
237-
[`${path}?foo=1&bar=2#foobar`, current],
238-
`${expected}?foo=1&bar=2#foobar`,
239-
])
240-
.map(([[path, current], expected]) =>
241-
it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
242-
expect(normalizeRoutePath(path, current)).toBe(expected)
243-
}),
244-
)
245-
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { expect, it } from 'vitest'
2+
import { splitPath } from '../../src/index.js'
3+
4+
const testCases: [string, ReturnType<typeof splitPath>][] = [
5+
['/a/b/c/', { pathname: '/a/b/c/', hashAndQueries: '' }],
6+
['/a/b/c/?a=1', { pathname: '/a/b/c/', hashAndQueries: '?a=1' }],
7+
['/a/b/c/#b', { pathname: '/a/b/c/', hashAndQueries: '#b' }],
8+
['/a/b/c/?a=1#b', { pathname: '/a/b/c/', hashAndQueries: '?a=1#b' }],
9+
['a/index.html', { pathname: 'a/index.html', hashAndQueries: '' }],
10+
['/a/index.html?a=1', { pathname: '/a/index.html', hashAndQueries: '?a=1' }],
11+
['/a/index.html#a', { pathname: '/a/index.html', hashAndQueries: '#a' }],
12+
[
13+
'/a/index.html?a=1#b',
14+
{ pathname: '/a/index.html', hashAndQueries: '?a=1#b' },
15+
],
16+
]
17+
18+
testCases.forEach(([source, expected]) => {
19+
it(`${source} -> ${expected}`, () => {
20+
expect(splitPath(source)).toEqual(expected)
21+
})
22+
})

0 commit comments

Comments
 (0)
Please sign in to comment.