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 2 years ago
parent b404c6671d
commit d32d8d4419

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

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

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

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

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

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

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

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

Loading…
Cancel
Save