feat: use git-based lastUpdated data

- The main reason behind this change is because previously the FS-based
timestamp is inaccurate and will change on every remote deployment in
CI environments, resulting in cache invalidation on every build. Using
git timestamps makes them stable.

- This is now disabled by default for performance reasons, as getting
the git timestamp requires spawning a child process and is expensive.
To enable it, use the new `lastUpdated: true` config option.
pull/540/head
Evan You 3 years ago
parent b404c6671d
commit d32d8d4419

@ -38,6 +38,7 @@
"docs-dev": "node ./bin/vitepress dev docs", "docs-dev": "node ./bin/vitepress dev docs",
"docs-debug": "node --inspect-brk ./bin/vitepress dev docs", "docs-debug": "node --inspect-brk ./bin/vitepress dev docs",
"docs-build": "npm run build && node ./bin/vitepress build docs", "docs-build": "npm run build && node ./bin/vitepress build docs",
"docs-build-only": "node ./bin/vitepress build docs",
"docs-serve": "node ./bin/vitepress serve docs", "docs-serve": "node ./bin/vitepress serve docs",
"ci-docs": "run-s build docs-build" "ci-docs": "run-s build docs-build"
}, },
@ -85,6 +86,7 @@
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.0.4", "@rollup/plugin-node-resolve": "^13.0.4",
"@types/compression": "^1.7.0", "@types/compression": "^1.7.0",
"@types/cross-spawn": "^6.0.2",
"@types/debug": "^4.1.7", "@types/debug": "^4.1.7",
"@types/fs-extra": "^9.0.11", "@types/fs-extra": "^9.0.11",
"@types/koa": "^2.13.1", "@types/koa": "^2.13.1",
@ -98,6 +100,7 @@
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"compression": "^1.7.4", "compression": "^1.7.4",
"conventional-changelog-cli": "^2.1.1", "conventional-changelog-cli": "^2.1.1",
"cross-spawn": "^7.0.3",
"debug": "^4.3.2", "debug": "^4.3.2",
"diacritics": "^1.3.0", "diacritics": "^1.3.0",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",

@ -12,6 +12,7 @@ importers:
'@rollup/plugin-json': ^4.1.0 '@rollup/plugin-json': ^4.1.0
'@rollup/plugin-node-resolve': ^13.0.4 '@rollup/plugin-node-resolve': ^13.0.4
'@types/compression': ^1.7.0 '@types/compression': ^1.7.0
'@types/cross-spawn': ^6.0.2
'@types/debug': ^4.1.7 '@types/debug': ^4.1.7
'@types/fs-extra': ^9.0.11 '@types/fs-extra': ^9.0.11
'@types/koa': ^2.13.1 '@types/koa': ^2.13.1
@ -26,6 +27,7 @@ importers:
chokidar: ^3.5.1 chokidar: ^3.5.1
compression: ^1.7.4 compression: ^1.7.4
conventional-changelog-cli: ^2.1.1 conventional-changelog-cli: ^2.1.1
cross-spawn: ^7.0.3
debug: ^4.3.2 debug: ^4.3.2
diacritics: ^1.3.0 diacritics: ^1.3.0
enquirer: ^2.3.6 enquirer: ^2.3.6
@ -75,6 +77,7 @@ importers:
'@rollup/plugin-json': 4.1.0_rollup@2.57.0 '@rollup/plugin-json': 4.1.0_rollup@2.57.0
'@rollup/plugin-node-resolve': 13.0.5_rollup@2.57.0 '@rollup/plugin-node-resolve': 13.0.5_rollup@2.57.0
'@types/compression': 1.7.2 '@types/compression': 1.7.2
'@types/cross-spawn': 6.0.2
'@types/debug': 4.1.7 '@types/debug': 4.1.7
'@types/fs-extra': 9.0.13 '@types/fs-extra': 9.0.13
'@types/koa': 2.13.4 '@types/koa': 2.13.4
@ -88,6 +91,7 @@ importers:
chokidar: 3.5.2 chokidar: 3.5.2
compression: 1.7.4 compression: 1.7.4
conventional-changelog-cli: 2.1.1 conventional-changelog-cli: 2.1.1
cross-spawn: 7.0.3
debug: 4.3.2 debug: 4.3.2
diacritics: 1.3.0 diacritics: 1.3.0
enquirer: 2.3.6 enquirer: 2.3.6
@ -544,6 +548,12 @@ packages:
'@types/node': 15.14.9 '@types/node': 15.14.9
dev: true dev: true
/@types/cross-spawn/6.0.2:
resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==}
dependencies:
'@types/node': 15.14.9
dev: true
/@types/debug/4.1.7: /@types/debug/4.1.7:
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
dependencies: dependencies:

@ -9,7 +9,6 @@ const currentVersion = require('../package.json').version
const versionIncrements = ['patch', 'minor', 'major'] const versionIncrements = ['patch', 'minor', 'major']
const inc = (i) => semver.inc(currentVersion, i) const inc = (i) => semver.inc(currentVersion, i)
const bin = (name) => path.resolve(__dirname, `../node_modules/.bin/${name}`)
const run = (bin, args, opts = {}) => const run = (bin, args, opts = {}) =>
execa(bin, args, { stdio: 'inherit', ...opts }) execa(bin, args, { stdio: 'inherit', ...opts })
const step = (msg) => console.log(chalk.cyan(msg)) const step = (msg) => console.log(chalk.cyan(msg))

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted, watchEffect } from 'vue'
import { useData } from 'vitepress' import { useData } from 'vitepress'
const { theme, page } = useData() const { theme, page } = useData()
@ -17,9 +17,11 @@ const prefix = computed(() => {
const datetime = ref('') const datetime = ref('')
onMounted(() => { onMounted(() => {
// locale string might be different based on end user watchEffect(() => {
// and will lead to potential hydration mismatch if calculated at build time // locale string might be different based on end user
datetime.value = new Date(page.value.lastUpdated).toLocaleString('en-US') // and will lead to potential hydration mismatch if calculated at build time
datetime.value = new Date(page.value.lastUpdated!).toLocaleString('en-US')
})
}) })
</script> </script>

@ -1,6 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import EditLink from './EditLink.vue' import EditLink from './EditLink.vue'
import LastUpdated from './LastUpdated.vue' import LastUpdated from './LastUpdated.vue'
import { useData } from 'vitepress'
const { page } = useData()
</script> </script>
<template> <template>
@ -9,7 +12,7 @@ import LastUpdated from './LastUpdated.vue'
<EditLink /> <EditLink />
</div> </div>
<div class="updated"> <div class="updated">
<LastUpdated /> <LastUpdated v-if="page.lastUpdated" />
</div> </div>
</footer> </footer>
</template> </template>

@ -37,6 +37,7 @@ export interface UserConfig<ThemeConfig = any> {
themeConfig?: ThemeConfig themeConfig?: ThemeConfig
locales?: Record<string, LocaleConfig> locales?: Record<string, LocaleConfig>
markdown?: MarkdownOptions markdown?: MarkdownOptions
lastUpdated?: boolean
/** /**
* Options to pass on to `@vitejs/plugin-vue` * Options to pass on to `@vitejs/plugin-vue`
*/ */
@ -72,7 +73,7 @@ export type RawConfigExports<ThemeConfig = any> =
export interface SiteConfig<ThemeConfig = any> export interface SiteConfig<ThemeConfig = any>
extends Pick< extends Pick<
UserConfig, UserConfig,
'markdown' | 'vue' | 'vite' | 'shouldPreload' | 'mpa' 'markdown' | 'vue' | 'vite' | 'shouldPreload' | 'mpa' | 'lastUpdated'
> { > {
root: string root: string
srcDir: string srcDir: string
@ -145,6 +146,7 @@ export async function resolveConfig(
outDir, outDir,
tempDir: resolve(root, '.temp'), tempDir: resolve(root, '.temp'),
markdown: userConfig.markdown, markdown: userConfig.markdown,
lastUpdated: userConfig.lastUpdated,
alias: resolveAliases(root, themeDir), alias: resolveAliases(root, themeDir),
vue: userConfig.vue, vue: userConfig.vue,
vite: userConfig.vite, vite: userConfig.vite,

@ -8,6 +8,7 @@ import { PageData, HeadConfig, EXTERNAL_URL_RE } from './shared'
import { slash } from './utils/slash' import { slash } from './utils/slash'
import chalk from 'chalk' import chalk from 'chalk'
import _debug from 'debug' import _debug from 'debug'
import { getGitTimestamp } from './utils/getGitTimestamp'
const debug = _debug('vitepress:md') const debug = _debug('vitepress:md')
const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 }) const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 })
@ -25,7 +26,8 @@ export function createMarkdownToVueRenderFn(
options: MarkdownOptions = {}, options: MarkdownOptions = {},
pages: string[], pages: string[],
userDefines: Record<string, any> | undefined, userDefines: Record<string, any> | undefined,
isBuild = false isBuild = false,
includeLastUpdatedData = false
) { ) {
const md = createMarkdownRenderer(srcDir, options) const md = createMarkdownRenderer(srcDir, options)
pages = pages.map((p) => slash(p.replace(/\.md$/, ''))) pages = pages.map((p) => slash(p.replace(/\.md$/, '')))
@ -39,11 +41,11 @@ export function createMarkdownToVueRenderFn(
) )
: null : null
return ( return async (
src: string, src: string,
file: string, file: string,
publicDir: string publicDir: string
): MarkdownCompileResult => { ): Promise<MarkdownCompileResult> => {
const relativePath = slash(path.relative(srcDir, file)) const relativePath = slash(path.relative(srcDir, file))
const dir = path.dirname(file) const dir = path.dirname(file)
@ -132,9 +134,11 @@ export function createMarkdownToVueRenderFn(
description: inferDescription(frontmatter), description: inferDescription(frontmatter),
frontmatter, frontmatter,
headers: data.headers || [], headers: data.headers || [],
relativePath, relativePath
// TODO use git timestamp? }
lastUpdated: Math.round(fs.statSync(file).mtimeMs)
if (includeLastUpdatedData) {
pageData.lastUpdated = await getGitTimestamp(file)
} }
const vueSrc = const vueSrc =

@ -1,10 +1,7 @@
import path from 'path' import path from 'path'
import { defineConfig, mergeConfig, Plugin, ResolvedConfig } from 'vite' import { defineConfig, mergeConfig, Plugin, ResolvedConfig } from 'vite'
import { SiteConfig, resolveSiteData } from './config' import { SiteConfig, resolveSiteData } from './config'
import { import { createMarkdownToVueRenderFn } from './markdownToVue'
createMarkdownToVueRenderFn,
MarkdownCompileResult
} from './markdownToVue'
import { DIST_CLIENT_PATH, APP_PATH, SITE_DATA_REQUEST_PATH } from './alias' import { DIST_CLIENT_PATH, APP_PATH, SITE_DATA_REQUEST_PATH } from './alias'
import { slash } from './utils/slash' import { slash } from './utils/slash'
import { OutputAsset, OutputChunk } from 'rollup' import { OutputAsset, OutputChunk } from 'rollup'
@ -49,11 +46,7 @@ export function createVitePressPlugin(
pages pages
} = siteConfig } = siteConfig
let markdownToVue: ( let markdownToVue: ReturnType<typeof createMarkdownToVueRenderFn>
src: string,
file: string,
publicDir: string
) => MarkdownCompileResult
// lazy require plugin-vue to respect NODE_ENV in @vue/compiler-x // lazy require plugin-vue to respect NODE_ENV in @vue/compiler-x
const vuePlugin = require('@vitejs/plugin-vue')({ const vuePlugin = require('@vitejs/plugin-vue')({
@ -84,7 +77,8 @@ export function createVitePressPlugin(
markdown, markdown,
pages, pages,
config.define, config.define,
config.command === 'build' config.command === 'build',
siteConfig.lastUpdated
) )
}, },
@ -131,12 +125,12 @@ export function createVitePressPlugin(
} }
}, },
transform(code, id) { async transform(code, id) {
if (id.endsWith('.vue')) { if (id.endsWith('.vue')) {
return processClientJS(code, id) return processClientJS(code, id)
} else if (id.endsWith('.md')) { } else if (id.endsWith('.md')) {
// transform .md files into vueSrc so plugin-vue can handle it // transform .md files into vueSrc so plugin-vue can handle it
const { vueSrc, deadLinks, includes } = markdownToVue( const { vueSrc, deadLinks, includes } = await markdownToVue(
code, code,
id, id,
config.publicDir config.publicDir
@ -257,7 +251,7 @@ export function createVitePressPlugin(
// hot reload .md files as .vue files // hot reload .md files as .vue files
if (file.endsWith('.md')) { if (file.endsWith('.md')) {
const content = await read() const content = await read()
const { pageData, vueSrc } = markdownToVue( const { pageData, vueSrc } = await markdownToVue(
content, content,
file, file,
config.publicDir config.publicDir

@ -0,0 +1,13 @@
import { spawn } from 'cross-spawn'
export function getGitTimestamp(file: string) {
return new Promise<number>((resolve, reject) => {
const child = spawn('git', ['log', '-1', '--pretty="%ci"', file])
let output = ''
child.stdout.on('data', (d) => (output += String(d)))
child.on('close', () => {
resolve(+new Date(output))
})
child.on('error', reject)
})
}

2
types/shared.d.ts vendored

@ -8,7 +8,7 @@ export interface PageData {
description: string description: string
headers: Header[] headers: Header[]
frontmatter: Record<string, any> frontmatter: Record<string, any>
lastUpdated: number lastUpdated?: number
} }
export interface SiteData<ThemeConfig = any> { export interface SiteData<ThemeConfig = any> {

Loading…
Cancel
Save