wip: dynamic routes

pull/2005/head
Evan You 2 years ago
parent ddb9020545
commit 9c4c2117a6

@ -15,6 +15,7 @@ import {
} from 'vite'
import { DEFAULT_THEME_PATH } from './alias'
import type { MarkdownOptions } from './markdown/markdown'
import { dynamicRouteRE } from './plugins/dynamicRoutesPlugin'
import {
APPEARANCE_KEY,
type Awaitable,
@ -178,6 +179,7 @@ export interface SiteConfig<ThemeConfig = any>
cacheDir: string
tempDir: string
pages: string[]
dynamicRoutes: string[]
rewrites: {
map: Record<string, string | undefined>
inv: Record<string, string | undefined>
@ -242,15 +244,17 @@ export async function resolveConfig(
// 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 pages = (
const allMarkdownFiles = (
await fg(['**.md'], {
cwd: srcDir,
ignore: ['**/node_modules', ...(userConfig.srcExclude || [])]
})
).sort()
const rewriteEntries = Object.entries(userConfig.rewrites || {})
const pages = allMarkdownFiles.filter((p) => !dynamicRouteRE.test(p))
const dynamicRoutes = allMarkdownFiles.filter((p) => dynamicRouteRE.test(p))
const rewriteEntries = Object.entries(userConfig.rewrites || {})
const rewrites = rewriteEntries.length
? Object.fromEntries(
pages
@ -270,6 +274,7 @@ export async function resolveConfig(
site,
themeDir,
pages,
dynamicRoutes,
configPath,
configDeps,
outDir,

@ -69,6 +69,16 @@ export async function createMarkdownToVueRenderFn(
const start = Date.now()
// resolve params for dynamic routes
let params
src = src.replace(
/^__VP_PARAMS_START([^]+?)__VP_PARAMS_END__/,
(_, paramsString) => {
params = JSON.parse(paramsString)
return ''
}
)
// resolve includes
let includes: string[] = []
src = src.replace(includesRE, (m, m1) => {
@ -151,6 +161,7 @@ export async function createMarkdownToVueRenderFn(
description: inferDescription(frontmatter),
frontmatter,
headers,
params,
relativePath
}

@ -1,5 +1,6 @@
import path from 'path'
import c from 'picocolors'
import { slash } from './utils/slash'
import type { OutputAsset, OutputChunk } from 'rollup'
import {
defineConfig,
@ -17,9 +18,9 @@ import {
import type { SiteConfig } from './config'
import { clearCache, createMarkdownToVueRenderFn } from './markdownToVue'
import type { PageDataPayload } from './shared'
import { staticDataPlugin } from './staticDataPlugin'
import { slash } from './utils/slash'
import { webFontsPlugin } from './webFontsPlugin'
import { staticDataPlugin } from './plugins/staticDataPlugin'
import { webFontsPlugin } from './plugins/webFontsPlugin'
import { dynamicRoutesPlugin } from './plugins/dynamicRoutesPlugin'
declare module 'vite' {
interface UserConfig {
@ -347,6 +348,7 @@ export async function createVitePressPlugin(
vuePlugin,
webFontsPlugin(siteConfig.useWebFonts),
...(userViteConfig?.plugins || []),
staticDataPlugin
staticDataPlugin,
await dynamicRoutesPlugin(siteConfig.dynamicRoutes)
]
}

@ -0,0 +1,110 @@
import { loadConfigFromFile, type Plugin } from 'vite'
import fs from 'fs-extra'
import c from 'picocolors'
import path from 'path'
export const dynamicRouteRE = /\[(\.\.\.)?\w+\]/
interface UserRouteConfig {
params: Record<string, string>
content?: string
}
interface RouteModule {
path: string
config: {
paths:
| UserRouteConfig[]
| (() => UserRouteConfig[] | Promise<UserRouteConfig[]>)
}
dependencies: string[]
}
type ResolvedRouteConfig = UserRouteConfig & {
/**
* the raw route, e.g. foo/[bar].md
*/
route: string
/**
* the actual path with params resolved, e.g. foo/1.md
*/
path: string
}
export const dynamicRoutesPlugin = async (
routes: string[]
): Promise<Plugin> => {
const pendingResolveRoutes: Promise<ResolvedRouteConfig[]>[] = []
for (const route of routes) {
// locate corresponding route paths file
const jsPathsFile = route.replace(/\.md$/, '.paths.js')
let pathsFile = jsPathsFile
if (!fs.existsSync(jsPathsFile)) {
pathsFile = route.replace(/\.md$/, '.paths.ts')
if (!fs.existsSync(pathsFile)) {
console.warn(
c.yellow(
`missing paths file for dynamic route ${route}: ` +
`a corresponding ${jsPathsFile} or ${pathsFile} is needed.`
)
)
continue
}
}
// load the paths loader module
let mod: RouteModule
try {
mod = (await loadConfigFromFile(
{} as any,
path.resolve(pathsFile)
)) as RouteModule
} catch (e) {
console.warn(`invalid paths file export in ${pathsFile}.`)
continue
}
if (mod) {
const resolveRoute = async (): Promise<ResolvedRouteConfig[]> => {
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]),
...userConfig
}
})
}
pendingResolveRoutes.push(resolveRoute())
}
}
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(/<!--\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
}
}

1
types/shared.d.ts vendored

@ -11,6 +11,7 @@ export interface PageData {
description: string
headers: Header[]
frontmatter: Record<string, any>
params?: Record<string, any>
lastUpdated?: number
}

Loading…
Cancel
Save