Skip to content

Commit 1766db9

Browse files
committed
Allow configuration of the manifest path
1 parent 4db291c commit 1766db9

File tree

9 files changed

+139
-28
lines changed

9 files changed

+139
-28
lines changed

.changeset/angry-students-pay.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ Add new `routeDiscovery` `react-router.config.ts` option to disable Lazy Route D
77

88
- The default value is `routeDiscovery: "lazy"`
99
- Setting `routeDiscovery: "initial"` will disable Lazy Route Discovery and send up all routes in the manifest on initial document load
10+
- There is also an object version of the config which allows you to customize the manifest path when using `lazy`
11+
- `routeDiscovery: { mode: "lazy", manifestPath: "/custom-manifest" }`

integration/fog-of-war-test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,6 +1409,52 @@ test.describe("Fog of War", () => {
14091409
await page.waitForSelector("#a-index");
14101410
});
14111411

1412+
test("allows configuration of the manifest path", async ({ page }) => {
1413+
let fixture = await createFixture({
1414+
files: {
1415+
...getFiles(),
1416+
"react-router.config.ts": reactRouterConfig({
1417+
routeDiscovery: { manifestPath: "/custom-manifest" },
1418+
}),
1419+
},
1420+
});
1421+
let appFixture = await createAppFixture(fixture);
1422+
let app = new PlaywrightFixture(appFixture, page);
1423+
1424+
let wrongManifestRequests: string[] = [];
1425+
let manifestRequests: string[] = [];
1426+
page.on("request", (req) => {
1427+
if (req.url().includes("/__manifest")) {
1428+
wrongManifestRequests.push(req.url());
1429+
}
1430+
if (req.url().includes("/custom-manifest")) {
1431+
manifestRequests.push(req.url());
1432+
}
1433+
});
1434+
1435+
await app.goto("/", true);
1436+
expect(
1437+
await page.evaluate(() =>
1438+
Object.keys((window as any).__reactRouterManifest.routes)
1439+
)
1440+
).toEqual(["root", "routes/_index", "routes/a"]);
1441+
expect(manifestRequests).toEqual([
1442+
expect.stringMatching(/\/custom-manifest\?p=%2F&p=%2Fa&version=/),
1443+
]);
1444+
manifestRequests = [];
1445+
1446+
await app.clickLink("/a");
1447+
await page.waitForSelector("#a");
1448+
expect(await app.getHtml("#a")).toBe(`<h1 id="a">A: A LOADER</h1>`);
1449+
// Wait for eager discovery to kick off
1450+
await new Promise((r) => setTimeout(r, 500));
1451+
expect(manifestRequests).toEqual([
1452+
expect.stringMatching(/\/custom-manifest\?p=%2Fa%2Fb&version=/),
1453+
]);
1454+
1455+
expect(wrongManifestRequests).toEqual([]);
1456+
});
1457+
14121458
test.describe("routeDiscovery=initial", () => {
14131459
test("loads full manifest on initial load", async ({ page }) => {
14141460
let fixture = await createFixture({

integration/helpers/vite.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,14 @@ export const reactRouterConfig = ({
4242
>["unstable_splitRouteModules"];
4343
viteEnvironmentApi?: boolean;
4444
middleware?: boolean;
45-
routeDiscovery?: "initial" | "lazy";
45+
routeDiscovery?:
46+
| "initial"
47+
| "lazy"
48+
| {
49+
mode?: "lazy";
50+
manifestPath: string;
51+
}
52+
| { mode: "initial" };
4653
}) => {
4754
let config: Config = {
4855
ssr,

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

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,28 @@ export type ReactRouterConfig = {
159159
*/
160160
presets?: Array<Preset>;
161161
/**
162-
* Control the "Lazy Route Discovery" behavior. By default, this resolves to
163-
* `lazy` which will lazily discover routes as the user navigates around your
164-
* application. You can set this to `initial` to opt-out of this behavior and
165-
* load all routes with the initial HTML document load.
162+
* Control the "Lazy Route Discovery" behavior.
163+
*
164+
* - `routeDiscovery.mode`: By default, this resolves to `lazy` which will
165+
* lazily discover routes as the user navigates around your application.
166+
* You can set this to `initial` to opt-out of this behavior and load all
167+
* routes with the initial HTML document load.
168+
* - `routeDiscovery.manifestPath`: The path to serve the manifest file from.
169+
* Only applies to `mode: "lazy"` and defaults to `/__manifest`.
170+
*
171+
* If you do not need to control the manifest, you can specify the mode
172+
* directly on the `routeDiscovery` config.
166173
*/
167-
routeDiscovery?: "lazy" | "initial";
174+
routeDiscovery?:
175+
| "lazy"
176+
| "initial"
177+
| {
178+
mode?: "lazy"; // Can only adjust the manifest path in `lazy` mode
179+
manifestPath: string;
180+
}
181+
| {
182+
mode: "initial";
183+
};
168184
/**
169185
* The file name of the server build output. This file
170186
* should end in a `.js` extension and should be deployed to your server.
@@ -218,7 +234,10 @@ export type ResolvedReactRouterConfig = Readonly<{
218234
* application. You can set this to `initial` to opt-out of this behavior and
219235
* load all routes with the initial HTML document load.
220236
*/
221-
routeDiscovery: ReactRouterConfig["routeDiscovery"];
237+
routeDiscovery: {
238+
mode: "lazy" | "initial";
239+
manifestPath: string;
240+
};
222241
/**
223242
* An object of all available routes, keyed by route id.
224243
*/
@@ -397,7 +416,6 @@ async function resolveConfig({
397416
let defaults = {
398417
basename: "/",
399418
buildDirectory: "build",
400-
routeDiscovery: "lazy",
401419
serverBuildFile: "index.js",
402420
serverModuleFormat: "esm",
403421
ssr: true,
@@ -414,7 +432,7 @@ async function resolveConfig({
414432
buildDirectory: userBuildDirectory,
415433
buildEnd,
416434
prerender,
417-
routeDiscovery,
435+
routeDiscovery: userRouteDiscovery,
418436
serverBuildFile,
419437
serverBundles,
420438
serverModuleFormat,
@@ -441,17 +459,43 @@ async function resolveConfig({
441459
);
442460
}
443461

444-
if (routeDiscovery === "lazy" && !ssr) {
445-
if (userAndPresetConfigs.routeDiscovery === "lazy") {
446-
// If the user set "lazy" and `ssr:false`, then it's an invalid config
447-
// and we want to fail the build
462+
let routeDiscovery: ResolvedReactRouterConfig["routeDiscovery"] = {
463+
mode: "lazy",
464+
manifestPath: "/__manifest",
465+
};
466+
467+
if (userRouteDiscovery == null) {
468+
if (!ssr) {
469+
// No FOW when SSR is disabled
470+
routeDiscovery.mode = "initial";
471+
} else {
472+
// no-op - use defaults
473+
}
474+
} else if (
475+
userRouteDiscovery === "initial" ||
476+
(typeof userRouteDiscovery === "object" &&
477+
userRouteDiscovery.mode === "initial")
478+
) {
479+
routeDiscovery.mode = "initial";
480+
} else {
481+
if (!ssr) {
448482
return err(
449-
'The `routeDiscovery` config cannot be set to "lazy" when setting `ssr:false`'
483+
'The `routeDiscovery` config must be left unset or set to "initial" ' +
484+
"when setting `ssr:false`"
450485
);
451-
} else {
452-
// But if the user didn't specify, then we want to default to "initial"
453-
// when SSR is disabled
454-
routeDiscovery = "initial";
486+
}
487+
if (typeof userRouteDiscovery === "object") {
488+
if (
489+
userRouteDiscovery.manifestPath != null &&
490+
!userRouteDiscovery.manifestPath.startsWith("/")
491+
) {
492+
return err(
493+
"The `routeDiscovery.manifestPath` config must be a root-relative " +
494+
'pathname beginning with a slash (i.e., "/__manifest")'
495+
);
496+
}
497+
routeDiscovery.manifestPath =
498+
userRouteDiscovery.manifestPath ?? "/__manifest";
455499
}
456500
}
457501

packages/react-router/lib/dom/ssr/components.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ export function Scripts(props: ScriptsProps) {
647647
useFrameworkContext();
648648
let { router, static: isStatic, staticContext } = useDataRouterContext();
649649
let { matches: routerMatches } = useDataRouterStateContext();
650-
let enableFogOfWar = routeDiscovery === "lazy";
650+
let enableFogOfWar = routeDiscovery.mode === "lazy";
651651

652652
// Let <ServerRouter> know that we hydrated and we should render the single
653653
// fetch streaming scripts

packages/react-router/lib/dom/ssr/fog-of-war.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function getPatchRoutesOnNavigationFunction(
7373
isSpaMode: boolean,
7474
basename: string | undefined
7575
): PatchRoutesOnNavigationFunction | undefined {
76-
if (routeDiscovery !== "lazy") {
76+
if (routeDiscovery.mode !== "lazy") {
7777
return undefined;
7878
}
7979

@@ -89,6 +89,7 @@ export function getPatchRoutesOnNavigationFunction(
8989
ssr,
9090
isSpaMode,
9191
basename,
92+
routeDiscovery.manifestPath,
9293
patch,
9394
signal
9495
);
@@ -105,7 +106,10 @@ export function useFogOFWarDiscovery(
105106
) {
106107
React.useEffect(() => {
107108
// Don't prefetch if not enabled or if the user has `saveData` enabled
108-
if (routeDiscovery !== "lazy" || navigator.connection?.saveData === true) {
109+
if (
110+
routeDiscovery.mode !== "lazy" ||
111+
navigator.connection?.saveData === true
112+
) {
109113
return;
110114
}
111115

@@ -156,6 +160,7 @@ export function useFogOFWarDiscovery(
156160
ssr,
157161
isSpaMode,
158162
router.basename,
163+
routeDiscovery.manifestPath,
159164
router.patchRoutes
160165
);
161166
} catch (e) {
@@ -193,13 +198,13 @@ export async function fetchAndApplyManifestPatches(
193198
ssr: boolean,
194199
isSpaMode: boolean,
195200
basename: string | undefined,
201+
_manifestPath: string,
196202
patchRoutes: DataRouter["patchRoutes"],
197203
signal?: AbortSignal
198204
): Promise<void> {
199-
let manifestPath = `${basename != null ? basename : "/"}/__manifest`.replace(
200-
/\/+/g,
201-
"/"
202-
);
205+
let manifestPath = `${
206+
basename != null ? basename : "/"
207+
}${_manifestPath}`.replace(/\/+/g, "/");
203208
let url = new URL(manifestPath, window.location.origin);
204209
paths.sort().forEach((path) => url.searchParams.append("p", path));
205210
url.searchParams.set("version", manifest.version);

packages/react-router/lib/dom/ssr/routes-test-stub.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export function createRoutesStub(
113113
routeModules: {},
114114
ssr: false,
115115
isSpaMode: false,
116-
routeDiscovery: "lazy",
116+
routeDiscovery: { mode: "lazy", manifestPath: "/__manifest" },
117117
};
118118

119119
// Update the routes to include context in the loader/action and populate

packages/react-router/lib/server-runtime/build.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ export interface ServerBuild {
3737
*/
3838
isSpaMode: boolean;
3939
prerender: string[];
40-
routeDiscovery: "lazy" | "initial";
40+
routeDiscovery: {
41+
mode: "lazy" | "initial";
42+
manifestPath: string;
43+
};
4144
}
4245

4346
export interface HandleDocumentRequestFunction {

packages/react-router/lib/server-runtime/server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,11 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
199199
}
200200

201201
// Manifest request for fog of war
202-
let manifestUrl = `${normalizedBasename}/__manifest`.replace(/\/+/g, "/");
202+
let manifestUrl =
203+
`${normalizedBasename}${_build.routeDiscovery.manifestPath}`.replace(
204+
/\/+/g,
205+
"/"
206+
);
203207
if (url.pathname === manifestUrl) {
204208
try {
205209
let res = await handleManifestRequest(_build, routes, url);

0 commit comments

Comments
 (0)