From 26fe81c88618d7df5d623d041ac3df96e7d7ee7b Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 14 Dec 2021 23:08:44 +0800 Subject: [PATCH] feat: support static data loaders --- package.json | 3 + pnpm-lock.yaml | 16 +++++ src/node/plugin.ts | 8 ++- src/node/staticDataPlugin.ts | 116 +++++++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 src/node/staticDataPlugin.ts diff --git a/package.json b/package.json index b7b7ad09..12133803 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "@types/koa-static": "^4.0.1", "@types/lru-cache": "^5.1.0", "@types/markdown-it": "^12.0.1", + "@types/micromatch": "^4.0.2", "@types/node": "^15.6.1", "@types/polka": "^0.5.3", "chalk": "^4.1.1", @@ -102,6 +103,7 @@ "esbuild": "^0.13.4", "escape-html": "^1.0.3", "execa": "^5.0.0", + "fast-glob": "^3.2.7", "fs-extra": "^10.0.0", "globby": "^11.0.3", "gray-matter": "^4.0.3", @@ -114,6 +116,7 @@ "markdown-it-container": "^3.0.0", "markdown-it-emoji": "^2.0.0", "markdown-it-table-of-contents": "^0.5.2", + "micromatch": "^4.0.4", "minimist": "^1.2.5", "npm-run-all": "^4.1.5", "ora": "^5.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8a8ed72..89a8bc79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,7 @@ importers: '@types/koa-static': ^4.0.1 '@types/lru-cache': ^5.1.0 '@types/markdown-it': ^12.0.1 + '@types/micromatch': ^4.0.2 '@types/node': ^15.6.1 '@types/polka': ^0.5.3 '@vitejs/plugin-vue': ^2.0.0 @@ -32,6 +33,7 @@ importers: esbuild: ^0.13.4 escape-html: ^1.0.3 execa: ^5.0.0 + fast-glob: ^3.2.7 fs-extra: ^10.0.0 globby: ^11.0.3 gray-matter: ^4.0.3 @@ -44,6 +46,7 @@ importers: markdown-it-container: ^3.0.0 markdown-it-emoji: ^2.0.0 markdown-it-table-of-contents: ^0.5.2 + micromatch: ^4.0.4 minimist: ^1.2.5 npm-run-all: ^4.1.5 ora: ^5.4.0 @@ -81,6 +84,7 @@ importers: '@types/koa-static': 4.0.2 '@types/lru-cache': 5.1.1 '@types/markdown-it': 12.2.1 + '@types/micromatch': 4.0.2 '@types/node': 15.14.9 '@types/polka': 0.5.3 chalk: 4.1.2 @@ -93,6 +97,7 @@ importers: esbuild: 0.13.4 escape-html: 1.0.3 execa: 5.1.1 + fast-glob: 3.2.7 fs-extra: 10.0.0 globby: 11.0.4 gray-matter: 4.0.3 @@ -105,6 +110,7 @@ importers: markdown-it-container: 3.0.0 markdown-it-emoji: 2.0.0 markdown-it-table-of-contents: 0.5.2 + micromatch: 4.0.4 minimist: 1.2.5 npm-run-all: 4.1.5 ora: 5.4.1 @@ -1076,6 +1082,10 @@ packages: '@types/node': 15.14.9 dev: true + /@types/braces/3.0.1: + resolution: {integrity: sha512-+euflG6ygo4bn0JHtn4pYqcXwRtLvElQ7/nnjDu7iYG56H0+OhCd7d6Ug0IE3WcFpZozBKW2+80FUbv5QGk5AQ==} + dev: true + /@types/compression/1.7.2: resolution: {integrity: sha512-lwEL4M/uAGWngWFLSG87ZDr2kLrbuR8p7X+QZB1OQlT+qkHsCPDVFnHPyXf4Vyl4yDDorNY+mAhosxkCvppatg==} dependencies: @@ -1231,6 +1241,12 @@ packages: resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==} dev: true + /@types/micromatch/4.0.2: + resolution: {integrity: sha512-oqXqVb0ci19GtH0vOA/U2TmHTcRY9kuZl4mqUxe0QmJAlIW13kzhuK5pi1i9+ngav8FjpSb9FVS/GE00GLX1VA==} + dependencies: + '@types/braces': 3.0.1 + dev: true + /@types/mime/1.3.2: resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} dev: true diff --git a/src/node/plugin.ts b/src/node/plugin.ts index e348c94f..7853d5c0 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -8,6 +8,7 @@ import { import { DIST_CLIENT_PATH, APP_PATH, SITE_DATA_REQUEST_PATH } from './alias' import { slash } from './utils/slash' import { OutputAsset, OutputChunk } from 'rollup' +import { staticDataPlugin } from './staticDataPlugin' const hashRE = /\.(\w+)\.js$/ const staticInjectMarkerRE = @@ -273,5 +274,10 @@ export function createVitePressPlugin( } } - return [vitePressPlugin, vuePlugin, ...(userViteConfig?.plugins || [])] + return [ + vitePressPlugin, + vuePlugin, + ...(userViteConfig?.plugins || []), + staticDataPlugin + ] } diff --git a/src/node/staticDataPlugin.ts b/src/node/staticDataPlugin.ts new file mode 100644 index 00000000..29cb522a --- /dev/null +++ b/src/node/staticDataPlugin.ts @@ -0,0 +1,116 @@ +// TODO figure out why it causes full page reload + +import { Plugin, ViteDevServer, loadConfigFromFile, normalizePath } from 'vite' +import { dirname, relative } from 'path' +import { isMatch } from 'micromatch' + +const loaderMatch = /\.data\.(j|t)s$/ + +let server: ViteDevServer + +interface LoaderModule { + base: string + pattern: string | undefined + loader: () => any +} + +const idToLoaderModulesMap: Record = + 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 | 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 any + pattern = loaderModule.watch + if (pattern && pattern.startsWith('./')) { + pattern = pattern.slice(2) + } + loader = loaderModule.load + } + + // load the data + const data = await loader() + + // record loader module for HMR + if (server) { + idToLoaderModulesMap[id] = { base, 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 { base, pattern } = idToLoaderModulesMap[id]! + ;(server as any)._globImporters[id] = { + module: server.moduleGraph.getModuleById(id), + importGlobs: [{ base, pattern }] + } + } + return null + }, + + handleHotUpdate(ctx) { + for (const id in idToLoaderModulesMap) { + const { base, pattern } = idToLoaderModulesMap[id]! + const isLoaderFile = normalizePath(ctx.file) === id + if (isLoaderFile) { + // invalidate loader file + delete idToLoaderModulesMap[id] + } + if ( + isLoaderFile || + (pattern && isMatch(relative(base, ctx.file), pattern)) + ) { + ctx.modules.push(server.moduleGraph.getModuleById(id)!) + } + } + } +}