diff --git a/e2e/docs/.vuepress/config.ts b/e2e/docs/.vuepress/config.ts
index 4e9fada490..39237568a9 100644
--- a/e2e/docs/.vuepress/config.ts
+++ b/e2e/docs/.vuepress/config.ts
@@ -69,7 +69,7 @@ export default defineUserConfig({
theme: e2eTheme(),
extendsPage: (page) => {
- if (page.path === '/page-data/route-meta.html') {
+ if (page.path === '/page-data/route-meta') {
page.routeMeta = {
a: 1,
b: 2,
diff --git a/e2e/docs/router/navigate-by-link.md b/e2e/docs/router/navigate-by-link.md
index 58200b2295..46e973d218 100644
--- a/e2e/docs/router/navigate-by-link.md
+++ b/e2e/docs/router/navigate-by-link.md
@@ -14,7 +14,16 @@
Home
Home
404
-404
+404
+
+## HTML Clean Links
+
+Home
+404
+Home
+Home
+404
+404
## Markdown Links with html paths
@@ -22,7 +31,18 @@
- [404](/404.html)
- [Home with query](/?home=true)
- [Home with query and hash](/?home=true#home)
-- [404 with hash](/404.html#404)
+- [404 with hash](/404.html#_404)
- [404 with complex hash](/404.html#/404?lang=en)
> Non-recommended usage. HTML paths could not be prepended with `base` correctly.
+
+## Markdown Clean Links
+
+- [Home](/)
+- [404](/404)
+- [Home with query](/?home=true)
+- [Home with query and hash](/?home=true#home)
+- [404 with hash](/404#_404)
+- [404 with complex hash](/404#/404?lang=en)
+
+> Non-recommended usage. HTML paths could not be prepended with `base` correctly.
diff --git a/e2e/docs/router/navigate-by-router.md b/e2e/docs/router/navigate-by-router.md
index 5fe23bcb7a..47e83b6010 100644
--- a/e2e/docs/router/navigate-by-router.md
+++ b/e2e/docs/router/navigate-by-router.md
@@ -1,37 +1,71 @@
-
-
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/e2e/tests/router/navigate-by-link.spec.ts b/e2e/tests/router/navigate-by-link.spec.ts
index de0c00b6cd..5bcb16cb44 100644
--- a/e2e/tests/router/navigate-by-link.spec.ts
+++ b/e2e/tests/router/navigate-by-link.spec.ts
@@ -74,14 +74,54 @@ test.describe('html links', () => {
})
test('should preserve hash', async ({ page }) => {
- await page.locator('#html-links + p > a').nth(4).click()
+ await page.locator(selector).nth(4).click()
await expect(page).toHaveURL(`${BASE}404.html#_404`)
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
})
- test('should preserve hash and query', async ({ page }) => {
+ test('should preserve complex hash', async ({ page }) => {
await page.locator('#html-links + p > a').nth(5).click()
- await expect(page).toHaveURL(`${BASE}404.html#_404?notFound=true`)
+ await expect(page).toHaveURL(`${BASE}404.html#/404?lang=en`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+})
+
+test.describe('html clean links', () => {
+ const selector = '#html-clean-links + p > a'
+
+ test('should navigate to home correctly', async ({ page }) => {
+ await page.locator(selector).nth(0).click()
+ await expect(page).toHaveURL(BASE)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('should navigate to 404 page correctly', async ({ page }) => {
+ await page.locator(selector).nth(1).click()
+ await expect(page).toHaveURL(`${BASE}404.html`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('should preserve query', async ({ page }) => {
+ await page.locator(selector).nth(2).click()
+ await expect(page).toHaveURL(`${BASE}?home=true`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('should preserve query and hash', async ({ page }) => {
+ await page.locator(selector).nth(3).click()
+ await expect(page).toHaveURL(`${BASE}?home=true#home`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('should preserve hash', async ({ page }) => {
+ await page.locator(selector).nth(4).click()
+ await expect(page).toHaveURL(`${BASE}404.html#_404`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('should preserve complex hash', async ({ page }) => {
+ await page.locator('#html-links + p > a').nth(5).click()
+ await expect(page).toHaveURL(`${BASE}404.html#/404?lang=en`)
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
})
})
@@ -149,7 +189,7 @@ test.describe('markdown links with html paths', () => {
await expect(page).toHaveURL(`${BASE}404.html#_404`)
await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
} else {
- await expect(locator).toHaveAttribute('href', '/404.html#404')
+ await expect(locator).toHaveAttribute('href', '/404.html#_404')
await expect(locator).toHaveAttribute('target', '_blank')
}
})
@@ -167,3 +207,85 @@ test.describe('markdown links with html paths', () => {
}
})
})
+
+test.describe('markdown clean links', () => {
+ const selector = '#markdown-clean-links + ul > li > a'
+
+ test('should navigate to home correctly', async ({ page }) => {
+ const locator = page.locator(selector).nth(0)
+
+ if (BASE === '/') {
+ await locator.click()
+ await expect(page).toHaveURL('/')
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ } else {
+ await expect(locator).toHaveAttribute('href', '/')
+ await expect(locator).toHaveAttribute('target', '_blank')
+ }
+ })
+
+ test('should navigate to 404 page correctly', async ({ page }) => {
+ const locator = page.locator(selector).nth(1)
+
+ if (BASE === '/') {
+ await locator.click()
+ await expect(page).toHaveURL(`${BASE}404.html`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ } else {
+ await expect(locator).toHaveAttribute('href', '/404')
+ await expect(locator).toHaveAttribute('target', '_blank')
+ }
+ })
+
+ test('should preserve query', async ({ page }) => {
+ const locator = page.locator(selector).nth(2)
+
+ if (BASE === '/') {
+ await locator.click()
+ await expect(page).toHaveURL(`${BASE}?home=true`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ } else {
+ await expect(locator).toHaveAttribute('href', '/?home=true')
+ await expect(locator).toHaveAttribute('target', '_blank')
+ }
+ })
+
+ test('should preserve query and hash', async ({ page }) => {
+ const locator = page.locator(selector).nth(3)
+
+ if (BASE === '/') {
+ await locator.click()
+ await expect(page).toHaveURL(`${BASE}?home=true#home`)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ } else {
+ await expect(locator).toHaveAttribute('href', '/?home=true#home')
+ await expect(locator).toHaveAttribute('target', '_blank')
+ }
+ })
+
+ test('should preserve hash', async ({ page }) => {
+ const locator = page.locator(selector).nth(4)
+
+ if (BASE === '/') {
+ await locator.click()
+ await expect(page).toHaveURL(`${BASE}404.html#_404`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ } else {
+ await expect(locator).toHaveAttribute('href', '/404#_404')
+ await expect(locator).toHaveAttribute('target', '_blank')
+ }
+ })
+
+ test('should preserve complex hash', async ({ page }) => {
+ const locator = page.locator(selector).nth(5)
+
+ if (BASE === '/') {
+ await locator.click()
+ await expect(page).toHaveURL(`${BASE}404.html#/404?lang=en`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ } else {
+ await expect(locator).toHaveAttribute('href', '/404#/404?lang=en')
+ await expect(locator).toHaveAttribute('target', '_blank')
+ }
+ })
+})
diff --git a/e2e/tests/router/navigate-by-router.spec.ts b/e2e/tests/router/navigate-by-router.spec.ts
index c8dff05ebc..9b19cf225a 100644
--- a/e2e/tests/router/navigate-by-router.spec.ts
+++ b/e2e/tests/router/navigate-by-router.spec.ts
@@ -6,38 +6,86 @@ test.beforeEach(async ({ page }) => {
await page.goto('router/navigate-by-router.html')
})
-test('should navigate to home correctly', async ({ page }) => {
- await page.locator('#home').click()
- await expect(page).toHaveURL(BASE)
- await expect(page.locator('#home-h2')).toHaveText('Home H2')
+test.describe('should navigate to home correctly', () => {
+ test('full', async ({ page }) => {
+ await page.locator('#full .home').click()
+ await expect(page).toHaveURL(BASE)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
+
+ test('clean', async ({ page }) => {
+ await page.locator('#clean .home').click()
+ await expect(page).toHaveURL(BASE)
+ await expect(page.locator('#home-h2')).toHaveText('Home H2')
+ })
})
-test('should navigate to 404 page correctly', async ({ page }) => {
- await page.locator('#not-found').click()
- await expect(page).toHaveURL(`${BASE}404.html`)
- await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+test.describe('should navigate to 404 page correctly', () => {
+ test('full', async ({ page }) => {
+ await page.locator('#full .not-found').click()
+ await expect(page).toHaveURL(`${BASE}404.html`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('clean', async ({ page }) => {
+ await page.locator('#clean .not-found').click()
+ await expect(page).toHaveURL(`${BASE}404.html`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
})
-test('should preserve query', async ({ page }) => {
- await page.locator('#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', () => {
+ 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('should preserve query and hash', async ({ page }) => {
- await page.locator('#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 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('should preserve hash', async ({ page }) => {
- await page.locator('#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', () => {
+ 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('should preserve complex hash', async ({ page }) => {
- await page.locator('#not-found-with-complex-hash').click()
- await expect(page).toHaveURL(`${BASE}404.html#/404?lang=en`)
- await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+test.describe('should preserve complex hash', () => {
+ test('full', async ({ page }) => {
+ await page.locator('#full .not-found-with-complex-hash').click()
+ await expect(page).toHaveURL(`${BASE}404.html#/404?lang=en`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
+
+ test('clean', async ({ page }) => {
+ await page.locator('#clean .not-found-with-complex-hash').click()
+ await expect(page).toHaveURL(`${BASE}404.html#/404?lang=en`)
+ await expect(page.locator('#notfound-h2')).toHaveText('NotFound H2')
+ })
})
diff --git a/packages/bundler-vite/src/plugins/vuepressConfigPlugin.ts b/packages/bundler-vite/src/plugins/vuepressConfigPlugin.ts
index e12ac90922..8e3c4e7d47 100644
--- a/packages/bundler-vite/src/plugins/vuepressConfigPlugin.ts
+++ b/packages/bundler-vite/src/plugins/vuepressConfigPlugin.ts
@@ -54,6 +54,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 17fbb25615..42d863f6f9 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/bundler-webpack/src/dev/createDevServerConfig.ts b/packages/bundler-webpack/src/dev/createDevServerConfig.ts
index cb2e19a941..9326f3bdb8 100644
--- a/packages/bundler-webpack/src/dev/createDevServerConfig.ts
+++ b/packages/bundler-webpack/src/dev/createDevServerConfig.ts
@@ -4,12 +4,8 @@ import type { App } from '@vuepress/core'
import { path } from '@vuepress/utils'
import type WebpackDevServer from 'webpack-dev-server'
-import type { WebpackBundlerOptions } from '../types.js'
-import { trailingSlashMiddleware } from './trailingSlashMiddleware.js'
-
export const createDevServerConfig = (
app: App,
- options: WebpackBundlerOptions,
): WebpackDevServer.Configuration => ({
allowedHosts: 'all',
compress: true,
@@ -27,12 +23,6 @@ export const createDevServerConfig = (
},
host: app.options.host,
hot: true,
- setupMiddlewares: (middlewares, devServer) => {
- devServer.app?.use(trailingSlashMiddleware)
- return (
- options.devServerSetupMiddlewares?.(middlewares, devServer) ?? middlewares
- )
- },
open: app.options.open,
port: app.options.port,
static: {
diff --git a/packages/bundler-webpack/src/dev/dev.ts b/packages/bundler-webpack/src/dev/dev.ts
index fc157fc51e..57b43c147e 100644
--- a/packages/bundler-webpack/src/dev/dev.ts
+++ b/packages/bundler-webpack/src/dev/dev.ts
@@ -30,7 +30,7 @@ export const dev = async (
const compiler = webpack(webpackConfig)
// create webpack-dev-server
- const serverConfig = createDevServerConfig(app, options)
+ const serverConfig = createDevServerConfig(app)
const server = new WebpackDevServer(serverConfig, compiler)
const [, close] = await Promise.all([
diff --git a/packages/bundler-webpack/src/dev/trailingSlashMiddleware.ts b/packages/bundler-webpack/src/dev/trailingSlashMiddleware.ts
deleted file mode 100644
index 34d34e0717..0000000000
--- a/packages/bundler-webpack/src/dev/trailingSlashMiddleware.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import type { RequestHandler } from 'express'
-
-/**
- * A middleware to add trailing slash to the url
- *
- * It will redirect '/foo' to '/foo/' with 302
- */
-export const trailingSlashMiddleware: RequestHandler = (req, res, next) => {
- if (
- // only add trailing slash in GET and HEAD requests
- !['GET', 'HEAD'].includes(req.method) ||
- // if the last section of the path has a dot, we think it has extension
- // and should not add trailing slash
- req.path.split('/').pop()?.includes('.') ||
- // if the path already has trailing slash
- req.path.endsWith('/')
- ) {
- next()
- return
- }
-
- // add trailing slash and retain query
- // notice that we should not use 301 in dev-server
- const query = req.url.slice(req.path.length)
- res.redirect(302, `${req.path}/${query}`)
-}
diff --git a/packages/cli/src/commands/dev/watchPageFiles.ts b/packages/cli/src/commands/dev/watchPageFiles.ts
index c796e38c94..356af479ac 100644
--- a/packages/cli/src/commands/dev/watchPageFiles.ts
+++ b/packages/cli/src/commands/dev/watchPageFiles.ts
@@ -149,7 +149,7 @@ export const watchPageFiles = (
// watch page files
const { matchPatterns, ignorePatterns } = processPagePatterns(
- app.options.pagePatterns,
+ app.options.route.pagePatterns,
)
const sourceDir = app.dir.source()
const tempDir = app.dir.temp()
diff --git a/packages/client/src/router/index.ts b/packages/client/src/router/index.ts
index 206f6f2f2b..6f6161ad4a 100644
--- a/packages/client/src/router/index.ts
+++ b/packages/client/src/router/index.ts
@@ -2,5 +2,5 @@ export type { Router, RouteLocationNormalizedLoaded } from 'vue-router'
export { useRoute, useRouter } from 'vue-router'
export * from './resolveRoute.js'
+export * from './resolveRouteCleanPath.js'
export * from './resolveRouteFullPath.js'
-export * from './resolveRoutePath.js'
diff --git a/packages/client/src/router/resolveRoute.ts b/packages/client/src/router/resolveRoute.ts
index 14f047bb04..b283ffcfce 100644
--- a/packages/client/src/router/resolveRoute.ts
+++ b/packages/client/src/router/resolveRoute.ts
@@ -1,8 +1,8 @@
-import { splitPath } from '@vuepress/shared'
+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 { resolveRouteCleanPath } from './resolveRouteCleanPath.js'
export interface ResolvedRoute<
T extends RouteMeta = RouteMeta,
@@ -22,21 +22,23 @@ export const resolveRoute = (
const { pathname, hashAndQueries } = splitPath(path)
// resolve the route path
- const routePath = resolveRoutePath(pathname, currentPath)
- const routeFullPath = routePath + hashAndQueries
+ const cleanRoutePath = resolveRouteCleanPath(pathname, currentPath)
+ const routeFullPath = __VUEPRESS_CLEAN_URL__
+ ? cleanRoutePath
+ : resolveRoutePathWithExt(cleanRoutePath) + hashAndQueries
// the route not found
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- unsafe indexed access
- if (!routes.value[routePath]) {
+ if (!routes.value[cleanRoutePath]) {
return {
- ...routes.value['/404.html'],
+ ...routes.value['/404'],
path: routeFullPath,
notFound: true,
} as ResolvedRoute
}
return {
- ...routes.value[routePath],
+ ...routes.value[cleanRoutePath],
path: routeFullPath,
notFound: false,
} as ResolvedRoute
diff --git a/packages/client/src/router/resolveRoutePath.ts b/packages/client/src/router/resolveRouteCleanPath.ts
similarity index 85%
rename from packages/client/src/router/resolveRoutePath.ts
rename to packages/client/src/router/resolveRouteCleanPath.ts
index 430f8f123f..69165eefb1 100644
--- a/packages/client/src/router/resolveRoutePath.ts
+++ b/packages/client/src/router/resolveRouteCleanPath.ts
@@ -1,16 +1,16 @@
-import { normalizeRoutePath } from '@vuepress/shared'
+import { normalizeRouteKey } from '@vuepress/shared'
import { redirects, routes } from '../internal/routes.js'
/**
* Resolve route path with given raw path
*/
-export const resolveRoutePath = (
+export const resolveRouteCleanPath = (
pathname: string,
currentPath?: string,
): string => {
// normalized path
- const normalizedRoutePath = normalizeRoutePath(pathname, currentPath)
+ const normalizedRoutePath = normalizeRouteKey(pathname, currentPath)
// check if the normalized path is in routes
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- unsafe indexed access
diff --git a/packages/client/src/router/resolveRouteFullPath.ts b/packages/client/src/router/resolveRouteFullPath.ts
index c756fe5070..c3fd1c2dc2 100644
--- a/packages/client/src/router/resolveRouteFullPath.ts
+++ b/packages/client/src/router/resolveRouteFullPath.ts
@@ -1,6 +1,6 @@
-import { splitPath } from '@vuepress/shared'
+import { resolveRoutePathWithExt, splitPath } from '@vuepress/shared'
-import { resolveRoutePath } from './resolveRoutePath.js'
+import { resolveRouteCleanPath } from './resolveRouteCleanPath.js'
/**
* Resolve route full path with given raw path
@@ -10,5 +10,11 @@ export const resolveRouteFullPath = (
currentPath?: string,
): string => {
const { pathname, hashAndQueries } = splitPath(path)
- return resolveRoutePath(pathname, currentPath) + hashAndQueries
+ const routeCleanPath = resolveRouteCleanPath(pathname, currentPath)
+
+ return (
+ (__VUEPRESS_CLEAN_URL__
+ ? routeCleanPath
+ : resolveRoutePathWithExt(routeCleanPath)) + hashAndQueries
+ )
}
diff --git a/packages/client/types.d.ts b/packages/client/types.d.ts
index 032e27fcfd..89087a11fb 100644
--- a/packages/client/types.d.ts
+++ b/packages/client/types.d.ts
@@ -2,6 +2,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_PROD_DEVTOOLS__: boolean
diff --git a/packages/core/src/app/prepare/prepareRoutes.ts b/packages/core/src/app/prepare/prepareRoutes.ts
index 7f40f68faf..784ff9839e 100644
--- a/packages/core/src/app/prepare/prepareRoutes.ts
+++ b/packages/core/src/app/prepare/prepareRoutes.ts
@@ -1,4 +1,4 @@
-import { normalizeRoutePath } from '@vuepress/shared'
+import { normalizeRouteKey } from '@vuepress/shared'
import type { App, Page } from '../../types/index.js'
@@ -31,7 +31,7 @@ const resolvePageRedirects = ({ path, pathInferred }: Page): string[] => {
// redirect from inferred path, notice that the inferred path is not uri-encoded
if (pathInferred !== null) {
- const normalizedPathInferred = normalizeRoutePath(pathInferred)
+ const normalizedPathInferred = normalizeRouteKey(pathInferred)
const encodedPathInferred = encodeURI(normalizedPathInferred)
// add redirect to the set when the redirect could not be normalized & encoded to the page path
diff --git a/packages/core/src/app/resolveAppMarkdown.ts b/packages/core/src/app/resolveAppMarkdown.ts
index f0446b8239..a6193a00bf 100644
--- a/packages/core/src/app/resolveAppMarkdown.ts
+++ b/packages/core/src/app/resolveAppMarkdown.ts
@@ -9,6 +9,13 @@ import type { App } from '../types/index.js'
* @internal
*/
export const resolveAppMarkdown = async (app: App): Promise => {
+ // links plugin is not disabled
+ if (app.options.markdown.links !== false) {
+ app.options.markdown.links ??= {}
+ // set the cleanUrl option
+ app.options.markdown.links.cleanUrl = app.options.route.cleanUrl
+ }
+
// plugin hook: extendsMarkdownOptions
await app.pluginApi.hooks.extendsMarkdownOptions.process(
app.options.markdown,
diff --git a/packages/core/src/app/resolveAppOptions.ts b/packages/core/src/app/resolveAppOptions.ts
index 12f9ee4bb5..578067aa8e 100644
--- a/packages/core/src/app/resolveAppOptions.ts
+++ b/packages/core/src/app/resolveAppOptions.ts
@@ -43,8 +43,18 @@ export const resolveAppOptions = ({
bundler,
debug = false,
markdown = {},
- pagePatterns = ['**/*.md', '!.vuepress'],
- permalinkPattern = null,
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
+ pagePatterns: _pagePatterns,
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
+ permalinkPattern: _permalinkPattern,
+ route: {
+ cleanUrl = false,
+ pagePatterns = ['**/*.md', '!.vuepress'],
+ permalinkPattern = null,
+ } = {
+ pagePatterns: _pagePatterns,
+ permalinkPattern: _permalinkPattern,
+ },
userStyle = null,
plugins = [],
theme,
@@ -75,8 +85,11 @@ export const resolveAppOptions = ({
bundler,
debug,
markdown,
- pagePatterns,
- permalinkPattern,
+ route: {
+ cleanUrl,
+ pagePatterns,
+ permalinkPattern,
+ },
userStyle,
plugins,
theme,
diff --git a/packages/core/src/app/resolveAppPages.ts b/packages/core/src/app/resolveAppPages.ts
index 3b3d1d874d..112b898ceb 100644
--- a/packages/core/src/app/resolveAppPages.ts
+++ b/packages/core/src/app/resolveAppPages.ts
@@ -17,7 +17,7 @@ export const resolveAppPages = async (
log('resolveAppPages start')
// resolve page absolute file paths according to the page patterns
- const pageFilePaths = await tinyglobby.glob(app.options.pagePatterns, {
+ const pageFilePaths = await tinyglobby.glob(app.options.route.pagePatterns, {
absolute: true,
cwd: app.dir.source(),
ignore: DEFAULT_IGNORE_PATTERNS,
@@ -35,7 +35,7 @@ export const resolveAppPages = async (
pagesMap[page.filePath] = page
}
// if there is a 404 page, set the default layout to NotFound
- if (page.path === '/404.html') {
+ if (page.path === '/404') {
page.frontmatter.layout ??= 'NotFound'
hasNotFoundPage = true
}
@@ -47,7 +47,7 @@ export const resolveAppPages = async (
if (!hasNotFoundPage) {
pages.push(
await createPage(app, {
- path: '/404.html',
+ path: '/404',
frontmatter: { layout: 'NotFound' },
content: '404 Not Found',
}),
diff --git a/packages/core/src/page/inferPagePath.ts b/packages/core/src/page/inferPagePath.ts
index 7ae91269f0..5bb889ccf3 100644
--- a/packages/core/src/page/inferPagePath.ts
+++ b/packages/core/src/page/inferPagePath.ts
@@ -1,6 +1,6 @@
import {
ensureLeadingSlash,
- inferRoutePath,
+ inferRouteKey,
resolveLocalePath,
} from '@vuepress/shared'
@@ -29,7 +29,7 @@ export const inferPagePath = ({
return {
pathInferred: filePathRelative
- ? inferRoutePath(ensureLeadingSlash(filePathRelative))
+ ? inferRouteKey(ensureLeadingSlash(filePathRelative))
: null,
pathLocale,
}
@@ -44,7 +44,7 @@ export const inferPagePath = ({
// infer page route path from file path
// foo/bar.md -> /foo/bar.html
- const pathInferred = inferRoutePath(ensureLeadingSlash(filePathRelative))
+ const pathInferred = inferRouteKey(ensureLeadingSlash(filePathRelative))
// resolve page locale path
const pathLocale = resolveLocalePath(app.siteData.locales, pathInferred)
diff --git a/packages/core/src/page/resolvePageHtmlInfo.ts b/packages/core/src/page/resolvePageHtmlInfo.ts
index ba855382af..7794e543f2 100644
--- a/packages/core/src/page/resolvePageHtmlInfo.ts
+++ b/packages/core/src/page/resolvePageHtmlInfo.ts
@@ -19,14 +19,10 @@ export const resolvePageHtmlInfo = ({
} => {
const path = decodeURI(pagePath)
- // /foo.html -> foo.html
+ // /foo -> foo.html
// /foo/ -> foo/index.html
const htmlFilePathRelative = removeLeadingSlash(
- path.endsWith('/')
- ? `${path}index.html`
- : path.endsWith('.html')
- ? path
- : `${path}.html`,
+ path.endsWith('/') ? `${path}index.html` : `${path}.html`,
)
const htmlFilePath = app.dir.dest(htmlFilePathRelative)
diff --git a/packages/core/src/page/resolvePagePath.ts b/packages/core/src/page/resolvePagePath.ts
index 9215769d8f..340f3310b4 100644
--- a/packages/core/src/page/resolvePagePath.ts
+++ b/packages/core/src/page/resolvePagePath.ts
@@ -1,3 +1,4 @@
+import { inferRouteKey } from '@vuepress/shared'
import { logger, sanitizeFileName } from '@vuepress/utils'
import type { PageOptions } from '../types/index.js'
@@ -16,7 +17,9 @@ export const resolvePagePath = ({
pathInferred: string | null
options: PageOptions
}): string => {
- const pagePath = options.path || permalink || pathInferred
+ const pagePath = options.path
+ ? inferRouteKey(options.path)
+ : permalink || pathInferred
if (!pagePath) {
throw logger.createError(
@@ -24,5 +27,9 @@ export const resolvePagePath = ({
)
}
- return encodeURI(pagePath.split('/').map(sanitizeFileName).join('/'))
+ return (
+ encodeURI(pagePath.split('/').map(sanitizeFileName).join('/'))
+ // get clean format
+ .replace(/\.html$/, '')
+ )
}
diff --git a/packages/core/src/page/resolvePagePermalink.ts b/packages/core/src/page/resolvePagePermalink.ts
index e46fba6bbc..2828d5e6cd 100644
--- a/packages/core/src/page/resolvePagePermalink.ts
+++ b/packages/core/src/page/resolvePagePermalink.ts
@@ -1,4 +1,4 @@
-import { ensureLeadingSlash, isString } from '@vuepress/shared'
+import { ensureLeadingSlash, inferRouteKey, isString } from '@vuepress/shared'
import { path } from '@vuepress/utils'
import type { App, PageFrontmatter } from '../types/index.js'
@@ -37,7 +37,7 @@ export const resolvePagePermalink = ({
}
const permalinkPattern =
- frontmatter.permalinkPattern || app.options.permalinkPattern
+ frontmatter.permalinkPattern || app.options.route.permalinkPattern
if (!isString(permalinkPattern)) {
return null
@@ -55,5 +55,5 @@ export const resolvePagePermalink = ({
.replace(/:raw/, pathInferred?.replace(/^\//, '') ?? ''),
)
- return ensureLeadingSlash(link)
+ return inferRouteKey(ensureLeadingSlash(link))
}
diff --git a/packages/core/src/types/app/options.ts b/packages/core/src/types/app/options.ts
index 7b47c6bae0..567ec5711b 100644
--- a/packages/core/src/types/app/options.ts
+++ b/packages/core/src/types/app/options.ts
@@ -6,6 +6,27 @@ import type { Bundler } from '../bundler.js'
import type { PluginConfig } from '../plugin.js'
import type { Theme } from '../theme.js'
+export interface RouteOptions {
+ /**
+ * Whether to use "clean url"
+ */
+ cleanUrl?: boolean
+
+ /**
+ * Patterns to match the markdown files as pages
+ *
+ * @default ['**\/*.md', '!.vuepress']
+ */
+ pagePatterns?: string[]
+
+ /**
+ * Pattern to generate permalink for pages
+ *
+ * @default null
+ */
+ permalinkPattern?: string | null
+}
+
/**
* Vuepress app common config that shared between dev and build
*/
@@ -70,18 +91,9 @@ export interface AppConfigCommon extends Partial {
markdown?: MarkdownOptions
/**
- * Patterns to match the markdown files as pages
- *
- * @default ['**\/*.md', '!.vuepress']
+ * Vuepress route options
*/
- pagePatterns?: string[]
-
- /**
- * Pattern to generate permalink for pages
- *
- * @default null
- */
- permalinkPattern?: string | null
+ route?: RouteOptions
/**
* Allow specifying user styles, which will be injected into client at the bottom
@@ -183,11 +195,27 @@ export interface AppConfigBuild {
*
* It would be provided by user, typically via a config file.
*/
-export type AppConfig = AppConfigBuild & AppConfigCommon & AppConfigDev
+export type AppConfig = AppConfigBuild &
+ AppConfigCommon &
+ AppConfigDev & {
+ /**
+ * @deprecated use `route.pagePatterns` instead
+ */
+ pagePatterns?: string[]
+
+ /**
+ * @deprecated use `route.permalinkPattern` instead
+ */
+ permalinkPattern?: string | null
+ }
/**
* Vuepress app options that resolved from user config.
*
* It fills all optional fields with a default value.
*/
-export type AppOptions = Required
+export type AppOptions = Required<
+ AppConfigBuild & AppConfigCommon & AppConfigDev
+> & {
+ route: Required
+}
diff --git a/packages/core/tests/app/resolveAppOptions.spec.ts b/packages/core/tests/app/resolveAppOptions.spec.ts
index ab3033c435..dd28e651ba 100644
--- a/packages/core/tests/app/resolveAppOptions.spec.ts
+++ b/packages/core/tests/app/resolveAppOptions.spec.ts
@@ -33,8 +33,11 @@ it('should create app options with default values', () => {
host: '0.0.0.0',
port: 8080,
open: false,
- pagePatterns: ['**/*.md', '!.vuepress'],
- permalinkPattern: null,
+ route: {
+ cleanUrl: false,
+ pagePatterns: ['**/*.md', '!.vuepress'],
+ permalinkPattern: null,
+ },
userStyle: null,
templateDev: path.normalize(
require.resolve('@vuepress/client/templates/dev.html'),
diff --git a/packages/core/tests/app/resolveAppPages.spec.ts b/packages/core/tests/app/resolveAppPages.spec.ts
index 624445eac2..d33b813233 100644
--- a/packages/core/tests/app/resolveAppPages.spec.ts
+++ b/packages/core/tests/app/resolveAppPages.spec.ts
@@ -14,9 +14,9 @@ it('should create two pages with default 404 page', async () => {
app.markdown = createMarkdown()
const { pages } = await resolveAppPages(app)
- const fooPage = pages.find((page) => page.path === '/foo.html')
- const barPage = pages.find((page) => page.path === '/bar.html')
- const notFoundPage = pages.find((page) => page.path === '/404.html')
+ const fooPage = pages.find((page) => page.path === '/foo')
+ const barPage = pages.find((page) => page.path === '/bar')
+ const notFoundPage = pages.find((page) => page.path === '/404')
expect(pages).toHaveLength(3)
expect(fooPage?.filePathRelative).toEqual('foo.md')
@@ -34,9 +34,9 @@ it('should create two pages with custom 404 page', async () => {
app.markdown = createMarkdown()
const { pages } = await resolveAppPages(app)
- const fooPage = pages.find((page) => page.path === '/foo.html')
- const barPage = pages.find((page) => page.path === '/bar.html')
- const notFoundPage = pages.find((page) => page.path === '/404.html')
+ const fooPage = pages.find((page) => page.path === '/foo')
+ const barPage = pages.find((page) => page.path === '/bar')
+ const notFoundPage = pages.find((page) => page.path === '/404')
expect(pages).toHaveLength(3)
expect(fooPage?.filePathRelative).toEqual('foo.md')
diff --git a/packages/core/tests/page/createPage.spec.ts b/packages/core/tests/page/createPage.spec.ts
index 77b16c4de3..a929f058e4 100644
--- a/packages/core/tests/page/createPage.spec.ts
+++ b/packages/core/tests/page/createPage.spec.ts
@@ -96,11 +96,11 @@ describe('should work without plugins', () => {
})
// page data
- expect(page.data.path).toBe('/zh/test.html')
+ expect(page.data.path).toBe('/zh/test')
expect(page.data.lang).toBe('zh-CN')
// base fields
- expect(page.path).toBe('/zh/test.html')
+ expect(page.path).toBe('/zh/test')
expect(page.lang).toBe('zh-CN')
// file info
diff --git a/packages/core/tests/page/inferPagePath.spec.ts b/packages/core/tests/page/inferPagePath.spec.ts
index 52984c43f0..2adcb59c55 100644
--- a/packages/core/tests/page/inferPagePath.spec.ts
+++ b/packages/core/tests/page/inferPagePath.spec.ts
@@ -32,7 +32,7 @@ const TEST_CASES: [string, ReturnType][] = [
[
'foo.md',
{
- pathInferred: '/foo.html',
+ pathInferred: '/foo',
pathLocale: '/',
},
],
@@ -46,7 +46,7 @@ const TEST_CASES: [string, ReturnType][] = [
[
'en/foo.md',
{
- pathInferred: '/en/foo.html',
+ pathInferred: '/en/foo',
pathLocale: '/en/',
},
],
@@ -60,7 +60,7 @@ const TEST_CASES: [string, ReturnType][] = [
[
'zh/foo.md',
{
- pathInferred: '/zh/foo.html',
+ pathInferred: '/zh/foo',
pathLocale: '/zh/',
},
],
@@ -74,7 +74,7 @@ const TEST_CASES: [string, ReturnType][] = [
[
'中文/foo.md',
{
- pathInferred: '/中文/foo.html',
+ pathInferred: '/中文/foo',
pathLocale: '/中文/',
},
],
@@ -102,7 +102,7 @@ it('should use `/` as the default locale path', () => {
options: {},
}),
).toEqual({
- pathInferred: '/en/foo/bar.html',
+ pathInferred: '/en/foo/bar',
pathLocale: '/',
})
})
@@ -128,7 +128,7 @@ it('should respect options.path', () => {
options: { path: '/custom/path.html' },
}),
).toEqual({
- pathInferred: '/foo/bar.html',
+ pathInferred: '/foo/bar',
pathLocale: '/',
})
@@ -139,7 +139,7 @@ it('should respect options.path', () => {
options: { path: '/zh/custom/path.html' },
}),
).toEqual({
- pathInferred: '/zh/foo/bar.html',
+ pathInferred: '/zh/foo/bar',
pathLocale: '/zh/',
})
})
diff --git a/packages/core/tests/page/resolvePageHtmlInfo.spec.ts b/packages/core/tests/page/resolvePageHtmlInfo.spec.ts
index 1d07c0bbc8..2fbcf79893 100644
--- a/packages/core/tests/page/resolvePageHtmlInfo.spec.ts
+++ b/packages/core/tests/page/resolvePageHtmlInfo.spec.ts
@@ -12,11 +12,7 @@ const app = createBaseApp({
const TEST_CASES: [string, string][] = [
['/foo', 'foo.html'],
- ['/foo.html', 'foo.html'],
- ['/foo/bar.html', 'foo/bar.html'],
['/foo/bar', 'foo/bar.html'],
- ['/foo/index.html', 'foo/index.html'],
- ['/foo/bar/index.html', 'foo/bar/index.html'],
['/foo/', 'foo/index.html'],
['/foo/bar/', 'foo/bar/index.html'],
]
diff --git a/packages/core/tests/page/resolvePagePath.spec.ts b/packages/core/tests/page/resolvePagePath.spec.ts
index 3eeb9efb2e..5b4493edc2 100644
--- a/packages/core/tests/page/resolvePagePath.spec.ts
+++ b/packages/core/tests/page/resolvePagePath.spec.ts
@@ -41,7 +41,7 @@ const TEST_CASES: [
},
},
],
- '/options.html',
+ '/options',
],
// use permalink
[
@@ -93,7 +93,7 @@ const TEST_CASES: [
options: {},
},
],
- '/inferred.html',
+ '/inferred',
],
]
diff --git a/packages/core/tests/page/resolvePagePermalink.spec.ts b/packages/core/tests/page/resolvePagePermalink.spec.ts
index 78c5ea2dbf..9f355cc6b4 100644
--- a/packages/core/tests/page/resolvePagePermalink.spec.ts
+++ b/packages/core/tests/page/resolvePagePermalink.spec.ts
@@ -166,11 +166,11 @@ describe('permalink pattern', () => {
},
slug: 'foo-bar',
date: '2020-10-07',
- pathInferred: '/raw.html',
+ pathInferred: '/raw',
pathLocale: '/en/',
})
- expect(resolved).toBe('/en/2020/10/07/foo-bar/raw.html')
+ expect(resolved).toBe('/en/2020/10/07/foo-bar/raw')
})
})
diff --git a/packages/markdown/src/plugins/linksPlugin/linksPlugin.ts b/packages/markdown/src/plugins/linksPlugin/linksPlugin.ts
index ea75a6bb21..963b00005f 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 {
+ inferRouteKey,
+ isLinkExternal,
+ resolveRoutePathWithExt,
+} from '@vuepress/shared'
import type { PluginWithOptions } from 'markdown-it'
import type Token from 'markdown-it/lib/token.mjs'
@@ -6,6 +10,13 @@ import type { MarkdownEnv } from '../../types.js'
import { resolvePaths } from './resolvePaths.js'
export interface LinksPluginOptions {
+ /**
+ * Whether use "clean url"
+ *
+ * @default false
+ */
+ cleanUrl?: boolean
+
/**
* Additional attributes for external links
*
@@ -48,6 +59,7 @@ export const linksPlugin: PluginWithOptions = (
const internalTag = options.internalTag || 'RouteLink'
const isExternal =
options.isExternal ?? ((href, env) => isLinkExternal(href, env.base))
+ const cleanUrl = options.cleanUrl ?? false
// attrs that going to be added to external links
const externalAttrs = {
@@ -131,19 +143,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(
+ const routeKey = inferRouteKey(
absolutePath
? absolutePath.replace(new RegExp(`^${base}`), '/')
: relativePath,
)
+
// replace the original href link with the normalized path
- hrefAttr[1] = `${normalizedPath}${rawHashAndQueries}`
+ hrefAttr[1] = `${cleanUrl ? routeKey : resolveRoutePathWithExt(routeKey)}${rawHashAndQueries}`
// set `hasOpenInternalLink` to modify the ending tag
hasOpenInternalLink = true
} else {
- const normalizedPath = inferRoutePath(absolutePath ?? relativePath)
+ // ext is added here
+ const routeKey = inferRouteKey(absolutePath ?? relativePath)
+
// replace the original href link with the normalized path
- hrefAttr[1] = `${normalizedPath}${rawHashAndQueries}`
+ hrefAttr[1] = `${cleanUrl ? routeKey : resolveRoutePathWithExt(routeKey)}${rawHashAndQueries}`
}
// extract internal links for file / page existence check
diff --git a/packages/shared/src/utils/routes/index.ts b/packages/shared/src/utils/routes/index.ts
index 5e67f4b001..a60f733e51 100644
--- a/packages/shared/src/utils/routes/index.ts
+++ b/packages/shared/src/utils/routes/index.ts
@@ -1,5 +1,6 @@
-export * from './inferRoutePath'
-export * from './normalizeRoutePath.js'
+export * from './inferRouteKey'
+export * from './normalizeRouteKey.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/inferRouteKey.ts b/packages/shared/src/utils/routes/inferRouteKey.ts
new file mode 100644
index 0000000000..7bd25cee1f
--- /dev/null
+++ b/packages/shared/src/utils/routes/inferRouteKey.ts
@@ -0,0 +1,26 @@
+/**
+ * Infer route key of the given raw path
+ */
+export const inferRouteKey = (rawPath: string): string => {
+ // if the raw path is empty or ends with `/`, return as is
+ if (!rawPath || rawPath.endsWith('/')) return rawPath
+
+ // convert README.md to index.html
+ let routePath = rawPath.replace(/(^|\/)README.md$/i, '$1index')
+
+ // convert /foo/bar.md to /foo/bar
+ if (routePath.endsWith('.md')) {
+ routePath = routePath.substring(0, routePath.length - 3)
+ }
+ // convert /foo/bar.html to /foo/bar
+ else if (routePath.endsWith('.html')) {
+ routePath = routePath.substring(0, routePath.length - 5)
+ }
+
+ // 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/inferRoutePath.ts b/packages/shared/src/utils/routes/inferRoutePath.ts
deleted file mode 100644
index 803cef50bf..0000000000
--- a/packages/shared/src/utils/routes/inferRoutePath.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Infer route path of the given raw path
- */
-export const inferRoutePath = (rawPath: string): string => {
- // if the raw path is empty or ends with `/`, return as is
- if (!rawPath || rawPath.endsWith('/')) return rawPath
-
- // convert README.md to index.html
- let routePath = rawPath.replace(/(^|\/)README.md$/i, '$1index.html')
-
- // convert /foo/bar.md to /foo/bar.html
- if (routePath.endsWith('.md')) {
- routePath = `${routePath.substring(0, routePath.length - 3)}.html`
- }
- // convert /foo/bar to /foo/bar.html
- else if (!routePath.endsWith('.html')) {
- routePath = `${routePath}.html`
- }
-
- // convert /foo/index.html to /foo/
- if (routePath.endsWith('/index.html')) {
- routePath = routePath.substring(0, routePath.length - 10)
- }
-
- return routePath
-}
diff --git a/packages/shared/src/utils/routes/normalizeRoutePath.ts b/packages/shared/src/utils/routes/normalizeRouteKey.ts
similarity index 51%
rename from packages/shared/src/utils/routes/normalizeRoutePath.ts
rename to packages/shared/src/utils/routes/normalizeRouteKey.ts
index 6c73ef172a..ffb7ac819d 100644
--- a/packages/shared/src/utils/routes/normalizeRoutePath.ts
+++ b/packages/shared/src/utils/routes/normalizeRouteKey.ts
@@ -1,11 +1,11 @@
-import { inferRoutePath } from './inferRoutePath.js'
+import { inferRouteKey } from './inferRouteKey.js'
const FAKE_HOST = 'http://.'
/**
- * Normalize the given pathname path to the final route path
+ * Normalize the given pathname path to the final route key
*/
-export const normalizeRoutePath = (
+export const normalizeRouteKey = (
pathname: string,
current?: string,
): string => {
@@ -13,8 +13,8 @@ export const normalizeRoutePath = (
// the relative path should be resolved against the current path
const loc = current.slice(0, current.lastIndexOf('/'))
- return inferRoutePath(new URL(`${loc}/${pathname}`, FAKE_HOST).pathname)
+ return inferRouteKey(new URL(`${loc}/${pathname}`, FAKE_HOST).pathname)
}
- return inferRoutePath(pathname)
+ return inferRouteKey(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..cd48fb29b1
--- /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/tests/routes/inferRoutePath.spec.ts b/packages/shared/tests/routes/inferRouteKey.spec.ts
similarity index 50%
rename from packages/shared/tests/routes/inferRoutePath.spec.ts
rename to packages/shared/tests/routes/inferRouteKey.spec.ts
index b63b0ebc3e..e45ee96b5e 100644
--- a/packages/shared/tests/routes/inferRoutePath.spec.ts
+++ b/packages/shared/tests/routes/inferRouteKey.spec.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
-import { inferRoutePath } from '../../src/index.js'
+import { inferRouteKey } from '../../src/index.js'
const TEST_CASES = [
// absolute index
@@ -16,19 +16,19 @@ const TEST_CASES = [
['/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/'],
@@ -39,25 +39,25 @@ const TEST_CASES = [
['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', () => {
+describe('should normalize route keys correctly', () => {
TEST_CASES.forEach(([path, expected]) => {
it(`"${path}" -> "${expected}"`, () => {
- expect(inferRoutePath(path)).toBe(expected)
+ expect(inferRouteKey(path)).toBe(expected)
})
})
})
diff --git a/packages/shared/tests/routes/normalizeRouteKey.spec.ts b/packages/shared/tests/routes/normalizeRouteKey.spec.ts
new file mode 100644
index 0000000000..90b6ce646b
--- /dev/null
+++ b/packages/shared/tests/routes/normalizeRouteKey.spec.ts
@@ -0,0 +1,209 @@
+import { describe, expect, it } from 'vitest'
+
+import { normalizeRouteKey } from '../../src/index.js'
+
+type TestCase = [[path: string, current?: string], expected: string]
+
+const TEST_CASES: TestCase[] = [
+ // absolute index
+ [['/'], '/'],
+ [['/README.md'], '/'],
+ [['/readme.md'], '/'],
+ [['/index.md'], '/'],
+ [['/index.html'], '/'],
+ [['/index'], '/'],
+ [['/foo/'], '/foo/'],
+ [['/foo/README.md'], '/foo/'],
+ [['/foo/readme.md'], '/foo/'],
+ [['/foo/index.md'], '/foo/'],
+ [['/foo/index.html'], '/foo/'],
+ [['/foo/index'], '/foo/'],
+ [['README.md'], 'index'],
+ [['readme.md'], 'index'],
+ [['index.md'], 'index'],
+ [['index.html'], 'index'],
+ [['index'], 'index'],
+
+ // absolute non-index
+ [['/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/'],
+ [['foo/README.md'], 'foo/'],
+ [['foo/readme.md'], 'foo/'],
+ [['foo/index.md'], 'foo/'],
+ [['foo/index.html'], 'foo/'],
+ [['foo/index'], 'foo/'],
+
+ // relative non index without current
+ [['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'],
+ [['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'],
+ [['/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'],
+
+ // empty
+ [[''], ''],
+
+ // unexpected corner cases
+ [['.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 route keys correctly', () => {
+ TEST_CASES.forEach(([[path, current], expected]) => {
+ it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
+ expect(normalizeRouteKey(path, current)).toBe(expected)
+ })
+ })
+})
diff --git a/packages/shared/tests/routes/normalizeRoutePath.spec.ts b/packages/shared/tests/routes/normalizeRoutePath.spec.ts
deleted file mode 100644
index 0816852635..0000000000
--- a/packages/shared/tests/routes/normalizeRoutePath.spec.ts
+++ /dev/null
@@ -1,207 +0,0 @@
-import { describe, expect, it } from 'vitest'
-
-import { normalizeRoutePath } from '../../src/index.js'
-
-const TEST_CASES = [
- // absolute index
- [['/'], '/'],
- [['/README.md'], '/'],
- [['/readme.md'], '/'],
- [['/index.md'], '/'],
- [['/index.html'], '/'],
- [['/index'], '/'],
- [['/foo/'], '/foo/'],
- [['/foo/README.md'], '/foo/'],
- [['/foo/readme.md'], '/foo/'],
- [['/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'],
-
- // 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'],
-
- // relative index without current
- [['foo/'], 'foo/'],
- [['foo/README.md'], 'foo/'],
- [['foo/readme.md'], 'foo/'],
- [['foo/index.md'], 'foo/'],
- [['foo/index.html'], 'foo/'],
- [['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'],
-
- // 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'],
-
- // 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'],
-
- // 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'],
-] as const
-
-describe('should normalize clean paths correctly', () => {
- TEST_CASES.forEach(([[path, current], expected]) => {
- it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
- expect(normalizeRoutePath(path, current)).toBe(expected)
- })
- })
-})