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 4fdc2117..d3ff1c70 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -47,6 +47,7 @@ export interface UserConfig { srcDir?: string srcExclude?: string[] + shouldPreload?: (link: string, page: string) => boolean /** * Enable MPA / zero-JS mode @@ -60,7 +61,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 @@ -70,10 +75,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) => @@ -127,6 +128,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)!) + } + } + } +}