diff --git a/src/node/plugins/dynamicRoutesPlugin.ts b/src/node/plugins/dynamicRoutesPlugin.ts index 79d8fc22..125f3937 100644 --- a/src/node/plugins/dynamicRoutesPlugin.ts +++ b/src/node/plugins/dynamicRoutesPlugin.ts @@ -26,7 +26,7 @@ export async function resolvePages( // JavaScript built-in sort() is mandated to be stable as of ES2019 and // supported in Node 12+, which is required by Vite. const allMarkdownFiles = ( - await glob(['**.md'], { + await glob(['**/*.md'], { cwd: srcDir, ignore: [ '**/node_modules/**', @@ -71,9 +71,12 @@ interface RouteModule { config: { paths: | UserRouteConfig[] - | (() => UserRouteConfig[] | Promise) + | (( + watchedFiles: string[] + ) => UserRouteConfig[] | Promise) } dependencies: string[] + watch?: string[] | string } const routeModuleCache = new Map() @@ -141,20 +144,36 @@ export const dynamicRoutesPlugin = async ( async hotUpdate({ file, modules: existingMods }) { if (this.environment.name !== 'client') return - routeModuleCache.delete(file) + const normalizedFile = normalizePath(file) + // Invalidate any cached route modules whose key or dependencies include the changed file. + for (const [cacheKey, mod] of routeModuleCache.entries()) { + const normalizedCacheKey = normalizePath(cacheKey) + if ( + normalizedCacheKey === normalizedFile || + mod.dependencies.some( + (dep) => normalizePath(path.resolve(dep)) === normalizedFile + ) + ) { + routeModuleCache.delete(cacheKey) + } + } + const modules: EnvironmentModuleNode[] = [] - const mods = config.dynamicRoutes.fileToModulesMap[file] + const mods = config.dynamicRoutes.fileToModulesMap[normalizedFile] if (mods) { // path loader module or deps updated, reset loaded routes - if (!file.endsWith('.md')) { + if (!normalizedFile.endsWith('.md')) { Object.assign( config, await resolvePages(config.srcDir, config.userConfig, config.logger) ) } for (const id of mods) { - modules.push(this.environment.moduleGraph.getModuleById(id)!) + const mod = this.environment.moduleGraph.getModuleById(id) + if (mod) { + modules.push(mod) + } } } @@ -210,19 +229,7 @@ export async function resolveDynamicRoutes( } } - // this array represents the virtual modules affected by this route - const matchedModuleIds = (routeFileToModulesMap[ - normalizePath(path.resolve(srcDir, route)) - ] = new Set()) - - // each dependency (including the loader module itself) also point to the - // same array - for (const dep of mod.dependencies) { - // deps are resolved relative to cwd - routeFileToModulesMap[normalizePath(path.resolve(dep))] = matchedModuleIds - } - - const loader = mod!.config.paths + const loader = mod.config.paths if (!loader) { logger.warn( c.yellow( @@ -233,9 +240,61 @@ export async function resolveDynamicRoutes( continue } + // Create or retrieve the set of virtual module IDs affected by this route. + const routeKey = normalizePath(path.resolve(srcDir, route)) + const matchedModuleIds = + routeFileToModulesMap[routeKey] || new Set() + routeFileToModulesMap[routeKey] = matchedModuleIds + + // Track loader dependencies (merging sets if shared) + for (const dep of mod.dependencies) { + const depPath = normalizePath(path.resolve(dep)) + if (!routeFileToModulesMap[depPath]) { + routeFileToModulesMap[depPath] = matchedModuleIds + } else { + for (const id of matchedModuleIds) { + routeFileToModulesMap[depPath].add(id) + } + } + } + + // Process custom watch files if provided. + let watch: string[] | undefined + if (mod.watch) { + watch = typeof mod.watch === 'string' ? [mod.watch] : mod.watch + watch = watch.map((p) => + p.startsWith('.') + ? normalizePath(path.resolve(path.dirname(pathsFile), p)) + : normalizePath(p) + ) + for (const watchFile of watch) { + if (!routeFileToModulesMap[watchFile]) { + routeFileToModulesMap[watchFile] = matchedModuleIds + } else { + for (const id of matchedModuleIds) { + routeFileToModulesMap[watchFile].add(id) + } + } + } + } + const resolveRoute = async (): Promise => { - const paths = await (typeof loader === 'function' ? loader() : loader) - return paths.map((userConfig) => { + let pathsData: UserRouteConfig[] + if (typeof loader === 'function') { + let watchedFiles: string[] = [] + if (watch) { + watchedFiles = ( + await glob(watch, { + ignore: ['**/node_modules/**', '**/dist/**'], + expandDirectories: false + }) + ).sort() + } + pathsData = await loader(watchedFiles) + } else { + pathsData = loader + } + return pathsData.map((userConfig) => { const resolvedPath = route.replace( dynamicRouteRE, (_, key) => userConfig.params[key]