Skip to content

Commit 26fe81c

Browse files
committed
feat: support static data loaders
1 parent 621d37a commit 26fe81c

File tree

4 files changed

+142
-1
lines changed

4 files changed

+142
-1
lines changed

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"@types/koa-static": "^4.0.1",
9191
"@types/lru-cache": "^5.1.0",
9292
"@types/markdown-it": "^12.0.1",
93+
"@types/micromatch": "^4.0.2",
9394
"@types/node": "^15.6.1",
9495
"@types/polka": "^0.5.3",
9596
"chalk": "^4.1.1",
@@ -102,6 +103,7 @@
102103
"esbuild": "^0.13.4",
103104
"escape-html": "^1.0.3",
104105
"execa": "^5.0.0",
106+
"fast-glob": "^3.2.7",
105107
"fs-extra": "^10.0.0",
106108
"globby": "^11.0.3",
107109
"gray-matter": "^4.0.3",
@@ -114,6 +116,7 @@
114116
"markdown-it-container": "^3.0.0",
115117
"markdown-it-emoji": "^2.0.0",
116118
"markdown-it-table-of-contents": "^0.5.2",
119+
"micromatch": "^4.0.4",
117120
"minimist": "^1.2.5",
118121
"npm-run-all": "^4.1.5",
119122
"ora": "^5.4.0",

pnpm-lock.yaml

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/node/plugin.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { DIST_CLIENT_PATH, APP_PATH, SITE_DATA_REQUEST_PATH } from './alias'
99
import { slash } from './utils/slash'
1010
import { OutputAsset, OutputChunk } from 'rollup'
11+
import { staticDataPlugin } from './staticDataPlugin'
1112

1213
const hashRE = /\.(\w+)\.js$/
1314
const staticInjectMarkerRE =
@@ -273,5 +274,10 @@ export function createVitePressPlugin(
273274
}
274275
}
275276

276-
return [vitePressPlugin, vuePlugin, ...(userViteConfig?.plugins || [])]
277+
return [
278+
vitePressPlugin,
279+
vuePlugin,
280+
...(userViteConfig?.plugins || []),
281+
staticDataPlugin
282+
]
277283
}

src/node/staticDataPlugin.ts

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// TODO figure out why it causes full page reload
2+
3+
import { Plugin, ViteDevServer, loadConfigFromFile, normalizePath } from 'vite'
4+
import { dirname, relative } from 'path'
5+
import { isMatch } from 'micromatch'
6+
7+
const loaderMatch = /\.data\.(j|t)s$/
8+
9+
let server: ViteDevServer
10+
11+
interface LoaderModule {
12+
base: string
13+
pattern: string | undefined
14+
loader: () => any
15+
}
16+
17+
const idToLoaderModulesMap: Record<string, LoaderModule | undefined> =
18+
Object.create(null)
19+
20+
// During build, the load hook will be called on the same file twice
21+
// once for client and once for server build. Not only is this wasteful, it
22+
// also leads to a race condition in loadConfigFromFile() that results in an
23+
// fs unlink error. So we reuse the same Promise during build to avoid double
24+
// loading.
25+
let idToPendingPromiseMap: Record<string, Promise<string> | undefined> =
26+
Object.create(null)
27+
let isBuild = false
28+
29+
export const staticDataPlugin: Plugin = {
30+
name: 'vitepress:data',
31+
32+
configResolved(config) {
33+
isBuild = config.command === 'build'
34+
},
35+
36+
configureServer(_server) {
37+
server = _server
38+
},
39+
40+
async load(id) {
41+
if (loaderMatch.test(id)) {
42+
let _resolve: ((res: any) => void) | undefined
43+
if (isBuild) {
44+
if (idToPendingPromiseMap[id]) {
45+
return idToPendingPromiseMap[id]
46+
}
47+
idToPendingPromiseMap[id] = new Promise((r) => {
48+
_resolve = r
49+
})
50+
}
51+
52+
const base = dirname(id)
53+
let pattern: string | undefined
54+
let loader: () => any
55+
56+
const existing = idToLoaderModulesMap[id]
57+
if (existing) {
58+
;({ pattern, loader } = existing)
59+
} else {
60+
// use vite's load config util as a away to load Node.js file with
61+
// TS & native ESM support
62+
const loaderModule = (await loadConfigFromFile({} as any, id))
63+
?.config as any
64+
pattern = loaderModule.watch
65+
if (pattern && pattern.startsWith('./')) {
66+
pattern = pattern.slice(2)
67+
}
68+
loader = loaderModule.load
69+
}
70+
71+
// load the data
72+
const data = await loader()
73+
74+
// record loader module for HMR
75+
if (server) {
76+
idToLoaderModulesMap[id] = { base, pattern, loader }
77+
}
78+
79+
const result = `export const data = JSON.parse(${JSON.stringify(
80+
JSON.stringify(data)
81+
)})`
82+
83+
if (_resolve) _resolve(result)
84+
return result
85+
}
86+
},
87+
88+
transform(_code, id) {
89+
if (server && loaderMatch.test(id)) {
90+
// register this module as a glob importer
91+
const { base, pattern } = idToLoaderModulesMap[id]!
92+
;(server as any)._globImporters[id] = {
93+
module: server.moduleGraph.getModuleById(id),
94+
importGlobs: [{ base, pattern }]
95+
}
96+
}
97+
return null
98+
},
99+
100+
handleHotUpdate(ctx) {
101+
for (const id in idToLoaderModulesMap) {
102+
const { base, pattern } = idToLoaderModulesMap[id]!
103+
const isLoaderFile = normalizePath(ctx.file) === id
104+
if (isLoaderFile) {
105+
// invalidate loader file
106+
delete idToLoaderModulesMap[id]
107+
}
108+
if (
109+
isLoaderFile ||
110+
(pattern && isMatch(relative(base, ctx.file), pattern))
111+
) {
112+
ctx.modules.push(server.moduleGraph.getModuleById(id)!)
113+
}
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)