diff --git a/src/node/config.ts b/src/node/config.ts index a962a25e..912f64c4 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -3,7 +3,6 @@ import _debug from 'debug' import fg from 'fast-glob' import fs from 'fs-extra' import path from 'path' -import { compile, match } from 'path-to-regexp' import c from 'picocolors' import { createLogger, @@ -17,9 +16,10 @@ import { DEFAULT_THEME_PATH } from './alias' import type { MarkdownOptions } from './markdown/markdown' import { dynamicRouteRE, - resolveRoutes, + resolveDynamicRoutes, type ResolvedRouteConfig } from './plugins/dynamicRoutesPlugin' +import { resolveRewrites } from './plugins/rewritesPlugin' import { APPEARANCE_KEY, type Awaitable, @@ -183,12 +183,16 @@ export interface SiteConfig cacheDir: string tempDir: string pages: string[] - dynamicRoutes: readonly [ResolvedRouteConfig[], Record] + dynamicRoutes: { + routes: ResolvedRouteConfig[] + fileToModulesMap: Record + } rewrites: { map: Record inv: Record } logger: Logger + userConfig: UserConfig } const resolve = (root: string, file: string) => @@ -242,39 +246,10 @@ export async function resolveConfig( ? userThemeDir : DEFAULT_THEME_PATH - // Important: fast-glob doesn't guarantee order of the returned files. - // We must sort the pages so the input list to rollup is stable across - // builds - otherwise different input order could result in different exports - // order in shared chunks which in turns invalidates the hash of every chunk! - // JavaScript built-in sort() is mandated to be stable as of ES2019 and - // supported in Node 12+, which is required by Vite. - const allMarkdownFiles = ( - await fg(['**.md'], { - cwd: srcDir, - ignore: ['**/node_modules', ...(userConfig.srcExclude || [])] - }) - ).sort() - - const pages = allMarkdownFiles.filter((p) => !dynamicRouteRE.test(p)) - const dynamicRouteFiles = allMarkdownFiles.filter((p) => - dynamicRouteRE.test(p) + const { pages, dynamicRoutes, rewrites } = await resolvePages( + srcDir, + userConfig ) - const dynamicRoutes = await resolveRoutes(dynamicRouteFiles) - pages.push(...dynamicRoutes[0].map((r) => r.path)) - - const rewriteEntries = Object.entries(userConfig.rewrites || {}) - const rewrites = rewriteEntries.length - ? Object.fromEntries( - pages - .map((src) => { - for (const [from, to] of rewriteEntries) { - const dest = rewrite(src, from, to) - if (dest) return [src, dest] - } - }) - .filter((e) => e != null) as [string, string][] - ) - : {} const config: SiteConfig = { root, @@ -305,10 +280,8 @@ export async function resolveConfig( transformHead: userConfig.transformHead, transformHtml: userConfig.transformHtml, transformPageData: userConfig.transformPageData, - rewrites: { - map: rewrites, - inv: Object.fromEntries(Object.entries(rewrites).map((a) => a.reverse())) - } + rewrites, + userConfig } return config @@ -444,10 +417,32 @@ function resolveSiteDataHead(userConfig?: UserConfig): HeadConfig[] { return head } -function rewrite(src: string, from: string, to: string) { - const urlMatch = match(from) - const res = urlMatch(src) - if (!res) return false - const toPath = compile(to) - return toPath(res.params) +export async function resolvePages(srcDir: string, userConfig: UserConfig) { + // Important: fast-glob doesn't guarantee order of the returned files. + // We must sort the pages so the input list to rollup is stable across + // builds - otherwise different input order could result in different exports + // order in shared chunks which in turns invalidates the hash of every chunk! + // JavaScript built-in sort() is mandated to be stable as of ES2019 and + // supported in Node 12+, which is required by Vite. + const allMarkdownFiles = ( + await fg(['**.md'], { + cwd: srcDir, + ignore: ['**/node_modules', ...(userConfig.srcExclude || [])] + }) + ).sort() + + const pages = allMarkdownFiles.filter((p) => !dynamicRouteRE.test(p)) + const dynamicRouteFiles = allMarkdownFiles.filter((p) => + dynamicRouteRE.test(p) + ) + const dynamicRoutes = await resolveDynamicRoutes(dynamicRouteFiles) + pages.push(...dynamicRoutes.routes.map((r) => r.path)) + + const rewrites = resolveRewrites(pages, userConfig.rewrites) + + return { + pages, + dynamicRoutes, + rewrites + } } diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 03a61bbf..9e1d4024 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -15,12 +15,13 @@ import { resolveAliases, SITE_DATA_REQUEST_PATH } from './alias' -import type { SiteConfig } from './config' +import { resolvePages, type SiteConfig } from './config' import { clearCache, createMarkdownToVueRenderFn } from './markdownToVue' import type { PageDataPayload } from './shared' import { staticDataPlugin } from './plugins/staticDataPlugin' import { webFontsPlugin } from './plugins/webFontsPlugin' import { dynamicRoutesPlugin } from './plugins/dynamicRoutesPlugin' +import { rewritesPlugin } from './plugins/rewritesPlugin' declare module 'vite' { interface UserConfig { @@ -70,8 +71,7 @@ export async function createVitePressPlugin( pages, ignoreDeadLinks, lastUpdated, - cleanUrls, - rewrites + cleanUrls } = siteConfig let markdownToVue: Awaited> @@ -198,15 +198,16 @@ export async function createVitePressPlugin( configDeps.forEach((file) => server.watcher.add(file)) } - server.middlewares.use((req, res, next) => { - if (req.url) { - const page = req.url.replace(/[?#].*$/, '').slice(site.base.length) - if (rewrites.inv[page]) { - req.url = req.url.replace(page, rewrites.inv[page]!) - } + // update pages, dynamicRoutes and rewrites on md file add / deletion + const onFileAddDelete = async (file: string) => { + if (file.endsWith('.md')) { + Object.assign( + siteConfig, + await resolvePages(siteConfig.srcDir, siteConfig.userConfig) + ) } - next() - }) + } + server.watcher.on('add', onFileAddDelete).on('unlink', onFileAddDelete) // serve our index.html after vite history fallback return () => { @@ -347,6 +348,7 @@ export async function createVitePressPlugin( return [ vitePressPlugin, + rewritesPlugin(siteConfig), vuePlugin, webFontsPlugin(siteConfig.useWebFonts), ...(userViteConfig?.plugins || []), diff --git a/src/node/plugins/dynamicRoutesPlugin.ts b/src/node/plugins/dynamicRoutesPlugin.ts index 4b734c57..b8c2f0a0 100644 --- a/src/node/plugins/dynamicRoutesPlugin.ts +++ b/src/node/plugins/dynamicRoutesPlugin.ts @@ -7,7 +7,7 @@ import { import fs from 'fs-extra' import c from 'picocolors' import path from 'path' -import type { SiteConfig } from '../config' +import { resolvePages, type SiteConfig } from '../config' export const dynamicRouteRE = /\[(\w+?)\]/g @@ -41,44 +41,12 @@ export const dynamicRoutesPlugin = async ( config: SiteConfig ): Promise => { let server: ViteDevServer - let routes = config.dynamicRoutes[0].map(r => r.route) - let [resolvedRoutes, routeFileToModulesMap] = config.dynamicRoutes - - // TODO: make this more efficient by only reloading the invalidated route - // TODO: invlidate modules for paths that are no longer present - async function invlidateRoutes() { - ;[resolvedRoutes, routeFileToModulesMap] = await resolveRoutes(routes) - } return { name: 'vitepress:dynamic-routes', configureServer(_server) { server = _server - - const onFileAddDelete = ( - file: string, - updateRoutes: (route: string) => void - ) => { - if (dynamicRouteRE.test(file) && /\.(md|paths\.[jt]s)$/.test(file)) { - if (file.endsWith('.md')) { - updateRoutes(normalizePath(path.relative(config.root, file))) - } - invlidateRoutes().then(() => { - server.ws.send({ type: 'full-reload' }) - }) - } - } - - server.watcher - .on('add', (file) => { - onFileAddDelete(file, (route) => routes.push(route)) - }) - .on('unlink', (file) => { - onFileAddDelete(file, (route) => { - routes = routes.filter((r) => r !== route) - }) - }) }, resolveId(id) { @@ -86,18 +54,20 @@ export const dynamicRoutesPlugin = async ( const normalizedId = id.startsWith(config.root) ? normalizePath(path.relative(config.root, id)) : id.replace(/^\//, '') - const matched = resolvedRoutes.find((r) => r.path === normalizedId) + const matched = config.dynamicRoutes.routes.find( + (r) => r.path === normalizedId + ) if (matched) { return normalizedId } }, load(id) { - const matched = resolvedRoutes.find((r) => r.path === id) + const matched = config.dynamicRoutes.routes.find((r) => r.path === id) if (matched) { const { route, params, content } = matched const routeFile = normalizePath(path.resolve(config.root, route)) - routeFileToModulesMap[routeFile].push(id) + config.dynamicRoutes.fileToModulesMap[routeFile].push(id) let baseContent = fs.readFileSync(routeFile, 'utf-8') @@ -118,11 +88,11 @@ export const dynamicRoutesPlugin = async ( }, async handleHotUpdate(ctx) { - const mods = routeFileToModulesMap[ctx.file] + const mods = config.dynamicRoutes.fileToModulesMap[ctx.file] if (mods) { // path loader module updated, reset loaded routes if (/\.paths\.[jt]s$/.test(ctx.file)) { - await invlidateRoutes() + await resolvePages(config.srcDir, config.userConfig) } for (const id of mods) { ctx.modules.push(server.moduleGraph.getModuleById(id)!) @@ -132,7 +102,9 @@ export const dynamicRoutesPlugin = async ( } } -export async function resolveRoutes(routes: string[]) { +export async function resolveDynamicRoutes( + routes: string[] +): Promise { const pendingResolveRoutes: Promise[] = [] const routeFileToModulesMap: Record = {} @@ -189,8 +161,8 @@ export async function resolveRoutes(routes: string[]) { } } - return [ - (await Promise.all(pendingResolveRoutes)).flat(), - routeFileToModulesMap - ] as const + return { + routes: (await Promise.all(pendingResolveRoutes)).flat(), + fileToModulesMap: routeFileToModulesMap + } } diff --git a/src/node/plugins/rewritesPlugin.ts b/src/node/plugins/rewritesPlugin.ts new file mode 100644 index 00000000..d018e310 --- /dev/null +++ b/src/node/plugins/rewritesPlugin.ts @@ -0,0 +1,54 @@ +import type { Plugin } from 'vite' +import { compile, match } from 'path-to-regexp' +import type { SiteConfig, UserConfig } from '../config' + +export function resolveRewrites( + pages: string[], + userRewrites: UserConfig['rewrites'] +) { + const rewriteEntries = Object.entries(userRewrites || {}) + const rewrites = rewriteEntries.length + ? Object.fromEntries( + pages + .map((src) => { + for (const [from, to] of rewriteEntries) { + const dest = rewrite(src, from, to) + if (dest) return [src, dest] + } + }) + .filter((e) => e != null) as [string, string][] + ) + : {} + return { + map: rewrites, + inv: Object.fromEntries(Object.entries(rewrites).map((a) => a.reverse())) + } +} + +function rewrite(src: string, from: string, to: string) { + const urlMatch = match(from) + const res = urlMatch(src) + if (!res) return false + const toPath = compile(to) + return toPath(res.params) +} + +export const rewritesPlugin = (config: SiteConfig): Plugin => { + return { + name: 'vitepress:rewrites', + configureServer(server) { + // dev rewrite + server.middlewares.use((req, _res, next) => { + if (req.url) { + const page = req.url + .replace(/[?#].*$/, '') + .slice(config.site.base.length) + if (config.rewrites.inv[page]) { + req.url = req.url.replace(page, config.rewrites.inv[page]!) + } + } + next() + }) + } + } +}