diff --git a/src/node/plugins/dynamicRoutesPlugin.ts b/src/node/plugins/dynamicRoutesPlugin.ts index 3d9730c2..1b7f943d 100644 --- a/src/node/plugins/dynamicRoutesPlugin.ts +++ b/src/node/plugins/dynamicRoutesPlugin.ts @@ -1,4 +1,4 @@ -import { loadConfigFromFile, type Plugin } from 'vite' +import { loadConfigFromFile, type Plugin, type ViteDevServer } from 'vite' import fs from 'fs-extra' import c from 'picocolors' import path from 'path' @@ -32,9 +32,67 @@ type ResolvedRouteConfig = UserRouteConfig & { } export const dynamicRoutesPlugin = async ( - routes: string[] + initialRoutes: string[] ): Promise => { + let server: ViteDevServer + let [resolvedRoutes, routeFileToModulesMap] = await resolveRoutes( + initialRoutes + ) + + return { + name: 'vitepress:dynamic-routes', + + configureServer(_server) { + server = _server + }, + + load(id) { + const matched = resolvedRoutes.find((r) => r.path === id) + if (matched) { + const { route, params, content } = matched + const routeFile = path.resolve(route) + routeFileToModulesMap[routeFile].push(id) + + let baseContent = fs.readFileSync(routeFile, 'utf-8') + + // inject raw content + // this is intended for integration with CMS + // we use a speical injection syntax so the content is rendered as + // static local content instead of included as runtime data. + if (content) { + baseContent = baseContent.replace(//, content) + } + + // params are injected with special markers and extracted as part of + // __pageData in ../markdownTovue.ts + return `__VP_PARAMS_START${JSON.stringify( + params + )}__VP_PARAMS_END__${baseContent}` + } + }, + + async handleHotUpdate(ctx) { + const mods = routeFileToModulesMap[ctx.file] + if (mods) { + // path loader module updated, reset loaded routes + // TODO: make this more efficient by only reloading the invalidated route + // TODO: invlidate modules for paths that are no longer present + if (/\.paths\.[jt]s$/.test(ctx.file)) { + ;[resolvedRoutes, routeFileToModulesMap] = await resolveRoutes( + initialRoutes + ) + } + for (const id of mods) { + ctx.modules.push(server.moduleGraph.getModuleById(id)!) + } + } + } + } +} + +async function resolveRoutes(routes: string[]) { const pendingResolveRoutes: Promise[] = [] + const routeFileToModulesMap: Record = {} for (const route of routes) { // locate corresponding route paths file @@ -66,15 +124,20 @@ export const dynamicRoutesPlugin = async ( } if (mod) { + // route md file and route paths loader file point to the same array + routeFileToModulesMap[mod.path] = routeFileToModulesMap[ + path.resolve(route) + ] = [] + const resolveRoute = async (): Promise => { const loader = mod.config.paths const paths = await (typeof loader === 'function' ? loader() : loader) return paths.map((userConfig) => { return { - route, path: '/' + route.replace(/\[(\w+)\]/g, (_, key) => userConfig.params[key]), + route, ...userConfig } }) @@ -83,28 +146,8 @@ export const dynamicRoutesPlugin = async ( } } - const resolvedRoutes = (await Promise.all(pendingResolveRoutes)).flat() - - return { - name: 'vitepress:dynamic-routes', - load(id) { - const matched = resolvedRoutes.find((r) => r.path === id) - if (matched) { - const { route, params, content } = matched - let baseContent = fs.readFileSync(route, 'utf-8') - - // inject raw content at build time - if (content) { - baseContent = baseContent.replace(//, content) - } - - // params are injected with special markers and extracted as part of - // __pageData in ../markdownTovue.ts - return `__VP_PARAMS_START${JSON.stringify( - params - )}__VP_PARAMS_END__${baseContent}` - } - } - // TODO HMR - } + return [ + (await Promise.all(pendingResolveRoutes)).flat(), + routeFileToModulesMap + ] as const }