diff --git a/integration/vite-basename-test.ts b/integration/vite-basename-test.ts index f77842a7c9..b9d7dc7d33 100644 --- a/integration/vite-basename-test.ts +++ b/integration/vite-basename-test.ts @@ -221,6 +221,13 @@ test.describe("Vite base + React Router basename", () => { await workflowDev({ page, cwd, port, basename: "/mybase/app/" }); }); + test("works when base and basename match the app directory name", async ({ + page, + }) => { + await setup({ base: "/app/", basename: "/app/" }); + await workflowDev({ page, cwd, port, base: "/app/", basename: "/app/" }); + }); + test("errors if basename does not start with base", async ({ page, }) => { @@ -421,6 +428,13 @@ test.describe("Vite base + React Router basename", () => { }); }); + test("works when base and basename match the app directory name", async ({ + page, + }) => { + await setup({ base: "/app/", basename: "/app/" }); + await workflowBuild({ page, port, base: "/app/", basename: "/app/" }); + }); + test("works when basename does not start with base", async ({ page, }) => { diff --git a/packages/react-router-dev/.changes/patch.fix-basename-app-directory-conflict.md b/packages/react-router-dev/.changes/patch.fix-basename-app-directory-conflict.md new file mode 100644 index 0000000000..2d2f2e8106 --- /dev/null +++ b/packages/react-router-dev/.changes/patch.fix-basename-app-directory-conflict.md @@ -0,0 +1,7 @@ +Fix `basename` conflicting with `app` directory name when Vite `base` is set + +When the Vite `base` config and React Router `basename` both match the +app directory name (e.g. `base: "/app/"`, `basename: "/app/"`), Vite would +strip the base prefix from server-build virtual module import paths, causing +"Failed to load url /root.tsx" errors. The fix uses `/@fs/` absolute paths +for those imports to bypass Vite's base-stripping logic. diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts index 60180d447d..7eaf995d1d 100644 --- a/packages/react-router-dev/vite/plugin.ts +++ b/packages/react-router-dev/vite/plugin.ts @@ -818,7 +818,9 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => { return ` import * as entryServer from ${JSON.stringify( - resolveFileUrl(ctx, ctx.entryServerFilePath), + resolveFileUrl(ctx, ctx.entryServerFilePath, { + publicPath: ctx.publicPath, + }), )}; ${Object.keys(routes) .map((key, index) => { @@ -834,6 +836,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => { resolveFileUrl( ctx, resolveRelativeRouteFilePath(route, ctx.reactRouterConfig), + { publicPath: ctx.publicPath }, ), )};`; } diff --git a/packages/react-router-dev/vite/resolve-file-url.ts b/packages/react-router-dev/vite/resolve-file-url.ts index ae688febed..8a7ff2a608 100644 --- a/packages/react-router-dev/vite/resolve-file-url.ts +++ b/packages/react-router-dev/vite/resolve-file-url.ts @@ -5,6 +5,7 @@ import { getVite } from "./vite"; export const resolveFileUrl = ( { rootDirectory }: { rootDirectory: string }, filePath: string, + { publicPath }: { publicPath?: string } = {}, ) => { let vite = getVite(); let relativePath = path.relative(rootDirectory, filePath); @@ -18,5 +19,16 @@ export const resolveFileUrl = ( return path.posix.join("/@fs", vite.normalizePath(filePath)); } - return "/" + vite.normalizePath(relativePath); + let url = "/" + vite.normalizePath(relativePath); + + // When the Vite base config (publicPath) matches the start of the + // root-relative file URL, Vite strips the base prefix during SSR module + // loading, causing the file to not be found (e.g. basename "/app/" with + // appDirectory "app/" makes "/app/root.tsx" resolve to "/root.tsx"). Use + // the /@fs/ absolute path form to bypass Vite's base stripping. + if (publicPath && publicPath !== "/" && url.startsWith(publicPath)) { + return path.posix.join("/@fs", vite.normalizePath(filePath)); + } + + return url; };