diff --git a/contributors.yml b/contributors.yml index f7cc03ee2c..df9faae6c8 100644 --- a/contributors.yml +++ b/contributors.yml @@ -105,6 +105,7 @@ - elylucas - emzoumpo - engpetermwangi +- EqualMa - ericschn - esadek - faergeek diff --git a/packages/react-router-dev/__tests__/path-starts-with-test.ts b/packages/react-router-dev/__tests__/path-starts-with-test.ts new file mode 100644 index 0000000000..b08ff5a465 --- /dev/null +++ b/packages/react-router-dev/__tests__/path-starts-with-test.ts @@ -0,0 +1,52 @@ +import pathStartsWith from "../config/path-starts-with"; + +describe("pathStartsWith", () => { + it("real world example", () => { + const APP_FOLDER = "/workspace/app"; + expect(pathStartsWith("/workspace/app/root.tsx", APP_FOLDER)).toBe(true); + + const IRRELEVANT_FILE = "/workspace/apps/irrelevant-project/package.json"; + expect(IRRELEVANT_FILE.startsWith(APP_FOLDER)).toBe(true); + expect(pathStartsWith(IRRELEVANT_FILE, APP_FOLDER)).toBe(false); + }); + + it("windows paths", () => { + const APP_FOLDER = "C:\\\\workspace\\app"; + expect(pathStartsWith("C:\\\\workspace\\app\\root.tsx", APP_FOLDER)).toBe( + true + ); + + const IRRELEVANT_FILE = + "C:\\\\workspace\\apps\\irrelevant-project\\package.json"; + expect(IRRELEVANT_FILE.startsWith(APP_FOLDER)).toBe(true); + expect(pathStartsWith(IRRELEVANT_FILE, APP_FOLDER)).toBe(false); + }); + + it("edge cases", () => { + expect(pathStartsWith("./dir", "./dir")).toBe(true); + expect(pathStartsWith("./dir/", "./dir")).toBe(true); + expect(pathStartsWith("./dir/path", "./dir")).toBe(true); + expect(pathStartsWith("./dir/path", "./dir/")).toBe(true); + expect(pathStartsWith("./dir/path/", "./dir")).toBe(true); + expect(pathStartsWith("./dir/path/", "./dir/")).toBe(true); + + expect(pathStartsWith("dir", "dir")).toBe(true); + expect(pathStartsWith("dir/", "dir")).toBe(true); + expect(pathStartsWith("dir/path", "dir")).toBe(true); + expect(pathStartsWith("dir/path", "dir/")).toBe(true); + expect(pathStartsWith("dir/path/", "dir")).toBe(true); + expect(pathStartsWith("dir/path/", "dir/")).toBe(true); + + expect(pathStartsWith("/dir", "/dir")).toBe(true); + expect(pathStartsWith("/dir/", "/dir")).toBe(true); + expect(pathStartsWith("/dir/path", "/dir")).toBe(true); + expect(pathStartsWith("/dir/path", "/dir/")).toBe(true); + expect(pathStartsWith("/dir/path/", "/dir")).toBe(true); + expect(pathStartsWith("/dir/path/", "/dir/")).toBe(true); + }); + + it("paths are not normalized intentionally", () => { + expect(pathStartsWith("./dir/path", "dir")).toBe(false); + expect(pathStartsWith("/dir/a/b/c/../../..", "/dir/a/b/c")).toBe(true); + }); +}); diff --git a/packages/react-router-dev/config/config.ts b/packages/react-router-dev/config/config.ts index 5057b1b8c3..a858eb8b66 100644 --- a/packages/react-router-dev/config/config.ts +++ b/packages/react-router-dev/config/config.ts @@ -22,6 +22,7 @@ import { configRoutesToRouteManifest, } from "./routes"; import { detectPackageManager } from "../cli/detectPackageManager"; +import pathStartsWith from "./path-starts-with"; const excludedConfigPresetKeys = ["presets"] as const satisfies ReadonlyArray< keyof ReactRouterConfig @@ -686,7 +687,7 @@ export async function createConfigLoader({ let dirname = Path.dirname(path); return ( - !dirname.startsWith(appDirectory) && + !pathStartsWith(dirname, 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 diff --git a/packages/react-router-dev/config/path-starts-with.ts b/packages/react-router-dev/config/path-starts-with.ts new file mode 100644 index 0000000000..68a670e06d --- /dev/null +++ b/packages/react-router-dev/config/path-starts-with.ts @@ -0,0 +1,19 @@ +/** + * Returns true if `a` is a path that starts with `b` and might contains subpath. + * + * Note that `a` and `b` will not be normalized + * so the returned boolean doesn't indicate whether `a` resolves to a path contained in `b`. + */ +export default function pathStartsWith(a: string, b: string) { + return ( + a.startsWith(b) && + // they are the same string + (a.length === b.length || + // or b is a directory path + b.endsWith("/") || + b.endsWith("\\") || + // or a is `${b}/${subpath}` + a[b.length] === "/" || + a[b.length] === "\\") + ); +}