diff --git a/.changeset/rude-meals-move.md b/.changeset/rude-meals-move.md new file mode 100644 index 00000000000..ce5b4c112eb --- /dev/null +++ b/.changeset/rude-meals-move.md @@ -0,0 +1,5 @@ +--- +"@remix-run/dev": patch +--- + +cache getRouteModuleExports calls to significantly speed up build and HMR rebuild times diff --git a/packages/remix-dev/compiler/compiler.ts b/packages/remix-dev/compiler/compiler.ts index 840caa5fa8f..ed464a955d5 100644 --- a/packages/remix-dev/compiler/compiler.ts +++ b/packages/remix-dev/compiler/compiler.ts @@ -107,6 +107,7 @@ export let create = async (ctx: Context): Promise => { config: ctx.config, metafile, hmr, + fileWatchCache: ctx.fileWatchCache, }); refs.manifestChannel.ok(manifest); options.onManifest?.(manifest); diff --git a/packages/remix-dev/compiler/js/plugins/routes.ts b/packages/remix-dev/compiler/js/plugins/routes.ts index e24936d36c5..d94f3f71792 100644 --- a/packages/remix-dev/compiler/js/plugins/routes.ts +++ b/packages/remix-dev/compiler/js/plugins/routes.ts @@ -1,3 +1,4 @@ +import * as path from "node:path"; import type esbuild from "esbuild"; import type { RemixConfig } from "../../../config"; @@ -22,7 +23,7 @@ const browserSafeRouteExports: { [name: string]: boolean } = { * that re-export only the route module exports that are safe for the browser. */ export function browserRouteModulesPlugin( - { config }: Context, + { config, fileWatchCache }: Context, suffixMatcher: RegExp ): esbuild.Plugin { return { @@ -54,7 +55,22 @@ export function browserRouteModulesPlugin( try { invariant(route, `Cannot get route by path: ${args.path}`); - theExports = (await getRouteModuleExports(config, route.id)).filter( + let cacheKey = `module-exports:${route.id}`; + let { cacheValue: sourceExports } = await fileWatchCache.getOrSet( + cacheKey, + async () => { + let file = path.resolve( + config.appDirectory, + config.routes[route!.id].file + ); + return { + cacheValue: await getRouteModuleExports(config, route!.id), + fileDependencies: new Set([file]), + }; + } + ); + + theExports = sourceExports.filter( (ex) => !!browserSafeRouteExports[ex] ); } catch (error: any) { diff --git a/packages/remix-dev/compiler/manifest.ts b/packages/remix-dev/compiler/manifest.ts index 0a0f60efa75..e803cd93a2c 100644 --- a/packages/remix-dev/compiler/manifest.ts +++ b/packages/remix-dev/compiler/manifest.ts @@ -7,6 +7,7 @@ import invariant from "../invariant"; import { type Manifest } from "../manifest"; import { getRouteModuleExports } from "./utils/routeExports"; import { getHash } from "./utils/crypto"; +import { type FileWatchCache } from "./fileWatchCache"; type Route = RemixConfig["routes"][string]; @@ -14,10 +15,12 @@ export async function create({ config, metafile, hmr, + fileWatchCache, }: { config: RemixConfig; metafile: esbuild.Metafile; hmr?: Manifest["hmr"]; + fileWatchCache: FileWatchCache; }): Promise { function resolveUrl(outputPath: string): string { return createUrl( @@ -70,7 +73,21 @@ export async function create({ `Cannot get route(s) for entry point ${output.entryPoint}` ); for (let route of groupedRoute) { - let sourceExports = await getRouteModuleExports(config, route.id); + let cacheKey = `module-exports:${route.id}`; + let { cacheValue: sourceExports } = await fileWatchCache.getOrSet( + cacheKey, + async () => { + let file = path.resolve( + config.appDirectory, + config.routes[route.id].file + ); + return { + cacheValue: await getRouteModuleExports(config, route.id), + fileDependencies: new Set([file]), + }; + } + ); + routes[route.id] = { id: route.id, parentId: route.parentId, diff --git a/packages/remix-dev/devServer_unstable/hdr.ts b/packages/remix-dev/devServer_unstable/hdr.ts index 69e859b2493..3e93fe0ca2a 100644 --- a/packages/remix-dev/devServer_unstable/hdr.ts +++ b/packages/remix-dev/devServer_unstable/hdr.ts @@ -52,7 +52,24 @@ export let detectLoaderChanges = async ( let file = args.path.replace(filter, ""); let route = routesByFile.get(file); invariant(route, `Cannot get route by path: ${args.path}`); - let theExports = await getRouteModuleExports(ctx.config, route.id); + let cacheKey = `module-exports:${route.id}`; + let { cacheValue: theExports } = await ctx.fileWatchCache.getOrSet( + cacheKey, + async () => { + let file = path.resolve( + ctx.config.appDirectory, + ctx.config.routes[route!.id].file + ); + return { + cacheValue: await getRouteModuleExports( + ctx.config, + route!.id + ), + fileDependencies: new Set([file]), + }; + } + ); + let contents = "module.exports = {};"; if (theExports.includes("loader")) { contents = `export { loader } from ${JSON.stringify(