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

@ -14,6 +14,11 @@ import type { DefaultTheme } from './defaultTheme'
import { resolvePages } from './plugins/dynamicRoutesPlugin' import { resolvePages } from './plugins/dynamicRoutesPlugin'
import { APPEARANCE_KEY, slash, type HeadConfig, type SiteData } from './shared' import { APPEARANCE_KEY, slash, type HeadConfig, type SiteData } from './shared'
import type { RawConfigExports, SiteConfig, UserConfig } from './siteConfig' import type { RawConfigExports, SiteConfig, UserConfig } from './siteConfig'
import type {
AdditionalConfigDict,
AdditionalConfigEntry
} from '../../types/shared'
import { glob } from 'tinyglobby'
export { resolvePages } from './plugins/dynamicRoutesPlugin' export { resolvePages } from './plugins/dynamicRoutesPlugin'
export * from './siteConfig' export * from './siteConfig'
@ -140,7 +145,65 @@ export async function resolveConfig(
return config 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( export async function resolveUserConfig(
root: string, root: string,
@ -170,6 +233,16 @@ export async function resolveUserConfig(
configDeps = configExports.dependencies.map((file) => configDeps = configExports.dependencies.map((file) =>
normalizePath(path.resolve(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)}`) debug(`loaded config at ${c.yellow(configPath)}`)
} }
@ -241,7 +314,8 @@ export async function resolveSiteData(
locales: userConfig.locales || {}, locales: userConfig.locales || {},
scrollOffset: userConfig.scrollOffset ?? 134, scrollOffset: userConfig.scrollOffset ?? 134,
cleanUrls: !!userConfig.cleanUrls, cleanUrls: !!userConfig.cleanUrls,
contentProps: userConfig.contentProps contentProps: userConfig.contentProps,
additionalConfig: userConfig.additionalConfig
} }
} }

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

@ -14,6 +14,7 @@ import type {
SSGContext, SSGContext,
SiteData SiteData
} from './shared' } from './shared'
import type { AdditionalConfig } from '../../types/shared'
export type RawConfigExports<ThemeConfig = any> = export type RawConfigExports<ThemeConfig = any> =
| Awaitable<UserConfig<ThemeConfig>> | Awaitable<UserConfig<ThemeConfig>>
@ -187,6 +188,13 @@ export interface UserConfig<ThemeConfig = any>
pageData: PageData, pageData: PageData,
ctx: TransformPageContext ctx: TransformPageContext
) => Awaitable<Partial<PageData> | { [key: string]: any } | void> ) => 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> export interface SiteConfig<ThemeConfig = any>
@ -209,6 +217,7 @@ export interface SiteConfig<ThemeConfig = any>
| 'transformHtml' | 'transformHtml'
| 'transformPageData' | 'transformPageData'
| 'sitemap' | 'sitemap'
| 'additionalConfig'
> { > {
root: string root: string
srcDir: string srcDir: string

22
types/shared.d.ts vendored

@ -134,6 +134,7 @@ export interface SiteData<ThemeConfig = any> {
router: { router: {
prefetchLinks: boolean prefetchLinks: boolean
} }
additionalConfig?: AdditionalConfig<ThemeConfig>
} }
export type HeadConfig = export type HeadConfig =
@ -161,6 +162,27 @@ export interface LocaleSpecificConfig<ThemeConfig = any> {
themeConfig?: ThemeConfig 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< export type LocaleConfig<ThemeConfig = any> = Record<
string, string,
LocaleSpecificConfig<ThemeConfig> & { label: string; link?: string } LocaleSpecificConfig<ThemeConfig> & { label: string; link?: string }

Loading…
Cancel
Save