From b3692e93746e202d4a4fc4c56aaf7660964cc2e8 Mon Sep 17 00:00:00 2001 From: "Mr.Hope" Date: Mon, 18 May 2026 21:17:53 +0800 Subject: [PATCH 1/3] feat: add route option (close #1505) --- e2e/docs/.vuepress/config.ts | 2 +- e2e/docs/router/navigate-by-link.md | 24 +- e2e/docs/router/navigate-by-router.md | 70 ++++-- e2e/tests/router/navigate-by-link.spec.ts | 130 ++++++++++- e2e/tests/router/navigate-by-router.spec.ts | 96 ++++++-- .../src/plugins/vuepressConfigPlugin.ts | 1 + .../src/config/handlePluginDefine.ts | 1 + .../cli/src/commands/dev/watchPageFiles.ts | 2 +- packages/client/src/router/index.ts | 2 +- packages/client/src/router/resolveRoute.ts | 16 +- ...eRoutePath.ts => resolveRouteCleanPath.ts} | 6 +- .../client/src/router/resolveRouteFullPath.ts | 12 +- packages/client/types.d.ts | 1 + .../core/src/app/prepare/prepareRoutes.ts | 4 +- packages/core/src/app/resolveAppMarkdown.ts | 7 + packages/core/src/app/resolveAppOptions.ts | 21 +- packages/core/src/app/resolveAppPages.ts | 6 +- packages/core/src/page/inferPagePath.ts | 6 +- packages/core/src/page/resolvePageHtmlInfo.ts | 8 +- packages/core/src/page/resolvePagePath.ts | 11 +- .../core/src/page/resolvePagePermalink.ts | 6 +- packages/core/src/types/app/options.ts | 54 +++-- .../core/tests/app/resolveAppOptions.spec.ts | 7 +- .../core/tests/app/resolveAppPages.spec.ts | 12 +- .../core/tests/page/inferPagePath.spec.ts | 10 +- .../core/tests/page/resolvePagePath.spec.ts | 4 +- .../src/plugins/linksPlugin/linksPlugin.ts | 25 ++- packages/shared/src/utils/routes/index.ts | 5 +- .../shared/src/utils/routes/inferRouteKey.ts | 26 +++ .../shared/src/utils/routes/inferRoutePath.ts | 26 --- ...alizeRoutePath.ts => normalizeRouteKey.ts} | 10 +- .../utils/routes/resolveRoutePathWithExt.ts | 2 + ...outePath.spec.ts => inferRouteKey.spec.ts} | 48 ++-- .../tests/routes/normalizeRouteKey.spec.ts | 209 ++++++++++++++++++ .../tests/routes/normalizeRoutePath.spec.ts | 207 ----------------- 35 files changed, 693 insertions(+), 384 deletions(-) rename packages/client/src/router/{resolveRoutePath.ts => resolveRouteCleanPath.ts} (85%) create mode 100644 packages/shared/src/utils/routes/inferRouteKey.ts delete mode 100644 packages/shared/src/utils/routes/inferRoutePath.ts rename packages/shared/src/utils/routes/{normalizeRoutePath.ts => normalizeRouteKey.ts} (51%) create mode 100644 packages/shared/src/utils/routes/resolveRoutePathWithExt.ts rename packages/shared/tests/routes/{inferRoutePath.spec.ts => inferRouteKey.spec.ts} (50%) create mode 100644 packages/shared/tests/routes/normalizeRouteKey.spec.ts delete mode 100644 packages/shared/tests/routes/normalizeRoutePath.spec.ts 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/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..dcd607577a 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', '!node_modules'], + 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/inferPagePath.spec.ts b/packages/core/tests/page/inferPagePath.spec.ts index 52984c43f0..530175a320 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: '/', }) }) 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/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) - }) - }) -}) From 74096fae8527b5d5ce1775825ac5bed2cfa5fef5 Mon Sep 17 00:00:00 2001 From: "Mr.Hope" Date: Mon, 18 May 2026 21:22:09 +0800 Subject: [PATCH 2/3] test: update unit tests --- packages/core/src/app/resolveAppOptions.ts | 2 +- packages/core/tests/page/createPage.spec.ts | 4 ++-- packages/core/tests/page/inferPagePath.spec.ts | 4 ++-- packages/core/tests/page/resolvePageHtmlInfo.spec.ts | 4 ---- packages/core/tests/page/resolvePagePermalink.spec.ts | 4 ++-- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/core/src/app/resolveAppOptions.ts b/packages/core/src/app/resolveAppOptions.ts index dcd607577a..578067aa8e 100644 --- a/packages/core/src/app/resolveAppOptions.ts +++ b/packages/core/src/app/resolveAppOptions.ts @@ -49,7 +49,7 @@ export const resolveAppOptions = ({ permalinkPattern: _permalinkPattern, route: { cleanUrl = false, - pagePatterns = ['**/*.md', '!.vuepress', '!node_modules'], + pagePatterns = ['**/*.md', '!.vuepress'], permalinkPattern = null, } = { pagePatterns: _pagePatterns, 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 530175a320..2adcb59c55 100644 --- a/packages/core/tests/page/inferPagePath.spec.ts +++ b/packages/core/tests/page/inferPagePath.spec.ts @@ -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/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') }) }) From cf57a08aff8394b1a304dd5aba77b00773a24b32 Mon Sep 17 00:00:00 2001 From: "Mr.Hope" Date: Mon, 18 May 2026 21:26:25 +0800 Subject: [PATCH 3/3] chore: remove webpack middleware to fix broken clean urls --- .../src/dev/createDevServerConfig.ts | 10 ------- packages/bundler-webpack/src/dev/dev.ts | 2 +- .../src/dev/trailingSlashMiddleware.ts | 26 ------------------- 3 files changed, 1 insertion(+), 37 deletions(-) delete mode 100644 packages/bundler-webpack/src/dev/trailingSlashMiddleware.ts 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}`) -}