import { type Plugin, type ViteDevServer, loadConfigFromFile, normalizePath } from 'vite' import { dirname, resolve } from 'path' import { isMatch } from 'micromatch' const loaderMatch = /\.data\.(j|t)s$/ let server: ViteDevServer interface LoaderModule { watch: string[] | string | undefined load: () => any } interface CachedLoaderModule { pattern: string[] | undefined loader: () => any } const idToLoaderModulesMap: Record<string, CachedLoaderModule | undefined> = Object.create(null) // During build, the load hook will be called on the same file twice // once for client and once for server build. Not only is this wasteful, it // also leads to a race condition in loadConfigFromFile() that results in an // fs unlink error. So we reuse the same Promise during build to avoid double // loading. let idToPendingPromiseMap: Record<string, Promise<string> | undefined> = Object.create(null) let isBuild = false export const staticDataPlugin: Plugin = { name: 'vitepress:data', configResolved(config) { isBuild = config.command === 'build' }, configureServer(_server) { server = _server }, async load(id) { if (loaderMatch.test(id)) { let _resolve: ((res: any) => void) | undefined if (isBuild) { if (idToPendingPromiseMap[id]) { return idToPendingPromiseMap[id] } idToPendingPromiseMap[id] = new Promise((r) => { _resolve = r }) } const base = dirname(id) let pattern: string[] | undefined let loader: () => any const existing = idToLoaderModulesMap[id] if (existing) { ;({ pattern, loader } = existing) } else { // use vite's load config util as a away to load Node.js file with // TS & native ESM support const loaderModule = (await loadConfigFromFile({} as any, id)) ?.config as LoaderModule pattern = typeof loaderModule.watch === 'string' ? [loaderModule.watch] : loaderModule.watch if (pattern) { pattern = pattern.map((p) => { return p.startsWith('.') ? normalizePath(resolve(base, p)) : normalizePath(p) }) } loader = loaderModule.load } // load the data const data = await loader() // record loader module for HMR if (server) { idToLoaderModulesMap[id] = { pattern, loader } } const result = `export const data = JSON.parse(${JSON.stringify( JSON.stringify(data) )})` if (_resolve) _resolve(result) return result } }, transform(_code, id) { if (server && loaderMatch.test(id)) { // register this module as a glob importer const { pattern } = idToLoaderModulesMap[id]! ;(server as any)._importGlobMap.set(id, [pattern]) } return null }, handleHotUpdate(ctx) { for (const id in idToLoaderModulesMap) { const { pattern } = idToLoaderModulesMap[id]! const isLoaderFile = normalizePath(ctx.file) === id if (isLoaderFile) { // invalidate loader file delete idToLoaderModulesMap[id] } if (isLoaderFile || (pattern && isMatch(ctx.file, pattern))) { ctx.modules.push(server.moduleGraph.getModuleById(id)!) } } } }