fix: module graph causing unnecessary route regeneration on every update

fix: preserve externally added dynamic routes and pages

chore: bump deps, lock vitest as its latest beta is broken
pull/4901/head
Divyansh Singh 4 weeks ago
parent 318c14fa7c
commit fc267ae6b7

@ -15,7 +15,7 @@
"open-cli": "^8.0.0",
"postcss-rtlcss": "^5.7.1",
"vitepress": "workspace:*",
"vitepress-plugin-group-icons": "^1.6.1",
"vitepress-plugin-llms": "^1.7.1"
"vitepress-plugin-group-icons": "^1.6.2",
"vitepress-plugin-llms": "^1.7.2"
}
}

@ -97,25 +97,25 @@
"dependencies": {
"@docsearch/css": "^4.0.0-beta.5",
"@docsearch/js": "^4.0.0-beta.5",
"@iconify-json/simple-icons": "^1.2.44",
"@shikijs/core": "^3.8.1",
"@shikijs/transformers": "^3.8.1",
"@shikijs/types": "^3.8.1",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/devtools-api": "^7.7.7",
"@iconify-json/simple-icons": "^1.2.46",
"@shikijs/core": "^3.9.2",
"@shikijs/transformers": "^3.9.2",
"@shikijs/types": "^3.9.2",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/devtools-api": "^8.0.0",
"@vue/shared": "^3.5.18",
"@vueuse/core": "^13.5.0",
"@vueuse/integrations": "^13.5.0",
"@vueuse/core": "^13.6.0",
"@vueuse/integrations": "^13.6.0",
"focus-trap": "^7.6.5",
"mark.js": "8.11.1",
"minisearch": "^7.1.2",
"shiki": "^3.8.1",
"vite": "^7.0.6",
"shiki": "^3.9.2",
"vite": "^7.1.1",
"vue": "^3.5.18"
},
"devDependencies": {
"@clack/prompts": "^1.0.0-alpha.1",
"@iconify/utils": "^2.3.0",
"@iconify/utils": "^3.0.0",
"@mdit-vue/plugin-component": "^2.1.4",
"@mdit-vue/plugin-frontmatter": "^2.1.4",
"@mdit-vue/plugin-headers": "^2.1.4",
@ -139,8 +139,8 @@
"@types/markdown-it-container": "^2.0.10",
"@types/markdown-it-emoji": "^3.0.1",
"@types/minimist": "^1.2.5",
"@types/node": "^24.1.0",
"@types/picomatch": "^4.0.1",
"@types/node": "^24.2.1",
"@types/picomatch": "^4.0.2",
"@types/postcss-prefix-selector": "^1.16.3",
"@types/prompts": "^2.4.9",
"chokidar": "^4.0.3",
@ -149,10 +149,10 @@
"debug": "^4.4.1",
"esbuild": "^0.25.8",
"execa": "^9.6.0",
"fs-extra": "^11.3.0",
"fs-extra": "^11.3.1",
"get-port": "^7.1.0",
"gray-matter": "^4.0.3",
"lint-staged": "^16.1.2",
"lint-staged": "^16.1.5",
"lodash.template": "^4.5.0",
"lru-cache": "^11.1.0",
"markdown-it": "^14.1.0",
@ -165,13 +165,13 @@
"minimist": "^1.2.8",
"nanoid": "^5.1.5",
"ora": "^8.2.0",
"oxc-minify": "^0.78.0",
"oxc-minify": "^0.81.0",
"p-map": "^7.0.3",
"package-directory": "^8.1.0",
"path-to-regexp": "^6.3.0",
"picocolors": "^1.1.1",
"picomatch": "^4.0.3",
"playwright-chromium": "^1.54.1",
"playwright-chromium": "^1.54.2",
"polka": "^1.0.0-next.28",
"postcss": "^8.5.6",
"postcss-prefix-selector": "^2.1.1",
@ -179,22 +179,22 @@
"prompts": "^2.4.2",
"punycode": "^2.3.1",
"rimraf": "^6.0.1",
"rollup": "^4.45.1",
"rollup": "^4.46.2",
"rollup-plugin-dts": "6.1.1",
"rollup-plugin-esbuild": "^6.2.1",
"semver": "^7.7.2",
"simple-git-hooks": "^2.13.0",
"simple-git-hooks": "^2.13.1",
"sirv": "^3.0.1",
"sitemap": "^8.0.0",
"tinyglobby": "^0.2.14",
"typescript": "^5.8.3",
"vitest": "^4.0.0-beta.4",
"vue-tsc": "^3.0.4",
"typescript": "^5.9.2",
"vitest": "4.0.0-beta.4",
"vue-tsc": "^3.0.5",
"wait-on": "^8.0.4"
},
"peerDependencies": {
"markdown-it-mathjax3": "^4",
"oxc-minify": "^0.78.0",
"oxc-minify": "^0.81.0",
"postcss": "^8"
},
"peerDependenciesMeta": {
@ -208,5 +208,5 @@
"optional": true
}
},
"packageManager": "pnpm@10.13.1"
"packageManager": "pnpm@10.14.0"
}

File diff suppressed because it is too large Load Diff

@ -168,7 +168,7 @@ export async function resolveConfig(
global.VITEPRESS_CONFIG = config
// resolve pages after setting global, so that path loaders can access it
Object.assign(config, await resolvePages(srcDir, userConfig, logger, true))
await resolvePages(config, true)
return config as SiteConfig
}

@ -274,16 +274,7 @@ export async function createVitePressPlugin(
}
// update pages, dynamicRoutes and rewrites on md file creation / deletion
if (file.endsWith('.md')) {
Object.assign(
siteConfig,
await resolvePages(
siteConfig.srcDir,
siteConfig.userConfig,
siteConfig.logger
)
)
}
if (file.endsWith('.md')) await resolvePages(siteConfig)
if (!added && importerMap[file]) {
delete importerMap[file]

@ -61,6 +61,7 @@ const pathLoaderRE = /\.paths\.m?[jt]s$/
const routeModuleCache = new Map<string, ResolvedRouteModule>()
let moduleGraph = new ModuleGraph()
let discoveredPages = new Set<string>()
/**
* Helper for defining routes with type inference
@ -69,20 +70,21 @@ export function defineRoutes(loader: RouteModule): RouteModule {
return loader
}
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
export async function resolvePages(
srcDir: string,
userConfig: UserConfig,
logger: Logger,
siteConfig: Optional<SiteConfig, 'pages' | 'dynamicRoutes' | 'rewrites'>,
rebuildCache = false
): Promise<Pick<SiteConfig, 'pages' | 'dynamicRoutes' | 'rewrites'>> {
): Promise<void> {
if (rebuildCache) {
moduleGraph = new ModuleGraph()
routeModuleCache.clear()
discoveredPages.clear()
}
const allMarkdownFiles = await glob(['**/*.md'], {
cwd: srcDir,
ignore: userConfig.srcExclude
cwd: siteConfig.srcDir,
ignore: siteConfig.userConfig.srcExclude
})
const pages: string[] = []
@ -94,22 +96,33 @@ export async function resolvePages(
})
const dynamicRoutes = await resolveDynamicRoutes(
srcDir,
siteConfig.srcDir,
dynamicRouteFiles,
logger
siteConfig.logger
)
pages.push(...dynamicRoutes.map((r) => r.path))
const rewrites = resolveRewrites(pages, userConfig.rewrites)
const externalDynamicRoutes =
siteConfig.dynamicRoutes?.filter((r) => !discoveredPages.has(r.path)) || []
const externalPages =
siteConfig.pages?.filter((p) => !discoveredPages.has(p)) || []
return {
pages,
dynamicRoutes,
const finalDynamicRoutes = [...dynamicRoutes, ...externalDynamicRoutes].sort(
(a, b) => a.path.localeCompare(b.path)
)
const finalPages = [...pages, ...externalPages].sort()
const rewrites = resolveRewrites(pages, siteConfig.userConfig.rewrites)
Object.assign(siteConfig, {
pages: finalPages,
dynamicRoutes: finalDynamicRoutes,
rewrites,
// @ts-expect-error internal flag to reload resolution cache in ../markdownToVue.ts
__dirty: true
}
} satisfies Partial<SiteConfig>)
discoveredPages = new Set(pages)
}
export const dynamicRoutesPlugin = async (
@ -193,10 +206,7 @@ export const dynamicRoutesPlugin = async (
pathLoaderRE.test(normalizedFile)
) {
// path loader module or deps updated, reset loaded routes
Object.assign(
config,
await resolvePages(config.srcDir, config.userConfig, config.logger)
)
await resolvePages(config)
}
return modules.length ? [...existingMods, ...modules] : undefined

@ -54,6 +54,8 @@ export class ModuleGraph {
*/
delete(module: string): Set<string> {
const deleted = new Set<string>()
if (!this.nodes.has(module)) return deleted
const stack: string[] = [module]
// Traverse the reverse dependency graph (using dependents).

Loading…
Cancel
Save