@@ -3,7 +3,7 @@ import { execSync } from "node:child_process";
3
3
import PackageJson from "@npmcli/package-json" ;
4
4
import * as ViteNode from "../vite/vite-node" ;
5
5
import type * as Vite from "vite" ;
6
- import path from "pathe" ;
6
+ import Path from "pathe" ;
7
7
import chokidar , {
8
8
type FSWatcher ,
9
9
type EmitArgs as ChokidarEmitArgs ,
@@ -485,14 +485,14 @@ async function resolveConfig({
485
485
routeDiscovery = userRouteDiscovery ;
486
486
}
487
487
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 ) ;
490
490
491
491
let rootRouteFile = findEntry ( appDirectory , "root" ) ;
492
492
if ( ! rootRouteFile ) {
493
- let rootRouteDisplayPath = path . relative (
493
+ let rootRouteDisplayPath = Path . relative (
494
494
root ,
495
- path . join ( appDirectory , "root.tsx" )
495
+ Path . join ( appDirectory , "root.tsx" )
496
496
) ;
497
497
return err (
498
498
`Could not find a root route module in the app directory as "${ rootRouteDisplayPath } "`
@@ -507,17 +507,17 @@ async function resolveConfig({
507
507
508
508
try {
509
509
if ( ! routeConfigFile ) {
510
- let routeConfigDisplayPath = path . relative (
510
+ let routeConfigDisplayPath = Path . relative (
511
511
root ,
512
- path . join ( appDirectory , "routes.ts" )
512
+ Path . join ( appDirectory , "routes.ts" )
513
513
) ;
514
514
return err ( `Route config file not found at "${ routeConfigDisplayPath } ".` ) ;
515
515
}
516
516
517
517
setAppDirectory ( appDirectory ) ;
518
518
let routeConfigExport = (
519
519
await viteNodeContext . runner . executeFile (
520
- path . join ( appDirectory , routeConfigFile )
520
+ Path . join ( appDirectory , routeConfigFile )
521
521
)
522
522
) . default ;
523
523
let routeConfig = await routeConfigExport ;
@@ -542,7 +542,7 @@ async function resolveConfig({
542
542
"" ,
543
543
error . loc ?. file && error . loc ?. column && error . frame
544
544
? [
545
- path . relative ( appDirectory , error . loc . file ) +
545
+ Path . relative ( appDirectory , error . loc . file ) +
546
546
":" +
547
547
error . loc . line +
548
548
":" +
@@ -595,7 +595,8 @@ type ChokidarEventName = ChokidarEmitArgs[0];
595
595
596
596
type ChangeHandler = ( args : {
597
597
result : Result < ResolvedReactRouterConfig > ;
598
- configCodeUpdated : boolean ;
598
+ configCodeChanged : boolean ;
599
+ routeConfigCodeChanged : boolean ;
599
600
configChanged : boolean ;
600
601
routeConfigChanged : boolean ;
601
602
path : string ;
@@ -617,16 +618,27 @@ export async function createConfigLoader({
617
618
rootDirectory ?: string ;
618
619
mode : string ;
619
620
} ) : Promise < ConfigLoader > {
620
- root = root ?? process . env . REACT_ROUTER_ROOT ?? process . cwd ( ) ;
621
+ root = Path . normalize ( root ?? process . env . REACT_ROUTER_ROOT ?? process . cwd ( ) ) ;
621
622
623
+ let vite = await import ( "vite" ) ;
622
624
let viteNodeContext = await ViteNode . createContext ( {
623
625
root,
624
626
mode,
627
+ // Filter out any info level logs from vite-node
628
+ customLogger : vite . createLogger ( "warn" , {
629
+ prefix : "[react-router]" ,
630
+ } ) ,
625
631
} ) ;
626
632
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 ( ) ;
630
642
631
643
let getConfig = ( ) =>
632
644
resolveConfig ( { root, viteNodeContext, reactRouterConfigFile } ) ;
@@ -639,9 +651,9 @@ export async function createConfigLoader({
639
651
throw new Error ( initialConfigResult . error ) ;
640
652
}
641
653
642
- appDirectory = initialConfigResult . value . appDirectory ;
654
+ appDirectory = Path . normalize ( initialConfigResult . value . appDirectory ) ;
643
655
644
- let lastConfig = initialConfigResult . value ;
656
+ let currentConfig = initialConfigResult . value ;
645
657
646
658
let fsWatcher : FSWatcher | undefined ;
647
659
let changeHandlers : ChangeHandler [ ] = [ ] ;
@@ -658,54 +670,108 @@ export async function createConfigLoader({
658
670
changeHandlers . push ( handler ) ;
659
671
660
672
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
+ } ) ;
668
687
669
688
fsWatcher . on ( "all" , async ( ...args : ChokidarEmitArgs ) => {
670
689
let [ event , rawFilepath ] = args ;
671
- let filepath = path . normalize ( rawFilepath ) ;
690
+ let filepath = Path . normalize ( rawFilepath ) ;
691
+
692
+ let fileAddedOrRemoved = event === "add" || event === "unlink" ;
672
693
673
694
let appFileAddedOrRemoved =
674
- appDirectory &&
675
- ( event === "add" || event === "unlink" ) &&
676
- filepath . startsWith ( path . normalize ( appDirectory ) ) ;
695
+ fileAddedOrRemoved &&
696
+ filepath . startsWith ( Path . normalize ( appDirectory ) ) ;
677
697
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 ) ;
681
703
682
- if ( configCodeUpdated || appFileAddedOrRemoved ) {
683
- viteNodeContext . devServer ?. moduleGraph . invalidateAll ( ) ;
684
- viteNodeContext . runner ?. moduleCache . clear ( ) ;
704
+ if ( configFileAddedOrRemoved ) {
705
+ updateReactRouterConfigFile ( ) ;
685
706
}
686
707
687
- if ( appFileAddedOrRemoved || configCodeUpdated ) {
688
- let result = await getConfig ( ) ;
708
+ let moduleGraphChanged =
709
+ configFileAddedOrRemoved ||
710
+ Boolean (
711
+ viteNodeContext . devServer ?. moduleGraph . getModuleById ( filepath )
712
+ ) ;
689
713
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
+ ) ;
691
728
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
+ }
694
733
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
+ }
705
772
706
- if ( result . ok ) {
707
- lastConfig = result . value ;
708
- }
773
+ if ( result . ok ) {
774
+ currentConfig = result . value ;
709
775
}
710
776
} ) ;
711
777
}
@@ -750,8 +816,8 @@ export async function resolveEntryFiles({
750
816
} ) {
751
817
let { appDirectory } = reactRouterConfig ;
752
818
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" ) ) ,
755
821
"dist" ,
756
822
"config" ,
757
823
"defaults"
@@ -775,7 +841,7 @@ export async function resolveEntryFiles({
775
841
) ;
776
842
}
777
843
778
- let packageJsonDirectory = path . dirname ( packageJsonPath ) ;
844
+ let packageJsonDirectory = Path . dirname ( packageJsonPath ) ;
779
845
let pkgJson = await PackageJson . load ( packageJsonDirectory ) ;
780
846
let deps = pkgJson . content . dependencies ?? { } ;
781
847
@@ -814,18 +880,31 @@ export async function resolveEntryFiles({
814
880
}
815
881
816
882
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 ) ;
819
885
820
886
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 ) ;
823
889
824
890
return { entryClientFilePath, entryServerFilePath } ;
825
891
}
826
892
893
+ function omitRoutes (
894
+ config : ResolvedReactRouterConfig
895
+ ) : ResolvedReactRouterConfig {
896
+ return {
897
+ ...config ,
898
+ routes : { } ,
899
+ } ;
900
+ }
901
+
827
902
const entryExts = [ ".js" , ".jsx" , ".ts" , ".tsx" ] ;
828
903
904
+ function isEntryFile ( entryBasename : string , filename : string ) {
905
+ return entryExts . some ( ( ext ) => filename === `${ entryBasename } ${ ext } ` ) ;
906
+ }
907
+
829
908
function findEntry (
830
909
dir : string ,
831
910
basename : string ,
@@ -835,22 +914,22 @@ function findEntry(
835
914
walkParents ?: boolean ;
836
915
}
837
916
) : 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 ) ;
840
919
841
920
while ( true ) {
842
921
for ( let ext of options ?. extensions ?? entryExts ) {
843
- let file = path . resolve ( currentDir , basename + ext ) ;
922
+ let file = Path . resolve ( currentDir , basename + ext ) ;
844
923
if ( fs . existsSync ( file ) ) {
845
- return options ?. absolute ?? false ? file : path . relative ( dir , file ) ;
924
+ return options ?. absolute ?? false ? file : Path . relative ( dir , file ) ;
846
925
}
847
926
}
848
927
849
928
if ( ! options ?. walkParents ) {
850
929
return undefined ;
851
930
}
852
931
853
- let parentDir = path . dirname ( currentDir ) ;
932
+ let parentDir = Path . dirname ( currentDir ) ;
854
933
// Break out when we've reached the root directory or we're about to get
855
934
// stuck in a loop where `path.dirname` keeps returning "/"
856
935
if ( currentDir === root || parentDir === currentDir ) {
@@ -860,3 +939,46 @@ function findEntry(
860
939
currentDir = parentDir ;
861
940
}
862
941
}
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