Skip to content

Commit 787b825

Browse files
committed
refactor: apply additional config for both SSR and client
1 parent c8bef32 commit 787b825

File tree

4 files changed

+134
-115
lines changed

4 files changed

+134
-115
lines changed

src/client/app/data.ts

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
type SiteData
2020
} from '../shared'
2121
import type { Route } from './router'
22-
import { stackView } from './utils'
2322

2423
export const dataSymbol: InjectionKey<VitePressData> = Symbol()
2524

@@ -70,46 +69,11 @@ if (import.meta.hot) {
7069
})
7170
}
7271

73-
function debugConfigLayers(path: string, layers: SiteData[]): SiteData[] {
74-
// This helps users to understand which configuration files are active
75-
if (inBrowser && import.meta.env.DEV) {
76-
const summaryTitle = `Config Layers for ${path}:`
77-
const summary = layers.map((c, i, arr) => {
78-
const n = i + 1
79-
if (n === arr.length) return `${n}. .vitepress/config (root)`
80-
return `${n}. ${(c as any)?.['[VP_SOURCE]'] ?? '(Unknown Source)'}`
81-
})
82-
console.debug(
83-
[summaryTitle, ''.padEnd(summaryTitle.length, '='), ...summary].join('\n')
84-
)
85-
}
86-
return layers
87-
}
88-
89-
function getConfigLayers(root: SiteData, path: string): SiteData[] {
90-
if (!path.startsWith('/')) path = `/${path}`
91-
const additionalConfig = root.additionalConfig
92-
if (additionalConfig === undefined) return [root]
93-
else if (typeof additionalConfig === 'function')
94-
return [...(additionalConfig(path) as SiteData[]), root]
95-
const configs: SiteData[] = []
96-
const segments = path.split('/').slice(1, -1)
97-
while (segments.length) {
98-
const key = `/${segments.join('/')}/`
99-
if (key in additionalConfig) configs.push(additionalConfig[key] as SiteData)
100-
segments.pop()
101-
}
102-
if ('/' in additionalConfig) configs.push(additionalConfig['/'] as SiteData)
103-
return [...configs, root]
104-
}
105-
10672
// per-app data
10773
export function initData(route: Route): VitePressData {
108-
const site = computed(() => {
109-
const path = route.data.relativePath
110-
const data = resolveSiteDataByRoute(siteDataRef.value, path)
111-
return stackView(...debugConfigLayers(path, getConfigLayers(data, path)))
112-
})
74+
const site = computed(() =>
75+
resolveSiteDataByRoute(siteDataRef.value, route.data.relativePath)
76+
)
11377

11478
const appearance = site.value.appearance // fine with reactivity being lost here, config change triggers a restart
11579
const isDark =

src/client/app/utils.ts

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -140,59 +140,3 @@ function tryOffsetSelector(selector: string, padding: number): number {
140140
if (bot < 0) return 0
141141
return bot + padding
142142
}
143-
144-
const unpackStackView = Symbol('unpackStackView')
145-
146-
function isStackable(obj: any) {
147-
return typeof obj === 'object' && obj !== null && !Array.isArray(obj)
148-
}
149-
/**
150-
* Creates a deep, merged view of multiple objects without mutating originals.
151-
* Returns a readonly proxy behaving like a merged object of the input objects.
152-
* Layers are merged in descending precedence, i.e. earlier layer is on top.
153-
*/
154-
export function stackView<T extends object>(...layers: T[]): T {
155-
layers = layers.filter((layer) => layer !== undefined)
156-
if (layers.length == 0) return undefined as any as T
157-
if (layers.length == 1 || !isStackable(layers[0])) return layers[0]
158-
layers = layers.filter(isStackable)
159-
if (layers.length == 1) return layers[0]
160-
return new Proxy(
161-
{},
162-
{
163-
get(target, prop) {
164-
if (prop === unpackStackView) {
165-
return layers
166-
}
167-
return stackView(...layers.map((layer) => (layer as any)?.[prop]))
168-
},
169-
set(target, prop, value) {
170-
throw new Error('StackView is read-only and cannot be mutated.')
171-
},
172-
has(target, prop) {
173-
for (const layer of layers) {
174-
if (prop in layer) return true
175-
}
176-
return false
177-
},
178-
ownKeys(target) {
179-
const keys = new Set<string>()
180-
for (const layer of layers) {
181-
for (const key of Object.keys(layer)) {
182-
keys.add(key)
183-
}
184-
}
185-
return Array.from(keys)
186-
},
187-
getOwnPropertyDescriptor(target, prop) {
188-
for (const layer of layers) {
189-
if (prop in layer) {
190-
return Object.getOwnPropertyDescriptor(layer, prop)
191-
}
192-
}
193-
}
194-
}
195-
) as T
196-
}
197-
198-
stackView.unpack = (obj: any) => obj?.[unpackStackView]

src/node/config.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ import {
1212
import { DEFAULT_THEME_PATH } from './alias'
1313
import type { DefaultTheme } from './defaultTheme'
1414
import { resolvePages } from './plugins/dynamicRoutesPlugin'
15-
import { APPEARANCE_KEY, slash, type HeadConfig, type SiteData } from './shared'
15+
import {
16+
APPEARANCE_KEY,
17+
VP_SOURCE_KEY,
18+
slash,
19+
type HeadConfig,
20+
type SiteData
21+
} from './shared'
1622
import type { RawConfigExports, SiteConfig, UserConfig } from './siteConfig'
1723
import type { AdditionalConfig, AdditionalConfigDict } from '../../types/shared'
1824
import { glob } from 'tinyglobby'
@@ -215,7 +221,7 @@ async function gatherAdditionalConfig(
215221
)
216222
)
217223
if (mode === 'development')
218-
(configExports.config as any)['[VP_SOURCE]'] = '/' + slash(file)
224+
(configExports.config as any)[VP_SOURCE_KEY] = '/' + slash(file)
219225
return [id, configExports.config as AdditionalConfig]
220226
})
221227
)

src/shared/shared.ts

Lines changed: 123 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -94,22 +94,29 @@ export function resolveSiteDataByRoute(
9494
relativePath: string
9595
): SiteData {
9696
const localeIndex = getLocaleForPath(siteData, relativePath)
97-
98-
return Object.assign({}, siteData, {
99-
localeIndex,
100-
lang: siteData.locales[localeIndex]?.lang ?? siteData.lang,
101-
dir: siteData.locales[localeIndex]?.dir ?? siteData.dir,
102-
title: siteData.locales[localeIndex]?.title ?? siteData.title,
103-
titleTemplate:
104-
siteData.locales[localeIndex]?.titleTemplate ?? siteData.titleTemplate,
105-
description:
106-
siteData.locales[localeIndex]?.description ?? siteData.description,
107-
head: mergeHead(siteData.head, siteData.locales[localeIndex]?.head ?? []),
108-
themeConfig: {
109-
...siteData.themeConfig,
110-
...siteData.locales[localeIndex]?.themeConfig
111-
}
112-
})
97+
const { label, link, ...localeConfig } = siteData.locales[localeIndex] ?? {}
98+
const additionalConfigs = resolveAdditionalConfig(siteData, relativePath)
99+
if (inBrowser && (import.meta as any).env?.DEV) {
100+
;(localeConfig as any)[VP_SOURCE_KEY] = `locale config (${localeIndex})`
101+
reportConfigLayers(relativePath, [
102+
...additionalConfigs,
103+
localeConfig as SiteData,
104+
siteData
105+
])
106+
}
107+
const topLayer = {
108+
head: mergeHead(
109+
siteData.head ?? [],
110+
localeConfig.head ?? [],
111+
...additionalConfigs.map((data) => data?.head ?? []).reverse()
112+
)
113+
} as SiteData
114+
return stackView<SiteData>(
115+
topLayer,
116+
...additionalConfigs,
117+
localeConfig,
118+
siteData
119+
)
113120
}
114121

115122
/**
@@ -161,8 +168,18 @@ function hasTag(head: HeadConfig[], tag: HeadConfig) {
161168
)
162169
}
163170

164-
export function mergeHead(prev: HeadConfig[], curr: HeadConfig[]) {
165-
return [...prev.filter((tagAttrs) => !hasTag(curr, tagAttrs)), ...curr]
171+
export function mergeHead(current: HeadConfig[], ...incoming: HeadConfig[][]) {
172+
return incoming
173+
.filter((el) => Array.isArray(el) && el.length > 0)
174+
.flat(1)
175+
.reverse()
176+
.reduce(
177+
(merged, tag) => {
178+
if (!hasTag(merged, tag)) merged.push(tag)
179+
return merged
180+
},
181+
[...current]
182+
)
166183
}
167184

168185
// https://github.com/rollup/rollup/blob/fec513270c6ac350072425cc045db367656c623b/src/utils/sanitizeFileName.ts
@@ -230,3 +247,91 @@ export function escapeHtml(str: string): string {
230247
.replace(/"/g, '&quot;')
231248
.replace(/&(?![\w#]+;)/g, '&amp;')
232249
}
250+
251+
export function resolveAdditionalConfig(site: SiteData, path: string) {
252+
if (!path.startsWith('/')) path = `/${path}`
253+
const additionalConfig = site.additionalConfig
254+
if (additionalConfig === undefined) return []
255+
else if (typeof additionalConfig === 'function')
256+
return additionalConfig(path) as SiteData[]
257+
const configs: SiteData[] = []
258+
const segments = path.split('/').slice(1, -1)
259+
while (segments.length) {
260+
const key = `/${segments.join('/')}/`
261+
if (key in additionalConfig) configs.push(additionalConfig[key] as SiteData)
262+
segments.pop()
263+
}
264+
if ('/' in additionalConfig) configs.push(additionalConfig['/'] as SiteData)
265+
return configs
266+
}
267+
268+
export const VP_SOURCE_KEY = '[VP_SOURCE]'
269+
270+
function reportConfigLayers(path: string, layers: SiteData[]) {
271+
// This helps users to understand which configuration files are active
272+
const summaryTitle = `Config Layers for ${path}:`
273+
const summary = layers.map((c, i, arr) => {
274+
const n = i + 1
275+
if (n === arr.length) return `${n}. .vitepress/config (root)`
276+
return `${n}. ${(c as any)?.[VP_SOURCE_KEY] ?? '(Unknown Source)'}`
277+
})
278+
console.debug(
279+
[summaryTitle, ''.padEnd(summaryTitle.length, '='), ...summary].join('\n')
280+
)
281+
}
282+
283+
/**
284+
* Creates a deep, merged view of multiple objects without mutating originals.
285+
* Returns a readonly proxy behaving like a merged object of the input objects.
286+
* Layers are merged in descending precedence, i.e. earlier layer is on top.
287+
*/
288+
export function stackView<T extends object>(...layers: Partial<T>[]): T {
289+
layers = layers.filter((layer) => layer !== undefined)
290+
if (!isStackable(layers[0])) return layers[0] as T
291+
layers = layers.filter(isStackable)
292+
if (layers.length <= 1) return layers[0] as T
293+
return new Proxy(
294+
{},
295+
{
296+
get(_, key) {
297+
return key === UnpackStackView
298+
? layers
299+
: stackView(...layers.map((layer) => (layer as any)?.[key]))
300+
},
301+
set(_, key, value) {
302+
throw new Error('StackView is read-only and cannot be mutated.')
303+
},
304+
has(_, key) {
305+
for (const layer of layers) {
306+
if (key in layer) return true
307+
}
308+
return false
309+
},
310+
ownKeys(_) {
311+
const keys = new Set<string>()
312+
for (const layer of layers) {
313+
for (const key of Object.keys(layer)) {
314+
keys.add(key)
315+
}
316+
}
317+
return Array.from(keys)
318+
},
319+
getOwnPropertyDescriptor(_, key) {
320+
for (const layer of layers) {
321+
if (key in layer) {
322+
return Object.getOwnPropertyDescriptor(layer, key)
323+
}
324+
}
325+
}
326+
}
327+
) as T
328+
}
329+
330+
function isStackable(obj: any) {
331+
return typeof obj === 'object' && obj !== null && !Array.isArray(obj)
332+
}
333+
334+
const UnpackStackView = Symbol('stack-view:unpack')
335+
stackView.unpack = function <T>(obj: T): T[] | undefined {
336+
return (obj as any)?.[UnpackStackView]
337+
}

0 commit comments

Comments
 (0)