Skip to content

Commit 09b7f49

Browse files
Improve config change handling (#12319)
Co-authored-by: Pedro Cattori <[email protected]>
1 parent b571356 commit 09b7f49

File tree

4 files changed

+208
-76
lines changed

4 files changed

+208
-76
lines changed

.changeset/stupid-kiwis-laugh.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-router/dev": patch
3+
---
4+
5+
Update config when `react-router.config.ts` is created or deleted during development.

packages/react-router-dev/config/config.ts

+185-63
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { execSync } from "node:child_process";
33
import PackageJson from "@npmcli/package-json";
44
import * as ViteNode from "../vite/vite-node";
55
import type * as Vite from "vite";
6-
import path from "pathe";
6+
import Path from "pathe";
77
import 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

596596
type 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+
827902
const entryExts = [".js", ".jsx", ".ts", ".tsx"];
828903

904+
function isEntryFile(entryBasename: string, filename: string) {
905+
return entryExts.some((ext) => filename === `${entryBasename}${ext}`);
906+
}
907+
829908
function 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

Comments
 (0)