major refactor: move main logic into resolveUserConfig()

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

@ -19,7 +19,7 @@ import {
type SiteData
} from '../shared'
import type { Route } from './router'
import { dirname, stackView } from './utils'
import { stackView } from './utils'
export const dataSymbol: InjectionKey<VitePressData> = Symbol()
@ -70,49 +70,45 @@ if (import.meta.hot) {
})
}
// hierarchical config pre-loading
const extraConfig: Record<string, SiteData> = Object.fromEntries(
Object.entries(
import.meta.glob('/**/config.([cm]?js|ts|json)', {
eager: true
function debugConfigLayers(path: string, layers: SiteData[]): SiteData[] {
// debug info
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)'}`
})
).map(([path, module]) => [
dirname(path),
{ __module__: path, ...((module as any)?.default ?? module) }
])
console.debug(
[summaryTitle, ''.padEnd(summaryTitle.length, '='), ...summary].join('\n')
)
}
return layers
}
function getExtraConfigs(path: string): SiteData[] {
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 extraConfig) configs.push(extraConfig[key])
if (key in additionalConfig) configs.push(additionalConfig[key] as SiteData)
segments.pop()
}
// debug info
if (inBrowser) {
const summaryTitle = `Config Layers for ${path}:`
const summary = configs.map(
(c, i) => ` ${i + 1}. ${(c as any).__module__}`
)
summary.push(` ${summary.length + 1}. .vitepress/config (root)`)
console.debug(
[summaryTitle, ''.padEnd(summaryTitle.length, '='), ...summary].join('\n')
)
}
return configs
return [...configs, root]
}
// per-app data
export function initData(route: Route): VitePressData {
const site = computed(() => {
const data = resolveSiteDataByRoute(
siteDataRef.value,
route.data.relativePath
)
return stackView(...getExtraConfigs(route.data.relativePath), data)
;(window as any).siteData = siteDataRef.value
const path = route.data.relativePath
const data = resolveSiteDataByRoute(siteDataRef.value, path)
return stackView(...debugConfigLayers(path, getConfigLayers(data, path)))
})
const appearance = site.value.appearance // fine with reactivity being lost here, config change triggers a restart

@ -141,12 +141,6 @@ function tryOffsetSelector(selector: string, padding: number): number {
return bot + padding
}
export function dirname(path: string) {
const segments = path.split('/')
segments[segments.length - 1] = ''
return segments.join('/')
}
const unpackStackView = Symbol('unpackStackView')
function isStackable(obj: any) {

@ -14,6 +14,11 @@ import type { DefaultTheme } from './defaultTheme'
import { resolvePages } from './plugins/dynamicRoutesPlugin'
import { APPEARANCE_KEY, slash, type HeadConfig, type SiteData } from './shared'
import type { RawConfigExports, SiteConfig, UserConfig } from './siteConfig'
import type {
AdditionalConfigDict,
AdditionalConfigEntry
} from '../../types/shared'
import { glob } from 'tinyglobby'
export { resolvePages } from './plugins/dynamicRoutesPlugin'
export * from './siteConfig'
@ -140,7 +145,65 @@ export async function resolveConfig(
return config
}
const supportedConfigExtensions = ['js', 'ts', 'mjs', 'mts']
export const supportedConfigExtensions = ['js', 'ts', 'mjs', 'mts']
export function isAdditionalConfigFile(path: string) {
const filename_to_check = path.split('/').pop() ?? ''
for (const filename of supportedConfigExtensions.map((e) => `config.${e}`)) {
if (filename_to_check === filename) {
return true
}
}
return false
}
/**
* Make sure the path ends with a slash.
* If path points to a file, remove the filename component.
* @param path
* @returns
*/
function dirname(path: string) {
const segments = path.split('/')
segments[segments.length - 1] = ''
return segments.join('/')
}
async function gatherAdditionalConfig(
root: string,
command: 'serve' | 'build',
mode: string
): Promise<[AdditionalConfigDict, string[][]]> {
const pattern = `**/config.{${supportedConfigExtensions.join(',')}}`
const candidates = await glob(pattern, {
cwd: root,
dot: false, // conveniently ignores .vitepress/*
ignore: ['**/node_modules/**', '**/.git/**']
})
const deps: string[][] = []
const exports = await Promise.all(
candidates.map(async (file) => {
const id = '/' + dirname(slash(file))
const configExports = await loadConfigFromFile(
{ command, mode },
normalizePath(path.resolve(root, file)),
root
).catch(console.error) // Skip additionalConfig file if it fails to load
if (!configExports) {
debug(`Failed to load additional config from ${file}`)
return [id, undefined]
}
deps.push(
configExports.dependencies.map((file) =>
normalizePath(path.resolve(file))
)
)
if (mode === 'development') (configExports.config as any).VP_SOURCE = file
return [id, configExports.config as AdditionalConfigEntry]
})
)
return [Object.fromEntries(exports.filter(([id, config]) => config)), deps]
}
export async function resolveUserConfig(
root: string,
@ -170,6 +233,16 @@ export async function resolveUserConfig(
configDeps = configExports.dependencies.map((file) =>
normalizePath(path.resolve(file))
)
// Auto-generate additional config if user leaves it unspecified
if (userConfig.additionalConfig === undefined) {
const [additionalConfig, additionalDeps] = await gatherAdditionalConfig(
root,
command,
mode
)
userConfig.additionalConfig = additionalConfig
configDeps = configDeps.concat(...additionalDeps)
}
}
debug(`loaded config at ${c.yellow(configPath)}`)
}
@ -241,7 +314,8 @@ export async function resolveSiteData(
locales: userConfig.locales || {},
scrollOffset: userConfig.scrollOffset ?? 134,
cleanUrls: !!userConfig.cleanUrls,
contentProps: userConfig.contentProps
contentProps: userConfig.contentProps,
additionalConfig: userConfig.additionalConfig
}
}

@ -15,7 +15,12 @@ import {
SITE_DATA_REQUEST_PATH,
resolveAliases
} from './alias'
import { resolvePages, resolveUserConfig, type SiteConfig } from './config'
import {
resolvePages,
resolveUserConfig,
isAdditionalConfigFile,
type SiteConfig
} from './config'
import { disposeMdItInstance } from './markdown/markdown'
import {
clearCache,
@ -383,7 +388,11 @@ export async function createVitePressPlugin(
async hotUpdate({ file }) {
if (this.environment.name !== 'client') return
if (file === configPath || configDeps.includes(file)) {
if (
file === configPath ||
configDeps.includes(file) ||
isAdditionalConfigFile(file)
) {
siteConfig.logger.info(
c.green(
`${path.relative(process.cwd(), file)} changed, restarting server...\n`

@ -14,6 +14,7 @@ import type {
SSGContext,
SiteData
} from './shared'
import type { AdditionalConfig } from '../../types/shared'
export type RawConfigExports<ThemeConfig = any> =
| Awaitable<UserConfig<ThemeConfig>>
@ -187,6 +188,13 @@ export interface UserConfig<ThemeConfig = any>
pageData: PageData,
ctx: TransformPageContext
) => Awaitable<Partial<PageData> | { [key: string]: any } | void>
/**
* @experimental
* Multi-layer configuration overloading.
* Auto-resolves to docs/.../config.(ts|js|json) when unspecified.
*/
additionalConfig?: AdditionalConfig
}
export interface SiteConfig<ThemeConfig = any>
@ -209,6 +217,7 @@ export interface SiteConfig<ThemeConfig = any>
| 'transformHtml'
| 'transformPageData'
| 'sitemap'
| 'additionalConfig'
> {
root: string
srcDir: string

22
types/shared.d.ts vendored

@ -134,6 +134,7 @@ export interface SiteData<ThemeConfig = any> {
router: {
prefetchLinks: boolean
}
additionalConfig?: AdditionalConfig<ThemeConfig>
}
export type HeadConfig =
@ -161,6 +162,27 @@ export interface LocaleSpecificConfig<ThemeConfig = any> {
themeConfig?: ThemeConfig
}
export interface AdditionalConfigEntry<ThemeConfig = any>
extends LocaleSpecificConfig<ThemeConfig> {
/**
* Source of current config entry, only available in development mode
*/
src?: string
}
export type AdditionalConfigDict<ThemeConfig = any> = Record<
string,
AdditionalConfigEntry<ThemeConfig>
>
export type AdditionalConfigLoader<ThemeConfig = any> = (
path: string
) => AdditionalConfigEntry<ThemeConfig>[]
export type AdditionalConfig<ThemeConfig = any> =
| AdditionalConfigDict<ThemeConfig>
| AdditionalConfigLoader<ThemeConfig>
export type LocaleConfig<ThemeConfig = any> = Record<
string,
LocaleSpecificConfig<ThemeConfig> & { label: string; link?: string }

Loading…
Cancel
Save