pull/5144/merge
Yuxuan Zhang 1 month ago committed by GitHub
commit 6c4ec714ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -100,6 +100,7 @@
"@docsearch/js": "^4.5.3",
"@docsearch/sidepanel-js": "^4.5.3",
"@iconify-json/simple-icons": "^1.2.69",
"@noble/hashes": "^2.0.1",
"@shikijs/core": "^3.22.0",
"@shikijs/transformers": "^3.22.0",
"@shikijs/types": "^3.22.0",

@ -38,6 +38,9 @@ importers:
'@iconify-json/simple-icons':
specifier: ^1.2.69
version: 1.2.69
'@noble/hashes':
specifier: ^2.0.1
version: 2.0.1
'@shikijs/core':
specifier: ^3.22.0
version: 3.22.0
@ -52,7 +55,7 @@ importers:
version: 14.1.2
'@vitejs/plugin-vue':
specifier: ^6.0.4
version: 6.0.4(rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(jiti@1.21.7)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))
version: 6.0.4(rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))
'@vue/devtools-api':
specifier: ^8.0.5
version: 8.0.5
@ -79,7 +82,7 @@ importers:
version: 3.22.0
vite:
specifier: npm:rolldown-vite@latest
version: rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(jiti@1.21.7)(yaml@2.8.2)
version: rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(yaml@2.8.2)
vue:
specifier: ^3.5.27
version: 3.5.27(typescript@5.9.3)
@ -302,7 +305,7 @@ importers:
version: 5.9.3
vitest:
specifier: 4.0.0-beta.4
version: 4.0.0-beta.4(@types/debug@4.1.12)(@types/node@25.2.0)(esbuild@0.27.2)(jiti@1.21.7)(yaml@2.8.2)
version: 4.0.0-beta.4(@types/node@25.2.0)(esbuild@0.27.2)(yaml@2.8.2)
vue-tsc:
specifier: ^3.2.4
version: 3.2.4(typescript@5.9.3)
@ -659,6 +662,10 @@ packages:
'@napi-rs/wasm-runtime@1.1.1':
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
'@noble/hashes@2.0.1':
resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==}
engines: {node: '>= 20.19.0'}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -3445,6 +3452,8 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
'@noble/hashes@2.0.1': {}
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -3847,10 +3856,10 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@vitejs/plugin-vue@6.0.4(rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(jiti@1.21.7)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))':
'@vitejs/plugin-vue@6.0.4(rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.2
vite: rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(jiti@1.21.7)(yaml@2.8.2)
vite: rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(yaml@2.8.2)
vue: 3.5.27(typescript@5.9.3)
'@vitest/expect@4.0.0-beta.4':
@ -3861,13 +3870,13 @@ snapshots:
chai: 5.3.3
tinyrainbow: 2.0.0
'@vitest/mocker@4.0.0-beta.4(rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(jiti@1.21.7)(yaml@2.8.2))':
'@vitest/mocker@4.0.0-beta.4(rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(yaml@2.8.2))':
dependencies:
'@vitest/spy': 4.0.0-beta.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(jiti@1.21.7)(yaml@2.8.2)
vite: rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(yaml@2.8.2)
'@vitest/pretty-format@4.0.0-beta.4':
dependencies:
@ -5392,6 +5401,21 @@ snapshots:
jiti: 1.21.7
yaml: 2.8.2
rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(yaml@2.8.2):
dependencies:
'@oxc-project/runtime': 0.101.0
fdir: 6.5.0(picomatch@4.0.3)
lightningcss: 1.31.1
picomatch: 4.0.3
postcss: 8.5.6
rolldown: 1.0.0-beta.53
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 25.2.0
esbuild: 0.27.2
fsevents: 2.3.3
yaml: 2.8.2
rolldown@1.0.0-beta.53:
dependencies:
'@oxc-project/types': 0.101.0
@ -5786,13 +5810,13 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vite-node@4.0.0-beta.4(@types/node@25.2.0)(esbuild@0.27.2)(jiti@1.21.7)(yaml@2.8.2):
vite-node@4.0.0-beta.4(@types/node@25.2.0)(esbuild@0.27.2)(yaml@2.8.2):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(jiti@1.21.7)(yaml@2.8.2)
vite: rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(yaml@2.8.2)
transitivePeerDependencies:
- '@types/node'
- esbuild
@ -5845,11 +5869,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
vitest@4.0.0-beta.4(@types/debug@4.1.12)(@types/node@25.2.0)(esbuild@0.27.2)(jiti@1.21.7)(yaml@2.8.2):
vitest@4.0.0-beta.4(@types/node@25.2.0)(esbuild@0.27.2)(yaml@2.8.2):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 4.0.0-beta.4
'@vitest/mocker': 4.0.0-beta.4(rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(jiti@1.21.7)(yaml@2.8.2))
'@vitest/mocker': 4.0.0-beta.4(rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(yaml@2.8.2))
'@vitest/pretty-format': 4.0.18
'@vitest/runner': 4.0.0-beta.4
'@vitest/snapshot': 4.0.0-beta.4
@ -5867,11 +5891,10 @@ snapshots:
tinyglobby: 0.2.15
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(jiti@1.21.7)(yaml@2.8.2)
vite-node: 4.0.0-beta.4(@types/node@25.2.0)(esbuild@0.27.2)(jiti@1.21.7)(yaml@2.8.2)
vite: rolldown-vite@7.3.1(@types/node@25.2.0)(esbuild@0.27.2)(yaml@2.8.2)
vite-node: 4.0.0-beta.4(@types/node@25.2.0)(esbuild@0.27.2)(yaml@2.8.2)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/debug': 4.1.12
'@types/node': 25.2.0
transitivePeerDependencies:
- esbuild

@ -65,8 +65,11 @@ export function usePrefetch() {
const { pathname } = link
if (!hasFetched.has(pathname)) {
hasFetched.add(pathname)
const pageChunkPath = pathToFile(pathname)
if (pageChunkPath) doFetch(pageChunkPath)
try {
doFetch(pathToFile(pathname))
} catch (e) {
// Silently fail prefetch errors.
}
}
}
})

@ -121,22 +121,18 @@ function newRouter(): Router {
let isInitialPageLoad = inBrowser
return createRouter((path) => {
let pageFilePath = pathToFile(path)
let pageModule = null
if (pageFilePath) {
try {
// use lean build if this is the initial page load
if (isInitialPageLoad) {
pageFilePath = pageFilePath.replace(/\.js$/, '.lean.js')
}
const chunkPath = pathToFile(path, isInitialPageLoad ? '.lean.js' : '.js')
// Client build always use lean build
if (inBrowser) isInitialPageLoad = false
if (import.meta.env.DEV) {
pageModule = import(/*@vite-ignore*/ pageFilePath).catch((e) => {
return import(/*@vite-ignore*/ chunkPath).catch((e) => {
// page load could fail for other reasons, don't swallow
console.error(e)
// try with/without trailing slash
// in prod this is handled in src/client/app/utils.ts#pathToFile
const url = new URL(pageFilePath!, 'http://a.com')
const url = new URL(chunkPath!, 'http://a.com')
const path =
(url.pathname.endsWith('/index.md')
? url.pathname.slice(0, -9) + '.md'
@ -146,15 +142,14 @@ function newRouter(): Router {
return import(/*@vite-ignore*/ path)
})
} else {
pageModule = import(/*@vite-ignore*/ pageFilePath)
return import(/*@vite-ignore*/ chunkPath)
}
} catch (e) {
// let 404 page load failure fall through
if (!/\/404(\.md|\.html|\/)?$/i.test(path))
console.error(`Failed to load page module for ${path}:`, e)
return null
}
if (inBrowser) {
isInitialPageLoad = false
}
return pageModule
}, Theme.NotFound)
}

@ -3,8 +3,8 @@ import { h, onMounted, shallowRef, type AsyncComponentLoader } from 'vue'
import {
EXTERNAL_URL_RE,
inBrowser,
sanitizeFileName,
type Awaitable
type Awaitable,
resolveChunkKeys
} from '../shared'
import { siteDataRef } from './data'
@ -26,46 +26,22 @@ export function withBase(path: string) {
: joinPath(siteDataRef.value.base, path)
}
/**
* Converts a url path to the corresponding js chunk filename.
*/
export function pathToFile(path: string) {
let pagePath = path.replace(/\.html$/, '')
pagePath = decodeURIComponent(pagePath)
pagePath = pagePath.replace(/\/$/, '/index') // /foo/ -> /foo/index
if (import.meta.env.DEV) {
// always force re-fetch content in dev
pagePath += `.md?t=${Date.now()}`
} else {
// in production, each .md file is built into a .md.js file following
// the path conversion scheme.
// /foo/bar.html -> ./foo_bar.md
if (inBrowser) {
const base = import.meta.env.BASE_URL
pagePath =
sanitizeFileName(
pagePath.slice(base.length).replace(/\//g, '_') || 'index'
) + '.md'
// client production build needs to account for page hash, which is
// injected directly in the page's html
let pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()]
if (!pageHash) {
pagePath = pagePath.endsWith('_index.md')
? pagePath.slice(0, -9) + '.md'
: pagePath.slice(0, -3) + '_index.md'
pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()]
}
if (!pageHash) return null
pagePath = `${base}${__ASSETS_DIR__}/${pagePath}.${pageHash}.js`
export function pathToFile(path: string, suffix: string = '.js') {
if (inBrowser) {
if (import.meta.env.DEV) {
// In dev server, always force re-fetch content
path = path.replace(/\/$/, '/index').replace(/\.html$/i, '')
return `${path}.md?t=${Date.now()}`
} else {
// ssr build uses much simpler name mapping
pagePath = `./${sanitizeFileName(
pagePath.slice(1).replace(/\//g, '_')
)}.md.js`
// in production, each .md file is built into [assetKey].[hash].js
const base = import.meta.env.BASE_URL
const { assetKey, hash } = resolveChunkKeys(path.slice(base.length), true)
return `${base}${__ASSETS_DIR__}/${assetKey}.${hash}${suffix}`
}
} else {
const { assetKey } = resolveChunkKeys(path, true)
return `./${assetKey}${suffix}`
}
return pagePath
}
export let contentUpdatedCallbacks: (() => any)[] = []

@ -66,7 +66,7 @@ export async function build(
}
try {
const { clientResult, serverResult, pageToHashMap } = await bundle(
const { clientResult, serverResult, hashmap } = await bundle(
siteConfig,
buildOptions
)
@ -75,6 +75,9 @@ export async function build(
return
}
// Expose the hash map for SSR `resolveChunkKeys()` to lookup chunk file
;(globalThis as any).__VP_HASH_MAP__ = hashmap
const entryPath = path.join(siteConfig.tempDir, 'app.js')
const { render } = await import(pathToFileURL(entryPath).href)
@ -113,7 +116,7 @@ export async function build(
chunk.moduleIds.some((id) => id.includes('client/theme-default'))
)
const metadataScript = generateMetadataScript(pageToHashMap, siteConfig)
const metadataScript = generateMetadataScript(hashmap, siteConfig)
if (isDefaultTheme) {
const fontURL = assets.find((file) =>
@ -146,7 +149,7 @@ export async function build(
appChunk,
cssChunk,
assets,
pageToHashMap,
hashmap,
metadataScript,
additionalHeadTags,
usedIcons
@ -169,10 +172,7 @@ export async function build(
// emit page hash map for the case where a user session is open
// when the site got redeployed (which invalidates current hash map)
fs.writeJSONSync(
path.join(siteConfig.outDir, 'hashmap.json'),
pageToHashMap
)
fs.writeJSONSync(path.join(siteConfig.outDir, 'hashmap.json'), hashmap)
} finally {
unlinkVue()
if (!process.env.DEBUG) {
@ -210,7 +210,7 @@ function linkVue() {
}
function generateMetadataScript(
pageToHashMap: Record<string, string>,
hashmap: Record<string, string>,
config: SiteConfig
) {
if (config.mpa) {
@ -221,7 +221,7 @@ function generateMetadataScript(
// so that it doesn't alter the main chunk's hash on every build.
// It's also embedded as a string and JSON.parsed from the client because
// it's faster than embedding as JS object literal.
const hashMapString = JSON.stringify(JSON.stringify(pageToHashMap))
const hashMapString = JSON.stringify(JSON.stringify(hashmap))
const siteDataString = JSON.stringify(
JSON.stringify(serializeFunctions({ ...config.site, head: [] }))
)
@ -235,7 +235,6 @@ function generateMetadataScript(
if (!config.metaChunk) {
return { html: `<script>${metadataContent}</script>`, inHead: false }
}
const metadataFile = path.join(
config.assetsDir,
'chunks',

@ -12,7 +12,12 @@ import {
import { APP_PATH } from '../alias'
import type { SiteConfig } from '../config'
import { createVitePressPlugin } from '../plugin'
import { escapeRegExp, sanitizeFileName, slash } from '../shared'
import {
escapeRegExp,
canonicalize,
hashKeys,
sanitizeFileName
} from '../shared'
import { task } from '../utils/task'
import { buildMPAClient } from './buildMPAClient'
@ -41,9 +46,10 @@ export async function bundle(
): Promise<{
clientResult: Rollup.RollupOutput | null
serverResult: Rollup.RollupOutput
pageToHashMap: Record<string, string>
hashmap: Record<string, string>
}> {
const pageToHashMap = Object.create(null) as Record<string, string>
const assetKeyToHashMap = Object.create(null) as Record<string, string>
const assetKeyToLookupKeyMap = Object.create(null) as Record<string, string>
const clientJSMap = Object.create(null) as Record<string, string>
// define custom rollup input
@ -51,11 +57,22 @@ export async function bundle(
// the loading is done via filename conversion rules so that the
// metadata doesn't need to be included in the main chunk.
const input: Record<string, string> = {}
const assetKeys = new Set<string>()
const lookupKeys = new Set<string>()
config.pages.forEach((file) => {
// page filename conversion
// foo/bar.md -> foo_bar.md
// 'foo/bar.md' -> hash('foo/bar.md') -> xxxxx
const alias = config.rewrites.map[file] || file
input[slash(alias).replace(/\//g, '_')] = path.resolve(config.srcDir, file)
const canonical = canonicalize(alias, config.caseSensitive)
const { assetKey, lookupKey } = hashKeys(canonical)
if (assetKeys.has(assetKey) || lookupKeys.has(lookupKey))
throw new Error(
`Hash collision detected for page ${file} (assetKey: ${assetKey}, lookupKey: ${lookupKey}). Consider enabling caseSensitive option or changing the file name.`
)
assetKeys.add(assetKey)
lookupKeys.add(lookupKey)
assetKeyToLookupKeyMap[assetKey] = lookupKey
input[assetKey] = path.resolve(config.srcDir, file)
})
const themeEntryRE = new RegExp(
@ -77,7 +94,7 @@ export async function bundle(
plugins: await createVitePressPlugin(
config,
ssr,
pageToHashMap,
assetKeyToHashMap,
clientJSMap
),
ssr: {
@ -206,15 +223,22 @@ export async function bundle(
}
}
// sort pageToHashMap to ensure stable output
const sortedPageToHashMap = Object.create(null) as Record<string, string>
Object.keys(pageToHashMap)
.sort()
.forEach((key) => {
sortedPageToHashMap[key] = pageToHashMap[key]
// sorted hashmap to ensure stable output
const hashmap = Object.create(null) as Record<string, string>
// Sorting by private asset key could reveal extra information for hash
// collision attack, so we sort by page hash instead, which is public information.
Object.entries(assetKeyToHashMap)
.sort((a, b) => a[1].localeCompare(b[1]))
.forEach(([assetKey, hash]) => {
if (assetKey in assetKeyToLookupKeyMap) {
const lookupKey = assetKeyToLookupKeyMap[assetKey]
hashmap[lookupKey] = hash
} else {
throw new Error(`Cannot find lookupKey for assetKey ${assetKey}`)
}
})
return { clientResult, serverResult, pageToHashMap: sortedPageToHashMap }
return { clientResult, serverResult, hashmap }
}
const cache = new Map<string, boolean>()

@ -13,13 +13,31 @@ import {
mergeHead,
notFoundPageData,
resolveSiteDataByRoute,
sanitizeFileName,
resolveChunkKeys,
slash,
type HeadConfig,
type PageData,
type SSGContext
} from '../shared'
async function loadPageData(
config: SiteConfig,
page: string,
hashmap: Record<string, string>
) {
try {
const keys = resolveChunkKeys(page, config.caseSensitive, hashmap)
// server build doesn't need hash
const ssrChunkPath = keys.assetKey + '.js'
// resolve page data so we can render head tags
const src = pathToFileURL(path.join(config.tempDir, ssrChunkPath)).href
const { __pageData: pageData } = await import(src)
return { pageData, keys }
} catch (e) {
if (page === '404.md') return { pageData: notFoundPageData, keys: null }
else throw e
}
}
export async function renderPage(
render: (path: string) => Promise<SSGContext>,
config: SiteConfig,
@ -28,7 +46,7 @@ export async function renderPage(
appChunk: Rollup.OutputChunk | null,
cssChunk: Rollup.OutputAsset | null,
assets: string[],
pageToHashMap: Record<string, string>,
hashmap: Record<string, string>,
metadataScript: { html: string; inHead: boolean },
additionalHeadTags: HeadConfig[],
usedIcons: Set<string>
@ -44,31 +62,8 @@ export async function renderPage(
// add used social icons to the set
vpSocialIcons.forEach((icon) => usedIcons.add(icon))
const pageName = sanitizeFileName(page.replace(/\//g, '_'))
// server build doesn't need hash
const pageServerJsFileName = pageName + '.js'
// for any initial page load, we only need the lean version of the page js
// since the static content is already on the page!
const pageHash = pageToHashMap[pageName.toLowerCase()]
const pageClientJsFileName = `${config.assetsDir}/${pageName}.${pageHash}.lean.js`
let pageData: PageData
let hasCustom404 = true
try {
// resolve page data so we can render head tags
const { __pageData } = await import(
pathToFileURL(path.join(config.tempDir, pageServerJsFileName)).href
)
pageData = __pageData
} catch (e) {
if (page === '404.md') {
hasCustom404 = false
pageData = notFoundPageData
} else {
throw e
}
}
const { pageData, keys } = await loadPageData(config, page, hashmap)
const isDefault404 = keys === null
const title: string = createTitle(siteData, pageData)
const description: string = pageData.description || siteData.description
@ -77,7 +72,7 @@ export async function renderPage(
: ''
let preloadLinks =
config.mpa || (!hasCustom404 && page === '404.md')
config.mpa || isDefault404
? []
: result && appChunk
? [
@ -86,7 +81,9 @@ export async function renderPage(
// 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
// for any initial page load, we only need the lean version of the page js
// since the static content is already on the page!
`${config.assetsDir}/${keys.assetKey}.${keys.hash}.lean.js`
])
]
: []

@ -131,6 +131,7 @@ export async function resolveConfig(
const config: Omit<SiteConfig, 'pages' | 'dynamicRoutes' | 'rewrites'> = {
root,
srcDir,
caseSensitive: userConfig.caseSensitive ?? false,
assetsDir,
site,
themeDir,

@ -68,7 +68,7 @@ const cleanUrl = (url: string): string => url.replace(/[?#].*$/s, '')
export async function createVitePressPlugin(
siteConfig: SiteConfig,
ssr = false,
pageToHashMap?: Record<string, string>,
assetKeyToHashMap?: Record<string, string>,
clientJSMap?: Record<string, string>,
restartServer?: () => Promise<void>
) {
@ -321,7 +321,7 @@ export async function createVitePressPlugin(
if (isPageChunk(chunk)) {
// record page -> hash relations
const hash = chunk.fileName.match(hashRE)![1]
pageToHashMap![chunk.name.toLowerCase()] = hash
assetKeyToHashMap![chunk.name] = hash
// inject another chunk with the content stripped
this.emitFile({

@ -46,6 +46,7 @@ export interface UserConfig<
base?: string
srcDir?: string
caseSensitive?: boolean
srcExclude?: string[]
outDir?: string
assetsDir?: string
@ -230,6 +231,7 @@ export interface SiteConfig<ThemeConfig = any> extends Pick<
> {
root: string
srcDir: string
caseSensitive: boolean
site: SiteData<ThemeConfig>
configPath: string | undefined
configDeps: string[]

@ -4,6 +4,7 @@ import type {
PageData,
SiteData
} from '../../types/shared'
import { sha256 } from '@noble/hashes/sha2.js'
export type {
Awaitable,
@ -225,6 +226,93 @@ export function slash(p: string): string {
return p.replace(/\\/g, '/')
}
export function canonicalize(
path: string,
caseSensitive: boolean = false
): string {
const normalizedPath = slash(path).replace(/\.(html|md)$/i, '')
const segments = normalizedPath.split('/')
const stack: string[] = []
for (const segment of segments.filter((s) => s && s !== '.')) {
if (segment === '..') {
if (stack.length && stack[stack.length - 1] !== '..') stack.pop()
else stack.push('..')
} else {
stack.push(segment)
}
}
const canonical = stack.join('/')
return caseSensitive ? canonical : canonical.toLowerCase()
}
/**
* Create one-way mappings:
*
* Canonical Path -> assetKey -> lookupKey
* (Maybe Private) (Server Files) (Shared with Client)
*
* -------------------------------------------------------------------------
* Client must know the canonical path in order to resolve the valid chunk.
* In case the site contains unlisted private pages or assets, client cannot
* reverse map from lookupKey to assetKey or path name, avoiding unintentional
* content leaks.
* @param uid canonical path of the page, e.g. /foo/bar[.md|.html]
*/
export function hashKeys(uid: string): {
lookupKey: string
assetKey: string
} {
// ensure leading slash for consistent hashing
const input = new TextEncoder().encode(uid)
const assetHash = sha256(input)
const assetKey = [...assetHash.slice(0, 8)]
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
const lookupBuffer = sha256(assetHash)
const lookupKey = [...lookupBuffer.slice(0, 8)]
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
return { lookupKey, assetKey }
}
declare const __VP_HASH_MAP__: Record<string, string>
export class ChunkNotFoundError extends Error {
constructor(
public readonly path: string,
public readonly attempted: string[]
) {
super(
[
`Cannot resolve chunk for ${JSON.stringify(path)}. Attempted:`,
...(attempted || ['(no candidates)'])
].join('\n - ')
)
}
}
export function resolveChunkKeys(
path: string,
caseSensitive: boolean = false,
hashmap: Record<string, string> = __VP_HASH_MAP__
) {
const isDir = path === '' || path.endsWith('/')
if (isDir) path += 'index' // /foo/ -> /foo/index
const normalized = canonicalize(path, caseSensitive)
const candidates = [normalized]
// Attempt /foo/index if /foo doesn't exist
if (!isDir) candidates.push(normalized + '/index')
const attempted: string[] = []
for (const candidate of candidates) {
attempted.push(candidate)
const { lookupKey, assetKey } = hashKeys(candidate)
// ssr build uses much simpler name mapping
if (lookupKey in hashmap)
return { assetKey, lookupKey, hash: hashmap[lookupKey] }
}
throw new ChunkNotFoundError(path, attempted)
}
const KNOWN_EXTENSIONS = new Set()
export function treatAsHtml(filename: string): boolean {

Loading…
Cancel
Save