diff --git a/.gitignore b/.gitignore index 6fec63b6..6e9dbd35 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /dist /node_modules +/src/client/shared +/src/node/shared *.log .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a505feb..85f6fe60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [0.7.3](https://github.com/vuejs/vitepress/compare/v0.7.2...v0.7.3) (2020-11-06) + +### Bug Fixes + +- Fix sidebar page switch layout shifting +- Fix production hydration mismatch + ## [0.7.2](https://github.com/vuejs/vitepress/compare/v0.7.1...v0.7.2) (2020-11-02) ### Bug Fixes diff --git a/package.json b/package.json index 3f696d9c..8a868adc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vitepress", - "version": "0.7.2", + "version": "0.7.3", "description": "", "main": "dist/node/index.js", "typings": "types/index.d.ts", @@ -27,14 +27,15 @@ }, "homepage": "https://github.com/vuejs/vitepress/tree/master/#readme", "scripts": { - "dev": "run-p dev-client dev-client-copy dev-node dev-shared", + "dev": "yarn dev-shared && yarn dev-start", + "dev-start": "run-p dev-client dev-node dev-watch", "dev-client": "tsc -w -p src/client", - "dev-client-copy": "node scripts/watchAndCopy", "dev-node": "tsc -w -p src/node", - "dev-shared": "tsc -w -p src/shared", - "release": "bash scripts/release.sh", - "build": "rimraf -rf dist && tsc -p src/client && tsc -p src/node && tsc -p src/shared && node scripts/copy", - "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s" + "dev-shared": "node scripts/copyShared", + "dev-watch": "node scripts/watchAndCopy", + "build": "rimraf -rf dist && node scripts/copyShared && tsc -p src/client && tsc -p src/node && node scripts/copyClient", + "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", + "release": "bash scripts/release.sh" }, "engines": { "node": ">=10.0.0" diff --git a/scripts/copy.js b/scripts/copyClient.js similarity index 100% rename from scripts/copy.js rename to scripts/copyClient.js diff --git a/scripts/copyShared.js b/scripts/copyShared.js new file mode 100644 index 00000000..af10bc26 --- /dev/null +++ b/scripts/copyShared.js @@ -0,0 +1,7 @@ +const fs = require('fs-extra') +const glob = require('globby') + +glob.sync('src/shared/**/*.ts').forEach((file) => { + fs.copy(file, file.replace(/^src\//, 'src/node/')) + fs.copy(file, file.replace(/^src\//, 'src/client/')) +}) diff --git a/scripts/watchAndCopy.js b/scripts/watchAndCopy.js index 898b708b..d88a1567 100644 --- a/scripts/watchAndCopy.js +++ b/scripts/watchAndCopy.js @@ -1,13 +1,31 @@ -// copy and watch non-ts files in src/client const fs = require('fs-extra') const chokidar = require('chokidar') -function toDest(file) { +function toClientAndNode(method, file) { + if (method === 'copy') { + fs.copy(file, file.replace(/^src\//, 'src/node/')) + fs.copy(file, file.replace(/^src\//, 'src/client/')) + } else if (method === 'remove') { + fs.remove(file.replace(/^src\//, 'src/node/')) + fs.remove(file.replace(/^src\//, 'src/client/')) + } +} + +function toDist(file) { return file.replace(/^src\//, 'dist/') } +// copy shared files to the client and node directory whenever they change. +chokidar + .watch('src/shared/**/*.ts') + .on('change', (file) => toClientAndNode('copy', file)) + .on('add', (file) => toClientAndNode('copy', file)) + .on('unlink', (file) => toClientAndNode('remove', file)) + +// copy non ts files, such as an html or css, to the dist directory whenever +// they change. chokidar .watch('src/client/**/!(*.ts|tsconfig.json)') - .on('change', (file) => fs.copy(file, toDest(file))) - .on('add', (file) => fs.copy(file, toDest(file))) - .on('unlink', (file) => fs.remove(toDest(file))) + .on('change', (file) => fs.copy(file, toDist(file))) + .on('add', (file) => fs.copy(file, toDist(file))) + .on('unlink', (file) => fs.remove(toDist(file))) diff --git a/src/client/app/components/Content.ts b/src/client/app/components/Content.ts index c2dcb634..82a0070e 100644 --- a/src/client/app/components/Content.ts +++ b/src/client/app/components/Content.ts @@ -9,6 +9,6 @@ export const Content = { // in prod mode, enable intersectionObserver based pre-fetch. usePrefetch() } - return () => (route.contentComponent ? h(route.contentComponent) : null) + return () => (route.component ? h(route.component) : null) } } diff --git a/src/client/app/composables/head.ts b/src/client/app/composables/head.ts index 30b17770..1230760a 100644 --- a/src/client/app/composables/head.ts +++ b/src/client/app/composables/head.ts @@ -1,11 +1,8 @@ import { watchEffect, Ref } from 'vue' -import { PageDataRef } from './pageData' import { HeadConfig, SiteData } from '../../../../types/shared' +import { Route } from '../router' -export function useUpdateHead( - pageDataRef: PageDataRef, - siteDataByRouteRef: Ref -) { +export function useUpdateHead(route: Route, siteDataByRouteRef: Ref) { const metaTags: HTMLElement[] = Array.from(document.querySelectorAll('meta')) let isFirstUpdate = true @@ -28,7 +25,7 @@ export function useUpdateHead( } watchEffect(() => { - const pageData = pageDataRef.value + const pageData = route.data const siteData = siteDataByRouteRef.value const pageTitle = pageData && pageData.title document.title = (pageTitle ? pageTitle + ` | ` : ``) + siteData.title diff --git a/src/client/app/composables/pageData.ts b/src/client/app/composables/pageData.ts deleted file mode 100644 index ab22bb79..00000000 --- a/src/client/app/composables/pageData.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { inject, InjectionKey, Ref } from 'vue' -import { PageData } from '../../../../types/shared' - -export type PageDataRef = Ref - -export const pageDataSymbol: InjectionKey = Symbol() - -export function usePageData(): PageDataRef { - const data = inject(pageDataSymbol) - if (!data) { - throw new Error('usePageData() is called without provider.') - } - return data -} diff --git a/src/client/app/exports.ts b/src/client/app/exports.ts index 2eff109e..226b402e 100644 --- a/src/client/app/exports.ts +++ b/src/client/app/exports.ts @@ -1,12 +1,11 @@ // exports in this file are exposed to themes and md files via 'vitepress' -// so the user can do `import { usePageData } from 'vitepress'` +// so the user can do `import { useRoute, useSiteData } from 'vitepress'` // theme types export * from './theme' // composables export { useSiteData } from './composables/siteData' -export { usePageData } from './composables/pageData' export { useSiteDataByRoute } from './composables/siteDataByRoute' export { useRouter, useRoute, Router, Route } from './router' diff --git a/src/client/app/index.ts b/src/client/app/index.ts index b9ba5bec..d5d4b732 100644 --- a/src/client/app/index.ts +++ b/src/client/app/index.ts @@ -1,7 +1,6 @@ -import { createApp as createClientApp, createSSRApp, ref, readonly } from 'vue' +import { createApp as createClientApp, createSSRApp } from 'vue' import { createRouter, RouterSymbol } from './router' import { useUpdateHead } from './composables/head' -import { pageDataSymbol } from './composables/pageData' import { Content } from './components/Content' import Debug from './components/Debug.vue' import Theme from '/@theme/index' @@ -12,63 +11,52 @@ import { siteDataRef } from './composables/siteData' const NotFound = Theme.NotFound || (() => '404 Not Found') export function createApp() { - // unlike site data which is static across all requests, page data is - // distinct per-request. - const pageDataRef = ref() - - if (import.meta.hot) { - // hot reload pageData - import.meta.hot!.on('vitepress:pageData', (data) => { - if ( - data.path.replace(/(\bindex)?\.md$/, '') === - location.pathname.replace(/(\bindex)?\.html$/, '') - ) { - pageDataRef.value = data.pageData - } - }) - } - let isInitialPageLoad = inBrowser let initialPath: string - const router = createRouter((route) => { - let pagePath = pathToFile(route.path) + const router = createRouter((path) => { + let pageFilePath = pathToFile(path) if (isInitialPageLoad) { - initialPath = pagePath + initialPath = pageFilePath } // use lean build if this is the initial page load or navigating back // to the initial loaded path (the static vnodes already adopted the // static content on that load so no need to re-fetch the page) - if (isInitialPageLoad || initialPath === pagePath) { - pagePath = pagePath.replace(/\.js$/, '.lean.js') + if (isInitialPageLoad || initialPath === pageFilePath) { + pageFilePath = pageFilePath.replace(/\.js$/, '.lean.js') } if (inBrowser) { isInitialPageLoad = false // in browser: native dynamic import - return import(/*@vite-ignore*/ pagePath).then((page) => { - if (page.__pageData) { - pageDataRef.value = readonly(JSON.parse(page.__pageData)) - } - return page.default - }) + return import(/*@vite-ignore*/ pageFilePath) } else { // SSR, sync require - const page = require(pagePath) - pageDataRef.value = JSON.parse(page.__pageData) - return page.default + return require(pageFilePath) } }, NotFound) + // update route.data on HMR updates of active page + if (import.meta.hot) { + // hot reload pageData + import.meta.hot!.on('vitepress:pageData', (payload) => { + if ( + payload.path.replace(/(\bindex)?\.md$/, '') === + location.pathname.replace(/(\bindex)?\.html$/, '') + ) { + router.route.data = payload.pageData + } + }) + } + const app = process.env.NODE_ENV === 'production' ? createSSRApp(Theme.Layout) : createClientApp(Theme.Layout) app.provide(RouterSymbol, router) - app.provide(pageDataSymbol, pageDataRef) app.component('Content', Content) app.component( @@ -80,7 +68,7 @@ export function createApp() { if (inBrowser) { // dynamically update head tags - useUpdateHead(pageDataRef, siteDataByRouteRef) + useUpdateHead(router.route, siteDataByRouteRef) } Object.defineProperties(app.config.globalProperties, { @@ -96,7 +84,7 @@ export function createApp() { }, $page: { get() { - return pageDataRef.value + return router.route.data } }, $theme: { diff --git a/src/client/app/router.ts b/src/client/app/router.ts index 097aa98d..9212efb7 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -1,9 +1,11 @@ -import { reactive, inject, markRaw, nextTick } from 'vue' +import { reactive, inject, markRaw, nextTick, readonly } from 'vue' import type { Component, InjectionKey } from 'vue' +import { PageData } from '../../../types/shared' export interface Route { path: string - contentComponent: Component | null + data: PageData + component: Component | null } export interface Router { @@ -19,19 +21,26 @@ const fakeHost = `http://a.com` const getDefaultRoute = (): Route => ({ path: '/', - contentComponent: null + component: null, + // this will be set upon initial page load, which is before + // the app is mounted, so it's guaranteed to be avaiable in + // components + data: null as any }) +interface PageModule { + __pageData: string + default: Component +} + export function createRouter( - loadComponent: (route: Route) => Component | Promise, + loadPageModule: (path: string) => PageModule | Promise, fallbackComponent?: Component ): Router { - // TODO: the cast shouldn't be necessary - const route = reactive(getDefaultRoute()) as Route + const route = reactive(getDefaultRoute()) const inBrowser = typeof window !== 'undefined' - function go(href?: string) { - href = href || (inBrowser ? location.href : '/') + function go(href: string = inBrowser ? location.href : '/') { // ensure correct deep link so page refresh lands on correct files. const url = new URL(href, fakeHost) if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) { @@ -46,21 +55,30 @@ export function createRouter( return loadPage(href) } + let latestPendingPath: string | null = null + async function loadPage(href: string, scrollPosition = 0) { const targetLoc = new URL(href, fakeHost) - const pendingPath = (route.path = targetLoc.pathname) + const pendingPath = (latestPendingPath = targetLoc.pathname) try { - let comp = loadComponent(route) + let page = loadPageModule(pendingPath) // only await if it returns a Promise - this allows sync resolution // on initial render in SSR. - if ('then' in comp && typeof comp.then === 'function') { - comp = await comp + if ('then' in page && typeof page.then === 'function') { + page = await page } - if (route.path === pendingPath) { + if (latestPendingPath === pendingPath) { + latestPendingPath = null + + const { default: comp, __pageData } = page as PageModule if (!comp) { throw new Error(`Invalid route component: ${comp}`) } - route.contentComponent = markRaw(comp) + + route.path = pendingPath + route.component = markRaw(comp) + route.data = readonly(JSON.parse(__pageData)) as PageData + if (inBrowser) { nextTick(() => { if (targetLoc.hash && !scrollPosition) { @@ -80,10 +98,10 @@ export function createRouter( if (!err.message.match(/fetch/)) { console.error(err) } - if (route.path === pendingPath) { - route.contentComponent = fallbackComponent - ? markRaw(fallbackComponent) - : null + if (latestPendingPath === pendingPath) { + latestPendingPath = null + route.path = pendingPath + route.component = fallbackComponent ? markRaw(fallbackComponent) : null } } } @@ -152,7 +170,8 @@ export function useRoute(): Route { } function scrollTo(el: HTMLElement, hash: string, smooth = false) { - const pageOffset = document.getElementById('app')!.offsetTop + const pageOffset = (document.querySelector('.navbar') as HTMLElement) + .offsetHeight const target = el.classList.contains('.header-anchor') ? el : document.querySelector(decodeURIComponent(hash)) diff --git a/src/client/theme-default/Layout.vue b/src/client/theme-default/Layout.vue index 366a9055..2175b7ab 100644 --- a/src/client/theme-default/Layout.vue +++ b/src/client/theme-default/Layout.vue @@ -54,7 +54,7 @@ import Home from './components/Home.vue' import ToggleSideBarButton from './components/ToggleSideBarButton.vue' import SideBar from './components/SideBar.vue' import Page from './components/Page.vue' -import { useRoute, usePageData, useSiteData, useSiteDataByRoute } from 'vitepress' +import { useRoute, useSiteData, useSiteDataByRoute } from 'vitepress' export default { components: { @@ -67,16 +67,15 @@ export default { setup() { const route = useRoute() - const pageData = usePageData() const siteData = useSiteData() const siteRouteData = useSiteDataByRoute() const openSideBar = ref(false) - const enableHome = computed(() => !!pageData.value.frontmatter.home) + const enableHome = computed(() => !!route.data.frontmatter.home) const showNavbar = computed(() => { const { themeConfig } = siteRouteData.value - const { frontmatter } = pageData.value + const { frontmatter } = route.data if ( frontmatter.navbar === false || themeConfig.navbar === false) { @@ -91,7 +90,7 @@ export default { }) const showSidebar = computed(() => { - const { frontmatter } = pageData.value + const { frontmatter } = route.data const { themeConfig } = siteRouteData.value return ( !frontmatter.home diff --git a/src/client/theme-default/components/Home.vue b/src/client/theme-default/components/Home.vue index 6c7b77c2..6bb07d34 100644 --- a/src/client/theme-default/components/Home.vue +++ b/src/client/theme-default/components/Home.vue @@ -57,7 +57,7 @@ import { defineComponent, computed } from 'vue' import NavBarLink from './NavBarLink.vue' import { withBase } from '../utils' -import { usePageData, useSiteData } from 'vitepress' +import { useRoute, useSiteData } from 'vitepress' export default defineComponent({ components: { @@ -65,10 +65,10 @@ export default defineComponent({ }, setup() { - const pageData = usePageData() + const route = useRoute() const siteData = useSiteData() - const data = computed(() => pageData.value.frontmatter) + const data = computed(() => route.data.frontmatter) const actionLink = computed(() => ({ link: data.value.actionLink, text: data.value.actionText diff --git a/src/client/theme-default/components/NextAndPrevLinks.ts b/src/client/theme-default/components/NextAndPrevLinks.ts index d87ce99c..09572b88 100644 --- a/src/client/theme-default/components/NextAndPrevLinks.ts +++ b/src/client/theme-default/components/NextAndPrevLinks.ts @@ -1,13 +1,14 @@ import { computed } from 'vue' -import { usePageData, useSiteData } from 'vitepress' +import { useRoute, useSiteData } from 'vitepress' import { DefaultTheme } from '../config' export default { setup() { - const pageData = usePageData() + const route = useRoute() // TODO: could this be useSiteData or is the siteData // resolved and has a different structure? const siteData = useSiteData() + const resolveLink = (targetLink: string) => { let target: DefaultTheme.SideBarLink | undefined Object.keys(siteData.value.themeConfig.sidebar).some((k) => { @@ -24,27 +25,33 @@ export default { }) return target } + const next = computed(() => { - if (pageData.value.frontmatter.next === false) { + const pageData = route.data + if (pageData.frontmatter.next === false) { return undefined } - if (typeof pageData.value.frontmatter.next === 'string') { - return resolveLink(pageData.value.frontmatter.next) + if (typeof pageData.frontmatter.next === 'string') { + return resolveLink(pageData.frontmatter.next) } - return pageData.value.next + return pageData.next }) + const prev = computed(() => { - if (pageData.value.frontmatter.prev === false) { + const pageData = route.data + if (pageData.frontmatter.prev === false) { return undefined } - if (typeof pageData.value.frontmatter.prev === 'string') { - return resolveLink(pageData.value.frontmatter.prev) + if (typeof pageData.frontmatter.prev === 'string') { + return resolveLink(pageData.frontmatter.prev) } - return pageData.value.prev + return pageData.prev }) + const hasLinks = computed(() => { return !!next || !!prev }) + return { next, prev, diff --git a/src/client/theme-default/components/PageEdit.ts b/src/client/theme-default/components/PageEdit.ts index 25b8a386..10f8ecdb 100644 --- a/src/client/theme-default/components/PageEdit.ts +++ b/src/client/theme-default/components/PageEdit.ts @@ -1,7 +1,7 @@ import { computed } from 'vue' import OutboundLink from './icons/OutboundLink.vue' import { endingSlashRE, isExternal } from '../utils' -import { usePageData, useSiteData } from 'vitepress' +import { useRoute, useSiteData } from 'vitepress' import { DefaultTheme } from '../config' function createEditLink( @@ -42,14 +42,15 @@ export default { }, setup() { - const pageData = usePageData() + const route = useRoute() const siteData = useSiteData() const editLink = computed(() => { + const pageData = route.data const showEditLink: boolean | undefined = - pageData.value.frontmatter.editLink == null + pageData.frontmatter.editLink == null ? siteData.value.themeConfig.editLinks - : pageData.value.frontmatter.editLink + : pageData.frontmatter.editLink const { repo, docsDir = '', @@ -57,7 +58,7 @@ export default { docsRepo = repo } = siteData.value.themeConfig - const { relativePath } = pageData.value + const { relativePath } = pageData if (showEditLink && relativePath && repo) { return createEditLink( repo, @@ -69,6 +70,7 @@ export default { } return null }) + const editLinkText = computed( () => siteData.value.themeConfig.editLinkText || 'Edit this page' ) diff --git a/src/client/theme-default/components/SideBar.ts b/src/client/theme-default/components/SideBar.ts index 9b3b52d3..69eb7807 100644 --- a/src/client/theme-default/components/SideBar.ts +++ b/src/client/theme-default/components/SideBar.ts @@ -1,37 +1,21 @@ -import { - usePageData, - useRoute, - useSiteDataByRoute, - useSiteData -} from 'vitepress' -import { computed, h, FunctionalComponent, VNode } from 'vue' +import { useRoute, useSiteDataByRoute } from 'vitepress' +import { computed } from 'vue' import { Header } from '../../../../types/shared' -import { isActive, joinUrl, getPathDirName } from '../utils' +import { getPathDirName } from '../utils' import { DefaultTheme } from '../config' import { useActiveSidebarLinks } from '../composables/activeSidebarLink' import NavBarLinks from './NavBarLinks.vue' +import { SideBarItem } from './SideBarItem' -const SideBarItem: FunctionalComponent<{ - item: ResolvedSidebarItem -}> = (props) => { - const { - item: { link: relLink, text, children } - } = props - - const route = useRoute() - const pageData = usePageData() - const siteData = useSiteData() - - const link = resolveLink(siteData.value.base, relLink || '') - const active = isActive(route, link) - const headers = pageData.value.headers - - return h('li', { class: 'sidebar-item' }, [ - createLink(active, text, link), - createChildren(active, children, headers) - ]) +export interface ResolvedSidebarItem { + text: string + link?: string + isGroup?: boolean + children?: ResolvedSidebarItem[] } +type ResolvedSidebar = ResolvedSidebarItem[] + export default { components: { NavBarLinks, @@ -39,9 +23,8 @@ export default { }, setup() { - const pageData = usePageData() - const siteData = useSiteDataByRoute() const route = useRoute() + const siteData = useSiteDataByRoute() useActiveSidebarLinks() @@ -50,7 +33,7 @@ export default { const { headers, frontmatter: { sidebar, sidebarDepth = 2 } - } = pageData.value + } = route.data if (sidebar === 'auto') { // auto, render headers of current page @@ -84,19 +67,6 @@ export default { } } -interface HeaderWithChildren extends Header { - children?: Header[] -} - -type ResolvedSidebar = ResolvedSidebarItem[] - -interface ResolvedSidebarItem { - text: string - link?: string - isGroup?: boolean - children?: ResolvedSidebarItem[] -} - function resolveAutoSidebar(headers: Header[], depth: number): ResolvedSidebar { const ret: ResolvedSidebar = [] @@ -151,68 +121,3 @@ function resolveMultiSidebar( return [] } - -function resolveLink(base: string, path: string): string | undefined { - return path - ? // keep relative hash to the same page - path.startsWith('#') - ? path - : joinUrl(base, path) - : undefined -} - -function createLink(active: boolean, text: string, link?: string): VNode { - const tag = link ? 'a' : 'p' - - const component = { - class: { 'sidebar-link': true, active }, - href: link - } - - return h(tag, component, text) -} - -function createChildren( - active: boolean, - children?: ResolvedSidebarItem[], - headers?: Header[] -): VNode | null { - if (children && children.length > 0) { - return h( - 'ul', - { class: 'sidebar-items' }, - children.map((c) => { - return h(SideBarItem, { item: c }) - }) - ) - } - - return active && headers - ? createChildren(false, resolveHeaders(headers)) - : null -} - -function resolveHeaders(headers: Header[]): ResolvedSidebarItem[] { - return mapHeaders(groupHeaders(headers)) -} - -function groupHeaders(headers: Header[]): HeaderWithChildren[] { - headers = headers.map((h) => Object.assign({}, h)) - let lastH2: HeaderWithChildren - headers.forEach((h) => { - if (h.level === 2) { - lastH2 = h - } else if (lastH2) { - ;(lastH2.children || (lastH2.children = [])).push(h) - } - }) - return headers.filter((h) => h.level === 2) -} - -function mapHeaders(headers: HeaderWithChildren[]): ResolvedSidebarItem[] { - return headers.map((header) => ({ - text: header.title, - link: `#${header.slug}`, - children: header.children ? mapHeaders(header.children) : undefined - })) -} diff --git a/src/client/theme-default/components/SideBarItem.ts b/src/client/theme-default/components/SideBarItem.ts new file mode 100644 index 00000000..0d213a30 --- /dev/null +++ b/src/client/theme-default/components/SideBarItem.ts @@ -0,0 +1,91 @@ +import { useRoute, useSiteData } from 'vitepress' +import { FunctionalComponent, h, VNode } from 'vue' +import { Header } from '../../../../types/shared' +import { joinUrl, isActive } from '../utils' +import { ResolvedSidebarItem } from './SideBar' + +interface HeaderWithChildren extends Header { + children?: Header[] +} + +export const SideBarItem: FunctionalComponent<{ + item: ResolvedSidebarItem +}> = (props) => { + const { + item: { link: relLink, text, children } + } = props + + const route = useRoute() + const siteData = useSiteData() + + const link = resolveLink(siteData.value.base, relLink || '') + const active = isActive(route, link) + const headers = route.data.headers + const childItems = createChildren(active, children, headers) + + return h('li', { class: 'sidebar-item' }, [ + h( + link ? 'a' : 'p', + { + class: { 'sidebar-link': true, active }, + href: link + }, + text + ), + childItems + ]) +} + +function resolveLink(base: string, path: string): string | undefined { + return path + ? // keep relative hash to the same page + path.startsWith('#') + ? path + : joinUrl(base, path) + : undefined +} + +function createChildren( + active: boolean, + children?: ResolvedSidebarItem[], + headers?: Header[] +): VNode | null { + if (children && children.length > 0) { + return h( + 'ul', + { class: 'sidebar-items' }, + children.map((c) => { + return h(SideBarItem, { item: c }) + }) + ) + } + + return active && headers + ? createChildren(false, resolveHeaders(headers)) + : null +} + +function resolveHeaders(headers: Header[]): ResolvedSidebarItem[] { + return mapHeaders(groupHeaders(headers)) +} + +function groupHeaders(headers: Header[]): HeaderWithChildren[] { + headers = headers.map((h) => Object.assign({}, h)) + let lastH2: HeaderWithChildren + headers.forEach((h) => { + if (h.level === 2) { + lastH2 = h + } else if (lastH2) { + ;(lastH2.children || (lastH2.children = [])).push(h) + } + }) + return headers.filter((h) => h.level === 2) +} + +function mapHeaders(headers: HeaderWithChildren[]): ResolvedSidebarItem[] { + return headers.map((header) => ({ + text: header.title, + link: `#${header.slug}`, + children: header.children ? mapHeaders(header.children) : undefined + })) +} diff --git a/src/client/theme-default/composables/activeSidebarLink.ts b/src/client/theme-default/composables/activeSidebarLink.ts index a5609134..2af89ed3 100644 --- a/src/client/theme-default/composables/activeSidebarLink.ts +++ b/src/client/theme-default/composables/activeSidebarLink.ts @@ -36,7 +36,8 @@ export function useActiveSidebarLinks() { sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash) ) as HTMLAnchorElement[] - const pageOffset = document.getElementById('app')!.offsetTop + const pageOffset = (document.querySelector('.navbar') as HTMLElement) + .offsetHeight const scrollTop = window.scrollY const getAnchorTop = (anchor: HTMLAnchorElement): number => @@ -50,6 +51,8 @@ export function useActiveSidebarLinks() { (scrollTop >= getAnchorTop(anchor) && (!nextAnchor || scrollTop < getAnchorTop(nextAnchor))) + // TODO: fix case when at page bottom + if (isActive) { const targetHash = decode(anchor.hash) history.replaceState(null, document.title, targetHash) diff --git a/src/client/theme-default/styles/layout.css b/src/client/theme-default/styles/layout.css index ff76131d..040ba2de 100644 --- a/src/client/theme-default/styles/layout.css +++ b/src/client/theme-default/styles/layout.css @@ -95,8 +95,8 @@ body { } .theme main { - margin-top: var(--header-height); - margin-left: var(--sidebar-width); + padding-top: var(--header-height); + padding-left: var(--sidebar-width); } @media screen and (min-width: 720px) { diff --git a/src/client/theme-default/utils.ts b/src/client/theme-default/utils.ts index d0b97b95..0e8ebc67 100644 --- a/src/client/theme-default/utils.ts +++ b/src/client/theme-default/utils.ts @@ -1,7 +1,7 @@ import { useSiteData, Route } from 'vitepress' export const hashRE = /#.*$/ -export const extRE = /\.(md|html)$/ +export const extRE = /(index)?\.(md|html)$/ export const endingSlashRE = /\/$/ export const outboundRE = /^[a-z]+:/i diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json index b004aa66..e23f7ed1 100644 --- a/src/client/tsconfig.json +++ b/src/client/tsconfig.json @@ -2,22 +2,20 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "baseUrl": ".", - "outDir": "../../dist", + "outDir": "../../dist/client", "module": "esnext", "lib": ["ESNext", "DOM"], "types": ["vite"], "paths": { "/@app/*": ["app/*"], "/@theme/*": ["theme-default/*"], - "/@shared/*": ["../shared/*"], + "/@shared/*": ["shared/*"], + "/@types/*": ["../../types/*"], "vitepress": ["app/exports.ts"] } }, "include": [ - ".", + ".", "../../types/shared.d.ts", - ], - "exclude": [ - "../shared" ] } diff --git a/src/node/config.ts b/src/node/config.ts index d90121c7..0c045397 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -6,7 +6,7 @@ import { createResolver, APP_PATH } from './resolver' import { Resolver } from 'vite' import { SiteData, HeadConfig, LocaleConfig } from '../../types/shared' import { MarkdownOptions } from './markdown/markdown' -export { resolveSiteDataByRoute } from '../shared/config' +export { resolveSiteDataByRoute } from './shared/config' const debug = require('debug')('vitepress:config') diff --git a/src/node/tsconfig.json b/src/node/tsconfig.json index e67dbbc7..4d912460 100644 --- a/src/node/tsconfig.json +++ b/src/node/tsconfig.json @@ -1,10 +1,17 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "../../dist", + "baseUrl": ".", + "outDir": "../../dist/node", "module": "commonjs", "lib": ["ESNext", "DOM"], - "sourceMap": true + "sourceMap": true, + "paths": { + "/@types/*": ["../../types/*"] + } }, - "include": [".", "../shared", "../../types/shared.d.ts"] + "include": [ + ".", + "../../types/shared.d.ts" + ] } diff --git a/src/shared/config.ts b/src/shared/config.ts index 2946c23d..378829e1 100644 --- a/src/shared/config.ts +++ b/src/shared/config.ts @@ -1,4 +1,4 @@ -import { SiteData } from '../../types/shared' +import { SiteData } from '/@types/shared' const inBrowser = typeof window !== 'undefined' diff --git a/src/shared/tsconfig.json b/src/shared/tsconfig.json index 34954d27..04fa6565 100644 --- a/src/shared/tsconfig.json +++ b/src/shared/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "baseUrl": ".", - "outDir": "../../dist/client/shared", - "module": "esnext", - "lib": ["ESNext", "DOM"], + "paths": { + "/@types/*": ["../../types/*"] + } }, "include": [ - ".", - "../../types/shared.d.ts", + ".", + "../../types/shared.d.ts" ] }