@@ -3,7 +3,7 @@ import { execSync } from "node:child_process";
33import PackageJson from "@npmcli/package-json" ;
44import * as ViteNode from "../vite/vite-node" ;
55import type * as Vite from "vite" ;
6- import path from "pathe" ;
6+ import Path from "pathe" ;
77import chokidar , {
88 type FSWatcher ,
99 type EmitArgs as ChokidarEmitArgs ,
@@ -17,12 +17,13 @@ import isEqual from "lodash/isEqual";
1717import {
1818 type RouteManifest ,
1919 type RouteManifestEntry ,
20- type RouteConfig ,
2120 setAppDirectory ,
2221 validateRouteConfig ,
2322 configRoutesToRouteManifest ,
2423} from "./routes" ;
2524import { detectPackageManager } from "../cli/detectPackageManager" ;
25+ import { importViteEsmSync } from "../vite/import-vite-esm-sync" ;
26+ import { preloadViteEsm } from "../vite/import-vite-esm-sync" ;
2627
2728const excludedConfigPresetKeys = [ "presets" ] as const satisfies ReadonlyArray <
2829 keyof ReactRouterConfig
@@ -405,14 +406,14 @@ async function resolveConfig({
405406 ) ;
406407 }
407408
408- let appDirectory = path . resolve ( root , userAppDirectory || "app" ) ;
409- let buildDirectory = path . resolve ( root , userBuildDirectory ) ;
409+ let appDirectory = Path . resolve ( root , userAppDirectory || "app" ) ;
410+ let buildDirectory = Path . resolve ( root , userBuildDirectory ) ;
410411
411412 let rootRouteFile = findEntry ( appDirectory , "root" ) ;
412413 if ( ! rootRouteFile ) {
413- let rootRouteDisplayPath = path . relative (
414+ let rootRouteDisplayPath = Path . relative (
414415 root ,
415- path . join ( appDirectory , "root.tsx" )
416+ Path . join ( appDirectory , "root.tsx" )
416417 ) ;
417418 return err (
418419 `Could not find a root route module in the app directory as "${ rootRouteDisplayPath } "`
@@ -427,17 +428,17 @@ async function resolveConfig({
427428
428429 try {
429430 if ( ! routeConfigFile ) {
430- let routeConfigDisplayPath = path . relative (
431+ let routeConfigDisplayPath = Path . relative (
431432 root ,
432- path . join ( appDirectory , "routes.ts" )
433+ Path . join ( appDirectory , "routes.ts" )
433434 ) ;
434435 return err ( `Route config file not found at "${ routeConfigDisplayPath } ".` ) ;
435436 }
436437
437438 setAppDirectory ( appDirectory ) ;
438439 let routeConfigExport = (
439440 await viteNodeContext . runner . executeFile (
440- path . join ( appDirectory , routeConfigFile )
441+ Path . join ( appDirectory , routeConfigFile )
441442 )
442443 ) . default ;
443444 let routeConfig = await routeConfigExport ;
@@ -462,7 +463,7 @@ async function resolveConfig({
462463 "" ,
463464 error . loc ?. file && error . loc ?. column && error . frame
464465 ? [
465- path . relative ( appDirectory , error . loc . file ) +
466+ Path . relative ( appDirectory , error . loc . file ) +
466467 ":" +
467468 error . loc . line +
468469 ":" +
@@ -506,7 +507,8 @@ type ChokidarEventName = ChokidarEmitArgs[0];
506507
507508type ChangeHandler = ( args : {
508509 result : Result < ResolvedReactRouterConfig > ;
509- configCodeUpdated : boolean ;
510+ configCodeChanged : boolean ;
511+ routeConfigCodeChanged : boolean ;
510512 configChanged : boolean ;
511513 routeConfigChanged : boolean ;
512514 path : string ;
@@ -526,20 +528,25 @@ export async function createConfigLoader({
526528 watch : boolean ;
527529 rootDirectory ?: string ;
528530} ) : Promise < ConfigLoader > {
529- root = root ?? process . env . REACT_ROUTER_ROOT ?? process . cwd ( ) ;
531+ root = Path . normalize ( root ?? process . env . REACT_ROUTER_ROOT ?? process . cwd ( ) ) ;
530532
531533 let viteNodeContext = await ViteNode . createContext ( {
532534 root,
533535 mode : watch ? "development" : "production" ,
534536 server : ! watch ? { watch : null } : { } ,
535- ssr : {
536- external : ssrExternals ,
537- } ,
537+ ssr : { external : ssrExternals } ,
538+ customLogger : await createCustomLogger ( ) ,
538539 } ) ;
539540
540- let reactRouterConfigFile = findEntry ( root , "react-router.config" , {
541- absolute : true ,
542- } ) ;
541+ let reactRouterConfigFile : string | undefined ;
542+
543+ let updateReactRouterConfigFile = ( ) => {
544+ reactRouterConfigFile = findEntry ( root , "react-router.config" , {
545+ absolute : true ,
546+ } ) ;
547+ } ;
548+
549+ updateReactRouterConfigFile ( ) ;
543550
544551 let getConfig = ( ) =>
545552 resolveConfig ( { root, viteNodeContext, reactRouterConfigFile } ) ;
@@ -552,9 +559,9 @@ export async function createConfigLoader({
552559 throw new Error ( initialConfigResult . error ) ;
553560 }
554561
555- appDirectory = initialConfigResult . value . appDirectory ;
562+ appDirectory = Path . normalize ( initialConfigResult . value . appDirectory ) ;
556563
557- let lastConfig = initialConfigResult . value ;
564+ let currentConfig = initialConfigResult . value ;
558565
559566 let fsWatcher : FSWatcher | undefined ;
560567 let changeHandlers : ChangeHandler [ ] = [ ] ;
@@ -571,54 +578,106 @@ export async function createConfigLoader({
571578 changeHandlers . push ( handler ) ;
572579
573580 if ( ! fsWatcher ) {
574- fsWatcher = chokidar . watch (
575- [
576- ...( reactRouterConfigFile ? [ reactRouterConfigFile ] : [ ] ) ,
577- appDirectory ,
578- ] ,
579- { ignoreInitial : true }
580- ) ;
581+ fsWatcher = chokidar . watch ( [ root , appDirectory ] , {
582+ ignoreInitial : true ,
583+ ignored : ( path ) => {
584+ let dirname = Path . dirname ( path ) ;
585+
586+ return (
587+ path !== root &&
588+ dirname !== root &&
589+ ! dirname . startsWith ( appDirectory )
590+ ) ;
591+ } ,
592+ } ) ;
581593
582594 fsWatcher . on ( "all" , async ( ...args : ChokidarEmitArgs ) => {
583595 let [ event , rawFilepath ] = args ;
584- let filepath = path . normalize ( rawFilepath ) ;
596+ let filepath = Path . normalize ( rawFilepath ) ;
597+
598+ let fileAddedOrRemoved = event === "add" || event === "unlink" ;
585599
586600 let appFileAddedOrRemoved =
587- appDirectory &&
588- ( event === "add" || event === "unlink" ) &&
589- filepath . startsWith ( path . normalize ( appDirectory ) ) ;
601+ fileAddedOrRemoved &&
602+ filepath . startsWith ( Path . normalize ( appDirectory ) ) ;
590603
591- let configCodeUpdated = Boolean (
592- viteNodeContext . devServer ?. moduleGraph . getModuleById ( filepath )
593- ) ;
604+ let rootRelativeFilepath = Path . relative ( root , filepath ) ;
605+
606+ let configFileAddedOrRemoved =
607+ fileAddedOrRemoved &&
608+ isEntryFile ( "react-router.config" , rootRelativeFilepath ) ;
594609
595- if ( configCodeUpdated || appFileAddedOrRemoved ) {
596- viteNodeContext . devServer ?. moduleGraph . invalidateAll ( ) ;
597- viteNodeContext . runner ?. moduleCache . clear ( ) ;
610+ if ( configFileAddedOrRemoved ) {
611+ updateReactRouterConfigFile ( ) ;
598612 }
599613
600- if ( appFileAddedOrRemoved || configCodeUpdated ) {
601- let result = await getConfig ( ) ;
614+ let moduleGraphChanged =
615+ configFileAddedOrRemoved ||
616+ Boolean (
617+ viteNodeContext . devServer ?. moduleGraph . getModuleById ( filepath )
618+ ) ;
602619
603- let configChanged = result . ok && ! isEqual ( lastConfig , result . value ) ;
620+ // Bail out if no relevant changes detected
621+ if ( ! moduleGraphChanged && ! appFileAddedOrRemoved ) {
622+ return ;
623+ }
624+
625+ viteNodeContext . devServer ?. moduleGraph . invalidateAll ( ) ;
626+ viteNodeContext . runner ?. moduleCache . clear ( ) ;
627+
628+ let result = await getConfig ( ) ;
629+
630+ let prevAppDirectory = appDirectory ;
631+ appDirectory = Path . normalize (
632+ ( result . value ?? currentConfig ) . appDirectory
633+ ) ;
604634
605- let routeConfigChanged =
606- result . ok && ! isEqual ( lastConfig ?. routes , result . value . routes ) ;
635+ if ( appDirectory !== prevAppDirectory ) {
636+ fsWatcher ! . unwatch ( prevAppDirectory ) ;
637+ fsWatcher ! . add ( appDirectory ) ;
638+ }
607639
608- for ( let handler of changeHandlers ) {
609- handler ( {
610- result,
611- configCodeUpdated,
612- configChanged,
613- routeConfigChanged,
614- path : filepath ,
615- event,
616- } ) ;
617- }
640+ let configCodeChanged =
641+ configFileAddedOrRemoved ||
642+ ( typeof reactRouterConfigFile === "string" &&
643+ isEntryFileDependency (
644+ viteNodeContext . devServer . moduleGraph ,
645+ reactRouterConfigFile ,
646+ filepath
647+ ) ) ;
648+
649+ let routeConfigFile = findEntry ( appDirectory , "routes" , {
650+ absolute : true ,
651+ } ) ;
652+ let routeConfigCodeChanged =
653+ typeof routeConfigFile === "string" &&
654+ isEntryFileDependency (
655+ viteNodeContext . devServer . moduleGraph ,
656+ routeConfigFile ,
657+ filepath
658+ ) ;
659+
660+ let configChanged =
661+ result . ok &&
662+ ! isEqual ( omitRoutes ( currentConfig ) , omitRoutes ( result . value ) ) ;
663+
664+ let routeConfigChanged =
665+ result . ok && ! isEqual ( currentConfig ?. routes , result . value . routes ) ;
666+
667+ for ( let handler of changeHandlers ) {
668+ handler ( {
669+ result,
670+ configCodeChanged,
671+ routeConfigCodeChanged,
672+ configChanged,
673+ routeConfigChanged,
674+ path : filepath ,
675+ event,
676+ } ) ;
677+ }
618678
619- if ( result . ok ) {
620- lastConfig = result . value ;
621- }
679+ if ( result . ok ) {
680+ currentConfig = result . value ;
622681 }
623682 } ) ;
624683 }
@@ -656,8 +715,8 @@ export async function resolveEntryFiles({
656715} ) {
657716 let { appDirectory } = reactRouterConfig ;
658717
659- let defaultsDirectory = path . resolve (
660- path . dirname ( require . resolve ( "@react-router/dev/package.json" ) ) ,
718+ let defaultsDirectory = Path . resolve (
719+ Path . dirname ( require . resolve ( "@react-router/dev/package.json" ) ) ,
661720 "dist" ,
662721 "config" ,
663722 "defaults"
@@ -707,12 +766,12 @@ export async function resolveEntryFiles({
707766 }
708767
709768 let entryClientFilePath = userEntryClientFile
710- ? path . resolve ( reactRouterConfig . appDirectory , userEntryClientFile )
711- : path . resolve ( defaultsDirectory , entryClientFile ) ;
769+ ? Path . resolve ( reactRouterConfig . appDirectory , userEntryClientFile )
770+ : Path . resolve ( defaultsDirectory , entryClientFile ) ;
712771
713772 let entryServerFilePath = userEntryServerFile
714- ? path . resolve ( reactRouterConfig . appDirectory , userEntryServerFile )
715- : path . resolve ( defaultsDirectory , entryServerFile ) ;
773+ ? Path . resolve ( reactRouterConfig . appDirectory , userEntryServerFile )
774+ : Path . resolve ( defaultsDirectory , entryServerFile ) ;
716775
717776 return { entryClientFilePath, entryServerFilePath } ;
718777}
@@ -736,28 +795,90 @@ export const ssrExternals = isInReactRouterMonorepo()
736795function isInReactRouterMonorepo ( ) {
737796 // We use '@react-router/node' for this check since it's a
738797 // dependency of this package and guaranteed to be in node_modules
739- let serverRuntimePath = path . dirname (
798+ let serverRuntimePath = Path . dirname (
740799 require . resolve ( "@react-router/node/package.json" )
741800 ) ;
742- let serverRuntimeParentDir = path . basename (
743- path . resolve ( serverRuntimePath , ".." )
801+ let serverRuntimeParentDir = Path . basename (
802+ Path . resolve ( serverRuntimePath , ".." )
744803 ) ;
745804 return serverRuntimeParentDir === "packages" ;
746805}
747806
807+ async function createCustomLogger ( ) {
808+ await preloadViteEsm ( ) ;
809+ const vite = importViteEsmSync ( ) ;
810+
811+ let customLogger = vite . createLogger ( undefined , {
812+ prefix : "[react-router config]" ,
813+ } ) ;
814+
815+ // Patch the info method to filter out page reload messages that show up in
816+ // the terminal when adding or removing the config file
817+ let originalInfo = customLogger . info ;
818+ customLogger . info = function ( msg , options ) {
819+ if ( msg . includes ( "page reload" ) ) {
820+ return ;
821+ }
822+ return originalInfo . call ( this , msg , options ) ;
823+ } ;
824+
825+ return customLogger ;
826+ }
827+
828+ function omitRoutes (
829+ config : ResolvedReactRouterConfig
830+ ) : ResolvedReactRouterConfig {
831+ return {
832+ ...config ,
833+ routes : { } ,
834+ } ;
835+ }
836+
748837const entryExts = [ ".js" , ".jsx" , ".ts" , ".tsx" ] ;
749838
839+ function isEntryFile ( entryBasename : string , filename : string ) {
840+ return entryExts . some ( ( ext ) => filename === `${ entryBasename } ${ ext } ` ) ;
841+ }
842+
750843function findEntry (
751844 dir : string ,
752845 basename : string ,
753846 options ?: { absolute ?: boolean }
754847) : string | undefined {
755848 for ( let ext of entryExts ) {
756- let file = path . resolve ( dir , basename + ext ) ;
849+ let file = Path . resolve ( dir , basename + ext ) ;
757850 if ( fs . existsSync ( file ) ) {
758- return options ?. absolute ?? false ? file : path . relative ( dir , file ) ;
851+ return options ?. absolute ?? false ? file : Path . relative ( dir , file ) ;
759852 }
760853 }
761854
762855 return undefined ;
763856}
857+
858+ function isEntryFileDependency (
859+ moduleGraph : Vite . ModuleGraph ,
860+ entryFilepath : string ,
861+ filepath : string
862+ ) : boolean {
863+ // Ensure normalized paths
864+ entryFilepath = Path . normalize ( entryFilepath ) ;
865+ filepath = Path . normalize ( filepath ) ;
866+
867+ if ( filepath === entryFilepath ) {
868+ return true ;
869+ }
870+
871+ let mod = moduleGraph . getModuleById ( filepath ) ;
872+
873+ if ( ! mod ) {
874+ return false ;
875+ }
876+
877+ for ( let importer of mod . importers ) {
878+ if ( importer . id === entryFilepath ) {
879+ return true ;
880+ }
881+ }
882+
883+ return false ;
884+ }
0 commit comments