diff --git a/lib/app/composables/pageData.js b/lib/app/composables/pageData.js index 2bbe3925..c062f904 100644 --- a/lib/app/composables/pageData.js +++ b/lib/app/composables/pageData.js @@ -1,5 +1,3 @@ export function usePageData() { - return { - msg: 'this is page' - } + } diff --git a/lib/app/composables/siteData.js b/lib/app/composables/siteData.js index 8eb788ce..63048e53 100644 --- a/lib/app/composables/siteData.js +++ b/lib/app/composables/siteData.js @@ -1,5 +1,21 @@ +import serialized from '@siteData' +import { hot } from '@hmr' +import { shallowRef, readonly } from 'vue' + +/** + * @param {string} data + */ +const parse = (data) => + __DEV__ ? readonly(JSON.parse(data)) : JSON.parse(data) + +// site data +export const siteDataRef = shallowRef(parse(serialized)) + export function useSiteData() { - return { - msg: 'this is site' - } + return siteDataRef } + +// hmr +hot.accept('/@siteData', (m) => { + siteDataRef.value = parse(m.default) +}) diff --git a/lib/app/index-dev.html b/lib/app/index-dev.html index 69e2efbb..80e530c2 100644 --- a/lib/app/index-dev.html +++ b/lib/app/index-dev.html @@ -1,2 +1,3 @@
+ diff --git a/lib/app/index.js b/lib/app/index.js index 8b466b7f..2b13a0c4 100644 --- a/lib/app/index.js +++ b/lib/app/index.js @@ -1,7 +1,7 @@ import { createApp, h } from 'vue' import { Content } from './components/Content' import { useRouter } from './composables/router' -import { useSiteData } from './composables/siteData' +import { siteDataRef } from './composables/siteData' import Theme from '/@theme/index' const App = { @@ -18,7 +18,7 @@ const App = { const app = createApp(App) Object.defineProperty(app.config.globalProperties, '$site', { - get: useSiteData + get: () => siteDataRef.value }) app.component('Content', Content) diff --git a/lib/shim.d.ts b/lib/shim.d.ts index 1ae3df6b..d31babf0 100644 --- a/lib/shim.d.ts +++ b/lib/shim.d.ts @@ -1,5 +1,18 @@ +declare const __DEV__: boolean + declare module "*.vue" { import { ComponentOptions } from 'vue' const comp: ComponentOptions export default comp } + +declare module "@siteData" { + const data: string + export default data +} + +declare module "@hmr" { + export declare const hot: { + accept(path: string, cb: (module: any) => void) + } +} diff --git a/lib/theme-default/Layout.vue b/lib/theme-default/Layout.vue index 54dd80e1..6d3124cb 100644 --- a/lib/theme-default/Layout.vue +++ b/lib/theme-default/Layout.vue @@ -13,10 +13,12 @@ import { useSiteData, usePageData } from 'vitepress' export default { - data: () => ({ - site: useSiteData(), - page: usePageData() - }) + setup() { + return { + site: useSiteData(), + page: usePageData() + } + } } diff --git a/src/resolveConfig.ts b/src/resolveConfig.ts index 975be2da..a43f4c43 100644 --- a/src/resolveConfig.ts +++ b/src/resolveConfig.ts @@ -5,7 +5,7 @@ import { Resolver } from 'vite' const debug = require('debug')('vitepress:config') -export interface UserConfig> { +export interface UserConfig { base?: string title?: string description?: string @@ -16,7 +16,7 @@ export interface UserConfig> { // TODO locales support etc. } -export interface SiteData> { +export interface SiteData { title: string description: string base: string @@ -28,22 +28,49 @@ export interface PageData { path: string } -export interface ResolvedConfig> { +export interface ResolvedConfig { site: SiteData - root: string // project root on file system themePath: string resolver: Resolver } +export const getConfigPath = (root: string) => + path.join(root, '.vitepress/config.js') + export async function resolveConfig(root: string): Promise { + const site = await resolveSiteData(root) + + // resolve theme path + const userThemePath = path.join(root, '.vitepress/theme') + let themePath: string + try { + await fs.stat(userThemePath) + themePath = userThemePath + } catch (e) { + themePath = path.join(__dirname, '../lib/theme-default') + } + + const config: ResolvedConfig = { + site, + themePath, + resolver: createResolver(themePath) + } + + return config +} + +export async function resolveSiteData(root: string): Promise { // 1. load user config - const configPath = path.join(root, '.vitepress/config.js') + const configPath = getConfigPath(root) let hasUserConfig = false try { await fs.stat(configPath) hasUserConfig = true debug(`loading user config at ${configPath}`) } catch (e) {} + + // always delete cache first before loading config + delete require.cache[configPath] const userConfig: UserConfig = hasUserConfig ? require(configPath) : {} // 2. TODO scan pages data @@ -57,22 +84,5 @@ export async function resolveConfig(root: string): Promise { pages: [] } - // 4. resolve theme path - const userThemePath = path.join(root, '.vitepress/theme') - let themePath: string - try { - await fs.stat(userThemePath) - themePath = userThemePath - } catch (e) { - themePath = path.join(__dirname, '../lib/theme-default') - } - - const config: ResolvedConfig = { - root, - site, - themePath, - resolver: createResolver(themePath) - } - - return config + return site } diff --git a/src/server.ts b/src/server.ts index a8682c1e..0d4e530c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,17 +5,25 @@ import { Plugin, ServerConfig } from 'vite' -import { resolveConfig, ResolvedConfig } from './resolveConfig' +import { + resolveConfig, + ResolvedConfig, + getConfigPath, + resolveSiteData +} from './resolveConfig' import { createMarkdownToVueRenderFn } from './markdown/markdownToVue' -import { APP_PATH } from './utils/pathResolver' +import { APP_PATH, SITE_DATA_REQUEST_PATH } from './utils/pathResolver' const debug = require('debug')('vitepress:serve') const debugHmr = require('debug')('vitepress:hmr') -function createVitePressPlugin({ - themePath, - resolver: vitepressResolver -}: ResolvedConfig): Plugin { +function createVitePressPlugin(config: ResolvedConfig): Plugin { + const { + themePath, + site: initialSiteData, + resolver: vitepressResolver + } = config + return ({ app, root, watcher, resolver }) => { const markdownToVue = createMarkdownToVueRenderFn(root) @@ -39,8 +47,38 @@ function createVitePressPlugin({ } }) + // hot reload handling for siteData + // the data is stringified twice so it is sent to the client as a string + // it is then parsed on the client via JSON.parse() which is faster than + // parsing the object literal as JavaScript. + let siteData = initialSiteData + let stringifiedData = JSON.stringify(JSON.stringify(initialSiteData)) + const configPath = getConfigPath(root) + watcher.add(configPath) + watcher.on('change', async (file) => { + if (file === configPath) { + const newData = await resolveSiteData(root) + stringifiedData = JSON.stringify(JSON.stringify(newData)) + if (newData.base !== siteData.base) { + console.warn( + `[vitepress]: config.base has changed. Please restart the dev server.` + ) + } + siteData = newData + watcher.handleJSReload(SITE_DATA_REQUEST_PATH) + } + }) + // inject Koa middleware app.use(async (ctx, next) => { + // serve siteData (which is a virtual file) + if (ctx.path === SITE_DATA_REQUEST_PATH) { + ctx.type = 'js' + ctx.body = `export default ${stringifiedData}` + debug(ctx.url) + return + } + // handle .md -> vue transforms if (ctx.path.endsWith('.md')) { const file = resolver.requestToFile(ctx.path) diff --git a/src/utils/pathResolver.ts b/src/utils/pathResolver.ts index 7b4ef089..849be1ce 100644 --- a/src/utils/pathResolver.ts +++ b/src/utils/pathResolver.ts @@ -4,6 +4,9 @@ import { Resolver } from "vite" // built ts files are placed into /dist export const APP_PATH = path.join(__dirname, '../../lib/app') +// speical virtual file +export const SITE_DATA_REQUEST_PATH = '/@siteData' + // this is a path resolver that is passed to vite // so that we can resolve custom requests that start with /@app or /@theme // we also need to map file paths back to their public served paths so that @@ -25,6 +28,9 @@ export function createResolver(themePath: string): Resolver { if (filePath.startsWith(themePath)) { return `/@theme/${path.relative(themePath, filePath)}` } + if (filePath === SITE_DATA_REQUEST_PATH) { + return SITE_DATA_REQUEST_PATH + } }, idToRequest(id) { if (id === 'vitepress') {