refactor: apply additional config for both SSR and client

pull/4660/head
Yuxuan Zhang 6 months ago
parent c8bef32aaf
commit 787b82599b
No known key found for this signature in database
GPG Key ID: 6910B04F3351EF7D

@ -19,7 +19,6 @@ import {
type SiteData
} from '../shared'
import type { Route } from './router'
import { stackView } from './utils'
export const dataSymbol: InjectionKey<VitePressData> = Symbol()
@ -70,46 +69,11 @@ if (import.meta.hot) {
})
}
function debugConfigLayers(path: string, layers: SiteData[]): SiteData[] {
// This helps users to understand which configuration files are active
if (inBrowser && import.meta.env.DEV) {
const summaryTitle = `Config Layers for ${path}:`
const summary = layers.map((c, i, arr) => {
const n = i + 1
if (n === arr.length) return `${n}. .vitepress/config (root)`
return `${n}. ${(c as any)?.['[VP_SOURCE]'] ?? '(Unknown Source)'}`
})
console.debug(
[summaryTitle, ''.padEnd(summaryTitle.length, '='), ...summary].join('\n')
)
}
return layers
}
function getConfigLayers(root: SiteData, path: string): SiteData[] {
if (!path.startsWith('/')) path = `/${path}`
const additionalConfig = root.additionalConfig
if (additionalConfig === undefined) return [root]
else if (typeof additionalConfig === 'function')
return [...(additionalConfig(path) as SiteData[]), root]
const configs: SiteData[] = []
const segments = path.split('/').slice(1, -1)
while (segments.length) {
const key = `/${segments.join('/')}/`
if (key in additionalConfig) configs.push(additionalConfig[key] as SiteData)
segments.pop()
}
if ('/' in additionalConfig) configs.push(additionalConfig['/'] as SiteData)
return [...configs, root]
}
// per-app data
export function initData(route: Route): VitePressData {
const site = computed(() => {
const path = route.data.relativePath
const data = resolveSiteDataByRoute(siteDataRef.value, path)
return stackView(...debugConfigLayers(path, getConfigLayers(data, path)))
})
const site = computed(() =>
resolveSiteDataByRoute(siteDataRef.value, route.data.relativePath)
)
const appearance = site.value.appearance // fine with reactivity being lost here, config change triggers a restart
const isDark =

@ -140,59 +140,3 @@ function tryOffsetSelector(selector: string, padding: number): number {
if (bot < 0) return 0
return bot + padding
}
const unpackStackView = Symbol('unpackStackView')
function isStackable(obj: any) {
return typeof obj === 'object' && obj !== null && !Array.isArray(obj)
}
/**
* Creates a deep, merged view of multiple objects without mutating originals.
* Returns a readonly proxy behaving like a merged object of the input objects.
* Layers are merged in descending precedence, i.e. earlier layer is on top.
*/
export function stackView<T extends object>(...layers: T[]): T {
layers = layers.filter((layer) => layer !== undefined)
if (layers.length == 0) return undefined as any as T
if (layers.length == 1 || !isStackable(layers[0])) return layers[0]
layers = layers.filter(isStackable)
if (layers.length == 1) return layers[0]
return new Proxy(
{},
{
get(target, prop) {
if (prop === unpackStackView) {
return layers
}
return stackView(...layers.map((layer) => (layer as any)?.[prop]))
},
set(target, prop, value) {
throw new Error('StackView is read-only and cannot be mutated.')
},
has(target, prop) {
for (const layer of layers) {
if (prop in layer) return true
}
return false
},
ownKeys(target) {
const keys = new Set<string>()
for (const layer of layers) {
for (const key of Object.keys(layer)) {
keys.add(key)
}
}
return Array.from(keys)
},
getOwnPropertyDescriptor(target, prop) {
for (const layer of layers) {
if (prop in layer) {
return Object.getOwnPropertyDescriptor(layer, prop)
}
}
}
}
) as T
}
stackView.unpack = (obj: any) => obj?.[unpackStackView]

@ -12,7 +12,13 @@ import {
import { DEFAULT_THEME_PATH } from './alias'
import type { DefaultTheme } from './defaultTheme'
import { resolvePages } from './plugins/dynamicRoutesPlugin'
import { APPEARANCE_KEY, slash, type HeadConfig, type SiteData } from './shared'
import {
APPEARANCE_KEY,
VP_SOURCE_KEY,
slash,
type HeadConfig,
type SiteData
} from './shared'
import type { RawConfigExports, SiteConfig, UserConfig } from './siteConfig'
import type { AdditionalConfig, AdditionalConfigDict } from '../../types/shared'
import { glob } from 'tinyglobby'
@ -215,7 +221,7 @@ async function gatherAdditionalConfig(
)
)
if (mode === 'development')
(configExports.config as any)['[VP_SOURCE]'] = '/' + slash(file)
(configExports.config as any)[VP_SOURCE_KEY] = '/' + slash(file)
return [id, configExports.config as AdditionalConfig]
})
)

@ -94,22 +94,29 @@ export function resolveSiteDataByRoute(
relativePath: string
): SiteData {
const localeIndex = getLocaleForPath(siteData, relativePath)
return Object.assign({}, siteData, {
localeIndex,
lang: siteData.locales[localeIndex]?.lang ?? siteData.lang,
dir: siteData.locales[localeIndex]?.dir ?? siteData.dir,
title: siteData.locales[localeIndex]?.title ?? siteData.title,
titleTemplate:
siteData.locales[localeIndex]?.titleTemplate ?? siteData.titleTemplate,
description:
siteData.locales[localeIndex]?.description ?? siteData.description,
head: mergeHead(siteData.head, siteData.locales[localeIndex]?.head ?? []),
themeConfig: {
...siteData.themeConfig,
...siteData.locales[localeIndex]?.themeConfig
}
})
const { label, link, ...localeConfig } = siteData.locales[localeIndex] ?? {}
const additionalConfigs = resolveAdditionalConfig(siteData, relativePath)
if (inBrowser && (import.meta as any).env?.DEV) {
;(localeConfig as any)[VP_SOURCE_KEY] = `locale config (${localeIndex})`
reportConfigLayers(relativePath, [
...additionalConfigs,
localeConfig as SiteData,
siteData
])
}
const topLayer = {
head: mergeHead(
siteData.head ?? [],
localeConfig.head ?? [],
...additionalConfigs.map((data) => data?.head ?? []).reverse()
)
} as SiteData
return stackView<SiteData>(
topLayer,
...additionalConfigs,
localeConfig,
siteData
)
}
/**
@ -161,8 +168,18 @@ function hasTag(head: HeadConfig[], tag: HeadConfig) {
)
}
export function mergeHead(prev: HeadConfig[], curr: HeadConfig[]) {
return [...prev.filter((tagAttrs) => !hasTag(curr, tagAttrs)), ...curr]
export function mergeHead(current: HeadConfig[], ...incoming: HeadConfig[][]) {
return incoming
.filter((el) => Array.isArray(el) && el.length > 0)
.flat(1)
.reverse()
.reduce(
(merged, tag) => {
if (!hasTag(merged, tag)) merged.push(tag)
return merged
},
[...current]
)
}
// https://github.com/rollup/rollup/blob/fec513270c6ac350072425cc045db367656c623b/src/utils/sanitizeFileName.ts
@ -230,3 +247,91 @@ export function escapeHtml(str: string): string {
.replace(/"/g, '&quot;')
.replace(/&(?![\w#]+;)/g, '&amp;')
}
export function resolveAdditionalConfig(site: SiteData, path: string) {
if (!path.startsWith('/')) path = `/${path}`
const additionalConfig = site.additionalConfig
if (additionalConfig === undefined) return []
else if (typeof additionalConfig === 'function')
return additionalConfig(path) as SiteData[]
const configs: SiteData[] = []
const segments = path.split('/').slice(1, -1)
while (segments.length) {
const key = `/${segments.join('/')}/`
if (key in additionalConfig) configs.push(additionalConfig[key] as SiteData)
segments.pop()
}
if ('/' in additionalConfig) configs.push(additionalConfig['/'] as SiteData)
return configs
}
export const VP_SOURCE_KEY = '[VP_SOURCE]'
function reportConfigLayers(path: string, layers: SiteData[]) {
// This helps users to understand which configuration files are active
const summaryTitle = `Config Layers for ${path}:`
const summary = layers.map((c, i, arr) => {
const n = i + 1
if (n === arr.length) return `${n}. .vitepress/config (root)`
return `${n}. ${(c as any)?.[VP_SOURCE_KEY] ?? '(Unknown Source)'}`
})
console.debug(
[summaryTitle, ''.padEnd(summaryTitle.length, '='), ...summary].join('\n')
)
}
/**
* Creates a deep, merged view of multiple objects without mutating originals.
* Returns a readonly proxy behaving like a merged object of the input objects.
* Layers are merged in descending precedence, i.e. earlier layer is on top.
*/
export function stackView<T extends object>(...layers: Partial<T>[]): T {
layers = layers.filter((layer) => layer !== undefined)
if (!isStackable(layers[0])) return layers[0] as T
layers = layers.filter(isStackable)
if (layers.length <= 1) return layers[0] as T
return new Proxy(
{},
{
get(_, key) {
return key === UnpackStackView
? layers
: stackView(...layers.map((layer) => (layer as any)?.[key]))
},
set(_, key, value) {
throw new Error('StackView is read-only and cannot be mutated.')
},
has(_, key) {
for (const layer of layers) {
if (key in layer) return true
}
return false
},
ownKeys(_) {
const keys = new Set<string>()
for (const layer of layers) {
for (const key of Object.keys(layer)) {
keys.add(key)
}
}
return Array.from(keys)
},
getOwnPropertyDescriptor(_, key) {
for (const layer of layers) {
if (key in layer) {
return Object.getOwnPropertyDescriptor(layer, key)
}
}
}
}
) as T
}
function isStackable(obj: any) {
return typeof obj === 'object' && obj !== null && !Array.isArray(obj)
}
const UnpackStackView = Symbol('stack-view:unpack')
stackView.unpack = function <T>(obj: T): T[] | undefined {
return (obj as any)?.[UnpackStackView]
}

Loading…
Cancel
Save