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 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<ThemeConfig = any>
cacheDir: string
tempDir: string
pages: string[]
dynamicRoutes: readonly [ResolvedRouteConfig[], Record<string, string[]>]
dynamicRoutes: {
routes: ResolvedRouteConfig[]
fileToModulesMap: Record<string, string[]>
}
rewrites: {
map: Record<string, string | undefined>
inv: Record<string, string | undefined>
}
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 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 { pages, dynamicRoutes, rewrites } = await resolvePages(
srcDir,
userConfig
)
: {}
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
}
}

@ -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<ReturnType<typeof createMarkdownToVueRenderFn>>
@ -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 || []),

@ -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<Plugin> => {
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<SiteConfig['dynamicRoutes']> {
const pendingResolveRoutes: Promise<ResolvedRouteConfig[]>[] = []
const routeFileToModulesMap: Record<string, string[]> = {}
@ -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
}
}

@ -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