From c3c0d71d1d5ee6244e6b51abef4bbead713aac03 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sat, 29 Mar 2025 23:00:07 -0400 Subject: [PATCH 01/40] first working version --- docs/.vitepress/config/index.ts | 3 +- docs/.vitepress/config/shared.ts | 2 - .../{.vitepress/config/zh.ts => zh/config.ts} | 27 ++++---- src/client/app/data.ts | 44 ++++++++++++- src/client/app/utils.ts | 62 +++++++++++++++++++ .../theme-default/support/translation.ts | 1 + 6 files changed, 121 insertions(+), 18 deletions(-) rename docs/{.vitepress/config/zh.ts => zh/config.ts} (90%) diff --git a/docs/.vitepress/config/index.ts b/docs/.vitepress/config/index.ts index 08a81fb94f99..1654ec5791c6 100644 --- a/docs/.vitepress/config/index.ts +++ b/docs/.vitepress/config/index.ts @@ -1,7 +1,6 @@ import { defineConfig } from 'vitepress' import { shared } from './shared' import { en } from './en' -import { zh } from './zh' import { pt } from './pt' import { ru } from './ru' import { es } from './es' @@ -12,7 +11,7 @@ export default defineConfig({ ...shared, locales: { root: { label: 'English', ...en }, - zh: { label: '简体中文', ...zh }, + zh: { label: '简体中文' }, pt: { label: 'Português', ...pt }, ru: { label: 'Русский', ...ru }, es: { label: 'Español', ...es }, diff --git a/docs/.vitepress/config/shared.ts b/docs/.vitepress/config/shared.ts index 1f4961d29a98..86eb104a8991 100644 --- a/docs/.vitepress/config/shared.ts +++ b/docs/.vitepress/config/shared.ts @@ -9,7 +9,6 @@ import { search as faSearch } from './fa' import { search as koSearch } from './ko' import { search as ptSearch } from './pt' import { search as ruSearch } from './ru' -import { search as zhSearch } from './zh' export const shared = defineConfig({ title: 'VitePress', @@ -99,7 +98,6 @@ export const shared = defineConfig({ apiKey: '52f578a92b88ad6abde815aae2b0ad7c', indexName: 'vitepress', locales: { - ...zhSearch, ...ptSearch, ...ruSearch, ...esSearch, diff --git a/docs/.vitepress/config/zh.ts b/docs/zh/config.ts similarity index 90% rename from docs/.vitepress/config/zh.ts rename to docs/zh/config.ts index e9d5fbcd4f72..4120d126d396 100644 --- a/docs/.vitepress/config/zh.ts +++ b/docs/zh/config.ts @@ -1,16 +1,14 @@ -import { createRequire } from 'module' -import { defineConfig, type DefaultTheme } from 'vitepress' +import { type DefaultTheme, type UserConfig } from 'vitepress' +import vitepress from 'vitepress/package.json' +import type { DocSearchProps } from '../../types/docsearch' -const require = createRequire(import.meta.url) -const pkg = require('vitepress/package.json') - -export const zh = defineConfig({ +export default { lang: 'zh-Hans', description: '由 Vite 和 Vue 驱动的静态站点生成器', themeConfig: { nav: nav(), - + search: { options: searchOptions() } as DefaultTheme.Config['search'], sidebar: { '/zh/guide/': { base: '/zh/guide/', items: sidebarGuide() }, '/zh/reference/': { base: '/zh/reference/', items: sidebarReference() } @@ -43,6 +41,13 @@ export const zh = defineConfig({ } }, + notFound: { + title: '页面未找到', + quote: '若你不改变航向,始终凝望远方,你终将抵达前行的彼岸。', + linkLabel: '返回首页', + linkText: '返回首页' + }, + langMenuLabel: '多语言', returnToTopLabel: '回到顶部', sidebarMenuLabel: '菜单', @@ -51,7 +56,7 @@ export const zh = defineConfig({ darkModeSwitchTitle: '切换到深色模式', skipToContentLabel: '跳转到内容' } -}) +} as UserConfig function nav(): DefaultTheme.NavItem[] { return [ @@ -66,7 +71,7 @@ function nav(): DefaultTheme.NavItem[] { activeMatch: '/zh/reference/' }, { - text: pkg.version, + text: vitepress.version, items: [ { text: '更新日志', @@ -160,8 +165,8 @@ function sidebarReference(): DefaultTheme.SidebarItem[] { ] } -export const search: DefaultTheme.AlgoliaSearchOptions['locales'] = { - zh: { +function searchOptions(): Partial { + return { placeholder: '搜索文档', translations: { button: { diff --git a/src/client/app/data.ts b/src/client/app/data.ts index 16288e8714c4..3a64e948e4fe 100644 --- a/src/client/app/data.ts +++ b/src/client/app/data.ts @@ -19,6 +19,7 @@ import { type SiteData } from '../shared' import type { Route } from './router' +import { dirname, stackView } from './utils' export const dataSymbol: InjectionKey = Symbol() @@ -69,11 +70,46 @@ if (import.meta.hot) { }) } +// hierarchical config pre-loading +const extraConfig: Record = Object.fromEntries( + Object.entries( + import.meta.glob('/**/config.([cm]?js|ts|json)', { + eager: true + }) + ).map(([path, module]) => [ + dirname(path), + { __module__: path, ...((module as any)?.default ?? module) } + ]) +) + +function getExtraConfigs(path: string): SiteData[] { + if (!path.startsWith('/')) path = `/${path}` + 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]) + segments.pop() + } + // debug info + const summaryTitle = `Extra Configs for ${path}:` + const summary = configs.map((c, i) => ` ${i + 1}. ${(c as any).__module__}`) + summary.push(` ${summary.length + 1}. .vitepress/config (root)`) + console.info( + [summaryTitle, ''.padEnd(summaryTitle.length, '='), ...summary].join('\n') + ) + return configs +} + // per-app data export function initData(route: Route): VitePressData { - const site = computed(() => - resolveSiteDataByRoute(siteDataRef.value, route.data.relativePath) - ) + const site = computed(() => { + const data = resolveSiteDataByRoute( + siteDataRef.value, + route.data.relativePath + ) + return stackView(...getExtraConfigs(route.data.relativePath), data) + }) const appearance = site.value.appearance // fine with reactivity being lost here, config change triggers a restart const isDark = @@ -124,6 +160,8 @@ export function initData(route: Route): VitePressData { export function useData(): VitePressData { const data = inject(dataSymbol) + ;(window as any).stackView = stackView + ;(window as any).data = data if (!data) { throw new Error('vitepress data not properly injected in app') } diff --git a/src/client/app/utils.ts b/src/client/app/utils.ts index 53ef6e514104..3190cd0af1e6 100644 --- a/src/client/app/utils.ts +++ b/src/client/app/utils.ts @@ -140,3 +140,65 @@ function tryOffsetSelector(selector: string, padding: number): number { if (bot < 0) return 0 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) { + 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/client/theme-default/support/translation.ts b/src/client/theme-default/support/translation.ts index 12ade37b6e3f..4c285cbb8500 100644 --- a/src/client/theme-default/support/translation.ts +++ b/src/client/theme-default/support/translation.ts @@ -15,6 +15,7 @@ export function createSearchTranslate( const isObject = themeObject && typeof themeObject === 'object' const locales = (isObject && themeObject.locales?.[localeIndex.value]?.translations) || + (isObject && themeObject?.translations) || null const translations = (isObject && themeObject.translations) || null From e33e9c34d7bd406a06a65457accfa6fed9ce6988 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sat, 29 Mar 2025 23:22:12 -0400 Subject: [PATCH 02/40] refactor docs: move language-specific files into their own folders. --- .../{config/shared.ts => config.ts} | 29 +++++++++---------- docs/.vitepress/config/index.ts | 21 -------------- .../{.vitepress/config/en.ts => en/config.ts} | 14 ++++----- .../{.vitepress/config/es.ts => es/config.ts} | 19 +++++------- .../{.vitepress/config/fa.ts => fa/config.ts} | 18 +++++------- .../{.vitepress/config/ko.ts => ko/config.ts} | 19 +++++------- .../{.vitepress/config/pt.ts => pt/config.ts} | 19 +++++------- .../{.vitepress/config/ru.ts => ru/config.ts} | 19 +++++------- docs/zh/config.ts | 3 +- 9 files changed, 60 insertions(+), 101 deletions(-) rename docs/.vitepress/{config/shared.ts => config.ts} (87%) delete mode 100644 docs/.vitepress/config/index.ts rename docs/{.vitepress/config/en.ts => en/config.ts} (93%) rename docs/{.vitepress/config/es.ts => es/config.ts} (94%) rename docs/{.vitepress/config/fa.ts => fa/config.ts} (95%) rename docs/{.vitepress/config/ko.ts => ko/config.ts} (94%) rename docs/{.vitepress/config/pt.ts => pt/config.ts} (94%) rename docs/{.vitepress/config/ru.ts => ru/config.ts} (95%) diff --git a/docs/.vitepress/config/shared.ts b/docs/.vitepress/config.ts similarity index 87% rename from docs/.vitepress/config/shared.ts rename to docs/.vitepress/config.ts index 86eb104a8991..1f23e1fd7a81 100644 --- a/docs/.vitepress/config/shared.ts +++ b/docs/.vitepress/config.ts @@ -4,13 +4,8 @@ import { groupIconVitePlugin, localIconLoader } from 'vitepress-plugin-group-icons' -import { search as esSearch } from './es' -import { search as faSearch } from './fa' -import { search as koSearch } from './ko' -import { search as ptSearch } from './pt' -import { search as ruSearch } from './ru' -export const shared = defineConfig({ +export default defineConfig({ title: 'VitePress', rewrites: { @@ -96,26 +91,30 @@ export const shared = defineConfig({ options: { appId: '8J64VVRP8K', apiKey: '52f578a92b88ad6abde815aae2b0ad7c', - indexName: 'vitepress', - locales: { - ...ptSearch, - ...ruSearch, - ...esSearch, - ...koSearch, - ...faSearch - } + indexName: 'vitepress' } }, carbonAds: { code: 'CEBDT27Y', placement: 'vuejsorg' } }, + + locales: { + root: { label: 'English' }, + zh: { label: '简体中文' }, + pt: { label: 'Português' }, + ru: { label: 'Русский' }, + es: { label: 'Español' }, + ko: { label: '한국어' }, + fa: { label: 'فارسی' } + }, + vite: { plugins: [ groupIconVitePlugin({ customIcon: { vitepress: localIconLoader( import.meta.url, - '../../public/vitepress-logo-mini.svg' + '../public/vitepress-logo-mini.svg' ), firebase: 'logos:firebase' } diff --git a/docs/.vitepress/config/index.ts b/docs/.vitepress/config/index.ts deleted file mode 100644 index 1654ec5791c6..000000000000 --- a/docs/.vitepress/config/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { defineConfig } from 'vitepress' -import { shared } from './shared' -import { en } from './en' -import { pt } from './pt' -import { ru } from './ru' -import { es } from './es' -import { ko } from './ko' -import { fa } from './fa' - -export default defineConfig({ - ...shared, - locales: { - root: { label: 'English', ...en }, - zh: { label: '简体中文' }, - pt: { label: 'Português', ...pt }, - ru: { label: 'Русский', ...ru }, - es: { label: 'Español', ...es }, - ko: { label: '한국어', ...ko }, - fa: { label: 'فارسی', ...fa } - } -}) diff --git a/docs/.vitepress/config/en.ts b/docs/en/config.ts similarity index 93% rename from docs/.vitepress/config/en.ts rename to docs/en/config.ts index 1f619347738e..41dbd2b8cd28 100644 --- a/docs/.vitepress/config/en.ts +++ b/docs/en/config.ts @@ -1,16 +1,12 @@ -import { createRequire } from 'module' -import { defineConfig, type DefaultTheme } from 'vitepress' +import { type DefaultTheme, type UserConfig } from 'vitepress' +import vitepress from 'vitepress/package.json' -const require = createRequire(import.meta.url) -const pkg = require('vitepress/package.json') - -export const en = defineConfig({ +export default { lang: 'en-US', description: 'Vite & Vue powered static site generator.', themeConfig: { nav: nav(), - sidebar: { '/guide/': { base: '/guide/', items: sidebarGuide() }, '/reference/': { base: '/reference/', items: sidebarReference() } @@ -26,7 +22,7 @@ export const en = defineConfig({ copyright: 'Copyright © 2019-present Evan You' } } -}) +} as UserConfig function nav(): DefaultTheme.NavItem[] { return [ @@ -41,7 +37,7 @@ function nav(): DefaultTheme.NavItem[] { activeMatch: '/reference/' }, { - text: pkg.version, + text: vitepress.version, items: [ { text: 'Changelog', diff --git a/docs/.vitepress/config/es.ts b/docs/es/config.ts similarity index 94% rename from docs/.vitepress/config/es.ts rename to docs/es/config.ts index d30fc89f89dc..e3a0079a072f 100644 --- a/docs/.vitepress/config/es.ts +++ b/docs/es/config.ts @@ -1,16 +1,13 @@ -import { createRequire } from 'module' -import { defineConfig, type DefaultTheme } from 'vitepress' +import { type DefaultTheme, type UserConfig } from 'vitepress' +import vitepress from 'vitepress/package.json' -const require = createRequire(import.meta.url) -const pkg = require('vitepress/package.json') - -export const es = defineConfig({ +export default { lang: 'es-CO', description: 'Generador de Sitios Estaticos desarrollado con Vite y Vue.', themeConfig: { nav: nav(), - + search: { options: searchOptions() } as DefaultTheme.Config['search'], sidebar: { '/es/guide/': { base: '/es/guide/', items: sidebarGuide() }, '/es/reference/': { base: '/es/reference/', items: sidebarReference() } @@ -51,7 +48,7 @@ export const es = defineConfig({ darkModeSwitchTitle: 'Cambiar a modo oscuro', skipToContentLabel: 'Saltar al contenido' } -}) +} as UserConfig function nav(): DefaultTheme.NavItem[] { return [ @@ -66,7 +63,7 @@ function nav(): DefaultTheme.NavItem[] { activeMatch: '/es/reference/' }, { - text: pkg.version, + text: vitepress.version, items: [ { text: 'Registro de cambios', @@ -170,8 +167,8 @@ function sidebarReference(): DefaultTheme.SidebarItem[] { ] } -export const search: DefaultTheme.AlgoliaSearchOptions['locales'] = { - es: { +function searchOptions(): Partial { + return { placeholder: 'Buscar documentos', translations: { button: { diff --git a/docs/.vitepress/config/fa.ts b/docs/fa/config.ts similarity index 95% rename from docs/.vitepress/config/fa.ts rename to docs/fa/config.ts index 5c91d0bc720a..a0508a240277 100644 --- a/docs/.vitepress/config/fa.ts +++ b/docs/fa/config.ts @@ -1,10 +1,7 @@ -import { createRequire } from 'module' -import { defineConfig, type DefaultTheme } from 'vitepress' +import { type DefaultTheme, type UserConfig } from 'vitepress' +import vitepress from 'vitepress/package.json' -const require = createRequire(import.meta.url) -const pkg = require('vitepress/package.json') - -export const fa = defineConfig({ +export default { title: 'ویت‌پرس', lang: 'fa-IR', description: 'Vite & Vue powered static site generator.', @@ -20,6 +17,7 @@ export const fa = defineConfig({ }, themeConfig: { nav: nav(), + search: { options: searchOptions() } as DefaultTheme.Config['search'], sidebar: { '/fa/guide/': { base: '/fa/guide/', items: sidebarGuide() }, '/fa/reference/': { base: '/fa/reference/', items: sidebarReference() } @@ -68,7 +66,7 @@ export const fa = defineConfig({ }, siteTitle: 'ویت‌پرس' } -}) +} as UserConfig function nav(): DefaultTheme.NavItem[] { return [ @@ -83,7 +81,7 @@ function nav(): DefaultTheme.NavItem[] { activeMatch: '/reference/' }, { - text: pkg.version, + text: vitepress.version, items: [ { text: 'Changelog', @@ -181,8 +179,8 @@ function sidebarReference(): DefaultTheme.SidebarItem[] { ] } -export const search: DefaultTheme.AlgoliaSearchOptions['locales'] = { - fa: { +function searchOptions(): Partial { + return { placeholder: 'جستجوی مستندات', translations: { button: { diff --git a/docs/.vitepress/config/ko.ts b/docs/ko/config.ts similarity index 94% rename from docs/.vitepress/config/ko.ts rename to docs/ko/config.ts index faebabf44e56..ccbc7b03cbce 100644 --- a/docs/.vitepress/config/ko.ts +++ b/docs/ko/config.ts @@ -1,16 +1,13 @@ -import { createRequire } from 'module' -import { defineConfig, type DefaultTheme } from 'vitepress' +import { type DefaultTheme, type UserConfig } from 'vitepress' +import vitepress from 'vitepress/package.json' -const require = createRequire(import.meta.url) -const pkg = require('vitepress/package.json') - -export const ko = defineConfig({ +export default { lang: 'ko-KR', description: 'Vite 및 Vue 기반 정적 사이트 생성기.', themeConfig: { nav: nav(), - + search: { options: searchOptions() } as DefaultTheme.Config['search'], sidebar: { '/ko/guide/': { base: '/ko/guide/', items: sidebarGuide() }, '/ko/reference/': { base: '/ko/reference/', items: sidebarReference() } @@ -47,7 +44,7 @@ export const ko = defineConfig({ darkModeSwitchTitle: '다크 모드로 변경', skipToContentLabel: '본문으로 건너뛰기' } -}) +} as UserConfig function nav(): DefaultTheme.NavItem[] { return [ @@ -62,7 +59,7 @@ function nav(): DefaultTheme.NavItem[] { activeMatch: '/ko/reference/' }, { - text: pkg.version, + text: vitepress.version, items: [ { text: '변경 로그', @@ -208,8 +205,8 @@ function sidebarReference(): DefaultTheme.SidebarItem[] { ] } -export const search: DefaultTheme.AlgoliaSearchOptions['locales'] = { - ko: { +function searchOptions(): Partial { + return { placeholder: '문서 검색', translations: { button: { diff --git a/docs/.vitepress/config/pt.ts b/docs/pt/config.ts similarity index 94% rename from docs/.vitepress/config/pt.ts rename to docs/pt/config.ts index 12cb52fae539..891202bf44d6 100644 --- a/docs/.vitepress/config/pt.ts +++ b/docs/pt/config.ts @@ -1,16 +1,13 @@ -import { createRequire } from 'module' -import { defineConfig, type DefaultTheme } from 'vitepress' +import { type DefaultTheme, type UserConfig } from 'vitepress' +import vitepress from 'vitepress/package.json' -const require = createRequire(import.meta.url) -const pkg = require('vitepress/package.json') - -export const pt = defineConfig({ +export default { lang: 'pt-BR', description: 'Gerador de Site Estático desenvolvido com Vite e Vue.', themeConfig: { nav: nav(), - + search: { options: searchOptions() } as DefaultTheme.Config['search'], sidebar: { '/pt/guide/': { base: '/pt/guide/', items: sidebarGuide() }, '/pt/reference/': { base: '/pt/reference/', items: sidebarReference() } @@ -51,7 +48,7 @@ export const pt = defineConfig({ darkModeSwitchTitle: 'Mudar para Modo Escuro', skipToContentLabel: 'Pular para o Conteúdo' } -}) +} as UserConfig function nav(): DefaultTheme.NavItem[] { return [ @@ -66,7 +63,7 @@ function nav(): DefaultTheme.NavItem[] { activeMatch: '/pt/reference/' }, { - text: pkg.version, + text: vitepress.version, items: [ { text: 'Registro de Mudanças', @@ -167,8 +164,8 @@ function sidebarReference(): DefaultTheme.SidebarItem[] { ] } -export const search: DefaultTheme.AlgoliaSearchOptions['locales'] = { - pt: { +function searchOptions(): Partial { + return { placeholder: 'Pesquisar documentos', translations: { button: { diff --git a/docs/.vitepress/config/ru.ts b/docs/ru/config.ts similarity index 95% rename from docs/.vitepress/config/ru.ts rename to docs/ru/config.ts index b75b1b77ee74..93ac435d3e46 100644 --- a/docs/.vitepress/config/ru.ts +++ b/docs/ru/config.ts @@ -1,16 +1,13 @@ -import { createRequire } from 'module' -import { defineConfig, type DefaultTheme } from 'vitepress' +import { type DefaultTheme, type UserConfig } from 'vitepress' +import vitepress from 'vitepress/package.json' -const require = createRequire(import.meta.url) -const pkg = require('vitepress/package.json') - -export const ru = defineConfig({ +export default { lang: 'ru-RU', description: 'Генератор статических сайтов на основе Vite и Vue.', themeConfig: { nav: nav(), - + search: { options: searchOptions() } as DefaultTheme.Config['search'], sidebar: { '/ru/guide/': { base: '/ru/guide/', items: sidebarGuide() }, '/ru/reference/': { base: '/ru/reference/', items: sidebarReference() } @@ -45,7 +42,7 @@ export const ru = defineConfig({ langMenuLabel: 'Изменить язык', skipToContentLabel: 'Перейти к содержимому' } -}) +} function nav(): DefaultTheme.NavItem[] { return [ @@ -60,7 +57,7 @@ function nav(): DefaultTheme.NavItem[] { activeMatch: '/ru/reference/' }, { - text: pkg.version, + text: vitepress.version, items: [ { text: 'Изменения', @@ -163,8 +160,8 @@ function sidebarReference(): DefaultTheme.SidebarItem[] { ] } -export const search: DefaultTheme.AlgoliaSearchOptions['locales'] = { - ru: { +function searchOptions(): Partial { + return { placeholder: 'Поиск в документации', translations: { button: { diff --git a/docs/zh/config.ts b/docs/zh/config.ts index 4120d126d396..6caf14301beb 100644 --- a/docs/zh/config.ts +++ b/docs/zh/config.ts @@ -1,6 +1,5 @@ import { type DefaultTheme, type UserConfig } from 'vitepress' import vitepress from 'vitepress/package.json' -import type { DocSearchProps } from '../../types/docsearch' export default { lang: 'zh-Hans', @@ -165,7 +164,7 @@ function sidebarReference(): DefaultTheme.SidebarItem[] { ] } -function searchOptions(): Partial { +function searchOptions(): Partial { return { placeholder: '搜索文档', translations: { From d33f6ffb28719cef5c91e3a12247cb637fa00dd4 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sun, 30 Mar 2025 00:43:41 -0400 Subject: [PATCH 03/40] fix: modify chunk rules to avoid circular import --- src/node/build/bundle.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/node/build/bundle.ts b/src/node/build/bundle.ts index 662e9593a3f2..67669ad4a5d5 100644 --- a/src/node/build/bundle.ts +++ b/src/node/build/bundle.ts @@ -139,6 +139,12 @@ export async function bundle( ) { return 'framework' } + if (/(^|\/)config\.([cm]?js|ts|json)$/.test(id)) { + return 'framework' + } + if (id.endsWith('/vitepress/package.json')) { + return 'framework' + } if ( (id.startsWith(`${clientDir}/theme-default`) || From fb5dbb2e7211b92a6e0f9207277d1fdc0f0ceaa5 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sun, 30 Mar 2025 00:44:15 -0400 Subject: [PATCH 04/40] update: clean up debug logging code --- src/client/app/data.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/client/app/data.ts b/src/client/app/data.ts index 3a64e948e4fe..d9b80ce077c9 100644 --- a/src/client/app/data.ts +++ b/src/client/app/data.ts @@ -92,12 +92,16 @@ function getExtraConfigs(path: string): SiteData[] { segments.pop() } // debug info - const summaryTitle = `Extra Configs for ${path}:` - const summary = configs.map((c, i) => ` ${i + 1}. ${(c as any).__module__}`) - summary.push(` ${summary.length + 1}. .vitepress/config (root)`) - console.info( - [summaryTitle, ''.padEnd(summaryTitle.length, '='), ...summary].join('\n') - ) + 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 } @@ -160,8 +164,6 @@ export function initData(route: Route): VitePressData { export function useData(): VitePressData { const data = inject(dataSymbol) - ;(window as any).stackView = stackView - ;(window as any).data = data if (!data) { throw new Error('vitepress data not properly injected in app') } From fde8f223e012bd8084589357dcc0a5ef4f4b4b90 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sun, 30 Mar 2025 00:56:12 -0400 Subject: [PATCH 05/40] fix bundle.ts: reliable detection for `vitepress/package.json` --- src/node/build/bundle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/build/bundle.ts b/src/node/build/bundle.ts index 67669ad4a5d5..4349f7cb6b7d 100644 --- a/src/node/build/bundle.ts +++ b/src/node/build/bundle.ts @@ -142,7 +142,7 @@ export async function bundle( if (/(^|\/)config\.([cm]?js|ts|json)$/.test(id)) { return 'framework' } - if (id.endsWith('/vitepress/package.json')) { + if (/(^|\/)vitepress\/package.json$/.test(id)) { return 'framework' } From 8d4ae1ce034a5885b35cf38b78647e1c94ec95a2 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sun, 30 Mar 2025 07:33:45 -0400 Subject: [PATCH 06/40] revert translation.ts: remove unneeded code --- src/client/theme-default/support/translation.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/theme-default/support/translation.ts b/src/client/theme-default/support/translation.ts index 4c285cbb8500..12ade37b6e3f 100644 --- a/src/client/theme-default/support/translation.ts +++ b/src/client/theme-default/support/translation.ts @@ -15,7 +15,6 @@ export function createSearchTranslate( const isObject = themeObject && typeof themeObject === 'object' const locales = (isObject && themeObject.locales?.[localeIndex.value]?.translations) || - (isObject && themeObject?.translations) || null const translations = (isObject && themeObject.translations) || null From 550e5f9b11c7a0c22148e668b36d2c0585892df8 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sun, 30 Mar 2025 09:40:57 -0400 Subject: [PATCH 07/40] revert bundle.ts --- src/node/build/bundle.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/node/build/bundle.ts b/src/node/build/bundle.ts index 4349f7cb6b7d..662e9593a3f2 100644 --- a/src/node/build/bundle.ts +++ b/src/node/build/bundle.ts @@ -139,12 +139,6 @@ export async function bundle( ) { return 'framework' } - if (/(^|\/)config\.([cm]?js|ts|json)$/.test(id)) { - return 'framework' - } - if (/(^|\/)vitepress\/package.json$/.test(id)) { - return 'framework' - } if ( (id.startsWith(`${clientDir}/theme-default`) || From f33687c3b2f5204916a59a8673d79318a8009211 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sun, 30 Mar 2025 11:21:28 -0400 Subject: [PATCH 08/40] major refactor: move main logic into resolveUserConfig() --- src/client/app/data.ts | 56 ++++++++++++++--------------- src/client/app/utils.ts | 6 ---- src/node/config.ts | 78 +++++++++++++++++++++++++++++++++++++++-- src/node/plugin.ts | 13 +++++-- src/node/siteConfig.ts | 9 +++++ types/shared.d.ts | 22 ++++++++++++ 6 files changed, 144 insertions(+), 40 deletions(-) diff --git a/src/client/app/data.ts b/src/client/app/data.ts index d9b80ce077c9..e342a6cf9212 100644 --- a/src/client/app/data.ts +++ b/src/client/app/data.ts @@ -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 = Symbol() @@ -70,49 +70,45 @@ if (import.meta.hot) { }) } -// hierarchical config pre-loading -const extraConfig: Record = 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 diff --git a/src/client/app/utils.ts b/src/client/app/utils.ts index 3190cd0af1e6..b701c08c4bfa 100644 --- a/src/client/app/utils.ts +++ b/src/client/app/utils.ts @@ -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) { diff --git a/src/node/config.ts b/src/node/config.ts index f5e0bb3985b7..a2634034fa92 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -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 } } diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 748c4da48045..f6c3d3f98ea0 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -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` diff --git a/src/node/siteConfig.ts b/src/node/siteConfig.ts index 49451cbeb0de..c591a95608ab 100644 --- a/src/node/siteConfig.ts +++ b/src/node/siteConfig.ts @@ -14,6 +14,7 @@ import type { SSGContext, SiteData } from './shared' +import type { AdditionalConfig } from '../../types/shared' export type RawConfigExports = | Awaitable> @@ -187,6 +188,13 @@ export interface UserConfig pageData: PageData, ctx: TransformPageContext ) => Awaitable | { [key: string]: any } | void> + + /** + * @experimental + * Multi-layer configuration overloading. + * Auto-resolves to docs/.../config.(ts|js|json) when unspecified. + */ + additionalConfig?: AdditionalConfig } export interface SiteConfig @@ -209,6 +217,7 @@ export interface SiteConfig | 'transformHtml' | 'transformPageData' | 'sitemap' + | 'additionalConfig' > { root: string srcDir: string diff --git a/types/shared.d.ts b/types/shared.d.ts index 1ebfa9b98d39..62a618c2207d 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -134,6 +134,7 @@ export interface SiteData { router: { prefetchLinks: boolean } + additionalConfig?: AdditionalConfig } export type HeadConfig = @@ -161,6 +162,27 @@ export interface LocaleSpecificConfig { themeConfig?: ThemeConfig } +export interface AdditionalConfigEntry + extends LocaleSpecificConfig { + /** + * Source of current config entry, only available in development mode + */ + src?: string +} + +export type AdditionalConfigDict = Record< + string, + AdditionalConfigEntry +> + +export type AdditionalConfigLoader = ( + path: string +) => AdditionalConfigEntry[] + +export type AdditionalConfig = + | AdditionalConfigDict + | AdditionalConfigLoader + export type LocaleConfig = Record< string, LocaleSpecificConfig & { label: string; link?: string } From 12b2a5865865bbb5add9a0ef5d8a68f0471928fa Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sun, 30 Mar 2025 11:21:56 -0400 Subject: [PATCH 09/40] update: use node API in distributed config files --- docs/en/config.ts | 13 ++++++++----- docs/es/config.ts | 13 ++++++++----- docs/fa/config.ts | 13 ++++++++----- docs/ko/config.ts | 13 ++++++++----- docs/pt/config.ts | 13 ++++++++----- docs/ru/config.ts | 13 ++++++++----- docs/zh/config.ts | 13 ++++++++----- 7 files changed, 56 insertions(+), 35 deletions(-) diff --git a/docs/en/config.ts b/docs/en/config.ts index 41dbd2b8cd28..6f959538d5e6 100644 --- a/docs/en/config.ts +++ b/docs/en/config.ts @@ -1,7 +1,10 @@ -import { type DefaultTheme, type UserConfig } from 'vitepress' -import vitepress from 'vitepress/package.json' +import { createRequire } from 'module' +import { defineConfig, type DefaultTheme } from 'vitepress' -export default { +const require = createRequire(import.meta.url) +const pkg = require('vitepress/package.json') + +export default defineConfig({ lang: 'en-US', description: 'Vite & Vue powered static site generator.', @@ -22,7 +25,7 @@ export default { copyright: 'Copyright © 2019-present Evan You' } } -} as UserConfig +}) function nav(): DefaultTheme.NavItem[] { return [ @@ -37,7 +40,7 @@ function nav(): DefaultTheme.NavItem[] { activeMatch: '/reference/' }, { - text: vitepress.version, + text: pkg.version, items: [ { text: 'Changelog', diff --git a/docs/es/config.ts b/docs/es/config.ts index e3a0079a072f..1420856d3ff8 100644 --- a/docs/es/config.ts +++ b/docs/es/config.ts @@ -1,7 +1,10 @@ -import { type DefaultTheme, type UserConfig } from 'vitepress' -import vitepress from 'vitepress/package.json' +import { createRequire } from 'module' +import { defineConfig, type DefaultTheme } from 'vitepress' -export default { +const require = createRequire(import.meta.url) +const pkg = require('vitepress/package.json') + +export default defineConfig({ lang: 'es-CO', description: 'Generador de Sitios Estaticos desarrollado con Vite y Vue.', @@ -48,7 +51,7 @@ export default { darkModeSwitchTitle: 'Cambiar a modo oscuro', skipToContentLabel: 'Saltar al contenido' } -} as UserConfig +}) function nav(): DefaultTheme.NavItem[] { return [ @@ -63,7 +66,7 @@ function nav(): DefaultTheme.NavItem[] { activeMatch: '/es/reference/' }, { - text: vitepress.version, + text: pkg.version, items: [ { text: 'Registro de cambios', diff --git a/docs/fa/config.ts b/docs/fa/config.ts index a0508a240277..55f53b52a9dd 100644 --- a/docs/fa/config.ts +++ b/docs/fa/config.ts @@ -1,7 +1,10 @@ -import { type DefaultTheme, type UserConfig } from 'vitepress' -import vitepress from 'vitepress/package.json' +import { createRequire } from 'module' +import { defineConfig, type DefaultTheme } from 'vitepress' -export default { +const require = createRequire(import.meta.url) +const pkg = require('vitepress/package.json') + +export default defineConfig({ title: 'ویت‌پرس', lang: 'fa-IR', description: 'Vite & Vue powered static site generator.', @@ -66,7 +69,7 @@ export default { }, siteTitle: 'ویت‌پرس' } -} as UserConfig +}) function nav(): DefaultTheme.NavItem[] { return [ @@ -81,7 +84,7 @@ function nav(): DefaultTheme.NavItem[] { activeMatch: '/reference/' }, { - text: vitepress.version, + text: pkg.version, items: [ { text: 'Changelog', diff --git a/docs/ko/config.ts b/docs/ko/config.ts index ccbc7b03cbce..08773ccb64e5 100644 --- a/docs/ko/config.ts +++ b/docs/ko/config.ts @@ -1,7 +1,10 @@ -import { type DefaultTheme, type UserConfig } from 'vitepress' -import vitepress from 'vitepress/package.json' +import { createRequire } from 'module' +import { defineConfig, type DefaultTheme } from 'vitepress' -export default { +const require = createRequire(import.meta.url) +const pkg = require('vitepress/package.json') + +export default defineConfig({ lang: 'ko-KR', description: 'Vite 및 Vue 기반 정적 사이트 생성기.', @@ -44,7 +47,7 @@ export default { darkModeSwitchTitle: '다크 모드로 변경', skipToContentLabel: '본문으로 건너뛰기' } -} as UserConfig +}) function nav(): DefaultTheme.NavItem[] { return [ @@ -59,7 +62,7 @@ function nav(): DefaultTheme.NavItem[] { activeMatch: '/ko/reference/' }, { - text: vitepress.version, + text: pkg.version, items: [ { text: '변경 로그', diff --git a/docs/pt/config.ts b/docs/pt/config.ts index 891202bf44d6..aa08b7faa87b 100644 --- a/docs/pt/config.ts +++ b/docs/pt/config.ts @@ -1,7 +1,10 @@ -import { type DefaultTheme, type UserConfig } from 'vitepress' -import vitepress from 'vitepress/package.json' +import { createRequire } from 'module' +import { defineConfig, type DefaultTheme } from 'vitepress' -export default { +const require = createRequire(import.meta.url) +const pkg = require('vitepress/package.json') + +export default defineConfig({ lang: 'pt-BR', description: 'Gerador de Site Estático desenvolvido com Vite e Vue.', @@ -48,7 +51,7 @@ export default { darkModeSwitchTitle: 'Mudar para Modo Escuro', skipToContentLabel: 'Pular para o Conteúdo' } -} as UserConfig +}) function nav(): DefaultTheme.NavItem[] { return [ @@ -63,7 +66,7 @@ function nav(): DefaultTheme.NavItem[] { activeMatch: '/pt/reference/' }, { - text: vitepress.version, + text: pkg.version, items: [ { text: 'Registro de Mudanças', diff --git a/docs/ru/config.ts b/docs/ru/config.ts index 93ac435d3e46..41582f5ee14c 100644 --- a/docs/ru/config.ts +++ b/docs/ru/config.ts @@ -1,7 +1,10 @@ -import { type DefaultTheme, type UserConfig } from 'vitepress' -import vitepress from 'vitepress/package.json' +import { createRequire } from 'module' +import { defineConfig, type DefaultTheme } from 'vitepress' -export default { +const require = createRequire(import.meta.url) +const pkg = require('vitepress/package.json') + +export default defineConfig({ lang: 'ru-RU', description: 'Генератор статических сайтов на основе Vite и Vue.', @@ -42,7 +45,7 @@ export default { langMenuLabel: 'Изменить язык', skipToContentLabel: 'Перейти к содержимому' } -} +}) function nav(): DefaultTheme.NavItem[] { return [ @@ -57,7 +60,7 @@ function nav(): DefaultTheme.NavItem[] { activeMatch: '/ru/reference/' }, { - text: vitepress.version, + text: pkg.version, items: [ { text: 'Изменения', diff --git a/docs/zh/config.ts b/docs/zh/config.ts index 6caf14301beb..f8ff24ee947b 100644 --- a/docs/zh/config.ts +++ b/docs/zh/config.ts @@ -1,7 +1,10 @@ -import { type DefaultTheme, type UserConfig } from 'vitepress' -import vitepress from 'vitepress/package.json' +import { createRequire } from 'module' +import { defineConfig, type DefaultTheme } from 'vitepress' -export default { +const require = createRequire(import.meta.url) +const pkg = require('vitepress/package.json') + +export default defineConfig({ lang: 'zh-Hans', description: '由 Vite 和 Vue 驱动的静态站点生成器', @@ -55,7 +58,7 @@ export default { darkModeSwitchTitle: '切换到深色模式', skipToContentLabel: '跳转到内容' } -} as UserConfig +}) function nav(): DefaultTheme.NavItem[] { return [ @@ -70,7 +73,7 @@ function nav(): DefaultTheme.NavItem[] { activeMatch: '/zh/reference/' }, { - text: vitepress.version, + text: pkg.version, items: [ { text: '更新日志', From a8428fe1fabea65c883cbe0aca5169147de75755 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sun, 30 Mar 2025 11:26:26 -0400 Subject: [PATCH 10/40] fix: remove debug code --- src/client/app/data.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/app/data.ts b/src/client/app/data.ts index e342a6cf9212..64a6b6be34cb 100644 --- a/src/client/app/data.ts +++ b/src/client/app/data.ts @@ -71,7 +71,7 @@ if (import.meta.hot) { } function debugConfigLayers(path: string, layers: SiteData[]): SiteData[] { - // debug info + // 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) => { @@ -105,7 +105,6 @@ function getConfigLayers(root: SiteData, path: string): SiteData[] { // per-app data export function initData(route: Route): VitePressData { const site = computed(() => { - ;(window as any).siteData = siteDataRef.value const path = route.data.relativePath const data = resolveSiteDataByRoute(siteDataRef.value, path) return stackView(...debugConfigLayers(path, getConfigLayers(data, path))) From 0fcb91651845ed3fe9348f82cc94e3be7564ff4d Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sun, 30 Mar 2025 11:56:03 -0400 Subject: [PATCH 11/40] update: remove unused code --- src/node/siteConfig.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/node/siteConfig.ts b/src/node/siteConfig.ts index c591a95608ab..e09352eeb465 100644 --- a/src/node/siteConfig.ts +++ b/src/node/siteConfig.ts @@ -217,7 +217,6 @@ export interface SiteConfig | 'transformHtml' | 'transformPageData' | 'sitemap' - | 'additionalConfig' > { root: string srcDir: string From 721124639941f184d893888d8faf70ca019d99b1 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sun, 30 Mar 2025 12:23:51 -0400 Subject: [PATCH 12/40] fix: move `/en/config.ts` to `/config.ts`, allow root-level configuration overloading --- docs/{en => }/config.ts | 0 src/client/app/data.ts | 1 + 2 files changed, 1 insertion(+) rename docs/{en => }/config.ts (100%) diff --git a/docs/en/config.ts b/docs/config.ts similarity index 100% rename from docs/en/config.ts rename to docs/config.ts diff --git a/src/client/app/data.ts b/src/client/app/data.ts index 64a6b6be34cb..abf379b75213 100644 --- a/src/client/app/data.ts +++ b/src/client/app/data.ts @@ -99,6 +99,7 @@ function getConfigLayers(root: SiteData, path: string): SiteData[] { if (key in additionalConfig) configs.push(additionalConfig[key] as SiteData) segments.pop() } + if ('/' in additionalConfig) configs.push(additionalConfig['/'] as SiteData) return [...configs, root] } From f5bdd2bbca066dd68dbfdb3b63beb87872a9f7d1 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Mon, 31 Mar 2025 17:19:20 -0400 Subject: [PATCH 13/40] update: add config type helper, clean up code --- src/client/app/data.ts | 2 +- src/node/siteConfig.ts | 9 +++++++-- types/shared.d.ts | 30 ++++++++++++++++-------------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/client/app/data.ts b/src/client/app/data.ts index abf379b75213..b7770ab04235 100644 --- a/src/client/app/data.ts +++ b/src/client/app/data.ts @@ -77,7 +77,7 @@ function debugConfigLayers(path: string, layers: SiteData[]): SiteData[] { 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)'}` + return `${n}. ${(c as any)?.['[VP_SOURCE]'] ?? '(Unknown Source)'}` }) console.debug( [summaryTitle, ''.padEnd(summaryTitle.length, '='), ...summary].join('\n') diff --git a/src/node/siteConfig.ts b/src/node/siteConfig.ts index e09352eeb465..bffa8015c8f5 100644 --- a/src/node/siteConfig.ts +++ b/src/node/siteConfig.ts @@ -14,7 +14,10 @@ import type { SSGContext, SiteData } from './shared' -import type { AdditionalConfig } from '../../types/shared' +import type { + AdditionalConfigDict, + AdditionalConfigLoader +} from '../../types/shared' export type RawConfigExports = | Awaitable> @@ -194,7 +197,9 @@ export interface UserConfig * Multi-layer configuration overloading. * Auto-resolves to docs/.../config.(ts|js|json) when unspecified. */ - additionalConfig?: AdditionalConfig + additionalConfig?: + | AdditionalConfigDict + | AdditionalConfigLoader } export interface SiteConfig diff --git a/types/shared.d.ts b/types/shared.d.ts index 62a618c2207d..b0f9bbd5d9a3 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -5,6 +5,14 @@ export type { DefaultTheme } from './default-theme.js' export type Awaitable = T | PromiseLike +// Beware that this may cause performance issues or infinite loops +// Use only when absolutely necessary +export type DeepPartial = T extends object + ? { + [K in keyof T]?: DeepPartial + } + : T + export interface PageData { relativePath: string /** @@ -134,7 +142,9 @@ export interface SiteData { router: { prefetchLinks: boolean } - additionalConfig?: AdditionalConfig + additionalConfig?: + | AdditionalConfigDict + | AdditionalConfigLoader } export type HeadConfig = @@ -162,26 +172,18 @@ export interface LocaleSpecificConfig { themeConfig?: ThemeConfig } -export interface AdditionalConfigEntry - extends LocaleSpecificConfig { - /** - * Source of current config entry, only available in development mode - */ - src?: string -} +export type AdditionalConfig = DeepPartial< + LocaleSpecificConfig +> export type AdditionalConfigDict = Record< string, - AdditionalConfigEntry + AdditionalConfig > export type AdditionalConfigLoader = ( path: string -) => AdditionalConfigEntry[] - -export type AdditionalConfig = - | AdditionalConfigDict - | AdditionalConfigLoader +) => AdditionalConfig[] export type LocaleConfig = Record< string, From 64000272b48c27a2db40ad5d6cf7fd0b044af5c4 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Mon, 31 Mar 2025 17:19:57 -0400 Subject: [PATCH 14/40] update docs/*/config.ts: use defineAdditionalConfig() instead of defineConfig --- docs/config.ts | 4 ++-- docs/es/config.ts | 6 +++--- docs/fa/config.ts | 6 +++--- docs/ko/config.ts | 6 +++--- docs/pt/config.ts | 6 +++--- docs/ru/config.ts | 6 +++--- docs/zh/config.ts | 6 +++--- src/node/config.ts | 28 ++++++++++++++++++++++------ 8 files changed, 42 insertions(+), 26 deletions(-) diff --git a/docs/config.ts b/docs/config.ts index 6f959538d5e6..b4cc4d2a1f32 100644 --- a/docs/config.ts +++ b/docs/config.ts @@ -1,10 +1,10 @@ import { createRequire } from 'module' -import { defineConfig, type DefaultTheme } from 'vitepress' +import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') -export default defineConfig({ +export default defineAdditionalConfig({ lang: 'en-US', description: 'Vite & Vue powered static site generator.', diff --git a/docs/es/config.ts b/docs/es/config.ts index 1420856d3ff8..141802805fc7 100644 --- a/docs/es/config.ts +++ b/docs/es/config.ts @@ -1,16 +1,16 @@ import { createRequire } from 'module' -import { defineConfig, type DefaultTheme } from 'vitepress' +import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') -export default defineConfig({ +export default defineAdditionalConfig({ lang: 'es-CO', description: 'Generador de Sitios Estaticos desarrollado con Vite y Vue.', themeConfig: { nav: nav(), - search: { options: searchOptions() } as DefaultTheme.Config['search'], + search: { options: searchOptions() }, sidebar: { '/es/guide/': { base: '/es/guide/', items: sidebarGuide() }, '/es/reference/': { base: '/es/reference/', items: sidebarReference() } diff --git a/docs/fa/config.ts b/docs/fa/config.ts index 55f53b52a9dd..6d38e2506cdc 100644 --- a/docs/fa/config.ts +++ b/docs/fa/config.ts @@ -1,10 +1,10 @@ import { createRequire } from 'module' -import { defineConfig, type DefaultTheme } from 'vitepress' +import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') -export default defineConfig({ +export default defineAdditionalConfig({ title: 'ویت‌پرس', lang: 'fa-IR', description: 'Vite & Vue powered static site generator.', @@ -20,7 +20,7 @@ export default defineConfig({ }, themeConfig: { nav: nav(), - search: { options: searchOptions() } as DefaultTheme.Config['search'], + search: { options: searchOptions() }, sidebar: { '/fa/guide/': { base: '/fa/guide/', items: sidebarGuide() }, '/fa/reference/': { base: '/fa/reference/', items: sidebarReference() } diff --git a/docs/ko/config.ts b/docs/ko/config.ts index 08773ccb64e5..b6d30187f5e9 100644 --- a/docs/ko/config.ts +++ b/docs/ko/config.ts @@ -1,16 +1,16 @@ import { createRequire } from 'module' -import { defineConfig, type DefaultTheme } from 'vitepress' +import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') -export default defineConfig({ +export default defineAdditionalConfig({ lang: 'ko-KR', description: 'Vite 및 Vue 기반 정적 사이트 생성기.', themeConfig: { nav: nav(), - search: { options: searchOptions() } as DefaultTheme.Config['search'], + search: { options: searchOptions() }, sidebar: { '/ko/guide/': { base: '/ko/guide/', items: sidebarGuide() }, '/ko/reference/': { base: '/ko/reference/', items: sidebarReference() } diff --git a/docs/pt/config.ts b/docs/pt/config.ts index aa08b7faa87b..d03e0e64596f 100644 --- a/docs/pt/config.ts +++ b/docs/pt/config.ts @@ -1,16 +1,16 @@ import { createRequire } from 'module' -import { defineConfig, type DefaultTheme } from 'vitepress' +import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') -export default defineConfig({ +export default defineAdditionalConfig({ lang: 'pt-BR', description: 'Gerador de Site Estático desenvolvido com Vite e Vue.', themeConfig: { nav: nav(), - search: { options: searchOptions() } as DefaultTheme.Config['search'], + search: { options: searchOptions() }, sidebar: { '/pt/guide/': { base: '/pt/guide/', items: sidebarGuide() }, '/pt/reference/': { base: '/pt/reference/', items: sidebarReference() } diff --git a/docs/ru/config.ts b/docs/ru/config.ts index 41582f5ee14c..5f856b1dd54c 100644 --- a/docs/ru/config.ts +++ b/docs/ru/config.ts @@ -1,16 +1,16 @@ import { createRequire } from 'module' -import { defineConfig, type DefaultTheme } from 'vitepress' +import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') -export default defineConfig({ +export default defineAdditionalConfig({ lang: 'ru-RU', description: 'Генератор статических сайтов на основе Vite и Vue.', themeConfig: { nav: nav(), - search: { options: searchOptions() } as DefaultTheme.Config['search'], + search: { options: searchOptions() }, sidebar: { '/ru/guide/': { base: '/ru/guide/', items: sidebarGuide() }, '/ru/reference/': { base: '/ru/reference/', items: sidebarReference() } diff --git a/docs/zh/config.ts b/docs/zh/config.ts index f8ff24ee947b..cf06d58a448e 100644 --- a/docs/zh/config.ts +++ b/docs/zh/config.ts @@ -1,16 +1,16 @@ import { createRequire } from 'module' -import { defineConfig, type DefaultTheme } from 'vitepress' +import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') -export default defineConfig({ +export default defineAdditionalConfig({ lang: 'zh-Hans', description: '由 Vite 和 Vue 驱动的静态站点生成器', themeConfig: { nav: nav(), - search: { options: searchOptions() } as DefaultTheme.Config['search'], + search: { options: searchOptions() }, sidebar: { '/zh/guide/': { base: '/zh/guide/', items: sidebarGuide() }, '/zh/reference/': { base: '/zh/reference/', items: sidebarReference() } diff --git a/src/node/config.ts b/src/node/config.ts index a2634034fa92..f657c3073c95 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -14,10 +14,7 @@ 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 type { AdditionalConfig, AdditionalConfigDict } from '../../types/shared' import { glob } from 'tinyglobby' export { resolvePages } from './plugins/dynamicRoutesPlugin' @@ -43,6 +40,15 @@ export function defineConfig(config: UserConfig) { return config } +/** + * Type additional config helper + */ +export function defineAdditionalConfig( + config: AdditionalConfig +) { + return config +} + /** * Type config helper for custom theme config */ @@ -52,6 +58,15 @@ export function defineConfigWithTheme( return config } +/** + * Type additional config helper + */ +export function defineAdditionalConfigWithTheme( + config: AdditionalConfig +) { + return config +} + export async function resolveConfig( root: string = process.cwd(), command: 'serve' | 'build' = 'serve', @@ -198,8 +213,9 @@ async function gatherAdditionalConfig( normalizePath(path.resolve(file)) ) ) - if (mode === 'development') (configExports.config as any).VP_SOURCE = file - return [id, configExports.config as AdditionalConfigEntry] + if (mode === 'development') + (configExports.config as any)['[VP_SOURCE]'] = file + return [id, configExports.config as AdditionalConfig] }) ) return [Object.fromEntries(exports.filter(([id, config]) => config)), deps] From c8bef32aaffa9aeb652ae809b50459f55dd9fc97 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Thu, 3 Apr 2025 03:40:31 -0400 Subject: [PATCH 15/40] fix: respect user configured `srcDir` --- src/node/config.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/node/config.ts b/src/node/config.ts index f657c3073c95..138d4515f06c 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -187,11 +187,12 @@ function dirname(path: string) { async function gatherAdditionalConfig( root: string, command: 'serve' | 'build', - mode: string + mode: string, + srcDir: string = '.' ): Promise<[AdditionalConfigDict, string[][]]> { const pattern = `**/config.{${supportedConfigExtensions.join(',')}}` const candidates = await glob(pattern, { - cwd: root, + cwd: path.resolve(root, srcDir), dot: false, // conveniently ignores .vitepress/* ignore: ['**/node_modules/**', '**/.git/**'] }) @@ -201,7 +202,7 @@ async function gatherAdditionalConfig( const id = '/' + dirname(slash(file)) const configExports = await loadConfigFromFile( { command, mode }, - normalizePath(path.resolve(root, file)), + normalizePath(path.resolve(root, srcDir, file)), root ).catch(console.error) // Skip additionalConfig file if it fails to load if (!configExports) { @@ -214,7 +215,7 @@ async function gatherAdditionalConfig( ) ) if (mode === 'development') - (configExports.config as any)['[VP_SOURCE]'] = file + (configExports.config as any)['[VP_SOURCE]'] = '/' + slash(file) return [id, configExports.config as AdditionalConfig] }) ) @@ -254,7 +255,8 @@ export async function resolveUserConfig( const [additionalConfig, additionalDeps] = await gatherAdditionalConfig( root, command, - mode + mode, + userConfig.srcDir ) userConfig.additionalConfig = additionalConfig configDeps = configDeps.concat(...additionalDeps) From 787b82599b10d649dd6d852acd664337be2f8384 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sat, 5 Apr 2025 07:14:33 -0400 Subject: [PATCH 16/40] 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 b7770ab04235..16288e8714c4 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 b701c08c4bfa..53ef6e514104 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 138d4515f06c..8518d9f223f3 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 0dd35db48376..0602088ceab4 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] +} From f4c18e87ac530287076a7af14bf83a3a7276a020 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sat, 5 Apr 2025 16:24:05 -0400 Subject: [PATCH 17/40] fix: preserve original mergeHead() behavior --- src/shared/shared.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/shared/shared.ts b/src/shared/shared.ts index 0602088ceab4..ca6ce7082f96 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -168,18 +168,16 @@ function hasTag(head: HeadConfig[], tag: HeadConfig) { ) } -export function mergeHead(current: HeadConfig[], ...incoming: HeadConfig[][]) { - return incoming - .filter((el) => Array.isArray(el) && el.length > 0) +// Merge head tags, overwrite duplicate meta tags in ascending precedence +export function mergeHead(...heads: HeadConfig[][]) { + return heads + .filter(Array.isArray) .flat(1) .reverse() - .reduce( - (merged, tag) => { - if (!hasTag(merged, tag)) merged.push(tag) - return merged - }, - [...current] - ) + .reduce((merged, tag) => { + if (!hasTag(merged, tag)) merged.push(tag) + return merged + }, []) as HeadConfig[] } // https://github.com/rollup/rollup/blob/fec513270c6ac350072425cc045db367656c623b/src/utils/sanitizeFileName.ts From fb8d4e51c5210a745d6a67bc53d3227cbe0342c7 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sat, 5 Apr 2025 16:30:01 -0400 Subject: [PATCH 18/40] update: revert last change, rename `mergeHead` arguments for clarity. --- src/shared/shared.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/shared/shared.ts b/src/shared/shared.ts index ca6ce7082f96..5d19338ae9e7 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -168,16 +168,18 @@ function hasTag(head: HeadConfig[], tag: HeadConfig) { ) } -// Merge head tags, overwrite duplicate meta tags in ascending precedence -export function mergeHead(...heads: HeadConfig[][]) { - return heads +export function mergeHead(incoming: HeadConfig[], ...current: HeadConfig[][]) { + return current .filter(Array.isArray) .flat(1) .reverse() - .reduce((merged, tag) => { - if (!hasTag(merged, tag)) merged.push(tag) - return merged - }, []) as HeadConfig[] + .reduce( + (merged, tag) => { + if (!hasTag(merged, tag)) merged.push(tag) + return merged + }, + [...incoming] + ) } // https://github.com/rollup/rollup/blob/fec513270c6ac350072425cc045db367656c623b/src/utils/sanitizeFileName.ts From 13bbf1b0d2f57262e510da749805d224b15e66fd Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sat, 5 Apr 2025 16:36:51 -0400 Subject: [PATCH 19/40] Revert "update: revert last change, rename `mergeHead` arguments for clarity." This reverts commit fb8d4e51c5210a745d6a67bc53d3227cbe0342c7. --- src/shared/shared.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/shared/shared.ts b/src/shared/shared.ts index 5d19338ae9e7..ca6ce7082f96 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -168,18 +168,16 @@ function hasTag(head: HeadConfig[], tag: HeadConfig) { ) } -export function mergeHead(incoming: HeadConfig[], ...current: HeadConfig[][]) { - return current +// Merge head tags, overwrite duplicate meta tags in ascending precedence +export function mergeHead(...heads: HeadConfig[][]) { + return heads .filter(Array.isArray) .flat(1) .reverse() - .reduce( - (merged, tag) => { - if (!hasTag(merged, tag)) merged.push(tag) - return merged - }, - [...incoming] - ) + .reduce((merged, tag) => { + if (!hasTag(merged, tag)) merged.push(tag) + return merged + }, []) as HeadConfig[] } // https://github.com/rollup/rollup/blob/fec513270c6ac350072425cc045db367656c623b/src/utils/sanitizeFileName.ts From bd80a64c126b226bf919315e72cfd89258a5517d Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sat, 5 Apr 2025 16:38:41 -0400 Subject: [PATCH 20/40] update: clarify comments for `mergeHead` --- src/shared/shared.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/shared/shared.ts b/src/shared/shared.ts index ca6ce7082f96..594fc1726f90 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -168,7 +168,10 @@ function hasTag(head: HeadConfig[], tag: HeadConfig) { ) } -// Merge head tags, overwrite duplicate meta tags in ascending precedence +/** + * Merge head tags ascending precedence + * Prior duplicates are skipped in favor of later ones + */ export function mergeHead(...heads: HeadConfig[][]) { return heads .filter(Array.isArray) From 72627effb3957e913e9a0acd15f1d771bad2a41a Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sun, 6 Apr 2025 00:24:06 -0400 Subject: [PATCH 21/40] fix: mitigate netlify javascript heap out of memory issue --- docs/.vitepress/config.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 1f23e1fd7a81..3e6d48d71363 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -5,6 +5,15 @@ import { localIconLoader } from 'vitepress-plugin-group-icons' +let buildConcurrency: number +try { + const { cpus } = await import('os') + buildConcurrency = 4 * cpus().length +} catch { + // edge services may not expose os module + buildConcurrency = 16 +} + export default defineConfig({ title: 'VitePress', @@ -15,6 +24,7 @@ export default defineConfig({ lastUpdated: true, cleanUrls: true, metaChunk: true, + buildConcurrency, markdown: { math: true, From a7e8c2cc4af932034a7ba8bb33452c0be1cd5fde Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sun, 13 Apr 2025 20:39:39 +0530 Subject: [PATCH 22/40] remove build concurrency workaround - no longer needed - didn't actually fix the actual issue which is because of rollup-plugin-dts not vitepress build - also buildConcurrency shouldn't be decided on the basis of number of threads but the available memory, it is used to compute how many pages are rendered at once, all of that is run on a single thread --- docs/.vitepress/config.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 2bfca70e3f8d..9074e08f6db4 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -6,15 +6,6 @@ import { } from 'vitepress-plugin-group-icons' import llmstxt from 'vitepress-plugin-llms' -let buildConcurrency: number -try { - const { cpus } = await import('os') - buildConcurrency = 4 * cpus().length -} catch { - // edge services may not expose os module - buildConcurrency = 16 -} - export default defineConfig({ title: 'VitePress', @@ -25,7 +16,6 @@ export default defineConfig({ lastUpdated: true, cleanUrls: true, metaChunk: true, - buildConcurrency, markdown: { math: true, From 01f600fd87ab5799b4638dcadfbc051facb7dd46 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sun, 13 Apr 2025 20:52:11 +0530 Subject: [PATCH 23/40] update configs --- docs/es/config.ts | 14 +++++++++----- docs/fa/config.ts | 35 +++++++++++------------------------ docs/ko/config.ts | 8 ++++++++ docs/pt/config.ts | 14 +++++++++----- docs/ru/config.ts | 8 ++++++++ docs/zh/config.ts | 13 +++++-------- 6 files changed, 50 insertions(+), 42 deletions(-) diff --git a/docs/es/config.ts b/docs/es/config.ts index 141802805fc7..c923e6d62b17 100644 --- a/docs/es/config.ts +++ b/docs/es/config.ts @@ -36,11 +36,15 @@ export default defineAdditionalConfig({ }, lastUpdated: { - text: 'Actualizado en', - formatOptions: { - dateStyle: 'short', - timeStyle: 'medium' - } + text: 'Actualizado en' + }, + + notFound: { + title: 'PÁGINA NO ENCONTRADA', + quote: + 'Pero si no cambias de dirección y sigues buscando, podrías terminar donde te diriges.', + linkLabel: 'ir a inicio', + linkText: 'Llévame a casa' }, langMenuLabel: 'Cambiar Idioma', diff --git a/docs/fa/config.ts b/docs/fa/config.ts index 6d38e2506cdc..3aa88813933b 100644 --- a/docs/fa/config.ts +++ b/docs/fa/config.ts @@ -5,19 +5,10 @@ const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') export default defineAdditionalConfig({ - title: 'ویت‌پرس', lang: 'fa-IR', - description: 'Vite & Vue powered static site generator.', + description: 'ژنراتور استاتیک وب‌سایت با Vite و Vue', dir: 'rtl', - markdown: { - container: { - tipLabel: 'نکته', - warningLabel: 'هشدار', - dangerLabel: 'خطر', - infoLabel: 'اطلاعات', - detailsLabel: 'جزئیات' - } - }, + themeConfig: { nav: nav(), search: { options: searchOptions() }, @@ -46,11 +37,15 @@ export default defineAdditionalConfig({ }, lastUpdated: { - text: 'آخرین به‌روزرسانی‌', - formatOptions: { - dateStyle: 'short', - timeStyle: 'medium' - } + text: 'آخرین به‌روزرسانی‌' + }, + + notFound: { + title: 'صفحه پیدا نشد', + quote: + 'اما اگر جهت خود را تغییر ندهید و همچنان به جستجو ادامه دهید، ممکن است در نهایت به جایی برسید که در حال رفتن به آن هستید.', + linkLabel: 'برو به خانه', + linkText: 'من را به خانه ببر' }, langMenuLabel: 'تغییر زبان', @@ -59,14 +54,6 @@ export default defineAdditionalConfig({ darkModeSwitchLabel: 'تم تاریک', lightModeSwitchTitle: 'رفتن به حالت روشن', darkModeSwitchTitle: 'رفتن به حالت تاریک', - notFound: { - linkLabel: 'بازگشت به خانه', - linkText: 'بازگشت به خانه', - title: 'صفحه مورد نظر یافت نشد', - code: '۴۰۴', - quote: - 'اما اگر جهت خود را تغییر ندهید و اگر ادامه دهید به دنبال چیزی که دنبال می‌کنید، ممکن است در نهایت به جایی که در حال رفتن به سمتش هستید، برسید.' - }, siteTitle: 'ویت‌پرس' } }) diff --git a/docs/ko/config.ts b/docs/ko/config.ts index b6d30187f5e9..3100fc76aea0 100644 --- a/docs/ko/config.ts +++ b/docs/ko/config.ts @@ -39,6 +39,14 @@ export default defineAdditionalConfig({ text: '업데이트 날짜' }, + notFound: { + title: '페이지를 찾을 수 없습니다', + quote: + '방향을 바꾸지 않고 계속 찾다 보면 결국 당신이 가고 있는 곳에 도달할 수도 있습니다.', + linkLabel: '홈으로 가기', + linkText: '집으로 데려가줘' + }, + langMenuLabel: '언어 변경', returnToTopLabel: '맨 위로 돌아가기', sidebarMenuLabel: '사이드바 메뉴', diff --git a/docs/pt/config.ts b/docs/pt/config.ts index d03e0e64596f..6a8989222b83 100644 --- a/docs/pt/config.ts +++ b/docs/pt/config.ts @@ -36,11 +36,15 @@ export default defineAdditionalConfig({ }, lastUpdated: { - text: 'Atualizado em', - formatOptions: { - dateStyle: 'short', - timeStyle: 'medium' - } + text: 'Atualizado em' + }, + + notFound: { + title: 'PÁGINA NÃO ENCONTRADA', + quote: + 'Mas se você não mudar de direção e continuar procurando, pode acabar onde está indo.', + linkLabel: 'ir para a página inicial', + linkText: 'Me leve para casa' }, langMenuLabel: 'Alterar Idioma', diff --git a/docs/ru/config.ts b/docs/ru/config.ts index 5f856b1dd54c..f9bd10cc7c07 100644 --- a/docs/ru/config.ts +++ b/docs/ru/config.ts @@ -37,6 +37,14 @@ export default defineAdditionalConfig({ text: 'Обновлено' }, + notFound: { + title: 'СТРАНИЦА НЕ НАЙДЕНА', + quote: + 'Но если ты не изменишь направление и продолжишь искать, ты можешь оказаться там, куда направляешься.', + linkLabel: 'перейти на главную', + linkText: 'Отведи меня домой' + }, + darkModeSwitchLabel: 'Оформление', lightModeSwitchTitle: 'Переключить на светлую тему', darkModeSwitchTitle: 'Переключить на тёмную тему', diff --git a/docs/zh/config.ts b/docs/zh/config.ts index cf06d58a448e..fc8cf0a5bd0e 100644 --- a/docs/zh/config.ts +++ b/docs/zh/config.ts @@ -36,18 +36,15 @@ export default defineAdditionalConfig({ }, lastUpdated: { - text: '最后更新于', - formatOptions: { - dateStyle: 'short', - timeStyle: 'medium' - } + text: '最后更新于' }, notFound: { title: '页面未找到', - quote: '若你不改变航向,始终凝望远方,你终将抵达前行的彼岸。', - linkLabel: '返回首页', - linkText: '返回首页' + quote: + '但如果你不改变方向,并且继续寻找,你可能最终会到达你所前往的地方。', + linkLabel: '前往首页', + linkText: '带我回首页' }, langMenuLabel: '多语言', From 886be3245ad4bc33f9a965416f23a92d85937b9d Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sun, 13 Apr 2025 21:22:30 +0530 Subject: [PATCH 24/40] no need to export deep partial and exclude non-pure-object stuff --- types/shared.d.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/types/shared.d.ts b/types/shared.d.ts index b0f9bbd5d9a3..28bb81a07f07 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -5,13 +5,18 @@ export type { DefaultTheme } from './default-theme.js' export type Awaitable = T | PromiseLike -// Beware that this may cause performance issues or infinite loops -// Use only when absolutely necessary -export type DeepPartial = T extends object - ? { - [K in keyof T]?: DeepPartial - } - : T +type DeepPartial = + T extends Record + ? T extends + | Date + | RegExp + | Function + | ReadonlyMap + | ReadonlySet + | ReadonlyArray + ? T + : { [P in keyof T]?: DeepPartial } + : T export interface PageData { relativePath: string From 83eb77dfda84ac25f1d201ccba49c0a7f2761e77 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sun, 13 Apr 2025 21:37:55 +0530 Subject: [PATCH 25/40] LocaleConfig can also now accept partial values --- types/shared.d.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/types/shared.d.ts b/types/shared.d.ts index 28bb81a07f07..18fe9d175229 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -174,13 +174,17 @@ export interface LocaleSpecificConfig { titleTemplate?: string | boolean description?: string head?: HeadConfig[] - themeConfig?: ThemeConfig + themeConfig?: DeepPartial } -export type AdditionalConfig = DeepPartial< - LocaleSpecificConfig +export type LocaleConfig = Record< + string, + LocaleSpecificConfig & { label: string; link?: string } > +export type AdditionalConfig = + LocaleSpecificConfig + export type AdditionalConfigDict = Record< string, AdditionalConfig @@ -190,14 +194,8 @@ export type AdditionalConfigLoader = ( path: string ) => AdditionalConfig[] -export type LocaleConfig = Record< - string, - LocaleSpecificConfig & { label: string; link?: string } -> - // Manually declaring all properties as rollup-plugin-dts // is unable to merge augmented module declarations - export interface MarkdownEnv { /** * The raw Markdown content without frontmatter From 13b215373f75dcb6d07c78ae9caea77c382bb652 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sun, 13 Apr 2025 21:38:22 +0530 Subject: [PATCH 26/40] unrelated, add llms.txt plugin on build only --- docs/.vitepress/config.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 9074e08f6db4..dc755a698870 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -120,10 +120,11 @@ export default defineConfig({ firebase: 'logos:firebase' } }), - llmstxt({ - workDir: 'en', - ignoreFiles: ['index.md'] - }) + process.env.NODE_ENV === 'production' && + llmstxt({ + workDir: 'en', + ignoreFiles: ['index.md'] + }) ] } }) From 2dd4d2b48fab6cf7fd539c68824e49c257caebe4 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sun, 13 Apr 2025 21:41:23 +0530 Subject: [PATCH 27/40] update comment --- src/node/siteConfig.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/node/siteConfig.ts b/src/node/siteConfig.ts index bffa8015c8f5..7f1758bfa688 100644 --- a/src/node/siteConfig.ts +++ b/src/node/siteConfig.ts @@ -193,9 +193,12 @@ export interface UserConfig ) => Awaitable | { [key: string]: any } | void> /** - * @experimental * Multi-layer configuration overloading. - * Auto-resolves to docs/.../config.(ts|js|json) when unspecified. + * Auto-resolves to `docs/.../config.(ts|js|json)` when unspecified. + * + * Set to `{}` to opt-out. + * + * @experimental */ additionalConfig?: | AdditionalConfigDict From e83577761571416970f9c66edfb75a25a72ae0b8 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sun, 13 Apr 2025 21:45:11 +0530 Subject: [PATCH 28/40] adjust imports --- src/node/config.ts | 5 +++-- src/shared/shared.ts | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/node/config.ts b/src/node/config.ts index 8518d9f223f3..90deca053fd8 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -2,6 +2,7 @@ import _debug from 'debug' import fs from 'fs-extra' import path from 'node:path' import c from 'picocolors' +import { glob } from 'tinyglobby' import { createLogger, loadConfigFromFile, @@ -16,12 +17,12 @@ import { APPEARANCE_KEY, VP_SOURCE_KEY, slash, + type AdditionalConfig, + type AdditionalConfigDict, type HeadConfig, type SiteData } from './shared' import type { RawConfigExports, SiteConfig, UserConfig } from './siteConfig' -import type { AdditionalConfig, AdditionalConfigDict } from '../../types/shared' -import { glob } from 'tinyglobby' export { resolvePages } from './plugins/dynamicRoutesPlugin' export * from './siteConfig' diff --git a/src/shared/shared.ts b/src/shared/shared.ts index 594fc1726f90..2701e97ec1b0 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -11,7 +11,9 @@ export type { PageData, PageDataPayload, SiteData, - SSGContext + SSGContext, + AdditionalConfig, + AdditionalConfigDict } from '../../types/shared' export const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i From b9d841951bd87be0cd4c5d23d092a6b502f41461 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sun, 13 Apr 2025 22:39:01 +0530 Subject: [PATCH 29/40] update type helpers --- src/node/config.ts | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/node/config.ts b/src/node/config.ts index 90deca053fd8..bd98bd693de6 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -19,6 +19,7 @@ import { slash, type AdditionalConfig, type AdditionalConfigDict, + type Awaitable, type HeadConfig, type SiteData } from './shared' @@ -34,30 +35,40 @@ const resolve = (root: string, file: string) => export type UserConfigFn = ( env: ConfigEnv -) => UserConfig | Promise> +) => Awaitable> export type UserConfigExport = - | UserConfig - | Promise> + | Awaitable> | UserConfigFn /** * Type config helper */ -export function defineConfig(config: UserConfig) { +export function defineConfig( + config: UserConfig> +) { return config } +export type AdditionalConfigFn = ( + env: ConfigEnv +) => Awaitable> +export type AdditionalConfigExport = + | Awaitable> + | AdditionalConfigFn + /** - * Type additional config helper + * Type config helper for additional/locale-specific config */ -export function defineAdditionalConfig( - config: AdditionalConfig +export function defineAdditionalConfig( + config: AdditionalConfig> ) { return config } /** * Type config helper for custom theme config + * + * @deprecated use `defineConfig` instead */ export function defineConfigWithTheme( config: UserConfig @@ -65,15 +76,6 @@ export function defineConfigWithTheme( return config } -/** - * Type additional config helper - */ -export function defineAdditionalConfigWithTheme( - config: AdditionalConfig -) { - return config -} - export async function resolveConfig( root: string = process.cwd(), command: 'serve' | 'build' = 'serve', From 27de822a778beb8d461356b09c78ffc8595d6bd5 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sun, 13 Apr 2025 23:24:49 +0530 Subject: [PATCH 30/40] update comment, json is no longer supported --- src/node/config.ts | 2 +- src/node/siteConfig.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/config.ts b/src/node/config.ts index bd98bd693de6..fa7aae79ab4b 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -169,7 +169,7 @@ export async function resolveConfig( return config } -export const supportedConfigExtensions = ['js', 'ts', 'mjs', 'mts'] +const supportedConfigExtensions = ['js', 'ts', 'mjs', 'mts'] export function isAdditionalConfigFile(path: string) { const filename_to_check = path.split('/').pop() ?? '' diff --git a/src/node/siteConfig.ts b/src/node/siteConfig.ts index 7f1758bfa688..82ab3162e6a8 100644 --- a/src/node/siteConfig.ts +++ b/src/node/siteConfig.ts @@ -194,7 +194,7 @@ export interface UserConfig /** * Multi-layer configuration overloading. - * Auto-resolves to `docs/.../config.(ts|js|json)` when unspecified. + * Auto-resolves to `docs/.../config.{js,mjs,ts,mts}` when unspecified. * * Set to `{}` to opt-out. * From e73dce3650ae3edc4f94753434d83c325ae13fa8 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Mon, 14 Apr 2025 00:00:54 +0530 Subject: [PATCH 31/40] minor tweaks --- src/node/config.ts | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/node/config.ts b/src/node/config.ts index fa7aae79ab4b..503964ff8c67 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -18,7 +18,6 @@ import { VP_SOURCE_KEY, slash, type AdditionalConfig, - type AdditionalConfigDict, type Awaitable, type HeadConfig, type SiteData @@ -170,15 +169,11 @@ export async function resolveConfig( } const supportedConfigExtensions = ['js', 'ts', 'mjs', 'mts'] +const additionalConfigRE = /(?:^|\/|\\)config\.m?[jt]s$/ +const additionalConfigGlob = `**/config.{js,mjs,ts,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 + return additionalConfigRE.test(path) } /** @@ -197,38 +192,50 @@ async function gatherAdditionalConfig( root: string, command: 'serve' | 'build', mode: string, - srcDir: string = '.' -): Promise<[AdditionalConfigDict, string[][]]> { - const pattern = `**/config.{${supportedConfigExtensions.join(',')}}` - const candidates = await glob(pattern, { + srcDir: string = '.', + srcExclude: string[] = [] +) { + // + + const candidates = await glob(additionalConfigGlob, { cwd: path.resolve(root, srcDir), dot: false, // conveniently ignores .vitepress/* - ignore: ['**/node_modules/**', '**/.git/**'] + ignore: ['**/node_modules/**', ...srcExclude], + expandDirectories: false }) + 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, srcDir, 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] + return } + deps.push( configExports.dependencies.map((file) => normalizePath(path.resolve(file)) ) ) - if (mode === 'development') - (configExports.config as any)[VP_SOURCE_KEY] = '/' + slash(file) - return [id, configExports.config as AdditionalConfig] + + if (mode === 'development') { + ;(configExports.config as any)[VP_SOURCE_KEY] = '/' + slash(file) + } + + return [id, configExports.config as AdditionalConfig] as const }) ) - return [Object.fromEntries(exports.filter(([id, config]) => config)), deps] + + return [Object.fromEntries(exports.filter((e) => e != null)), deps] as const } export async function resolveUserConfig( @@ -265,7 +272,8 @@ export async function resolveUserConfig( root, command, mode, - userConfig.srcDir + userConfig.srcDir, + userConfig.srcExclude ) userConfig.additionalConfig = additionalConfig configDeps = configDeps.concat(...additionalDeps) From 21eda0561f800109580cace30e430d6697949878 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Mon, 14 Apr 2025 00:05:13 +0530 Subject: [PATCH 32/40] format --- docs/config.ts | 1 + docs/es/config.ts | 2 ++ docs/fa/config.ts | 2 ++ docs/ko/config.ts | 2 ++ docs/pt/config.ts | 2 ++ docs/ru/config.ts | 2 ++ docs/zh/config.ts | 2 ++ 7 files changed, 13 insertions(+) diff --git a/docs/config.ts b/docs/config.ts index b4cc4d2a1f32..a09a3a67c114 100644 --- a/docs/config.ts +++ b/docs/config.ts @@ -10,6 +10,7 @@ export default defineAdditionalConfig({ themeConfig: { nav: nav(), + sidebar: { '/guide/': { base: '/guide/', items: sidebarGuide() }, '/reference/': { base: '/reference/', items: sidebarReference() } diff --git a/docs/es/config.ts b/docs/es/config.ts index c923e6d62b17..099edba64b68 100644 --- a/docs/es/config.ts +++ b/docs/es/config.ts @@ -10,7 +10,9 @@ export default defineAdditionalConfig({ themeConfig: { nav: nav(), + search: { options: searchOptions() }, + sidebar: { '/es/guide/': { base: '/es/guide/', items: sidebarGuide() }, '/es/reference/': { base: '/es/reference/', items: sidebarReference() } diff --git a/docs/fa/config.ts b/docs/fa/config.ts index 3aa88813933b..d4698db07f0d 100644 --- a/docs/fa/config.ts +++ b/docs/fa/config.ts @@ -11,7 +11,9 @@ export default defineAdditionalConfig({ themeConfig: { nav: nav(), + search: { options: searchOptions() }, + sidebar: { '/fa/guide/': { base: '/fa/guide/', items: sidebarGuide() }, '/fa/reference/': { base: '/fa/reference/', items: sidebarReference() } diff --git a/docs/ko/config.ts b/docs/ko/config.ts index 3100fc76aea0..78b5c3f3d416 100644 --- a/docs/ko/config.ts +++ b/docs/ko/config.ts @@ -10,7 +10,9 @@ export default defineAdditionalConfig({ themeConfig: { nav: nav(), + search: { options: searchOptions() }, + sidebar: { '/ko/guide/': { base: '/ko/guide/', items: sidebarGuide() }, '/ko/reference/': { base: '/ko/reference/', items: sidebarReference() } diff --git a/docs/pt/config.ts b/docs/pt/config.ts index 6a8989222b83..4926e697ffa0 100644 --- a/docs/pt/config.ts +++ b/docs/pt/config.ts @@ -10,7 +10,9 @@ export default defineAdditionalConfig({ themeConfig: { nav: nav(), + search: { options: searchOptions() }, + sidebar: { '/pt/guide/': { base: '/pt/guide/', items: sidebarGuide() }, '/pt/reference/': { base: '/pt/reference/', items: sidebarReference() } diff --git a/docs/ru/config.ts b/docs/ru/config.ts index f9bd10cc7c07..739cfbf2ae26 100644 --- a/docs/ru/config.ts +++ b/docs/ru/config.ts @@ -10,7 +10,9 @@ export default defineAdditionalConfig({ themeConfig: { nav: nav(), + search: { options: searchOptions() }, + sidebar: { '/ru/guide/': { base: '/ru/guide/', items: sidebarGuide() }, '/ru/reference/': { base: '/ru/reference/', items: sidebarReference() } diff --git a/docs/zh/config.ts b/docs/zh/config.ts index fc8cf0a5bd0e..22fb8b95121f 100644 --- a/docs/zh/config.ts +++ b/docs/zh/config.ts @@ -10,7 +10,9 @@ export default defineAdditionalConfig({ themeConfig: { nav: nav(), + search: { options: searchOptions() }, + sidebar: { '/zh/guide/': { base: '/zh/guide/', items: sidebarGuide() }, '/zh/reference/': { base: '/zh/reference/', items: sidebarReference() } From aa29aada4da705947b7029779eb1bd865ef0d738 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Mon, 14 Apr 2025 00:38:59 +0530 Subject: [PATCH 33/40] use builtin functions --- src/node/config.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/node/config.ts b/src/node/config.ts index 503964ff8c67..5d7510344412 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -176,18 +176,6 @@ export function isAdditionalConfigFile(path: string) { return additionalConfigRE.test(path) } -/** - * 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', @@ -208,7 +196,7 @@ async function gatherAdditionalConfig( const exports = await Promise.all( candidates.map(async (file) => { - const id = '/' + dirname(slash(file)) + const id = normalizePath(`/${path.dirname(file)}/`) const configExports = await loadConfigFromFile( { command, mode }, From 5fdd3741c0c7b7d3b7d5a3cf5910a69d2dd5cc90 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sun, 13 Apr 2025 17:02:56 -0400 Subject: [PATCH 34/40] fix: properly override `localeIndex` property on resolved siteData --- src/shared/shared.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/shared.ts b/src/shared/shared.ts index 2701e97ec1b0..62f748892ca4 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -97,6 +97,7 @@ export function resolveSiteDataByRoute( ): SiteData { const localeIndex = getLocaleForPath(siteData, relativePath) const { label, link, ...localeConfig } = siteData.locales[localeIndex] ?? {} + Object.assign(localeConfig, { localeIndex }) const additionalConfigs = resolveAdditionalConfig(siteData, relativePath) if (inBrowser && (import.meta as any).env?.DEV) { ;(localeConfig as any)[VP_SOURCE_KEY] = `locale config (${localeIndex})` From 0e48535eba8e7eb6f3a80ddb1c59f5d6bfed42e3 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:40:06 +0530 Subject: [PATCH 35/40] fix the locale for path thing --- src/shared/shared.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/shared.ts b/src/shared/shared.ts index 62f748892ca4..124a69f59709 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -83,7 +83,7 @@ export function getLocaleForPath( (key) => key !== 'root' && !isExternal(key) && - isActive(relativePath, `/${key}/`, true) + isActive(relativePath, `^/${key}/`, true) ) || 'root' ) } From 369ba701388724bff8dedfef56e277470e46a488 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:46:30 +0530 Subject: [PATCH 36/40] share isObject code --- src/node/config.ts | 5 +---- src/shared/shared.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/node/config.ts b/src/node/config.ts index 5d7510344412..164c4978c11b 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -16,6 +16,7 @@ import { resolvePages } from './plugins/dynamicRoutesPlugin' import { APPEARANCE_KEY, VP_SOURCE_KEY, + isObject, slash, type AdditionalConfig, type Awaitable, @@ -309,10 +310,6 @@ export function mergeConfig(a: UserConfig, b: UserConfig, isRoot = true) { return merged } -function isObject(value: unknown): value is Record { - return Object.prototype.toString.call(value) === '[object Object]' -} - export async function resolveSiteData( root: string, userConfig?: UserConfig, diff --git a/src/shared/shared.ts b/src/shared/shared.ts index 124a69f59709..03aa224ea893 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -291,8 +291,8 @@ function reportConfigLayers(path: string, layers: SiteData[]) { */ 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 (!isObject(layers[0])) return layers[0] as T + layers = layers.filter(isObject) if (layers.length <= 1) return layers[0] as T return new Proxy( {}, @@ -331,11 +331,11 @@ export function stackView(...layers: Partial[]): T { ) 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] } + +export function isObject(value: unknown): value is Record { + return Object.prototype.toString.call(value) === '[object Object]' +} From 53369abdc23aa618e5ae9cf5ef0837c25b452a1d Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:15:15 +0530 Subject: [PATCH 37/40] update lunaria config --- docs/lunaria.config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/lunaria.config.json b/docs/lunaria.config.json index 60abdd98a436..4f93f4dca566 100644 --- a/docs/lunaria.config.json +++ b/docs/lunaria.config.json @@ -6,8 +6,8 @@ }, "files": [ { - "location": ".vitepress/config/{en,zh,pt,ru,es,ko,fa}.ts", - "pattern": ".vitepress/config/@lang.ts", + "location": "**/config.ts", + "pattern": "@lang/@path", "type": "universal" }, { From d5b15634e7327384125ad68a0650dc1461c69a69 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Tue, 15 Apr 2025 18:15:16 +0530 Subject: [PATCH 38/40] optimize head merging and preserve order --- src/shared/shared.ts | 48 ++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/shared/shared.ts b/src/shared/shared.ts index 03aa224ea893..3582fc7096dc 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -161,29 +161,33 @@ function createTitleTemplate( return ` | ${template}` } -function hasTag(head: HeadConfig[], tag: HeadConfig) { - const [tagType, tagAttrs] = tag - if (tagType !== 'meta') return false - const keyAttr = Object.entries(tagAttrs)[0] // First key - if (keyAttr == null) return false - return head.some( - ([type, attrs]) => type === tagType && attrs[keyAttr[0]] === keyAttr[1] - ) -} +export function mergeHead(...headArrays: HeadConfig[][]): HeadConfig[] { + const merged: HeadConfig[] = [] + const metaKeyMap = new Map() + + for (const current of headArrays) { + for (const tag of current) { + const [type, attrs] = tag + const keyAttr = Object.entries(attrs)[0] + + if (type !== 'meta' || !keyAttr) { + merged.push(tag) + continue + } -/** - * Merge head tags ascending precedence - * Prior duplicates are skipped in favor of later ones - */ -export function mergeHead(...heads: HeadConfig[][]) { - return heads - .filter(Array.isArray) - .flat(1) - .reverse() - .reduce((merged, tag) => { - if (!hasTag(merged, tag)) merged.push(tag) - return merged - }, []) as HeadConfig[] + const key = `${keyAttr[0]}=${keyAttr[1]}` + const existingIndex = metaKeyMap.get(key) + + if (existingIndex != null) { + merged[existingIndex] = tag // replace existing tag + } else { + metaKeyMap.set(key, merged.length) + merged.push(tag) + } + } + } + + return merged } // https://github.com/rollup/rollup/blob/fec513270c6ac350072425cc045db367656c623b/src/utils/sanitizeFileName.ts From 87048f5e3fd1583c57c84b7a849ba3cc76e4cbf4 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Tue, 15 Apr 2025 21:44:15 +0530 Subject: [PATCH 39/40] adjust implementation --- docs/.vitepress/config.ts | 2 +- src/node/build/render.ts | 2 +- src/shared/shared.ts | 122 ++++++++++++++++++++------------------ types/shared.d.ts | 2 +- 4 files changed, 68 insertions(+), 60 deletions(-) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index dc755a698870..bb45c4c23ddb 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -120,7 +120,7 @@ export default defineConfig({ firebase: 'logos:firebase' } }), - process.env.NODE_ENV === 'production' && + !!process.env.NETLIFY && llmstxt({ workDir: 'en', ignoreFiles: ['index.md'] diff --git a/src/node/build/render.ts b/src/node/build/render.ts index 2a2f22862c6e..2b616b2a60b6 100644 --- a/src/node/build/render.ts +++ b/src/node/build/render.ts @@ -33,7 +33,7 @@ export async function renderPage( usedIcons: Set ) { const routePath = `/${page.replace(/\.md$/, '')}` - const siteData = resolveSiteDataByRoute(config.site, routePath) + const siteData = resolveSiteDataByRoute(config.site, page) // render page const context = await render(routePath) diff --git a/src/shared/shared.ts b/src/shared/shared.ts index 3582fc7096dc..a372a9b14003 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -1,4 +1,9 @@ -import type { HeadConfig, PageData, SiteData } from '../../types/shared' +import type { + AdditionalConfig, + HeadConfig, + PageData, + SiteData +} from '../../types/shared' export type { Awaitable, @@ -13,12 +18,16 @@ export type { SiteData, SSGContext, AdditionalConfig, - AdditionalConfigDict + AdditionalConfigDict, + AdditionalConfigLoader } from '../../types/shared' export const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i export const APPEARANCE_KEY = 'vitepress-theme-appearance' +export const VP_SOURCE_KEY = '[VP_SOURCE]' +const UnpackStackView = Symbol('stack-view:unpack') + const HASH_RE = /#.*$/ const HASH_OR_QUERY_RE = /[?#].*$/ const INDEX_OR_EXT_RE = /(?:(^|\/)index)?\.(?:md|html)$/ @@ -98,15 +107,18 @@ export function resolveSiteDataByRoute( const localeIndex = getLocaleForPath(siteData, relativePath) const { label, link, ...localeConfig } = siteData.locales[localeIndex] ?? {} Object.assign(localeConfig, { 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, + localeConfig, siteData ]) } + const topLayer = { head: mergeHead( siteData.head ?? [], @@ -114,6 +126,7 @@ export function resolveSiteDataByRoute( ...additionalConfigs.map((data) => data?.head ?? []).reverse() ) } as SiteData + return stackView( topLayer, ...additionalConfigs, @@ -256,33 +269,36 @@ export function escapeHtml(str: string): string { .replace(/&(?![\w#]+;)/g, '&') } -export function resolveAdditionalConfig(site: SiteData, path: string) { - if (!path.startsWith('/')) path = `/${path}` - const additionalConfig = site.additionalConfig +function resolveAdditionalConfig( + { additionalConfig }: SiteData, + path: string +): 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) + if (typeof additionalConfig === 'function') return additionalConfig(path) + + const configs: AdditionalConfig[] = [] + const segments = path.split('/').slice(0, -1) // remove file name + while (segments.length) { const key = `/${segments.join('/')}/` - if (key in additionalConfig) configs.push(additionalConfig[key] as SiteData) + configs.push(additionalConfig[key]) segments.pop() } - if ('/' in additionalConfig) configs.push(additionalConfig['/'] as SiteData) - return configs -} -export const VP_SOURCE_KEY = '[VP_SOURCE]' + configs.push(additionalConfig['/']) + return configs.filter((config) => config !== undefined) +} -function reportConfigLayers(path: string, layers: SiteData[]) { - // This helps users to understand which configuration files are active +// This helps users to understand which configuration files are active +function reportConfigLayers(path: string, layers: Partial[]) { 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') ) @@ -293,53 +309,45 @@ function reportConfigLayers(path: string, layers: SiteData[]) { * 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 (!isObject(layers[0])) return layers[0] as T - layers = layers.filter(isObject) - 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) - } - } +export function stackView(..._layers: Partial[]): T { + const layers = _layers.filter((layer) => isObject(layer)) + if (layers.length <= 1) return _layers[0] as T + + const allKeys = new Set(layers.flatMap((layer) => Reflect.ownKeys(layer))) + const allKeysArray = [...allKeys] + + return new Proxy({} as T, { + get(_, prop) { + if (prop === UnpackStackView) return layers + return stackView( + ...layers + .map((layer) => layer[prop]) + .filter((v): v is NonNullable => v !== undefined) + ) + }, + set() { + throw new Error('StackView is read-only and cannot be mutated.') + }, + has(_, prop) { + return allKeys.has(prop) + }, + ownKeys() { + return allKeysArray + }, + getOwnPropertyDescriptor(_, prop) { + for (const layer of layers) { + const descriptor = Object.getOwnPropertyDescriptor(layer, prop) + if (descriptor) return descriptor } } - ) as T + }) } -const UnpackStackView = Symbol('stack-view:unpack') stackView.unpack = function (obj: T): T[] | undefined { return (obj as any)?.[UnpackStackView] } -export function isObject(value: unknown): value is Record { +type ObjectType = Record +export function isObject(value: unknown): value is ObjectType { return Object.prototype.toString.call(value) === '[object Object]' } diff --git a/types/shared.d.ts b/types/shared.d.ts index 18fe9d175229..445bb82ee733 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -191,7 +191,7 @@ export type AdditionalConfigDict = Record< > export type AdditionalConfigLoader = ( - path: string + relativePath: string ) => AdditionalConfig[] // Manually declaring all properties as rollup-plugin-dts From 4995fb909cf171c5db5b4aa47ec4b27c4068c028 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Tue, 15 Apr 2025 22:38:39 +0530 Subject: [PATCH 40/40] update opengraph stuff in docs, export resolveSiteDataByRoute --- docs/.vitepress/config.ts | 28 +++++++++++++++++++++++----- docs/en/index.md | 3 --- docs/es/index.md | 3 --- docs/fa/index.md | 3 --- docs/ko/index.md | 3 --- docs/pt/index.md | 3 --- docs/ru/index.md | 3 --- docs/zh/index.md | 3 --- src/node/config.ts | 1 + src/shared/shared.ts | 3 ++- 10 files changed, 26 insertions(+), 27 deletions(-) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index bb45c4c23ddb..575d114dd5d9 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,4 +1,8 @@ -import { defineConfig } from 'vitepress' +import { + defineConfig, + resolveSiteDataByRoute, + type HeadConfig +} from 'vitepress' import { groupIconMdPlugin, groupIconVitePlugin, @@ -6,6 +10,8 @@ import { } from 'vitepress-plugin-group-icons' import llmstxt from 'vitepress-plugin-llms' +const prod = !!process.env.NETLIFY + export default defineConfig({ title: 'VitePress', @@ -72,8 +78,6 @@ export default defineConfig({ ['link', { rel: 'icon', type: 'image/png', href: '/vitepress-logo-mini.png' }], ['meta', { name: 'theme-color', content: '#5f67ee' }], ['meta', { property: 'og:type', content: 'website' }], - ['meta', { property: 'og:locale', content: 'en' }], - ['meta', { property: 'og:title', content: 'VitePress | Vite & Vue Powered Static Site Generator' }], ['meta', { property: 'og:site_name', content: 'VitePress' }], ['meta', { property: 'og:image', content: 'https://vitepress.dev/vitepress-og.jpg' }], ['meta', { property: 'og:url', content: 'https://vitepress.dev/' }], @@ -120,11 +124,25 @@ export default defineConfig({ firebase: 'logos:firebase' } }), - !!process.env.NETLIFY && + prod && llmstxt({ workDir: 'en', ignoreFiles: ['index.md'] }) ] - } + }, + + transformPageData: prod + ? (pageData, ctx) => { + const site = resolveSiteDataByRoute( + ctx.siteConfig.site, + pageData.relativePath + ) + const title = `${pageData.title || site.title} | ${pageData.description || site.description}` + ;((pageData.frontmatter.head ??= []) as HeadConfig[]).push( + ['meta', { property: 'og:locale', content: site.lang }], + ['meta', { property: 'og:title', content: title }] + ) + } + : undefined }) diff --git a/docs/en/index.md b/docs/en/index.md index c91ce9531b59..61a2b003d27d 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -1,9 +1,6 @@ --- layout: home -title: VitePress -titleTemplate: Vite & Vue Powered Static Site Generator - hero: name: VitePress text: Vite & Vue Powered Static Site Generator diff --git a/docs/es/index.md b/docs/es/index.md index 04a4c68fd42c..24b5034cf485 100644 --- a/docs/es/index.md +++ b/docs/es/index.md @@ -1,9 +1,6 @@ --- layout: home -title: VitePress -titleTemplate: Generador de Sitios Estáticos desarrollado con Vite y Vue - hero: name: VitePress text: Generador de Sitios Estáticos Vite y Vue diff --git a/docs/fa/index.md b/docs/fa/index.md index cd6552baedb0..8fa79bd31148 100644 --- a/docs/fa/index.md +++ b/docs/fa/index.md @@ -1,9 +1,6 @@ --- layout: home -title: ویت‌پرس -titleTemplate: Vite & Vue Powered Static Site Generator - hero: name: ویت‌پرس text: سازنده سایت‌های ایستا به کمک Vite و Vue diff --git a/docs/ko/index.md b/docs/ko/index.md index 3ddaa07dca62..ae7df4c8f271 100644 --- a/docs/ko/index.md +++ b/docs/ko/index.md @@ -1,9 +1,6 @@ --- layout: home -title: VitePress -titleTemplate: Vite & Vue 기반 정적 사이트 생성기 - hero: name: VitePress text: Vite & Vue 기반 정적 사이트 생성기 diff --git a/docs/pt/index.md b/docs/pt/index.md index 71fff26b7cf1..67cab365b054 100644 --- a/docs/pt/index.md +++ b/docs/pt/index.md @@ -1,9 +1,6 @@ --- layout: home -title: VitePress -titleTemplate: Gerador de Site Estático desenvolvido com Vite & Vue - hero: name: VitePress text: Gerador de Site Estático Vite & Vue diff --git a/docs/ru/index.md b/docs/ru/index.md index 77f2072a0135..e5bc7732741c 100644 --- a/docs/ru/index.md +++ b/docs/ru/index.md @@ -1,9 +1,6 @@ --- layout: home -title: VitePress -titleTemplate: Генератор статических сайтов на основе Vite и Vue - hero: name: VitePress text: Генератор статических сайтов на основе Vite и Vue diff --git a/docs/zh/index.md b/docs/zh/index.md index e07f9136b412..7beeb9988e50 100644 --- a/docs/zh/index.md +++ b/docs/zh/index.md @@ -1,9 +1,6 @@ --- layout: home -title: VitePress -titleTemplate: 由 Vite 和 Vue 驱动的静态站点生成器 - hero: name: VitePress text: 由 Vite 和 Vue 驱动的静态站点生成器 diff --git a/src/node/config.ts b/src/node/config.ts index 164c4978c11b..959e39bd761e 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -26,6 +26,7 @@ import { import type { RawConfigExports, SiteConfig, UserConfig } from './siteConfig' export { resolvePages } from './plugins/dynamicRoutesPlugin' +export { resolveSiteDataByRoute } from './shared' export * from './siteConfig' const debug = _debug('vitepress:config') diff --git a/src/shared/shared.ts b/src/shared/shared.ts index a372a9b14003..3caca9a92189 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -123,7 +123,7 @@ export function resolveSiteDataByRoute( head: mergeHead( siteData.head ?? [], localeConfig.head ?? [], - ...additionalConfigs.map((data) => data?.head ?? []).reverse() + ...additionalConfigs.map((data) => data.head ?? []).reverse() ) } as SiteData @@ -317,6 +317,7 @@ export function stackView(..._layers: Partial[]): T { const allKeysArray = [...allKeys] return new Proxy({} as T, { + // TODO: optimize for performance, this is a hot path get(_, prop) { if (prop === UnpackStackView) return layers return stackView(