diff --git a/e2e/docs/.vuepress/config.ts b/e2e/docs/.vuepress/config.ts
index 12507eeeb0..8419bae8f2 100644
--- a/e2e/docs/.vuepress/config.ts
+++ b/e2e/docs/.vuepress/config.ts
@@ -52,4 +52,14 @@ export default defineUserConfig({
bundler: E2E_BUNDLER === 'webpack' ? webpackBundler() : viteBundler(),
theme: e2eTheme(),
+
+ extendsPage: (page) => {
+ if (page.path === '/page-data/route-meta.html') {
+ page.routeMeta = {
+ a: 1,
+ b: 2,
+ ...page.routeMeta,
+ }
+ }
+ },
})
diff --git a/e2e/docs/404.md b/e2e/docs/404.md
new file mode 100644
index 0000000000..fac3cec274
--- /dev/null
+++ b/e2e/docs/404.md
@@ -0,0 +1,4 @@
+---
+routeMeta:
+ foo: bar
+---
diff --git a/e2e/docs/page-data/route-meta.md b/e2e/docs/page-data/route-meta.md
new file mode 100644
index 0000000000..b319753d1c
--- /dev/null
+++ b/e2e/docs/page-data/route-meta.md
@@ -0,0 +1,5 @@
+---
+routeMeta:
+ a: 0
+ c: 3
+---
diff --git a/e2e/docs/router/resolve-route.md b/e2e/docs/router/resolve-route.md
new file mode 100644
index 0000000000..c556b6c66b
--- /dev/null
+++ b/e2e/docs/router/resolve-route.md
@@ -0,0 +1,43 @@
+## resolve
+
+### Path
+
+#### Index
+
+- Clean URL: {{ JSON.stringify(resolveRoute('/')) }}
+- HTML: {{ JSON.stringify(resolveRoute('/index.html')) }}
+- Markdown: {{ JSON.stringify(resolveRoute('/README.md')) }}
+
+#### Non-Index
+
+- Clean URL: {{ JSON.stringify(resolveRoute('/router/resolve-route')) }}
+- HTML: {{ JSON.stringify(resolveRoute('/router/resolve-route.html')) }}
+- Markdown: {{ JSON.stringify(resolveRoute('/router/resolve-route.md')) }}
+
+#### Non-ASCII
+
+- Clean URL: {{ JSON.stringify(resolveRoute('/routes/non-ascii-paths/中文目录名/中文文件名')) }}
+- HTML: {{ JSON.stringify(resolveRoute('/routes/non-ascii-paths/中文目录名/中文文件名.html')) }}
+- Markdown: {{ JSON.stringify(resolveRoute('/routes/non-ascii-paths/中文目录名/中文文件名.md')) }}
+
+#### Non-ASCII Encoded
+
+- Clean URL: {{ JSON.stringify(resolveRoute(encodeURI('/routes/non-ascii-paths/中文目录名/中文文件名'))) }}
+- HTML: {{ JSON.stringify(resolveRoute(encodeURI('/routes/non-ascii-paths/中文目录名/中文文件名.html'))) }}
+- Markdown: {{ JSON.stringify(resolveRoute(encodeURI('/routes/non-ascii-paths/中文目录名/中文文件名.md'))) }}
+
+#### Non-Existent
+
+- Clean URL: {{ JSON.stringify(resolveRoute('/non-existent')) }}
+- HTML: {{ JSON.stringify(resolveRoute('/non-existent.html')) }}
+- Markdown: {{ JSON.stringify(resolveRoute('/non-existent.md')) }}
+
+#### Route Meta
+
+- Clean URL: {{ JSON.stringify(resolveRoute('/page-data/route-meta')) }}
+- HTML: {{ JSON.stringify(resolveRoute('/page-data/route-meta.html')) }}
+- Markdown: {{ JSON.stringify(resolveRoute('/page-data/route-meta.md')) }}
+
+
diff --git a/e2e/tests/router/resolve-route.cy.ts b/e2e/tests/router/resolve-route.cy.ts
new file mode 100644
index 0000000000..360df88089
--- /dev/null
+++ b/e2e/tests/router/resolve-route.cy.ts
@@ -0,0 +1,66 @@
+const testCases = [
+ {
+ selector: '#index',
+ expected: {
+ path: '/',
+ meta: {},
+ notFound: false,
+ },
+ },
+ {
+ selector: '#non-index',
+ expected: {
+ path: '/router/resolve-route.html',
+ meta: {},
+ notFound: false,
+ },
+ },
+ {
+ selector: '#non-ascii',
+ expected: {
+ path: encodeURI('/routes/non-ascii-paths/中文目录名/中文文件名.html'),
+ meta: {},
+ notFound: false,
+ },
+ },
+ {
+ selector: '#non-ascii-encoded',
+ expected: {
+ path: encodeURI('/routes/non-ascii-paths/中文目录名/中文文件名.html'),
+ meta: {},
+ notFound: false,
+ },
+ },
+ {
+ selector: '#non-existent',
+ expected: {
+ path: '/non-existent.html',
+ meta: { foo: 'bar' },
+ notFound: true,
+ },
+ },
+ {
+ selector: '#route-meta',
+ expected: {
+ path: '/page-data/route-meta.html',
+ meta: { a: 0, b: 2, c: 3 },
+ notFound: false,
+ },
+ },
+]
+
+const parseResolvedRouteFromElement = (el: Cypress.JQueryWithSelector) =>
+ JSON.parse(/: (\{.*\})\s*$/.exec(el.text())![1])
+
+it('should resolve routes correctly', () => {
+ cy.visit('/router/resolve-route.html')
+
+ testCases.forEach(({ selector, expected }) => {
+ cy.get(`.e2e-theme-content ${selector} + ul > li`).each((el) => {
+ const resolvedRoute = parseResolvedRouteFromElement(el)
+ expect(resolvedRoute.path).to.equal(expected.path)
+ expect(resolvedRoute.meta).to.deep.equal(expected.meta)
+ expect(resolvedRoute.notFound).to.equal(expected.notFound)
+ })
+ })
+})
diff --git a/packages/bundler-vite/src/build/resolvePageChunkFiles.ts b/packages/bundler-vite/src/build/resolvePageChunkFiles.ts
index 96590994fc..a22408517c 100644
--- a/packages/bundler-vite/src/build/resolvePageChunkFiles.ts
+++ b/packages/bundler-vite/src/build/resolvePageChunkFiles.ts
@@ -11,9 +11,7 @@ export const resolvePageChunkFiles = ({
output
.filter(
(item): item is OutputChunk =>
- item.type === 'chunk' &&
- (item.facadeModuleId === page.componentFilePath ||
- item.facadeModuleId === page.dataFilePath),
+ item.type === 'chunk' && item.facadeModuleId === page.chunkFilePath,
)
.flatMap(({ fileName, imports, dynamicImports }) => [
fileName,
diff --git a/packages/cli/src/commands/dev/handlePageAdd.ts b/packages/cli/src/commands/dev/handlePageAdd.ts
index 2f61e0cee5..8f4b367975 100644
--- a/packages/cli/src/commands/dev/handlePageAdd.ts
+++ b/packages/cli/src/commands/dev/handlePageAdd.ts
@@ -1,10 +1,8 @@
import {
createPage,
+ preparePageChunk,
preparePageComponent,
- preparePageData,
- preparePagesComponents,
- preparePagesData,
- preparePagesRoutes,
+ prepareRoutes,
} from '@vuepress/core'
import type { App, Page } from '@vuepress/core'
@@ -33,12 +31,10 @@ export const handlePageAdd = async (
// prepare page files
await preparePageComponent(app, page)
- await preparePageData(app, page)
+ await preparePageChunk(app, page)
- // prepare pages entry
- await preparePagesComponents(app)
- await preparePagesData(app)
- await preparePagesRoutes(app)
+ // prepare routes file
+ await prepareRoutes(app)
return page
}
diff --git a/packages/cli/src/commands/dev/handlePageChange.ts b/packages/cli/src/commands/dev/handlePageChange.ts
index 19b1295628..6eeff179aa 100644
--- a/packages/cli/src/commands/dev/handlePageChange.ts
+++ b/packages/cli/src/commands/dev/handlePageChange.ts
@@ -1,10 +1,8 @@
import {
createPage,
+ preparePageChunk,
preparePageComponent,
- preparePageData,
- preparePagesComponents,
- preparePagesData,
- preparePagesRoutes,
+ prepareRoutes,
} from '@vuepress/core'
import type { App, Page } from '@vuepress/core'
@@ -36,21 +34,15 @@ export const handlePageChange = async (
// prepare page files
await preparePageComponent(app, pageNew)
- await preparePageData(app, pageNew)
+ await preparePageChunk(app, pageNew)
const isPathChanged = pageOld.path !== pageNew.path
const isRouteMetaChanged =
JSON.stringify(pageOld.routeMeta) !== JSON.stringify(pageNew.routeMeta)
- // prepare pages entry if the path is changed
- if (isPathChanged) {
- await preparePagesComponents(app)
- await preparePagesData(app)
- }
-
- // prepare pages routes if the path or routeMeta is changed
+ // prepare routes file if the path or route meta is changed
if (isPathChanged || isRouteMetaChanged) {
- await preparePagesRoutes(app)
+ await prepareRoutes(app)
}
return [pageOld, pageNew]
diff --git a/packages/cli/src/commands/dev/handlePageUnlink.ts b/packages/cli/src/commands/dev/handlePageUnlink.ts
index 792f865949..1260480292 100644
--- a/packages/cli/src/commands/dev/handlePageUnlink.ts
+++ b/packages/cli/src/commands/dev/handlePageUnlink.ts
@@ -1,8 +1,4 @@
-import {
- preparePagesComponents,
- preparePagesData,
- preparePagesRoutes,
-} from '@vuepress/core'
+import { prepareRoutes } from '@vuepress/core'
import type { App, Page } from '@vuepress/core'
/**
@@ -25,10 +21,8 @@ export const handlePageUnlink = async (
// remove the old page
app.pages.splice(pageIndex, 1)
- // re-prepare page files
- await preparePagesComponents(app)
- await preparePagesData(app)
- await preparePagesRoutes(app)
+ // re-prepare routes file
+ await prepareRoutes(app)
return page
}
diff --git a/packages/client/package.json b/packages/client/package.json
index c4e5f7fc37..9bbe205b20 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -59,9 +59,7 @@
"external": [
"@internal/clientConfigs",
"@internal/layoutComponents",
- "@internal/pagesComponents",
- "@internal/pagesData",
- "@internal/pagesRoutes",
+ "@internal/routes",
"@internal/siteData"
],
"format": [
diff --git a/packages/client/src/app.ts b/packages/client/src/app.ts
index 73f53e01aa..5098bcc218 100644
--- a/packages/client/src/app.ts
+++ b/packages/client/src/app.ts
@@ -2,7 +2,7 @@ import { clientConfigs } from '@internal/clientConfigs'
import { createApp, createSSRApp, h } from 'vue'
import { RouterView } from 'vue-router'
import { siteData } from './composables/index.js'
-import { createVueRouter } from './router.js'
+import { createVueRouter } from './createVueRouter.js'
import { setupGlobalComponents } from './setupGlobalComponents.js'
import { setupGlobalComputed } from './setupGlobalComputed.js'
import { setupUpdateHead } from './setupUpdateHead.js'
diff --git a/packages/client/src/components/Content.ts b/packages/client/src/components/Content.ts
index 1923756d2a..ea35bf9793 100644
--- a/packages/client/src/components/Content.ts
+++ b/packages/client/src/components/Content.ts
@@ -1,6 +1,6 @@
-import { pagesComponents } from '@internal/pagesComponents'
-import { computed, defineComponent, h } from 'vue'
+import { computed, defineAsyncComponent, defineComponent, h } from 'vue'
import { usePageData } from '../composables/index.js'
+import { resolveRoute } from '../router/index.js'
/**
* Markdown rendered content
@@ -10,7 +10,7 @@ export const Content = defineComponent({
name: 'Content',
props: {
- pageKey: {
+ path: {
type: String,
required: false,
default: '',
@@ -18,20 +18,12 @@ export const Content = defineComponent({
},
setup(props) {
- const page = usePageData()
- const pageComponent = computed(
- () => pagesComponents[props.pageKey || page.value.key],
- )
- return () =>
- pageComponent.value
- ? // use page component
- h(pageComponent.value)
- : // fallback content
- h(
- 'div',
- __VUEPRESS_DEV__
- ? 'Page does not exist. This is a fallback content.'
- : '404 Not Found',
- )
+ const pageData = usePageData()
+ const pageComponent = computed(() => {
+ const route = resolveRoute(props.path || pageData.value.path)
+ return defineAsyncComponent(() => route.loader().then(({ comp }) => comp))
+ })
+
+ return () => h(pageComponent.value)
},
})
diff --git a/packages/client/src/components/VPLink.ts b/packages/client/src/components/VPLink.ts
new file mode 100644
index 0000000000..72e6677c3e
--- /dev/null
+++ b/packages/client/src/components/VPLink.ts
@@ -0,0 +1,53 @@
+import { h } from 'vue'
+import type { FunctionalComponent, VNode } from 'vue'
+import { useRouter } from 'vue-router'
+import { withBase } from '../helpers/index.js'
+import { resolveRoutePath } from '../router/index.js'
+
+/**
+ * Forked from https://github.com/vuejs/router/blob/941b2131e80550009e5221d4db9f366b1fea3fd5/packages/router/src/RouterLink.ts#L293
+ */
+const guardEvent = (event: MouseEvent): boolean | void => {
+ // don't redirect with control keys
+ if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return
+ // don't redirect when preventDefault called
+ if (event.defaultPrevented) return
+ // don't redirect on right click
+ if (event.button !== undefined && event.button !== 0) return
+ // don't redirect if `target="_blank"`
+ if (event.currentTarget) {
+ const target = (event.currentTarget as HTMLElement).getAttribute('target')
+ if (target?.match(/\b_blank\b/i)) return
+ }
+ event.preventDefault()
+ return true
+}
+
+export interface VPLinkProps {
+ to: string
+}
+
+export const VPLink: FunctionalComponent<
+ VPLinkProps,
+ Record,
+ {
+ default: () => string | VNode | (string | VNode)[]
+ }
+> = ({ to = '' }, { slots }) => {
+ const router = useRouter()
+ const path = withBase(resolveRoutePath(to))
+
+ return h(
+ 'a',
+ {
+ class: 'vp-link',
+ href: path,
+ onClick: (event: MouseEvent = {} as MouseEvent) => {
+ guardEvent(event) ? router.push(to).catch() : Promise.resolve()
+ },
+ },
+ slots.default?.(),
+ )
+}
+
+VPLink.displayName = 'VPLink'
diff --git a/packages/client/src/components/index.ts b/packages/client/src/components/index.ts
index f11ca2bf0f..145c00508c 100644
--- a/packages/client/src/components/index.ts
+++ b/packages/client/src/components/index.ts
@@ -1,2 +1,3 @@
export * from './ClientOnly.js'
export * from './Content.js'
+export * from './VPLink.js'
diff --git a/packages/client/src/composables/index.ts b/packages/client/src/composables/index.ts
index 838b38b191..f6c1751901 100644
--- a/packages/client/src/composables/index.ts
+++ b/packages/client/src/composables/index.ts
@@ -5,8 +5,8 @@ export * from './pageHead.js'
export * from './pageHeadTitle.js'
export * from './pageLang.js'
export * from './pageLayout.js'
-export * from './pagesData.js'
export * from './routeLocale.js'
+export * from './routes.js'
export * from './siteData.js'
export * from './siteLocaleData.js'
export * from './updateHead.js'
diff --git a/packages/client/src/composables/pageData.ts b/packages/client/src/composables/pageData.ts
index 3f96a00659..e9147272b2 100644
--- a/packages/client/src/composables/pageData.ts
+++ b/packages/client/src/composables/pageData.ts
@@ -1,6 +1,6 @@
import type { PageData } from '@vuepress/shared'
import type { InjectionKey, Ref } from 'vue'
-import { inject, readonly } from 'vue'
+import { inject } from 'vue'
export type { PageData }
@@ -17,18 +17,6 @@ export const pageDataSymbol: InjectionKey = Symbol(
__VUEPRESS_DEV__ ? 'pageData' : '',
)
-/**
- * Empty page data to be used as the fallback value
- */
-export const pageDataEmpty = readonly({
- key: '',
- path: '',
- title: '',
- lang: '',
- frontmatter: {},
- headers: [],
-} as PageData) as PageData
-
/**
* Returns the ref of the data of current page
*/
diff --git a/packages/client/src/composables/pagesData.ts b/packages/client/src/composables/pagesData.ts
deleted file mode 100644
index 6583ba089a..0000000000
--- a/packages/client/src/composables/pagesData.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { pagesData as pagesDataRaw } from '@internal/pagesData'
-import type { PageData } from '@vuepress/shared'
-import { ref } from 'vue'
-import type { Ref } from 'vue'
-
-/**
- * Data resolvers of all pages
- *
- * The key is page key, and the value is an async function that
- * returns the page data
- */
-export type PagesData = Record Promise) | undefined>
-
-/**
- * Ref wrapper of `PagesData`
- */
-export type PagesDataRef = Ref
-
-/**
- * Global pages data ref
- */
-export const pagesData: PagesDataRef = ref(pagesDataRaw)
-
-/**
- * Returns the ref of data resolvers of all pages
- */
-export const usePagesData = (): PagesDataRef => pagesData
diff --git a/packages/client/src/composables/routes.ts b/packages/client/src/composables/routes.ts
new file mode 100644
index 0000000000..65a736eab2
--- /dev/null
+++ b/packages/client/src/composables/routes.ts
@@ -0,0 +1,11 @@
+import { redirects, routes } from '../router/index.js'
+
+/**
+ * Returns the ref of pages map
+ */
+export const useRedirects = (): typeof redirects => redirects
+
+/**
+ * Returns the ref of routes map
+ */
+export const useRoutes = (): typeof routes => routes
diff --git a/packages/client/src/composables/siteData.ts b/packages/client/src/composables/siteData.ts
index 89a16b47b8..f070e48713 100644
--- a/packages/client/src/composables/siteData.ts
+++ b/packages/client/src/composables/siteData.ts
@@ -1,6 +1,6 @@
import { siteData as siteDataRaw } from '@internal/siteData'
import type { SiteData } from '@vuepress/shared'
-import { ref } from 'vue'
+import { shallowRef } from 'vue'
import type { Ref } from 'vue'
export type { SiteData }
@@ -13,7 +13,7 @@ export type SiteDataRef = Ref
/**
* Global site data ref
*/
-export const siteData: SiteDataRef = ref(siteDataRaw)
+export const siteData: SiteDataRef = shallowRef(siteDataRaw)
/**
* Returns the ref of the site data
diff --git a/packages/client/src/router.ts b/packages/client/src/createVueRouter.ts
similarity index 61%
rename from packages/client/src/router.ts
rename to packages/client/src/createVueRouter.ts
index b9c6596d01..50f2078b53 100644
--- a/packages/client/src/router.ts
+++ b/packages/client/src/createVueRouter.ts
@@ -1,4 +1,3 @@
-import { pagesComponents } from '@internal/pagesComponents'
import { removeEndingSlash } from '@vuepress/shared'
import type { Router } from 'vue-router'
import {
@@ -7,9 +6,9 @@ import {
createWebHistory,
START_LOCATION,
} from 'vue-router'
+import { Vuepress } from './components/Vuepress.js'
import type { PageData } from './composables/index.js'
-import { resolvers } from './resolvers.js'
-import { createRoutes } from './routes.js'
+import { resolveRoute } from './router/index.js'
/**
* - use `createWebHistory` in dev mode and build mode client bundle
@@ -24,8 +23,14 @@ export const createVueRouter = (): Router => {
const router = createRouter({
// it might be an issue of vue-router that have to remove the ending slash
history: historyCreator(removeEndingSlash(__VUEPRESS_BASE__)),
- routes: createRoutes(),
- scrollBehavior: (to, from, savedPosition) => {
+ routes: [
+ {
+ name: 'vuepress-route',
+ path: '/:catchAll(.*)',
+ component: Vuepress,
+ },
+ ],
+ scrollBehavior: (to, _from, savedPosition) => {
if (savedPosition) return savedPosition
if (to.hash) return { el: to.hash }
return { top: 0 }
@@ -34,12 +39,21 @@ export const createVueRouter = (): Router => {
// ensure page data and page component have been loaded before resolving the route,
// and save page data to route meta
- router.beforeResolve(async (to, from) => {
+ router.beforeResolve(async (to, from): Promise => {
if (to.path !== from.path || from === START_LOCATION) {
- ;[to.meta._data] = await Promise.all([
- resolvers.resolvePageData(to.name as string),
- pagesComponents[to.name as string]?.__asyncLoader(),
- ])
+ const route = resolveRoute(to.path)
+
+ if (route.path !== to.path) {
+ return route.path
+ }
+ const pageChunk = await route.loader()
+
+ to.meta = {
+ // attach route meta
+ ...route.meta,
+ // attach page data to route meta to trigger page data computed when route changes
+ _data: pageChunk.data,
+ }
}
})
@@ -49,7 +63,7 @@ export const createVueRouter = (): Router => {
declare module 'vue-router' {
interface RouteMeta {
/**
- * Store page data to route meta
+ * Store page data in route meta
*
* @internal only for internal use
*/
diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts
index 88174b4ff8..ee9dd3a3df 100644
--- a/packages/client/src/index.ts
+++ b/packages/client/src/index.ts
@@ -3,5 +3,6 @@ export type { PageHeader } from '@vuepress/shared'
export * from './components/index.js'
export * from './composables/index.js'
export * from './helpers/index.js'
+export * from './router/index.js'
export * from './resolvers.js'
export * from './types/index.js'
diff --git a/packages/client/src/resolvers.ts b/packages/client/src/resolvers.ts
index 3d7ec20f43..fe1c30e12c 100644
--- a/packages/client/src/resolvers.ts
+++ b/packages/client/src/resolvers.ts
@@ -11,7 +11,6 @@ import type {
SiteData,
SiteLocaleData,
} from './composables/index.js'
-import { pageDataEmpty, pagesData } from './composables/index.js'
import { LAYOUT_NAME_DEFAULT, LAYOUT_NAME_NOT_FOUND } from './constants.js'
import type { ClientConfig, Layouts } from './types/index.js'
@@ -19,6 +18,8 @@ import type { ClientConfig, Layouts } from './types/index.js'
* Resolver methods to get global computed
*
* Users can override corresponding method for advanced customization
+ *
+ * @experimental - This is an experimental API and may be changed in minor versions
*/
export const resolvers = reactive({
/**
@@ -33,15 +34,6 @@ export const resolvers = reactive({
{} as Layouts,
),
- /**
- * Resolve page data according to page key
- */
- resolvePageData: async (pageKey: string): Promise => {
- const pageDataResolver = pagesData.value[pageKey]
- const pageData = await pageDataResolver?.()
- return pageData ?? pageDataEmpty
- },
-
/**
* Resolve page frontmatter from page data
*/
diff --git a/packages/client/src/router/index.ts b/packages/client/src/router/index.ts
new file mode 100644
index 0000000000..84600902f1
--- /dev/null
+++ b/packages/client/src/router/index.ts
@@ -0,0 +1,3 @@
+export * from './resolveRoute.js'
+export * from './resolveRoutePath.js'
+export * from './routes.js'
diff --git a/packages/client/src/router/resolveRoute.ts b/packages/client/src/router/resolveRoute.ts
new file mode 100644
index 0000000000..e15109ebfd
--- /dev/null
+++ b/packages/client/src/router/resolveRoute.ts
@@ -0,0 +1,30 @@
+import { resolveRoutePath } from './resolveRoutePath.js'
+import type { PageMetaDefault, Route } from './routes.js'
+import { routes } from './routes.js'
+
+interface ResolvedRoute
+ extends Route {
+ path: string
+ notFound: boolean
+}
+
+/**
+ * Resolve route with given path
+ */
+export const resolveRoute = <
+ PageMeta extends PageMetaDefault = PageMetaDefault,
+>(
+ path: string,
+): ResolvedRoute => {
+ const routePath = resolveRoutePath(path)
+ const route = routes.value[routePath] ?? {
+ ...routes.value['/404.html'],
+ notFound: true,
+ }
+
+ return {
+ path: routePath,
+ notFound: false,
+ ...route,
+ } as ResolvedRoute
+}
diff --git a/packages/client/src/router/resolveRoutePath.ts b/packages/client/src/router/resolveRoutePath.ts
new file mode 100644
index 0000000000..ca3ae57349
--- /dev/null
+++ b/packages/client/src/router/resolveRoutePath.ts
@@ -0,0 +1,18 @@
+import { normalizeRoutePath } from '@vuepress/shared'
+import { redirects, routes } from './routes.js'
+
+/**
+ * Resolve route path with given raw path
+ */
+export const resolveRoutePath = (path: string): string => {
+ // normalized path
+ const normalizedPath = normalizeRoutePath(path)
+ if (routes.value[normalizedPath]) return normalizedPath
+
+ // encoded path
+ const encodedPath = encodeURI(normalizedPath)
+ if (routes.value[encodedPath]) return encodedPath
+
+ // redirected path or fallback to the normalized path
+ return redirects.value[normalizedPath] || normalizedPath
+}
diff --git a/packages/client/src/router/routes.ts b/packages/client/src/router/routes.ts
new file mode 100644
index 0000000000..b523b9b5be
--- /dev/null
+++ b/packages/client/src/router/routes.ts
@@ -0,0 +1,35 @@
+import {
+ redirects as redirectsRaw,
+ routes as routesRaw,
+} from '@internal/routes'
+import type {
+ PageChunk,
+ PageMetaDefault,
+ Redirects,
+ Route,
+ Routes,
+} from '@internal/routes'
+import { shallowRef } from 'vue'
+import type { Ref } from 'vue'
+
+export type { PageMetaDefault, PageChunk, Redirects, Route, Routes }
+
+/**
+ * Global redirects ref
+ */
+export const redirects: Ref = shallowRef(redirectsRaw)
+
+/**
+ * Global routes ref
+ */
+export const routes: Ref = shallowRef(routesRaw)
+
+if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
+ // reuse vue HMR runtime
+ __VUE_HMR_RUNTIME__.updateRoutes = (data: Routes) => {
+ routes.value = data
+ }
+ __VUE_HMR_RUNTIME__.updateRedirects = (data: Redirects) => {
+ redirects.value = data
+ }
+}
diff --git a/packages/client/src/routes.ts b/packages/client/src/routes.ts
deleted file mode 100644
index a0b6e3a816..0000000000
--- a/packages/client/src/routes.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { pagesRoutes } from '@internal/pagesRoutes'
-import type { RouteRecordRaw } from 'vue-router'
-import { Vuepress } from './components/Vuepress.js'
-
-/**
- * Create routes for vue-router
- */
-export const createRoutes = (): RouteRecordRaw[] =>
- pagesRoutes.reduce(
- (result, [name, path, meta, redirects]) => {
- result.push(
- {
- name,
- path,
- component: Vuepress,
- meta,
- },
- {
- path: path.endsWith('/')
- ? // redirect from `/index.html` to `/`
- path + 'index.html'
- : // redirect from `/foo` to `/foo.html`
- path.substring(0, path.length - 5),
- redirect: path,
- },
- ...redirects.map((item) => ({
- path:
- item === ':md'
- ? // redirect from `/foo.md` to `/foo.html`
- path.substring(0, path.length - 5) + '.md'
- : item,
- redirect: path,
- })),
- )
- return result
- },
- [
- {
- name: '404',
- path: '/:catchAll(.*)',
- component: Vuepress,
- },
- ] as RouteRecordRaw[],
- )
diff --git a/packages/client/src/setupGlobalComponents.ts b/packages/client/src/setupGlobalComponents.ts
index b31443fd82..bd35c3628d 100644
--- a/packages/client/src/setupGlobalComponents.ts
+++ b/packages/client/src/setupGlobalComponents.ts
@@ -1,5 +1,5 @@
import type { App } from 'vue'
-import { ClientOnly, Content } from './components/index.js'
+import { ClientOnly, Content, VPLink } from './components/index.js'
/**
* Register global built-in components
@@ -8,5 +8,6 @@ export const setupGlobalComponents = (app: App): void => {
/* eslint-disable vue/match-component-file-name, vue/no-reserved-component-names */
app.component('ClientOnly', ClientOnly)
app.component('Content', Content)
+ app.component('VPLink', VPLink)
/* eslint-enable vue/match-component-file-name, vue/no-reserved-component-names */
}
diff --git a/packages/client/src/setupGlobalComputed.ts b/packages/client/src/setupGlobalComputed.ts
index e12945c57c..7554a76cd9 100644
--- a/packages/client/src/setupGlobalComputed.ts
+++ b/packages/client/src/setupGlobalComputed.ts
@@ -30,13 +30,13 @@ import {
pageHeadTitleSymbol,
pageLangSymbol,
pageLayoutSymbol,
- pagesData,
routeLocaleSymbol,
siteData,
siteLocaleDataSymbol,
} from './composables/index.js'
import { withBase } from './helpers/index.js'
import { resolvers } from './resolvers.js'
+import { routes } from './router/index.js'
import type { ClientConfig } from './types/index.js'
/**
@@ -73,9 +73,11 @@ export const setupGlobalComputed = (
)
// handle page data HMR
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
- __VUE_HMR_RUNTIME__.updatePageData = (data: PageData) => {
- pagesData.value[data.key] = () => Promise.resolve(data)
- if (data.key === router.currentRoute.value.meta._data?.key) {
+ __VUE_HMR_RUNTIME__.updatePageData = async (data: PageData) => {
+ const pageChunk = await routes.value[data.path].loader()
+ routes.value[data.path].loader = () =>
+ Promise.resolve({ comp: pageChunk.comp, data })
+ if (data.path === router.currentRoute.value.meta._data?.path) {
router.currentRoute.value.meta._data = data
pageData.trigger()
}
diff --git a/packages/client/src/types/index.ts b/packages/client/src/types/index.ts
index 0988bf7ec3..ec9c7595e1 100644
--- a/packages/client/src/types/index.ts
+++ b/packages/client/src/types/index.ts
@@ -1,4 +1,3 @@
export * from './clientConfig.js'
export * from './createVueAppFunction.js'
export * from './layouts.js'
-export * from './pageRouteItem.js'
diff --git a/packages/client/src/types/internal/pagesComponents.d.ts b/packages/client/src/types/internal/pagesComponents.d.ts
deleted file mode 100644
index e711c6fd6d..0000000000
--- a/packages/client/src/types/internal/pagesComponents.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import type { ComponentOptions } from 'vue'
-
-declare module '@internal/pagesComponents' {
- export const pagesComponents: Record
-}
diff --git a/packages/client/src/types/internal/pagesData.d.ts b/packages/client/src/types/internal/pagesData.d.ts
deleted file mode 100644
index 0671aeedba..0000000000
--- a/packages/client/src/types/internal/pagesData.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import type { PageData } from '@vuepress/shared'
-
-declare module '@internal/pagesData' {
- export const pagesData: Record Promise>
-}
diff --git a/packages/client/src/types/internal/pagesRoutes.d.ts b/packages/client/src/types/internal/pagesRoutes.d.ts
deleted file mode 100644
index 878d753d59..0000000000
--- a/packages/client/src/types/internal/pagesRoutes.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import type { PageRouteItem } from '../pageRouteItem.js'
-
-declare module '@internal/pagesRoutes' {
- export const pagesRoutes: PageRouteItem[]
-}
diff --git a/packages/client/src/types/internal/routes.d.ts b/packages/client/src/types/internal/routes.d.ts
new file mode 100644
index 0000000000..96d8109469
--- /dev/null
+++ b/packages/client/src/types/internal/routes.d.ts
@@ -0,0 +1,22 @@
+import type { PageData } from '@vuepress/shared'
+import type { ComponentOptions } from 'vue'
+
+declare module '@internal/routes' {
+ export interface PageChunk {
+ comp: ComponentOptions
+ data: PageData
+ }
+
+ export type PageMetaDefault = Record
+
+ export interface Route {
+ loader: () => Promise
+ meta: PageMeta
+ }
+
+ export type Redirects = Record
+ export type Routes = Record
+
+ export const redirects: Redirects
+ export const routes: Routes
+}
diff --git a/packages/client/src/types/pageRouteItem.ts b/packages/client/src/types/pageRouteItem.ts
deleted file mode 100644
index 0cbac2808f..0000000000
--- a/packages/client/src/types/pageRouteItem.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type { RouteMeta } from 'vue-router'
-
-export type PageRouteItem = [
- name: string,
- path: string,
- meta: RouteMeta,
- redirects: string[],
-]
diff --git a/packages/core/src/app/appPrepare.ts b/packages/core/src/app/appPrepare.ts
index 85b8b7b854..e2552022ca 100644
--- a/packages/core/src/app/appPrepare.ts
+++ b/packages/core/src/app/appPrepare.ts
@@ -2,11 +2,9 @@ import { debug } from '@vuepress/utils'
import type { App } from '../types/index.js'
import {
prepareClientConfigs,
+ preparePageChunk,
preparePageComponent,
- preparePageData,
- preparePagesComponents,
- preparePagesData,
- preparePagesRoutes,
+ prepareRoutes,
prepareSiteData,
} from './prepare/index.js'
@@ -27,16 +25,14 @@ export const appPrepare = async (app: App): Promise => {
for (const page of app.pages) {
await preparePageComponent(app, page)
}
- await preparePagesComponents(app)
- // generate page data files
+ // generate page files
for (const page of app.pages) {
- await preparePageData(app, page)
+ await preparePageChunk(app, page)
}
- await preparePagesData(app)
// generate routes file
- await preparePagesRoutes(app)
+ await prepareRoutes(app)
// generate site data file
await prepareSiteData(app)
diff --git a/packages/core/src/app/prepare/index.ts b/packages/core/src/app/prepare/index.ts
index 393e1c7801..ea49aa7600 100644
--- a/packages/core/src/app/prepare/index.ts
+++ b/packages/core/src/app/prepare/index.ts
@@ -1,7 +1,5 @@
export * from './prepareClientConfigs.js'
+export * from './preparePageChunk.js'
export * from './preparePageComponent.js'
-export * from './preparePageData.js'
-export * from './preparePagesComponents.js'
-export * from './preparePagesData.js'
-export * from './preparePagesRoutes.js'
+export * from './prepareRoutes.js'
export * from './prepareSiteData.js'
diff --git a/packages/core/src/app/prepare/preparePageData.ts b/packages/core/src/app/prepare/preparePageChunk.ts
similarity index 53%
rename from packages/core/src/app/prepare/preparePageData.ts
rename to packages/core/src/app/prepare/preparePageChunk.ts
index 7bf96372b4..353a98eee1 100644
--- a/packages/core/src/app/prepare/preparePageData.ts
+++ b/packages/core/src/app/prepare/preparePageChunk.ts
@@ -16,13 +16,14 @@ if (import.meta.hot) {
`
/**
- * Generate page data temp file of a single page
+ * Generate page chunk temp file of a single page
*/
-export const preparePageData = async (app: App, page: Page): Promise => {
- // page data file content
- let content = `export const data = JSON.parse(${JSON.stringify(
- JSON.stringify(page.data),
- )})
+export const preparePageChunk = async (app: App, page: Page): Promise => {
+ // page chunk file content
+ let content = `\
+import comp from ${JSON.stringify(page.componentFilePath)}
+const data = JSON.parse(${JSON.stringify(JSON.stringify(page.data))})
+export { comp, data }
`
// inject HMR code
@@ -30,5 +31,5 @@ export const preparePageData = async (app: App, page: Page): Promise => {
content += HMR_CODE
}
- await app.writeTemp(page.dataFilePathRelative, content)
+ await app.writeTemp(page.chunkFilePathRelative, content)
}
diff --git a/packages/core/src/app/prepare/preparePagesComponents.ts b/packages/core/src/app/prepare/preparePagesComponents.ts
deleted file mode 100644
index a63b678d60..0000000000
--- a/packages/core/src/app/prepare/preparePagesComponents.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import type { App } from '../../types/index.js'
-
-/**
- * Generate page key to page component map temp file
- */
-export const preparePagesComponents = async (app: App): Promise => {
- // generate page component map file
- const content = `\
-import { defineAsyncComponent } from 'vue'
-
-export const pagesComponents = {\
-${app.pages
- .map(
- ({ key, path, componentFilePath, componentFileChunkName }) => `
- // path: ${path}
- ${JSON.stringify(key)}: defineAsyncComponent(() => import(${
- componentFileChunkName
- ? `/* webpackChunkName: "${componentFileChunkName}" */`
- : ''
- }${JSON.stringify(componentFilePath)})),`,
- )
- .join('')}
-}
-`
-
- await app.writeTemp('internal/pagesComponents.js', content)
-}
diff --git a/packages/core/src/app/prepare/preparePagesData.ts b/packages/core/src/app/prepare/preparePagesData.ts
deleted file mode 100644
index 8d2c582f6d..0000000000
--- a/packages/core/src/app/prepare/preparePagesData.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { App } from '../../types/index.js'
-
-/**
- * Generate page path to page data map temp file
- */
-export const preparePagesData = async (app: App): Promise => {
- // generate page data map
- const content = `\
-export const pagesData = {\
-${app.pages
- .map(
- ({ key, path, dataFilePath, dataFileChunkName }) => `
- // path: ${path}
- ${JSON.stringify(key)}: () => import(${
- dataFileChunkName ? `/* webpackChunkName: "${dataFileChunkName}" */` : ''
- }${JSON.stringify(dataFilePath)}).then(({ data }) => data),`,
- )
- .join('')}
-}
-`
-
- await app.writeTemp('internal/pagesData.js', content)
-}
diff --git a/packages/core/src/app/prepare/preparePagesRoutes.ts b/packages/core/src/app/prepare/preparePagesRoutes.ts
deleted file mode 100644
index 5c9130049e..0000000000
--- a/packages/core/src/app/prepare/preparePagesRoutes.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import type { PageRouteItem } from '@vuepress/client'
-import { ensureLeadingSlash } from '@vuepress/shared'
-import type { App, Page } from '../../types/index.js'
-
-/**
- * Resolve page route item
- */
-const resolvePageRouteItem = ({
- key,
- path,
- pathInferred,
- filePathRelative,
- routeMeta,
-}: Page): PageRouteItem => {
- // paths that should redirect to this page, use set to dedupe
- const redirectsSet = new Set()
-
- // redirect from decoded path
- redirectsSet.add(decodeURI(path))
-
- // redirect from inferred path
- if (pathInferred !== null) {
- redirectsSet.add(pathInferred)
- redirectsSet.add(encodeURI(pathInferred))
- }
-
- // redirect from filename path
- if (filePathRelative !== null) {
- const filenamePath = ensureLeadingSlash(filePathRelative)
- redirectsSet.add(filenamePath)
- redirectsSet.add(encodeURI(filenamePath))
- }
-
- // avoid redirect from the page path itself
- redirectsSet.delete(path)
-
- return [
- key,
- path,
- routeMeta,
- [...redirectsSet].map((item) =>
- item.replace(/\.md$/, '.html') === path ? ':md' : item,
- ),
- ]
-}
-
-/**
- * Generate routes temp file
- */
-export const preparePagesRoutes = async (app: App): Promise => {
- const routeItems = app.pages.map(resolvePageRouteItem)
- const content = `\
-export const pagesRoutes = [\
-${routeItems.map((routeItem) => `\n ${JSON.stringify(routeItem)},`).join('')}
-]
-`
-
- await app.writeTemp('internal/pagesRoutes.js', content)
-}
diff --git a/packages/core/src/app/prepare/prepareRoutes.ts b/packages/core/src/app/prepare/prepareRoutes.ts
new file mode 100644
index 0000000000..969ed879c1
--- /dev/null
+++ b/packages/core/src/app/prepare/prepareRoutes.ts
@@ -0,0 +1,90 @@
+import { ensureLeadingSlash, normalizeRoutePath } from '@vuepress/shared'
+import type { App, Page } from '../../types/index.js'
+
+const HMR_CODE = `
+if (import.meta.webpackHot) {
+ import.meta.webpackHot.accept()
+ if (__VUE_HMR_RUNTIME__.updateRoutes) {
+ __VUE_HMR_RUNTIME__.updateRoutes(routes)
+ }
+ if (__VUE_HMR_RUNTIME__.updateRedirects) {
+ __VUE_HMR_RUNTIME__.updateRedirects(redirects)
+ }
+}
+
+if (import.meta.hot) {
+ import.meta.hot.accept(({ routes, redirects }) => {
+ __VUE_HMR_RUNTIME__.updateRoutes(routes)
+ __VUE_HMR_RUNTIME__.updateRedirects(redirects)
+ })
+}
+`
+
+/**
+ * Resolve page redirects
+ */
+const resolvePageRedirects = ({
+ path,
+ pathInferred,
+ filePathRelative,
+}: Page): string[] => {
+ // paths that should redirect to this page, use set to dedupe
+ const redirectsSet = new Set()
+
+ // add redirect to the set when the redirect could not be normalized & encoded to the page path
+ const addRedirect = (redirect: string): void => {
+ const normalizedPath = normalizeRoutePath(redirect)
+ if (normalizedPath === path) return
+
+ const encodedPath = encodeURI(normalizedPath)
+ if (encodedPath === path) return
+
+ redirectsSet.add(redirect)
+ }
+
+ // redirect from inferred path
+ if (pathInferred !== null) {
+ addRedirect(pathInferred)
+ }
+
+ // redirect from filename path
+ if (filePathRelative !== null) {
+ addRedirect(ensureLeadingSlash(filePathRelative))
+ }
+
+ return Array.from(redirectsSet)
+}
+
+/**
+ * Generate routes temp file
+ */
+export const prepareRoutes = async (app: App): Promise => {
+ // routes file content
+ let content = `\
+export const redirects = JSON.parse(${JSON.stringify(
+ JSON.stringify(
+ Object.fromEntries(
+ app.pages.flatMap((page) =>
+ resolvePageRedirects(page).map((redirect) => [redirect, page.path]),
+ ),
+ ),
+ ),
+ )})
+
+export const routes = Object.fromEntries([
+${app.pages
+ .map(
+ ({ chunkFilePath, chunkName, path, routeMeta }) =>
+ ` [${JSON.stringify(path)}, { loader: () => import(${chunkName ? `/* webpackChunkName: "${chunkName}" */` : ''}${JSON.stringify(chunkFilePath)}), meta: ${JSON.stringify(routeMeta)} }],`,
+ )
+ .join('\n')}
+]);
+`
+
+ // inject HMR code
+ if (app.env.isDev) {
+ content += HMR_CODE
+ }
+
+ await app.writeTemp('internal/routes.js', content)
+}
diff --git a/packages/core/src/app/resolveAppPages.ts b/packages/core/src/app/resolveAppPages.ts
index d253ec547f..2f721fa7fd 100644
--- a/packages/core/src/app/resolveAppPages.ts
+++ b/packages/core/src/app/resolveAppPages.ts
@@ -21,14 +21,20 @@ export const resolveAppPages = async (app: App): Promise => {
pageFilePaths.map((filePath) => createPage(app, { filePath })),
)
+ // find the 404 page
+ const notFoundPage = pages.find((page) => page.path === '/404.html')
+
+ // if there is a 404 page, set the default layout to NotFound
+ if (notFoundPage) {
+ notFoundPage.frontmatter.layout ??= 'NotFound'
+ }
// if there is no 404 page, add one
- if (!pages.some((page) => page.path === '/404.html')) {
+ else {
pages.push(
await createPage(app, {
path: '/404.html',
- frontmatter: {
- layout: 'NotFound',
- },
+ frontmatter: { layout: 'NotFound' },
+ content: '404 Not Found',
}),
)
}
diff --git a/packages/core/src/page/createPage.ts b/packages/core/src/page/createPage.ts
index f1edd0d963..ab9b05deb6 100644
--- a/packages/core/src/page/createPage.ts
+++ b/packages/core/src/page/createPage.ts
@@ -1,13 +1,12 @@
import type { App, Page, PageOptions } from '../types/index.js'
import { inferPagePath } from './inferPagePath.js'
import { renderPageContent } from './renderPageContent.js'
+import { resolvePageChunkInfo } from './resolvePageChunkInfo.js'
import { resolvePageComponentInfo } from './resolvePageComponentInfo.js'
-import { resolvePageDataInfo } from './resolvePageDataInfo.js'
import { resolvePageDate } from './resolvePageDate.js'
import { resolvePageFileContent } from './resolvePageFileContent.js'
import { resolvePageFilePath } from './resolvePageFilePath.js'
import { resolvePageHtmlInfo } from './resolvePageHtmlInfo.js'
-import { resolvePageKey } from './resolvePageKey.js'
import { resolvePageLang } from './resolvePageLang.js'
import { resolvePagePath } from './resolvePagePath.js'
import { resolvePagePermalink } from './resolvePagePermalink.js'
@@ -76,9 +75,6 @@ export const createPage = async (
// resolve page path
const path = resolvePagePath({ permalink, pathInferred, options })
- // resolve page key
- const key = resolvePageKey({ path })
-
// resolve page rendered html file path
const { htmlFilePath, htmlFilePathRelative } = resolvePageHtmlInfo({
app,
@@ -86,23 +82,18 @@ export const createPage = async (
})
// resolve page component and extract headers & links
- const {
- componentFilePath,
- componentFilePathRelative,
- componentFileChunkName,
- } = await resolvePageComponentInfo({
- app,
- htmlFilePathRelative,
- key,
- })
+ const { componentFilePath, componentFilePathRelative } =
+ await resolvePageComponentInfo({
+ app,
+ htmlFilePathRelative,
+ })
- const { dataFilePath, dataFilePathRelative, dataFileChunkName } =
- resolvePageDataInfo({ app, htmlFilePathRelative, key })
+ const { chunkFilePath, chunkFilePathRelative, chunkName } =
+ resolvePageChunkInfo({ app, htmlFilePathRelative })
const page: Page = {
// page data
data: {
- key,
path,
title,
lang,
@@ -111,7 +102,6 @@ export const createPage = async (
},
// base fields
- key,
path,
title,
lang,
@@ -137,10 +127,9 @@ export const createPage = async (
filePathRelative,
componentFilePath,
componentFilePathRelative,
- componentFileChunkName,
- dataFilePath,
- dataFilePathRelative,
- dataFileChunkName,
+ chunkFilePath,
+ chunkFilePathRelative,
+ chunkName,
htmlFilePath,
htmlFilePathRelative,
}
diff --git a/packages/core/src/page/index.ts b/packages/core/src/page/index.ts
index 1d41ae5ae1..179d9f4cc6 100644
--- a/packages/core/src/page/index.ts
+++ b/packages/core/src/page/index.ts
@@ -1,13 +1,12 @@
export * from './createPage.js'
export * from './inferPagePath.js'
export * from './renderPageContent.js'
+export * from './resolvePageChunkInfo.js'
export * from './resolvePageComponentInfo.js'
-export * from './resolvePageDataInfo.js'
export * from './resolvePageDate.js'
export * from './resolvePageFileContent.js'
export * from './resolvePageFilePath.js'
export * from './resolvePageHtmlInfo.js'
-export * from './resolvePageKey.js'
export * from './resolvePageLang.js'
export * from './resolvePagePath.js'
export * from './resolvePagePermalink.js'
diff --git a/packages/core/src/page/resolvePageChunkInfo.ts b/packages/core/src/page/resolvePageChunkInfo.ts
new file mode 100644
index 0000000000..79cb154ce4
--- /dev/null
+++ b/packages/core/src/page/resolvePageChunkInfo.ts
@@ -0,0 +1,27 @@
+import { hash, path } from '@vuepress/utils'
+import type { App } from '../types/index.js'
+
+/**
+ * Resolve page data file path
+ */
+export const resolvePageChunkInfo = ({
+ app,
+ htmlFilePathRelative,
+}: {
+ app: App
+ htmlFilePathRelative: string
+}): {
+ chunkFilePath: string
+ chunkFilePathRelative: string
+ chunkName: string
+} => {
+ const chunkFilePathRelative = path.join('pages', `${htmlFilePathRelative}.js`)
+ const chunkFilePath = app.dir.temp(chunkFilePathRelative)
+ const chunkName = `v-${hash(htmlFilePathRelative)}`
+
+ return {
+ chunkFilePath,
+ chunkFilePathRelative,
+ chunkName,
+ }
+}
diff --git a/packages/core/src/page/resolvePageComponentInfo.ts b/packages/core/src/page/resolvePageComponentInfo.ts
index 1fed489ad7..d27aa1933c 100644
--- a/packages/core/src/page/resolvePageComponentInfo.ts
+++ b/packages/core/src/page/resolvePageComponentInfo.ts
@@ -7,15 +7,12 @@ import type { App } from '../types/index.js'
export const resolvePageComponentInfo = async ({
app,
htmlFilePathRelative,
- key,
}: {
app: App
htmlFilePathRelative: string
- key: string
}): Promise<{
componentFilePath: string
componentFilePathRelative: string
- componentFileChunkName: string
}> => {
// resolve component file path
const componentFilePathRelative = path.join(
@@ -23,11 +20,9 @@ export const resolvePageComponentInfo = async ({
`${htmlFilePathRelative}.vue`,
)
const componentFilePath = app.dir.temp(componentFilePathRelative)
- const componentFileChunkName = key
return {
componentFilePath,
componentFilePathRelative,
- componentFileChunkName,
}
}
diff --git a/packages/core/src/page/resolvePageDataInfo.ts b/packages/core/src/page/resolvePageDataInfo.ts
deleted file mode 100644
index 60a826c97f..0000000000
--- a/packages/core/src/page/resolvePageDataInfo.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { path } from '@vuepress/utils'
-import type { App } from '../types/index.js'
-
-/**
- * Resolve page data file path
- */
-export const resolvePageDataInfo = ({
- app,
- htmlFilePathRelative,
- key,
-}: {
- app: App
- htmlFilePathRelative: string
- key: string
-}): {
- dataFilePath: string
- dataFilePathRelative: string
- dataFileChunkName: string
-} => {
- const dataFilePathRelative = path.join('pages', `${htmlFilePathRelative}.js`)
- const dataFilePath = app.dir.temp(dataFilePathRelative)
- const dataFileChunkName = key
-
- return {
- dataFilePath,
- dataFilePathRelative,
- dataFileChunkName,
- }
-}
diff --git a/packages/core/src/page/resolvePageHtmlInfo.ts b/packages/core/src/page/resolvePageHtmlInfo.ts
index 5cecab0323..58d1c63b4b 100644
--- a/packages/core/src/page/resolvePageHtmlInfo.ts
+++ b/packages/core/src/page/resolvePageHtmlInfo.ts
@@ -14,10 +14,16 @@ export const resolvePageHtmlInfo = ({
htmlFilePath: string
htmlFilePathRelative: string
} => {
+ const path = decodeURI(pagePath)
+
// /foo.html -> foo.html
// /foo/ -> foo/index.html
const htmlFilePathRelative = removeLeadingSlash(
- decodeURI(pagePath.replace(/\/$/, '/index.html')),
+ path.endsWith('/')
+ ? path + 'index.html'
+ : path.endsWith('.html')
+ ? path
+ : path + '.html',
)
const htmlFilePath = app.dir.dest(htmlFilePathRelative)
diff --git a/packages/core/src/page/resolvePageKey.ts b/packages/core/src/page/resolvePageKey.ts
deleted file mode 100644
index 859fcec450..0000000000
--- a/packages/core/src/page/resolvePageKey.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { hash } from '@vuepress/utils'
-
-/**
- * Resolve page key to identify the page
- */
-export const resolvePageKey = ({ path }: { path: string }): string =>
- `v-${hash(path)}`
diff --git a/packages/core/src/page/resolvePagePath.ts b/packages/core/src/page/resolvePagePath.ts
index 3929d6955c..c40e366455 100644
--- a/packages/core/src/page/resolvePagePath.ts
+++ b/packages/core/src/page/resolvePagePath.ts
@@ -1,4 +1,3 @@
-import { ensureEndingSlash } from '@vuepress/shared'
import { logger, sanitizeFileName } from '@vuepress/utils'
import type { PageOptions } from '../types/index.js'
@@ -14,7 +13,7 @@ export const resolvePagePath = ({
pathInferred: string | null
options: PageOptions
}): string => {
- let pagePath = options.path || permalink || pathInferred
+ const pagePath = options.path || permalink || pathInferred
if (!pagePath) {
throw logger.createError(
@@ -22,9 +21,5 @@ export const resolvePagePath = ({
)
}
- if (!pagePath.endsWith('.html')) {
- pagePath = ensureEndingSlash(pagePath)
- }
-
return encodeURI(pagePath.split('/').map(sanitizeFileName).join('/'))
}
diff --git a/packages/core/src/types/page.ts b/packages/core/src/types/page.ts
index a274bd442f..5aadd8d7ae 100644
--- a/packages/core/src/types/page.ts
+++ b/packages/core/src/types/page.ts
@@ -73,9 +73,7 @@ export type Page<
permalink: string | null
/**
- * Custom data to be attached to the page route record of vue-router
- *
- * @see https://router.vuejs.org/api/#meta
+ * Custom data to be attached to route record
*/
routeMeta: Record
@@ -114,28 +112,21 @@ export type Page<
componentFilePathRelative: string
/**
- * Component file chunk name
- *
- * Only take effect in webpack
- */
- componentFileChunkName: string
-
- /**
- * Page data file path
+ * Chunk file path
*/
- dataFilePath: string
+ chunkFilePath: string
/**
- * Page data file path relative to temp directory
+ * Chunk file path relative to temp directory
*/
- dataFilePathRelative: string
+ chunkFilePathRelative: string
/**
- * Page data file chunk name
+ * Chunk name
*
- * Only take effect in webpack
+ * This will only take effect in webpack
*/
- dataFileChunkName: string
+ chunkName: string
/**
* Rendered html file path
diff --git a/packages/core/tests/page/createPage.spec.ts b/packages/core/tests/page/createPage.spec.ts
index 67d37ef3ee..e2f832b2ab 100644
--- a/packages/core/tests/page/createPage.spec.ts
+++ b/packages/core/tests/page/createPage.spec.ts
@@ -29,7 +29,6 @@ describe('core > page > createPage', () => {
})
// page data
- expect(page.data.key).toBeTruthy()
expect(page.data.path).toBe('/')
expect(page.data.lang).toBe('en-US')
expect(page.data.title).toBe('')
@@ -37,7 +36,6 @@ describe('core > page > createPage', () => {
expect(page.data.headers).toEqual([])
// base fields
- expect(page.key).toBeTruthy()
expect(page.path).toBe('/')
expect(page.lang).toBe('en-US')
expect(page.title).toBe('')
@@ -80,14 +78,13 @@ describe('core > page > createPage', () => {
expect(page.componentFilePathRelative).toBe(
`pages/${page.htmlFilePathRelative}.vue`,
)
- expect(page.componentFileChunkName).toBe(page.key)
- expect(page.dataFilePath).toBe(
+ expect(page.chunkFilePath).toBe(
app.dir.temp(`pages/${page.htmlFilePathRelative}.js`),
)
- expect(page.dataFilePathRelative).toBe(
+ expect(page.chunkFilePathRelative).toBe(
`pages/${page.htmlFilePathRelative}.js`,
)
- expect(page.dataFileChunkName).toBe(page.key)
+ expect(page.chunkName).toBeTruthy()
})
it('should be extended by plugin correctly', async () => {
diff --git a/packages/core/tests/page/resolvePageChunkInfo.spec.ts b/packages/core/tests/page/resolvePageChunkInfo.spec.ts
new file mode 100644
index 0000000000..46e4ce6cce
--- /dev/null
+++ b/packages/core/tests/page/resolvePageChunkInfo.spec.ts
@@ -0,0 +1,24 @@
+import { hash, path } from '@vuepress/utils'
+import { describe, expect, it } from 'vitest'
+import { createBaseApp, resolvePageChunkInfo } from '../../src/index.js'
+
+const app = createBaseApp({
+ source: path.resolve(__dirname, 'fake-source'),
+ theme: { name: 'test' },
+ bundler: {} as any,
+})
+
+describe('core > page > resolvePageChunkInfo', () => {
+ it('should resolve page chunk info correctly', () => {
+ const resolved = resolvePageChunkInfo({
+ app,
+ htmlFilePathRelative: 'foo.html',
+ })
+
+ expect(resolved).toEqual({
+ chunkFilePath: app.dir.temp('pages/foo.html.js'),
+ chunkFilePathRelative: 'pages/foo.html.js',
+ chunkName: `v-${hash('foo.html')}`,
+ })
+ })
+})
diff --git a/packages/core/tests/page/resolvePageComponentInfo.spec.ts b/packages/core/tests/page/resolvePageComponentInfo.spec.ts
index 5689940cbe..15c9b009b9 100644
--- a/packages/core/tests/page/resolvePageComponentInfo.spec.ts
+++ b/packages/core/tests/page/resolvePageComponentInfo.spec.ts
@@ -13,13 +13,11 @@ describe('core > page > resolvePageComponentInfo', () => {
const resolved = await resolvePageComponentInfo({
app,
htmlFilePathRelative: 'foo.html',
- key: 'key',
})
expect(resolved).toEqual({
componentFilePath: app.dir.temp('pages/foo.html.vue'),
componentFilePathRelative: 'pages/foo.html.vue',
- componentFileChunkName: 'key',
})
})
})
diff --git a/packages/core/tests/page/resolvePageDataInfo.spec.ts b/packages/core/tests/page/resolvePageDataInfo.spec.ts
index 6ee7a164ce..1e9cc8d662 100644
--- a/packages/core/tests/page/resolvePageDataInfo.spec.ts
+++ b/packages/core/tests/page/resolvePageDataInfo.spec.ts
@@ -1,6 +1,6 @@
-import { path } from '@vuepress/utils'
+import { hash, path } from '@vuepress/utils'
import { describe, expect, it } from 'vitest'
-import { createBaseApp, resolvePageDataInfo } from '../../src/index.js'
+import { createBaseApp, resolvePageChunkInfo } from '../../src/index.js'
const app = createBaseApp({
source: path.resolve(__dirname, 'fake-source'),
@@ -10,19 +10,17 @@ const app = createBaseApp({
describe('core > page > resolvePageDataInfo', () => {
it('should resolve page data file path correctly', () => {
- const key = 'foobar'
const htmlFilePathRelative = 'foobar.html'
const expectedFilePath = app.dir.temp(`pages/${htmlFilePathRelative}.js`)
expect(
- resolvePageDataInfo({
+ resolvePageChunkInfo({
app,
- key,
htmlFilePathRelative,
}),
).toEqual({
- dataFilePath: expectedFilePath,
- dataFilePathRelative: path.relative(app.dir.temp(), expectedFilePath),
- dataFileChunkName: key,
+ chunkFilePath: expectedFilePath,
+ chunkFilePathRelative: path.relative(app.dir.temp(), expectedFilePath),
+ chunkName: `v-${hash(htmlFilePathRelative)}`,
})
})
})
diff --git a/packages/core/tests/page/resolvePageHtmlInfo.spec.ts b/packages/core/tests/page/resolvePageHtmlInfo.spec.ts
index 52bed79e04..58042587be 100644
--- a/packages/core/tests/page/resolvePageHtmlInfo.spec.ts
+++ b/packages/core/tests/page/resolvePageHtmlInfo.spec.ts
@@ -9,8 +9,10 @@ const app = createBaseApp({
})
const testCases: [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'],
diff --git a/packages/core/tests/page/resolvePageKey.spec.ts b/packages/core/tests/page/resolvePageKey.spec.ts
deleted file mode 100644
index b574908edf..0000000000
--- a/packages/core/tests/page/resolvePageKey.spec.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import { resolvePageKey } from '../../src/index.js'
-
-describe('core > page > resolvePageKey', () => {
- it('should begin with "v-"', () => {
- const key = resolvePageKey({ path: 'foobar' })
-
- expect(key.startsWith('v-')).toBe(true)
- })
-
- it('should return different page key with different identifier', () => {
- const keyFoo = resolvePageKey({ path: 'foo' })
- const keyBar = resolvePageKey({ path: 'bar' })
-
- expect(keyFoo).not.toEqual(keyBar)
- })
-})
diff --git a/packages/core/tests/page/resolvePagePath.spec.ts b/packages/core/tests/page/resolvePagePath.spec.ts
index 56f44a0ec3..1ca68d2d65 100644
--- a/packages/core/tests/page/resolvePagePath.spec.ts
+++ b/packages/core/tests/page/resolvePagePath.spec.ts
@@ -16,7 +16,7 @@ const testCases: [
},
},
],
- '/options/',
+ '/options',
],
[
[
@@ -51,7 +51,7 @@ const testCases: [
options: {},
},
],
- '/permalink/',
+ '/permalink',
],
[
[
@@ -72,7 +72,7 @@ const testCases: [
options: {},
},
],
- '/inferred/',
+ '/inferred',
],
[
[
diff --git a/packages/markdown/src/plugins/linksPlugin/linksPlugin.ts b/packages/markdown/src/plugins/linksPlugin/linksPlugin.ts
index ecd65ae3a8..f2702f663b 100644
--- a/packages/markdown/src/plugins/linksPlugin/linksPlugin.ts
+++ b/packages/markdown/src/plugins/linksPlugin/linksPlugin.ts
@@ -8,9 +8,9 @@ export interface LinksPluginOptions {
/**
* Tag for internal links
*
- * @default 'RouterLink'
+ * @default 'VPLink'
*/
- internalTag?: 'a' | 'RouterLink'
+ internalTag?: 'a' | 'VPLink' | 'RouterLink'
/**
* Additional attributes for external links
@@ -29,7 +29,7 @@ export interface LinksPluginOptions {
/**
* Process links in markdown file
*
- * - internal links: convert them into ``
+ * - internal links: convert them into ``
* - external links: add extra attrs and external icon
*/
export const linksPlugin: PluginWithOptions = (
@@ -37,7 +37,7 @@ export const linksPlugin: PluginWithOptions = (
options: LinksPluginOptions = {},
): void => {
// tag of internal links
- const internalTag = options.internalTag || 'RouterLink'
+ const internalTag = options.internalTag || 'VPLink'
// attrs that going to be added to external links
const externalAttrs = {
@@ -93,7 +93,7 @@ export const linksPlugin: PluginWithOptions = (
// convert
//
// to
- //
+ //
// notice that the path and hash are encoded by markdown-it
const rawPath = internalLinkMatch[1]
@@ -109,7 +109,7 @@ 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 ``
+ // passed to ``
//
// '/foo/index.md' => '/foo/'
// '/foo/bar.md' => '/foo/bar.html'
@@ -118,8 +118,8 @@ export const linksPlugin: PluginWithOptions = (
.replace(/(^|\/)(README|index).md$/i, '$1')
.replace(/\.md$/, '.html')
- if (internalTag === 'RouterLink') {
- // convert starting tag of internal link to ``
+ if (['RouterLink', 'VPLink'].includes(internalTag)) {
+ // convert starting tag of internal link to `internalTag`
token.tag = internalTag
// replace the original `href` attr with `to` attr
hrefAttr[0] = 'to'
diff --git a/packages/markdown/tests/plugins/linksPlugin.spec.ts b/packages/markdown/tests/plugins/linksPlugin.spec.ts
index e9986f954c..75760afa3f 100644
--- a/packages/markdown/tests/plugins/linksPlugin.spec.ts
+++ b/packages/markdown/tests/plugins/linksPlugin.spec.ts
@@ -293,24 +293,24 @@ describe('@vuepress/markdown > plugins > linksPlugin', () => {
expect(rendered).toEqual(
[
- 'foo1',
- 'foo2',
- 'foo3',
- 'bar1',
- 'bar2',
- 'bar3',
- 'foobar1',
- 'foobar2',
- 'foobar3',
- 'foobar4',
- 'index1',
- 'index2',
- 'index3',
- 'index4',
- 'index5',
- 'readme1',
- 'readme2',
- 'readme3',
+ 'foo1',
+ 'foo2',
+ 'foo3',
+ 'bar1',
+ 'bar2',
+ 'bar3',
+ 'foobar1',
+ 'foobar2',
+ 'foobar3',
+ 'foobar4',
+ 'index1',
+ 'index2',
+ 'index3',
+ 'index4',
+ 'index5',
+ 'readme1',
+ 'readme2',
+ 'readme3',
]
.map((a) => `${a}
`)
.join('\n') + '\n',
@@ -420,24 +420,24 @@ describe('@vuepress/markdown > plugins > linksPlugin', () => {
expect(rendered).toEqual(
[
- 'foo1',
- 'foo2',
- 'foo3',
- 'bar1',
- 'bar2',
- 'bar3',
- 'foobar1',
- 'foobar2',
- 'foobar3',
- 'foobar4',
- 'index1',
- 'index2',
- 'index3',
- 'index4',
- 'index5',
- 'readme1',
- 'readme2',
- 'readme3',
+ 'foo1',
+ 'foo2',
+ 'foo3',
+ 'bar1',
+ 'bar2',
+ 'bar3',
+ 'foobar1',
+ 'foobar2',
+ 'foobar3',
+ 'foobar4',
+ 'index1',
+ 'index2',
+ 'index3',
+ 'index4',
+ 'index5',
+ 'readme1',
+ 'readme2',
+ 'readme3',
]
.map((a) => `${a}
`)
.join('\n') + '\n',
@@ -549,24 +549,24 @@ describe('@vuepress/markdown > plugins > linksPlugin', () => {
expect(rendered).toEqual(
[
- `foo1`,
- `foo2`,
- `foo3`,
- `bar1`,
- `bar2`,
- `bar3`,
- `foobar1`,
- `foobar2`,
- `foobar3`,
- `foobar4`,
- `index1`,
- `index2`,
- `index3`,
- `index4`,
- `index5`,
- `readme1`,
- `readme2`,
- `readme3`,
+ `foo1`,
+ `foo2`,
+ `foo3`,
+ `bar1`,
+ `bar2`,
+ `bar3`,
+ `foobar1`,
+ `foobar2`,
+ `foobar3`,
+ `foobar4`,
+ `index1`,
+ `index2`,
+ `index3`,
+ `index4`,
+ `index5`,
+ `readme1`,
+ `readme2`,
+ `readme3`,
]
.map((a) => `${a}
`)
.join('\n') + '\n',
@@ -677,24 +677,24 @@ describe('@vuepress/markdown > plugins > linksPlugin', () => {
expect(rendered).toEqual(
[
- 'foo1',
- 'foo2',
- 'foo3',
- 'bar1',
- 'bar2',
- 'bar3',
- 'foobar1',
- 'foobar2',
- 'foobar3',
- 'foobar4',
- 'index1',
- 'index2',
- 'index3',
- 'index4',
- 'index5',
- 'readme1',
- 'readme2',
- 'readme3',
+ 'foo1',
+ 'foo2',
+ 'foo3',
+ 'bar1',
+ 'bar2',
+ 'bar3',
+ 'foobar1',
+ 'foobar2',
+ 'foobar3',
+ 'foobar4',
+ 'index1',
+ 'index2',
+ 'index3',
+ 'index4',
+ 'index5',
+ 'readme1',
+ 'readme2',
+ 'readme3',
]
.map((a) => `${a}
`)
.join('\n') + '\n',
@@ -812,9 +812,9 @@ describe('@vuepress/markdown > plugins > linksPlugin', () => {
expect(rendered).toEqual(
[
- 'md',
- 'md-with-redundant-base',
- 'html',
+ 'md',
+ 'md-with-redundant-base',
+ 'html',
]
.map((a) => `${a}
`)
.join('\n') + '\n',
diff --git a/packages/shared/src/types/page.ts b/packages/shared/src/types/page.ts
index 4ffecb23e8..f05a83ad2c 100644
--- a/packages/shared/src/types/page.ts
+++ b/packages/shared/src/types/page.ts
@@ -7,15 +7,6 @@ import type { HeadConfig } from './head.js'
export interface PageBase<
ExtraPageFrontmatter extends Record = Record,
> {
- /**
- * Identifier of the page
- *
- * Will also be used as the component name
- *
- * @example 'v-foobar'
- */
- key: string
-
/**
* Route path of the page
*
diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts
index a017bf8c31..a14fe7c3d9 100644
--- a/packages/shared/src/utils/index.ts
+++ b/packages/shared/src/utils/index.ts
@@ -6,6 +6,7 @@ export * from './isLinkExternal.js'
export * from './isLinkHttp.js'
export * from './isLinkWithProtocol.js'
export * from './isPlainObject.js'
+export * from './normalizeRoutePath.js'
export * from './omit.js'
export * from './removeEndingSlash.js'
export * from './removeLeadingSlash.js'
diff --git a/packages/shared/src/utils/normalizeRoutePath.ts b/packages/shared/src/utils/normalizeRoutePath.ts
new file mode 100644
index 0000000000..c3924ac90b
--- /dev/null
+++ b/packages/shared/src/utils/normalizeRoutePath.ts
@@ -0,0 +1,16 @@
+/**
+ * Normalize the given path to the final route path
+ */
+export const normalizeRoutePath = (path: string): string => {
+ const convertedMdPath = path.endsWith('README.md')
+ ? path.substring(0, path.length - 9)
+ : path.endsWith('.md')
+ ? path.substring(0, path.length - 3) + '.html'
+ : path
+
+ return convertedMdPath.endsWith('/index.html')
+ ? convertedMdPath.substring(0, convertedMdPath.length - 10)
+ : convertedMdPath.endsWith('.html') || convertedMdPath.endsWith('/')
+ ? convertedMdPath
+ : convertedMdPath + '.html'
+}
diff --git a/packages/shared/tests/normalizeRoutePath.spec.ts b/packages/shared/tests/normalizeRoutePath.spec.ts
new file mode 100644
index 0000000000..b448ffceeb
--- /dev/null
+++ b/packages/shared/tests/normalizeRoutePath.spec.ts
@@ -0,0 +1,28 @@
+import { expect, it } from 'vitest'
+import { normalizeRoutePath } from '../src/index.js'
+
+const testCases = [
+ ['/', '/'],
+ ['/README.md', '/'],
+ ['/index.md', '/'],
+ ['/index.html', '/'],
+ ['/foo', '/foo.html'],
+ ['/foo.md', '/foo.html'],
+ ['/foo/', '/foo/'],
+ ['/foo/README.md', '/foo/'],
+ ['/foo/index.md', '/foo/'],
+ ['/foo/index.html', '/foo/'],
+ ['/foo/bar', '/foo/bar.html'],
+ ['/foo/bar/', '/foo/bar/'],
+ ['/foo/bar/README.md', '/foo/bar/'],
+ ['/foo/bar/index.md', '/foo/bar/'],
+ ['/foo/bar/index.html', '/foo/bar/'],
+ ['/foo/bar.md', '/foo/bar.html'],
+ ['/foo/bar.html', '/foo/bar.html'],
+]
+
+testCases.forEach(([path, expected]) =>
+ it(`should normalize "${path}" to "${expected}"`, () => {
+ expect(normalizeRoutePath(path)).toBe(expected)
+ }),
+)