diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 7f763acda8..44c2c7dc3a 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -1,6 +1,12 @@
module.exports = {
root: true,
extends: 'vuepress',
+
+ // FIXME: This should be added to `eslint-config-vuepress`
+ globals: {
+ __VUEPRESS_CLEAN_URL__: 'readonly',
+ },
+
overrides: [
{
files: ['*.ts', '*.vue', '*.cts'],
diff --git a/e2e/docs/.vuepress/theme/client/layouts/NotFound.vue b/e2e/docs/.vuepress/theme/client/layouts/NotFound.vue
index 75e27d4c0e..1e98d1e27f 100644
--- a/e2e/docs/.vuepress/theme/client/layouts/NotFound.vue
+++ b/e2e/docs/.vuepress/theme/client/layouts/NotFound.vue
@@ -1,3 +1,4 @@
404 Not Found
+
diff --git a/e2e/docs/404.md b/e2e/docs/404.md
index fac3cec274..937c74d960 100644
--- a/e2e/docs/404.md
+++ b/e2e/docs/404.md
@@ -2,3 +2,5 @@
routeMeta:
foo: bar
---
+
+## NotFound H2
diff --git a/e2e/docs/README.md b/e2e/docs/README.md
index 257cc5642c..eb63f0ccbf 100644
--- a/e2e/docs/README.md
+++ b/e2e/docs/README.md
@@ -1 +1,3 @@
foo
+
+## Home H2
diff --git a/e2e/docs/router/navigate-by-link.md b/e2e/docs/router/navigate-by-link.md
new file mode 100644
index 0000000000..8548144397
--- /dev/null
+++ b/e2e/docs/router/navigate-by-link.md
@@ -0,0 +1,34 @@
+## Markdown Links with html
+
+- [Home with query](/?home=true)
+- [Home with query and hash](/?home=true#home)
+- [404 with hash](/404.html#404)
+- [404 with hash and query](/404.html#404?notFound=true)
+
+## Markdown Links with md
+
+- [Home with query](/README.md?home=true)
+- [Home with query and hash](/README.md?home=true#home)
+- [404 with hash](/404.md#404)
+- [404 with hash and query](/404.md#404?notFound=true)
+
+## Markdown Clean Links
+
+- [Home with query](/?home=true)
+- [Home with query and hash](/?home=true#home)
+- [404 with hash](/404#404)
+- [404 with hash and query](/404#404?notFound=true)
+
+## HTML Full Links
+
+Home
+Home
+404
+404
+
+## HTML Clean Links
+
+Home
+Home
+404
+404
diff --git a/e2e/docs/router/navigate-by-router.md b/e2e/docs/router/navigate-by-router.md
new file mode 100644
index 0000000000..ec7fcb997c
--- /dev/null
+++ b/e2e/docs/router/navigate-by-router.md
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/e2e/docs/router/navigation.md b/e2e/docs/router/navigation.md
deleted file mode 100644
index 624df7c8f1..0000000000
--- a/e2e/docs/router/navigation.md
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
diff --git a/e2e/docs/router/resolve-route-query-hash.md b/e2e/docs/router/resolve-route-query-hash.md
new file mode 100644
index 0000000000..a21266220f
--- /dev/null
+++ b/e2e/docs/router/resolve-route-query-hash.md
@@ -0,0 +1,12 @@
+# Resolve Route FullPath
+
+## Includes Query And Hash
+
+- Search Query: {{ JSON.stringify(resolveRoute('/?query=1')) }}
+- Hash: {{ JSON.stringify(resolveRoute('/#hash')) }}
+- Search Query And Hash: {{ JSON.stringify(resolveRoute('/?query=1#hash')) }}
+- Permalink And Search Query: {{ JSON.stringify(resolveRoute('/routes/permalinks/ascii-non-ascii.md?query=1')) }}
+
+
diff --git a/e2e/tests/router/navigate-by-link.spec.ts b/e2e/tests/router/navigate-by-link.spec.ts
new file mode 100644
index 0000000000..d878a26146
--- /dev/null
+++ b/e2e/tests/router/navigate-by-link.spec.ts
@@ -0,0 +1,134 @@
+import { expect, test } from '@playwright/test'
+import { BASE } from '../../utils/env'
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('router/navigate-by-link.html')
+})
+
+test.describe('should preserve query', () => {
+ test('markdown links with html suffix', async ({ page }) => {
+ await page.locator('#markdown-links-with-html + ul > li > a').nth(0).click()
+ await expect(page).toHaveURL(`${BASE}?home=true`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('markdown links with md suffix', async ({ page }) => {
+ await page.locator('#markdown-links-with-md + ul > li > a').nth(0).click()
+ await expect(page).toHaveURL(`${BASE}?home=true`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('markdown clean links', async ({ page }) => {
+ await page.locator('#markdown-clean-links + ul > li > a').nth(0).click()
+ await expect(page).toHaveURL(`${BASE}?home=true`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('html full links', async ({ page }) => {
+ await page.locator('#html-full-links + p > a').nth(0).click()
+ await expect(page).toHaveURL(`${BASE}?home=true`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('html clean links', async ({ page }) => {
+ await page.locator('#html-clean-links + p > a').nth(0).click()
+ await expect(page).toHaveURL(`${BASE}?home=true`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+})
+
+test.describe('should preserve query and hash', () => {
+ test('markdown links with html suffix', async ({ page }) => {
+ await page.locator('#markdown-links-with-html + ul > li > a').nth(1).click()
+ await expect(page).toHaveURL(`${BASE}?home=true#home`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('markdown links with md suffix', async ({ page }) => {
+ await page.locator('#markdown-links-with-md + ul > li > a').nth(1).click()
+ await expect(page).toHaveURL(`${BASE}?home=true#home`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('markdown clean links', async ({ page }) => {
+ await page.locator('#markdown-clean-links + ul > li > a').nth(1).click()
+ await expect(page).toHaveURL(`${BASE}?home=true#home`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('html full links', async ({ page }) => {
+ await page.locator('#html-full-links + p > a').nth(1).click()
+ await expect(page).toHaveURL(`${BASE}?home=true#home`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('html clean links', async ({ page }) => {
+ await page.locator('#html-clean-links + p > a').nth(1).click()
+ await expect(page).toHaveURL(`${BASE}?home=true#home`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+})
+
+test.describe('should preserve hash', () => {
+ test('markdown links with html suffix', async ({ page }) => {
+ await page.locator('#markdown-links-with-html + ul > li > a').nth(2).click()
+ await expect(page).toHaveURL(`${BASE}404.html#404`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('markdown links with md suffix', async ({ page }) => {
+ await page.locator('#markdown-links-with-md + ul > li > a').nth(2).click()
+ await expect(page).toHaveURL(`${BASE}404.html#404`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('markdown clean links', async ({ page }) => {
+ await page.locator('#markdown-clean-links + ul > li > a').nth(2).click()
+ await expect(page).toHaveURL(`${BASE}404.html#404`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('html full links', async ({ page }) => {
+ await page.locator('#html-full-links + p > a').nth(2).click()
+ await expect(page).toHaveURL(`${BASE}404.html#404`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('html clean links', async ({ page }) => {
+ await page.locator('#html-clean-links + p > a').nth(2).click()
+ await expect(page).toHaveURL(`${BASE}404.html#404`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+})
+
+test.describe('should preserve hash and query', () => {
+ test('markdown links with html suffix', async ({ page }) => {
+ await page.locator('#markdown-links-with-html + ul > li > a').nth(3).click()
+ await expect(page).toHaveURL(`${BASE}404.html#404?notFound=true`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('markdown links with md suffix', async ({ page }) => {
+ await page.locator('#markdown-links-with-md + ul > li > a').nth(3).click()
+ await expect(page).toHaveURL(`${BASE}404.html#404?notFound=true`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('markdown clean links', async ({ page }) => {
+ await page.locator('#markdown-clean-links + ul > li > a').nth(3).click()
+ await expect(page).toHaveURL(`${BASE}404.html#404?notFound=true`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('html full links', async ({ page }) => {
+ await page.locator('#html-full-links + p > a').nth(3).click()
+ await expect(page).toHaveURL(`${BASE}404.html#404?notFound=true`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('html clean links', async ({ page }) => {
+ await page.locator('#html-clean-links + p > a').nth(3).click()
+ await expect(page).toHaveURL(`${BASE}404.html#404?notFound=true`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+})
diff --git a/e2e/tests/router/navigate-by-router.spec.ts b/e2e/tests/router/navigate-by-router.spec.ts
new file mode 100644
index 0000000000..9f2115e139
--- /dev/null
+++ b/e2e/tests/router/navigate-by-router.spec.ts
@@ -0,0 +1,62 @@
+import { expect, test } from '@playwright/test'
+import { BASE } from '../../utils/env'
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('router/navigate-by-router.html')
+})
+
+test.describe('should preserve query', () => {
+ test('full', async ({ page }) => {
+ await page.locator('#full .home-with-query').click()
+ await expect(page).toHaveURL(`${BASE}?home=true`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('clean', async ({ page }) => {
+ await page.locator('#clean .home-with-query').click()
+ await expect(page).toHaveURL(`${BASE}?home=true`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+})
+
+test.describe('should preserve query and hash', () => {
+ test('full', async ({ page }) => {
+ await page.locator('#full .home-with-query-and-hash').click()
+ await expect(page).toHaveURL(`${BASE}?home=true#home`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('clean', async ({ page }) => {
+ await page.locator('#clean .home-with-query-and-hash').click()
+ await expect(page).toHaveURL(`${BASE}?home=true#home`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+})
+
+test.describe('should preserve hash', () => {
+ test('full', async ({ page }) => {
+ await page.locator('#full .not-found-with-hash').click()
+ await expect(page).toHaveURL(`${BASE}404.html#404`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('clean', async ({ page }) => {
+ await page.locator('#clean .not-found-with-hash').click()
+ await expect(page).toHaveURL(`${BASE}404.html#404`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+})
+
+test.describe('should preserve hash and query', () => {
+ test('full', async ({ page }) => {
+ await page.locator('#full .not-found-with-hash-and-query').click()
+ await expect(page).toHaveURL(`${BASE}404.html#404?notFound=true`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('clean', async ({ page }) => {
+ await page.locator('#clean .not-found-with-hash-and-query').click()
+ await expect(page).toHaveURL(`${BASE}404.html#404?notFound=true`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+})
diff --git a/e2e/tests/router/navigation.spec.ts b/e2e/tests/router/navigation.spec.ts
deleted file mode 100644
index 76573acf6c..0000000000
--- a/e2e/tests/router/navigation.spec.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { expect, test } from '@playwright/test'
-import { BASE } from '../../utils/env'
-
-test('should preserve query', async ({ page }) => {
- await page.goto('router/navigation.html')
-
- await page.locator('#home').click()
-
- await expect(page).toHaveURL(`${BASE}?home=true`)
-})
-
-test('should preserve hash', async ({ page }) => {
- await page.goto('router/navigation.html')
-
- await page.locator('#not-found').click()
-
- await expect(page).toHaveURL(`${BASE}404.html#404`)
-})
diff --git a/e2e/tests/router/resolve-route-query-hash.spec.ts b/e2e/tests/router/resolve-route-query-hash.spec.ts
new file mode 100644
index 0000000000..d7554c6425
--- /dev/null
+++ b/e2e/tests/router/resolve-route-query-hash.spec.ts
@@ -0,0 +1,35 @@
+import { expect, test } from '@playwright/test'
+
+const testCases = [
+ {
+ path: '/?query=1',
+ notFound: false,
+ },
+ {
+ path: '/#hash',
+ notFound: false,
+ },
+ {
+ path: '/?query=1#hash',
+ notFound: false,
+ },
+ {
+ path: encodeURI('/永久链接-ascii-中文/?query=1'),
+ notFound: false,
+ },
+]
+
+test('should resolve routes when including both the query and hash', async ({
+ page,
+}) => {
+ const listItemsLocator = await page
+ .locator('.e2e-theme-content #includes-query-and-hash + ul > li')
+ .all()
+
+ for (const [index, li] of listItemsLocator.entries()) {
+ const textContent = await li.textContent()
+ const resolvedRoute = JSON.parse(/: (\{.*\})\s*$/.exec(textContent!)![1])
+ expect(resolvedRoute.path).toEqual(testCases[index].path)
+ expect(resolvedRoute.notFound).toEqual(testCases[index].notFound)
+ }
+})
diff --git a/packages/bundler-vite/src/plugins/vuepressMainPlugin.ts b/packages/bundler-vite/src/plugins/vuepressMainPlugin.ts
index 05a53d3b0d..9c73ea36a6 100644
--- a/packages/bundler-vite/src/plugins/vuepressMainPlugin.ts
+++ b/packages/bundler-vite/src/plugins/vuepressMainPlugin.ts
@@ -205,6 +205,7 @@ const resolveDefine = async ({
const define: UserConfig['define'] = {
__VUEPRESS_VERSION__: JSON.stringify(app.version),
__VUEPRESS_BASE__: JSON.stringify(app.options.base),
+ __VUEPRESS_CLEAN_URL__: JSON.stringify(app.options.route.cleanUrl),
__VUEPRESS_DEV__: JSON.stringify(!isBuild),
__VUEPRESS_SSR__: JSON.stringify(isServer),
// @see http://link.vuejs.org/feature-flags
diff --git a/packages/bundler-webpack/src/config/handlePluginDefine.ts b/packages/bundler-webpack/src/config/handlePluginDefine.ts
index d118844d48..7681c70f5a 100644
--- a/packages/bundler-webpack/src/config/handlePluginDefine.ts
+++ b/packages/bundler-webpack/src/config/handlePluginDefine.ts
@@ -21,6 +21,7 @@ export const handlePluginDefine = async ({
{
__VUEPRESS_VERSION__: JSON.stringify(app.version),
__VUEPRESS_BASE__: JSON.stringify(app.options.base),
+ __VUEPRESS_CLEAN_URL__: JSON.stringify(app.options.route.cleanUrl),
__VUEPRESS_DEV__: JSON.stringify(!isBuild),
__VUEPRESS_SSR__: JSON.stringify(isServer),
// @see http://link.vuejs.org/feature-flags
diff --git a/packages/cli/src/commands/dev/watchPageFiles.ts b/packages/cli/src/commands/dev/watchPageFiles.ts
index 48dcee2060..10c1bf7ad7 100644
--- a/packages/cli/src/commands/dev/watchPageFiles.ts
+++ b/packages/cli/src/commands/dev/watchPageFiles.ts
@@ -41,7 +41,7 @@ export const watchPageFiles = (app: App): FSWatcher[] => {
app.pages.forEach((page) => addDeps(page))
// watch page files
- const pagesWatcher = chokidar.watch(app.options.pagePatterns, {
+ const pagesWatcher = chokidar.watch(app.options.route.pagePatterns, {
cwd: app.dir.source(),
ignoreInitial: true,
})
diff --git a/packages/client/src/components/RouteLink.ts b/packages/client/src/components/RouteLink.ts
index 99c4875533..1b1e62b157 100644
--- a/packages/client/src/components/RouteLink.ts
+++ b/packages/client/src/components/RouteLink.ts
@@ -1,7 +1,7 @@
import { computed, defineComponent, h } from 'vue'
import type { SlotsType, VNode } from 'vue'
import { useRoute, useRouter } from 'vue-router'
-import { resolveRoutePath } from '../router/index.js'
+import { resolveRouteFullPath } from '../router/index.js'
/**
* Forked from https://github.com/vuejs/router/blob/941b2131e80550009e5221d4db9f366b1fea3fd5/packages/router/src/RouterLink.ts#L293
@@ -91,7 +91,7 @@ export const RouteLink = defineComponent({
const path = computed(() =>
props.to.startsWith('#') || props.to.startsWith('?')
? props.to
- : `${__VUEPRESS_BASE__}${resolveRoutePath(props.to, route.path).substring(1)}`,
+ : `${__VUEPRESS_BASE__}${resolveRouteFullPath(props.to, route.path).substring(1)}`,
)
return () =>
diff --git a/packages/client/src/router/index.ts b/packages/client/src/router/index.ts
index dae3d58e2c..b7801c6fd7 100644
--- a/packages/client/src/router/index.ts
+++ b/packages/client/src/router/index.ts
@@ -2,4 +2,6 @@ export type { Router, RouteLocationNormalizedLoaded } from 'vue-router'
export { useRoute, useRouter } from 'vue-router'
export * from './resolveRoute.js'
+export * from './resolveRouteFullPath.js'
+export * from './resolveRouteKey.js'
export * from './resolveRoutePath.js'
diff --git a/packages/client/src/router/resolveRoute.ts b/packages/client/src/router/resolveRoute.ts
index e4345c9a9b..0bbb627016 100644
--- a/packages/client/src/router/resolveRoute.ts
+++ b/packages/client/src/router/resolveRoute.ts
@@ -1,6 +1,7 @@
+import { resolveRoutePathWithExt, splitPath } from '@vuepress/shared'
import { routes } from '../internal/routes.js'
import type { Route, RouteMeta } from '../types/index.js'
-import { resolveRoutePath } from './resolveRoutePath.js'
+import { resolveRouteKey } from './resolveRouteKey.js'
export interface ResolvedRoute
extends Route {
@@ -15,15 +16,26 @@ export const resolveRoute = (
path: string,
currentPath?: string,
): ResolvedRoute => {
- const routePath = resolveRoutePath(path, currentPath)
- const route = routes.value[routePath] ?? {
- ...routes.value['/404.html'],
- notFound: true,
+ const { pathname, hashAndQueries } = splitPath(path)
+
+ // calculate the route key and full path
+ const routeKey = resolveRouteKey(pathname, currentPath)
+ const routeFullPath =
+ (__VUEPRESS_CLEAN_URL__ ? routeKey : resolveRoutePathWithExt(routeKey)) +
+ hashAndQueries
+
+ // the route not found
+ if (!routes.value[routeKey]) {
+ return {
+ ...routes.value['/404'],
+ path: routeFullPath,
+ notFound: true,
+ } as ResolvedRoute
}
return {
- path: routePath,
+ ...routes.value[routeKey],
+ path: routeFullPath,
notFound: false,
- ...route,
} as ResolvedRoute
}
diff --git a/packages/client/src/router/resolveRouteFullPath.ts b/packages/client/src/router/resolveRouteFullPath.ts
new file mode 100644
index 0000000000..694386d74b
--- /dev/null
+++ b/packages/client/src/router/resolveRouteFullPath.ts
@@ -0,0 +1,13 @@
+import { splitPath } from '@vuepress/shared'
+import { resolveRoutePath } from './resolveRoutePath.js'
+
+/**
+ * Resolve route full path with given raw path
+ */
+export const resolveRouteFullPath = (
+ path: string,
+ currentPath?: string,
+): string => {
+ const { pathname, hashAndQueries } = splitPath(path)
+ return resolveRoutePath(pathname, currentPath) + hashAndQueries
+}
diff --git a/packages/client/src/router/resolveRouteKey.ts b/packages/client/src/router/resolveRouteKey.ts
new file mode 100644
index 0000000000..8c2e43d371
--- /dev/null
+++ b/packages/client/src/router/resolveRouteKey.ts
@@ -0,0 +1,34 @@
+import { normalizeRoutePath } from '@vuepress/shared'
+import { redirects, routes } from '../internal/routes.js'
+
+/**
+ * Resolve route path with given raw path
+ */
+export const resolveRouteKey = (
+ pathname: string,
+ currentPath?: string,
+): string => {
+ // normalized path
+ const routePath = normalizeRoutePath(pathname, currentPath)
+
+ // check if the normalized path is in routes
+ if (routes.value[routePath]) return routePath
+
+ // check encoded path
+ const encodedRoutePath = encodeURI(routePath)
+
+ if (routes.value[encodedRoutePath]) {
+ return encodedRoutePath
+ }
+
+ // check redirected path with normalized path and encoded path
+ const redirectedRoutePath =
+ redirects.value[routePath] || redirects.value[encodedRoutePath]
+
+ if (redirectedRoutePath) {
+ return redirectedRoutePath
+ }
+
+ // default to normalized route path
+ return routePath
+}
diff --git a/packages/client/src/router/resolveRoutePath.ts b/packages/client/src/router/resolveRoutePath.ts
index f5aece8f9b..964b7d8399 100644
--- a/packages/client/src/router/resolveRoutePath.ts
+++ b/packages/client/src/router/resolveRoutePath.ts
@@ -1,25 +1,15 @@
-import { normalizeRoutePath } from '@vuepress/shared'
-import { redirects, routes } from '../internal/routes.js'
+import { resolveRoutePathWithExt } from '@vuepress/shared'
+import { resolveRouteKey } from './resolveRouteKey.js'
/**
* Resolve route path with given raw path
*/
export const resolveRoutePath = (
- path: string,
+ pathname: string,
currentPath?: string,
): string => {
- // normalized path
- const normalizedPath = normalizeRoutePath(path, currentPath)
- if (routes.value[normalizedPath]) return normalizedPath
+ // clean route path format used as key in routes
+ const routeKey = resolveRouteKey(pathname, currentPath)
- // encoded path
- const encodedPath = encodeURI(normalizedPath)
- if (routes.value[encodedPath]) return encodedPath
-
- // redirected path or fallback to the normalized path
- return (
- redirects.value[normalizedPath] ||
- redirects.value[encodedPath] ||
- normalizedPath
- )
+ return __VUEPRESS_CLEAN_URL__ ? routeKey : resolveRoutePathWithExt(routeKey)
}
diff --git a/packages/client/src/setupGlobalComputed.ts b/packages/client/src/setupGlobalComputed.ts
index 57197148a6..f33b8ee3fd 100644
--- a/packages/client/src/setupGlobalComputed.ts
+++ b/packages/client/src/setupGlobalComputed.ts
@@ -5,6 +5,7 @@ import { clientDataSymbol } from './composables/index.js'
import { redirects, routes } from './internal/routes.js'
import { siteData } from './internal/siteData.js'
import { resolvers } from './resolvers.js'
+import { resolveRouteKey } from './router/resolveRouteKey.js'
import type {
ClientConfig,
ClientData,
@@ -46,10 +47,10 @@ export const setupGlobalComputed = (
// handle page data HMR
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
__VUE_HMR_RUNTIME__.updatePageData = async (newPageData: PageData) => {
- const oldPageChunk = await routes.value[newPageData.path].loader()
+ const routeKey = resolveRouteKey(newPageData.path)
+ const oldPageChunk = await routes.value[routeKey].loader()
const newPageChunk = { comp: oldPageChunk.comp, data: newPageData }
- routes.value[newPageData.path].loader = () =>
- Promise.resolve(newPageChunk)
+ routes.value[routeKey].loader = () => Promise.resolve(newPageChunk)
if (
newPageData.path ===
router.currentRoute.value.meta._pageChunk?.data.path
diff --git a/packages/client/types.d.ts b/packages/client/types.d.ts
index 2d55050eb3..fdaa55b854 100644
--- a/packages/client/types.d.ts
+++ b/packages/client/types.d.ts
@@ -1,6 +1,7 @@
declare const __VUEPRESS_VERSION__: string
declare const __VUEPRESS_BASE__: string
declare const __VUEPRESS_DEV__: boolean
+declare const __VUEPRESS_CLEAN_URL__: boolean
declare const __VUEPRESS_SSR__: boolean
declare const __VUE_HMR_RUNTIME__: Record
declare const __VUE_OPTIONS_API__: boolean
diff --git a/packages/core/src/app/prepare/prepareRoutes.ts b/packages/core/src/app/prepare/prepareRoutes.ts
index b3556e8041..780a0d4721 100644
--- a/packages/core/src/app/prepare/prepareRoutes.ts
+++ b/packages/core/src/app/prepare/prepareRoutes.ts
@@ -51,7 +51,10 @@ export const redirects = JSON.parse(${JSON.stringify(
JSON.stringify(
Object.fromEntries(
app.pages.flatMap((page) =>
- resolvePageRedirects(page).map((redirect) => [redirect, page.path]),
+ resolvePageRedirects(page).map((redirect) => [
+ redirect,
+ page.routeKey,
+ ]),
),
),
),
@@ -60,8 +63,8 @@ export const redirects = JSON.parse(${JSON.stringify(
export const routes = Object.fromEntries([
${app.pages
.map(
- ({ chunkFilePath, chunkName, path, routeMeta }) =>
- ` [${JSON.stringify(path)}, { loader: () => import(${chunkName ? `/* webpackChunkName: "${chunkName}" */` : ''}${JSON.stringify(chunkFilePath)}), meta: ${JSON.stringify(routeMeta)} }],`,
+ ({ chunkFilePath, chunkName, routeKey, routeMeta }) =>
+ ` [${JSON.stringify(routeKey)}, { loader: () => import(${chunkName ? `/* webpackChunkName: "${chunkName}" */` : ''}${JSON.stringify(chunkFilePath)}), meta: ${JSON.stringify(routeMeta)} }],`,
)
.join('\n')}
]);
diff --git a/packages/core/src/app/resolveAppOptions.ts b/packages/core/src/app/resolveAppOptions.ts
index d851c3d969..04538847b1 100644
--- a/packages/core/src/app/resolveAppOptions.ts
+++ b/packages/core/src/app/resolveAppOptions.ts
@@ -38,8 +38,16 @@ export const resolveAppOptions = ({
bundler,
debug = false,
markdown = {},
- pagePatterns = ['**/*.md', '!.vuepress', '!node_modules'],
- permalinkPattern = null,
+ pagePatterns: _pagePatterns,
+ permalinkPattern: _permalinkPattern,
+ route: {
+ cleanUrl = false,
+ pagePatterns = ['**/*.md', '!.vuepress', '!node_modules'],
+ permalinkPattern = null,
+ } = {
+ pagePatterns: _pagePatterns,
+ permalinkPattern: _permalinkPattern,
+ },
plugins = [],
theme,
}: AppConfig): AppOptions => ({
@@ -65,8 +73,11 @@ export const resolveAppOptions = ({
bundler,
debug,
markdown,
- pagePatterns,
- permalinkPattern,
+ route: {
+ cleanUrl,
+ pagePatterns,
+ permalinkPattern,
+ },
plugins,
theme,
})
diff --git a/packages/core/src/app/resolveAppPages.ts b/packages/core/src/app/resolveAppPages.ts
index 2f721fa7fd..039c18c276 100644
--- a/packages/core/src/app/resolveAppPages.ts
+++ b/packages/core/src/app/resolveAppPages.ts
@@ -11,7 +11,7 @@ export const resolveAppPages = async (app: App): Promise => {
log('resolveAppPages start')
// resolve page absolute file paths according to the page patterns
- const pageFilePaths = await globby(app.options.pagePatterns, {
+ const pageFilePaths = await globby(app.options.route.pagePatterns, {
absolute: true,
cwd: app.dir.source(),
})
@@ -22,7 +22,7 @@ export const resolveAppPages = async (app: App): Promise => {
)
// find the 404 page
- const notFoundPage = pages.find((page) => page.path === '/404.html')
+ const notFoundPage = pages.find((page) => page.routeKey === '/404')
// if there is a 404 page, set the default layout to NotFound
if (notFoundPage) {
diff --git a/packages/core/src/page/createPage.ts b/packages/core/src/page/createPage.ts
index ab9b05deb6..c5147133f3 100644
--- a/packages/core/src/page/createPage.ts
+++ b/packages/core/src/page/createPage.ts
@@ -10,6 +10,7 @@ import { resolvePageHtmlInfo } from './resolvePageHtmlInfo.js'
import { resolvePageLang } from './resolvePageLang.js'
import { resolvePagePath } from './resolvePagePath.js'
import { resolvePagePermalink } from './resolvePagePermalink.js'
+import { resolvePageRouteKey } from './resolvePageRouteKey.js'
import { resolvePageRouteMeta } from './resolvePageRouteMeta.js'
import { resolvePageSlug } from './resolvePageSlug.js'
@@ -75,6 +76,9 @@ export const createPage = async (
// resolve page path
const path = resolvePagePath({ permalink, pathInferred, options })
+ // resolve page routeKey
+ const routeKey = resolvePageRouteKey(path)
+
// resolve page rendered html file path
const { htmlFilePath, htmlFilePathRelative } = resolvePageHtmlInfo({
app,
@@ -118,6 +122,7 @@ export const createPage = async (
pathInferred,
pathLocale,
permalink,
+ routeKey,
routeMeta,
sfcBlocks,
slug,
diff --git a/packages/core/src/page/resolvePagePermalink.ts b/packages/core/src/page/resolvePagePermalink.ts
index 6a549085f2..14ca26bb13 100644
--- a/packages/core/src/page/resolvePagePermalink.ts
+++ b/packages/core/src/page/resolvePagePermalink.ts
@@ -34,7 +34,7 @@ export const resolvePagePermalink = ({
}
const permalinkPattern =
- frontmatter.permalinkPattern || app.options.permalinkPattern
+ frontmatter.permalinkPattern || app.options.route.permalinkPattern
if (!isString(permalinkPattern)) {
return null
diff --git a/packages/core/src/page/resolvePageRouteKey.ts b/packages/core/src/page/resolvePageRouteKey.ts
new file mode 100644
index 0000000000..33d523832d
--- /dev/null
+++ b/packages/core/src/page/resolvePageRouteKey.ts
@@ -0,0 +1,6 @@
+/**
+ * Resolve the final route path of a page
+ */
+export const resolvePageRouteKey = (path: string): string =>
+ // convert to the clean format
+ path.replace(/\.html$/, '').replace(/\/index$/i, '/')
diff --git a/packages/core/src/types/app/options.ts b/packages/core/src/types/app/options.ts
index 93271fd4bf..6721a3b930 100644
--- a/packages/core/src/types/app/options.ts
+++ b/packages/core/src/types/app/options.ts
@@ -5,6 +5,12 @@ import type { Bundler } from '../bundler.js'
import type { PluginConfig } from '../plugin.js'
import type { Theme } from '../theme.js'
+export interface RouteOptions {
+ cleanUrl?: boolean
+ pagePatterns?: string[]
+ permalinkPattern?: string | null
+}
+
/**
* Vuepress app common config that shared between dev and build
*/
@@ -14,11 +20,9 @@ export interface AppConfigCommon extends Partial {
temp?: string
cache?: string
public?: string
-
debug?: boolean
markdown?: MarkdownOptions
- pagePatterns?: string[]
- permalinkPattern?: string | null
+ route?: RouteOptions
bundler: Bundler
theme: Theme
plugins?: PluginConfig
@@ -95,9 +99,20 @@ export interface AppConfigBuild {
/**
* Vuepress app config
*/
-export type AppConfig = AppConfigCommon & AppConfigDev & AppConfigBuild
+export type AppConfig = AppConfigCommon &
+ AppConfigDev &
+ AppConfigBuild & {
+ /** @deprecated use route.pagePatterns instead */
+ pagePatterns?: string[]
+ /** @deprecated use route.permalinkPattern instead */
+ permalinkPattern?: string | null
+ }
/**
* Vuepress app options
*/
-export type AppOptions = Required
+export type AppOptions = Required<
+ AppConfigCommon & AppConfigDev & AppConfigBuild
+> & {
+ route: Required
+}
diff --git a/packages/core/src/types/page.ts b/packages/core/src/types/page.ts
index 5aadd8d7ae..ca8c7b03af 100644
--- a/packages/core/src/types/page.ts
+++ b/packages/core/src/types/page.ts
@@ -72,6 +72,11 @@ export type Page<
*/
permalink: string | null
+ /**
+ * Key in routes record
+ */
+ routeKey: string
+
/**
* Custom data to be attached to route record
*/
diff --git a/packages/core/tests/app/resolveAppOptions.spec.ts b/packages/core/tests/app/resolveAppOptions.spec.ts
index a7f3ff125c..1a269dfac6 100644
--- a/packages/core/tests/app/resolveAppOptions.spec.ts
+++ b/packages/core/tests/app/resolveAppOptions.spec.ts
@@ -30,8 +30,11 @@ describe('core > app > resolveAppOptions', () => {
host: '0.0.0.0',
port: 8080,
open: false,
- pagePatterns: ['**/*.md', '!.vuepress', '!node_modules'],
- permalinkPattern: null,
+ route: {
+ cleanUrl: false,
+ pagePatterns: ['**/*.md', '!.vuepress', '!node_modules'],
+ permalinkPattern: null,
+ },
templateDev: path.normalize(
require.resolve('@vuepress/client/templates/dev.html'),
),
diff --git a/packages/markdown/src/plugins/linksPlugin/linksPlugin.ts b/packages/markdown/src/plugins/linksPlugin/linksPlugin.ts
index 5fb407656a..d2d0e05b58 100644
--- a/packages/markdown/src/plugins/linksPlugin/linksPlugin.ts
+++ b/packages/markdown/src/plugins/linksPlugin/linksPlugin.ts
@@ -1,4 +1,8 @@
-import { inferRoutePath, isLinkExternal } from '@vuepress/shared'
+import {
+ inferRoutePath,
+ isLinkExternal,
+ resolveRoutePathWithExt,
+} from '@vuepress/shared'
import type { PluginWithOptions } from 'markdown-it'
import type Token from 'markdown-it/lib/token.mjs'
import type { MarkdownEnv } from '../../types.js'
@@ -114,17 +118,22 @@ export const linksPlugin: PluginWithOptions = (
// normalize markdown file path to route path
// we are removing the `base` from absolute path because it should not be
// passed to `` or ``
- const normalizedPath = inferRoutePath(
- absolutePath
- ? absolutePath.replace(new RegExp(`^${base}`), '/')
- : relativePath,
+ const normalizedPath = resolveRoutePathWithExt(
+ inferRoutePath(
+ absolutePath
+ ? absolutePath.replace(new RegExp(`^${base}`), '/')
+ : relativePath,
+ ),
)
// replace the original href link with the normalized path
hrefAttr[1] = `${normalizedPath}${rawHashAndQueries}`
// set `hasOpenInternalLink` to modify the ending tag
hasOpenInternalLink = true
} else {
- const normalizedPath = inferRoutePath(absolutePath ?? relativePath)
+ // ext is added here
+ const normalizedPath = resolveRoutePathWithExt(
+ inferRoutePath(absolutePath ?? relativePath),
+ )
// replace the original href link with the normalized path
hrefAttr[1] = `${normalizedPath}${rawHashAndQueries}`
}
diff --git a/packages/shared/src/utils/routes/index.ts b/packages/shared/src/utils/routes/index.ts
index a83b4caa70..e0558e4a4c 100644
--- a/packages/shared/src/utils/routes/index.ts
+++ b/packages/shared/src/utils/routes/index.ts
@@ -1,4 +1,6 @@
export * from './inferRoutePath'
export * from './normalizeRoutePath.js'
+export * from './resolveRoutePathWithExt.js'
export * from './resolveLocalePath.js'
export * from './resolveRoutePathFromUrl.js'
+export * from './splitPath.js'
diff --git a/packages/shared/src/utils/routes/inferRoutePath.ts b/packages/shared/src/utils/routes/inferRoutePath.ts
index 0a1ba2e4aa..863b394852 100644
--- a/packages/shared/src/utils/routes/inferRoutePath.ts
+++ b/packages/shared/src/utils/routes/inferRoutePath.ts
@@ -6,20 +6,20 @@ export const inferRoutePath = (path: string): string => {
if (!path || path.endsWith('/')) return path
// convert README.md to index.html
- let routePath = path.replace(/(^|\/)README.md$/i, '$1index.html')
+ let routePath = path.replace(/(^|\/)README.md$/i, '$1index')
- // convert /foo/bar.md to /foo/bar.html
+ // convert /foo/bar.md to /foo/bar
if (routePath.endsWith('.md')) {
- routePath = routePath.substring(0, routePath.length - 3) + '.html'
+ routePath = routePath.substring(0, routePath.length - 3)
}
- // convert /foo/bar to /foo/bar.html
- else if (!routePath.endsWith('.html')) {
- routePath = routePath + '.html'
+ // convert /foo/bar.html to /foo/bar
+ else if (routePath.endsWith('.html')) {
+ routePath = routePath.substring(0, routePath.length - 5)
}
- // convert /foo/index.html to /foo/
- if (routePath.endsWith('/index.html')) {
- routePath = routePath.substring(0, routePath.length - 10)
+ // convert /foo/index to /foo/
+ if (routePath.endsWith('/index')) {
+ routePath = routePath.substring(0, routePath.length - 5)
}
return routePath
diff --git a/packages/shared/src/utils/routes/normalizeRoutePath.ts b/packages/shared/src/utils/routes/normalizeRoutePath.ts
index 334fc308c1..6c73ef172a 100644
--- a/packages/shared/src/utils/routes/normalizeRoutePath.ts
+++ b/packages/shared/src/utils/routes/normalizeRoutePath.ts
@@ -3,19 +3,18 @@ import { inferRoutePath } from './inferRoutePath.js'
const FAKE_HOST = 'http://.'
/**
- * Normalize the given path to the final route path
+ * Normalize the given pathname path to the final route path
*/
-export const normalizeRoutePath = (path: string, current?: string): string => {
- if (!path.startsWith('/') && current) {
+export const normalizeRoutePath = (
+ pathname: string,
+ current?: string,
+): string => {
+ if (!pathname.startsWith('/') && current) {
// the relative path should be resolved against the current path
const loc = current.slice(0, current.lastIndexOf('/'))
- const { pathname, search, hash } = new URL(`${loc}/${path}`, FAKE_HOST)
-
- return inferRoutePath(pathname) + search + hash
+ return inferRoutePath(new URL(`${loc}/${pathname}`, FAKE_HOST).pathname)
}
- const [pathname, ...queryAndHash] = path.split(/(\?|#)/)
-
- return inferRoutePath(pathname) + queryAndHash.join('')
+ return inferRoutePath(pathname)
}
diff --git a/packages/shared/src/utils/routes/resolveRoutePathWithExt.ts b/packages/shared/src/utils/routes/resolveRoutePathWithExt.ts
new file mode 100644
index 0000000000..a3d9924af8
--- /dev/null
+++ b/packages/shared/src/utils/routes/resolveRoutePathWithExt.ts
@@ -0,0 +1,2 @@
+export const resolveRoutePathWithExt = (routePath: string): string =>
+ routePath.endsWith('/') ? routePath : routePath + '.html'
diff --git a/packages/shared/src/utils/routes/splitPath.ts b/packages/shared/src/utils/routes/splitPath.ts
new file mode 100644
index 0000000000..2aa3906dc6
--- /dev/null
+++ b/packages/shared/src/utils/routes/splitPath.ts
@@ -0,0 +1,17 @@
+const SPLIT_CHAR_REGEXP = /(#|\?)/
+
+/**
+ * Split a path into pathname and hashAndQueries
+ */
+export const splitPath = (
+ path: string,
+): {
+ pathname: string
+ hashAndQueries: string
+} => {
+ const [pathname, ...hashAndQueries] = path.split(SPLIT_CHAR_REGEXP)
+ return {
+ pathname,
+ hashAndQueries: hashAndQueries.join(''),
+ }
+}
diff --git a/packages/shared/tests/routes/inferRoutePath.spec.ts b/packages/shared/tests/routes/inferRoutePath.spec.ts
index 053fa22a89..71960e6edf 100644
--- a/packages/shared/tests/routes/inferRoutePath.spec.ts
+++ b/packages/shared/tests/routes/inferRoutePath.spec.ts
@@ -15,19 +15,19 @@ const testCases = [
['/foo/index.md', '/foo/'],
['/foo/index.html', '/foo/'],
['/foo/index', '/foo/'],
- ['README.md', 'index.html'],
- ['readme.md', 'index.html'],
- ['index.md', 'index.html'],
- ['index.html', 'index.html'],
- ['index', 'index.html'],
+ ['README.md', 'index'],
+ ['readme.md', 'index'],
+ ['index.md', 'index'],
+ ['index.html', 'index'],
+ ['index', 'index'],
// absolute non-index
- ['/foo', '/foo.html'],
- ['/foo.md', '/foo.html'],
- ['/foo.html', '/foo.html'],
- ['/foo/bar', '/foo/bar.html'],
- ['/foo/bar.md', '/foo/bar.html'],
- ['/foo/bar.html', '/foo/bar.html'],
+ ['/foo', '/foo'],
+ ['/foo.md', '/foo'],
+ ['/foo.html', '/foo'],
+ ['/foo/bar', '/foo/bar'],
+ ['/foo/bar.md', '/foo/bar'],
+ ['/foo/bar.html', '/foo/bar'],
// relative index without current
['foo/', 'foo/'],
@@ -38,19 +38,19 @@ const testCases = [
['foo/index', 'foo/'],
// relative non index without current
- ['foo', 'foo.html'],
- ['foo.md', 'foo.html'],
- ['foo.html', 'foo.html'],
- ['foo/bar', 'foo/bar.html'],
- ['foo/bar.md', 'foo/bar.html'],
- ['foo/bar.html', 'foo/bar.html'],
+ ['foo', 'foo'],
+ ['foo.md', 'foo'],
+ ['foo.html', 'foo'],
+ ['foo/bar', 'foo/bar'],
+ ['foo/bar.md', 'foo/bar'],
+ ['foo/bar.html', 'foo/bar'],
// unexpected corner cases
['', ''],
- ['.md', '.html'],
- ['foo/.md', 'foo/.html'],
- ['/.md', '/.html'],
- ['/foo/.md', '/foo/.html'],
+ ['.md', ''],
+ ['foo/.md', 'foo/'],
+ ['/.md', '/'],
+ ['/foo/.md', '/foo/'],
]
describe('should normalize clean paths correctly', () => {
diff --git a/packages/shared/tests/routes/normalizeRoutePath.spec.ts b/packages/shared/tests/routes/normalizeRoutePath.spec.ts
index d18a00b035..85fec82d27 100644
--- a/packages/shared/tests/routes/normalizeRoutePath.spec.ts
+++ b/packages/shared/tests/routes/normalizeRoutePath.spec.ts
@@ -15,19 +15,19 @@ const testCases = [
[['/foo/index.md'], '/foo/'],
[['/foo/index.html'], '/foo/'],
[['/foo/index'], '/foo/'],
- [['README.md'], 'index.html'],
- [['readme.md'], 'index.html'],
- [['index.md'], 'index.html'],
- [['index.html'], 'index.html'],
- [['index'], 'index.html'],
+ [['README.md'], 'index'],
+ [['readme.md'], 'index'],
+ [['index.md'], 'index'],
+ [['index.html'], 'index'],
+ [['index'], 'index'],
// absolute non-index
- [['/foo'], '/foo.html'],
- [['/foo.md'], '/foo.html'],
- [['/foo.html'], '/foo.html'],
- [['/foo/bar'], '/foo/bar.html'],
- [['/foo/bar.md'], '/foo/bar.html'],
- [['/foo/bar.html'], '/foo/bar.html'],
+ [['/foo'], '/foo'],
+ [['/foo.md'], '/foo'],
+ [['/foo.html'], '/foo'],
+ [['/foo/bar'], '/foo/bar'],
+ [['/foo/bar.md'], '/foo/bar'],
+ [['/foo/bar.html'], '/foo/bar'],
// relative index without current
[['foo/'], 'foo/'],
@@ -38,163 +38,163 @@ const testCases = [
[['foo/index'], 'foo/'],
// relative non index without current
- [['foo'], 'foo.html'],
- [['foo.md'], 'foo.html'],
- [['foo.html'], 'foo.html'],
- [['foo/bar'], 'foo/bar.html'],
- [['foo/bar.md'], 'foo/bar.html'],
- [['foo/bar.html'], 'foo/bar.html'],
+ [['foo'], 'foo'],
+ [['foo.md'], 'foo'],
+ [['foo.html'], 'foo'],
+ [['foo/bar'], 'foo/bar'],
+ [['foo/bar.md'], 'foo/bar'],
+ [['foo/bar.html'], 'foo/bar'],
// relative non index with current
- [['foo', '/'], '/foo.html'],
- [['foo', '/a.html'], '/foo.html'],
- [['foo', '/index.html'], '/foo.html'],
- [['foo', '/a/'], '/a/foo.html'],
- [['foo', '/a/index.html'], '/a/foo.html'],
- [['foo', '/a/b.html'], '/a/foo.html'],
- [['foo.md', '/'], '/foo.html'],
- [['foo.md', '/a.html'], '/foo.html'],
- [['foo.md', '/index.html'], '/foo.html'],
- [['foo.md', '/a/'], '/a/foo.html'],
- [['foo.md', '/a/index.html'], '/a/foo.html'],
- [['foo.md', '/a/b.html'], '/a/foo.html'],
- [['foo.html', '/'], '/foo.html'],
- [['foo.html', '/a.html'], '/foo.html'],
- [['foo.html', '/index.html'], '/foo.html'],
- [['foo.html', '/a/'], '/a/foo.html'],
- [['foo.html', '/a/index.html'], '/a/foo.html'],
- [['foo.html', '/a/b.html'], '/a/foo.html'],
- [['foo/bar', '/'], '/foo/bar.html'],
- [['foo/bar', '/a.html'], '/foo/bar.html'],
- [['foo/bar', '/index.html'], '/foo/bar.html'],
- [['foo/bar', '/a/'], '/a/foo/bar.html'],
- [['foo/bar', '/a/index.html'], '/a/foo/bar.html'],
- [['foo/bar', '/a/b.html'], '/a/foo/bar.html'],
- [['foo/bar.md', '/'], '/foo/bar.html'],
- [['foo/bar.md', '/a.html'], '/foo/bar.html'],
- [['foo/bar.md', '/index.html'], '/foo/bar.html'],
- [['foo/bar.md', '/a/'], '/a/foo/bar.html'],
- [['foo/bar.md', '/a/index.html'], '/a/foo/bar.html'],
- [['foo/bar.md', '/a/b.html'], '/a/foo/bar.html'],
- [['foo/bar.html', '/'], '/foo/bar.html'],
- [['foo/bar.html', '/a.html'], '/foo/bar.html'],
- [['foo/bar.html', '/index.html'], '/foo/bar.html'],
- [['foo/bar.html', '/a/'], '/a/foo/bar.html'],
- [['foo/bar.html', '/a/index.html'], '/a/foo/bar.html'],
- [['foo/bar.html', '/a/b.html'], '/a/foo/bar.html'],
- [['./foo', '/'], '/foo.html'],
- [['./foo', '/a.html'], '/foo.html'],
- [['./foo', '/index.html'], '/foo.html'],
- [['./foo', '/a/'], '/a/foo.html'],
- [['./foo', '/a/index.html'], '/a/foo.html'],
- [['./foo', '/a/b.html'], '/a/foo.html'],
- [['./foo.md', '/'], '/foo.html'],
- [['./foo.md', '/a.html'], '/foo.html'],
- [['./foo.md', '/index.html'], '/foo.html'],
- [['./foo.md', '/a/'], '/a/foo.html'],
- [['./foo.md', '/a/index.html'], '/a/foo.html'],
- [['./foo.md', '/a/b.html'], '/a/foo.html'],
- [['./foo.html', '/'], '/foo.html'],
- [['./foo.html', '/a.html'], '/foo.html'],
- [['./foo.html', '/index.html'], '/foo.html'],
- [['./foo.html', '/a/'], '/a/foo.html'],
- [['./foo.html', '/a/index.html'], '/a/foo.html'],
- [['./foo.html', '/a/b.html'], '/a/foo.html'],
- [['./foo/bar', '/'], '/foo/bar.html'],
- [['./foo/bar', '/a.html'], '/foo/bar.html'],
- [['./foo/bar', '/index.html'], '/foo/bar.html'],
- [['./foo/bar', '/a/'], '/a/foo/bar.html'],
- [['./foo/bar', '/a/index.html'], '/a/foo/bar.html'],
- [['./foo/bar', '/a/b.html'], '/a/foo/bar.html'],
- [['./foo/bar.md', '/'], '/foo/bar.html'],
- [['./foo/bar.md', '/a.html'], '/foo/bar.html'],
- [['./foo/bar.md', '/index.html'], '/foo/bar.html'],
- [['./foo/bar.md', '/a/'], '/a/foo/bar.html'],
- [['./foo/bar.md', '/a/index.html'], '/a/foo/bar.html'],
- [['./foo/bar.md', '/a/b.html'], '/a/foo/bar.html'],
- [['./foo/bar.html', '/'], '/foo/bar.html'],
- [['./foo/bar.html', '/a.html'], '/foo/bar.html'],
- [['./foo/bar.html', '/index.html'], '/foo/bar.html'],
- [['./foo/bar.html', '/a/'], '/a/foo/bar.html'],
- [['./foo/bar.html', '/a/index.html'], '/a/foo/bar.html'],
- [['./foo/bar.html', '/a/b.html'], '/a/foo/bar.html'],
- [['../foo', '/a/'], '/foo.html'],
- [['../foo', '/a/index.html'], '/foo.html'],
- [['../foo', '/a/b.html'], '/foo.html'],
- [['../foo.md', '/a/'], '/foo.html'],
- [['../foo.md', '/a/index.html'], '/foo.html'],
- [['../foo.md', '/a/b.html'], '/foo.html'],
- [['../foo.html', '/a/'], '/foo.html'],
- [['../foo.html', '/a/index.html'], '/foo.html'],
- [['../foo.html', '/a/b.html'], '/foo.html'],
- [['../foo/bar', '/a/'], '/foo/bar.html'],
- [['../foo/bar', '/a/index.html'], '/foo/bar.html'],
- [['../foo/bar', '/a/b.html'], '/foo/bar.html'],
- [['../foo/bar.md', '/a/'], '/foo/bar.html'],
- [['../foo/bar.md', '/a/index.html'], '/foo/bar.html'],
- [['../foo/bar.md', '/a/b.html'], '/foo/bar.html'],
- [['../foo/bar.html', '/a/'], '/foo/bar.html'],
- [['../foo/bar.html', '/a/index.html'], '/foo/bar.html'],
- [['../foo/bar.html', '/a/b.html'], '/foo/bar.html'],
+ [['foo', '/'], '/foo'],
+ [['foo', '/a.html'], '/foo'],
+ [['foo', '/index.html'], '/foo'],
+ [['foo', '/a/'], '/a/foo'],
+ [['foo', '/a/index.html'], '/a/foo'],
+ [['foo', '/a/b.html'], '/a/foo'],
+ [['foo.md', '/'], '/foo'],
+ [['foo.md', '/a.html'], '/foo'],
+ [['foo.md', '/index.html'], '/foo'],
+ [['foo.md', '/a/'], '/a/foo'],
+ [['foo.md', '/a/index.html'], '/a/foo'],
+ [['foo.md', '/a/b.html'], '/a/foo'],
+ [['foo.html', '/'], '/foo'],
+ [['foo.html', '/a.html'], '/foo'],
+ [['foo.html', '/index.html'], '/foo'],
+ [['foo.html', '/a/'], '/a/foo'],
+ [['foo.html', '/a/index.html'], '/a/foo'],
+ [['foo.html', '/a/b.html'], '/a/foo'],
+ [['foo/bar', '/'], '/foo/bar'],
+ [['foo/bar', '/a.html'], '/foo/bar'],
+ [['foo/bar', '/index.html'], '/foo/bar'],
+ [['foo/bar', '/a/'], '/a/foo/bar'],
+ [['foo/bar', '/a/index.html'], '/a/foo/bar'],
+ [['foo/bar', '/a/b.html'], '/a/foo/bar'],
+ [['foo/bar.md', '/'], '/foo/bar'],
+ [['foo/bar.md', '/a.html'], '/foo/bar'],
+ [['foo/bar.md', '/index.html'], '/foo/bar'],
+ [['foo/bar.md', '/a/'], '/a/foo/bar'],
+ [['foo/bar.md', '/a/index.html'], '/a/foo/bar'],
+ [['foo/bar.md', '/a/b.html'], '/a/foo/bar'],
+ [['foo/bar.html', '/'], '/foo/bar'],
+ [['foo/bar.html', '/a.html'], '/foo/bar'],
+ [['foo/bar.html', '/index.html'], '/foo/bar'],
+ [['foo/bar.html', '/a/'], '/a/foo/bar'],
+ [['foo/bar.html', '/a/index.html'], '/a/foo/bar'],
+ [['foo/bar.html', '/a/b.html'], '/a/foo/bar'],
+ [['./foo', '/'], '/foo'],
+ [['./foo', '/a.html'], '/foo'],
+ [['./foo', '/index.html'], '/foo'],
+ [['./foo', '/a/'], '/a/foo'],
+ [['./foo', '/a/index.html'], '/a/foo'],
+ [['./foo', '/a/b.html'], '/a/foo'],
+ [['./foo.md', '/'], '/foo'],
+ [['./foo.md', '/a.html'], '/foo'],
+ [['./foo.md', '/index.html'], '/foo'],
+ [['./foo.md', '/a/'], '/a/foo'],
+ [['./foo.md', '/a/index.html'], '/a/foo'],
+ [['./foo.md', '/a/b.html'], '/a/foo'],
+ [['./foo.html', '/'], '/foo'],
+ [['./foo.html', '/a.html'], '/foo'],
+ [['./foo.html', '/index.html'], '/foo'],
+ [['./foo.html', '/a/'], '/a/foo'],
+ [['./foo.html', '/a/index.html'], '/a/foo'],
+ [['./foo.html', '/a/b.html'], '/a/foo'],
+ [['./foo/bar', '/'], '/foo/bar'],
+ [['./foo/bar', '/a.html'], '/foo/bar'],
+ [['./foo/bar', '/index.html'], '/foo/bar'],
+ [['./foo/bar', '/a/'], '/a/foo/bar'],
+ [['./foo/bar', '/a/index.html'], '/a/foo/bar'],
+ [['./foo/bar', '/a/b.html'], '/a/foo/bar'],
+ [['./foo/bar.md', '/'], '/foo/bar'],
+ [['./foo/bar.md', '/a.html'], '/foo/bar'],
+ [['./foo/bar.md', '/index.html'], '/foo/bar'],
+ [['./foo/bar.md', '/a/'], '/a/foo/bar'],
+ [['./foo/bar.md', '/a/index.html'], '/a/foo/bar'],
+ [['./foo/bar.md', '/a/b.html'], '/a/foo/bar'],
+ [['./foo/bar.html', '/'], '/foo/bar'],
+ [['./foo/bar.html', '/a.html'], '/foo/bar'],
+ [['./foo/bar.html', '/index.html'], '/foo/bar'],
+ [['./foo/bar.html', '/a/'], '/a/foo/bar'],
+ [['./foo/bar.html', '/a/index.html'], '/a/foo/bar'],
+ [['./foo/bar.html', '/a/b.html'], '/a/foo/bar'],
+ [['../foo', '/a/'], '/foo'],
+ [['../foo', '/a/index.html'], '/foo'],
+ [['../foo', '/a/b.html'], '/foo'],
+ [['../foo.md', '/a/'], '/foo'],
+ [['../foo.md', '/a/index.html'], '/foo'],
+ [['../foo.md', '/a/b.html'], '/foo'],
+ [['../foo.html', '/a/'], '/foo'],
+ [['../foo.html', '/a/index.html'], '/foo'],
+ [['../foo.html', '/a/b.html'], '/foo'],
+ [['../foo/bar', '/a/'], '/foo/bar'],
+ [['../foo/bar', '/a/index.html'], '/foo/bar'],
+ [['../foo/bar', '/a/b.html'], '/foo/bar'],
+ [['../foo/bar.md', '/a/'], '/foo/bar'],
+ [['../foo/bar.md', '/a/index.html'], '/foo/bar'],
+ [['../foo/bar.md', '/a/b.html'], '/foo/bar'],
+ [['../foo/bar.html', '/a/'], '/foo/bar'],
+ [['../foo/bar.html', '/a/index.html'], '/foo/bar'],
+ [['../foo/bar.html', '/a/b.html'], '/foo/bar'],
// absolute non index with current
- [['/foo', '/'], '/foo.html'],
- [['/foo', '/a.html'], '/foo.html'],
- [['/foo', '/index.html'], '/foo.html'],
- [['/foo', '/a/'], '/foo.html'],
- [['/foo', '/a/index.html'], '/foo.html'],
- [['/foo', '/a/b.html'], '/foo.html'],
- [['/foo.md', '/'], '/foo.html'],
- [['/foo.md', '/a.html'], '/foo.html'],
- [['/foo.md', '/index.html'], '/foo.html'],
- [['/foo.md', '/a/'], '/foo.html'],
- [['/foo.md', '/a/index.html'], '/foo.html'],
- [['/foo.md', '/a/b.html'], '/foo.html'],
- [['/foo.html', '/'], '/foo.html'],
- [['/foo.html', '/a.html'], '/foo.html'],
- [['/foo.html', '/index.html'], '/foo.html'],
- [['/foo.html', '/a/'], '/foo.html'],
- [['/foo.html', '/a/index.html'], '/foo.html'],
- [['/foo.html', '/a/b.html'], '/foo.html'],
- [['/foo/bar', '/'], '/foo/bar.html'],
- [['/foo/bar', '/a.html'], '/foo/bar.html'],
- [['/foo/bar', '/index.html'], '/foo/bar.html'],
- [['/foo/bar', '/a/'], '/foo/bar.html'],
- [['/foo/bar', '/a/index.html'], '/foo/bar.html'],
- [['/foo/bar', '/a/b.html'], '/foo/bar.html'],
- [['/foo/bar.md', '/'], '/foo/bar.html'],
- [['/foo/bar.md', '/a.html'], '/foo/bar.html'],
- [['/foo/bar.md', '/index.html'], '/foo/bar.html'],
- [['/foo/bar.md', '/a/'], '/foo/bar.html'],
- [['/foo/bar.md', '/a/index.html'], '/foo/bar.html'],
- [['/foo/bar.md', '/a/b.html'], '/foo/bar.html'],
- [['/foo/bar.html', '/'], '/foo/bar.html'],
- [['/foo/bar.html', '/a.html'], '/foo/bar.html'],
- [['/foo/bar.html', '/index.html'], '/foo/bar.html'],
- [['/foo/bar.html', '/a/'], '/foo/bar.html'],
- [['/foo/bar.html', '/a/index.html'], '/foo/bar.html'],
- [['/foo/bar.html', '/a/b.html'], '/foo/bar.html'],
+ [['/foo', '/'], '/foo'],
+ [['/foo', '/a.html'], '/foo'],
+ [['/foo', '/index.html'], '/foo'],
+ [['/foo', '/a/'], '/foo'],
+ [['/foo', '/a/index.html'], '/foo'],
+ [['/foo', '/a/b.html'], '/foo'],
+ [['/foo.md', '/'], '/foo'],
+ [['/foo.md', '/a.html'], '/foo'],
+ [['/foo.md', '/index.html'], '/foo'],
+ [['/foo.md', '/a/'], '/foo'],
+ [['/foo.md', '/a/index.html'], '/foo'],
+ [['/foo.md', '/a/b.html'], '/foo'],
+ [['/foo.html', '/'], '/foo'],
+ [['/foo.html', '/a.html'], '/foo'],
+ [['/foo.html', '/index.html'], '/foo'],
+ [['/foo.html', '/a/'], '/foo'],
+ [['/foo.html', '/a/index.html'], '/foo'],
+ [['/foo.html', '/a/b.html'], '/foo'],
+ [['/foo/bar', '/'], '/foo/bar'],
+ [['/foo/bar', '/a.html'], '/foo/bar'],
+ [['/foo/bar', '/index.html'], '/foo/bar'],
+ [['/foo/bar', '/a/'], '/foo/bar'],
+ [['/foo/bar', '/a/index.html'], '/foo/bar'],
+ [['/foo/bar', '/a/b.html'], '/foo/bar'],
+ [['/foo/bar.md', '/'], '/foo/bar'],
+ [['/foo/bar.md', '/a.html'], '/foo/bar'],
+ [['/foo/bar.md', '/index.html'], '/foo/bar'],
+ [['/foo/bar.md', '/a/'], '/foo/bar'],
+ [['/foo/bar.md', '/a/index.html'], '/foo/bar'],
+ [['/foo/bar.md', '/a/b.html'], '/foo/bar'],
+ [['/foo/bar.html', '/'], '/foo/bar'],
+ [['/foo/bar.html', '/a.html'], '/foo/bar'],
+ [['/foo/bar.html', '/index.html'], '/foo/bar'],
+ [['/foo/bar.html', '/a/'], '/foo/bar'],
+ [['/foo/bar.html', '/a/index.html'], '/foo/bar'],
+ [['/foo/bar.html', '/a/b.html'], '/foo/bar'],
// only hash and query
[[''], ''],
// unexpected corner cases
- [['.md'], '.html'],
- [['foo/.md'], 'foo/.html'],
- [['/.md'], '/.html'],
- [['/foo/.md'], '/foo/.html'],
- [['.md', '/a/'], '/a/.html'],
- [['foo/.md', '/a/'], '/a/foo/.html'],
- [['/.md', '/a/'], '/.html'],
- [['/foo/.md', '/a/'], '/foo/.html'],
- [['.md', '/a/index.html'], '/a/.html'],
- [['foo/.md', '/a/index.html'], '/a/foo/.html'],
- [['/.md', '/a/index.html'], '/.html'],
- [['/foo/.md', '/a/index.html'], '/foo/.html'],
- [['.md', '/a/b.html'], '/a/.html'],
- [['foo/.md', '/a/b.html'], '/a/foo/.html'],
- [['/.md', '/a/b.html'], '/.html'],
- [['/foo/.md', '/a/b.html'], '/foo/.html'],
+ [['.md'], ''],
+ [['foo/.md'], 'foo/'],
+ [['/.md'], '/'],
+ [['/foo/.md'], '/foo/'],
+ [['.md', '/a/'], '/a/'],
+ [['foo/.md', '/a/'], '/a/foo/'],
+ [['/.md', '/a/'], '/'],
+ [['/foo/.md', '/a/'], '/foo/'],
+ [['.md', '/a/index.html'], '/a/'],
+ [['foo/.md', '/a/index.html'], '/a/foo/'],
+ [['/.md', '/a/index.html'], '/'],
+ [['/foo/.md', '/a/index.html'], '/foo/'],
+ [['.md', '/a/b.html'], '/a/'],
+ [['foo/.md', '/a/b.html'], '/a/foo/'],
+ [['/.md', '/a/b.html'], '/'],
+ [['/foo/.md', '/a/b.html'], '/foo/'],
]
describe('should normalize clean paths correctly', () => {
@@ -204,42 +204,3 @@ describe('should normalize clean paths correctly', () => {
}),
)
})
-
-describe('should normalize paths with query correctly', () => {
- testCases
- .map(([[path, current], expected]) => [
- [`${path}?foo=bar`, current],
- `${expected}?foo=bar`,
- ])
- .forEach(([[path, current], expected]) =>
- it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
- expect(normalizeRoutePath(path, current)).toBe(expected)
- }),
- )
-})
-
-describe('should normalize paths with hash correctly', () => {
- testCases
- .map(([[path, current], expected]) => [
- [`${path}#foobar`, current],
- `${expected}#foobar`,
- ])
- .map(([[path, current], expected]) =>
- it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
- expect(normalizeRoutePath(path, current)).toBe(expected)
- }),
- )
-})
-
-describe('should normalize paths with query and hash correctly', () => {
- testCases
- .map(([[path, current], expected]) => [
- [`${path}?foo=1&bar=2#foobar`, current],
- `${expected}?foo=1&bar=2#foobar`,
- ])
- .map(([[path, current], expected]) =>
- it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
- expect(normalizeRoutePath(path, current)).toBe(expected)
- }),
- )
-})
diff --git a/packages/shared/tests/routes/splitPath.spec.ts b/packages/shared/tests/routes/splitPath.spec.ts
new file mode 100644
index 0000000000..6dfd2452fb
--- /dev/null
+++ b/packages/shared/tests/routes/splitPath.spec.ts
@@ -0,0 +1,22 @@
+import { expect, it } from 'vitest'
+import { splitPath } from '../../src/index.js'
+
+const testCases: [string, ReturnType][] = [
+ ['/a/b/c/', { pathname: '/a/b/c/', hashAndQueries: '' }],
+ ['/a/b/c/?a=1', { pathname: '/a/b/c/', hashAndQueries: '?a=1' }],
+ ['/a/b/c/#b', { pathname: '/a/b/c/', hashAndQueries: '#b' }],
+ ['/a/b/c/?a=1#b', { pathname: '/a/b/c/', hashAndQueries: '?a=1#b' }],
+ ['a/index.html', { pathname: 'a/index.html', hashAndQueries: '' }],
+ ['/a/index.html?a=1', { pathname: '/a/index.html', hashAndQueries: '?a=1' }],
+ ['/a/index.html#a', { pathname: '/a/index.html', hashAndQueries: '#a' }],
+ [
+ '/a/index.html?a=1#b',
+ { pathname: '/a/index.html', hashAndQueries: '?a=1#b' },
+ ],
+]
+
+testCases.forEach(([source, expected]) => {
+ it(`${source} -> ${expected}`, () => {
+ expect(splitPath(source)).toEqual(expected)
+ })
+})