From ebbd48c8c76a1f794ed52d8d4d9917f1bd52bea0 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Thu, 27 Aug 2026 05:13:58 +0530 Subject: [PATCH] feat(theme): un-deprecate Theme.setup and compose it across extends (#5404) --- docs/en/guide/custom-theme.md | 27 ++++++++++++++++++++++++++- src/client/app/index.ts | 8 ++++++-- src/client/app/theme.ts | 3 ++- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/en/guide/custom-theme.md b/docs/en/guide/custom-theme.md index 957c27dc..21cf4e26 100644 --- a/docs/en/guide/custom-theme.md +++ b/docs/en/guide/custom-theme.md @@ -38,7 +38,12 @@ interface Theme { */ enhanceApp?: (ctx: EnhanceAppContext) => Awaitable /** - * Extend another theme, calling its `enhanceApp` before ours + * Runs inside the root component's `setup()` + * @optional + */ + setup?: () => void + /** + * Extend another theme, calling its `enhanceApp` and `setup` before ours * @optional */ extends?: Theme @@ -88,6 +93,26 @@ export default { Return `false` from `onBeforeRouteChange` or `onBeforePageLoad` to cancel navigation. +The `setup` hook runs inside the root component's `setup()`, so Composition API calls (`onMounted`, `watch`, composables, ...) work there without wrapping the layout component: + +```ts [.vitepress/theme/index.ts] +import { watch } from 'vue' +import { useData } from 'vitepress' +import DefaultTheme from 'vitepress/theme' + +export default { + extends: DefaultTheme, + setup() { + const { page } = useData() + watch(() => page.value.relativePath, (path) => { + console.log('now viewing', path) + }) + } +} +``` + +With `extends`, each theme's `setup` runs base-first, like `enhanceApp`. It also runs during SSR/SSG rendering, so keep browser-only work inside `onMounted`. + The default export is the only contract for a custom theme, and only the `Layout` property is required. So technically, a VitePress theme can be as simple as a single Vue component. Inside your layout component, it works just like a normal Vite + Vue 3 application. Do note the theme also needs to be [SSR-compatible](./ssr-compat). diff --git a/src/client/app/index.ts b/src/client/app/index.ts index 15f489e3..952e52f5 100644 --- a/src/client/app/index.ts +++ b/src/client/app/index.ts @@ -26,8 +26,12 @@ function resolveThemeExtends(theme: typeof RawTheme): typeof RawTheme { ...base, ...theme, async enhanceApp(ctx) { - if (base.enhanceApp) await base.enhanceApp(ctx) - if (theme.enhanceApp) await theme.enhanceApp(ctx) + await base.enhanceApp?.(ctx) + await theme.enhanceApp?.(ctx) + }, + setup() { + base.setup?.() + theme.setup?.() } } } diff --git a/src/client/app/theme.ts b/src/client/app/theme.ts index 8c3fd2c1..4ebb222a 100644 --- a/src/client/app/theme.ts +++ b/src/client/app/theme.ts @@ -15,7 +15,8 @@ export interface Theme { extends?: Theme /** - * @deprecated can be replaced by wrapping layout component + * Runs inside the root component's `setup()` (during SSR too). With + * `extends`, setups run base-first, like `enhanceApp`. */ setup?: () => void