From 787b82599b10d649dd6d852acd664337be2f8384 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sat, 5 Apr 2025 07:14:33 -0400 Subject: [PATCH] refactor: apply additional config for both SSR and client --- src/client/app/data.ts | 42 +----------- src/client/app/utils.ts | 56 ---------------- src/node/config.ts | 10 ++- src/shared/shared.ts | 141 +++++++++++++++++++++++++++++++++++----- 4 files changed, 134 insertions(+), 115 deletions(-) diff --git a/src/client/app/data.ts b/src/client/app/data.ts index b7770ab0..16288e87 100644 --- a/src/client/app/data.ts +++ b/src/client/app/data.ts @@ -19,7 +19,6 @@ import { type SiteData } from '../shared' import type { Route } from './router' -import { stackView } from './utils' export const dataSymbol: InjectionKey = 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 = diff --git a/src/client/app/utils.ts b/src/client/app/utils.ts index b701c08c..53ef6e51 100644 --- a/src/client/app/utils.ts +++ b/src/client/app/utils.ts @@ -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(...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() - 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] diff --git a/src/node/config.ts b/src/node/config.ts index 138d4515..8518d9f2 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -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] }) ) diff --git a/src/shared/shared.ts b/src/shared/shared.ts index 0dd35db4..0602088c 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -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( + 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, '"') .replace(/&(?![\w#]+;)/g, '&') } + +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(...layers: Partial[]): 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() + 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 (obj: T): T[] | undefined { + return (obj as any)?.[UnpackStackView] +}