vitepress/src/node/plugins/staticDataPlugin.ts

125 lines
3.3 KiB

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)!)
}
}
}
}