@@ -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 ,
@@ -485,14 +485,14 @@ async function resolveConfig({
485485 routeDiscovery = userRouteDiscovery ;
486486 }
487487
488- let appDirectory = path . resolve ( root , userAppDirectory || "app" ) ;
489- let buildDirectory = path . resolve ( root , userBuildDirectory ) ;
488+ let appDirectory = Path . resolve ( root , userAppDirectory || "app" ) ;
489+ let buildDirectory = Path . resolve ( root , userBuildDirectory ) ;
490490
491491 let rootRouteFile = findEntry ( appDirectory , "root" ) ;
492492 if ( ! rootRouteFile ) {
493- let rootRouteDisplayPath = path . relative (
493+ let rootRouteDisplayPath = Path . relative (
494494 root ,
495- path . join ( appDirectory , "root.tsx" )
495+ Path . join ( appDirectory , "root.tsx" )
496496 ) ;
497497 return err (
498498 `Could not find a root route module in the app directory as "${ rootRouteDisplayPath } "`
@@ -507,17 +507,17 @@ async function resolveConfig({
507507
508508 try {
509509 if ( ! routeConfigFile ) {
510- let routeConfigDisplayPath = path . relative (
510+ let routeConfigDisplayPath = Path . relative (
511511 root ,
512- path . join ( appDirectory , "routes.ts" )
512+ Path . join ( appDirectory , "routes.ts" )
513513 ) ;
514514 return err ( `Route config file not found at "${ routeConfigDisplayPath } ".` ) ;
515515 }
516516
517517 setAppDirectory ( appDirectory ) ;
518518 let routeConfigExport = (
519519 await viteNodeContext . runner . executeFile (
520- path . join ( appDirectory , routeConfigFile )
520+ Path . join ( appDirectory , routeConfigFile )
521521 )
522522 ) . default ;
523523 let routeConfig = await routeConfigExport ;
@@ -542,7 +542,7 @@ async function resolveConfig({
542542 "" ,
543543 error . loc ?. file && error . loc ?. column && error . frame
544544 ? [
545- path . relative ( appDirectory , error . loc . file ) +
545+ Path . relative ( appDirectory , error . loc . file ) +
546546 ":" +
547547 error . loc . line +
548548 ":" +
@@ -595,7 +595,8 @@ type ChokidarEventName = ChokidarEmitArgs[0];
595595
596596type ChangeHandler = ( args : {
597597 result : Result < ResolvedReactRouterConfig > ;
598- configCodeUpdated : boolean ;
598+ configCodeChanged : boolean ;
599+ routeConfigCodeChanged : boolean ;
599600 configChanged : boolean ;
600601 routeConfigChanged : boolean ;
601602 path : string ;
@@ -617,16 +618,27 @@ export async function createConfigLoader({
617618 rootDirectory ?: string ;
618619 mode : string ;
619620} ) : Promise < ConfigLoader > {
620- root = root ?? process . env . REACT_ROUTER_ROOT ?? process . cwd ( ) ;
621+ root = Path . normalize ( root ?? process . env . REACT_ROUTER_ROOT ?? process . cwd ( ) ) ;
621622
623+ let vite = await import ( "vite" ) ;
622624 let viteNodeContext = await ViteNode . createContext ( {
623625 root,
624626 mode,
627+ // Filter out any info level logs from vite-node
628+ customLogger : vite . createLogger ( "warn" , {
629+ prefix : "[react-router]" ,
630+ } ) ,
625631 } ) ;
626632
627- let reactRouterConfigFile = findEntry ( root , "react-router.config" , {
628- absolute : true ,
629- } ) ;
633+ let reactRouterConfigFile : string | undefined ;
634+
635+ let updateReactRouterConfigFile = ( ) => {
636+ reactRouterConfigFile = findEntry ( root , "react-router.config" , {
637+ absolute : true ,
638+ } ) ;
639+ } ;
640+
641+ updateReactRouterConfigFile ( ) ;
630642
631643 let getConfig = ( ) =>
632644 resolveConfig ( { root, viteNodeContext, reactRouterConfigFile } ) ;
@@ -639,9 +651,9 @@ export async function createConfigLoader({
639651 throw new Error ( initialConfigResult . error ) ;
640652 }
641653
642- appDirectory = initialConfigResult . value . appDirectory ;
654+ appDirectory = Path . normalize ( initialConfigResult . value . appDirectory ) ;
643655
644- let lastConfig = initialConfigResult . value ;
656+ let currentConfig = initialConfigResult . value ;
645657
646658 let fsWatcher : FSWatcher | undefined ;
647659 let changeHandlers : ChangeHandler [ ] = [ ] ;
@@ -658,54 +670,108 @@ export async function createConfigLoader({
658670 changeHandlers . push ( handler ) ;
659671
660672 if ( ! fsWatcher ) {
661- fsWatcher = chokidar . watch (
662- [
663- ...( reactRouterConfigFile ? [ reactRouterConfigFile ] : [ ] ) ,
664- appDirectory ,
665- ] ,
666- { ignoreInitial : true }
667- ) ;
673+ fsWatcher = chokidar . watch ( [ root , appDirectory ] , {
674+ ignoreInitial : true ,
675+ ignored : ( path ) => {
676+ let dirname = Path . dirname ( path ) ;
677+
678+ return (
679+ ! dirname . startsWith ( appDirectory ) &&
680+ // Ensure we're only watching files outside of the app directory
681+ // that are at the root level, not nested in subdirectories
682+ path !== root && // Watch the root directory itself
683+ dirname !== root // Watch files at the root level
684+ ) ;
685+ } ,
686+ } ) ;
668687
669688 fsWatcher . on ( "all" , async ( ...args : ChokidarEmitArgs ) => {
670689 let [ event , rawFilepath ] = args ;
671- let filepath = path . normalize ( rawFilepath ) ;
690+ let filepath = Path . normalize ( rawFilepath ) ;
691+
692+ let fileAddedOrRemoved = event === "add" || event === "unlink" ;
672693
673694 let appFileAddedOrRemoved =
674- appDirectory &&
675- ( event === "add" || event === "unlink" ) &&
676- filepath . startsWith ( path . normalize ( appDirectory ) ) ;
695+ fileAddedOrRemoved &&
696+ filepath . startsWith ( Path . normalize ( appDirectory ) ) ;
677697
678- let configCodeUpdated = Boolean (
679- viteNodeContext . devServer ?. moduleGraph . getModuleById ( filepath )
680- ) ;
698+ let rootRelativeFilepath = Path . relative ( root , filepath ) ;
699+
700+ let configFileAddedOrRemoved =
701+ fileAddedOrRemoved &&
702+ isEntryFile ( "react-router.config" , rootRelativeFilepath ) ;
681703
682- if ( configCodeUpdated || appFileAddedOrRemoved ) {
683- viteNodeContext . devServer ?. moduleGraph . invalidateAll ( ) ;
684- viteNodeContext . runner ?. moduleCache . clear ( ) ;
704+ if ( configFileAddedOrRemoved ) {
705+ updateReactRouterConfigFile ( ) ;
685706 }
686707
687- if ( appFileAddedOrRemoved || configCodeUpdated ) {
688- let result = await getConfig ( ) ;
708+ let moduleGraphChanged =
709+ configFileAddedOrRemoved ||
710+ Boolean (
711+ viteNodeContext . devServer ?. moduleGraph . getModuleById ( filepath )
712+ ) ;
689713
690- let configChanged = result . ok && ! isEqual ( lastConfig , result . value ) ;
714+ // Bail out if no relevant changes detected
715+ if ( ! moduleGraphChanged && ! appFileAddedOrRemoved ) {
716+ return ;
717+ }
718+
719+ viteNodeContext . devServer ?. moduleGraph . invalidateAll ( ) ;
720+ viteNodeContext . runner ?. moduleCache . clear ( ) ;
721+
722+ let result = await getConfig ( ) ;
723+
724+ let prevAppDirectory = appDirectory ;
725+ appDirectory = Path . normalize (
726+ ( result . value ?? currentConfig ) . appDirectory
727+ ) ;
691728
692- let routeConfigChanged =
693- result . ok && ! isEqual ( lastConfig ?. routes , result . value . routes ) ;
729+ if ( appDirectory !== prevAppDirectory ) {
730+ fsWatcher ! . unwatch ( prevAppDirectory ) ;
731+ fsWatcher ! . add ( appDirectory ) ;
732+ }
694733
695- for ( let handler of changeHandlers ) {
696- handler ( {
697- result,
698- configCodeUpdated,
699- configChanged,
700- routeConfigChanged,
701- path : filepath ,
702- event,
703- } ) ;
704- }
734+ let configCodeChanged =
735+ configFileAddedOrRemoved ||
736+ ( reactRouterConfigFile !== undefined &&
737+ isEntryFileDependency (
738+ viteNodeContext . devServer . moduleGraph ,
739+ reactRouterConfigFile ,
740+ filepath
741+ ) ) ;
742+
743+ let routeConfigFile = findEntry ( appDirectory , "routes" , {
744+ absolute : true ,
745+ } ) ;
746+ let routeConfigCodeChanged =
747+ routeConfigFile !== undefined &&
748+ isEntryFileDependency (
749+ viteNodeContext . devServer . moduleGraph ,
750+ routeConfigFile ,
751+ filepath
752+ ) ;
753+
754+ let configChanged =
755+ result . ok &&
756+ ! isEqual ( omitRoutes ( currentConfig ) , omitRoutes ( result . value ) ) ;
757+
758+ let routeConfigChanged =
759+ result . ok && ! isEqual ( currentConfig ?. routes , result . value . routes ) ;
760+
761+ for ( let handler of changeHandlers ) {
762+ handler ( {
763+ result,
764+ configCodeChanged,
765+ routeConfigCodeChanged,
766+ configChanged,
767+ routeConfigChanged,
768+ path : filepath ,
769+ event,
770+ } ) ;
771+ }
705772
706- if ( result . ok ) {
707- lastConfig = result . value ;
708- }
773+ if ( result . ok ) {
774+ currentConfig = result . value ;
709775 }
710776 } ) ;
711777 }
@@ -750,8 +816,8 @@ export async function resolveEntryFiles({
750816} ) {
751817 let { appDirectory } = reactRouterConfig ;
752818
753- let defaultsDirectory = path . resolve (
754- path . dirname ( require . resolve ( "@react-router/dev/package.json" ) ) ,
819+ let defaultsDirectory = Path . resolve (
820+ Path . dirname ( require . resolve ( "@react-router/dev/package.json" ) ) ,
755821 "dist" ,
756822 "config" ,
757823 "defaults"
@@ -775,7 +841,7 @@ export async function resolveEntryFiles({
775841 ) ;
776842 }
777843
778- let packageJsonDirectory = path . dirname ( packageJsonPath ) ;
844+ let packageJsonDirectory = Path . dirname ( packageJsonPath ) ;
779845 let pkgJson = await PackageJson . load ( packageJsonDirectory ) ;
780846 let deps = pkgJson . content . dependencies ?? { } ;
781847
@@ -814,18 +880,31 @@ export async function resolveEntryFiles({
814880 }
815881
816882 let entryClientFilePath = userEntryClientFile
817- ? path . resolve ( reactRouterConfig . appDirectory , userEntryClientFile )
818- : path . resolve ( defaultsDirectory , entryClientFile ) ;
883+ ? Path . resolve ( reactRouterConfig . appDirectory , userEntryClientFile )
884+ : Path . resolve ( defaultsDirectory , entryClientFile ) ;
819885
820886 let entryServerFilePath = userEntryServerFile
821- ? path . resolve ( reactRouterConfig . appDirectory , userEntryServerFile )
822- : path . resolve ( defaultsDirectory , entryServerFile ) ;
887+ ? Path . resolve ( reactRouterConfig . appDirectory , userEntryServerFile )
888+ : Path . resolve ( defaultsDirectory , entryServerFile ) ;
823889
824890 return { entryClientFilePath, entryServerFilePath } ;
825891}
826892
893+ function omitRoutes (
894+ config : ResolvedReactRouterConfig
895+ ) : ResolvedReactRouterConfig {
896+ return {
897+ ...config ,
898+ routes : { } ,
899+ } ;
900+ }
901+
827902const entryExts = [ ".js" , ".jsx" , ".ts" , ".tsx" ] ;
828903
904+ function isEntryFile ( entryBasename : string , filename : string ) {
905+ return entryExts . some ( ( ext ) => filename === `${ entryBasename } ${ ext } ` ) ;
906+ }
907+
829908function findEntry (
830909 dir : string ,
831910 basename : string ,
@@ -835,22 +914,22 @@ function findEntry(
835914 walkParents ?: boolean ;
836915 }
837916) : string | undefined {
838- let currentDir = path . resolve ( dir ) ;
839- let { root } = path . parse ( currentDir ) ;
917+ let currentDir = Path . resolve ( dir ) ;
918+ let { root } = Path . parse ( currentDir ) ;
840919
841920 while ( true ) {
842921 for ( let ext of options ?. extensions ?? entryExts ) {
843- let file = path . resolve ( currentDir , basename + ext ) ;
922+ let file = Path . resolve ( currentDir , basename + ext ) ;
844923 if ( fs . existsSync ( file ) ) {
845- return options ?. absolute ?? false ? file : path . relative ( dir , file ) ;
924+ return options ?. absolute ?? false ? file : Path . relative ( dir , file ) ;
846925 }
847926 }
848927
849928 if ( ! options ?. walkParents ) {
850929 return undefined ;
851930 }
852931
853- let parentDir = path . dirname ( currentDir ) ;
932+ let parentDir = Path . dirname ( currentDir ) ;
854933 // Break out when we've reached the root directory or we're about to get
855934 // stuck in a loop where `path.dirname` keeps returning "/"
856935 if ( currentDir === root || parentDir === currentDir ) {
@@ -860,3 +939,46 @@ function findEntry(
860939 currentDir = parentDir ;
861940 }
862941}
942+
943+ function isEntryFileDependency (
944+ moduleGraph : Vite . ModuleGraph ,
945+ entryFilepath : string ,
946+ filepath : string ,
947+ visited = new Set < string > ( )
948+ ) : boolean {
949+ // Ensure normalized paths
950+ entryFilepath = Path . normalize ( entryFilepath ) ;
951+ filepath = Path . normalize ( filepath ) ;
952+
953+ if ( visited . has ( filepath ) ) {
954+ return false ;
955+ }
956+
957+ visited . add ( filepath ) ;
958+
959+ if ( filepath === entryFilepath ) {
960+ return true ;
961+ }
962+
963+ let mod = moduleGraph . getModuleById ( filepath ) ;
964+
965+ if ( ! mod ) {
966+ return false ;
967+ }
968+
969+ // Recursively check all importers to see if any of them are the entry file
970+ for ( let importer of mod . importers ) {
971+ if ( ! importer . id ) {
972+ continue ;
973+ }
974+
975+ if (
976+ importer . id === entryFilepath ||
977+ isEntryFileDependency ( moduleGraph , entryFilepath , importer . id , visited )
978+ ) {
979+ return true ;
980+ }
981+ }
982+
983+ return false ;
984+ }
0 commit comments