fix: update route configs on file add / delete

pull/2005/head
Evan You 2 years ago
parent 34b149ba25
commit bccce98c62

@ -3,7 +3,6 @@ import _debug from 'debug'
import fg from 'fast-glob' import fg from 'fast-glob'
import fs from 'fs-extra' import fs from 'fs-extra'
import path from 'path' import path from 'path'
import { compile, match } from 'path-to-regexp'
import c from 'picocolors' import c from 'picocolors'
import { import {
createLogger, createLogger,
@ -17,9 +16,10 @@ import { DEFAULT_THEME_PATH } from './alias'
import type { MarkdownOptions } from './markdown/markdown' import type { MarkdownOptions } from './markdown/markdown'
import { import {
dynamicRouteRE, dynamicRouteRE,
resolveRoutes, resolveDynamicRoutes,
type ResolvedRouteConfig type ResolvedRouteConfig
} from './plugins/dynamicRoutesPlugin' } from './plugins/dynamicRoutesPlugin'
import { resolveRewrites } from './plugins/rewritesPlugin'
import { import {
APPEARANCE_KEY, APPEARANCE_KEY,
type Awaitable, type Awaitable,
@ -183,12 +183,16 @@ export interface SiteConfig<ThemeConfig = any>
cacheDir: string cacheDir: string
tempDir: string tempDir: string
pages: string[] pages: string[]
dynamicRoutes: readonly [ResolvedRouteConfig[], Record<string, string[]>] dynamicRoutes: {
routes: ResolvedRouteConfig[]
fileToModulesMap: Record<string, string[]>
}
rewrites: { rewrites: {
map: Record<string, string | undefined> map: Record<string, string | undefined>
inv: Record<string, string | undefined> inv: Record<string, string | undefined>
} }
logger: Logger logger: Logger
userConfig: UserConfig
} }
const resolve = (root: string, file: string) => const resolve = (root: string, file: string) =>
@ -242,39 +246,10 @@ export async function resolveConfig(
? userThemeDir ? userThemeDir
: DEFAULT_THEME_PATH : DEFAULT_THEME_PATH
// Important: fast-glob doesn't guarantee order of the returned files. const { pages, dynamicRoutes, rewrites } = await resolvePages(
// We must sort the pages so the input list to rollup is stable across srcDir,
// builds - otherwise different input order could result in different exports userConfig
// 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 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 = { const config: SiteConfig = {
root, root,
@ -305,10 +280,8 @@ export async function resolveConfig(
transformHead: userConfig.transformHead, transformHead: userConfig.transformHead,
transformHtml: userConfig.transformHtml, transformHtml: userConfig.transformHtml,
transformPageData: userConfig.transformPageData, transformPageData: userConfig.transformPageData,
rewrites: { rewrites,
map: rewrites, userConfig
inv: Object.fromEntries(Object.entries(rewrites).map((a) => a.reverse()))
}
} }
return config return config
@ -444,10 +417,32 @@ function resolveSiteDataHead(userConfig?: UserConfig): HeadConfig[] {
return head return head
} }
function rewrite(src: string, from: string, to: string) { export async function resolvePages(srcDir: string, userConfig: UserConfig) {
const urlMatch = match(from) // Important: fast-glob doesn't guarantee order of the returned files.
const res = urlMatch(src) // We must sort the pages so the input list to rollup is stable across
if (!res) return false // builds - otherwise different input order could result in different exports
const toPath = compile(to) // order in shared chunks which in turns invalidates the hash of every chunk!
return toPath(res.params) // 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
}
} }

@ -15,12 +15,13 @@ import {
resolveAliases, resolveAliases,
SITE_DATA_REQUEST_PATH SITE_DATA_REQUEST_PATH
} from './alias' } from './alias'
import type { SiteConfig } from './config' import { resolvePages, type SiteConfig } from './config'
import { clearCache, createMarkdownToVueRenderFn } from './markdownToVue' import { clearCache, createMarkdownToVueRenderFn } from './markdownToVue'
import type { PageDataPayload } from './shared' import type { PageDataPayload } from './shared'
import { staticDataPlugin } from './plugins/staticDataPlugin' import { staticDataPlugin } from './plugins/staticDataPlugin'
import { webFontsPlugin } from './plugins/webFontsPlugin' import { webFontsPlugin } from './plugins/webFontsPlugin'
import { dynamicRoutesPlugin } from './plugins/dynamicRoutesPlugin' import { dynamicRoutesPlugin } from './plugins/dynamicRoutesPlugin'
import { rewritesPlugin } from './plugins/rewritesPlugin'
declare module 'vite' { declare module 'vite' {
interface UserConfig { interface UserConfig {
@ -70,8 +71,7 @@ export async function createVitePressPlugin(
pages, pages,
ignoreDeadLinks, ignoreDeadLinks,
lastUpdated, lastUpdated,
cleanUrls, cleanUrls
rewrites
} = siteConfig } = siteConfig
let markdownToVue: Awaited<ReturnType<typeof createMarkdownToVueRenderFn>> let markdownToVue: Awaited<ReturnType<typeof createMarkdownToVueRenderFn>>
@ -198,15 +198,16 @@ export async function createVitePressPlugin(
configDeps.forEach((file) => server.watcher.add(file)) configDeps.forEach((file) => server.watcher.add(file))
} }
server.middlewares.use((req, res, next) => { // update pages, dynamicRoutes and rewrites on md file add / deletion
if (req.url) { const onFileAddDelete = async (file: string) => {
const page = req.url.replace(/[?#].*$/, '').slice(site.base.length) if (file.endsWith('.md')) {
if (rewrites.inv[page]) { Object.assign(
req.url = req.url.replace(page, rewrites.inv[page]!) siteConfig,
await resolvePages(siteConfig.srcDir, siteConfig.userConfig)
)
} }
} }
next() server.watcher.on('add', onFileAddDelete).on('unlink', onFileAddDelete)
})
// serve our index.html after vite history fallback // serve our index.html after vite history fallback
return () => { return () => {
@ -347,6 +348,7 @@ export async function createVitePressPlugin(
return [ return [
vitePressPlugin, vitePressPlugin,
rewritesPlugin(siteConfig),
vuePlugin, vuePlugin,
webFontsPlugin(siteConfig.useWebFonts), webFontsPlugin(siteConfig.useWebFonts),
...(userViteConfig?.plugins || []), ...(userViteConfig?.plugins || []),

@ -7,7 +7,7 @@ import {
import fs from 'fs-extra' import fs from 'fs-extra'
import c from 'picocolors' import c from 'picocolors'
import path from 'path' import path from 'path'
import type { SiteConfig } from '../config' import { resolvePages, type SiteConfig } from '../config'
export const dynamicRouteRE = /\[(\w+?)\]/g export const dynamicRouteRE = /\[(\w+?)\]/g
@ -41,44 +41,12 @@ export const dynamicRoutesPlugin = async (
config: SiteConfig config: SiteConfig
): Promise<Plugin> => { ): Promise<Plugin> => {
let server: ViteDevServer 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 { return {
name: 'vitepress:dynamic-routes', name: 'vitepress:dynamic-routes',
configureServer(_server) { configureServer(_server) {
server = _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) { resolveId(id) {
@ -86,18 +54,20 @@ export const dynamicRoutesPlugin = async (
const normalizedId = id.startsWith(config.root) const normalizedId = id.startsWith(config.root)
? normalizePath(path.relative(config.root, id)) ? normalizePath(path.relative(config.root, id))
: id.replace(/^\//, '') : id.replace(/^\//, '')
const matched = resolvedRoutes.find((r) => r.path === normalizedId) const matched = config.dynamicRoutes.routes.find(
(r) => r.path === normalizedId
)
if (matched) { if (matched) {
return normalizedId return normalizedId
} }
}, },
load(id) { load(id) {
const matched = resolvedRoutes.find((r) => r.path === id) const matched = config.dynamicRoutes.routes.find((r) => r.path === id)
if (matched) { if (matched) {
const { route, params, content } = matched const { route, params, content } = matched
const routeFile = normalizePath(path.resolve(config.root, route)) 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') let baseContent = fs.readFileSync(routeFile, 'utf-8')
@ -118,11 +88,11 @@ export const dynamicRoutesPlugin = async (
}, },
async handleHotUpdate(ctx) { async handleHotUpdate(ctx) {
const mods = routeFileToModulesMap[ctx.file] const mods = config.dynamicRoutes.fileToModulesMap[ctx.file]
if (mods) { if (mods) {
// path loader module updated, reset loaded routes // path loader module updated, reset loaded routes
if (/\.paths\.[jt]s$/.test(ctx.file)) { if (/\.paths\.[jt]s$/.test(ctx.file)) {
await invlidateRoutes() await resolvePages(config.srcDir, config.userConfig)
} }
for (const id of mods) { for (const id of mods) {
ctx.modules.push(server.moduleGraph.getModuleById(id)!) 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<SiteConfig['dynamicRoutes']> {
const pendingResolveRoutes: Promise<ResolvedRouteConfig[]>[] = [] const pendingResolveRoutes: Promise<ResolvedRouteConfig[]>[] = []
const routeFileToModulesMap: Record<string, string[]> = {} const routeFileToModulesMap: Record<string, string[]> = {}
@ -189,8 +161,8 @@ export async function resolveRoutes(routes: string[]) {
} }
} }
return [ return {
(await Promise.all(pendingResolveRoutes)).flat(), routes: (await Promise.all(pendingResolveRoutes)).flat(),
routeFileToModulesMap fileToModulesMap: routeFileToModulesMap
] as const }
} }

@ -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()
})
}
}
}
Loading…
Cancel
Save