Context
The current plugin integration requires users to wire up four distinct API calls with shared options, handle ESM __dirname resolution manually, and ensure correct call ordering (page generation before defineConfig). This produces ~20 lines of boilerplate that is easy to get wrong and intimidating for new users.
The core constraint is that generateOpenSpecPages() must run synchronously before VitePress scans srcDir for Markdown routes — no Vite plugin hook fires early enough. Any simplified API must preserve this invariant.
Goals / Non-Goals
Goals:
- Single
withOpenSpec(vitepressConfig, options?)call replaces all manual wiring - Auto-detect
specDirandsrcDirwithout requiring__dirnamemanipulation - Merge openspec Vite plugin, nav entry, and sidebar section automatically
- Remain fully additive — all existing lower-level APIs continue to work unchanged
Non-Goals:
- Changing how page generation works internally
- Providing async variants (constraint: must stay synchronous)
- Auto-detecting the correct
specDirby walking up the directory tree (fragile) - Supporting per-call options overrides beyond the top-level
withOpenSpecoptions
Decisions
1. Function signature: wraps UserConfig, returns UserConfig
function withOpenSpec(config: UserConfig, options?: WithOpenSpecOptions): UserConfigconfig is the plain VitePress config object (same as what defineConfig accepts). withOpenSpec is called in place of — or wrapping — defineConfig:
// Option A: replace defineConfig
export default withOpenSpec({ ... }, { specDir: '../openspec' })
// Option B: wrap defineConfig output (also valid)
export default defineConfig(withOpenSpec({ ... }))Rationale: Accepting the raw config object (not a wrapped defineConfig call) is simpler and avoids wrapping a wrapper. Both usage patterns work since defineConfig is an identity function.
2. srcDir auto-detection uses process.cwd()
When srcDir is not provided, fall back to process.cwd(). This matches the existing default in generateOpenSpecPages. Users who place config.ts inside docs/.vitepress/ and have openspec/ at the repo root will need to pass srcDir explicitly — but this was already true. No regression.
Alternative considered: Parse import.meta.url from the caller stack frame. Rejected as fragile and non-standard.
3. Nav merge: prepend to themeConfig.nav array
openspecNav() returns a single NavItem. withOpenSpec prepends it to config.themeConfig.nav if the array exists, or creates it. Can be opted out with nav: false in options.
Alternative considered: Append instead of prepend. Rejected because the openspec section is often a primary nav item and users expect it first. Users who want it elsewhere should use the lower-level API.
4. Sidebar merge: inject under the outDir key
generateOpenSpecSidebar() returns SidebarItem[]. withOpenSpec sets config.themeConfig.sidebar[/${outDir}/] if sidebar is an object (VitePress multi-sidebar format). Can be opted out with sidebar: false in options.
5. Type: import UserConfig from vitepress (peer dep)
vitepress is already a declared peer dependency. Importing its types is acceptable. Use UserConfig from 'vitepress' for the config parameter type — this gives full type safety without duplicating the interface.
6. WithOpenSpecOptions: extends OpenSpecPluginOptions
interface WithOpenSpecOptions extends OpenSpecPluginOptions {
nav?: boolean // default: true — whether to inject nav entry
sidebar?: boolean // default: true — whether to inject sidebar section
}Risks / Trade-offs
- Sidebar collision: If the user already defines
sidebar['/openspec/'],withOpenSpecwill overwrite it. → Mitigation: only set the key if not already defined; log a warning if overwriting. - Tight coupling to themeConfig shape: VitePress's
DefaultThemesidebar/nav types are not guaranteed stable across major versions. → Mitigation: use defensive type casting; this risk already exists in the nav/sidebar helpers. defineConfignot called: If user useswithOpenSpecwithoutdefineConfig, they lose VitePress's type narrowing. → Non-issue:defineConfigis an identity function in VitePress; wrapping with it is optional and documented as such.
Migration Plan
No migration required — purely additive. Existing users can adopt withOpenSpec at their own pace. README updated to show the new API as primary, with existing API documented as "advanced / manual" alternative.