Skip to content

Commit a873564

Browse files
committed
feat: use hashed page file names
1 parent b61e239 commit a873564

File tree

5 files changed

+89
-31
lines changed

5 files changed

+89
-31
lines changed

lib/app/index.js

+17-6
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,23 @@ export function createApp() {
5656
} else {
5757
// in production, each .md file is built into a .md.js file following
5858
// the path conversion scheme.
59-
// /foo/bar.html -> /js/foo_bar.md.js
60-
const useLeanBuild = isInitialPageLoad || initialPath === pagePath
61-
pagePath =
62-
(inBrowser ? __BASE__ + '_assets/' : './') +
63-
pagePath.slice(inBrowser ? __BASE__.length : 1).replace(/\//g, '_') +
64-
(useLeanBuild ? '.md.lean.js' : '.md.js')
59+
// /foo/bar.html -> ./foo_bar.md
60+
61+
if (inBrowser) {
62+
pagePath = pagePath.slice(__BASE__.length).replace(/\//g, '_') + '.md'
63+
// client production build needs to account for page hash, which is
64+
// injected directly in the page's html
65+
const pageHash = __VP_HASH_MAP__[pagePath]
66+
// use lean build if this is the initial page load or navigating back
67+
// to the initial loaded path (the static vnodes already adopted the
68+
// static content on that load so no need to re-fetch the page)
69+
const ext =
70+
isInitialPageLoad || initialPath === pagePath ? 'lean.js' : 'js'
71+
pagePath = `${__BASE__}_assets/${pagePath}.${pageHash}.${ext}`
72+
} else {
73+
// ssr build uses much simpler name mapping
74+
pagePath = `./${pagePath.slice(1).replace(/\//g, '_')}.md.js`
75+
}
6576
}
6677

6778
if (inBrowser) {

lib/shim.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
declare const __DEV__: boolean
22
declare const __BASE__: string
3+
declare const __VP_HASH_MAP__: Record<string, string>
34

45
declare module '*.vue' {
56
import { ComponentOptions } from 'vue'

src/build/build.ts

+25-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { bundle } from './bundle'
33
import { BuildConfig as ViteBuildOptions } from 'vite'
44
import { resolveConfig } from '../config'
55
import { renderPage } from './render'
6+
import { OutputChunk } from 'rollup'
67

78
export type BuildOptions = Pick<
89
ViteBuildOptions,
@@ -17,10 +18,32 @@ export const ASSETS_DIR = '_assets/'
1718
export async function build(buildOptions: BuildOptions = {}) {
1819
const siteConfig = await resolveConfig(buildOptions.root)
1920
try {
20-
const [clientResult] = await bundle(siteConfig, buildOptions)
21+
const [clientResult, , pageToHashMap] = await bundle(
22+
siteConfig,
23+
buildOptions
24+
)
2125
console.log('rendering pages...')
26+
27+
const indexChunk = clientResult.assets.find(
28+
(chunk) =>
29+
chunk.type === 'chunk' && chunk.fileName.match(/^index\.\w+\.js$/)
30+
) as OutputChunk
31+
32+
// We embed the hash map string into each page directly so that it doesn't
33+
// alter the main chunk's hash on every build. It's also embedded as a
34+
// string and JSON.parsed from the client because it's faster than embedding
35+
// as JS object literal.
36+
const hashMapStirng = JSON.stringify(JSON.stringify(pageToHashMap))
37+
2238
for (const page of siteConfig.pages) {
23-
await renderPage(siteConfig, page, clientResult)
39+
await renderPage(
40+
siteConfig,
41+
page,
42+
clientResult,
43+
indexChunk,
44+
pageToHashMap,
45+
hashMapStirng
46+
)
2447
}
2548
} finally {
2649
await fs.remove(siteConfig.tempDir)

src/build/bundle.ts

+24-9
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@ const isPageChunk = (
3131
export async function bundle(
3232
config: SiteConfig,
3333
options: BuildOptions
34-
): Promise<BuildResult[]> {
34+
): Promise<[BuildResult, BuildResult, Record<string, string>]> {
3535
const root = config.root
3636
const resolver = createResolver(config.themeDir)
3737
const markdownToVue = createMarkdownToVueRenderFn(root)
3838

3939
let isClientBuild = true
40+
const pageToHashMap = Object.create(null)
4041

4142
const VitePressPlugin: Plugin = {
4243
name: 'vitepress',
@@ -82,13 +83,18 @@ export async function bundle(
8283
const chunk = bundle[name]
8384
if (isPageChunk(chunk)) {
8485
// foo/bar.md -> foo_bar.md.js
85-
chunk.fileName =
86-
slash(path.relative(root, chunk.facadeModuleId)).replace(
87-
/\//g,
88-
'_'
89-
) + '.js'
86+
const hash = isClientBuild
87+
? chunk.fileName.match(/\.(\w+)\.js$/)![1]
88+
: ``
89+
const pageName = slash(
90+
path.relative(root, chunk.facadeModuleId)
91+
).replace(/\//g, '_')
92+
chunk.fileName = `${pageName}${hash ? `.${hash}` : ``}.js`
9093

9194
if (isClientBuild) {
95+
// record page -> hash relations
96+
pageToHashMap[pageName] = hash
97+
9298
// inject another chunk with the content stripped
9399
bundle[name + '-lean'] = {
94100
...chunk,
@@ -131,13 +137,22 @@ export async function bundle(
131137
preserveEntrySignatures: 'allow-extension',
132138
plugins: [VitePressPlugin, ...(rollupInputOptions.plugins || [])]
133139
},
134-
rollupOutputOptions,
140+
rollupOutputOptions: {
141+
...rollupOutputOptions,
142+
chunkFileNames: `common-[hash].js`
143+
},
135144
silent: !process.env.DEBUG,
136145
minify: !process.env.DEBUG
137146
}
138147

139148
console.log('building client bundle...')
140-
const clientResult = await build(viteOptions)
149+
const clientResult = await build({
150+
...viteOptions,
151+
rollupOutputOptions: {
152+
...viteOptions.rollupOutputOptions,
153+
entryFileNames: `[name].[hash].js`
154+
}
155+
})
141156

142157
console.log('building server bundle...')
143158
isClientBuild = false
@@ -146,5 +161,5 @@ export async function bundle(
146161
outDir: config.tempDir
147162
})
148163

149-
return [clientResult, serverResult]
164+
return [clientResult, serverResult, pageToHashMap]
150165
}

src/build/render.ts

+22-14
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ const escape = require('escape-html')
1111
export async function renderPage(
1212
config: SiteConfig,
1313
page: string, // foo.md
14-
result: BuildResult
14+
result: BuildResult,
15+
indexChunk: OutputChunk,
16+
pageToHashMap: Record<string, string>,
17+
hashMapStirng: string
1518
) {
1619
const { createApp } = require(path.join(
1720
config.tempDir,
@@ -23,27 +26,30 @@ export async function renderPage(
2326
router.go(routePath)
2427
const content = await renderToString(app)
2528

26-
const pageJsFileName = page.replace(/\//g, '_') + '.js'
29+
const pageName = page.replace(/\//g, '_')
30+
// server build doesn't need hash
31+
const pageServerJsFileName = pageName + '.js'
32+
// for any initial page load, we only need the lean version of the page js
33+
// since the static content is already on the page!
34+
const pageHash = pageToHashMap[pageName]
35+
const pageClientJsFileName = pageName + `.` + pageHash + '.lean.js'
2736

2837
// resolve page data so we can render head tags
2938
const { __pageData } = require(path.join(
3039
config.tempDir,
3140
ASSETS_DIR,
32-
pageJsFileName
41+
pageServerJsFileName
3342
))
3443
const pageData = JSON.parse(__pageData)
3544

3645
const assetPath = `${config.site.base}${ASSETS_DIR}`
37-
3846
const preloadLinks = [
3947
// resolve imports for index.js + page.md.js and inject script tags for
4048
// them as well so we fetch everything as early as possible without having
4149
// to wait for entry chunks to parse
42-
...resolvePageImports(config, page, result),
43-
// for any initial page load, we only need the lean version of the page js
44-
// since the static content is already on the page!
45-
pageJsFileName.replace(/\.js$/, '.lean.js'),
46-
'index.js'
50+
...resolvePageImports(config, page, result, indexChunk),
51+
pageClientJsFileName,
52+
indexChunk.fileName
4753
]
4854
.map((file) => {
4955
return `<link rel="modulepreload" href="${assetPath}${file}">`
@@ -64,7 +70,10 @@ export async function renderPage(
6470
</head>
6571
<body>
6672
<div id="app">${content}</div>
67-
<script type="module" async src="${assetPath}index.js"></script>
73+
<script>__VP_HASH_MAP__ = JSON.parse(${hashMapStirng})</script>
74+
<script type="module" async src="${assetPath}${
75+
indexChunk.fileName
76+
}"></script>
6877
</body>
6978
</html>`.trim()
7079
const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html'))
@@ -75,13 +84,12 @@ export async function renderPage(
7584
function resolvePageImports(
7685
config: SiteConfig,
7786
page: string,
78-
result: BuildResult
87+
result: BuildResult,
88+
indexChunk: OutputChunk
7989
) {
8090
// find the page's js chunk and inject script tags for its imports so that
8191
// they are start fetching as early as possible
82-
const indexChunk = result.assets.find(
83-
(chunk) => chunk.type === 'chunk' && chunk.fileName === `index.js`
84-
) as OutputChunk
92+
8593
const srcPath = path.join(config.root, page)
8694
const pageChunk = result.assets.find(
8795
(chunk) => chunk.type === 'chunk' && chunk.facadeModuleId === srcPath

0 commit comments

Comments
 (0)