diff --git a/CHANGELOG.md b/CHANGELOG.md
index 031001ae..0e357735 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,24 @@
+## [0.20.9](https://github.com/vuejs/vitepress/compare/v0.20.8...v0.20.9) (2021-12-15)
+
+### Features
+
+- shouldPreload hook ([e721d60](https://github.com/vuejs/vitepress/commit/e721d605851be4e27f4948d96d5c3bab6d23ead2))
+- support array of patterns in data loaders ([f5308d7](https://github.com/vuejs/vitepress/commit/f5308d746f3089ef6818b0139fe249827a47628b))
+
+## [0.20.8](https://github.com/vuejs/vitepress/compare/v0.20.7...v0.20.8) (2021-12-14)
+
+## [0.20.7](https://github.com/vuejs/vitepress/compare/v0.20.6...v0.20.7) (2021-12-14)
+
+### Features
+
+- **types:** re-export vite client type ([4caa7b2](https://github.com/vuejs/vitepress/commit/4caa7b231753ddedb83365a37b8c259ae461bd37))
+
+## [0.20.6](https://github.com/vuejs/vitepress/compare/v0.20.4...v0.20.6) (2021-12-14)
+
+### Features
+
+- support static data loaders ([26fe81c](https://github.com/vuejs/vitepress/commit/26fe81c88618d7df5d623d041ac3df96e7d7ee7b))
+
## [0.20.5](https://github.com/vuejs/vitepress/compare/v0.20.4...v0.20.5) (2021-12-12)
- Bump vue & vite versions
diff --git a/client.d.ts b/client.d.ts
new file mode 100644
index 00000000..78cda7e2
--- /dev/null
+++ b/client.d.ts
@@ -0,0 +1,4 @@
+// re-export vite client types
+// with strict installers like pnpm, user won't be able to reference vite/client
+// in project root
+///
diff --git a/package.json b/package.json
index b7b7ad09..ed5f104e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "vitepress",
- "version": "0.20.5",
+ "version": "0.20.9",
"description": "Vite & Vue powered static site generator",
"main": "dist/node/index.js",
"typings": "types/index.d.ts",
@@ -10,7 +10,8 @@
"files": [
"bin",
"dist",
- "types"
+ "types",
+ "client.d.ts"
],
"scripts": {
"dev": "run-s dev-shared dev-start",
@@ -90,6 +91,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 +104,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 +117,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/build/render.ts b/src/node/build/render.ts
index cec38728..ae851343 100644
--- a/src/node/build/render.ts
+++ b/src/node/build/render.ts
@@ -39,27 +39,43 @@ export async function renderPage(
))
const pageData = JSON.parse(__pageData)
- const preloadLinks = (
- config.mpa
- ? appChunk
- ? [appChunk.fileName]
- : []
- : result && appChunk
- ? [
+ let preloadLinks = config.mpa
+ ? appChunk
+ ? [appChunk.fileName]
+ : []
+ : result && appChunk
+ ? [
+ ...new Set([
// resolve imports for index.js + page.md.js and inject script tags for
// them as well so we fetch everything as early as possible without having
// to wait for entry chunks to parse
...resolvePageImports(config, page, result, appChunk),
pageClientJsFileName,
appChunk.fileName
- ]
- : []
- )
+ ])
+ ]
+ : []
+
+ let prefetchLinks: string[] = []
+
+ const { shouldPreload } = config
+ if (shouldPreload) {
+ prefetchLinks = preloadLinks.filter((link) => !shouldPreload(link, page))
+ preloadLinks = preloadLinks.filter((link) => shouldPreload(link, page))
+ }
+
+ const preloadLinksString = preloadLinks
.map((file) => {
return ``
})
.join('\n ')
+ const prefetchLinkString = prefetchLinks
+ .map((file) => {
+ return ``
+ })
+ .join('\n ')
+
const stylesheetLink = cssChunk
? ``
: ''
@@ -103,7 +119,8 @@ export async function renderPage(
pageData.description || siteData.description
}">
${stylesheetLink}
- ${preloadLinks}
+ ${preloadLinksString}
+ ${prefetchLinkString}
${renderHead(head)}
@@ -130,24 +147,22 @@ function resolvePageImports(
config: SiteConfig,
page: string,
result: RollupOutput,
- indexChunk: OutputChunk
+ appChunk: OutputChunk
) {
// find the page's js chunk and inject script tags for its imports so that
- // they are start fetching as early as possible
+ // they start fetching as early as possible
const srcPath = normalizePath(
fs.realpathSync(path.resolve(config.srcDir, page))
)
const pageChunk = result.output.find(
(chunk) => chunk.type === 'chunk' && chunk.facadeModuleId === srcPath
) as OutputChunk
- return Array.from(
- new Set([
- ...indexChunk.imports,
- ...indexChunk.dynamicImports,
- ...pageChunk.imports,
- ...pageChunk.dynamicImports
- ])
- )
+ return [
+ ...appChunk.imports,
+ ...appChunk.dynamicImports,
+ ...pageChunk.imports,
+ ...pageChunk.dynamicImports
+ ]
}
function renderHead(head: HeadConfig[]) {
diff --git a/src/node/config.ts b/src/node/config.ts
index a4deaeff..076b837c 100644
--- a/src/node/config.ts
+++ b/src/node/config.ts
@@ -48,6 +48,7 @@ export interface UserConfig {
srcDir?: string
srcExclude?: string[]
outDir?: string
+ shouldPreload?: (link: string, page: string) => boolean
/**
* Enable MPA / zero-JS mode
@@ -61,7 +62,11 @@ export type RawConfigExports =
| Promise
| (() => UserConfig | Promise)
-export interface SiteConfig {
+export interface SiteConfig
+ extends Pick<
+ UserConfig,
+ 'markdown' | 'vue' | 'vite' | 'shouldPreload' | 'mpa'
+ > {
root: string
srcDir: string
site: SiteData
@@ -71,10 +76,6 @@ export interface SiteConfig {
tempDir: string
alias: AliasOptions
pages: string[]
- markdown: MarkdownOptions | undefined
- vue: VuePluginOptions | undefined
- vite: ViteConfig | undefined
- mpa: boolean
}
const resolve = (root: string, file: string) =>
@@ -129,6 +130,7 @@ export async function resolveConfig(
alias: resolveAliases(themeDir),
vue: userConfig.vue,
vite: userConfig.vite,
+ shouldPreload: userConfig.shouldPreload,
mpa: !!userConfig.mpa
}
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..7ff9dc91
--- /dev/null
+++ b/src/node/staticDataPlugin.ts
@@ -0,0 +1,126 @@
+// 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 {
+ watch: string[] | string | undefined
+ load: () => any
+}
+
+interface CachedLoaderModule {
+ 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 LoaderModule
+ pattern =
+ typeof loaderModule.watch === 'string'
+ ? [loaderModule.watch]
+ : loaderModule.watch
+ if (pattern) {
+ pattern = pattern.map((p) => {
+ return p.startsWith('./') ? p.slice(2) : p
+ })
+ }
+ 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: pattern?.map((pattern) => ({ 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)!)
+ }
+ }
+ }
+}