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) + }), +)