Skip to content

Commit f33687c

Browse files
committed
major refactor: move main logic into resolveUserConfig()
1 parent 550e5f9 commit f33687c

File tree

6 files changed

+144
-40
lines changed

6 files changed

+144
-40
lines changed

src/client/app/data.ts

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

2424
export const dataSymbol: InjectionKey<VitePressData> = Symbol()
2525

@@ -70,49 +70,45 @@ if (import.meta.hot) {
7070
})
7171
}
7272

73-
// hierarchical config pre-loading
74-
const extraConfig: Record<string, SiteData> = Object.fromEntries(
75-
Object.entries(
76-
import.meta.glob('/**/config.([cm]?js|ts|json)', {
77-
eager: true
73+
function debugConfigLayers(path: string, layers: SiteData[]): SiteData[] {
74+
// debug info
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)'}`
7881
})
79-
).map(([path, module]) => [
80-
dirname(path),
81-
{ __module__: path, ...((module as any)?.default ?? module) }
82-
])
83-
)
82+
console.debug(
83+
[summaryTitle, ''.padEnd(summaryTitle.length, '='), ...summary].join('\n')
84+
)
85+
}
86+
return layers
87+
}
8488

85-
function getExtraConfigs(path: string): SiteData[] {
89+
function getConfigLayers(root: SiteData, path: string): SiteData[] {
8690
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]
8795
const configs: SiteData[] = []
8896
const segments = path.split('/').slice(1, -1)
8997
while (segments.length) {
9098
const key = `/${segments.join('/')}/`
91-
if (key in extraConfig) configs.push(extraConfig[key])
99+
if (key in additionalConfig) configs.push(additionalConfig[key] as SiteData)
92100
segments.pop()
93101
}
94-
// debug info
95-
if (inBrowser) {
96-
const summaryTitle = `Config Layers for ${path}:`
97-
const summary = configs.map(
98-
(c, i) => ` ${i + 1}. ${(c as any).__module__}`
99-
)
100-
summary.push(` ${summary.length + 1}. .vitepress/config (root)`)
101-
console.debug(
102-
[summaryTitle, ''.padEnd(summaryTitle.length, '='), ...summary].join('\n')
103-
)
104-
}
105-
return configs
102+
return [...configs, root]
106103
}
107104

108105
// per-app data
109106
export function initData(route: Route): VitePressData {
110107
const site = computed(() => {
111-
const data = resolveSiteDataByRoute(
112-
siteDataRef.value,
113-
route.data.relativePath
114-
)
115-
return stackView(...getExtraConfigs(route.data.relativePath), data)
108+
;(window as any).siteData = siteDataRef.value
109+
const path = route.data.relativePath
110+
const data = resolveSiteDataByRoute(siteDataRef.value, path)
111+
return stackView(...debugConfigLayers(path, getConfigLayers(data, path)))
116112
})
117113

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

src/client/app/utils.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,6 @@ function tryOffsetSelector(selector: string, padding: number): number {
141141
return bot + padding
142142
}
143143

144-
export function dirname(path: string) {
145-
const segments = path.split('/')
146-
segments[segments.length - 1] = ''
147-
return segments.join('/')
148-
}
149-
150144
const unpackStackView = Symbol('unpackStackView')
151145

152146
function isStackable(obj: any) {

src/node/config.ts

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import type { DefaultTheme } from './defaultTheme'
1414
import { resolvePages } from './plugins/dynamicRoutesPlugin'
1515
import { APPEARANCE_KEY, slash, type HeadConfig, type SiteData } from './shared'
1616
import type { RawConfigExports, SiteConfig, UserConfig } from './siteConfig'
17+
import type {
18+
AdditionalConfigDict,
19+
AdditionalConfigEntry
20+
} from '../../types/shared'
21+
import { glob } from 'tinyglobby'
1722

1823
export { resolvePages } from './plugins/dynamicRoutesPlugin'
1924
export * from './siteConfig'
@@ -140,7 +145,65 @@ export async function resolveConfig(
140145
return config
141146
}
142147

143-
const supportedConfigExtensions = ['js', 'ts', 'mjs', 'mts']
148+
export const supportedConfigExtensions = ['js', 'ts', 'mjs', 'mts']
149+
150+
export function isAdditionalConfigFile(path: string) {
151+
const filename_to_check = path.split('/').pop() ?? ''
152+
for (const filename of supportedConfigExtensions.map((e) => `config.${e}`)) {
153+
if (filename_to_check === filename) {
154+
return true
155+
}
156+
}
157+
return false
158+
}
159+
160+
/**
161+
* Make sure the path ends with a slash.
162+
* If path points to a file, remove the filename component.
163+
* @param path
164+
* @returns
165+
*/
166+
function dirname(path: string) {
167+
const segments = path.split('/')
168+
segments[segments.length - 1] = ''
169+
return segments.join('/')
170+
}
171+
172+
async function gatherAdditionalConfig(
173+
root: string,
174+
command: 'serve' | 'build',
175+
mode: string
176+
): Promise<[AdditionalConfigDict, string[][]]> {
177+
const pattern = `**/config.{${supportedConfigExtensions.join(',')}}`
178+
const candidates = await glob(pattern, {
179+
cwd: root,
180+
dot: false, // conveniently ignores .vitepress/*
181+
ignore: ['**/node_modules/**', '**/.git/**']
182+
})
183+
const deps: string[][] = []
184+
const exports = await Promise.all(
185+
candidates.map(async (file) => {
186+
const id = '/' + dirname(slash(file))
187+
const configExports = await loadConfigFromFile(
188+
{ command, mode },
189+
normalizePath(path.resolve(root, file)),
190+
root
191+
).catch(console.error) // Skip additionalConfig file if it fails to load
192+
if (!configExports) {
193+
debug(`Failed to load additional config from ${file}`)
194+
return [id, undefined]
195+
}
196+
deps.push(
197+
configExports.dependencies.map((file) =>
198+
normalizePath(path.resolve(file))
199+
)
200+
)
201+
if (mode === 'development') (configExports.config as any).VP_SOURCE = file
202+
return [id, configExports.config as AdditionalConfigEntry]
203+
})
204+
)
205+
return [Object.fromEntries(exports.filter(([id, config]) => config)), deps]
206+
}
144207

145208
export async function resolveUserConfig(
146209
root: string,
@@ -170,6 +233,16 @@ export async function resolveUserConfig(
170233
configDeps = configExports.dependencies.map((file) =>
171234
normalizePath(path.resolve(file))
172235
)
236+
// Auto-generate additional config if user leaves it unspecified
237+
if (userConfig.additionalConfig === undefined) {
238+
const [additionalConfig, additionalDeps] = await gatherAdditionalConfig(
239+
root,
240+
command,
241+
mode
242+
)
243+
userConfig.additionalConfig = additionalConfig
244+
configDeps = configDeps.concat(...additionalDeps)
245+
}
173246
}
174247
debug(`loaded config at ${c.yellow(configPath)}`)
175248
}
@@ -241,7 +314,8 @@ export async function resolveSiteData(
241314
locales: userConfig.locales || {},
242315
scrollOffset: userConfig.scrollOffset ?? 134,
243316
cleanUrls: !!userConfig.cleanUrls,
244-
contentProps: userConfig.contentProps
317+
contentProps: userConfig.contentProps,
318+
additionalConfig: userConfig.additionalConfig
245319
}
246320
}
247321

src/node/plugin.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ import {
1515
SITE_DATA_REQUEST_PATH,
1616
resolveAliases
1717
} from './alias'
18-
import { resolvePages, resolveUserConfig, type SiteConfig } from './config'
18+
import {
19+
resolvePages,
20+
resolveUserConfig,
21+
isAdditionalConfigFile,
22+
type SiteConfig
23+
} from './config'
1924
import { disposeMdItInstance } from './markdown/markdown'
2025
import {
2126
clearCache,
@@ -383,7 +388,11 @@ export async function createVitePressPlugin(
383388
async hotUpdate({ file }) {
384389
if (this.environment.name !== 'client') return
385390

386-
if (file === configPath || configDeps.includes(file)) {
391+
if (
392+
file === configPath ||
393+
configDeps.includes(file) ||
394+
isAdditionalConfigFile(file)
395+
) {
387396
siteConfig.logger.info(
388397
c.green(
389398
`${path.relative(process.cwd(), file)} changed, restarting server...\n`

src/node/siteConfig.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
SSGContext,
1515
SiteData
1616
} from './shared'
17+
import type { AdditionalConfig } from '../../types/shared'
1718

1819
export type RawConfigExports<ThemeConfig = any> =
1920
| Awaitable<UserConfig<ThemeConfig>>
@@ -187,6 +188,13 @@ export interface UserConfig<ThemeConfig = any>
187188
pageData: PageData,
188189
ctx: TransformPageContext
189190
) => Awaitable<Partial<PageData> | { [key: string]: any } | void>
191+
192+
/**
193+
* @experimental
194+
* Multi-layer configuration overloading.
195+
* Auto-resolves to docs/.../config.(ts|js|json) when unspecified.
196+
*/
197+
additionalConfig?: AdditionalConfig
190198
}
191199

192200
export interface SiteConfig<ThemeConfig = any>
@@ -209,6 +217,7 @@ export interface SiteConfig<ThemeConfig = any>
209217
| 'transformHtml'
210218
| 'transformPageData'
211219
| 'sitemap'
220+
| 'additionalConfig'
212221
> {
213222
root: string
214223
srcDir: string

types/shared.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export interface SiteData<ThemeConfig = any> {
134134
router: {
135135
prefetchLinks: boolean
136136
}
137+
additionalConfig?: AdditionalConfig<ThemeConfig>
137138
}
138139

139140
export type HeadConfig =
@@ -161,6 +162,27 @@ export interface LocaleSpecificConfig<ThemeConfig = any> {
161162
themeConfig?: ThemeConfig
162163
}
163164

165+
export interface AdditionalConfigEntry<ThemeConfig = any>
166+
extends LocaleSpecificConfig<ThemeConfig> {
167+
/**
168+
* Source of current config entry, only available in development mode
169+
*/
170+
src?: string
171+
}
172+
173+
export type AdditionalConfigDict<ThemeConfig = any> = Record<
174+
string,
175+
AdditionalConfigEntry<ThemeConfig>
176+
>
177+
178+
export type AdditionalConfigLoader<ThemeConfig = any> = (
179+
path: string
180+
) => AdditionalConfigEntry<ThemeConfig>[]
181+
182+
export type AdditionalConfig<ThemeConfig = any> =
183+
| AdditionalConfigDict<ThemeConfig>
184+
| AdditionalConfigLoader<ThemeConfig>
185+
164186
export type LocaleConfig<ThemeConfig = any> = Record<
165187
string,
166188
LocaleSpecificConfig<ThemeConfig> & { label: string; link?: string }

0 commit comments

Comments
 (0)