|
|
@ -1,4 +1,4 @@
|
|
|
|
import { loadConfigFromFile, type Plugin } from 'vite'
|
|
|
|
import { loadConfigFromFile, type Plugin, type ViteDevServer } from 'vite'
|
|
|
|
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'
|
|
|
@ -32,9 +32,67 @@ type ResolvedRouteConfig = UserRouteConfig & {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const dynamicRoutesPlugin = async (
|
|
|
|
export const dynamicRoutesPlugin = async (
|
|
|
|
routes: string[]
|
|
|
|
initialRoutes: string[]
|
|
|
|
): Promise<Plugin> => {
|
|
|
|
): Promise<Plugin> => {
|
|
|
|
|
|
|
|
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(/<!--\s*@content\s*-->/, 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<ResolvedRouteConfig[]>[] = []
|
|
|
|
const pendingResolveRoutes: Promise<ResolvedRouteConfig[]>[] = []
|
|
|
|
|
|
|
|
const routeFileToModulesMap: Record<string, string[]> = {}
|
|
|
|
|
|
|
|
|
|
|
|
for (const route of routes) {
|
|
|
|
for (const route of routes) {
|
|
|
|
// locate corresponding route paths file
|
|
|
|
// locate corresponding route paths file
|
|
|
@ -66,15 +124,20 @@ export const dynamicRoutesPlugin = async (
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (mod) {
|
|
|
|
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<ResolvedRouteConfig[]> => {
|
|
|
|
const resolveRoute = async (): Promise<ResolvedRouteConfig[]> => {
|
|
|
|
const loader = mod.config.paths
|
|
|
|
const loader = mod.config.paths
|
|
|
|
const paths = await (typeof loader === 'function' ? loader() : loader)
|
|
|
|
const paths = await (typeof loader === 'function' ? loader() : loader)
|
|
|
|
return paths.map((userConfig) => {
|
|
|
|
return paths.map((userConfig) => {
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
route,
|
|
|
|
|
|
|
|
path:
|
|
|
|
path:
|
|
|
|
'/' +
|
|
|
|
'/' +
|
|
|
|
route.replace(/\[(\w+)\]/g, (_, key) => userConfig.params[key]),
|
|
|
|
route.replace(/\[(\w+)\]/g, (_, key) => userConfig.params[key]),
|
|
|
|
|
|
|
|
route,
|
|
|
|
...userConfig
|
|
|
|
...userConfig
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
@ -83,28 +146,8 @@ export const dynamicRoutesPlugin = async (
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const resolvedRoutes = (await Promise.all(pendingResolveRoutes)).flat()
|
|
|
|
return [
|
|
|
|
|
|
|
|
(await Promise.all(pendingResolveRoutes)).flat(),
|
|
|
|
return {
|
|
|
|
routeFileToModulesMap
|
|
|
|
name: 'vitepress:dynamic-routes',
|
|
|
|
] as const
|
|
|
|
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(/<!--\s*@content\s*-->/, 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
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|