Repository: remix-run/react-router
File: packages/react-router-dev/config/config.ts (TypeScript)

Code before:
          });
          let routeConfigCodeChanged =
            typeof routeConfigFile === "string" &&
            isEntryFileDependency(
              viteNodeContext.devServer.moduleGraph,
              routeConfigFile,
              filepath
            );

          let configChanged =
            result.ok &&
            !isEqual(omitRoutes(currentConfig), omitRoutes(result.value));

          let routeConfigChanged =
            result.ok && !isEqual(currentConfig?.routes, result.value.routes);

          for (let handler of changeHandlers) {
            handler({
              result,
              configCodeChanged,
              routeConfigCodeChanged,
              configChanged,
              routeConfigChanged,
              path: filepath,
              event,
            });
          }

          if (result.ok) {
            currentConfig = result.value;
          }
        });
      }

      return () => {
        changeHandlers = changeHandlers.filter(
          (changeHandler) => changeHandler !== handler
        );
      };
    },
    close: async () => {
      changeHandlers = [];
      await viteNodeContext.devServer.close();
      await fsWatcher?.close();
    },
  };
}

export async function loadConfig({ rootDirectory }: { rootDirectory: string }) {
  let configLoader = await createConfigLoader({
    rootDirectory,

Review comment (suggestion):
Ideally, we could watch the root for _only_ changes to `<root>/react-router.config.ts` (and its module graph) and the corresponding app directory, rather than coarse-grained allowlist for any file in the root. But that's not a blocker.

Code after:
  let fsWatcher: FSWatcher | undefined;
  let changeHandlers: ChangeHandler[] = [];

  return {
    getConfig,
    onChange: (handler: ChangeHandler) => {
      if (!watch) {
        throw new Error(
          "onChange is not supported when watch mode is disabled"
        );
      }

      changeHandlers.push(handler);

      if (!fsWatcher) {
        fsWatcher = chokidar.watch([root, appDirectory], {
          ignoreInitial: true,
          ignored: (path) => {
            let dirname = Path.dirname(path);

            return (
              !dirname.startsWith(appDirectory) &&
              // Ensure we're only watching files outside of the app directory
              // that are at the root level, not nested in subdirectories
              path !== root && // Watch the root directory itself
              dirname !== root // Watch files at the root level
            );
          },
        });

        fsWatcher.on("all", async (...args: ChokidarEmitArgs) => {
          let [event, rawFilepath] = args;
          let filepath = Path.normalize(rawFilepath);

          let fileAddedOrRemoved = event === "add" || event === "unlink";

          let appFileAddedOrRemoved =
            fileAddedOrRemoved &&
            filepath.startsWith(Path.normalize(appDirectory));

          let rootRelativeFilepath = Path.relative(root, filepath);

          let configFileAddedOrRemoved =
            fileAddedOrRemoved &&
            isEntryFile("react-router.config", rootRelativeFilepath);

          if (configFileAddedOrRemoved) {
            updateReactRouterConfigFile();
          }

          let moduleGraphChanged =