remix
remix copied to clipboard
`v3_lazyRouteDiscovery` does not respect `Vite#base` configuration
Reproduction
I’ve noticed that in the latest Remix release, v2.13.1, v3_lazyRouteDiscovery does not respect Vite’s base configuration.
After looking into the source code, it appears that while lazyRouteDiscovery respects Remix’s basename, however the two configuration options are for different purposes.
Ideally, I’d like the __manifest file to be prefixed with Vite’s base configuration (aligning it with all other JS being served).
It's my understanding that all assets are served under this base (similar to how webpack publicPath) if provided; however, __manifest is not, which is causing an error.
An asset that works (respecting Vite's base)
v3_lazyRouteDiscovery not respecting the base, thus 404'ing
Reproduction: https://github.com/hanford/remix-fogOfWar-base
System Info
System:
OS: macOS 15.0
CPU: (10) arm64 Apple M1 Pro
Memory: 110.59 MB / 32.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 20.10.0 - /opt/homebrew/bin/node
Yarn: 1.22.22 - /opt/homebrew/bin/yarn
npm: 10.2.3 - /opt/homebrew/bin/npm
pnpm: 7.32.4 - ~/.npm-global/bin/pnpm
bun: 1.1.22 - ~/.bun/bin/bun
Watchman: 2024.08.12.00 - /opt/homebrew/bin/watchman
Browsers:
Chrome: 129.0.6668.101
Safari: 18.0
npmPackages:
@remix-run/dev: ^2.13.1 => 2.13.1
@remix-run/node: ^2.13.1 => 2.13.1
@remix-run/react: ^2.13.1 => 2.13.1
@remix-run/serve: ^2.13.1 => 2.13.1
vite: ^5.1.0 => 5.4.9
Used Package Manager
pnpm
Expected Behavior
__manifest should be servered from base + __manifest
Actual Behavior
__manifest doesn't respect Vite's base, which leads to a 404 breaking the future flag
I'm seeing a 404 on https://github.com/hanford/remix-fogOfWar-base - can you check the permissions on that?
After looking into the source code, it appears that while lazyRouteDiscovery respects Remix’s basename, however the two configuration options are for different purposes.
Can you elaborate on your use case? Knowing what you're expected dev/prod setups are would help us ensure we've got the use cases handled properly.
To my recollection, when we did our initial basename implementation we found that during vite dev, basename had to start with base for vite dev to be able to work at all and I thought we had a warning/error in there when it didn't. It looks like when basename === base and when basename.startsWith(base), these __manifest requests work as expected.
Made the repo public!
My situation is as follows, I have ~10 separate frontend applications being served on different paths...
example.com/employee/* <-- Application one
example.com/admin/* <-- Application two
example.com/feature/* <-- Application three
...
We have a reverse proxy in front of our applications that looks at the path of an incoming requests, and proxies it to the correct place.
These applications link to one another, share session information, UI components, and from a users perspective, it's just one application.
We aren't using Remix's basename, because we don't want our Links to automatically be prefixed within the application. Instead we're naming all of our routes with what would be the basename. e.g.
admin-application/
/app/routes/
/admin-route-1.tsx
/admin-route-2.tsx
/admin-route-3.tsx
However, we are using Vite#base, so that our application artifacts (JS/other assets) are prefixed with the applications base path so at the router level we know where to send requests for it's assets.
So, we want to enable lazyRouteDiscovery, but need the __manifest.json request to be prefixed with basePath similar to other application artifacts otherwise at the router level, we don't which application the request should go to.
Hopefully this makes sense, happy to provide more details or try alternative Remix/Vite configurations. Today we're actually a full Next.js shop (with our own patches on top of Next.js) and using a combination of Next.js features that enable this whole setup to work. We're trying to emulate the current setup w/ Remix to hopefully migrate some (or all) applications to Remix.
We also hit this issue when trying to adopt v3_lazyRouteDiscovery. A lot of our apps were created before basename support was added so are using expressjs and the remix routes config prop to add a custom basename to routes instead.
Looks like this means we'll need to adopt basename before migrating to v7. Quite a big change as the basename is passed manually everywhere to redirects and fetchers etc.
Same for us over here. Basename is not an option for us as we have mutliple possible basenames, which are handled via an optional param in the route. But as we are behind a proxy, having all the remix/react/vite resource path on root is not possible as well. Vite base allows us to set a basename for all the dev and asset urls, while keeping the optional first route segment fully functional for other routes with differend basenames. So if this is not considered a bug which is getting fixed, we are out of options =/
We were able to add support for this via a couple of .patch's to a few packages that fixed the functionality for us. We'll look into upstreaming these changes in the future, but for now the patch files should do the trick.
These patches can be applied using https://www.npmjs.com/package/patch-package
@remix-run+react+2.15.2.patch:
diff --git a/node_modules/@remix-run/react/dist/browser.d.ts b/node_modules/@remix-run/react/dist/browser.d.ts
index dc9f13e..65ae2e1 100644
--- a/node_modules/@remix-run/react/dist/browser.d.ts
+++ b/node_modules/@remix-run/react/dist/browser.d.ts
@@ -5,6 +5,7 @@ import type { RouteModules } from "./routeModules";
declare global {
var __remixContext: {
basename?: string;
+ publicPath?: string;
state: HydrationState;
criticalCss?: string;
future: FutureConfig;
diff --git a/node_modules/@remix-run/react/dist/browser.js b/node_modules/@remix-run/react/dist/browser.js
index 230c30d..02bc62d 100644
--- a/node_modules/@remix-run/react/dist/browser.js
+++ b/node_modules/@remix-run/react/dist/browser.js
@@ -156,7 +156,7 @@ function RemixBrowser(_props) {
hydrationData,
mapRouteProperties: reactRouter.UNSAFE_mapRouteProperties,
dataStrategy: window.__remixContext.future.v3_singleFetch ? singleFetch.getSingleFetchDataStrategy(window.__remixManifest, window.__remixRouteModules, () => router) : undefined,
- patchRoutesOnNavigation: fogOfWar.getPatchRoutesOnNavigationFunction(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.basename)
+ patchRoutesOnNavigation: fogOfWar.getPatchRoutesOnNavigationFunction(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.publicPath)
});
// We can call initialize() immediately if the router doesn't have any
@@ -205,7 +205,7 @@ function RemixBrowser(_props) {
}
});
}, [location]);
- fogOfWar.useFogOFWarDiscovery(router, window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode);
+ fogOfWar.useFogOFWarDiscovery(router, window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.publicPath);
// We need to include a wrapper RemixErrorBoundary here in case the root error
// boundary also throws and we need to bubble up outside of the router entirely.
diff --git a/node_modules/@remix-run/react/dist/esm/browser.js b/node_modules/@remix-run/react/dist/esm/browser.js
index 8c7d3eb..c7636cb 100644
--- a/node_modules/@remix-run/react/dist/esm/browser.js
+++ b/node_modules/@remix-run/react/dist/esm/browser.js
@@ -196,7 +196,7 @@ function RemixBrowser(_props) {
hydrationData,
mapRouteProperties: UNSAFE_mapRouteProperties,
dataStrategy: window.__remixContext.future.v3_singleFetch ? getSingleFetchDataStrategy(window.__remixManifest, window.__remixRouteModules, () => router) : undefined,
- patchRoutesOnNavigation: getPatchRoutesOnNavigationFunction(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.basename)
+ patchRoutesOnNavigation: getPatchRoutesOnNavigationFunction(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.publicPath)
});
// We can call initialize() immediately if the router doesn't have any
@@ -245,7 +245,7 @@ function RemixBrowser(_props) {
}
});
}, [location]);
- useFogOFWarDiscovery(router, window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode);
+ useFogOFWarDiscovery(router, window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.publicPath);
// We need to include a wrapper RemixErrorBoundary here in case the root error
// boundary also throws and we need to bubble up outside of the router entirely.
diff --git a/node_modules/@remix-run/react/dist/esm/fog-of-war.js b/node_modules/@remix-run/react/dist/esm/fog-of-war.js
index c152087..71c8bd8 100644
--- a/node_modules/@remix-run/react/dist/esm/fog-of-war.js
+++ b/node_modules/@remix-run/react/dist/esm/fog-of-war.js
@@ -55,7 +55,7 @@ function getPartialManifest(manifest, router) {
routes: initialRoutes
};
}
-function getPatchRoutesOnNavigationFunction(manifest, routeModules, future, isSpaMode, basename) {
+function getPatchRoutesOnNavigationFunction(manifest, routeModules, future, isSpaMode, publicPath) {
if (!isFogOfWarEnabled(future, isSpaMode)) {
return undefined;
}
@@ -66,10 +66,10 @@ function getPatchRoutesOnNavigationFunction(manifest, routeModules, future, isSp
if (discoveredPaths.has(path)) {
return;
}
- await fetchAndApplyManifestPatches([path], manifest, routeModules, future, isSpaMode, basename, patch);
+ await fetchAndApplyManifestPatches([path], manifest, routeModules, future, isSpaMode, publicPath, patch);
};
}
-function useFogOFWarDiscovery(router, manifest, routeModules, future, isSpaMode) {
+function useFogOFWarDiscovery(router, manifest, routeModules, future, isSpaMode, publicPath) {
React.useEffect(() => {
var _navigator$connection;
// Don't prefetch if not enabled or if the user has `saveData` enabled
@@ -102,7 +102,7 @@ function useFogOFWarDiscovery(router, manifest, routeModules, future, isSpaMode)
return;
}
try {
- await fetchAndApplyManifestPatches(lazyPaths, manifest, routeModules, future, isSpaMode, router.basename, router.patchRoutes);
+ await fetchAndApplyManifestPatches(lazyPaths, manifest, routeModules, future, isSpaMode, publicPath, router.patchRoutes);
} catch (e) {
console.error("Failed to fetch manifest patches", e);
}
@@ -144,8 +144,8 @@ function useFogOFWarDiscovery(router, manifest, routeModules, future, isSpaMode)
return () => observer.disconnect();
}, [future, isSpaMode, manifest, routeModules, router]);
}
-async function fetchAndApplyManifestPatches(paths, manifest, routeModules, future, isSpaMode, basename, patchRoutes) {
- let manifestPath = `${basename ?? "/"}/__manifest`.replace(/\/+/g, "/");
+async function fetchAndApplyManifestPatches(paths, manifest, routeModules, future, isSpaMode, publicPath, patchRoutes) {
+ let manifestPath = `${publicPath ?? "/"}/__manifest`.replace(/\/+/g, "/");
let url = new URL(manifestPath, window.location.origin);
paths.sort().forEach(path => url.searchParams.append("p", path));
url.searchParams.set("version", manifest.version);
diff --git a/node_modules/@remix-run/react/dist/fog-of-war.d.ts b/node_modules/@remix-run/react/dist/fog-of-war.d.ts
index cd8c2e1..08b4cc7 100644
--- a/node_modules/@remix-run/react/dist/fog-of-war.d.ts
+++ b/node_modules/@remix-run/react/dist/fog-of-war.d.ts
@@ -23,6 +23,6 @@ export declare function getPartialManifest(manifest: AssetsManifest, router: Rou
runtime: string;
} | undefined;
};
-export declare function getPatchRoutesOnNavigationFunction(manifest: AssetsManifest, routeModules: RouteModules, future: FutureConfig, isSpaMode: boolean, basename: string | undefined): PatchRoutesOnNavigationFunction | undefined;
-export declare function useFogOFWarDiscovery(router: Router, manifest: AssetsManifest, routeModules: RouteModules, future: FutureConfig, isSpaMode: boolean): void;
-export declare function fetchAndApplyManifestPatches(paths: string[], manifest: AssetsManifest, routeModules: RouteModules, future: FutureConfig, isSpaMode: boolean, basename: string | undefined, patchRoutes: Router["patchRoutes"]): Promise<void>;
+export declare function getPatchRoutesOnNavigationFunction(manifest: AssetsManifest, routeModules: RouteModules, future: FutureConfig, isSpaMode: boolean, publicPath: string | undefined): PatchRoutesOnNavigationFunction | undefined;
+export declare function useFogOFWarDiscovery(router: Router, manifest: AssetsManifest, routeModules: RouteModules, future: FutureConfig, isSpaMode: boolean, publicPath: string | undefined): void;
+export declare function fetchAndApplyManifestPatches(paths: string[], manifest: AssetsManifest, routeModules: RouteModules, future: FutureConfig, isSpaMode: boolean, publicPath: string | undefined, patchRoutes: Router["patchRoutes"]): Promise<void>;
diff --git a/node_modules/@remix-run/react/dist/fog-of-war.js b/node_modules/@remix-run/react/dist/fog-of-war.js
index 2f7bed2..0fca668 100644
--- a/node_modules/@remix-run/react/dist/fog-of-war.js
+++ b/node_modules/@remix-run/react/dist/fog-of-war.js
@@ -79,7 +79,7 @@ function getPartialManifest(manifest, router$1) {
routes: initialRoutes
};
}
-function getPatchRoutesOnNavigationFunction(manifest, routeModules, future, isSpaMode, basename) {
+function getPatchRoutesOnNavigationFunction(manifest, routeModules, future, isSpaMode, publicPath) {
if (!isFogOfWarEnabled(future, isSpaMode)) {
return undefined;
}
@@ -90,10 +90,10 @@ function getPatchRoutesOnNavigationFunction(manifest, routeModules, future, isSp
if (discoveredPaths.has(path)) {
return;
}
- await fetchAndApplyManifestPatches([path], manifest, routeModules, future, isSpaMode, basename, patch);
+ await fetchAndApplyManifestPatches([path], manifest, routeModules, future, isSpaMode, publicPath, patch);
};
}
-function useFogOFWarDiscovery(router, manifest, routeModules, future, isSpaMode) {
+function useFogOFWarDiscovery(router, manifest, routeModules, future, isSpaMode, publicPath) {
React__namespace.useEffect(() => {
var _navigator$connection;
// Don't prefetch if not enabled or if the user has `saveData` enabled
@@ -126,7 +126,7 @@ function useFogOFWarDiscovery(router, manifest, routeModules, future, isSpaMode)
return;
}
try {
- await fetchAndApplyManifestPatches(lazyPaths, manifest, routeModules, future, isSpaMode, router.basename, router.patchRoutes);
+ await fetchAndApplyManifestPatches(lazyPaths, manifest, routeModules, future, isSpaMode, publicPath, router.patchRoutes);
} catch (e) {
console.error("Failed to fetch manifest patches", e);
}
@@ -168,8 +168,8 @@ function useFogOFWarDiscovery(router, manifest, routeModules, future, isSpaMode)
return () => observer.disconnect();
}, [future, isSpaMode, manifest, routeModules, router]);
}
-async function fetchAndApplyManifestPatches(paths, manifest, routeModules, future, isSpaMode, basename, patchRoutes) {
- let manifestPath = `${basename ?? "/"}/__manifest`.replace(/\/+/g, "/");
+async function fetchAndApplyManifestPatches(paths, manifest, routeModules, future, isSpaMode, publicPath, patchRoutes) {
+ let manifestPath = `${publicPath ?? "/"}/__manifest`.replace(/\/+/g, "/");
let url = new URL(manifestPath, window.location.origin);
paths.sort().forEach(path => url.searchParams.append("p", path));
url.searchParams.set("version", manifest.version);
@remix-run+server-runtime+2.15.2.patch:
diff --git a/node_modules/@remix-run/server-runtime/dist/server.js b/node_modules/@remix-run/server-runtime/dist/server.js
index 7e639d9..055ba68 100644
--- a/node_modules/@remix-run/server-runtime/dist/server.js
+++ b/node_modules/@remix-run/server-runtime/dist/server.js
@@ -93,7 +93,7 @@ const createRequestHandler = (build, mode$1) => {
// Manifest request for fog of war
- let manifestUrl = `${_build.basename ?? "/"}/__manifest`.replace(/\/+/g, "/");
+ let manifestUrl = `${_build.publicPath ?? "/"}/__manifest`.replace(/\/+/g, "/");
if (url.pathname === manifestUrl) {
try {
let res = await handleManifestRequest(_build, routes, url);
@@ -320,6 +320,7 @@ async function handleDocumentRequest(serverMode, build, staticHandler, request,
criticalCss,
serverHandoffString: serverHandoff.createServerHandoffString({
basename: build.basename,
+ publicPath: build.publicPath,
criticalCss,
future: build.future,
isSpaMode: build.isSpaMode,
@@ -375,6 +376,7 @@ async function handleDocumentRequest(serverMode, build, staticHandler, request,
staticHandlerContext: context,
serverHandoffString: serverHandoff.createServerHandoffString({
basename: build.basename,
+ publicPath: build.publicPath,
future: build.future,
isSpaMode: build.isSpaMode,
...(!build.future.v3_singleFetch ? {
diff --git a/node_modules/@remix-run/server-runtime/dist/serverHandoff.d.ts b/node_modules/@remix-run/server-runtime/dist/serverHandoff.d.ts
index 3c0be61..778a81e 100644
--- a/node_modules/@remix-run/server-runtime/dist/serverHandoff.d.ts
+++ b/node_modules/@remix-run/server-runtime/dist/serverHandoff.d.ts
@@ -5,6 +5,7 @@ export declare function createServerHandoffString<T>(serverHandoff: {
state?: ValidateShape<T, HydrationState>;
criticalCss?: string;
basename: string | undefined;
+ publicPath: string | undefined;
future: FutureConfig;
isSpaMode: boolean;
}): string;
@brophdawg11 Could you do a cursory pass and ensure we're not doing anything extremely heinous? If we aren't, I'll whip up a PR w/ these changes.
Perhaps in addition to these changes, we could write a helper that tries to use both publicPath and basename with some dedupe logic if they're the same values
I encountered the same issue.
To redirect from an action to another app, I used redirectDocument.
This is most useful when you have a Remix app living next to a non-Remix app on the same domain and need to redirect from the Remix app to the non-Remix app: https://remix.run/docs/en/main/utils/redirectDocument
However, if I set a basename, the redirect path ends up including that basename.
In my case, I only want to use vite.base without setting a basename.
Will this be fixed in react-router 7 or in remix? I posted on the other issue and have seen no response. It seems like this one should have been closed in favor of the other one. https://github.com/remix-run/react-router/issues/12749
Hey folks - we're adding the ability to set this path directly over in RRv7 so I think that will be the preferred solution here. If you find you still need base proxied through, we probably need the plumbing from https://github.com/remix-run/react-router/issues/12656 to do that so I'm going to close this out and that could be a new feature request over in RRv7.