diff --git a/integration/client-data-test.ts b/integration/client-data-test.ts
index 611ca5bf41..e0006135ae 100644
--- a/integration/client-data-test.ts
+++ b/integration/client-data-test.ts
@@ -335,7 +335,6 @@ test.describe("Client Data", () => {
}),
"app/routes/parent.child.tsx": js`
import * as React from 'react';
- import { json } from "react-router"
import { Await, useLoaderData } from "react-router"
export function loader() {
return {
@@ -685,7 +684,6 @@ test.describe("Client Data", () => {
}),
"app/routes/parent.child.tsx": js`
import * as React from 'react';
- import { json } from "react-router";
import { useLoaderData, useRevalidator } from "react-router";
let isFirstCall = true;
export async function loader({ serverLoader }) {
@@ -763,7 +761,7 @@ test.describe("Client Data", () => {
childClientLoaderHydrate: false,
}),
"app/routes/parent.child.tsx": js`
- import { ClientLoaderFunctionArgs, useRouteError } from "react-router";
+ import { useRouteError } from "react-router";
export function loader() {
throw new Error("Broken!")
@@ -1286,7 +1284,6 @@ test.describe("Client Data", () => {
}),
"app/routes/parent.child.tsx": js`
import * as React from 'react';
- import { json } from "react-router";
import { Form, useRouteError } from "react-router";
export async function clientAction({ serverAction }) {
return await serverAction();
@@ -1508,7 +1505,6 @@ test.describe("Client Data", () => {
}),
"app/routes/parent.child.tsx": js`
import * as React from 'react';
- import { json } from "react-router";
import { Form, useRouteError } from "react-router";
export async function clientAction({ serverAction }) {
return await serverAction();
diff --git a/integration/error-boundary-test.ts b/integration/error-boundary-test.ts
index 0ec725d5fa..42f2312ddb 100644
--- a/integration/error-boundary-test.ts
+++ b/integration/error-boundary-test.ts
@@ -971,7 +971,6 @@ test("Allows back-button out of an error boundary after a hard reload", async ({
`,
"app/routes/boom.tsx": js`
- import { json } from "react-router";
export function loader() { return boom(); }
export default function() { return
my page; }
`,
diff --git a/integration/fetch-globals-test.ts b/integration/fetch-globals-test.ts
index f7a5c3fe42..2d07e9ada6 100644
--- a/integration/fetch-globals-test.ts
+++ b/integration/fetch-globals-test.ts
@@ -14,7 +14,6 @@ test.beforeAll(async () => {
fixture = await createFixture({
files: {
"app/routes/_index.tsx": js`
- import { json } from "react-router";
import { useLoaderData } from "react-router";
export async function loader() {
const resp = await fetch('http://github.com/service/https://reqres.in/api/users?page=2');
diff --git a/integration/fetcher-test.ts b/integration/fetcher-test.ts
index 138300cb25..36472a25cf 100644
--- a/integration/fetcher-test.ts
+++ b/integration/fetcher-test.ts
@@ -23,7 +23,6 @@ test.describe("useFetcher", () => {
fixture = await createFixture({
files: {
"app/routes/resource-route-action-only.ts": js`
- import { json } from "react-router";
export function action() {
return new Response("${CHEESESTEAK}");
}
diff --git a/integration/fs-routes-test.ts b/integration/fs-routes-test.ts
index 5708622e2a..03d5a723c3 100644
--- a/integration/fs-routes-test.ts
+++ b/integration/fs-routes-test.ts
@@ -19,14 +19,6 @@ test.describe("fs-routes", () => {
test.beforeAll(async () => {
fixture = await createFixture({
files: {
- "vite.config.js": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- plugins: [reactRouter()],
- });
- `,
"app/routes.ts": js`
import { type RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
diff --git a/integration/helpers/create-fixture.ts b/integration/helpers/create-fixture.ts
index 672faef6dd..bd482ba731 100644
--- a/integration/helpers/create-fixture.ts
+++ b/integration/helpers/create-fixture.ts
@@ -18,7 +18,7 @@ import {
import { createRequestHandler as createExpressHandler } from "@react-router/express";
import { createReadableStreamFromReadable } from "@react-router/node";
-import { viteConfig, reactRouterConfig } from "./vite.js";
+import { type TemplateName, viteConfig, reactRouterConfig } from "./vite.js";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
const root = path.join(__dirname, "../..");
@@ -31,6 +31,7 @@ export interface FixtureInit {
spaMode?: boolean;
prerender?: boolean;
port?: number;
+ templateName?: TemplateName;
}
export type Fixture = Awaited
>;
@@ -362,7 +363,7 @@ export async function createFixtureProject(
init: FixtureInit = {},
mode?: ServerMode
): Promise {
- let template = "vite-5-template";
+ let template = init.templateName ?? "vite-5-template";
let integrationTemplateDir = path.resolve(__dirname, template);
let projectName = `rr-${template}-${Math.random().toString(32).slice(2)}`;
let projectDir = path.join(TMP_DIR, projectName);
@@ -424,6 +425,9 @@ function build(projectDir: string, buildStdio?: Writable, mode?: ServerMode) {
env: {
...process.env,
NODE_ENV: mode || ServerMode.Production,
+ // Ensure build can pass in Rolldown. This can be removed once
+ // "preserveEntrySignatures" is supported in rolldown-vite.
+ ROLLDOWN_OPTIONS_VALIDATION: "loose",
},
});
diff --git a/integration/helpers/vite-rolldown-template/.gitignore b/integration/helpers/vite-rolldown-template/.gitignore
new file mode 100644
index 0000000000..c08251ce0e
--- /dev/null
+++ b/integration/helpers/vite-rolldown-template/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+
+/.cache
+/build
+.env
+.react-router
diff --git a/integration/helpers/vite-rolldown-template/app/root.tsx b/integration/helpers/vite-rolldown-template/app/root.tsx
new file mode 100644
index 0000000000..b36392b4dd
--- /dev/null
+++ b/integration/helpers/vite-rolldown-template/app/root.tsx
@@ -0,0 +1,19 @@
+import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
+
+export default function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/integration/helpers/vite-rolldown-template/app/routes.ts b/integration/helpers/vite-rolldown-template/app/routes.ts
new file mode 100644
index 0000000000..4c05936cb6
--- /dev/null
+++ b/integration/helpers/vite-rolldown-template/app/routes.ts
@@ -0,0 +1,4 @@
+import { type RouteConfig } from "@react-router/dev/routes";
+import { flatRoutes } from "@react-router/fs-routes";
+
+export default flatRoutes() satisfies RouteConfig;
diff --git a/integration/helpers/vite-rolldown-template/app/routes/_index.tsx b/integration/helpers/vite-rolldown-template/app/routes/_index.tsx
new file mode 100644
index 0000000000..ecfc25c614
--- /dev/null
+++ b/integration/helpers/vite-rolldown-template/app/routes/_index.tsx
@@ -0,0 +1,16 @@
+import type { MetaFunction } from "react-router";
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: "New React Router App" },
+ { name: "description", content: "Welcome to React Router!" },
+ ];
+};
+
+export default function Index() {
+ return (
+
+
Welcome to React Router
+
+ );
+}
diff --git a/integration/helpers/vite-rolldown-template/env.d.ts b/integration/helpers/vite-rolldown-template/env.d.ts
new file mode 100644
index 0000000000..5e7dfe5dd9
--- /dev/null
+++ b/integration/helpers/vite-rolldown-template/env.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/integration/helpers/vite-rolldown-template/package.json b/integration/helpers/vite-rolldown-template/package.json
new file mode 100644
index 0000000000..01dd8b78a8
--- /dev/null
+++ b/integration/helpers/vite-rolldown-template/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "integration-vite-rolldown-template",
+ "version": "0.0.0",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "react-router dev",
+ "build": "cross-env ROLLDOWN_OPTIONS_VALIDATION=loose react-router build",
+ "start": "react-router-serve ./build/server/index.js",
+ "typecheck": "react-router typegen && tsc"
+ },
+ "dependencies": {
+ "@react-router/express": "workspace:*",
+ "@react-router/node": "workspace:*",
+ "@react-router/serve": "workspace:*",
+ "@vanilla-extract/css": "^1.10.0",
+ "@vanilla-extract/vite-plugin": "^3.9.2",
+ "express": "^4.19.2",
+ "isbot": "^5.1.11",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router": "workspace:*",
+ "serialize-javascript": "^6.0.1"
+ },
+ "devDependencies": {
+ "@react-router/dev": "workspace:*",
+ "@react-router/fs-routes": "workspace:*",
+ "@react-router/remix-routes-option-adapter": "workspace:*",
+ "@types/react": "^18.2.20",
+ "@types/react-dom": "^18.2.7",
+ "cross-env": "^7.0.3",
+ "eslint": "^8.38.0",
+ "typescript": "^5.1.6",
+ "vite": "npm:rolldown-vite@6.3.0-beta.5",
+ "vite-env-only": "^3.0.1",
+ "vite-tsconfig-paths": "^4.2.1"
+ },
+ "overrides": {
+ "vite": "npm:rolldown-vite@6.3.0-beta.5"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+}
diff --git a/integration/helpers/vite-rolldown-template/public/favicon.ico b/integration/helpers/vite-rolldown-template/public/favicon.ico
new file mode 100644
index 0000000000..5dbdfcddcb
Binary files /dev/null and b/integration/helpers/vite-rolldown-template/public/favicon.ico differ
diff --git a/integration/helpers/vite-rolldown-template/tsconfig.json b/integration/helpers/vite-rolldown-template/tsconfig.json
new file mode 100644
index 0000000000..62bbb55722
--- /dev/null
+++ b/integration/helpers/vite-rolldown-template/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "include": ["env.d.ts", "**/*.ts", "**/*.tsx", ".react-router/types/**/*"],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "verbatimModuleSyntax": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "target": "ES2022",
+ "strict": true,
+ "allowJs": true,
+ "skipLibCheck": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./app/*"]
+ },
+ "noEmit": true,
+ "rootDirs": [".", ".react-router/types/"]
+ }
+}
diff --git a/integration/helpers/vite-rolldown-template/vite.config.ts b/integration/helpers/vite-rolldown-template/vite.config.ts
new file mode 100644
index 0000000000..fac933f23c
--- /dev/null
+++ b/integration/helpers/vite-rolldown-template/vite.config.ts
@@ -0,0 +1,11 @@
+import { reactRouter } from "@react-router/dev/vite";
+import { defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+export default defineConfig({
+ plugins: [
+ // @ts-expect-error `dev` depends on Vite 6, Plugin type is mismatched.
+ reactRouter(),
+ tsconfigPaths(),
+ ],
+});
diff --git a/integration/helpers/vite.ts b/integration/helpers/vite.ts
index be6e7bc0a8..1330c291e1 100644
--- a/integration/helpers/vite.ts
+++ b/integration/helpers/vite.ts
@@ -61,14 +61,29 @@ export const reactRouterConfig = ({
`;
};
-type ViteConfigArgs = {
+type ViteConfigServerArgs = {
port: number;
fsAllow?: string[];
+};
+
+type ViteConfigBuildArgs = {
+ assetsInlineLimit?: number;
+ assetsDir?: string;
+};
+
+type ViteConfigBaseArgs = {
envDir?: string;
};
+type ViteConfigArgs = (
+ | ViteConfigServerArgs
+ | { [K in keyof ViteConfigServerArgs]?: never }
+) &
+ ViteConfigBuildArgs &
+ ViteConfigBaseArgs;
+
export const viteConfig = {
- server: async (args: ViteConfigArgs) => {
+ server: async (args: ViteConfigServerArgs) => {
let { port, fsAllow } = args;
let hmrPort = await getPort();
let text = dedent`
@@ -81,21 +96,42 @@ export const viteConfig = {
`;
return text;
},
+ build: ({ assetsInlineLimit, assetsDir }: ViteConfigBuildArgs = {}) => {
+ return dedent`
+ build: {
+ // Detect rolldown-vite. This should ideally use "rolldownVersion"
+ // but that's not exported. Once that's available, this
+ // check should be updated to use it.
+ rollupOptions: "transformWithOxc" in (await import("vite"))
+ ? {
+ onwarn(warning, warn) {
+ // Ignore "The built-in minifier is still under development." warning
+ if (warning.code === "MINIFY_WARNING") return;
+ warn(warning);
+ },
+ }
+ : undefined,
+ assetsInlineLimit: ${assetsInlineLimit ?? "undefined"},
+ assetsDir: ${assetsDir ? `"${assetsDir}"` : "undefined"},
+ },
+ `;
+ },
basic: async (args: ViteConfigArgs) => {
return dedent`
import { reactRouter } from "@react-router/dev/vite";
import { envOnlyMacros } from "vite-env-only";
import tsconfigPaths from "vite-tsconfig-paths";
- export default {
- ${await viteConfig.server(args)}
+ export default async () => ({
+ ${args.port ? await viteConfig.server(args) : ""}
+ ${viteConfig.build(args)}
envDir: ${args.envDir ? `"${args.envDir}"` : "undefined"},
plugins: [
reactRouter(),
envOnlyMacros(),
tsconfigPaths()
],
- };
+ });
`;
},
};
@@ -145,14 +181,19 @@ export const EXPRESS_SERVER = (args: {
`;
export type TemplateName =
+ | "cloudflare-dev-proxy-template"
| "vite-5-template"
| "vite-6-template"
- | "cloudflare-dev-proxy-template"
- | "vite-plugin-cloudflare-template";
+ | "vite-plugin-cloudflare-template"
+ | "vite-rolldown-template";
export const viteMajorTemplates = [
{ templateName: "vite-5-template", templateDisplayName: "Vite 5" },
{ templateName: "vite-6-template", templateDisplayName: "Vite 6" },
+ {
+ templateName: "vite-rolldown-template",
+ templateDisplayName: "Vite Rolldown",
+ },
] as const satisfies Array<{
templateName: TemplateName;
templateDisplayName: string;
@@ -205,6 +246,9 @@ export const build = ({
...process.env,
...colorEnv,
...env,
+ // Ensure build can pass in Rolldown. This can be removed once
+ // "preserveEntrySignatures" is supported in rolldown-vite.
+ ROLLDOWN_OPTIONS_VALIDATION: "loose",
},
});
};
diff --git a/integration/link-test.ts b/integration/link-test.ts
index 1c7ffb3c23..4d2d331f43 100644
--- a/integration/link-test.ts
+++ b/integration/link-test.ts
@@ -357,8 +357,7 @@ test.describe("route module link export", () => {
`,
"app/routes/gists.$username.tsx": js`
- import { data, redirect } from "react-router";
- import { Link, useLoaderData, useParams } from "react-router";
+ import { data, redirect, Link, useLoaderData, useParams } from "react-router";
export async function loader({ params }) {
let { username } = params;
if (username === "mjijackson") {
diff --git a/integration/multiple-cookies-test.ts b/integration/multiple-cookies-test.ts
index 96eabed064..e83b5640fb 100644
--- a/integration/multiple-cookies-test.ts
+++ b/integration/multiple-cookies-test.ts
@@ -16,8 +16,7 @@ test.describe("pathless layout routes", () => {
await createFixture({
files: {
"app/routes/_index.tsx": js`
- import { data, redirect } from "react-router";
- import { Form, useActionData } from "react-router";
+ import { data, redirect, Form, useActionData } from "react-router";
export let loader = async () => {
let headers = new Headers();
diff --git a/integration/prefetch-test.ts b/integration/prefetch-test.ts
index 211030f1d7..bee1b75991 100644
--- a/integration/prefetch-test.ts
+++ b/integration/prefetch-test.ts
@@ -12,12 +12,17 @@ import type {
AppFixture,
} from "./helpers/create-fixture.js";
import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+import { type TemplateName, viteMajorTemplates } from "./helpers/vite.js";
type PrefetchType = "intent" | "render" | "none" | "viewport";
// Generate the test app using the given prefetch mode
-function fixtureFactory(mode: PrefetchType): FixtureInit {
+function fixtureFactory(
+ mode: PrefetchType,
+ templateName: TemplateName
+): FixtureInit {
return {
+ templateName,
files: {
"app/root.tsx": js`
import {
@@ -84,530 +89,561 @@ function fixtureFactory(mode: PrefetchType): FixtureInit {
};
}
-test.describe("prefetch=none", () => {
- let fixture: Fixture;
- let appFixture: AppFixture;
-
- test.beforeAll(async () => {
- fixture = await createFixture(fixtureFactory("none"));
- appFixture = await createAppFixture(fixture);
- });
-
- test.afterAll(() => {
- appFixture.close();
- });
-
- test("does not render prefetch tags during SSR", async ({ page }) => {
- let res = await fixture.requestDocument("/");
- expect(res.status).toBe(200);
- expect(await page.locator("#nav link").count()).toBe(0);
- });
-
- test("does not add prefetch tags on hydration", async ({ page }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- expect(await page.locator("#nav link").count()).toBe(0);
- });
-});
-
-test.describe("prefetch=render", () => {
- let fixture: Fixture;
- let appFixture: AppFixture;
-
- test.beforeAll(async () => {
- fixture = await createFixture(fixtureFactory("render"));
- appFixture = await createAppFixture(fixture);
- });
-
- test.afterAll(() => {
- appFixture.close();
- });
-
- test("does not render prefetch tags during SSR", async ({ page }) => {
- let res = await fixture.requestDocument("/");
- expect(res.status).toBe(200);
- expect(await page.locator("#nav link").count()).toBe(0);
- });
-
- test("adds prefetch tags on hydration", async ({ page }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
-
- // Both data and asset fetch for /with-loader
- await page.waitForSelector(
- "#nav link[rel='prefetch'][as='fetch'][href='http://github.com/with-loader.data']",
- { state: "attached" }
- );
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/with-loader-']",
- { state: "attached" }
- );
-
- // Only asset fetch for /without-loader
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/without-loader-']",
- { state: "attached" }
- );
-
- // These 2 are common and duped for both - but they've already loaded on
- // page load so they don't trigger network requests
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/with-props-']",
- { state: "attached" }
- );
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/chunk-']",
- { state: "attached" }
- );
-
- // Ensure no other links in the #nav element
- expect(await page.locator("#nav link").count()).toBe(7);
- });
-});
-
-test.describe("prefetch=intent (hover)", () => {
- let fixture: Fixture;
- let appFixture: AppFixture;
-
- test.beforeAll(async () => {
- fixture = await createFixture(fixtureFactory("intent"));
- appFixture = await createAppFixture(fixture);
- });
-
- test.afterAll(() => {
- appFixture.close();
- });
-
- test("does not render prefetch tags during SSR", async ({ page }) => {
- let res = await fixture.requestDocument("/");
- expect(res.status).toBe(200);
- expect(await page.locator("#nav link").count()).toBe(0);
- });
-
- test("does not add prefetch tags on hydration", async ({ page }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- expect(await page.locator("#nav link").count()).toBe(0);
- });
-
- test("adds prefetch tags on hover", async ({ page }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.hover("a[href='http://github.com/with-loader']");
- await page.waitForSelector(
- "#nav link[rel='prefetch'][as='fetch'][href='http://github.com/with-loader.data']",
- { state: "attached" }
- );
- // Check href prefix due to hashed filenames
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/with-loader-']",
- { state: "attached" }
- );
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/with-props-']",
- { state: "attached" }
- );
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/chunk-']",
- { state: "attached" }
- );
- expect(await page.locator("#nav link").count()).toBe(4);
-
- await page.hover("a[href='http://github.com/without-loader']");
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/without-loader-']",
- { state: "attached" }
- );
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/with-props-']",
- { state: "attached" }
- );
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/chunk-']",
- { state: "attached" }
- );
- expect(await page.locator("#nav link").count()).toBe(3);
- });
-
- test("removes prefetch tags after navigating to/from the page", async ({
- page,
- }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
-
- // Links added on hover
- await page.hover("a[href='http://github.com/with-loader']");
- await page.waitForSelector("#nav link", { state: "attached" });
- expect(await page.locator("#nav link").count()).toBe(4);
-
- // Links removed upon navigating to the page
- await page.click("a[href='http://github.com/with-loader']");
- await page.waitForSelector("h2.with-loader", { state: "attached" });
- expect(await page.locator("#nav link").count()).toBe(0);
-
- // Links stay removed upon navigating away from the page
- await page.click("a[href='http://github.com/without-loader']");
- await page.waitForSelector("h2.without-loader", { state: "attached" });
- expect(await page.locator("#nav link").count()).toBe(0);
- });
-});
-
-test.describe("prefetch=intent (focus)", () => {
- let fixture: Fixture;
- let appFixture: AppFixture;
-
- test.beforeAll(async () => {
- fixture = await createFixture(fixtureFactory("intent"));
- appFixture = await createAppFixture(fixture);
- });
-
- test.afterAll(() => {
- appFixture.close();
- });
+test.describe("prefetch", () => {
+ viteMajorTemplates.forEach(({ templateName, templateDisplayName }) => {
+ test.describe(templateDisplayName, () => {
+ test.describe("prefetch=none", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture(fixtureFactory("none", templateName));
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ test("does not render prefetch tags during SSR", async ({ page }) => {
+ let res = await fixture.requestDocument("/");
+ expect(res.status).toBe(200);
+ expect(await page.locator("#nav link").count()).toBe(0);
+ });
+
+ test("does not add prefetch tags on hydration", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ expect(await page.locator("#nav link").count()).toBe(0);
+ });
+ });
- test("does not render prefetch tags during SSR", async ({ page }) => {
- let res = await fixture.requestDocument("/");
- expect(res.status).toBe(200);
- expect(await page.locator("#nav link").count()).toBe(0);
- });
+ test.describe("prefetch=render", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture(fixtureFactory("render", templateName));
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ test("does not render prefetch tags during SSR", async ({ page }) => {
+ let res = await fixture.requestDocument("/");
+ expect(res.status).toBe(200);
+ expect(await page.locator("#nav link").count()).toBe(0);
+ });
+
+ test("adds prefetch tags on hydration", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+
+ // Both data and asset fetch for /with-loader
+ await page.waitForSelector(
+ "#nav link[rel='prefetch'][as='fetch'][href='http://github.com/with-loader.data']",
+ { state: "attached" }
+ );
+ await page.waitForSelector(
+ "#nav link[rel='modulepreload'][href^='/assets/with-loader-']",
+ { state: "attached" }
+ );
- test("does not add prefetch tags on hydration", async ({ page }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- expect(await page.locator("#nav link").count()).toBe(0);
- });
+ // Only asset fetch for /without-loader
+ await page.waitForSelector(
+ "#nav link[rel='modulepreload'][href^='/assets/without-loader-']",
+ { state: "attached" }
+ );
- test("adds prefetch tags on focus", async ({ page }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- // This click is needed to transfer focus to the main window, allowing
- // subsequent focus events to fire
- await page.click("body");
- await page.focus("a[href='http://github.com/with-loader']");
- await page.waitForSelector(
- "#nav link[rel='prefetch'][as='fetch'][href='http://github.com/with-loader.data']",
- { state: "attached" }
- );
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/with-loader-']",
- { state: "attached" }
- );
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/with-props-']",
- { state: "attached" }
- );
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/chunk-']",
- { state: "attached" }
- );
- expect(await page.locator("#nav link").count()).toBe(4);
-
- await page.focus("a[href='http://github.com/without-loader']");
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/without-loader-']",
- { state: "attached" }
- );
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/with-props-']",
- { state: "attached" }
- );
- await page.waitForSelector(
- "#nav link[rel='modulepreload'][href^='/assets/chunk-']",
- { state: "attached" }
- );
- expect(await page.locator("#nav link").count()).toBe(3);
- });
-});
+ // These 2 are common and duped for both - but they've already loaded on
+ // page load so they don't trigger network requests
+ await page.waitForSelector(
+ "#nav link[rel='modulepreload'][href^='/assets/with-props-']",
+ { state: "attached" }
+ );
+ await page.waitForSelector(
+ // Look for either Rollup or Rolldown chunks
+ [
+ "#nav link[rel='modulepreload'][href^='/assets/chunk-']",
+ "#nav link[rel='modulepreload'][href^='/assets/jsx-runtime-']",
+ ].join(","),
+ { state: "attached" }
+ );
-test.describe("prefetch=viewport", () => {
- let fixture: Fixture;
- let appFixture: AppFixture;
-
- test.beforeAll(async () => {
- fixture = await createFixture({
- files: {
- "app/routes/_index.tsx": js`
- import { Link } from "react-router";
-
- export default function Component() {
- return (
- <>
- Index Page - Scroll Down
-
- Click me!
-
- >
- );
- }
- `,
-
- "app/routes/test.tsx": js`
- export function loader() {
- return null;
- }
- export default function Component() {
- return Test Page
;
- }
- `,
- },
- });
+ // Ensure no other links in the #nav element
+ expect(await page.locator("#nav link").count()).toBe(7);
+ });
+ });
- // This creates an interactive app using puppeteer.
- appFixture = await createAppFixture(fixture);
- });
+ test.describe("prefetch=intent (hover)", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture(fixtureFactory("intent", templateName));
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ test("does not render prefetch tags during SSR", async ({ page }) => {
+ let res = await fixture.requestDocument("/");
+ expect(res.status).toBe(200);
+ expect(await page.locator("#nav link").count()).toBe(0);
+ });
+
+ test("does not add prefetch tags on hydration", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ expect(await page.locator("#nav link").count()).toBe(0);
+ });
+
+ test("adds prefetch tags on hover", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await page.hover("a[href='http://github.com/with-loader']");
+ await page.waitForSelector(
+ "#nav link[rel='prefetch'][as='fetch'][href='http://github.com/with-loader.data']",
+ { state: "attached" }
+ );
+ // Check href prefix due to hashed filenames
+ await page.waitForSelector(
+ "#nav link[rel='modulepreload'][href^='/assets/with-loader-']",
+ { state: "attached" }
+ );
+ await page.waitForSelector(
+ "#nav link[rel='modulepreload'][href^='/assets/with-props-']",
+ { state: "attached" }
+ );
+ await page.waitForSelector(
+ // Look for either Rollup or Rolldown chunks
+ [
+ "#nav link[rel='modulepreload'][href^='/assets/chunk-']",
+ "#nav link[rel='modulepreload'][href^='/assets/jsx-runtime-']",
+ ].join(","),
+ { state: "attached" }
+ );
+ expect(await page.locator("#nav link").count()).toBe(4);
- test.afterAll(() => {
- appFixture.close();
- });
+ await page.hover("a[href='http://github.com/without-loader']");
+ await page.waitForSelector(
+ "#nav link[rel='modulepreload'][href^='/assets/without-loader-']",
+ { state: "attached" }
+ );
+ await page.waitForSelector(
+ "#nav link[rel='modulepreload'][href^='/assets/with-props-']",
+ { state: "attached" }
+ );
+ await page.waitForSelector(
+ // Look for either Rollup or Rolldown chunks
+ [
+ "#nav link[rel='modulepreload'][href^='/assets/chunk-']",
+ "#nav link[rel='modulepreload'][href^='/assets/jsx-runtime-']",
+ ].join(","),
+ { state: "attached" }
+ );
+ expect(await page.locator("#nav link").count()).toBe(3);
+ });
+
+ test("removes prefetch tags after navigating to/from the page", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+
+ // Links added on hover
+ await page.hover("a[href='http://github.com/with-loader']");
+ await page.waitForSelector("#nav link", { state: "attached" });
+ expect(await page.locator("#nav link").count()).toBe(4);
+
+ // Links removed upon navigating to the page
+ await page.click("a[href='http://github.com/with-loader']");
+ await page.waitForSelector("h2.with-loader", { state: "attached" });
+ expect(await page.locator("#nav link").count()).toBe(0);
+
+ // Links stay removed upon navigating away from the page
+ await page.click("a[href='http://github.com/without-loader']");
+ await page.waitForSelector("h2.without-loader", {
+ state: "attached",
+ });
+ expect(await page.locator("#nav link").count()).toBe(0);
+ });
+ });
- test("should prefetch when the link enters the viewport", async ({
- page,
- }) => {
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
-
- // No preloads to start
- await expect(page.locator("div link")).toHaveCount(0);
-
- // Preloads render on scroll down
- await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
-
- await page.waitForSelector(
- "div link[rel='prefetch'][as='fetch'][href='http://github.com/test.data']",
- { state: "attached" }
- );
- await page.waitForSelector(
- "div link[rel='modulepreload'][href^='/assets/test-']",
- { state: "attached" }
- );
-
- // Preloads removed on scroll up
- await page.evaluate(() => window.scrollTo(0, 0));
- await expect(page.locator("div link")).toHaveCount(0);
- });
-});
+ test.describe("prefetch=intent (focus)", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture(fixtureFactory("intent", templateName));
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ test("does not render prefetch tags during SSR", async ({ page }) => {
+ let res = await fixture.requestDocument("/");
+ expect(res.status).toBe(200);
+ expect(await page.locator("#nav link").count()).toBe(0);
+ });
+
+ test("does not add prefetch tags on hydration", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ expect(await page.locator("#nav link").count()).toBe(0);
+ });
+
+ test("adds prefetch tags on focus", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ // This click is needed to transfer focus to the main window, allowing
+ // subsequent focus events to fire
+ await page.click("body");
+ await page.focus("a[href='http://github.com/with-loader']");
+ await page.waitForSelector(
+ "#nav link[rel='prefetch'][as='fetch'][href='http://github.com/with-loader.data']",
+ { state: "attached" }
+ );
+ await page.waitForSelector(
+ "#nav link[rel='modulepreload'][href^='/assets/with-loader-']",
+ { state: "attached" }
+ );
+ await page.waitForSelector(
+ "#nav link[rel='modulepreload'][href^='/assets/with-props-']",
+ { state: "attached" }
+ );
+ await page.waitForSelector(
+ // Look for either Rollup or Rolldown chunks
+ [
+ "#nav link[rel='modulepreload'][href^='/assets/chunk-']",
+ "#nav link[rel='modulepreload'][href^='/assets/jsx-runtime-']",
+ ].join(","),
+ { state: "attached" }
+ );
+ expect(await page.locator("#nav link").count()).toBe(4);
-test.describe("other scenarios", () => {
- let fixture: Fixture;
- let appFixture: AppFixture;
+ await page.focus("a[href='http://github.com/without-loader']");
+ await page.waitForSelector(
+ "#nav link[rel='modulepreload'][href^='/assets/without-loader-']",
+ { state: "attached" }
+ );
+ await page.waitForSelector(
+ "#nav link[rel='modulepreload'][href^='/assets/with-props-']",
+ { state: "attached" }
+ );
+ await page.waitForSelector(
+ // Look for either Rollup or Rolldown chunks
+ [
+ "#nav link[rel='modulepreload'][href^='/assets/chunk-']",
+ "#nav link[rel='modulepreload'][href^='/assets/jsx-runtime-']",
+ ].join(","),
+ { state: "attached" }
+ );
+ expect(await page.locator("#nav link").count()).toBe(3);
+ });
+ });
- test.afterAll(() => {
- appFixture?.close();
- });
+ test.describe("prefetch=viewport", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router";
+
+ export default function Component() {
+ return (
+ <>
+ Index Page - Scroll Down
+
+ Click me!
+
+ >
+ );
+ }
+ `,
+
+ "app/routes/test.tsx": js`
+ export function loader() {
+ return null;
+ }
+ export default function Component() {
+ return Test Page
;
+ }
+ `,
+ },
+ });
+
+ // This creates an interactive app using puppeteer.
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ test("should prefetch when the link enters the viewport", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+
+ // No preloads to start
+ await expect(page.locator("div link")).toHaveCount(0);
+
+ // Preloads render on scroll down
+ await page.evaluate(() =>
+ window.scrollTo(0, document.body.scrollHeight)
+ );
- test("does not add prefetch links for stylesheets already in the DOM (active routes)", async ({
- page,
- }) => {
- fixture = await createFixture({
- files: {
- "app/root.tsx": js`
- import { Links, Meta, Scripts, useFetcher } from "react-router";
- import globalCss from "./global.css?url";
-
- export function links() {
- return [{ rel: "stylesheet", href: globalCss }];
- }
-
- export async function action() {
- return null;
- }
-
- export async function loader() {
- return null;
- }
-
- export default function Root() {
- let fetcher = useFetcher();
-
- return (
-
-
-
-
-
-
-
- {fetcher.state}
-
-
-
- );
- }
- `,
-
- "app/global.css": `
- body {
- background-color: black;
- color: white;
- }
- `,
-
- "app/routes/_index.tsx": js`
- export default function() {
- return Index
;
- }
- `,
- },
- });
- appFixture = await createAppFixture(fixture);
- let requests: { type: string; url: string }[] = [];
+ await page.waitForSelector(
+ "div link[rel='prefetch'][as='fetch'][href='http://github.com/test.data']",
+ { state: "attached" }
+ );
+ await page.waitForSelector(
+ "div link[rel='modulepreload'][href^='/assets/test-']",
+ { state: "attached" }
+ );
- page.on("request", (req) => {
- requests.push({
- type: req.resourceType(),
- url: req.url(),
+ // Preloads removed on scroll up
+ await page.evaluate(() => window.scrollTo(0, 0));
+ await expect(page.locator("div link")).toHaveCount(0);
+ });
});
- });
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.click("#submit-fetcher");
- await page.waitForSelector("#fetcher-state--idle");
- // We should not send a second request for this root stylesheet that's
- // already been rendered in the DOM
- let stylesheets = requests.filter(
- (r) => r.type === "stylesheet" && /\/global-[a-z0-9]+\.css/i.test(r.url)
- );
- expect(stylesheets.length).toBe(1);
- });
-
- test("dedupes prefetch tags", async ({ page }) => {
- fixture = await createFixture({
- files: {
- "app/root.tsx": js`
- import {
- Link,
- Links,
- Meta,
- Outlet,
- Scripts,
- useLoaderData,
- } from "react-router";
-
- export default function Root() {
- const styles =
- 'a:hover { color: red; } a:hover:after { content: " (hovered)"; }' +
- 'a:focus { color: green; } a:focus:after { content: " (focused)"; }';
-
- return (
-
-
-
-
-
-
-
- Root
-
-
-
-
-
- );
- }
- `,
-
- "app/global.css": css`
- .global-class {
- background-color: gray;
- color: black;
- }
- `,
-
- "app/local.css": css`
- .local-class {
- background-color: black;
- color: white;
- }
- `,
-
- "app/routes/_index.tsx": js`
- export default function() {
- return Index
;
- }
- `,
-
- "app/routes/with-nested-links.tsx": js`
- import { Outlet } from "react-router";
- import globalCss from "../global.css?url";
-
- export function links() {
- return [
- // Same links as child route but with different key order
- {
- rel: "stylesheet",
- href: globalCss,
- },
- {
- rel: "preload",
- as: "image",
- imageSrcSet: "image-600.jpg 600w, image-1200.jpg 1200w",
- imageSizes: "9999px",
- },
- ];
- }
- export default function() {
- return ;
- }
- `,
-
- "app/routes/with-nested-links.nested.tsx": js`
- import globalCss from '../global.css?url';
- import localCss from '../local.css?url';
-
- export function links() {
- return [
- // Same links as parent route but with different key order
- {
- href: globalCss,
- rel: "stylesheet",
- },
- {
- imageSrcSet: "image-600.jpg 600w, image-1200.jpg 1200w",
- imageSizes: "9999px",
- rel: "preload",
- as: "image",
- },
- // Unique links for child route
- {
- rel: "stylesheet",
- href: localCss,
- },
- {
- rel: "preload",
- as: "image",
- imageSrcSet: "image-700.jpg 700w, image-1400.jpg 1400w",
- imageSizes: "9999px",
- },
- ];
- }
- export default function() {
- return With Nested Links
;
- }
- `,
- },
- });
- appFixture = await createAppFixture(fixture);
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.hover("a[href='http://github.com/with-nested-links/nested']");
- await page.waitForSelector("#nav link[rel='prefetch'][as='style']", {
- state: "attached",
+ test.describe("other scenarios", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.afterAll(() => {
+ appFixture?.close();
+ });
+
+ test("does not add prefetch links for stylesheets already in the DOM (active routes)", async ({
+ page,
+ }) => {
+ fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Links, Meta, Scripts, useFetcher } from "react-router";
+ import globalCss from "./global.css?url";
+
+ export function links() {
+ return [{ rel: "stylesheet", href: globalCss }];
+ }
+
+ export async function action() {
+ return null;
+ }
+
+ export async function loader() {
+ return null;
+ }
+
+ export default function Root() {
+ let fetcher = useFetcher();
+
+ return (
+
+
+
+
+
+
+
+ {fetcher.state}
+
+
+
+ );
+ }
+ `,
+
+ "app/global.css": `
+ body {
+ background-color: black;
+ color: white;
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function() {
+ return Index
;
+ }
+ `,
+ },
+ });
+ appFixture = await createAppFixture(fixture);
+ let requests: { type: string; url: string }[] = [];
+
+ page.on("request", (req) => {
+ requests.push({
+ type: req.resourceType(),
+ url: req.url(),
+ });
+ });
+
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await page.click("#submit-fetcher");
+ await page.waitForSelector("#fetcher-state--idle");
+ // We should not send a second request for this root stylesheet that's
+ // already been rendered in the DOM
+ let stylesheets = requests.filter(
+ (r) =>
+ r.type === "stylesheet" && /\/global-[a-z0-9-]+\.css/i.test(r.url)
+ );
+ expect(stylesheets.length).toBe(1);
+ });
+
+ test("dedupes prefetch tags", async ({ page }) => {
+ fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import {
+ Link,
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ useLoaderData,
+ } from "react-router";
+
+ export default function Root() {
+ const styles =
+ 'a:hover { color: red; } a:hover:after { content: " (hovered)"; }' +
+ 'a:focus { color: green; } a:focus:after { content: " (focused)"; }';
+
+ return (
+
+
+
+
+
+
+
+ Root
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/global.css": css`
+ .global-class {
+ background-color: gray;
+ color: black;
+ }
+ `,
+
+ "app/local.css": css`
+ .local-class {
+ background-color: black;
+ color: white;
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function() {
+ return Index
;
+ }
+ `,
+
+ "app/routes/with-nested-links.tsx": js`
+ import { Outlet } from "react-router";
+ import globalCss from "../global.css?url";
+
+ export function links() {
+ return [
+ // Same links as child route but with different key order
+ {
+ rel: "stylesheet",
+ href: globalCss,
+ },
+ {
+ rel: "preload",
+ as: "image",
+ imageSrcSet: "image-600.jpg 600w, image-1200.jpg 1200w",
+ imageSizes: "9999px",
+ },
+ ];
+ }
+ export default function() {
+ return ;
+ }
+ `,
+
+ "app/routes/with-nested-links.nested.tsx": js`
+ import globalCss from '../global.css?url';
+ import localCss from '../local.css?url';
+
+ export function links() {
+ return [
+ // Same links as parent route but with different key order
+ {
+ href: globalCss,
+ rel: "stylesheet",
+ },
+ {
+ imageSrcSet: "image-600.jpg 600w, image-1200.jpg 1200w",
+ imageSizes: "9999px",
+ rel: "preload",
+ as: "image",
+ },
+ // Unique links for child route
+ {
+ rel: "stylesheet",
+ href: localCss,
+ },
+ {
+ rel: "preload",
+ as: "image",
+ imageSrcSet: "image-700.jpg 700w, image-1400.jpg 1400w",
+ imageSizes: "9999px",
+ },
+ ];
+ }
+ export default function() {
+ return With Nested Links
;
+ }
+ `,
+ },
+ });
+ appFixture = await createAppFixture(fixture);
+
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await page.hover("a[href='http://github.com/with-nested-links/nested']");
+ await page.waitForSelector("#nav link[rel='prefetch'][as='style']", {
+ state: "attached",
+ });
+ expect(
+ await page.locator("#nav link[rel='prefetch'][as='style']").count()
+ ).toBe(2);
+ expect(
+ await page.locator("#nav link[rel='prefetch'][as='image']").count()
+ ).toBe(2);
+ });
+ });
});
- expect(
- await page.locator("#nav link[rel='prefetch'][as='style']").count()
- ).toBe(2);
- expect(
- await page.locator("#nav link[rel='prefetch'][as='image']").count()
- ).toBe(2);
});
});
diff --git a/integration/redirects-test.ts b/integration/redirects-test.ts
index b498f0a0d8..f3eefd7963 100644
--- a/integration/redirects-test.ts
+++ b/integration/redirects-test.ts
@@ -35,8 +35,7 @@ test.describe("redirects", () => {
`,
"app/routes/absolute._index.tsx": js`
- import { redirect } from "react-router";
- import { Form } from "react-router";
+ import { redirect, Form } from "react-router";
export async function action({ request }) {
return redirect(new URL(request.url).origin + "/absolute/landing");
diff --git a/integration/resource-routes-test.ts b/integration/resource-routes-test.ts
index 1e607c754f..0d02cef85f 100644
--- a/integration/resource-routes-test.ts
+++ b/integration/resource-routes-test.ts
@@ -114,7 +114,6 @@ test.describe("loader in an app", async () => {
}
`,
"app/routes/$.tsx": js`
- import { json } from "react-router";
import { useRouteError } from "react-router";
export function loader({ request }) {
throw Response.json({
diff --git a/integration/revalidate-test.ts b/integration/revalidate-test.ts
index 96f7ef6228..b14c73ff5d 100644
--- a/integration/revalidate-test.ts
+++ b/integration/revalidate-test.ts
@@ -46,8 +46,7 @@ test.describe("Revalidation", () => {
`,
"app/routes/parent.tsx": js`
- import { data } from "react-router";
- import { Outlet, useLoaderData } from "react-router";
+ import { data, Outlet, useLoaderData } from "react-router";
export async function loader({ request }) {
let header = request.headers.get('Cookie') || '';
@@ -86,8 +85,7 @@ test.describe("Revalidation", () => {
`,
"app/routes/parent.child.tsx": js`
- import { data } from "react-router";
- import { Form, useLoaderData, useRevalidator } from "react-router";
+ import { data, Form, useLoaderData, useRevalidator } from "react-router";
export async function action() {
return { action: 'data' }
diff --git a/integration/scroll-test.ts b/integration/scroll-test.ts
index 93f3a1978c..51056397d1 100644
--- a/integration/scroll-test.ts
+++ b/integration/scroll-test.ts
@@ -15,8 +15,7 @@ test.beforeAll(async () => {
fixture = await createFixture({
files: {
"app/routes/_index.tsx": js`
- import { redirect } from "react-router";
- import { Form } from "react-router";
+ import { redirect, Form } from "react-router";
export function action() {
return redirect("/test");
diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts
index 51a670b208..358922905d 100644
--- a/integration/single-fetch-test.ts
+++ b/integration/single-fetch-test.ts
@@ -408,6 +408,80 @@ test.describe("single-fetch", () => {
]);
});
+ test("revalidates on reused routes by default", async ({ page }) => {
+ let fixture = await createFixture({
+ files: {
+ ...files,
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router";
+ export default function Index() {
+ return Go to Parent
+ }
+ `,
+ "app/routes/parent.tsx": js`
+ import { Link, Outlet } from "react-router";
+ import type { Route } from "./+types/parent";
+
+ let count = 0;
+ export function loader() {
+ return ++count;
+ }
+
+ export default function Parent({ loaderData }: Route.ComponentProps) {
+ return (
+ <>
+ PARENT:{loaderData}
+ Go to Parent
+ Go to Child
+
+ >
+ );
+ }
+ `,
+ "app/routes/parent.child.tsx": js`
+ import { Outlet } from "react-router";
+ import type { Route } from "./+types/parent";
+
+ export function loader() {
+ return "CHILD"
+ }
+
+ export default function Parent({ loaderData }: Route.ComponentProps) {
+ return {loaderData}
+ }
+ `,
+ },
+ });
+
+ let urls: string[] = [];
+ page.on("request", (req) => {
+ let url = new URL(req.url());
+ if (req.method() === "GET" && url.pathname.endsWith(".data")) {
+ urls.push(url.pathname + url.search);
+ }
+ });
+
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+
+ await app.clickLink("/parent");
+ await page.waitForSelector('[data-parent="1"]');
+ expect(urls).toEqual(["/parent.data"]);
+ urls.length = 0;
+
+ await app.clickLink("/parent/child");
+ await page.waitForSelector("[data-child]");
+ await expect(page.locator('[data-parent="2"]')).toBeDefined();
+ expect(urls).toEqual(["/parent/child.data"]);
+ urls.length = 0;
+
+ await app.clickLink("/parent");
+ await page.waitForSelector('[data-parent="3"]');
+ expect(urls).toEqual(["/parent.data"]);
+ urls.length = 0;
+ });
+
test("does not revalidate on 4xx/5xx action responses", async ({ page }) => {
let fixture = await createFixture({
files: {
diff --git a/integration/split-route-modules-test.ts b/integration/split-route-modules-test.ts
index 404673504c..cc35670c4a 100644
--- a/integration/split-route-modules-test.ts
+++ b/integration/split-route-modules-test.ts
@@ -56,7 +56,7 @@ const files = {
}
})();
const timeoutPromise = new Promise((_, reject) => {
- setTimeout(() => reject(new Error("Client loader wasn't unblocked after 2s")), 2000);
+ setTimeout(() => reject(new Error("Client loader wasn't unblocked after 5s")), 5000);
});
await Promise.race([pollingPromise, timeoutPromise]);
return {
@@ -131,7 +131,7 @@ const files = {
}
})();
const timeoutPromise = new Promise((_, reject) => {
- setTimeout(() => reject(new Error("Client loader wasn't unblocked after 2s")), 2000);
+ setTimeout(() => reject(new Error("Client loader wasn't unblocked after 5s")), 5000);
});
await Promise.race([pollingPromise, timeoutPromise]);
return "clientLoader in main chunk: " + eval("typeof inUnsplittableMainChunk === 'function'");
@@ -419,8 +419,10 @@ test.describe("Split route modules", async () => {
// Ensure splittable client loader works during SSR
await page.goto(`http://localhost:${port}/splittable`);
- expect(page.locator("[data-hydrate-fallback]")).toHaveText("Loading...");
- expect(page.locator("[data-hydrate-fallback]")).toHaveCSS(
+ await expect(page.locator("[data-hydrate-fallback]")).toHaveText(
+ "Loading..."
+ );
+ await expect(page.locator("[data-hydrate-fallback]")).toHaveCSS(
"padding",
"20px"
);
@@ -431,7 +433,9 @@ test.describe("Split route modules", async () => {
// Ensure unsplittable client loader works during SSR
await page.goto(`http://localhost:${port}/unsplittable`);
- expect(page.locator("[data-hydrate-fallback]")).toHaveText("Loading...");
+ await expect(page.locator("[data-hydrate-fallback]")).toHaveText(
+ "Loading..."
+ );
await unblockClientLoader(page);
await expect(page.locator("[data-loader-data]")).toHaveText(
`loaderData = "clientLoader in main chunk: true"`
diff --git a/integration/transition-test.ts b/integration/transition-test.ts
index 1521257e6b..78e3cc16ce 100644
--- a/integration/transition-test.ts
+++ b/integration/transition-test.ts
@@ -139,8 +139,7 @@ test.describe("rendering", () => {
`,
"app/routes/gh-1691.tsx": js`
- import { json, redirect } from "react-router";
- import { useFetcher} from "react-router";
+ import { redirect, useFetcher } from "react-router";
export const action = async ( ) => {
return redirect("/gh-1691");
@@ -195,8 +194,7 @@ test.describe("rendering", () => {
`,
"app/routes/parent.child.tsx": js`
- import { redirect } from "react-router";
- import { useFetcher} from "react-router";
+ import { redirect, useFetcher } from "react-router";
export const action = async ({ request }) => {
return redirect("/parent");
diff --git a/integration/vite-basename-test.ts b/integration/vite-basename-test.ts
index 111a9201e8..af4c3bf53b 100644
--- a/integration/vite-basename-test.ts
+++ b/integration/vite-basename-test.ts
@@ -70,14 +70,15 @@ async function configFiles({
basename: basename !== "/" ? basename : undefined,
}),
"vite.config.js": js`
- import { reactRouter } from "@react-router/dev/vite";
+ import { reactRouter } from "@react-router/dev/vite";
- export default {
- ${base !== "/" ? 'base: "' + base + '",' : ""}
- ${await viteConfig.server({ port })}
- plugins: [reactRouter()]
- }
- `,
+ export default async () => ({
+ ${base !== "/" ? 'base: "' + base + '",' : ""}
+ ${await viteConfig.server({ port })}
+ ${viteConfig.build()}
+ plugins: [reactRouter()]
+ })
+ `,
};
}
diff --git a/integration/vite-build-test.ts b/integration/vite-build-test.ts
index 59155b5502..40f81ef097 100644
--- a/integration/vite-build-test.ts
+++ b/integration/vite-build-test.ts
@@ -10,6 +10,7 @@ import {
reactRouterConfig,
viteConfig,
grep,
+ viteMajorTemplates,
} from "./helpers/vite.js";
let port: number;
@@ -20,364 +21,376 @@ const js = String.raw;
test.describe("Build", () => {
[false, true].forEach((viteEnvironmentApi) => {
- test.describe(`viteEnvironmentApi enabled: ${viteEnvironmentApi}`, () => {
- test.beforeAll(async () => {
- port = await getPort();
- cwd = await createProject(
- {
- ".env": `
- ENV_VAR_FROM_DOTENV_FILE=true
- `,
- "react-router.config.ts": reactRouterConfig({ viteEnvironmentApi }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
- import mdx from "@mdx-js/rollup";
-
- export default defineConfig({
- ${await viteConfig.server({ port })}
- build: {
- // force emitting asset files instead of inlined as data-url
- assetsInlineLimit: 0,
- },
- plugins: [
- mdx(),
- reactRouter(),
- ],
- });
- `,
- "app/root.tsx": js`
- import { Links, Meta, Outlet, Scripts } from "react-router";
-
- export default function Root() {
- return (
-
-
-
-
-
-
-
-
Root
-
-
-
-
-
- );
- }
- `,
- "app/routes/_index.tsx": js`
- import { useState, useEffect } from "react";
-
- import { serverOnly1, serverOnly2 } from "../utils.server";
-
- export const loader = () => {
- return { serverOnly1 }
- }
-
- export const action = () => {
- console.log(serverOnly2)
- return null
- }
-
- export default function() {
- const [mounted, setMounted] = useState(false);
- useEffect(() => {
- setMounted(true);
- }, []);
-
- return (
- <>
- Index
- {!mounted ? Loading...
: Mounted
}
- >
- );
- }
- `,
- "app/utils.server.ts": js`
- export const serverOnly1 = "SERVER_ONLY_1"
- export const serverOnly2 = "SERVER_ONLY_2"
- `,
- "app/routes/resource.ts": js`
- import { serverOnly1, serverOnly2 } from "../utils.server";
-
- export const loader = () => {
- return { serverOnly1 }
- }
-
- export const action = () => {
- console.log(serverOnly2)
- return null
- }
- `,
- "app/routes/mdx.mdx": js`
- import { useEffect, useState } from "react";
- import { useLoaderData } from "react-router";
-
- import { serverOnly1, serverOnly2 } from "../utils.server";
-
- export const loader = () => {
- return {
- serverOnly1,
- content: "MDX route content from loader",
+ viteMajorTemplates.forEach(({ templateName, templateDisplayName }) => {
+ // Vite 5 doesn't support the Environment API
+ if (templateName === "vite-5-template" && viteEnvironmentApi) {
+ return;
+ }
+
+ test.describe(`${templateDisplayName}${
+ viteEnvironmentApi ? " with Vite Environment API" : ""
+ }`, () => {
+ test.beforeAll(async () => {
+ port = await getPort();
+ cwd = await createProject(
+ {
+ ".env": `
+ ENV_VAR_FROM_DOTENV_FILE=true
+ `,
+ "react-router.config.ts": reactRouterConfig({
+ viteEnvironmentApi,
+ }),
+ "vite.config.ts": js`
+ import { defineConfig } from "vite";
+ import { reactRouter } from "@react-router/dev/vite";
+ import mdx from "@mdx-js/rollup";
+
+ export default defineConfig({
+ ${await viteConfig.server({ port })}
+ ${viteConfig.build({
+ assetsInlineLimit: 0,
+ })}
+ plugins: [
+ mdx(),
+ reactRouter(),
+ ],
+ });
+ `,
+ "app/root.tsx": js`
+ import { Links, Meta, Outlet, Scripts } from "react-router";
+
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
Root
+
+
+
+
+
+ );
}
- }
-
- export const action = () => {
- console.log(serverOnly2)
- return null
- }
-
- export function MdxComponent() {
- const [mounted, setMounted] = useState(false);
- useEffect(() => {
- setMounted(true);
- }, []);
- const { content } = useLoaderData();
- const text = content + (mounted
- ? ": mounted"
- : ": not mounted");
- return {text}
- }
-
- ## MDX Route
-
-
- `,
- "app/routes/code-split1.tsx": js`
- import { CodeSplitComponent } from "../code-split-component";
-
- export default function CodeSplit1Route() {
- return
;
- }
- `,
- "app/routes/code-split2.tsx": js`
- import { CodeSplitComponent } from "../code-split-component";
-
- export default function CodeSplit2Route() {
- return
;
- }
- `,
- "app/code-split-component.tsx": js`
- import classes from "./code-split.module.css";
-
- export function CodeSplitComponent() {
- return ok
- }
- `,
- "app/code-split.module.css": js`
- .test {
- background-color: rgb(255, 170, 0);
- }
- `,
- "app/routes/dotenv.tsx": js`
- import { useLoaderData } from "react-router";
-
- export const loader = () => {
- return {
- loaderContent: process.env.ENV_VAR_FROM_DOTENV_FILE ?? '.env file was NOT loaded, which is a good thing',
+ `,
+ "app/routes/_index.tsx": js`
+ import { useState, useEffect } from "react";
+
+ import { serverOnly1, serverOnly2 } from "../utils.server";
+
+ export const loader = () => {
+ return { serverOnly1 }
}
- }
-
- export default function DotenvRoute() {
- const { loaderContent } = useLoaderData();
-
- return {loaderContent}
;
- }
- `,
-
- "app/assets/test.txt": "test",
- "app/routes/ssr-only-assets.tsx": js`
- import txtUrl from "../assets/test.txt?url";
- import { useLoaderData } from "react-router"
-
- export const loader: LoaderFunction = () => {
- return { txtUrl };
- };
-
- export default function SsrOnlyAssetsRoute() {
- const loaderData = useLoaderData();
- return (
-
- );
- }
- `,
-
- "app/assets/test.css": ".test{color:red}",
- "app/routes/ssr-only-css-url-files.tsx": js`
- import cssUrl from "../assets/test.css?url";
- import { useLoaderData } from "react-router"
-
- export const loader: LoaderFunction = () => {
- return { cssUrl };
- };
-
- export default function SsrOnlyCssUrlFilesRoute() {
- const loaderData = useLoaderData();
- return (
-
- );
- }
- `,
-
- "app/routes/ssr-code-split.tsx": js`
- import { useLoaderData } from "react-router"
-
- export const loader: LoaderFunction = async () => {
- const lib = await import("../ssr-code-split-lib");
- return lib.ssrCodeSplitTest();
- };
-
- export default function SsrCodeSplitRoute() {
- const loaderData = useLoaderData();
- return (
- {loaderData}
- );
- }
- `,
-
- "app/ssr-code-split-lib.ts": js`
- export function ssrCodeSplitTest() {
- return "ssrCodeSplitTest";
- }
- `,
- },
- viteEnvironmentApi ? "vite-6-template" : "vite-5-template"
- );
-
- let { stderr, status } = build({ cwd });
- expect(
- stderr
- .toString()
- // This can be removed when this issue is fixed: https://github.com/remix-run/remix/issues/9055
- .replace('Generated an empty chunk: "resource".', "")
- .trim()
- ).toBeFalsy();
- expect(status).toBe(0);
- stop = await reactRouterServe({ cwd, port });
- });
- test.afterAll(() => stop());
-
- test("server code is removed from client build", async () => {
- expect(
- grep(path.join(cwd, "build/client"), /SERVER_ONLY_1/).length
- ).toBe(0);
- expect(
- grep(path.join(cwd, "build/client"), /SERVER_ONLY_2/).length
- ).toBe(0);
- });
- test("renders matching MDX routes", async ({ page }) => {
- let pageErrors: Error[] = [];
- page.on("pageerror", (error) => pageErrors.push(error));
+ export const action = () => {
+ console.log(serverOnly2)
+ return null
+ }
- await page.goto(`http://localhost:${port}/mdx`, {
- waitUntil: "networkidle",
- });
- await expect(page.locator("[data-mdx-route]")).toHaveText(
- "MDX route content from loader: mounted"
- );
- expect(pageErrors).toEqual([]);
- });
+ export default function() {
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ return (
+ <>
+ Index
+ {!mounted ? Loading...
: Mounted
}
+ >
+ );
+ }
+ `,
+ "app/utils.server.ts": js`
+ export const serverOnly1 = "SERVER_ONLY_1"
+ export const serverOnly2 = "SERVER_ONLY_2"
+ `,
+ "app/routes/resource.ts": js`
+ import { serverOnly1, serverOnly2 } from "../utils.server";
+
+ export const loader = () => {
+ return { serverOnly1 }
+ }
- test("emits SSR-only assets to the client assets directory", async ({
- page,
- }) => {
- let pageErrors: Error[] = [];
- page.on("pageerror", (error) => pageErrors.push(error));
+ export const action = () => {
+ console.log(serverOnly2)
+ return null
+ }
+ `,
+ "app/routes/mdx.mdx": js`
+ import { useEffect, useState } from "react";
+ import { useLoaderData } from "react-router";
+
+ import { serverOnly1, serverOnly2 } from "../utils.server";
+
+ export const loader = () => {
+ return {
+ serverOnly1,
+ content: "MDX route content from loader",
+ }
+ }
- await page.goto(`http://localhost:${port}/ssr-only-assets`, {
- waitUntil: "networkidle",
- });
+ export const action = () => {
+ console.log(serverOnly2)
+ return null
+ }
- await page.getByRole("link", { name: "txtUrl" }).click();
- await page.waitForURL("**/assets/test-*.txt");
- await expect(page.getByText("test")).toBeVisible();
- expect(pageErrors).toEqual([]);
- });
+ export function MdxComponent() {
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+ const { content } = useLoaderData();
+ const text = content + (mounted
+ ? ": mounted"
+ : ": not mounted");
+ return {text}
+ }
- test("emits SSR-only .css?url files to the client assets directory", async ({
- page,
- }) => {
- let pageErrors: Error[] = [];
- page.on("pageerror", (error) => pageErrors.push(error));
+ ## MDX Route
- await page.goto(`http://localhost:${port}/ssr-only-css-url-files`, {
- waitUntil: "networkidle",
- });
+
+ `,
+ "app/routes/code-split1.tsx": js`
+ import { CodeSplitComponent } from "../code-split-component";
- await page.getByRole("link", { name: "cssUrl" }).click();
- await page.waitForURL("**/assets/test-*.css");
- await expect(page.getByText(".test{")).toBeVisible();
- expect(pageErrors).toEqual([]);
- });
+ export default function CodeSplit1Route() {
+ return
;
+ }
+ `,
+ "app/routes/code-split2.tsx": js`
+ import { CodeSplitComponent } from "../code-split-component";
+
+ export default function CodeSplit2Route() {
+ return
;
+ }
+ `,
+ "app/code-split-component.tsx": js`
+ import classes from "./code-split.module.css";
+
+ export function CodeSplitComponent() {
+ return ok
+ }
+ `,
+ "app/code-split.module.css": js`
+ .test {
+ background-color: rgb(255, 170, 0);
+ }
+ `,
+ "app/routes/dotenv.tsx": js`
+ import { useLoaderData } from "react-router";
+
+ export const loader = () => {
+ return {
+ loaderContent: process.env.ENV_VAR_FROM_DOTENV_FILE ?? '.env file was NOT loaded, which is a good thing',
+ }
+ }
- test("supports code-split JS from SSR build", async ({ page }) => {
- let pageErrors: Error[] = [];
- page.on("pageerror", (error) => pageErrors.push(error));
+ export default function DotenvRoute() {
+ const { loaderContent } = useLoaderData();
- await page.goto(`http://localhost:${port}/ssr-code-split`, {
- waitUntil: "networkidle",
+ return {loaderContent}
;
+ }
+ `,
+
+ "app/assets/test.txt": "test",
+ "app/routes/ssr-only-assets.tsx": js`
+ import txtUrl from "../assets/test.txt?url";
+ import { useLoaderData } from "react-router"
+
+ export const loader: LoaderFunction = () => {
+ return { txtUrl };
+ };
+
+ export default function SsrOnlyAssetsRoute() {
+ const loaderData = useLoaderData();
+ return (
+
+ );
+ }
+ `,
+
+ "app/assets/test.css": ".test{color:red}",
+ "app/routes/ssr-only-css-url-files.tsx": js`
+ import cssUrl from "../assets/test.css?url";
+ import { useLoaderData } from "react-router"
+
+ export const loader: LoaderFunction = () => {
+ return { cssUrl };
+ };
+
+ export default function SsrOnlyCssUrlFilesRoute() {
+ const loaderData = useLoaderData();
+ return (
+
+ );
+ }
+ `,
+
+ "app/routes/ssr-code-split.tsx": js`
+ import { useLoaderData } from "react-router"
+
+ export const loader: LoaderFunction = async () => {
+ const lib = await import("../ssr-code-split-lib");
+ return lib.ssrCodeSplitTest();
+ };
+
+ export default function SsrCodeSplitRoute() {
+ const loaderData = useLoaderData();
+ return (
+ {loaderData}
+ );
+ }
+ `,
+
+ "app/ssr-code-split-lib.ts": js`
+ export function ssrCodeSplitTest() {
+ return "ssrCodeSplitTest";
+ }
+ `,
+ },
+ templateName
+ );
+
+ let { stderr, status } = build({ cwd });
+ expect(
+ stderr
+ .toString()
+ // This can be removed when this issue is fixed: https://github.com/remix-run/remix/issues/9055
+ .replace('Generated an empty chunk: "resource".', "")
+ .trim()
+ ).toBeFalsy();
+ expect(status).toBe(0);
+ stop = await reactRouterServe({ cwd, port });
+ });
+ test.afterAll(() => stop());
+
+ test("server code is removed from client build", async () => {
+ expect(
+ grep(path.join(cwd, "build/client"), /SERVER_ONLY_1/).length
+ ).toBe(0);
+ expect(
+ grep(path.join(cwd, "build/client"), /SERVER_ONLY_2/).length
+ ).toBe(0);
});
- await expect(page.locator("[data-ssr-code-split]")).toHaveText(
- "ssrCodeSplitTest"
- );
- expect(pageErrors).toEqual([]);
- });
+ test("renders matching MDX routes", async ({ page }) => {
+ let pageErrors: Error[] = [];
+ page.on("pageerror", (error) => pageErrors.push(error));
+
+ await page.goto(`http://localhost:${port}/mdx`, {
+ waitUntil: "networkidle",
+ });
+ await expect(page.locator("[data-mdx-route]")).toHaveText(
+ "MDX route content from loader: mounted"
+ );
+ expect(pageErrors).toEqual([]);
+ });
- test("removes assets (other than code-split JS) and CSS files from SSR build", async () => {
- let assetFiles = glob.sync("build/server/assets/**/*", { cwd });
- let [asset, ...rest] = assetFiles;
- expect(rest).toEqual([]); // Provide more useful test output if this fails
- expect(asset).toMatch(/ssr-code-split-lib-.*\.js/);
- });
+ test("emits SSR-only assets to the client assets directory", async ({
+ page,
+ }) => {
+ let pageErrors: Error[] = [];
+ page.on("pageerror", (error) => pageErrors.push(error));
- test("supports code-split CSS", async ({ page }) => {
- let pageErrors: unknown[] = [];
- page.on("pageerror", (error) => pageErrors.push(error));
+ await page.goto(`http://localhost:${port}/ssr-only-assets`, {
+ waitUntil: "networkidle",
+ });
- await page.goto(`http://localhost:${port}/code-split1`, {
- waitUntil: "networkidle",
+ await page.getByRole("link", { name: "txtUrl" }).click();
+ await page.waitForURL("**/assets/test-*.txt");
+ await expect(page.getByText("test")).toBeVisible();
+ expect(pageErrors).toEqual([]);
});
- expect(
- await page
- .locator("#code-split1 span")
- .evaluate((e) => window.getComputedStyle(e).backgroundColor)
- ).toBe("rgb(255, 170, 0)");
-
- await page.goto(`http://localhost:${port}/code-split2`, {
- waitUntil: "networkidle",
+
+ test("emits SSR-only .css?url files to the client assets directory", async ({
+ page,
+ }) => {
+ let pageErrors: Error[] = [];
+ page.on("pageerror", (error) => pageErrors.push(error));
+
+ await page.goto(`http://localhost:${port}/ssr-only-css-url-files`, {
+ waitUntil: "networkidle",
+ });
+
+ await page.getByRole("link", { name: "cssUrl" }).click();
+ await page.waitForURL("**/assets/test-*.css");
+ await expect(page.getByText(".test{")).toBeVisible();
+ expect(pageErrors).toEqual([]);
});
- expect(
- await page
- .locator("#code-split2 span")
- .evaluate((e) => window.getComputedStyle(e).backgroundColor)
- ).toBe("rgb(255, 170, 0)");
- expect(pageErrors).toEqual([]);
- });
+ test("supports code-split JS from SSR build", async ({ page }) => {
+ let pageErrors: Error[] = [];
+ page.on("pageerror", (error) => pageErrors.push(error));
+
+ await page.goto(`http://localhost:${port}/ssr-code-split`, {
+ waitUntil: "networkidle",
+ });
- test("doesn't load .env file", async ({ page }) => {
- let pageErrors: unknown[] = [];
- page.on("pageerror", (error) => pageErrors.push(error));
+ await expect(page.locator("[data-ssr-code-split]")).toHaveText(
+ "ssrCodeSplitTest"
+ );
+ expect(pageErrors).toEqual([]);
+ });
+
+ test("removes assets (other than code-split JS) and CSS files from SSR build", async () => {
+ let assetFiles = glob.sync("build/server/assets/**/*", { cwd });
+ let [asset, ...rest] = assetFiles;
+ expect(rest).toEqual([]); // Provide more useful test output if this fails
+ expect(asset).toMatch(/ssr-code-split-lib-.*\.js/);
+ });
- await page.goto(`http://localhost:${port}/dotenv`, {
- waitUntil: "networkidle",
+ test("supports code-split CSS", async ({ page }) => {
+ let pageErrors: unknown[] = [];
+ page.on("pageerror", (error) => pageErrors.push(error));
+
+ await page.goto(`http://localhost:${port}/code-split1`, {
+ waitUntil: "networkidle",
+ });
+ expect(
+ await page
+ .locator("#code-split1 span")
+ .evaluate((e) => window.getComputedStyle(e).backgroundColor)
+ ).toBe("rgb(255, 170, 0)");
+
+ await page.goto(`http://localhost:${port}/code-split2`, {
+ waitUntil: "networkidle",
+ });
+ expect(
+ await page
+ .locator("#code-split2 span")
+ .evaluate((e) => window.getComputedStyle(e).backgroundColor)
+ ).toBe("rgb(255, 170, 0)");
+
+ expect(pageErrors).toEqual([]);
});
- expect(pageErrors).toEqual([]);
- let loaderContent = page.locator("[data-dotenv-route-loader-content]");
- await expect(loaderContent).toHaveText(
- ".env file was NOT loaded, which is a good thing"
- );
+ test("doesn't load .env file", async ({ page }) => {
+ let pageErrors: unknown[] = [];
+ page.on("pageerror", (error) => pageErrors.push(error));
- expect(pageErrors).toEqual([]);
+ await page.goto(`http://localhost:${port}/dotenv`, {
+ waitUntil: "networkidle",
+ });
+ expect(pageErrors).toEqual([]);
+
+ let loaderContent = page.locator(
+ "[data-dotenv-route-loader-content]"
+ );
+ await expect(loaderContent).toHaveText(
+ ".env file was NOT loaded, which is a good thing"
+ );
+
+ expect(pageErrors).toEqual([]);
+ });
});
});
});
diff --git a/integration/vite-css-test.ts b/integration/vite-css-test.ts
index 257f04286c..866ec4bdfd 100644
--- a/integration/vite-css-test.ts
+++ b/integration/vite-css-test.ts
@@ -162,8 +162,9 @@ const VITE_CONFIG = async ({
import { reactRouter } from "@react-router/dev/vite";
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
- export default {
+ export default async () => ({
${await viteConfig.server({ port })}
+ ${viteConfig.build()}
${base ? `base: "${base}",` : ""}
plugins: [
reactRouter(),
@@ -171,7 +172,7 @@ const VITE_CONFIG = async ({
emitCssInSsr: true,
}),
],
- }
+ });
`;
test.describe("Vite CSS", () => {
@@ -260,11 +261,14 @@ test.describe("Vite CSS", () => {
test.beforeAll(async () => {
port = await getPort();
- cwd = await createProject({
- "vite.config.ts": await VITE_CONFIG({ port }),
- "server.mjs": EXPRESS_SERVER({ port }),
- ...files,
- });
+ cwd = await createProject(
+ {
+ "vite.config.ts": await VITE_CONFIG({ port }),
+ "server.mjs": EXPRESS_SERVER({ port }),
+ ...files,
+ },
+ templateName
+ );
stop = await customDev({ cwd, port });
});
test.afterAll(() => stop());
@@ -292,10 +296,13 @@ test.describe("Vite CSS", () => {
test.beforeAll(async () => {
port = await getPort();
- cwd = await createProject({
- "vite.config.ts": await VITE_CONFIG({ port }),
- ...files,
- });
+ cwd = await createProject(
+ {
+ "vite.config.ts": await VITE_CONFIG({ port }),
+ ...files,
+ },
+ templateName
+ );
let edit = createEditor(cwd);
await edit("package.json", (contents) =>
diff --git a/integration/vite-prerender-test.ts b/integration/vite-prerender-test.ts
index c6b0c6d18c..182271a085 100644
--- a/integration/vite-prerender-test.ts
+++ b/integration/vite-prerender-test.ts
@@ -54,6 +54,7 @@ let files = {
Home
About
Not Found
+ Redirect
{children}
@@ -169,38 +170,38 @@ test.describe("Prerendering", () => {
files: {
...files,
"app/routes/parent.tsx": js`
- import { Outlet } from 'react-router'
- export default function Component() {
- return
- }
- `,
+ import { Outlet } from 'react-router'
+ export default function Component() {
+ return
+ }
+ `,
"app/routes/parent.child.tsx": js`
- import { Outlet } from 'react-router'
- export function loader() {
- return null;
- }
- export default function Component() {
- return
- }
- `,
+ import { Outlet } from 'react-router'
+ export function loader() {
+ return null;
+ }
+ export default function Component() {
+ return
+ }
+ `,
"app/routes/$slug.tsx": js`
- import { Outlet } from 'react-router'
- export function loader() {
- return null;
- }
- export default function Component() {
- return
- }
- `,
+ import { Outlet } from 'react-router'
+ export function loader() {
+ return null;
+ }
+ export default function Component() {
+ return
+ }
+ `,
"app/routes/$.tsx": js`
- import { Outlet } from 'react-router'
- export function loader() {
- return null;
- }
- export default function Component() {
- return
- }
- `,
+ import { Outlet } from 'react-router'
+ export function loader() {
+ return null;
+ }
+ export default function Component() {
+ return
+ }
+ `,
},
});
@@ -239,24 +240,24 @@ test.describe("Prerendering", () => {
files: {
...files,
"react-router.config.ts": js`
- export default {
- async prerender() {
- await new Promise(r => setTimeout(r, 1));
- return ['/', '/about'];
- },
- }
- `,
+ export default {
+ async prerender() {
+ await new Promise(r => setTimeout(r, 1));
+ return ['/', '/about'];
+ },
+ }
+ `,
"vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true },
- plugins: [
- reactRouter()
- ],
- });
- `,
+ import { defineConfig } from "vite";
+ import { reactRouter } from "@react-router/dev/vite";
+
+ export default defineConfig({
+ build: { manifest: true },
+ plugins: [
+ reactRouter()
+ ],
+ });
+ `,
},
});
appFixture = await createAppFixture(fixture);
@@ -291,26 +292,26 @@ test.describe("Prerendering", () => {
files: {
...files,
"react-router.config.ts": js`
- let counter = 1;
- export default {
- serverBundles: () => "server" + counter++,
- async prerender() {
- await new Promise(r => setTimeout(r, 1));
- return ['/', '/about'];
- },
- }
- `,
+ let counter = 1;
+ export default {
+ serverBundles: () => "server" + counter++,
+ async prerender() {
+ await new Promise(r => setTimeout(r, 1));
+ return ['/', '/about'];
+ },
+ }
+ `,
"vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true },
- plugins: [
- reactRouter()
- ],
- });
- `,
+ import { defineConfig } from "vite";
+ import { reactRouter } from "@react-router/dev/vite";
+
+ export default defineConfig({
+ build: { manifest: true },
+ plugins: [
+ reactRouter()
+ ],
+ });
+ `,
},
});
appFixture = await createAppFixture(fixture);
@@ -344,29 +345,29 @@ test.describe("Prerendering", () => {
files: {
...files,
"react-router.config.ts": js`
- export default {
- async prerender({ getStaticPaths }) {
- return [...getStaticPaths(), "/a", "/b"];
- },
- }
- `,
+ export default {
+ async prerender({ getStaticPaths }) {
+ return [...getStaticPaths(), "/a", "/b"];
+ },
+ }
+ `,
"vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
+ import { defineConfig } from "vite";
+ import { reactRouter } from "@react-router/dev/vite";
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
+ export default defineConfig({
+ build: { manifest: true },
+ plugins: [reactRouter()],
+ });
+ `,
"app/routes/$slug.tsx": js`
- export function loader() {
- return null
- }
- export default function component() {
- return null;
- }
- `,
+ export function loader() {
+ return null
+ }
+ export default function component() {
+ return null;
+ }
+ `,
},
});
appFixture = await createAppFixture(fixture);
@@ -443,19 +444,19 @@ test.describe("Prerendering", () => {
files: {
...files,
"app/routes/text[.txt].tsx": js`
- export function loader() {
- return new Response("Hello, world");
- }
- `,
+ export function loader() {
+ return new Response("Hello, world");
+ }
+ `,
"app/routes/json[.json].tsx": js`
- export function loader() {
- return new Response(JSON.stringify({ hello: 'world' }), {
- headers: {
- 'Content-Type': 'application/json',
- }
- });
- }
- `,
+ export function loader() {
+ return new Response(JSON.stringify({ hello: 'world' }), {
+ headers: {
+ 'Content-Type': 'application/json',
+ }
+ });
+ }
+ `,
"app/routes/image[.png].tsx": js`
export function loader() {
return new Response(
@@ -531,24 +532,24 @@ test.describe("Prerendering", () => {
files: {
...files,
"react-router.config.ts": js`
- export default {
- async prerender() {
- await new Promise(r => setTimeout(r, 1));
- return ['/', 'about'];
- },
- }
- `,
+ export default {
+ async prerender() {
+ await new Promise(r => setTimeout(r, 1));
+ return ['/', 'about'];
+ },
+ }
+ `,
"vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true },
- plugins: [
- reactRouter()
- ],
- });
- `,
+ import { defineConfig } from "vite";
+ import { reactRouter } from "@react-router/dev/vite";
+
+ export default defineConfig({
+ build: { manifest: true },
+ plugins: [
+ reactRouter()
+ ],
+ });
+ `,
},
});
appFixture = await createAppFixture(fixture);
@@ -590,36 +591,36 @@ test.describe("Prerendering", () => {
prerender: ["/", "/about"],
}),
"vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
+ import { defineConfig } from "vite";
+ import { reactRouter } from "@react-router/dev/vite";
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
+ export default defineConfig({
+ build: { manifest: true },
+ plugins: [reactRouter()],
+ });
+ `,
"app/routes/about.tsx": js`
- import { useLoaderData } from 'react-router';
- export function loader({ request }) {
- return "ABOUT-" + request.headers.has('X-React-Router-Prerender');
- }
-
- export default function Comp() {
- let data = useLoaderData();
- return About: {data}
- }
- `,
+ import { useLoaderData } from 'react-router';
+ export function loader({ request }) {
+ return "ABOUT-" + request.headers.has('X-React-Router-Prerender');
+ }
+
+ export default function Comp() {
+ let data = useLoaderData();
+ return About: {data}
+ }
+ `,
"app/routes/not-prerendered.tsx": js`
- import { useLoaderData } from 'react-router';
- export function loader({ request }) {
- return "NOT-PRERENDERED-" + request.headers.has('X-React-Router-Prerender');
- }
-
- export default function Comp() {
- let data = useLoaderData();
- return Not-Prerendered: {data}
- }
- `,
+ import { useLoaderData } from 'react-router';
+ export function loader({ request }) {
+ return "NOT-PRERENDERED-" + request.headers.has('X-React-Router-Prerender');
+ }
+
+ export default function Comp() {
+ let data = useLoaderData();
+ return Not-Prerendered: {data}
+ }
+ `,
},
});
appFixture = await createAppFixture(fixture);
@@ -646,35 +647,35 @@ test.describe("Prerendering", () => {
prerender: ["/", "/about"],
}),
"vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
+ import { defineConfig } from "vite";
+ import { reactRouter } from "@react-router/dev/vite";
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
+ export default defineConfig({
+ build: { manifest: true },
+ plugins: [reactRouter()],
+ });
+ `,
"app/routes/about.tsx": js`
- import { useLoaderData } from 'react-router';
- export function loader({ request }) {
- return {
- prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no',
- // 24999 characters
- data: new Array(5000).fill('test').join('-'),
- };
- }
-
- export default function Comp() {
- let data = useLoaderData();
- return (
- <>
- Large loader
- {data.prerendered}
- {data.data.length}
- >
- );
- }
- `,
+ import { useLoaderData } from 'react-router';
+ export function loader({ request }) {
+ return {
+ prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no',
+ // 24999 characters
+ data: new Array(5000).fill('test').join('-'),
+ };
+ }
+
+ export default function Comp() {
+ let data = useLoaderData();
+ return (
+ <>
+ Large loader
+ {data.prerendered}
+ {data.data.length}
+ >
+ );
+ }
+ `,
},
});
appFixture = await createAppFixture(fixture);
@@ -699,54 +700,54 @@ test.describe("Prerendering", () => {
prerender: ["/", "/utf8-prerendered"],
}),
"vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
+ import { defineConfig } from "vite";
+ import { reactRouter } from "@react-router/dev/vite";
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
+ export default defineConfig({
+ build: { manifest: true },
+ plugins: [reactRouter()],
+ });
+ `,
"app/routes/utf8-prerendered.tsx": js`
- import { useLoaderData } from 'react-router';
- export function loader({ request }) {
- return {
- prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no',
- data: "한글 데이터 - UTF-8 문자",
- };
- }
-
- export default function Comp() {
- let data = useLoaderData();
- return (
- <>
- UTF-8 Prerendered
- {data.prerendered}
- {data.data}
- >
- );
- }
- `,
+ import { useLoaderData } from 'react-router';
+ export function loader({ request }) {
+ return {
+ prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no',
+ data: "한글 데이터 - UTF-8 문자",
+ };
+ }
+
+ export default function Comp() {
+ let data = useLoaderData();
+ return (
+ <>
+ UTF-8 Prerendered
+ {data.prerendered}
+ {data.data}
+ >
+ );
+ }
+ `,
"app/routes/utf8-not-prerendered.tsx": js`
- import { useLoaderData } from 'react-router';
- export function loader({ request }) {
- return {
- prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no',
- data: "非プリレンダリングデータ - UTF-8文字",
- };
- }
-
- export default function Comp() {
- let data = useLoaderData();
- return (
- <>
- UTF-8 Not Prerendered
- {data.prerendered}
- {data.data}
- >
- );
- }
- `,
+ import { useLoaderData } from 'react-router';
+ export function loader({ request }) {
+ return {
+ prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no',
+ data: "非プリレンダリングデータ - UTF-8文字",
+ };
+ }
+
+ export default function Comp() {
+ let data = useLoaderData();
+ return (
+ <>
+ UTF-8 Not Prerendered
+ {data.prerendered}
+ {data.data}
+ >
+ );
+ }
+ `,
},
});
appFixture = await createAppFixture(fixture);
@@ -782,47 +783,47 @@ test.describe("Prerendering", () => {
prerender: ["/", "/parent", "/parent/child"],
}),
"vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
+ import { defineConfig } from "vite";
+ import { reactRouter } from "@react-router/dev/vite";
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
+ export default defineConfig({
+ build: { manifest: true },
+ plugins: [reactRouter()],
+ });
+ `,
"app/routes/parent.tsx": js`
- import { Outlet, useLoaderData } from 'react-router';
- export function loader() {
- return "PARENT";
- }
- export default function Comp() {
- let data = useLoaderData();
- return <>Parent: {data}
>
- }
- `,
+ import { Outlet, useLoaderData } from 'react-router';
+ export function loader() {
+ return "PARENT";
+ }
+ export default function Comp() {
+ let data = useLoaderData();
+ return <>Parent: {data}
>
+ }
+ `,
"app/routes/parent.child.tsx": js`
- import { Outlet, useLoaderData } from 'react-router';
- export function loader() {
- return "CHILD";
- }
- export function HydrateFallback() {
- return Child loading...
- }
- export default function Comp() {
- let data = useLoaderData();
- return <>Child: {data}
>
- }
- `,
+ import { Outlet, useLoaderData } from 'react-router';
+ export function loader() {
+ return "CHILD";
+ }
+ export function HydrateFallback() {
+ return Child loading...
+ }
+ export default function Comp() {
+ let data = useLoaderData();
+ return <>Child: {data}
>
+ }
+ `,
"app/routes/parent.child._index.tsx": js`
- import { Outlet, useLoaderData } from 'react-router';
- export function clientLoader() {
- return "INDEX";
- }
- export default function Comp() {
- let data = useLoaderData();
- return <>Index: {data}
>
- }
- `,
+ import { Outlet, useLoaderData } from 'react-router';
+ export function clientLoader() {
+ return "INDEX";
+ }
+ export default function Comp() {
+ let data = useLoaderData();
+ return <>Index: {data}
>
+ }
+ `,
},
});
appFixture = await createAppFixture(fixture);
@@ -853,6 +854,12 @@ test.describe("Prerendering", () => {
return requests;
}
+ function clearRequests(requests: string[]) {
+ while (requests.length) {
+ requests.pop();
+ }
+ }
+
test("Errors on headers/action functions in any route", async () => {
let cwd = await createProject({
"react-router.config.ts": reactRouterConfig({
@@ -1206,43 +1213,45 @@ test.describe("Prerendering", () => {
expect(await (await page.$("[data-page]"))?.innerText()).toBe(
"PAGE DATA"
);
+ expect(requests).toEqual(["/page.data"]);
+ clearRequests(requests);
await app.clickSubmitButton("/page");
await page.waitForSelector("[data-page-action]");
expect(await (await page.$("[data-page-action]"))?.innerText()).toBe(
"PAGE ACTION 1"
);
+ // No revalidation after submission to self
+ expect(requests).toEqual([]);
await app.clickLink("/page2");
await page.waitForSelector("[data-page2]");
expect(await (await page.$("[data-page2]"))?.innerText()).toBe(
"PAGE2 DATA"
);
+ expect(requests).toEqual([]);
await app.clickSubmitButton("/page2");
await page.waitForSelector("[data-page2-action]");
expect(await (await page.$("[data-page2-action]"))?.innerText()).toBe(
"PAGE2 ACTION 1"
);
+ expect(requests).toEqual([]);
await app.clickSubmitButton("/page");
await page.waitForSelector("[data-page-action]");
expect(await (await page.$("[data-page-action]"))?.innerText()).toBe(
"PAGE ACTION 2"
);
+ expect(requests).toEqual(["/page.data"]);
+ clearRequests(requests);
await app.clickSubmitButton("/page2");
await page.waitForSelector("[data-page2-action]");
expect(await (await page.$("[data-page2-action]"))?.innerText()).toBe(
"PAGE2 ACTION 2"
);
-
- // We should only make this call when navigating to the prerendered route
- // 2 calls (no revalidation after submission to self):
- // - ✅ Initial navigation
- // - ❌ No revalidation after submission to self
- // - ✅ After submission back from /page
- expect(requests).toEqual(["/page.data", "/page.data"]);
+ expect(requests).toEqual([]);
});
test("Navigates across SPA/prerender pages when starting from a prerendered page", async ({
@@ -1345,43 +1354,45 @@ test.describe("Prerendering", () => {
expect(await (await page.$("[data-page]"))?.innerText()).toBe(
"PAGE DATA"
);
+ expect(requests).toEqual(["/page.data"]);
+ clearRequests(requests);
await app.clickSubmitButton("/page");
await page.waitForSelector("[data-page-action]");
expect(await (await page.$("[data-page-action]"))?.innerText()).toBe(
"PAGE ACTION 1"
);
+ // No revalidation after submission to self
+ expect(requests).toEqual([]);
await app.clickLink("/page2");
await page.waitForSelector("[data-page2]");
expect(await (await page.$("[data-page2]"))?.innerText()).toBe(
"PAGE2 DATA"
);
+ expect(requests).toEqual([]);
await app.clickSubmitButton("/page2");
await page.waitForSelector("[data-page2-action]");
expect(await (await page.$("[data-page2-action]"))?.innerText()).toBe(
"PAGE2 ACTION 1"
);
+ expect(requests).toEqual([]);
await app.clickSubmitButton("/page");
await page.waitForSelector("[data-page-action]");
expect(await (await page.$("[data-page-action]"))?.innerText()).toBe(
"PAGE ACTION 2"
);
+ expect(requests).toEqual(["/page.data"]);
+ clearRequests(requests);
await app.clickSubmitButton("/page2");
await page.waitForSelector("[data-page2-action]");
expect(await (await page.$("[data-page2-action]"))?.innerText()).toBe(
"PAGE2 ACTION 2"
);
-
- // We should only make this call when navigating to the prerendered route
- // 2 calls (no revalidation after submission to self):
- // - ✅ Initial navigation
- // - ❌ No revalidation after submission to self
- // - ✅ After submission back from /page
- expect(requests).toEqual(["/page.data", "/page.data"]);
+ expect(requests).toEqual([]);
});
test("Navigates across SPA/prerender pages when starting from a SPA page and a root loader exists", async ({
@@ -1496,43 +1507,45 @@ test.describe("Prerendering", () => {
expect(await (await page.$("[data-page]"))?.innerText()).toBe(
"PAGE DATA"
);
+ expect(requests).toEqual(["/page.data"]);
+ clearRequests(requests);
await app.clickSubmitButton("/page");
await page.waitForSelector("[data-page-action]");
expect(await (await page.$("[data-page-action]"))?.innerText()).toBe(
"PAGE ACTION 1"
);
+ // No revalidation after submission to self
+ expect(requests).toEqual([]);
await app.clickLink("/page2");
await page.waitForSelector("[data-page2]");
expect(await (await page.$("[data-page2]"))?.innerText()).toBe(
"PAGE2 DATA"
);
+ expect(requests).toEqual([]);
await app.clickSubmitButton("/page2");
await page.waitForSelector("[data-page2-action]");
expect(await (await page.$("[data-page2-action]"))?.innerText()).toBe(
"PAGE2 ACTION 1"
);
+ expect(requests).toEqual([]);
await app.clickSubmitButton("/page");
await page.waitForSelector("[data-page-action]");
expect(await (await page.$("[data-page-action]"))?.innerText()).toBe(
"PAGE ACTION 2"
);
+ expect(requests).toEqual(["/page.data"]);
+ clearRequests(requests);
await app.clickSubmitButton("/page2");
await page.waitForSelector("[data-page2-action]");
expect(await (await page.$("[data-page2-action]"))?.innerText()).toBe(
"PAGE2 ACTION 2"
);
-
- // We should only make this call when navigating to the prerendered route
- // 2 calls (no revalidation after submission to self):
- // - ✅ Initial navigation
- // - ❌ No revalidation after submission to self
- // - ✅ After submission back from /page
- expect(requests).toEqual(["/page.data", "/page.data"]);
+ expect(requests).toEqual([]);
});
test("Navigates across SPA/prerender pages when starting from a prerendered page and a root loader exists", async ({
@@ -1647,43 +1660,45 @@ test.describe("Prerendering", () => {
expect(await (await page.$("[data-page]"))?.innerText()).toBe(
"PAGE DATA"
);
+ expect(requests).toEqual(["/page.data"]);
+ clearRequests(requests);
await app.clickSubmitButton("/page");
await page.waitForSelector("[data-page-action]");
expect(await (await page.$("[data-page-action]"))?.innerText()).toBe(
"PAGE ACTION 1"
);
+ // No revalidation after submission to self
+ expect(requests).toEqual([]);
await app.clickLink("/page2");
await page.waitForSelector("[data-page2]");
expect(await (await page.$("[data-page2]"))?.innerText()).toBe(
"PAGE2 DATA"
);
+ expect(requests).toEqual([]);
await app.clickSubmitButton("/page2");
await page.waitForSelector("[data-page2-action]");
expect(await (await page.$("[data-page2-action]"))?.innerText()).toBe(
"PAGE2 ACTION 1"
);
+ expect(requests).toEqual([]);
await app.clickSubmitButton("/page");
await page.waitForSelector("[data-page-action]");
expect(await (await page.$("[data-page-action]"))?.innerText()).toBe(
"PAGE ACTION 2"
);
+ expect(requests).toEqual(["/page.data"]);
+ clearRequests(requests);
await app.clickSubmitButton("/page2");
await page.waitForSelector("[data-page2-action]");
expect(await (await page.$("[data-page2-action]"))?.innerText()).toBe(
"PAGE2 ACTION 2"
);
-
- // We should only make this call when navigating to the prerendered route
- // 2 calls (no revalidation after submission to self):
- // - ✅ Initial navigation
- // - ❌ No revalidation after submission to self
- // - ✅ After submission back from /page
- expect(requests).toEqual(["/page.data", "/page.data"]);
+ expect(requests).toEqual([]);
});
test("Navigates between prerendered parent and child SPA route", async ({
@@ -2241,6 +2256,183 @@ test.describe("Prerendering", () => {
expect(requests).toEqual(["/parent/child.data"]);
});
+ test("Navigates prerender pages when params exist", async ({ page }) => {
+ fixture = await createFixture({
+ prerender: true,
+ files: {
+ "react-router.config.ts": reactRouterConfig({
+ ssr: false, // turn off fog of war since we're serving with a static server
+ prerender: ["/", "/page", "/param/1", "/param/2"],
+ }),
+ "vite.config.ts": files["vite.config.ts"],
+ "app/root.tsx": js`
+ import * as React from "react";
+ import { Link, Outlet, Scripts, useNavigation } from "react-router";
+
+ export function Layout({ children }) {
+ let navigation = useNavigation();
+ return (
+
+
+
+
+ {navigation.state}
+ {children}
+
+
+
+ );
+ }
+
+ export default function Root({ loaderData }) {
+ return
+ }
+ `,
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index
+ }
+ `,
+ "app/routes/page.tsx": js`
+ export function loader() {
+ return "PAGE DATA"
+ }
+ export default function Page({ loaderData }) {
+ return {loaderData}
;
+ }
+ `,
+ "app/routes/param.$id.tsx": js`
+ export function loader({ params }) {
+ return params.id;
+ }
+ export default function Page({ loaderData }) {
+ return Param {loaderData}
;
+ }
+ `,
+ },
+ });
+ appFixture = await createAppFixture(fixture);
+
+ let requests = captureRequests(page);
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+ await page.waitForSelector("[data-index]");
+
+ await app.clickLink("/page");
+ await page.waitForSelector("[data-page]");
+ expect(await (await page.$("[data-page]"))?.innerText()).toBe(
+ "PAGE DATA"
+ );
+ expect(requests).toEqual(["/page.data"]);
+ clearRequests(requests);
+
+ await app.clickLink("/page");
+ await page.waitForSelector("#navigation-idle");
+ expect(await (await page.$("[data-page]"))?.innerText()).toBe(
+ "PAGE DATA"
+ );
+ // No revalidation since page.data is static
+ expect(requests).toEqual([]);
+
+ await app.clickLink("/param/1");
+ await page.waitForSelector('[data-param="1"]');
+ expect(await (await page.$("[data-param]"))?.innerText()).toBe("Param 1");
+ expect(requests).toEqual(["/param/1.data"]);
+ clearRequests(requests);
+
+ await app.clickLink("/param/2");
+ await page.waitForSelector('[data-param="2"]');
+ expect(await (await page.$("[data-param]"))?.innerText()).toBe("Param 2");
+ expect(requests).toEqual(["/param/2.data"]);
+ clearRequests(requests);
+
+ await app.clickLink("/page");
+ await page.waitForSelector("[data-page]");
+ expect(await (await page.$("[data-page]"))?.innerText()).toBe(
+ "PAGE DATA"
+ );
+ expect(requests).toEqual(["/page.data"]);
+ });
+
+ test("Returns a 404 if navigating to a non-prerendered param value", async ({
+ page,
+ }) => {
+ fixture = await createFixture({
+ prerender: true,
+ files: {
+ "react-router.config.ts": reactRouterConfig({
+ ssr: false, // turn off fog of war since we're serving with a static server
+ prerender: ["/param/1"],
+ }),
+ "vite.config.ts": files["vite.config.ts"],
+ "app/root.tsx": js`
+ import * as React from "react";
+ import { Link, Outlet, Scripts, useNavigation } from "react-router";
+
+ export function Layout({ children }) {
+ let navigation = useNavigation();
+ return (
+
+
+
+
+
{navigation.state}
+ {children}
+
+
+
+ );
+ }
+
+ export default function Root({ loaderData }) {
+ return
+ }
+ `,
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return
Index
+ }
+ `,
+ "app/routes/param.$id.tsx": js`
+ export function loader({ params }) {
+ return params.id;
+ }
+ export default function Page({ loaderData }) {
+ return
Param {loaderData}
;
+ }
+
+ export function ErrorBoundary({ error }) {
+ return
{error.status}
;
+ }
+ `,
+ },
+ });
+ appFixture = await createAppFixture(fixture);
+
+ let requests = captureRequests(page);
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+ await page.waitForSelector("[data-index]");
+
+ await app.clickLink("/param/1");
+ await page.waitForSelector('[data-param="1"]');
+ expect(await (await page.$("[data-param]"))?.innerText()).toBe("Param 1");
+ expect(requests).toEqual(["/param/1.data"]);
+ clearRequests(requests);
+
+ await app.clickLink("/param/404");
+ await page.waitForSelector('[data-error="404"]');
+ expect(requests).toEqual(["/param/404.data"]);
+ });
+
test("Navigates to prerendered parent with clientLoader calling loader", async ({
page,
}) => {
@@ -2347,5 +2539,47 @@ test.describe("Prerendering", () => {
await page.waitForSelector("[data-error]:has-text('404 Not Found')");
expect(requests).toEqual(["/not-found.data"]);
});
+
+ test("Handles redirects in prerendered pages", async ({ page }) => {
+ fixture = await createFixture({
+ prerender: true,
+ files: {
+ ...files,
+ "react-router.config.ts": reactRouterConfig({
+ ssr: false, // turn off fog of war since we're serving with a static server
+ prerender: true,
+ }),
+ "app/routes/redirect.tsx": js`
+ import { redirect } from "react-router"
+ export function loader() {
+ return redirect('/target', 301);
+ }
+ export default function Component() {
+
Nope
+ }
+ `,
+ "app/routes/target.tsx": js`
+ export default function Component() {
+ return
Target
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+
+ // Document loads
+ let requests = captureRequests(page);
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/redirect");
+ await page.waitForSelector("#target");
+ expect(requests).toEqual([]);
+
+ // Client side navigations
+ await app.goto("/", true);
+ app.clickLink("/redirect");
+ await page.waitForSelector("#target");
+ expect(requests).toEqual(["/redirect.data"]);
+ });
});
});
diff --git a/integration/vite-presets-test.ts b/integration/vite-presets-test.ts
index 12ea017b71..6128229996 100644
--- a/integration/vite-presets-test.ts
+++ b/integration/vite-presets-test.ts
@@ -5,7 +5,13 @@ import { expect } from "@playwright/test";
import { normalizePath } from "vite";
import dedent from "dedent";
-import { build, test, createProject } from "./helpers/vite.js";
+import {
+ build,
+ test,
+ createProject,
+ viteMajorTemplates,
+ viteConfig,
+} from "./helpers/vite.js";
const js = String.raw;
@@ -131,124 +137,121 @@ const files = {
],
}
`),
- "vite.config.ts": dedent(js`
- import { reactRouter } from "@react-router/dev/vite";
-
- export default {
- build: {
- assetsDir: "custom-assets-dir",
- },
- plugins: [reactRouter()],
- }
- `),
+ "vite.config.ts": await viteConfig.basic({
+ assetsDir: "custom-assets-dir",
+ }),
};
-test("Vite / presets", async () => {
- let cwd = await createProject(files);
- let { status, stderr } = build({ cwd });
- expect(stderr.toString()).toBeFalsy();
- expect(status).toBe(0);
-
- function pathStartsWithCwd(pathname: string) {
- return normalizePath(pathname).startsWith(normalizePath(cwd));
- }
-
- function relativeToCwd(pathname: string) {
- return normalizePath(path.relative(cwd, pathname));
- }
-
- let buildEndArgsMeta: any = await import(
- URL.pathToFileURL(path.join(cwd, "BUILD_END_META.js")).href
- );
-
- let { reactRouterConfig } = buildEndArgsMeta;
-
- // Smoke test Vite config
- expect(buildEndArgsMeta.assetsDir).toBe("custom-assets-dir");
-
- // Before rewriting to relative paths, assert that paths are absolute within cwd
- expect(pathStartsWithCwd(reactRouterConfig.buildDirectory)).toBe(true);
-
- // Rewrite path args to be relative and normalized for snapshot test
- reactRouterConfig.buildDirectory = relativeToCwd(
- reactRouterConfig.buildDirectory
- );
-
- // Ensure preset configs are merged in correct order, resulting in the correct build directory
- expect(reactRouterConfig.buildDirectory).toBe("build");
-
- // Ensure preset config takes lower precedence than user config
- expect(reactRouterConfig.serverModuleFormat).toBe("esm");
-
- // Ensure `reactRouterConfig` is called with a frozen user config
- expect(
- JSON.parse(
- await fs.readFile(
- path.join(cwd, "PRESET_REACT_ROUTER_CONFIG_META.json"),
- "utf-8"
- )
- )
- ).toEqual({
- reactRouterUserConfigFrozen: true,
- });
+test.describe("Vite / presets", async () => {
+ viteMajorTemplates.forEach(({ templateName, templateDisplayName }) => {
+ test(templateDisplayName, async () => {
+ let cwd = await createProject(files, templateName);
+ let { status, stderr } = build({ cwd });
+ expect(stderr.toString()).toBeFalsy();
+ expect(status).toBe(0);
- // Ensure `reactRouterConfigResolved` is called with a frozen config
- expect(
- JSON.parse(
- await fs.readFile(
- path.join(cwd, "PRESET_REACT_ROUTER_CONFIG_RESOLVED_META.json"),
- "utf-8"
- )
- )
- ).toEqual({
- reactRouterUserConfigFrozen: true,
- });
+ function pathStartsWithCwd(pathname: string) {
+ return normalizePath(pathname).startsWith(normalizePath(cwd));
+ }
+
+ function relativeToCwd(pathname: string) {
+ return normalizePath(path.relative(cwd, pathname));
+ }
+
+ let buildEndArgsMeta: any = await import(
+ URL.pathToFileURL(path.join(cwd, "BUILD_END_META.js")).href
+ );
+
+ let { reactRouterConfig } = buildEndArgsMeta;
+
+ // Smoke test Vite config
+ expect(buildEndArgsMeta.assetsDir).toBe("custom-assets-dir");
+
+ // Before rewriting to relative paths, assert that paths are absolute within cwd
+ expect(pathStartsWithCwd(reactRouterConfig.buildDirectory)).toBe(true);
+
+ // Rewrite path args to be relative and normalized for snapshot test
+ reactRouterConfig.buildDirectory = relativeToCwd(
+ reactRouterConfig.buildDirectory
+ );
+
+ // Ensure preset configs are merged in correct order, resulting in the correct build directory
+ expect(reactRouterConfig.buildDirectory).toBe("build");
+
+ // Ensure preset config takes lower precedence than user config
+ expect(reactRouterConfig.serverModuleFormat).toBe("esm");
+
+ // Ensure `reactRouterConfig` is called with a frozen user config
+ expect(
+ JSON.parse(
+ await fs.readFile(
+ path.join(cwd, "PRESET_REACT_ROUTER_CONFIG_META.json"),
+ "utf-8"
+ )
+ )
+ ).toEqual({
+ reactRouterUserConfigFrozen: true,
+ });
+
+ // Ensure `reactRouterConfigResolved` is called with a frozen config
+ expect(
+ JSON.parse(
+ await fs.readFile(
+ path.join(cwd, "PRESET_REACT_ROUTER_CONFIG_RESOLVED_META.json"),
+ "utf-8"
+ )
+ )
+ ).toEqual({
+ reactRouterUserConfigFrozen: true,
+ });
+
+ // Snapshot the buildEnd args keys
+ expect(buildEndArgsMeta.keys).toEqual([
+ "buildManifest",
+ "reactRouterConfig",
+ "viteConfig",
+ ]);
+
+ // Smoke test the resolved config
+ expect(Object.keys(reactRouterConfig)).toEqual([
+ "appDirectory",
+ "basename",
+ "buildDirectory",
+ "buildEnd",
+ "future",
+ "prerender",
+ "routes",
+ "serverBuildFile",
+ "serverBundles",
+ "serverModuleFormat",
+ "ssr",
+ ]);
- // Snapshot the buildEnd args keys
- expect(buildEndArgsMeta.keys).toEqual([
- "buildManifest",
- "reactRouterConfig",
- "viteConfig",
- ]);
-
- // Smoke test the resolved config
- expect(Object.keys(reactRouterConfig)).toEqual([
- "appDirectory",
- "basename",
- "buildDirectory",
- "buildEnd",
- "future",
- "prerender",
- "routes",
- "serverBuildFile",
- "serverBundles",
- "serverModuleFormat",
- "ssr",
- ]);
-
- // Ensure we get a valid build manifest
- expect(buildEndArgsMeta.buildManifest).toEqual({
- routeIdToServerBundleId: {
- "routes/_index": "preset-server-bundle-id",
- },
- routes: {
- root: {
- file: "app/root.tsx",
- id: "root",
- path: "",
- },
- "routes/_index": {
- file: "app/routes/_index.tsx",
- id: "routes/_index",
- index: true,
- parentId: "root",
- },
- },
- serverBundles: {
- "preset-server-bundle-id": {
- file: "build/server/preset-server-bundle-id/index.js",
- id: "preset-server-bundle-id",
- },
- },
+ // Ensure we get a valid build manifest
+ expect(buildEndArgsMeta.buildManifest).toEqual({
+ routeIdToServerBundleId: {
+ "routes/_index": "preset-server-bundle-id",
+ },
+ routes: {
+ root: {
+ file: "app/root.tsx",
+ id: "root",
+ path: "",
+ },
+ "routes/_index": {
+ file: "app/routes/_index.tsx",
+ id: "routes/_index",
+ index: true,
+ parentId: "root",
+ },
+ },
+ serverBundles: {
+ "preset-server-bundle-id": {
+ file: "build/server/preset-server-bundle-id/index.js",
+ id: "preset-server-bundle-id",
+ },
+ },
+ });
+ });
});
});
diff --git a/integration/vite-route-exports-modified-offscreen-test.ts b/integration/vite-route-exports-modified-offscreen-test.ts
index 6ee7562998..56d5d50894 100644
--- a/integration/vite-route-exports-modified-offscreen-test.ts
+++ b/integration/vite-route-exports-modified-offscreen-test.ts
@@ -76,6 +76,8 @@ test.describe(async () => {
originalContents = contents;
return contents.replace(/export const loader.*/, "");
});
+ // Give the server time to pick the manifest change
+ await new Promise((resolve) => setTimeout(resolve, 200));
// After browser reload, client should be aware that there's no loader on the other route
if (browserName === "webkit") {
@@ -83,12 +85,15 @@ test.describe(async () => {
// Otherwise browser doesn't seem to fetch new manifest probably due to caching.
page = await context.newPage();
}
- await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
- await expect(page.locator("[data-mounted]")).toHaveText("Mounted: yes");
- await page.getByRole("link", { name: "/other" }).click();
- await expect(page.locator("[data-loader-data]")).toHaveText(
- "loaderData = null"
- );
+ // In case the earlier wait wasn't enough, let the test try again
+ await expect(async () => {
+ await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
+ await expect(page.locator("[data-mounted]")).toHaveText("Mounted: yes");
+ await page.getByRole("link", { name: "/other" }).click();
+ await expect(page.locator("[data-loader-data]")).toHaveText(
+ "loaderData = null"
+ );
+ }).toPass();
expect(pageErrors).toEqual([]);
// Revert route to original state to check HMR works and to ensure the
diff --git a/packages/create-react-router/CHANGELOG.md b/packages/create-react-router/CHANGELOG.md
index f2389a1fe9..03f4e9eaf2 100644
--- a/packages/create-react-router/CHANGELOG.md
+++ b/packages/create-react-router/CHANGELOG.md
@@ -1,5 +1,9 @@
# `create-react-router`
+## 7.5.1
+
+_No changes_
+
## 7.5.0
_No changes_
diff --git a/packages/create-react-router/package.json b/packages/create-react-router/package.json
index 3b9eb35514..71cfef4b9a 100644
--- a/packages/create-react-router/package.json
+++ b/packages/create-react-router/package.json
@@ -1,6 +1,6 @@
{
"name": "create-react-router",
- "version": "7.5.0",
+ "version": "7.5.1",
"description": "Create a new React Router app",
"homepage": "https://reactrouter.com/",
"bugs": {
diff --git a/packages/react-router-architect/CHANGELOG.md b/packages/react-router-architect/CHANGELOG.md
index d1fd97f236..e780924ea8 100644
--- a/packages/react-router-architect/CHANGELOG.md
+++ b/packages/react-router-architect/CHANGELOG.md
@@ -1,5 +1,13 @@
# `@react-router/architect`
+## 7.5.1
+
+### Patch Changes
+
+- Updated dependencies:
+ - `react-router@7.5.1`
+ - `@react-router/node@7.5.1`
+
## 7.5.0
### Patch Changes
diff --git a/packages/react-router-architect/package.json b/packages/react-router-architect/package.json
index 338b6b5af4..8fae1dd38b 100644
--- a/packages/react-router-architect/package.json
+++ b/packages/react-router-architect/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-router/architect",
- "version": "7.5.0",
+ "version": "7.5.1",
"description": "Architect server request handler for React Router",
"bugs": {
"url": "https://github.com/remix-run/react-router/issues"
diff --git a/packages/react-router-cloudflare/CHANGELOG.md b/packages/react-router-cloudflare/CHANGELOG.md
index 669bccf158..f296000790 100644
--- a/packages/react-router-cloudflare/CHANGELOG.md
+++ b/packages/react-router-cloudflare/CHANGELOG.md
@@ -1,5 +1,12 @@
# `@react-router/cloudflare`
+## 7.5.1
+
+### Patch Changes
+
+- Updated dependencies:
+ - `react-router@7.5.1`
+
## 7.5.0
### Patch Changes
diff --git a/packages/react-router-cloudflare/package.json b/packages/react-router-cloudflare/package.json
index 6c37a6e3de..04032c2910 100644
--- a/packages/react-router-cloudflare/package.json
+++ b/packages/react-router-cloudflare/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-router/cloudflare",
- "version": "7.5.0",
+ "version": "7.5.1",
"description": "Cloudflare platform abstractions for React Router",
"bugs": {
"url": "https://github.com/remix-run/react-router/issues"
diff --git a/packages/react-router-dev/CHANGELOG.md b/packages/react-router-dev/CHANGELOG.md
index c4c6cb6589..47a840a414 100644
--- a/packages/react-router-dev/CHANGELOG.md
+++ b/packages/react-router-dev/CHANGELOG.md
@@ -1,5 +1,15 @@
# `@react-router/dev`
+## 7.5.1
+
+### Patch Changes
+
+- Fix prerendering when a loader returns a redirect ([#13365](https://github.com/remix-run/react-router/pull/13365))
+- Updated dependencies:
+ - `react-router@7.5.1`
+ - `@react-router/node@7.5.1`
+ - `@react-router/serve@7.5.1`
+
## 7.5.0
### Patch Changes
diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json
index 28ba35bc3b..fe8ddd6cae 100644
--- a/packages/react-router-dev/package.json
+++ b/packages/react-router-dev/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-router/dev",
- "version": "7.5.0",
+ "version": "7.5.1",
"description": "Dev tools and CLI for React Router",
"homepage": "https://reactrouter.com/",
"bugs": {
diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts
index 96b51e1711..66c24acfd9 100644
--- a/packages/react-router-dev/vite/plugin.ts
+++ b/packages/react-router-dev/vite/plugin.ts
@@ -2761,7 +2761,8 @@ async function prerenderData(
let response = await handler(request);
let data = await response.text();
- if (response.status !== 200) {
+ // 202 is used for `.data` redirects
+ if (response.status !== 200 && response.status !== 202) {
throw new Error(
`Prerender (data): Received a ${response.status} status code from ` +
`\`entry.server.tsx\` while prerendering the \`${prerenderPath}\` ` +
@@ -2780,6 +2781,8 @@ async function prerenderData(
return data;
}
+let redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
+
async function prerenderRoute(
handler: RequestHandler,
prerenderPath: string,
@@ -2796,7 +2799,29 @@ async function prerenderRoute(
let response = await handler(request);
let html = await response.text();
- if (response.status !== 200) {
+ if (redirectStatusCodes.has(response.status)) {
+ // This isn't ideal but gets the job done as a fallback if the user can't
+ // implement proper redirects via .htaccess or something else. This is the
+ // approach used by Astro as well so there's some precedent.
+ // https://github.com/withastro/roadmap/issues/466
+ // https://github.com/withastro/astro/blob/main/packages/astro/src/core/routing/3xx.ts
+ let location = response.headers.get("Location");
+ // A short delay causes Google to interpret the redirect as temporary.
+ // https://developers.google.com/search/docs/crawling-indexing/301-redirects#metarefresh
+ let delay = response.status === 302 ? 2 : 0;
+ html = `
+
+
Redirecting to: ${location}
+
+
+
+
+
+ Redirecting from ${normalizedPath}
to ${location}
+
+
+`;
+ } else if (response.status !== 200) {
throw new Error(
`Prerender (html): Received a ${response.status} status code from ` +
`\`entry.server.tsx\` while prerendering the \`${normalizedPath}\` ` +
@@ -3383,29 +3408,46 @@ export async function getEnvironmentOptionsResolvers(
}: {
viteUserConfig: Vite.UserConfig;
}): EnvironmentOptions {
+ // This is a workaround for type errors when running in vite-ecosystem-ci
+ // against rolldown-vite since "preserveEntrySignatures" is not yet
+ // supported. We're doing this instead of using `ts-ignore` so we're still
+ // type checking against regular Vite build options. Once it's supported,
+ // this custom type can be removed and the build config can be inlined.
+ type RollupOptionsWithPreserveEntrySignatures =
+ Vite.BuildOptions["rollupOptions"] extends {
+ preserveEntrySignatures: any;
+ }
+ ? Vite.BuildOptions["rollupOptions"]
+ : Vite.BuildOptions["rollupOptions"] & {
+ // We hard-code the one value we're using. If it's not valid in
+ // Rollup, the build will fail.
+ preserveEntrySignatures: "exports-only";
+ };
+ const rollupOptions: RollupOptionsWithPreserveEntrySignatures = {
+ preserveEntrySignatures: "exports-only",
+ // Silence Rollup "use client" warnings
+ // Adapted from https://github.com/vitejs/vite-plugin-react/pull/144
+ onwarn(warning, defaultHandler) {
+ if (
+ warning.code === "MODULE_LEVEL_DIRECTIVE" &&
+ warning.message.includes("use client")
+ ) {
+ return;
+ }
+ let userHandler = viteUserConfig.build?.rollupOptions?.onwarn;
+ if (userHandler) {
+ userHandler(warning, defaultHandler);
+ } else {
+ defaultHandler(warning);
+ }
+ },
+ };
+
return {
build: {
cssMinify: viteUserConfig.build?.cssMinify ?? true,
manifest: true, // The manifest is enabled for all builds to detect SSR-only assets
- rollupOptions: {
- preserveEntrySignatures: "exports-only",
- // Silence Rollup "use client" warnings
- // Adapted from https://github.com/vitejs/vite-plugin-react/pull/144
- onwarn(warning, defaultHandler) {
- if (
- warning.code === "MODULE_LEVEL_DIRECTIVE" &&
- warning.message.includes("use client")
- ) {
- return;
- }
- let userHandler = viteUserConfig.build?.rollupOptions?.onwarn;
- if (userHandler) {
- userHandler(warning, defaultHandler);
- } else {
- defaultHandler(warning);
- }
- },
- },
+ rollupOptions,
},
};
}
diff --git a/packages/react-router-dom/CHANGELOG.md b/packages/react-router-dom/CHANGELOG.md
index 3609e587e4..686451a0dc 100644
--- a/packages/react-router-dom/CHANGELOG.md
+++ b/packages/react-router-dom/CHANGELOG.md
@@ -1,5 +1,12 @@
# react-router-dom
+## 7.5.1
+
+### Patch Changes
+
+- Updated dependencies:
+ - `react-router@7.5.1`
+
## 7.5.0
### Patch Changes
diff --git a/packages/react-router-dom/package.json b/packages/react-router-dom/package.json
index 5526d99435..2ad165215b 100644
--- a/packages/react-router-dom/package.json
+++ b/packages/react-router-dom/package.json
@@ -1,6 +1,6 @@
{
"name": "react-router-dom",
- "version": "7.5.0",
+ "version": "7.5.1",
"description": "Declarative routing for React web applications",
"keywords": [
"react",
diff --git a/packages/react-router-express/CHANGELOG.md b/packages/react-router-express/CHANGELOG.md
index cc01059c42..7e4dd0cb93 100644
--- a/packages/react-router-express/CHANGELOG.md
+++ b/packages/react-router-express/CHANGELOG.md
@@ -1,5 +1,13 @@
# `@react-router/express`
+## 7.5.1
+
+### Patch Changes
+
+- Updated dependencies:
+ - `react-router@7.5.1`
+ - `@react-router/node@7.5.1`
+
## 7.5.0
### Patch Changes
diff --git a/packages/react-router-express/package.json b/packages/react-router-express/package.json
index 1daecbcba3..6d619406d8 100644
--- a/packages/react-router-express/package.json
+++ b/packages/react-router-express/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-router/express",
- "version": "7.5.0",
+ "version": "7.5.1",
"description": "Express server request handler for React Router",
"bugs": {
"url": "https://github.com/remix-run/react-router/issues"
diff --git a/packages/react-router-fs-routes/CHANGELOG.md b/packages/react-router-fs-routes/CHANGELOG.md
index f7b9bfad12..a951bf9172 100644
--- a/packages/react-router-fs-routes/CHANGELOG.md
+++ b/packages/react-router-fs-routes/CHANGELOG.md
@@ -1,5 +1,12 @@
# `@react-router/fs-routes`
+## 7.5.1
+
+### Patch Changes
+
+- Updated dependencies:
+ - `@react-router/dev@7.5.1`
+
## 7.5.0
### Patch Changes
diff --git a/packages/react-router-fs-routes/package.json b/packages/react-router-fs-routes/package.json
index 4080e7d819..2a6d3d8cf0 100644
--- a/packages/react-router-fs-routes/package.json
+++ b/packages/react-router-fs-routes/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-router/fs-routes",
- "version": "7.5.0",
+ "version": "7.5.1",
"description": "File system routing conventions for React Router, for use within routes.ts",
"bugs": {
"url": "https://github.com/remix-run/react-router/issues"
diff --git a/packages/react-router-node/CHANGELOG.md b/packages/react-router-node/CHANGELOG.md
index 4833d1984f..6c94b7bf42 100644
--- a/packages/react-router-node/CHANGELOG.md
+++ b/packages/react-router-node/CHANGELOG.md
@@ -1,5 +1,12 @@
# `@react-router/node`
+## 7.5.1
+
+### Patch Changes
+
+- Updated dependencies:
+ - `react-router@7.5.1`
+
## 7.5.0
### Patch Changes
diff --git a/packages/react-router-node/package.json b/packages/react-router-node/package.json
index 7971c7196a..1148620eb7 100644
--- a/packages/react-router-node/package.json
+++ b/packages/react-router-node/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-router/node",
- "version": "7.5.0",
+ "version": "7.5.1",
"description": "Node.js platform abstractions for React Router",
"bugs": {
"url": "https://github.com/remix-run/react-router/issues"
diff --git a/packages/react-router-remix-routes-option-adapter/CHANGELOG.md b/packages/react-router-remix-routes-option-adapter/CHANGELOG.md
index f3886ffc33..aab264a2a7 100644
--- a/packages/react-router-remix-routes-option-adapter/CHANGELOG.md
+++ b/packages/react-router-remix-routes-option-adapter/CHANGELOG.md
@@ -1,5 +1,12 @@
# `@react-router/remix-config-routes-adapter`
+## 7.5.1
+
+### Patch Changes
+
+- Updated dependencies:
+ - `@react-router/dev@7.5.1`
+
## 7.5.0
### Patch Changes
diff --git a/packages/react-router-remix-routes-option-adapter/package.json b/packages/react-router-remix-routes-option-adapter/package.json
index 46152b4126..f0ae5a27f3 100644
--- a/packages/react-router-remix-routes-option-adapter/package.json
+++ b/packages/react-router-remix-routes-option-adapter/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-router/remix-routes-option-adapter",
- "version": "7.5.0",
+ "version": "7.5.1",
"description": "Adapter for Remix's \"routes\" config option, for use within routes.ts",
"bugs": {
"url": "https://github.com/remix-run/react-router/issues"
diff --git a/packages/react-router-serve/CHANGELOG.md b/packages/react-router-serve/CHANGELOG.md
index a4e8235eec..28b5e311e7 100644
--- a/packages/react-router-serve/CHANGELOG.md
+++ b/packages/react-router-serve/CHANGELOG.md
@@ -1,5 +1,14 @@
# `@react-router/serve`
+## 7.5.1
+
+### Patch Changes
+
+- Updated dependencies:
+ - `react-router@7.5.1`
+ - `@react-router/node@7.5.1`
+ - `@react-router/express@7.5.1`
+
## 7.5.0
### Patch Changes
diff --git a/packages/react-router-serve/package.json b/packages/react-router-serve/package.json
index 7dead65971..626f1a8d35 100644
--- a/packages/react-router-serve/package.json
+++ b/packages/react-router-serve/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-router/serve",
- "version": "7.5.0",
+ "version": "7.5.1",
"description": "Production application server for React Router",
"bugs": {
"url": "https://github.com/remix-run/react-router/issues"
diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md
index 0a70cb7365..521ca6cb52 100644
--- a/packages/react-router/CHANGELOG.md
+++ b/packages/react-router/CHANGELOG.md
@@ -1,5 +1,47 @@
# `react-router`
+## 7.5.1
+
+### Patch Changes
+
+- Fix single fetch bug where no revalidation request would be made when navigating upwards to a reused parent route ([#13253](https://github.com/remix-run/react-router/pull/13253))
+
+- When using the object-based `route.lazy` API, the `HydrateFallback` and `hydrateFallbackElement` properties are now skipped when lazy loading routes after hydration. ([#13376](https://github.com/remix-run/react-router/pull/13376))
+
+ If you move the code for these properties into a separate file, you can use this optimization to avoid downloading unused hydration code. For example:
+
+ ```ts
+ createBrowserRouter([
+ {
+ path: "/show/:showId",
+ lazy: {
+ loader: async () => (await import("./show.loader.js")).loader,
+ Component: async () => (await import("./show.component.js")).Component,
+ HydrateFallback: async () =>
+ (await import("./show.hydrate-fallback.js")).HydrateFallback,
+ },
+ },
+ ]);
+ ```
+
+- Properly revalidate prerendered paths when param values change ([#13380](https://github.com/remix-run/react-router/pull/13380))
+
+- UNSTABLE: Add a new `unstable_runClientMiddleware` argument to `dataStrategy` to enable middleware execution in custom `dataStrategy` implementations ([#13395](https://github.com/remix-run/react-router/pull/13395))
+
+- UNSTABLE: Add better error messaging when `getLoadContext` is not updated to return a `Map`" ([#13242](https://github.com/remix-run/react-router/pull/13242))
+
+- Do not automatically add `null` to `staticHandler.query()` `context.loaderData` if routes do not have loaders ([#13223](https://github.com/remix-run/react-router/pull/13223))
+
+ - This was a Remix v2 implementation detail inadvertently left in for React Router v7
+ - Now that we allow returning `undefined` from loaders, our prior check of `loaderData[routeId] !== undefined` was no longer sufficient and was changed to a `routeId in loaderData` check - these `null` values can cause issues for this new check
+ - ⚠️ This could be a "breaking bug fix" for you if you are doing manual SSR with `createStaticHandler()`/`
`, and using `context.loaderData` to control `` hydration behavior on the client
+
+- Fix prerendering when a loader returns a redirect ([#13365](https://github.com/remix-run/react-router/pull/13365))
+
+- UNSTABLE: Update context type for `LoaderFunctionArgs`/`ActionFunctionArgs` when middleware is enabled ([#13381](https://github.com/remix-run/react-router/pull/13381))
+
+- Add support for the new `unstable_shouldCallHandler`/`unstable_shouldRevalidateArgs` APIs in `dataStrategy` ([#13253](https://github.com/remix-run/react-router/pull/13253))
+
## 7.5.0
### Minor Changes
diff --git a/packages/react-router/__tests__/dom/data-static-router-test.tsx b/packages/react-router/__tests__/dom/data-static-router-test.tsx
index 9a738d1a90..e7a7a2f6cb 100644
--- a/packages/react-router/__tests__/dom/data-static-router-test.tsx
+++ b/packages/react-router/__tests__/dom/data-static-router-test.tsx
@@ -901,10 +901,7 @@ describe("A ", () => {
let expectedJsonString = JSON.stringify(
JSON.stringify({
- loaderData: {
- 0: null,
- "0-0": null,
- },
+ loaderData: {},
actionData: null,
errors: null,
})
diff --git a/packages/react-router/__tests__/router/data-strategy-test.ts b/packages/react-router/__tests__/router/data-strategy-test.ts
index 4ef1cd2b98..eb385fe4ae 100644
--- a/packages/react-router/__tests__/router/data-strategy-test.ts
+++ b/packages/react-router/__tests__/router/data-strategy-test.ts
@@ -5,7 +5,7 @@ import type {
} from "../../lib/router/utils";
import {
createDeferred,
- createLazyStub,
+ createAsyncStub,
setup,
} from "./utils/data-router-setup";
import { createFormData, tick } from "./utils/utils";
@@ -99,10 +99,8 @@ describe("router dataStrategy", () => {
keyedResults(matches, results)
)
);
- let { lazyStub: lazyJsonStub, lazyDeferred: lazyJsonDeferred } =
- createLazyStub();
- let { lazyStub: lazyTextStub, lazyDeferred: lazyTextDeferred } =
- createLazyStub();
+ let [lazyJson, lazyJsonDeferred] = createAsyncStub();
+ let [lazyText, lazyTextDeferred] = createAsyncStub();
let t = setup({
routes: [
{
@@ -111,12 +109,12 @@ describe("router dataStrategy", () => {
{
id: "json",
path: "/test",
- lazy: lazyJsonStub,
+ lazy: lazyJson,
children: [
{
id: "text",
index: true,
- lazy: lazyTextStub,
+ lazy: lazyText,
},
],
},
@@ -219,7 +217,7 @@ describe("router dataStrategy", () => {
});
it("should allow custom implementations to override default behavior with lazy", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazy, lazyDeferred] = createAsyncStub();
let t = setup({
routes: [
{
@@ -228,7 +226,7 @@ describe("router dataStrategy", () => {
{
id: "test",
path: "/test",
- lazy: lazyStub,
+ lazy,
},
],
async dataStrategy({ matches }) {
@@ -374,7 +372,7 @@ describe("router dataStrategy", () => {
});
it("does not require resolve to be called if a match is not being loaded", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazy, lazyDeferred] = createAsyncStub();
let t = setup({
routes: [
{
@@ -389,7 +387,7 @@ describe("router dataStrategy", () => {
{
id: "child",
path: "child",
- lazy: lazyStub,
+ lazy,
},
],
},
@@ -451,7 +449,7 @@ describe("router dataStrategy", () => {
keyedResults(matches, results)
);
});
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazy, lazyDeferred] = createAsyncStub();
let t = setup({
routes: [
{
@@ -468,7 +466,7 @@ describe("router dataStrategy", () => {
{
id: "child",
path: "child",
- lazy: lazyStub,
+ lazy,
},
],
},
@@ -579,6 +577,92 @@ describe("router dataStrategy", () => {
child: "CHILD",
});
});
+
+ it("does not short circuit when there are no matchesToLoad", async () => {
+ let dataStrategy = mockDataStrategy(async ({ matches }) => {
+ let results = await Promise.all(
+ matches.map((m) => m.resolve((handler) => handler()))
+ );
+ // Don't use keyedResults since it checks for shouldLoad and this test
+ // is always loading
+ return results.reduce(
+ (acc, r, i) => Object.assign(acc, { [matches[i].route.id]: r }),
+ {}
+ );
+ });
+ let t = setup({
+ routes: [
+ {
+ path: "/",
+ },
+ {
+ id: "parent",
+ path: "/parent",
+ loader: true,
+ children: [
+ {
+ id: "child",
+ path: "child",
+ loader: true,
+ },
+ ],
+ },
+ ],
+ dataStrategy,
+ });
+
+ let A = await t.navigate("/parent");
+ await A.loaders.parent.resolve("PARENT1");
+ expect(A.loaders.parent.stub).toHaveBeenCalled();
+ expect(t.router.state.loaderData).toEqual({
+ parent: "PARENT1",
+ });
+ expect(dataStrategy.mock.calls[0][0].matches).toEqual([
+ expect.objectContaining({
+ route: expect.objectContaining({
+ id: "parent",
+ }),
+ }),
+ ]);
+
+ let B = await t.navigate("/parent/child");
+ await B.loaders.parent.resolve("PARENT2");
+ await B.loaders.child.resolve("CHILD");
+ expect(B.loaders.parent.stub).toHaveBeenCalled();
+ expect(B.loaders.child.stub).toHaveBeenCalled();
+ expect(t.router.state.loaderData).toEqual({
+ parent: "PARENT2",
+ child: "CHILD",
+ });
+ expect(dataStrategy.mock.calls[1][0].matches).toEqual([
+ expect.objectContaining({
+ route: expect.objectContaining({
+ id: "parent",
+ }),
+ }),
+ expect.objectContaining({
+ route: expect.objectContaining({
+ id: "child",
+ }),
+ }),
+ ]);
+
+ let C = await t.navigate("/parent");
+ await C.loaders.parent.resolve("PARENT3");
+ expect(C.loaders.parent.stub).toHaveBeenCalled();
+ expect(t.router.state.loaderData).toEqual({
+ parent: "PARENT3",
+ });
+ expect(dataStrategy.mock.calls[2][0].matches).toEqual([
+ expect.objectContaining({
+ route: expect.objectContaining({
+ id: "parent",
+ }),
+ }),
+ ]);
+
+ expect(dataStrategy).toHaveBeenCalledTimes(3);
+ });
});
describe("actions", () => {
@@ -632,7 +716,7 @@ describe("router dataStrategy", () => {
keyedResults(matches, results)
)
);
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazy, lazyDeferred] = createAsyncStub();
let t = setup({
routes: [
{
@@ -641,7 +725,7 @@ describe("router dataStrategy", () => {
{
id: "json",
path: "/test",
- lazy: lazyStub,
+ lazy,
},
],
dataStrategy,
@@ -721,7 +805,7 @@ describe("router dataStrategy", () => {
keyedResults(matches, results)
)
);
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazy, lazyDeferred] = createAsyncStub();
let t = setup({
routes: [
{
@@ -730,7 +814,7 @@ describe("router dataStrategy", () => {
{
id: "json",
path: "/test",
- lazy: lazyStub,
+ lazy,
},
],
dataStrategy,
@@ -808,7 +892,7 @@ describe("router dataStrategy", () => {
keyedResults(matches, results)
)
);
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazy, lazyDeferred] = createAsyncStub();
let t = setup({
routes: [
{
@@ -817,7 +901,7 @@ describe("router dataStrategy", () => {
{
id: "json",
path: "/test",
- lazy: lazyStub,
+ lazy,
},
],
dataStrategy,
diff --git a/packages/react-router/__tests__/router/lazy-test.ts b/packages/react-router/__tests__/router/lazy-test.ts
index 00d931ea55..cc6dcfa7ce 100644
--- a/packages/react-router/__tests__/router/lazy-test.ts
+++ b/packages/react-router/__tests__/router/lazy-test.ts
@@ -1,5 +1,9 @@
import { createMemoryHistory } from "../../lib/router/history";
import { createRouter, createStaticHandler } from "../../lib/router/router";
+import {
+ createMemoryRouter,
+ hydrationRouteProperties,
+} from "../../lib/components";
import type {
TestNonIndexRouteObject,
@@ -8,7 +12,7 @@ import type {
import {
cleanup,
createDeferred,
- createLazyStub,
+ createAsyncStub,
setup,
} from "./utils/data-router-setup";
import {
@@ -52,13 +56,13 @@ describe("lazily loaded route modules", () => {
const createBasicLazyFunctionRoutes = (): {
routes: TestRouteObject[];
- lazyStub: jest.Mock;
+ lazy: jest.Mock;
lazyDeferred: ReturnType;
} => {
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazy, lazyDeferred] = createAsyncStub();
return {
- routes: createBasicLazyRoutes(lazyStub),
- lazyStub,
+ routes: createBasicLazyRoutes(lazy),
+ lazy,
lazyDeferred,
};
};
@@ -162,7 +166,7 @@ describe("lazily loaded route modules", () => {
it("ignores and warns on unsupported lazy route function properties on router initialization", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let lazyLoaderDeferred = createDeferred();
+ let loaderDeferred = createDeferred();
let router = createRouter({
routes: [
{
@@ -170,7 +174,7 @@ describe("lazily loaded route modules", () => {
// @ts-expect-error
lazy: async () => {
return {
- loader: () => lazyLoaderDeferred.promise,
+ loader: () => loaderDeferred.promise,
lazy: async () => {
throw new Error("SHOULD NOT BE CALLED");
},
@@ -191,7 +195,7 @@ describe("lazily loaded route modules", () => {
router.initialize();
let LOADER_DATA = 123;
- await lazyLoaderDeferred.resolve(LOADER_DATA);
+ await loaderDeferred.resolve(LOADER_DATA);
expect(router.state.location.pathname).toBe("/lazy");
expect(router.state.navigation.state).toBe("idle");
@@ -216,13 +220,13 @@ describe("lazily loaded route modules", () => {
it("ignores and warns on unsupported lazy route properties on router initialization", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let lazyLoaderDeferred = createDeferred();
+ let loaderDeferred = createDeferred();
let router = createRouter({
routes: [
{
path: "/lazy",
lazy: {
- loader: () => lazyLoaderDeferred.promise,
+ loader: () => loaderDeferred.promise,
// @ts-expect-error
lazy: async () => {
throw new Error("SHOULD NOT BE CALLED");
@@ -244,7 +248,7 @@ describe("lazily loaded route modules", () => {
let LOADER_DATA = 123;
let loader = () => LOADER_DATA;
- await lazyLoaderDeferred.resolve(loader);
+ await loaderDeferred.resolve(loader);
expect(router.state.location.pathname).toBe("/lazy");
expect(router.state.navigation.state).toBe("idle");
@@ -323,7 +327,7 @@ describe("lazily loaded route modules", () => {
router.initialize();
// Ensure loader is called as soon as it's loaded
- let { lazyStub: loader, lazyDeferred: loaderDeferred } = createLazyStub();
+ let [loader, loaderDeferred] = createAsyncStub();
await lazyLoaderDeferred.resolve(loader);
expect(loader).toHaveBeenCalledTimes(1);
expect(router.state.initialized).toBe(false);
@@ -346,14 +350,14 @@ describe("lazily loaded route modules", () => {
describe("happy path", () => {
it("fetches lazy route functions on loading navigation", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
let loaderDeferred = createDeferred();
lazyDeferred.resolve({
@@ -361,7 +365,7 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
await loaderDeferred.resolve("LAZY LOADER");
@@ -370,14 +374,12 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.loaderData).toEqual({
lazy: "LAZY LOADER",
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("resolves lazy route properties on loading navigation", async () => {
- let { lazyStub: lazyLoader, lazyDeferred: lazyLoaderDeferred } =
- createLazyStub();
- let { lazyStub: lazyAction, lazyDeferred: lazyActionDeferred } =
- createLazyStub();
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
let routes = createBasicLazyRoutes({
loader: lazyLoader,
action: lazyAction,
@@ -391,7 +393,7 @@ describe("lazily loaded route modules", () => {
expect(lazyLoader).toHaveBeenCalledTimes(1);
// Ensure loader is called as soon as it's loaded
- let { lazyStub: loader, lazyDeferred: loaderDeferred } = createLazyStub();
+ let [loader, loaderDeferred] = createAsyncStub();
await lazyLoaderDeferred.resolve(loader);
expect(loader).toHaveBeenCalledTimes(1);
expect(t.router.state.location.pathname).toBe("/");
@@ -417,30 +419,28 @@ describe("lazily loaded route modules", () => {
});
it("ignores falsy lazy route properties on loading navigation", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
- let routes = createBasicLazyRoutes({
- loader: lazyStub,
- });
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let routes = createBasicLazyRoutes({ loader: lazyLoader });
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
- await lazyDeferred.resolve(null);
+ await lazyLoaderDeferred.resolve(null);
expect(t.router.state.matches[0].route.loader).toBeUndefined();
expect(t.router.state.location.pathname).toBe("/lazy");
expect(t.router.state.navigation.state).toBe("idle");
expect(t.router.state.loaderData).toEqual({});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
});
it("fetches lazy route functions on submission navigation", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
await t.navigate("/lazy", {
formMethod: "post",
@@ -448,7 +448,7 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
let actionDeferred = createDeferred();
let loaderDeferred = createDeferred();
@@ -477,21 +477,19 @@ describe("lazily loaded route modules", () => {
lazy: "LAZY LOADER",
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("resolves lazy route properties on submission navigation", async () => {
- let { lazyStub: lazyLoaderStub, lazyDeferred: lazyLoaderDeferred } =
- createLazyStub();
- let { lazyStub: lazyActionStub, lazyDeferred: lazyActionDeferred } =
- createLazyStub();
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
let routes = createBasicLazyRoutes({
- loader: lazyLoaderStub,
- action: lazyActionStub,
+ loader: lazyLoader,
+ action: lazyAction,
});
let t = setup({ routes });
- expect(lazyLoaderStub).not.toHaveBeenCalled();
- expect(lazyActionStub).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
+ expect(lazyAction).not.toHaveBeenCalled();
await t.navigate("/lazy", {
formMethod: "post",
@@ -499,11 +497,11 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
- expect(lazyActionStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
- let { lazyStub: action, lazyDeferred: actionDeferred } = createLazyStub();
- let { lazyStub: loader, lazyDeferred: loaderDeferred } = createLazyStub();
+ let [action, actionDeferred] = createAsyncStub();
+ let [loader, loaderDeferred] = createAsyncStub();
// Ensure action is called as soon as it's loaded
await lazyActionDeferred.resolve(action);
@@ -531,22 +529,20 @@ describe("lazily loaded route modules", () => {
lazy: "LAZY LOADER",
});
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
- expect(lazyActionStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
});
it("ignores falsy lazy route properties on submission navigation", async () => {
- let { lazyStub: lazyLoaderStub, lazyDeferred: lazyLoaderDeferred } =
- createLazyStub();
- let { lazyStub: lazyActionStub, lazyDeferred: lazyActionDeferred } =
- createLazyStub();
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
let routes = createBasicLazyRoutes({
- loader: lazyLoaderStub,
- action: lazyActionStub,
+ loader: lazyLoader,
+ action: lazyAction,
});
let t = setup({ routes });
- expect(lazyLoaderStub).not.toHaveBeenCalled();
- expect(lazyActionStub).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
+ expect(lazyAction).not.toHaveBeenCalled();
await t.navigate("/lazy", {
formMethod: "post",
@@ -554,8 +550,8 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
- expect(lazyActionStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
await lazyLoaderDeferred.resolve(undefined);
await lazyActionDeferred.resolve(null);
@@ -563,21 +559,84 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.navigation.state).toBe("idle");
expect(t.router.state.actionData).toEqual(null);
expect(t.router.state.loaderData).toEqual({});
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
- expect(lazyActionStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
expect(t.router.state.matches[0].route.loader).toBeUndefined();
expect(t.router.state.matches[0].route.action).toBeUndefined();
});
+ it("only resolves lazy hydration route properties on hydration", async () => {
+ let [lazyLoaderForHydration, lazyLoaderDeferredForHydration] =
+ createAsyncStub();
+ let [lazyLoaderForNavigation, lazyLoaderDeferredForNavigation] =
+ createAsyncStub();
+ let [
+ lazyHydrateFallbackForHydration,
+ lazyHydrateFallbackDeferredForHydration,
+ ] = createAsyncStub();
+ let [
+ lazyHydrateFallbackElementForHydration,
+ lazyHydrateFallbackElementDeferredForHydration,
+ ] = createAsyncStub();
+ let lazyHydrateFallbackForNavigation = jest.fn(async () => null);
+ let lazyHydrateFallbackElementForNavigation = jest.fn(async () => null);
+ let router = createMemoryRouter(
+ [
+ {
+ path: "/hydration",
+ lazy: {
+ HydrateFallback: lazyHydrateFallbackForHydration,
+ hydrateFallbackElement: lazyHydrateFallbackElementForHydration,
+ loader: lazyLoaderForHydration,
+ },
+ },
+ {
+ path: "/navigation",
+ lazy: {
+ HydrateFallback: lazyHydrateFallbackForNavigation,
+ hydrateFallbackElement: lazyHydrateFallbackElementForNavigation,
+ loader: lazyLoaderForNavigation,
+ },
+ },
+ ],
+ {
+ initialEntries: ["/hydration"],
+ }
+ );
+ expect(router.state.initialized).toBe(false);
+
+ expect(lazyHydrateFallbackForHydration).toHaveBeenCalledTimes(1);
+ expect(lazyHydrateFallbackElementForHydration).toHaveBeenCalledTimes(1);
+ expect(lazyLoaderForHydration).toHaveBeenCalledTimes(1);
+ await lazyHydrateFallbackDeferredForHydration.resolve(null);
+ await lazyHydrateFallbackElementDeferredForHydration.resolve(null);
+ await lazyLoaderDeferredForHydration.resolve(null);
+
+ expect(router.state.location.pathname).toBe("/hydration");
+ expect(router.state.navigation.state).toBe("idle");
+ expect(router.state.initialized).toBe(true);
+
+ let navigationPromise = router.navigate("/navigation");
+ expect(router.state.location.pathname).toBe("/hydration");
+ expect(router.state.navigation.state).toBe("loading");
+ expect(lazyHydrateFallbackForNavigation).not.toHaveBeenCalled();
+ expect(lazyHydrateFallbackElementForNavigation).not.toHaveBeenCalled();
+ expect(lazyLoaderForNavigation).toHaveBeenCalledTimes(1);
+ await lazyLoaderDeferredForNavigation.resolve(null);
+ await navigationPromise;
+ expect(router.state.location.pathname).toBe("/navigation");
+ expect(router.state.navigation.state).toBe("idle");
+ });
+
it("fetches lazy route functions on fetcher.load", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
let loaderDeferred = createDeferred();
lazyDeferred.resolve({
@@ -589,37 +648,69 @@ describe("lazily loaded route modules", () => {
expect(t.fetchers[key].state).toBe("idle");
expect(t.fetchers[key].data).toBe("LAZY LOADER");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("resolves lazy route properties on fetcher.load", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let routes = createBasicLazyRoutes({ loader: lazyLoader });
+ let t = setup({ routes });
+ expect(lazyLoader).not.toHaveBeenCalled();
+
+ let key = "key";
+ await t.fetch("http://github.com/lazy", key);
+ expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+
+ let loaderDeferred = createDeferred();
+ lazyLoaderDeferred.resolve(() => loaderDeferred.promise);
+ expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
+
+ await loaderDeferred.resolve("LAZY LOADER");
+ expect(t.fetchers[key].state).toBe("idle");
+ expect(t.fetchers[key].data).toBe("LAZY LOADER");
+
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+ });
+
+ it("skips lazy hydration route properties on fetcher.load", async () => {
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let lazyHydrateFallback = jest.fn(async () => null);
+ let lazyHydrateFallbackElement = jest.fn(async () => null);
let routes = createBasicLazyRoutes({
- loader: lazyStub,
+ loader: lazyLoader,
+ // @ts-expect-error
+ HydrateFallback: lazyHydrateFallback,
+ hydrateFallbackElement: lazyHydrateFallbackElement,
});
- let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ let t = setup({ routes, hydrationRouteProperties });
+ expect(lazyHydrateFallback).not.toHaveBeenCalled();
+ expect(lazyHydrateFallbackElement).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+ expect(lazyHydrateFallback).not.toHaveBeenCalled();
+ expect(lazyHydrateFallbackElement).not.toHaveBeenCalled();
let loaderDeferred = createDeferred();
- lazyDeferred.resolve(() => loaderDeferred.promise);
+ lazyLoaderDeferred.resolve(() => loaderDeferred.promise);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
await loaderDeferred.resolve("LAZY LOADER");
expect(t.fetchers[key].state).toBe("idle");
expect(t.fetchers[key].data).toBe("LAZY LOADER");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+ expect(lazyHydrateFallback).not.toHaveBeenCalled();
+ expect(lazyHydrateFallbackElement).not.toHaveBeenCalled();
});
it("fetches lazy route functions on fetcher.submit", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key, {
@@ -627,7 +718,7 @@ describe("lazily loaded route modules", () => {
formData: createFormData({}),
});
expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
let actionDeferred = createDeferred();
lazyDeferred.resolve({
@@ -639,19 +730,56 @@ describe("lazily loaded route modules", () => {
expect(t.fetchers[key]?.state).toBe("idle");
expect(t.fetchers[key]?.data).toBe("LAZY ACTION");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("resolves lazy route properties on fetcher.submit", async () => {
- let { lazyStub: lazyLoaderStub, lazyDeferred: lazyLoaderDeferred } =
- createLazyStub();
- let { lazyStub: lazyActionStub, lazyDeferred: lazyActionDeferred } =
- createLazyStub();
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
+ let routes = createBasicLazyRoutes({
+ loader: lazyLoader,
+ action: lazyAction,
+ });
+ let t = setup({ routes });
+ expect(lazyLoader).not.toHaveBeenCalled();
+ expect(lazyAction).not.toHaveBeenCalled();
+
+ let key = "key";
+ await t.fetch("http://github.com/lazy", key, {
+ formMethod: "post",
+ formData: createFormData({}),
+ });
+ expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
+
+ let actionDeferred = createDeferred();
+ let loaderDeferred = createDeferred();
+ lazyLoaderDeferred.resolve(() => loaderDeferred.promise);
+ lazyActionDeferred.resolve(() => actionDeferred.promise);
+ expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
+
+ await actionDeferred.resolve("LAZY ACTION");
+ expect(t.fetchers[key]?.state).toBe("idle");
+ expect(t.fetchers[key]?.data).toBe("LAZY ACTION");
+
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
+ });
+
+ it("skips lazy hydration route properties on fetcher.submit", async () => {
+ let [lazyLoaderStub, lazyLoaderDeferred] = createAsyncStub();
+ let [lazyActionStub, lazyActionDeferred] = createAsyncStub();
+ let lazyHydrateFallback = jest.fn(async () => null);
+ let lazyHydrateFallbackElement = jest.fn(async () => null);
let routes = createBasicLazyRoutes({
loader: lazyLoaderStub,
action: lazyActionStub,
+ // @ts-expect-error
+ HydrateFallback: lazyHydrateFallback,
+ hydrateFallbackElement: lazyHydrateFallbackElement,
});
- let t = setup({ routes });
+ let t = setup({ routes, hydrationRouteProperties });
expect(lazyLoaderStub).not.toHaveBeenCalled();
expect(lazyActionStub).not.toHaveBeenCalled();
@@ -663,6 +791,8 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
expect(lazyActionStub).toHaveBeenCalledTimes(1);
+ expect(lazyHydrateFallback).not.toHaveBeenCalled();
+ expect(lazyHydrateFallbackElement).not.toHaveBeenCalled();
let actionDeferred = createDeferred();
let loaderDeferred = createDeferred();
@@ -676,6 +806,8 @@ describe("lazily loaded route modules", () => {
expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
expect(lazyActionStub).toHaveBeenCalledTimes(1);
+ expect(lazyHydrateFallback).not.toHaveBeenCalled();
+ expect(lazyHydrateFallbackElement).not.toHaveBeenCalled();
});
it("fetches lazy route functions on staticHandler.query()", async () => {
@@ -763,19 +895,48 @@ describe("lazily loaded route modules", () => {
let data = await response.json();
expect(data).toEqual({ value: "LAZY LOADER" });
});
+
+ it("resolves lazy hydration route properties on staticHandler.queryRoute()", async () => {
+ let lazyHydrateFallback = jest.fn(async () => null);
+ let lazyHydrateFallbackElement = jest.fn(async () => null);
+ let { queryRoute } = createStaticHandler(
+ [
+ {
+ id: "lazy",
+ path: "/lazy",
+ lazy: {
+ loader: async () => {
+ await tick();
+ return () => Response.json({ value: "LAZY LOADER" });
+ },
+ // @ts-expect-error
+ HydrateFallback: lazyHydrateFallback,
+ hydrateFallbackElement: lazyHydrateFallbackElement,
+ },
+ },
+ ],
+ { hydrationRouteProperties }
+ );
+
+ let response = await queryRoute(createRequest("/lazy"));
+ let data = await response.json();
+ expect(data).toEqual({ value: "LAZY LOADER" });
+ expect(lazyHydrateFallback).toHaveBeenCalled();
+ expect(lazyHydrateFallbackElement).toHaveBeenCalled();
+ });
});
describe("statically defined fields", () => {
it("prefers statically defined loader over lazily defined loader via lazy function", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazy, lazyDeferred] = createAsyncStub();
let t = setup({
routes: [
{
id: "lazy",
path: "/lazy",
loader: true,
- lazy: lazyStub,
+ lazy,
},
],
});
@@ -785,12 +946,10 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.navigation.state).toBe("loading");
// Execute in parallel
expect(A.loaders.lazy.stub).toHaveBeenCalled();
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
- let lazyLoaderStub = jest.fn(() => "LAZY LOADER");
- lazyDeferred.resolve({
- loader: lazyLoaderStub,
- });
+ let loader = jest.fn(() => "LAZY LOADER");
+ lazyDeferred.resolve({ loader });
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
@@ -801,12 +960,12 @@ describe("lazily loaded route modules", () => {
lazy: "STATIC LOADER",
});
- let lazyRoute = findRouteById(t.router.routes, "lazy");
- expect(lazyRoute.lazy).toBeUndefined();
- expect(lazyRoute.loader).toEqual(expect.any(Function));
- expect(lazyRoute.loader).not.toBe(lazyLoaderStub);
- expect(lazyLoaderStub).not.toHaveBeenCalled();
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ let route = findRouteById(t.router.routes, "lazy");
+ expect(route.lazy).toBeUndefined();
+ expect(route.loader).toEqual(expect.any(Function));
+ expect(route.loader).not.toBe(loader);
+ expect(loader).not.toHaveBeenCalled();
+ expect(lazy).toHaveBeenCalledTimes(1);
expect(consoleWarn).toHaveBeenCalledTimes(1);
expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(
@@ -817,7 +976,7 @@ describe("lazily loaded route modules", () => {
it("prefers statically defined loader over lazily defined loader via lazy property", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
let t = setup({
routes: [
{
@@ -825,7 +984,7 @@ describe("lazily loaded route modules", () => {
path: "/lazy",
loader: true,
lazy: {
- loader: lazyStub,
+ loader: lazyLoader,
},
},
],
@@ -836,10 +995,10 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.navigation.state).toBe("loading");
// Execute in parallel
expect(A.loaders.lazy.stub).toHaveBeenCalled();
- expect(lazyStub).toHaveBeenCalledTimes(0);
+ expect(lazyLoader).toHaveBeenCalledTimes(0);
- let lazyLoaderStub = jest.fn(() => "LAZY LOADER");
- lazyDeferred.resolve(lazyLoaderStub);
+ let loader = jest.fn(() => "LAZY LOADER");
+ lazyLoaderDeferred.resolve(loader);
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
@@ -850,12 +1009,12 @@ describe("lazily loaded route modules", () => {
lazy: "STATIC LOADER",
});
- let lazyRoute = findRouteById(t.router.routes, "lazy");
- expect(lazyRoute.lazy).toBeUndefined();
- expect(lazyRoute.loader).toEqual(expect.any(Function));
- expect(lazyRoute.loader).not.toBe(lazyLoaderStub);
- expect(lazyLoaderStub).not.toHaveBeenCalled();
- expect(lazyStub).toHaveBeenCalledTimes(0);
+ let route = findRouteById(t.router.routes, "lazy");
+ expect(route.lazy).toBeUndefined();
+ expect(route.loader).toEqual(expect.any(Function));
+ expect(route.loader).not.toBe(loader);
+ expect(loader).not.toHaveBeenCalled();
+ expect(lazyLoader).toHaveBeenCalledTimes(0);
expect(consoleWarn).toHaveBeenCalledTimes(1);
expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(
@@ -866,7 +1025,7 @@ describe("lazily loaded route modules", () => {
it("prefers statically defined loader over lazily defined falsy loader via lazy property", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
let t = setup({
routes: [
{
@@ -874,7 +1033,7 @@ describe("lazily loaded route modules", () => {
path: "/lazy",
loader: true,
lazy: {
- loader: lazyStub,
+ loader: lazyLoader,
},
},
],
@@ -885,9 +1044,9 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.navigation.state).toBe("loading");
// Execute in parallel
expect(A.loaders.lazy.stub).toHaveBeenCalled();
- expect(lazyStub).toHaveBeenCalledTimes(0);
+ expect(lazyLoader).toHaveBeenCalledTimes(0);
- lazyDeferred.resolve(null);
+ lazyLoaderDeferred.resolve(null);
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
@@ -898,11 +1057,11 @@ describe("lazily loaded route modules", () => {
lazy: "STATIC LOADER",
});
- let lazyRoute = findRouteById(t.router.routes, "lazy");
- expect(lazyRoute.lazy).toBeUndefined();
- expect(lazyRoute.loader).toEqual(expect.any(Function));
- expect(lazyRoute.loader).toBeInstanceOf(Function);
- expect(lazyStub).toHaveBeenCalledTimes(0);
+ let route = findRouteById(t.router.routes, "lazy");
+ expect(route.lazy).toBeUndefined();
+ expect(route.loader).toEqual(expect.any(Function));
+ expect(route.loader).toBeInstanceOf(Function);
+ expect(lazyLoader).toHaveBeenCalledTimes(0);
expect(consoleWarn).toHaveBeenCalledTimes(1);
expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(
@@ -913,14 +1072,14 @@ describe("lazily loaded route modules", () => {
it("prefers statically defined action over lazily loaded action via lazy function", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazy, lazyDeferred] = createAsyncStub();
let t = setup({
routes: [
{
id: "lazy",
path: "/lazy",
action: true,
- lazy: lazyStub,
+ lazy,
},
],
});
@@ -933,12 +1092,12 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.navigation.state).toBe("submitting");
// Execute in parallel
expect(A.actions.lazy.stub).toHaveBeenCalled();
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
- let lazyActionStub = jest.fn(() => "LAZY ACTION");
+ let lazyAction = jest.fn(() => "LAZY ACTION");
let loaderDeferred = createDeferred();
await lazyDeferred.resolve({
- action: lazyActionStub,
+ action: lazyAction,
loader: () => loaderDeferred.promise,
});
expect(t.router.state.location.pathname).toBe("/");
@@ -962,12 +1121,12 @@ describe("lazily loaded route modules", () => {
lazy: "LAZY LOADER",
});
- let lazyRoute = findRouteById(t.router.routes, "lazy");
- expect(lazyRoute.lazy).toBeUndefined();
- expect(lazyRoute.action).toEqual(expect.any(Function));
- expect(lazyRoute.action).not.toBe(lazyActionStub);
- expect(lazyActionStub).not.toHaveBeenCalled();
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ let route = findRouteById(t.router.routes, "lazy");
+ expect(route.lazy).toBeUndefined();
+ expect(route.action).toEqual(expect.any(Function));
+ expect(route.action).not.toBe(lazyAction);
+ expect(lazyAction).not.toHaveBeenCalled();
+ expect(lazy).toHaveBeenCalledTimes(1);
expect(consoleWarn).toHaveBeenCalledTimes(1);
expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(
@@ -978,10 +1137,8 @@ describe("lazily loaded route modules", () => {
it("prefers statically defined action over lazily loaded action via lazy property", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let { lazyStub: lazyLoaderStub, lazyDeferred: lazyLoaderDeferred } =
- createLazyStub();
- let { lazyStub: lazyActionStub, lazyDeferred: lazyActionDeferred } =
- createLazyStub();
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
let t = setup({
routes: [
{
@@ -989,8 +1146,8 @@ describe("lazily loaded route modules", () => {
path: "/lazy",
action: true,
lazy: {
- action: lazyActionStub,
- loader: lazyLoaderStub,
+ action: lazyAction,
+ loader: lazyLoader,
},
},
],
@@ -1004,12 +1161,12 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.navigation.state).toBe("submitting");
// Execute in parallel
expect(A.actions.lazy.stub).toHaveBeenCalled();
- expect(lazyActionStub).toHaveBeenCalledTimes(0);
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(0);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
- let actionStub = jest.fn(() => "LAZY ACTION");
+ let action = jest.fn(() => "LAZY ACTION");
let loaderDeferred = createDeferred();
- lazyActionDeferred.resolve(actionStub);
+ lazyActionDeferred.resolve(action);
lazyLoaderDeferred.resolve(() => loaderDeferred.promise);
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
@@ -1032,13 +1189,13 @@ describe("lazily loaded route modules", () => {
lazy: "LAZY LOADER",
});
- let lazyRoute = findRouteById(t.router.routes, "lazy");
- expect(lazyRoute.lazy).toBeUndefined();
- expect(lazyRoute.action).toEqual(expect.any(Function));
- expect(lazyRoute.action).not.toBe(actionStub);
- expect(actionStub).not.toHaveBeenCalled();
- expect(lazyActionStub).toHaveBeenCalledTimes(0);
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
+ let route = findRouteById(t.router.routes, "lazy");
+ expect(route.lazy).toBeUndefined();
+ expect(route.action).toEqual(expect.any(Function));
+ expect(route.action).not.toBe(action);
+ expect(action).not.toHaveBeenCalled();
+ expect(lazyAction).toHaveBeenCalledTimes(0);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
expect(consoleWarn).toHaveBeenCalledTimes(1);
expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(
@@ -1049,7 +1206,7 @@ describe("lazily loaded route modules", () => {
it("prefers statically defined action/loader over lazily defined action/loader via lazy function", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazy, lazyDeferred] = createAsyncStub();
let t = setup({
routes: [
{
@@ -1057,7 +1214,7 @@ describe("lazily loaded route modules", () => {
path: "/lazy",
action: true,
loader: true,
- lazy: lazyStub,
+ lazy,
},
],
});
@@ -1068,14 +1225,11 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
- let lazyActionStub = jest.fn(() => "LAZY ACTION");
- let lazyLoaderStub = jest.fn(() => "LAZY LOADER");
- await lazyDeferred.resolve({
- action: lazyActionStub,
- loader: lazyLoaderStub,
- });
+ let action = jest.fn(() => "LAZY ACTION");
+ let loader = jest.fn(() => "LAZY LOADER");
+ await lazyDeferred.resolve({ action, loader });
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
@@ -1097,15 +1251,15 @@ describe("lazily loaded route modules", () => {
lazy: "STATIC LOADER",
});
- let lazyRoute = findRouteById(t.router.routes, "lazy");
- expect(lazyRoute.lazy).toBeUndefined();
- expect(lazyRoute.action).toEqual(expect.any(Function));
- expect(lazyRoute.loader).toEqual(expect.any(Function));
- expect(lazyRoute.action).not.toBe(lazyActionStub);
- expect(lazyRoute.loader).not.toBe(lazyLoaderStub);
- expect(lazyActionStub).not.toHaveBeenCalled();
- expect(lazyLoaderStub).not.toHaveBeenCalled();
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ let route = findRouteById(t.router.routes, "lazy");
+ expect(route.lazy).toBeUndefined();
+ expect(route.action).toEqual(expect.any(Function));
+ expect(route.loader).toEqual(expect.any(Function));
+ expect(route.action).not.toBe(action);
+ expect(route.loader).not.toBe(loader);
+ expect(action).not.toHaveBeenCalled();
+ expect(loader).not.toHaveBeenCalled();
+ expect(lazy).toHaveBeenCalledTimes(1);
expect(consoleWarn).toHaveBeenCalledTimes(2);
expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(
@@ -1119,10 +1273,8 @@ describe("lazily loaded route modules", () => {
it("prefers statically defined action/loader over lazily defined action/loader via lazy property", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let { lazyStub: lazyActionStub, lazyDeferred: lazyActionDeferred } =
- createLazyStub();
- let { lazyStub: lazyLoaderStub, lazyDeferred: lazyLoaderDeferred } =
- createLazyStub();
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
let t = setup({
routes: [
{
@@ -1131,8 +1283,8 @@ describe("lazily loaded route modules", () => {
action: true,
loader: true,
lazy: {
- action: lazyActionStub,
- loader: lazyLoaderStub,
+ action: lazyAction,
+ loader: lazyLoader,
},
},
],
@@ -1144,13 +1296,13 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyLoaderStub).toHaveBeenCalledTimes(0);
+ expect(lazyLoader).toHaveBeenCalledTimes(0);
- expect(lazyActionStub).toHaveBeenCalledTimes(0);
- let actionStub = jest.fn(() => "LAZY ACTION");
- let loaderStub = jest.fn(() => "LAZY LOADER");
- lazyActionDeferred.resolve(actionStub);
- lazyLoaderDeferred.resolve(loaderStub);
+ expect(lazyAction).toHaveBeenCalledTimes(0);
+ let action = jest.fn(() => "LAZY ACTION");
+ let loader = jest.fn(() => "LAZY LOADER");
+ lazyActionDeferred.resolve(action);
+ lazyLoaderDeferred.resolve(loader);
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
@@ -1172,16 +1324,16 @@ describe("lazily loaded route modules", () => {
lazy: "STATIC LOADER",
});
- let lazyRoute = findRouteById(t.router.routes, "lazy");
- expect(lazyRoute.lazy).toBeUndefined();
- expect(lazyRoute.action).toEqual(expect.any(Function));
- expect(lazyRoute.loader).toEqual(expect.any(Function));
- expect(lazyRoute.action).not.toBe(actionStub);
- expect(lazyRoute.loader).not.toBe(loaderStub);
- expect(actionStub).not.toHaveBeenCalled();
- expect(loaderStub).not.toHaveBeenCalled();
- expect(lazyActionStub).toHaveBeenCalledTimes(0);
- expect(lazyLoaderStub).toHaveBeenCalledTimes(0);
+ let route = findRouteById(t.router.routes, "lazy");
+ expect(route.lazy).toBeUndefined();
+ expect(route.action).toEqual(expect.any(Function));
+ expect(route.loader).toEqual(expect.any(Function));
+ expect(route.action).not.toBe(action);
+ expect(route.loader).not.toBe(loader);
+ expect(action).not.toHaveBeenCalled();
+ expect(loader).not.toHaveBeenCalled();
+ expect(lazyAction).toHaveBeenCalledTimes(0);
+ expect(lazyLoader).toHaveBeenCalledTimes(0);
expect(consoleWarn).toHaveBeenCalledTimes(2);
expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(
@@ -1195,14 +1347,14 @@ describe("lazily loaded route modules", () => {
it("prefers statically defined loader over lazily defined loader via lazy function (staticHandler.query)", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let lazyLoaderStub = jest.fn(async () => {
+ let loader = jest.fn(async () => {
await tick();
return Response.json({ value: "LAZY LOADER" });
});
- let lazyStub = jest.fn(async () => {
+ let lazy = jest.fn(async () => {
await tick();
return {
- loader: lazyLoaderStub,
+ loader,
};
});
@@ -1214,7 +1366,7 @@ describe("lazily loaded route modules", () => {
await tick();
return Response.json({ value: "STATIC LOADER" });
},
- lazy: lazyStub,
+ lazy,
},
]);
@@ -1226,8 +1378,8 @@ describe("lazily loaded route modules", () => {
expect(context.loaderData).toEqual({
lazy: { value: "STATIC LOADER" },
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
- expect(lazyLoaderStub).not.toHaveBeenCalled();
+ expect(lazy).toHaveBeenCalledTimes(1);
+ expect(loader).not.toHaveBeenCalled();
expect(consoleWarn).toHaveBeenCalledTimes(1);
expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(
@@ -1238,13 +1390,13 @@ describe("lazily loaded route modules", () => {
it("prefers statically defined loader over lazily defined loader via lazy property (staticHandler.query)", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let loaderStub = jest.fn(async () => {
+ let loader = jest.fn(async () => {
await tick();
return Response.json({ value: "LAZY LOADER" });
});
- let lazyStub = jest.fn(async () => {
+ let lazyLoader = jest.fn(async () => {
await tick();
- return loaderStub;
+ return loader;
});
let { query } = createStaticHandler([
@@ -1256,7 +1408,7 @@ describe("lazily loaded route modules", () => {
return Response.json({ value: "STATIC LOADER" });
},
lazy: {
- loader: lazyStub,
+ loader: lazyLoader,
},
},
]);
@@ -1269,8 +1421,8 @@ describe("lazily loaded route modules", () => {
expect(context.loaderData).toEqual({
lazy: { value: "STATIC LOADER" },
});
- expect(lazyStub).not.toHaveBeenCalled();
- expect(loaderStub).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
+ expect(loader).not.toHaveBeenCalled();
expect(consoleWarn).toHaveBeenCalledTimes(1);
expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(
@@ -1281,7 +1433,7 @@ describe("lazily loaded route modules", () => {
it("prefers statically defined loader over lazily defined loader via lazy function (staticHandler.queryRoute)", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let lazyLoaderStub = jest.fn(async () => {
+ let loader = jest.fn(async () => {
await tick();
return Response.json({ value: "LAZY LOADER" });
});
@@ -1297,7 +1449,7 @@ describe("lazily loaded route modules", () => {
lazy: async () => {
await tick();
return {
- loader: lazyLoaderStub,
+ loader,
};
},
},
@@ -1311,7 +1463,7 @@ describe("lazily loaded route modules", () => {
expect(context.loaderData).toEqual({
lazy: { value: "STATIC LOADER" },
});
- expect(lazyLoaderStub).not.toHaveBeenCalled();
+ expect(loader).not.toHaveBeenCalled();
expect(consoleWarn).toHaveBeenCalledTimes(1);
expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(
@@ -1322,13 +1474,13 @@ describe("lazily loaded route modules", () => {
it("prefers statically defined loader over lazily defined loader via lazy property (staticHandler.queryRoute)", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let loaderStub = jest.fn(async () => {
+ let loader = jest.fn(async () => {
await tick();
return Response.json({ value: "LAZY LOADER" });
});
- let lazyLoaderStub = jest.fn(async () => {
+ let lazyLoader = jest.fn(async () => {
await tick();
- return loaderStub;
+ return loader;
});
let { query } = createStaticHandler([
@@ -1340,7 +1492,7 @@ describe("lazily loaded route modules", () => {
return Response.json({ value: "STATIC LOADER" });
},
lazy: {
- loader: lazyLoaderStub,
+ loader: lazyLoader,
},
},
]);
@@ -1353,8 +1505,8 @@ describe("lazily loaded route modules", () => {
expect(context.loaderData).toEqual({
lazy: { value: "STATIC LOADER" },
});
- expect(loaderStub).not.toHaveBeenCalled();
- expect(lazyLoaderStub).not.toHaveBeenCalled();
+ expect(loader).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
expect(consoleWarn).toHaveBeenCalledTimes(1);
expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(
@@ -1365,7 +1517,7 @@ describe("lazily loaded route modules", () => {
it("handles errors thrown from static loaders before lazy function has completed", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazy, lazyDeferred] = createAsyncStub();
let t = setup({
routes: [
{
@@ -1376,13 +1528,13 @@ describe("lazily loaded route modules", () => {
id: "lazy",
path: "lazy",
loader: true,
- lazy: lazyStub,
+ lazy,
},
],
},
],
});
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
let A = await t.navigate("/lazy");
@@ -1400,13 +1552,14 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
lazy: "STATIC LOADER ERROR",
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
consoleWarn.mockReset();
});
it("handles errors thrown from static loaders before lazy property has resolved", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazyHasErrorBoundary, lazyHasErrorBoundaryDeferred] =
+ createAsyncStub();
let t = setup({
routes: [
{
@@ -1418,14 +1571,14 @@ describe("lazily loaded route modules", () => {
path: "lazy",
loader: true,
lazy: {
- hasErrorBoundary: lazyStub,
+ hasErrorBoundary: lazyHasErrorBoundary,
},
},
],
},
],
});
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazyHasErrorBoundary).not.toHaveBeenCalled();
let A = await t.navigate("/lazy");
@@ -1434,21 +1587,22 @@ describe("lazily loaded route modules", () => {
// We shouldn't bubble the loader error until after this resolves
// so we know if it has a boundary or not
- await lazyDeferred.resolve(true);
+ await lazyHasErrorBoundaryDeferred.resolve(true);
expect(t.router.state.location.pathname).toBe("/lazy");
expect(t.router.state.navigation.state).toBe("idle");
expect(t.router.state.loaderData).toEqual({});
expect(t.router.state.errors).toEqual({
lazy: "STATIC LOADER ERROR",
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);
consoleWarn.mockReset();
});
});
it("bubbles errors thrown from static loaders before lazy property has resolved if lazy 'hasErrorBoundary' is falsy", async () => {
let consoleWarn = jest.spyOn(console, "warn");
- let { lazyStub, lazyDeferred } = createLazyStub();
+ let [lazyHasErrorBoundary, lazyHasErrorBoundaryDeferred] =
+ createAsyncStub();
let t = setup({
routes: [
{
@@ -1460,14 +1614,14 @@ describe("lazily loaded route modules", () => {
path: "lazy",
loader: true,
lazy: {
- hasErrorBoundary: lazyStub,
+ hasErrorBoundary: lazyHasErrorBoundary,
},
},
],
},
],
});
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazyHasErrorBoundary).not.toHaveBeenCalled();
let A = await t.navigate("/lazy");
@@ -1476,22 +1630,22 @@ describe("lazily loaded route modules", () => {
// We shouldn't bubble the loader error until after this resolves
// so we know if it has a boundary or not
- await lazyDeferred.resolve(null);
+ await lazyHasErrorBoundaryDeferred.resolve(null);
expect(t.router.state.location.pathname).toBe("/lazy");
expect(t.router.state.navigation.state).toBe("idle");
expect(t.router.state.loaderData).toEqual({});
expect(t.router.state.errors).toEqual({
root: "STATIC LOADER ERROR",
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);
consoleWarn.mockReset();
});
describe("interruptions", () => {
it("runs lazily loaded route loader even if lazy function is interrupted", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
@@ -1501,29 +1655,25 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("idle");
- let lazyLoaderStub = jest.fn(() => "LAZY LOADER");
- await lazyDeferred.resolve({
- loader: lazyLoaderStub,
- });
+ let loader = jest.fn(() => "LAZY LOADER");
+ await lazyDeferred.resolve({ loader });
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("idle");
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
+ expect(loader).toHaveBeenCalledTimes(1);
// Ensure the lazy route object update still happened
- let lazyRoute = findRouteById(t.router.routes, "lazy");
- expect(lazyRoute.lazy).toBeUndefined();
- expect(lazyRoute.loader).toBe(lazyLoaderStub);
+ let route = findRouteById(t.router.routes, "lazy");
+ expect(route.lazy).toBeUndefined();
+ expect(route.loader).toBe(loader);
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("runs lazily loaded route loader even if lazy property is interrupted", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
- let routes = createBasicLazyRoutes({
- loader: lazyStub,
- });
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let routes = createBasicLazyRoutes({ loader: lazyLoader });
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
@@ -1533,24 +1683,24 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("idle");
- let lazyLoaderStub = jest.fn(() => "LAZY LOADER");
- await lazyDeferred.resolve(lazyLoaderStub);
+ let loader = jest.fn(() => "LAZY LOADER");
+ await lazyLoaderDeferred.resolve(loader);
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("idle");
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
+ expect(loader).toHaveBeenCalledTimes(1);
// Ensure the lazy route object update still happened
- let lazyRoute = findRouteById(t.router.routes, "lazy");
- expect(lazyRoute.lazy).toBeUndefined();
- expect(lazyRoute.loader).toBe(lazyLoaderStub);
+ let route = findRouteById(t.router.routes, "lazy");
+ expect(route.lazy).toBeUndefined();
+ expect(route.loader).toBe(loader);
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
});
it("runs lazily loaded route action even if lazy function is interrupted", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
await t.navigate("/lazy", {
formMethod: "post",
@@ -1563,34 +1713,29 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("idle");
- let lazyActionStub = jest.fn(() => "LAZY ACTION");
- let lazyLoaderStub = jest.fn(() => "LAZY LOADER");
- await lazyDeferred.resolve({
- action: lazyActionStub,
- loader: lazyLoaderStub,
- });
+ let action = jest.fn(() => "LAZY ACTION");
+ let loader = jest.fn(() => "LAZY LOADER");
+ await lazyDeferred.resolve({ action, loader });
- let lazyRoute = findRouteById(t.router.routes, "lazy");
- expect(lazyActionStub).toHaveBeenCalledTimes(1);
- expect(lazyLoaderStub).not.toHaveBeenCalled();
- expect(lazyRoute.lazy).toBeUndefined();
- expect(lazyRoute.action).toBe(lazyActionStub);
- expect(lazyRoute.loader).toBe(lazyLoaderStub);
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ let route = findRouteById(t.router.routes, "lazy");
+ expect(action).toHaveBeenCalledTimes(1);
+ expect(loader).not.toHaveBeenCalled();
+ expect(route.lazy).toBeUndefined();
+ expect(route.action).toBe(action);
+ expect(route.loader).toBe(loader);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("runs lazily loaded route action even if lazy property is interrupted", async () => {
- let { lazyStub: lazyActionStub, lazyDeferred: lazyActionDeferred } =
- createLazyStub();
- let { lazyStub: lazyLoaderStub, lazyDeferred: lazyLoaderDeferred } =
- createLazyStub();
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
let routes = createBasicLazyRoutes({
- action: lazyActionStub,
- loader: lazyLoaderStub,
+ action: lazyAction,
+ loader: lazyLoader,
});
let t = setup({ routes });
- expect(lazyActionStub).not.toHaveBeenCalled();
- expect(lazyLoaderStub).not.toHaveBeenCalled();
+ expect(lazyAction).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
await t.navigate("/lazy", {
formMethod: "post",
@@ -1603,84 +1748,78 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("idle");
- let actionStub = jest.fn(() => "LAZY ACTION");
- let loaderStub = jest.fn(() => "LAZY LOADER");
- await lazyActionDeferred.resolve(actionStub);
- await lazyLoaderDeferred.resolve(loaderStub);
-
- let lazyRoute = findRouteById(t.router.routes, "lazy");
- expect(actionStub).toHaveBeenCalledTimes(1);
- expect(loaderStub).not.toHaveBeenCalled();
- expect(lazyRoute.lazy).toBeUndefined();
- expect(lazyRoute.action).toBe(actionStub);
- expect(lazyRoute.loader).toBe(loaderStub);
- expect(lazyActionStub).toHaveBeenCalledTimes(1);
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
+ let action = jest.fn(() => "LAZY ACTION");
+ let loader = jest.fn(() => "LAZY LOADER");
+ await lazyActionDeferred.resolve(action);
+ await lazyLoaderDeferred.resolve(loader);
+
+ let route = findRouteById(t.router.routes, "lazy");
+ expect(action).toHaveBeenCalledTimes(1);
+ expect(loader).not.toHaveBeenCalled();
+ expect(route.lazy).toBeUndefined();
+ expect(route.action).toBe(action);
+ expect(route.loader).toBe(loader);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
});
it("runs lazily loaded route loader on fetcher.load() even if lazy function is interrupted", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
await t.fetch("http://github.com/lazy", key);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
- let loaderDeferred = createDeferred();
- let lazyLoaderStub = jest.fn(() => loaderDeferred.promise);
- await lazyDeferred.resolve({
- loader: lazyLoaderStub,
- });
+ let [loader, loaderDeferred] = createAsyncStub();
+ await lazyDeferred.resolve({ loader });
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
await loaderDeferred.resolve("LAZY LOADER");
expect(t.fetchers[key].state).toBe("idle");
expect(t.fetchers[key].data).toBe("LAZY LOADER");
- expect(lazyLoaderStub).toHaveBeenCalledTimes(2);
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(loader).toHaveBeenCalledTimes(2);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("runs lazily loaded route loader on fetcher.load() even if lazy property is interrupted", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
- let routes = createBasicLazyRoutes({
- loader: lazyStub,
- });
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let routes = createBasicLazyRoutes({ loader: lazyLoader });
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
await t.fetch("http://github.com/lazy", key);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
- let loaderDeferred = createDeferred();
- let loaderStub = jest.fn(() => loaderDeferred.promise);
- await lazyDeferred.resolve(loaderStub);
+ let [loader, loaderDeferred] = createAsyncStub();
+ await lazyLoaderDeferred.resolve(loader);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
await loaderDeferred.resolve("LAZY LOADER");
expect(t.fetchers[key].state).toBe("idle");
expect(t.fetchers[key].data).toBe("LAZY LOADER");
- expect(loaderStub).toHaveBeenCalledTimes(2);
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(loader).toHaveBeenCalledTimes(2);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
});
it("runs lazily loaded route action on fetcher.submit() even if lazy function is interrupted", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key, {
@@ -1688,37 +1827,32 @@ describe("lazily loaded route modules", () => {
formData: createFormData({}),
});
expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
await t.fetch("http://github.com/lazy", key, {
formMethod: "post",
formData: createFormData({}),
});
expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
- let actionDeferred = createDeferred();
- let lazyActionStub = jest.fn(() => actionDeferred.promise);
- await lazyDeferred.resolve({
- action: lazyActionStub,
- });
+ let [action, actionDeferred] = createAsyncStub();
+ await lazyDeferred.resolve({ action });
expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
await actionDeferred.resolve("LAZY ACTION");
expect(t.fetchers[key].state).toBe("idle");
expect(t.fetchers[key].data).toBe("LAZY ACTION");
- expect(lazyActionStub).toHaveBeenCalledTimes(2);
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(action).toHaveBeenCalledTimes(2);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("runs lazily loaded route action on fetcher.submit() even if lazy property is interrupted", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
- let routes = createBasicLazyRoutes({
- action: lazyStub,
- });
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
+ let routes = createBasicLazyRoutes({ action: lazyAction });
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazyAction).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key, {
@@ -1726,48 +1860,44 @@ describe("lazily loaded route modules", () => {
formData: createFormData({}),
});
expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
await t.fetch("http://github.com/lazy", key, {
formMethod: "post",
formData: createFormData({}),
});
expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
- let actionDeferred = createDeferred();
- let lazyActionStub = jest.fn(() => actionDeferred.promise);
- await lazyDeferred.resolve(lazyActionStub);
+ let [action, actionDeferred] = createAsyncStub();
+ await lazyActionDeferred.resolve(action);
expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
await actionDeferred.resolve("LAZY ACTION");
expect(t.fetchers[key].state).toBe("idle");
expect(t.fetchers[key].data).toBe("LAZY ACTION");
- expect(lazyActionStub).toHaveBeenCalledTimes(2);
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(action).toHaveBeenCalledTimes(2);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
});
it("uses the first-called lazy function execution on repeated loading navigations", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
- let loaderDeferred = createDeferred();
- let lazyLoaderStub = jest.fn(() => loaderDeferred.promise);
- await lazyDeferred.resolve({
- loader: lazyLoaderStub,
- });
+ let [loader, loaderDeferred] = createAsyncStub();
+ await lazyDeferred.resolve({ loader });
await loaderDeferred.resolve("LAZY LOADER");
@@ -1776,31 +1906,28 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.loaderData).toEqual({ lazy: "LAZY LOADER" });
- expect(lazyLoaderStub).toHaveBeenCalledTimes(2);
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(loader).toHaveBeenCalledTimes(2);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("uses the first-called lazy property execution on repeated loading navigations", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
- let routes = createBasicLazyRoutes({
- loader: lazyStub,
- });
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let routes = createBasicLazyRoutes({ loader: lazyLoader });
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
- let loaderDeferred = createDeferred();
- let lazyLoaderStub = jest.fn(() => loaderDeferred.promise);
- await lazyDeferred.resolve(lazyLoaderStub);
+ let [loader, loaderDeferred] = createAsyncStub();
+ await lazyLoaderDeferred.resolve(loader);
await loaderDeferred.resolve("LAZY LOADER");
@@ -1809,14 +1936,14 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.loaderData).toEqual({ lazy: "LAZY LOADER" });
- expect(lazyLoaderStub).toHaveBeenCalledTimes(2);
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(loader).toHaveBeenCalledTimes(2);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
});
it("uses the first-called lazy function execution on repeated submission navigations", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
await t.navigate("/lazy", {
formMethod: "post",
@@ -1824,7 +1951,7 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
await t.navigate("/lazy", {
formMethod: "post",
@@ -1832,16 +1959,11 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
- let loaderDeferred = createDeferred();
- let actionDeferred = createDeferred();
- let lazyLoaderStub = jest.fn(() => loaderDeferred.promise);
- let lazyActionStub = jest.fn(() => actionDeferred.promise);
- await lazyDeferred.resolve({
- action: lazyActionStub,
- loader: lazyLoaderStub,
- });
+ let [action, actionDeferred] = createAsyncStub();
+ let [loader, loaderDeferred] = createAsyncStub();
+ await lazyDeferred.resolve({ action, loader });
await actionDeferred.resolve("LAZY ACTION");
await loaderDeferred.resolve("LAZY LOADER");
@@ -1852,23 +1974,21 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.actionData).toEqual({ lazy: "LAZY ACTION" });
expect(t.router.state.loaderData).toEqual({ lazy: "LAZY LOADER" });
- expect(lazyActionStub).toHaveBeenCalledTimes(2);
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(action).toHaveBeenCalledTimes(2);
+ expect(loader).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("uses the first-called lazy function property on repeated submission navigations", async () => {
- let { lazyStub: lazyActionStub, lazyDeferred: lazyActionDeferred } =
- createLazyStub();
- let { lazyStub: lazyLoaderStub, lazyDeferred: lazyLoaderDeferred } =
- createLazyStub();
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
let routes = createBasicLazyRoutes({
- action: lazyActionStub,
- loader: lazyLoaderStub,
+ action: lazyAction,
+ loader: lazyLoader,
});
let t = setup({ routes });
- expect(lazyActionStub).not.toHaveBeenCalled();
- expect(lazyLoaderStub).not.toHaveBeenCalled();
+ expect(lazyAction).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
await t.navigate("/lazy", {
formMethod: "post",
@@ -1876,8 +1996,8 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyActionStub).toHaveBeenCalledTimes(1);
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
await t.navigate("/lazy", {
formMethod: "post",
@@ -1885,15 +2005,13 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyActionStub).toHaveBeenCalledTimes(1);
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
- let loaderDeferred = createDeferred();
- let actionDeferred = createDeferred();
- let loaderStub = jest.fn(() => loaderDeferred.promise);
- let actionStub = jest.fn(() => actionDeferred.promise);
- await lazyActionDeferred.resolve(actionStub);
- await lazyLoaderDeferred.resolve(loaderStub);
+ let [action, actionDeferred] = createAsyncStub();
+ let [loader, loaderDeferred] = createAsyncStub();
+ await lazyActionDeferred.resolve(action);
+ await lazyLoaderDeferred.resolve(loader);
await actionDeferred.resolve("LAZY ACTION");
await loaderDeferred.resolve("LAZY LOADER");
@@ -1904,31 +2022,28 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.actionData).toEqual({ lazy: "LAZY ACTION" });
expect(t.router.state.loaderData).toEqual({ lazy: "LAZY LOADER" });
- expect(actionStub).toHaveBeenCalledTimes(2);
- expect(loaderStub).toHaveBeenCalledTimes(1);
- expect(lazyActionStub).toHaveBeenCalledTimes(1);
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
+ expect(action).toHaveBeenCalledTimes(2);
+ expect(loader).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
});
it("uses the first-called lazy function execution on repeated fetcher.load calls", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
await t.fetch("http://github.com/lazy", key);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
- let loaderDeferred = createDeferred();
- let lazyLoaderStub = jest.fn(() => loaderDeferred.promise);
- await lazyDeferred.resolve({
- loader: lazyLoaderStub,
- });
+ let [loader, loaderDeferred] = createAsyncStub();
+ await lazyDeferred.resolve({ loader });
expect(t.fetchers[key].state).toBe("loading");
@@ -1936,30 +2051,27 @@ describe("lazily loaded route modules", () => {
expect(t.fetchers[key].state).toBe("idle");
expect(t.fetchers[key].data).toBe("LAZY LOADER");
- expect(lazyLoaderStub).toHaveBeenCalledTimes(2);
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(loader).toHaveBeenCalledTimes(2);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("uses the first-called lazy property execution on repeated fetcher.load calls", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
- let routes = createBasicLazyRoutes({
- loader: lazyStub,
- });
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let routes = createBasicLazyRoutes({ loader: lazyLoader });
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
await t.fetch("http://github.com/lazy", key);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
- let loaderDeferred = createDeferred();
- let lazyLoaderStub = jest.fn(() => loaderDeferred.promise);
- await lazyDeferred.resolve(lazyLoaderStub);
+ let [loader, loaderDeferred] = createAsyncStub();
+ await lazyLoaderDeferred.resolve(loader);
expect(t.fetchers[key].state).toBe("loading");
@@ -1967,8 +2079,8 @@ describe("lazily loaded route modules", () => {
expect(t.fetchers[key].state).toBe("idle");
expect(t.fetchers[key].data).toBe("LAZY LOADER");
- expect(lazyLoaderStub).toHaveBeenCalledTimes(2);
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(loader).toHaveBeenCalledTimes(2);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
});
});
@@ -2003,7 +2115,7 @@ describe("lazily loaded route modules", () => {
});
it("handles errors when failing to resolve lazy route property on initialization", async () => {
- let lazyDeferred = createDeferred();
+ let lazyLoaderDeferred = createDeferred();
let router = createRouter({
history: createMemoryHistory({ initialEntries: ["/lazy"] }),
routes: [
@@ -2016,7 +2128,7 @@ describe("lazily loaded route modules", () => {
id: "lazy",
path: "lazy",
lazy: {
- loader: () => lazyDeferred.promise,
+ loader: () => lazyLoaderDeferred.promise,
},
},
],
@@ -2025,7 +2137,7 @@ describe("lazily loaded route modules", () => {
}).initialize();
expect(router.state.initialized).toBe(false);
- lazyDeferred.reject(new Error("LAZY PROPERTY ERROR"));
+ lazyLoaderDeferred.reject(new Error("LAZY PROPERTY ERROR"));
await tick();
expect(router.state.errors).toEqual({
root: new Error("LAZY PROPERTY ERROR"),
@@ -2034,14 +2146,14 @@ describe("lazily loaded route modules", () => {
});
it("handles errors when failing to resolve lazy route function on loading navigation", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
await lazyDeferred.reject(new Error("LAZY FUNCTION ERROR"));
expect(t.router.state.location.pathname).toBe("/lazy");
@@ -2051,23 +2163,21 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
root: new Error("LAZY FUNCTION ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("handles errors when failing to resolve lazy route loader property on loading navigation", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
- let routes = createBasicLazyRoutes({
- loader: lazyStub,
- });
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let routes = createBasicLazyRoutes({ loader: lazyLoader });
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
- await lazyDeferred.reject(new Error("LAZY PROPERTY ERROR"));
+ await lazyLoaderDeferred.reject(new Error("LAZY PROPERTY ERROR"));
expect(t.router.state.location.pathname).toBe("/lazy");
expect(t.router.state.navigation.state).toBe("idle");
@@ -2075,14 +2185,12 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
root: new Error("LAZY PROPERTY ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
});
it("handles errors when failing to resolve other lazy route properties on loading navigation", async () => {
- let { lazyStub: lazyLoader, lazyDeferred: lazyLoaderDeferred } =
- createLazyStub();
- let { lazyStub: lazyAction, lazyDeferred: lazyActionDeferred } =
- createLazyStub();
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
let routes = createBasicLazyRoutes({
loader: lazyLoader,
action: lazyAction,
@@ -2116,14 +2224,14 @@ describe("lazily loaded route modules", () => {
});
it("handles loader errors from lazy route functions when the route has an error boundary", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
let loaderDeferred = createDeferred();
await lazyDeferred.resolve({
@@ -2132,7 +2240,7 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
await loaderDeferred.reject(new Error("LAZY LOADER ERROR"));
expect(t.router.state.location.pathname).toBe("/lazy");
@@ -2140,36 +2248,33 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
lazy: new Error("LAZY LOADER ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("handles loader errors from lazy route properties when the route has an error boundary", async () => {
- let { lazyStub: lazyLoaderStub, lazyDeferred: lazyLoaderDeferred } =
- createLazyStub();
- let {
- lazyStub: lazyHasErrorBoundaryStub,
- lazyDeferred: lazyHasErrorBoundaryDeferred,
- } = createLazyStub();
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let [lazyHasErrorBoundary, lazyHasErrorBoundaryDeferred] =
+ createAsyncStub();
let routes = createBasicLazyRoutes({
- loader: lazyLoaderStub,
- hasErrorBoundary: lazyHasErrorBoundaryStub,
+ loader: lazyLoader,
+ hasErrorBoundary: lazyHasErrorBoundary,
});
let t = setup({ routes });
- expect(lazyLoaderStub).not.toHaveBeenCalled();
- expect(lazyHasErrorBoundaryStub).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
+ expect(lazyHasErrorBoundary).not.toHaveBeenCalled();
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
- expect(lazyHasErrorBoundaryStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+ expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);
let loaderDeferred = createDeferred();
await lazyLoaderDeferred.resolve(() => loaderDeferred.promise);
await lazyHasErrorBoundaryDeferred.resolve(() => true);
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
await loaderDeferred.reject(new Error("LAZY LOADER ERROR"));
expect(t.router.state.location.pathname).toBe("/lazy");
@@ -2177,19 +2282,19 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
lazy: new Error("LAZY LOADER ERROR"),
});
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
- expect(lazyHasErrorBoundaryStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+ expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);
});
it("bubbles loader errors from in lazy route functions when the route does not specify an error boundary", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
let loaderDeferred = createDeferred();
await lazyDeferred.resolve({
@@ -2205,24 +2310,22 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
root: new Error("LAZY LOADER ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("bubbles loader errors from in lazy route properties when the route does not specify an error boundary", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
- let routes = createBasicLazyRoutes({
- loader: lazyStub,
- });
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let routes = createBasicLazyRoutes({ loader: lazyLoader });
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
let loaderDeferred = createDeferred();
- await lazyDeferred.resolve(() => loaderDeferred.promise);
+ await lazyLoaderDeferred.resolve(() => loaderDeferred.promise);
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
@@ -2233,18 +2336,18 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
root: new Error("LAZY LOADER ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
});
it("bubbles loader errors from lazy route functions when the route specifies hasErrorBoundary:false", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
let loaderDeferred = createDeferred();
await lazyDeferred.resolve({
@@ -2261,29 +2364,26 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
root: new Error("LAZY LOADER ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("bubbles loader errors from lazy route properties when the route specifies hasErrorBoundary:false", async () => {
- let { lazyStub: lazyLoaderStub, lazyDeferred: lazyLoaderDeferred } =
- createLazyStub();
- let {
- lazyStub: lazyHasErrorBoundaryStub,
- lazyDeferred: lazyHasErrorBoundaryDeferred,
- } = createLazyStub();
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let [lazyHasErrorBoundary, lazyHasErrorBoundaryDeferred] =
+ createAsyncStub();
let routes = createBasicLazyRoutes({
- loader: lazyLoaderStub,
- hasErrorBoundary: lazyHasErrorBoundaryStub,
+ loader: lazyLoader,
+ hasErrorBoundary: lazyHasErrorBoundary,
});
let t = setup({ routes });
- expect(lazyLoaderStub).not.toHaveBeenCalled();
- expect(lazyHasErrorBoundaryStub).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
+ expect(lazyHasErrorBoundary).not.toHaveBeenCalled();
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
- expect(lazyHasErrorBoundaryStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+ expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);
let loaderDeferred = createDeferred();
await lazyLoaderDeferred.resolve(() => loaderDeferred.promise);
@@ -2298,30 +2398,27 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
root: new Error("LAZY LOADER ERROR"),
});
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
- expect(lazyHasErrorBoundaryStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+ expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);
});
it("bubbles loader errors from lazy route properties when the route specifies hasErrorBoundary:null", async () => {
- let { lazyStub: lazyLoaderStub, lazyDeferred: lazyLoaderDeferred } =
- createLazyStub();
- let {
- lazyStub: lazyHasErrorBoundaryStub,
- lazyDeferred: lazyHasErrorBoundaryDeferred,
- } = createLazyStub();
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let [lazyHasErrorBoundary, lazyHasErrorBoundaryDeferred] =
+ createAsyncStub();
let routes = createBasicLazyRoutes({
- loader: lazyLoaderStub,
- hasErrorBoundary: lazyHasErrorBoundaryStub,
+ loader: lazyLoader,
+ hasErrorBoundary: lazyHasErrorBoundary,
});
let t = setup({ routes });
- expect(lazyLoaderStub).not.toHaveBeenCalled();
- expect(lazyHasErrorBoundaryStub).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
+ expect(lazyHasErrorBoundary).not.toHaveBeenCalled();
await t.navigate("/lazy");
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("loading");
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
- expect(lazyHasErrorBoundaryStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+ expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);
let loaderDeferred = createDeferred();
await lazyLoaderDeferred.resolve(() => loaderDeferred.promise);
@@ -2336,14 +2433,14 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
root: new Error("LAZY LOADER ERROR"),
});
- expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
- expect(lazyHasErrorBoundaryStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
+ expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);
});
it("handles errors when failing to resolve lazy route functions on submission navigation", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
await t.navigate("/lazy", {
formMethod: "post",
@@ -2351,7 +2448,7 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
await lazyDeferred.reject(new Error("LAZY FUNCTION ERROR"));
expect(t.router.state.location.pathname).toBe("/lazy");
@@ -2362,16 +2459,14 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.actionData).toEqual(null);
expect(t.router.state.loaderData).toEqual({});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("handles errors when failing to resolve lazy route properties on submission navigation", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
- let routes = createBasicLazyRoutes({
- action: lazyStub,
- });
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
+ let routes = createBasicLazyRoutes({ action: lazyAction });
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazyAction).not.toHaveBeenCalled();
await t.navigate("/lazy", {
formMethod: "post",
@@ -2379,9 +2474,9 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
- await lazyDeferred.reject(new Error("LAZY FUNCTION ERROR"));
+ await lazyActionDeferred.reject(new Error("LAZY FUNCTION ERROR"));
expect(t.router.state.location.pathname).toBe("/lazy");
expect(t.router.state.navigation.state).toBe("idle");
@@ -2390,13 +2485,13 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.actionData).toEqual(null);
expect(t.router.state.loaderData).toEqual({});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
});
it("handles action errors from lazy route functions on submission navigation", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
await t.navigate("/lazy", {
formMethod: "post",
@@ -2404,7 +2499,7 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
let actionDeferred = createDeferred();
await lazyDeferred.resolve({
@@ -2421,22 +2516,19 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
lazy: new Error("LAZY ACTION ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("handles action errors from lazy route properties on submission navigation", async () => {
- let { lazyStub: lazyActionStub, lazyDeferred: lazyActionDeferred } =
- createLazyStub();
- let {
- lazyStub: lazyErrorBoundaryStub,
- lazyDeferred: lazyErrorBoundaryDeferred,
- } = createLazyStub();
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
+ let [lazyErrorBoundaryStub, lazyErrorBoundaryDeferred] =
+ createAsyncStub();
let routes = createBasicLazyRoutes({
- action: lazyActionStub,
+ action: lazyAction,
hasErrorBoundary: lazyErrorBoundaryStub,
});
let t = setup({ routes });
- expect(lazyActionStub).not.toHaveBeenCalled();
+ expect(lazyAction).not.toHaveBeenCalled();
expect(lazyErrorBoundaryStub).not.toHaveBeenCalled();
await t.navigate("/lazy", {
@@ -2445,7 +2537,7 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyActionStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
expect(lazyErrorBoundaryStub).toHaveBeenCalledTimes(1);
let actionDeferred = createDeferred();
@@ -2461,14 +2553,14 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
lazy: new Error("LAZY ACTION ERROR"),
});
- expect(lazyActionStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
expect(lazyErrorBoundaryStub).toHaveBeenCalledTimes(1);
});
it("bubbles action errors from lazy route functions when the route specifies hasErrorBoundary:false", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
await t.navigate("/lazy", {
formMethod: "post",
@@ -2476,7 +2568,7 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
let actionDeferred = createDeferred();
await lazyDeferred.resolve({
@@ -2493,22 +2585,19 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
root: new Error("LAZY ACTION ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("bubbles action errors from lazy route properties when the route specifies hasErrorBoundary:false", async () => {
- let { lazyStub: lazyActionStub, lazyDeferred: lazyActionDeferred } =
- createLazyStub();
- let {
- lazyStub: lazyErrorBoundaryStub,
- lazyDeferred: lazyErrorBoundaryDeferred,
- } = createLazyStub();
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
+ let [lazyErrorBoundaryStub, lazyErrorBoundaryDeferred] =
+ createAsyncStub();
let routes = createBasicLazyRoutes({
- action: lazyActionStub,
+ action: lazyAction,
hasErrorBoundary: lazyErrorBoundaryStub,
});
let t = setup({ routes });
- expect(lazyActionStub).not.toHaveBeenCalled();
+ expect(lazyAction).not.toHaveBeenCalled();
expect(lazyErrorBoundaryStub).not.toHaveBeenCalled();
await t.navigate("/lazy", {
@@ -2517,7 +2606,7 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyActionStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
expect(lazyErrorBoundaryStub).toHaveBeenCalledTimes(1);
let actionDeferred = createDeferred();
@@ -2533,23 +2622,20 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
root: new Error("LAZY ACTION ERROR"),
});
- expect(lazyActionStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
expect(lazyErrorBoundaryStub).toHaveBeenCalledTimes(1);
});
it("bubbles action errors from lazy route properties when the route specifies hasErrorBoundary:null", async () => {
- let { lazyStub: lazyActionStub, lazyDeferred: lazyActionDeferred } =
- createLazyStub();
- let {
- lazyStub: lazyErrorBoundaryStub,
- lazyDeferred: lazyErrorBoundaryDeferred,
- } = createLazyStub();
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
+ let [lazyErrorBoundaryStub, lazyErrorBoundaryDeferred] =
+ createAsyncStub();
let routes = createBasicLazyRoutes({
- action: lazyActionStub,
+ action: lazyAction,
hasErrorBoundary: lazyErrorBoundaryStub,
});
let t = setup({ routes });
- expect(lazyActionStub).not.toHaveBeenCalled();
+ expect(lazyAction).not.toHaveBeenCalled();
expect(lazyErrorBoundaryStub).not.toHaveBeenCalled();
await t.navigate("/lazy", {
@@ -2558,7 +2644,7 @@ describe("lazily loaded route modules", () => {
});
expect(t.router.state.location.pathname).toBe("/");
expect(t.router.state.navigation.state).toBe("submitting");
- expect(lazyActionStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
expect(lazyErrorBoundaryStub).toHaveBeenCalledTimes(1);
let actionDeferred = createDeferred();
@@ -2574,58 +2660,56 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
root: new Error("LAZY ACTION ERROR"),
});
- expect(lazyActionStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
expect(lazyErrorBoundaryStub).toHaveBeenCalledTimes(1);
});
it("handles errors when failing to load lazy route functions on fetcher.load", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
await lazyDeferred.reject(new Error("LAZY FUNCTION ERROR"));
expect(t.router.state.fetchers.get(key)).toBeUndefined();
expect(t.router.state.errors).toEqual({
root: new Error("LAZY FUNCTION ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("handles errors when failing to load lazy route properties on fetcher.load", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
- let routes = createBasicLazyRoutes({
- loader: lazyStub,
- });
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let routes = createBasicLazyRoutes({ loader: lazyLoader });
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
- await lazyDeferred.reject(new Error("LAZY FUNCTION ERROR"));
+ await lazyLoaderDeferred.reject(new Error("LAZY FUNCTION ERROR"));
expect(t.router.state.fetchers.get(key)).toBeUndefined();
expect(t.router.state.errors).toEqual({
root: new Error("LAZY FUNCTION ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
});
it("handles loader errors in lazy route functions on fetcher.load", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
let loaderDeferred = createDeferred();
await lazyDeferred.resolve({
@@ -2638,24 +2722,22 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
root: new Error("LAZY LOADER ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("handles loader errors in lazy route properties on fetcher.load", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
- let routes = createBasicLazyRoutes({
- loader: lazyStub,
- });
+ let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
+ let routes = createBasicLazyRoutes({ loader: lazyLoader });
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazyLoader).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
let loaderDeferred = createDeferred();
- await lazyDeferred.resolve(() => loaderDeferred.promise);
+ await lazyLoaderDeferred.resolve(() => loaderDeferred.promise);
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
await loaderDeferred.reject(new Error("LAZY LOADER ERROR"));
@@ -2663,13 +2745,13 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
root: new Error("LAZY LOADER ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyLoader).toHaveBeenCalledTimes(1);
});
it("handles errors when failing to load lazy route functions on fetcher.submit", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key, {
@@ -2677,23 +2759,21 @@ describe("lazily loaded route modules", () => {
formData: createFormData({}),
});
expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
await lazyDeferred.reject(new Error("LAZY FUNCTION ERROR"));
expect(t.router.state.fetchers.get(key)).toBeUndefined();
expect(t.router.state.errors).toEqual({
root: new Error("LAZY FUNCTION ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("handles errors when failing to load lazy route properties on fetcher.submit", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
- let routes = createBasicLazyRoutes({
- action: lazyStub,
- });
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
+ let routes = createBasicLazyRoutes({ action: lazyAction });
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazyAction).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key, {
@@ -2701,20 +2781,20 @@ describe("lazily loaded route modules", () => {
formData: createFormData({}),
});
expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
- await lazyDeferred.reject(new Error("LAZY FUNCTION ERROR"));
+ await lazyActionDeferred.reject(new Error("LAZY FUNCTION ERROR"));
expect(t.router.state.fetchers.get(key)).toBeUndefined();
expect(t.router.state.errors).toEqual({
root: new Error("LAZY FUNCTION ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
});
it("handles action errors in lazy route functions on fetcher.submit", async () => {
- let { routes, lazyStub, lazyDeferred } = createBasicLazyFunctionRoutes();
+ let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazy).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key, {
@@ -2722,7 +2802,7 @@ describe("lazily loaded route modules", () => {
formData: createFormData({}),
});
expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
let actionDeferred = createDeferred();
await lazyDeferred.resolve({
@@ -2736,16 +2816,14 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
root: new Error("LAZY ACTION ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazy).toHaveBeenCalledTimes(1);
});
it("handles action errors in lazy route properties on fetcher.submit", async () => {
- let { lazyStub, lazyDeferred } = createLazyStub();
- let routes = createBasicLazyRoutes({
- action: lazyStub,
- });
+ let [lazyAction, lazyActionDeferred] = createAsyncStub();
+ let routes = createBasicLazyRoutes({ action: lazyAction });
let t = setup({ routes });
- expect(lazyStub).not.toHaveBeenCalled();
+ expect(lazyAction).not.toHaveBeenCalled();
let key = "key";
await t.fetch("http://github.com/lazy", key, {
@@ -2753,10 +2831,10 @@ describe("lazily loaded route modules", () => {
formData: createFormData({}),
});
expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
let actionDeferred = createDeferred();
- await lazyDeferred.resolve(() => actionDeferred.promise);
+ await lazyActionDeferred.resolve(() => actionDeferred.promise);
expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
await actionDeferred.reject(new Error("LAZY ACTION ERROR"));
@@ -2765,7 +2843,7 @@ describe("lazily loaded route modules", () => {
expect(t.router.state.errors).toEqual({
root: new Error("LAZY ACTION ERROR"),
});
- expect(lazyStub).toHaveBeenCalledTimes(1);
+ expect(lazyAction).toHaveBeenCalledTimes(1);
});
it("throws when failing to resolve lazy route functions on staticHandler.query()", async () => {
@@ -2852,9 +2930,7 @@ describe("lazily loaded route modules", () => {
!(context instanceof Response),
"Expected a StaticContext instance"
);
- expect(context.loaderData).toEqual({
- root: null,
- });
+ expect(context.loaderData).toEqual({});
expect(context.errors).toEqual({
lazy: new Error("LAZY LOADER ERROR"),
});
@@ -2891,9 +2967,7 @@ describe("lazily loaded route modules", () => {
!(context instanceof Response),
"Expected a StaticContext instance"
);
- expect(context.loaderData).toEqual({
- root: null,
- });
+ expect(context.loaderData).toEqual({});
expect(context.errors).toEqual({
lazy: new Error("LAZY LOADER ERROR"),
});
@@ -2928,9 +3002,7 @@ describe("lazily loaded route modules", () => {
!(context instanceof Response),
"Expected a StaticContext instance"
);
- expect(context.loaderData).toEqual({
- root: null,
- });
+ expect(context.loaderData).toEqual({});
expect(context.errors).toEqual({
root: new Error("LAZY LOADER ERROR"),
});
@@ -2967,9 +3039,7 @@ describe("lazily loaded route modules", () => {
!(context instanceof Response),
"Expected a StaticContext instance"
);
- expect(context.loaderData).toEqual({
- root: null,
- });
+ expect(context.loaderData).toEqual({});
expect(context.errors).toEqual({
root: new Error("LAZY LOADER ERROR"),
});
@@ -3006,9 +3076,7 @@ describe("lazily loaded route modules", () => {
!(context instanceof Response),
"Expected a StaticContext instance"
);
- expect(context.loaderData).toEqual({
- root: null,
- });
+ expect(context.loaderData).toEqual({});
expect(context.errors).toEqual({
root: new Error("LAZY LOADER ERROR"),
});
diff --git a/packages/react-router/__tests__/router/ssr-test.ts b/packages/react-router/__tests__/router/ssr-test.ts
index 4ef923a368..77faaaba05 100644
--- a/packages/react-router/__tests__/router/ssr-test.ts
+++ b/packages/react-router/__tests__/router/ssr-test.ts
@@ -186,7 +186,7 @@ describe("ssr", () => {
});
});
- it("should fill in null loaderData values for routes without loaders", async () => {
+ it("should not fill in null loaderData values for routes without loaders", async () => {
let { query } = createStaticHandler([
{
id: "root",
@@ -215,10 +215,7 @@ describe("ssr", () => {
let context = await query(createRequest("/none"));
expect(context).toMatchObject({
actionData: null,
- loaderData: {
- root: null,
- none: null,
- },
+ loaderData: {},
errors: null,
location: { pathname: "/none" },
});
@@ -228,9 +225,7 @@ describe("ssr", () => {
expect(context).toMatchObject({
actionData: null,
loaderData: {
- root: null,
a: "A",
- b: null,
},
errors: null,
location: { pathname: "/a/b" },
diff --git a/packages/react-router/__tests__/router/utils/data-router-setup.ts b/packages/react-router/__tests__/router/utils/data-router-setup.ts
index f219cbb58d..b097054049 100644
--- a/packages/react-router/__tests__/router/utils/data-router-setup.ts
+++ b/packages/react-router/__tests__/router/utils/data-router-setup.ts
@@ -139,6 +139,7 @@ type SetupOpts = {
basename?: string;
initialEntries?: InitialEntry[];
initialIndex?: number;
+ hydrationRouteProperties?: string[];
hydrationData?: HydrationState;
dataStrategy?: DataStrategyFunction;
};
@@ -168,17 +169,14 @@ export function createDeferred() {
};
}
-export function createLazyStub(): {
- lazyStub: jest.Mock;
- lazyDeferred: ReturnType;
-} {
- let lazyDeferred = createDeferred();
- let lazyStub = jest.fn(() => lazyDeferred.promise);
+export function createAsyncStub(): [
+ asyncStub: jest.Mock,
+ deferred: ReturnType
+] {
+ let deferred = createDeferred();
+ let asyncStub = jest.fn(() => deferred.promise);
- return {
- lazyStub,
- lazyDeferred,
- };
+ return [asyncStub, deferred];
}
export function getFetcherData(router: Router) {
@@ -207,6 +205,7 @@ export function setup({
basename,
initialEntries,
initialIndex,
+ hydrationRouteProperties,
hydrationData,
dataStrategy,
}: SetupOpts) {
@@ -322,6 +321,7 @@ export function setup({
basename,
history,
routes: enhanceRoutes(routes),
+ hydrationRouteProperties,
hydrationData,
window: testWindow,
dataStrategy: dataStrategy,
diff --git a/packages/react-router/__tests__/server-runtime/server-test.ts b/packages/react-router/__tests__/server-runtime/server-test.ts
index 9301f68ee6..fa62752bb8 100644
--- a/packages/react-router/__tests__/server-runtime/server-test.ts
+++ b/packages/react-router/__tests__/server-runtime/server-test.ts
@@ -2,7 +2,10 @@
* @jest-environment node
*/
-import type { StaticHandlerContext } from "react-router";
+import {
+ unstable_createContext,
+ type StaticHandlerContext,
+} from "react-router";
import { createRequestHandler } from "../../lib/server-runtime/server";
import { ServerMode } from "../../lib/server-runtime/mode";
@@ -24,7 +27,7 @@ function spyConsole() {
return spy;
}
-describe.skip("server", () => {
+describe("server", () => {
let routeId = "root";
let build: ServerBuild = {
ssr: true,
@@ -72,20 +75,20 @@ describe.skip("server", () => {
});
let allowThrough = [
- ["GET", "/"],
- ["GET", "/?_data=root"],
- ["POST", "/"],
- ["POST", "/?_data=root"],
- ["PUT", "/"],
- ["PUT", "/?_data=root"],
- ["DELETE", "/"],
- ["DELETE", "/?_data=root"],
- ["PATCH", "/"],
- ["PATCH", "/?_data=root"],
+ ["GET", "/", "COMPONENT"],
+ ["GET", "/_root.data", "LOADER"],
+ ["POST", "/", "COMPONENT"],
+ ["POST", "/_root.data", "ACTION"],
+ ["PUT", "/", "COMPONENT"],
+ ["PUT", "/_root.data", "ACTION"],
+ ["DELETE", "/", "COMPONENT"],
+ ["DELETE", "/_root.data", "ACTION"],
+ ["PATCH", "/", "COMPONENT"],
+ ["PATCH", "/_root.data", "ACTION"],
];
it.each(allowThrough)(
`allows through %s request to %s`,
- async (method, to) => {
+ async (method, to, expected) => {
let handler = createRequestHandler(build);
let response = await handler(
new Request(`http://localhost:3000${to}`, {
@@ -96,11 +99,6 @@ describe.skip("server", () => {
expect(response.status).toBe(200);
let text = await response.text();
expect(text).toContain(method);
- let expected = !to.includes("?_data=root")
- ? "COMPONENT"
- : method === "GET"
- ? "LOADER"
- : "ACTION";
expect(text).toContain(expected);
expect(spy.console).not.toHaveBeenCalled();
}
@@ -116,6 +114,203 @@ describe.skip("server", () => {
expect(await response.text()).toBe("");
});
+
+ it("accepts proper values from getLoadContext (without middleware)", async () => {
+ let handler = createRequestHandler({
+ ssr: true,
+ entry: {
+ module: {
+ default: async (request) => {
+ return new Response(
+ `${request.method}, ${request.url} COMPONENT`
+ );
+ },
+ },
+ },
+ routes: {
+ root: {
+ id: "root",
+ path: "",
+ module: {
+ loader: ({ context }) => context.foo,
+ default: () => "COMPONENT",
+ },
+ },
+ },
+ assets: {
+ routes: {
+ root: {
+ clientActionModule: undefined,
+ clientLoaderModule: undefined,
+ clientMiddlewareModule: undefined,
+ hasAction: true,
+ hasClientAction: false,
+ hasClientLoader: false,
+ hasClientMiddleware: false,
+ hasErrorBoundary: false,
+ hasLoader: true,
+ hydrateFallbackModule: undefined,
+ id: routeId,
+ module: routeId,
+ path: "",
+ },
+ },
+ entry: { imports: [], module: "" },
+ url: "",
+ version: "",
+ },
+ future: {
+ unstable_middleware: false,
+ },
+ prerender: [],
+ publicPath: "/",
+ assetsBuildDirectory: "/",
+ isSpaMode: false,
+ });
+ let response = await handler(
+ new Request("http://localhost:3000/_root.data"),
+ {
+ foo: "FOO",
+ }
+ );
+
+ expect(await response.text()).toContain("FOO");
+ });
+
+ it("accepts proper values from getLoadContext (with middleware)", async () => {
+ let fooContext = unstable_createContext();
+ let handler = createRequestHandler({
+ ssr: true,
+ entry: {
+ module: {
+ default: async (request) => {
+ return new Response(
+ `${request.method}, ${request.url} COMPONENT`
+ );
+ },
+ },
+ },
+ routes: {
+ root: {
+ id: "root",
+ path: "",
+ module: {
+ loader: ({ context }) => context.get(fooContext),
+ default: () => "COMPONENT",
+ },
+ },
+ },
+ assets: {
+ routes: {
+ root: {
+ clientActionModule: undefined,
+ clientLoaderModule: undefined,
+ clientMiddlewareModule: undefined,
+ hasAction: true,
+ hasClientAction: false,
+ hasClientLoader: false,
+ hasClientMiddleware: false,
+ hasErrorBoundary: false,
+ hasLoader: true,
+ hydrateFallbackModule: undefined,
+ id: routeId,
+ module: routeId,
+ path: "",
+ },
+ },
+ entry: { imports: [], module: "" },
+ url: "",
+ version: "",
+ },
+ future: {
+ unstable_middleware: true,
+ },
+ prerender: [],
+ publicPath: "/",
+ assetsBuildDirectory: "/",
+ isSpaMode: false,
+ });
+ let response = await handler(
+ new Request("http://localhost:3000/_root.data"),
+ // @ts-expect-error In apps the expected type is handled via the Future interface
+ new Map([[fooContext, "FOO"]])
+ );
+
+ expect(await response.text()).toContain("FOO");
+ });
+
+ it("errors if an invalid value is returned from getLoadContext (with middleware)", async () => {
+ let handleErrorSpy = jest.fn();
+ let handler = createRequestHandler({
+ ssr: true,
+ entry: {
+ module: {
+ handleError: handleErrorSpy,
+ default: async (request) => {
+ return new Response(
+ `${request.method}, ${request.url} COMPONENT`
+ );
+ },
+ },
+ },
+ routes: {
+ root: {
+ id: "root",
+ path: "",
+ module: {
+ loader: ({ context }) => context.foo,
+ default: () => "COMPONENT",
+ },
+ },
+ },
+ assets: {
+ routes: {
+ root: {
+ clientActionModule: undefined,
+ clientLoaderModule: undefined,
+ clientMiddlewareModule: undefined,
+ hasAction: true,
+ hasClientAction: false,
+ hasClientLoader: false,
+ hasClientMiddleware: false,
+ hasErrorBoundary: false,
+ hasLoader: true,
+ hydrateFallbackModule: undefined,
+ id: routeId,
+ module: routeId,
+ path: "",
+ },
+ },
+ entry: { imports: [], module: "" },
+ url: "",
+ version: "",
+ },
+ future: {
+ unstable_middleware: true,
+ },
+ prerender: [],
+ publicPath: "/",
+ assetsBuildDirectory: "/",
+ isSpaMode: false,
+ });
+
+ let response = await handler(
+ new Request("http://localhost:3000/_root.data"),
+ {
+ foo: "FOO",
+ }
+ );
+
+ expect(response.status).toBe(500);
+ expect(await response.text()).toContain("Unexpected Server Error");
+ expect(handleErrorSpy).toHaveBeenCalledTimes(1);
+ expect(handleErrorSpy.mock.calls[0][0]).toMatchInlineSnapshot(`
+ [Error: Unable to create initial \`unstable_RouterContextProvider\` instance. Please confirm you are returning an instance of \`Map\` from your \`getLoadContext\` function.
+
+ Error: TypeError: init is not iterable]
+ `);
+ handleErrorSpy.mockRestore();
+ });
});
});
@@ -1144,10 +1339,7 @@ describe("shared server runtime", () => {
let context = calls[0][3].staticHandlerContext as StaticHandlerContext;
expect(context.errors).toBeTruthy();
expect(context.errors!.root.status).toBe(400);
- expect(context.loaderData).toEqual({
- root: null,
- "routes/test": null,
- });
+ expect(context.loaderData).toEqual({});
});
test("thrown action responses bubble up for index routes", async () => {
@@ -1191,10 +1383,7 @@ describe("shared server runtime", () => {
let context = calls[0][3].staticHandlerContext as StaticHandlerContext;
expect(context.errors).toBeTruthy();
expect(context.errors!.root.status).toBe(400);
- expect(context.loaderData).toEqual({
- root: null,
- "routes/_index": null,
- });
+ expect(context.loaderData).toEqual({});
});
test("thrown action responses catch deep", async () => {
@@ -1240,7 +1429,6 @@ describe("shared server runtime", () => {
expect(context.errors!["routes/test"].status).toBe(400);
expect(context.loaderData).toEqual({
root: "root",
- "routes/test": null,
});
});
@@ -1287,7 +1475,6 @@ describe("shared server runtime", () => {
expect(context.errors!["routes/_index"].status).toBe(400);
expect(context.loaderData).toEqual({
root: "root",
- "routes/_index": null,
});
});
@@ -1342,8 +1529,6 @@ describe("shared server runtime", () => {
expect(context.errors!["routes/__layout"].data).toBe("action");
expect(context.loaderData).toEqual({
root: "root",
- "routes/__layout": null,
- "routes/__layout/test": null,
});
});
@@ -1398,8 +1583,6 @@ describe("shared server runtime", () => {
expect(context.errors!["routes/__layout"].data).toBe("action");
expect(context.loaderData).toEqual({
root: "root",
- "routes/__layout": null,
- "routes/__layout/index": null,
});
});
@@ -1533,10 +1716,7 @@ describe("shared server runtime", () => {
expect(context.errors!.root).toBeInstanceOf(Error);
expect(context.errors!.root.message).toBe("Unexpected Server Error");
expect(context.errors!.root.stack).toBeUndefined();
- expect(context.loaderData).toEqual({
- root: null,
- "routes/test": null,
- });
+ expect(context.loaderData).toEqual({});
});
test("action errors bubble up for index routes", async () => {
@@ -1582,10 +1762,7 @@ describe("shared server runtime", () => {
expect(context.errors!.root).toBeInstanceOf(Error);
expect(context.errors!.root.message).toBe("Unexpected Server Error");
expect(context.errors!.root.stack).toBeUndefined();
- expect(context.loaderData).toEqual({
- root: null,
- "routes/_index": null,
- });
+ expect(context.loaderData).toEqual({});
});
test("action errors catch deep", async () => {
@@ -1635,7 +1812,6 @@ describe("shared server runtime", () => {
expect(context.errors!["routes/test"].stack).toBeUndefined();
expect(context.loaderData).toEqual({
root: "root",
- "routes/test": null,
});
});
@@ -1686,7 +1862,6 @@ describe("shared server runtime", () => {
expect(context.errors!["routes/_index"].stack).toBeUndefined();
expect(context.loaderData).toEqual({
root: "root",
- "routes/_index": null,
});
});
@@ -1745,8 +1920,6 @@ describe("shared server runtime", () => {
expect(context.errors!["routes/__layout"].stack).toBeUndefined();
expect(context.loaderData).toEqual({
root: "root",
- "routes/__layout": null,
- "routes/__layout/test": null,
});
});
@@ -1805,8 +1978,6 @@ describe("shared server runtime", () => {
expect(context.errors!["routes/__layout"].stack).toBeUndefined();
expect(context.loaderData).toEqual({
root: "root",
- "routes/__layout": null,
- "routes/__layout/index": null,
});
});
diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts
index 2fc07c9e73..f72fd36a32 100644
--- a/packages/react-router/index.ts
+++ b/packages/react-router/index.ts
@@ -317,7 +317,10 @@ export {
} from "./lib/context";
/** @internal */
-export { mapRouteProperties as UNSAFE_mapRouteProperties } from "./lib/components";
+export {
+ hydrationRouteProperties as UNSAFE_hydrationRouteProperties,
+ mapRouteProperties as UNSAFE_mapRouteProperties,
+} from "./lib/components";
/** @internal */
export { FrameworkContext as UNSAFE_FrameworkContext } from "./lib/dom/ssr/components";
diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx
index 18f52b93f2..f0a7346e47 100644
--- a/packages/react-router/lib/components.tsx
+++ b/packages/react-router/lib/components.tsx
@@ -131,6 +131,11 @@ export function mapRouteProperties(route: RouteObject) {
return updates;
}
+export const hydrationRouteProperties: (keyof RouteObject)[] = [
+ "HydrateFallback",
+ "hydrateFallbackElement",
+];
+
export interface MemoryRouterOpts {
/**
* Basename path for the application.
@@ -194,6 +199,7 @@ export function createMemoryRouter(
}),
hydrationData: opts?.hydrationData,
routes,
+ hydrationRouteProperties,
mapRouteProperties,
dataStrategy: opts?.dataStrategy,
patchRoutesOnNavigation: opts?.patchRoutesOnNavigation,
diff --git a/packages/react-router/lib/dom-export/hydrated-router.tsx b/packages/react-router/lib/dom-export/hydrated-router.tsx
index 388820ccd7..ea3dddc7db 100644
--- a/packages/react-router/lib/dom-export/hydrated-router.tsx
+++ b/packages/react-router/lib/dom-export/hydrated-router.tsx
@@ -21,6 +21,7 @@ import {
UNSAFE_shouldHydrateRouteLoader as shouldHydrateRouteLoader,
UNSAFE_useFogOFWarDiscovery as useFogOFWarDiscovery,
UNSAFE_mapRouteProperties as mapRouteProperties,
+ UNSAFE_hydrationRouteProperties as hydrationRouteProperties,
UNSAFE_createClientRoutesWithHMRRevalidationOptOut as createClientRoutesWithHMRRevalidationOptOut,
matchRoutes,
} from "react-router";
@@ -201,16 +202,29 @@ function createHydratedRouter({
basename: ssrInfo.context.basename,
unstable_getContext,
hydrationData,
+ hydrationRouteProperties,
mapRouteProperties,
future: {
unstable_middleware: ssrInfo.context.future.unstable_middleware,
},
dataStrategy: getSingleFetchDataStrategy(
- ssrInfo.manifest,
- ssrInfo.routeModules,
+ () => router,
+ (routeId: string) => {
+ let manifestRoute = ssrInfo!.manifest.routes[routeId];
+ invariant(manifestRoute, "Route not found in manifest/routeModules");
+ let routeModule = ssrInfo!.routeModules[routeId];
+ return {
+ hasLoader: manifestRoute.hasLoader,
+ hasClientLoader: manifestRoute.hasClientLoader,
+ // In some cases the module may not be loaded yet and we don't care
+ // if it's got shouldRevalidate or not
+ hasShouldRevalidate: routeModule
+ ? routeModule.shouldRevalidate != null
+ : undefined,
+ };
+ },
ssrInfo.context.ssr,
- ssrInfo.context.basename,
- () => router
+ ssrInfo.context.basename
),
patchRoutesOnNavigation: getPatchRoutesOnNavigationFunction(
ssrInfo.manifest,
diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx
index 50a456ef9f..e280d79691 100644
--- a/packages/react-router/lib/dom/lib.tsx
+++ b/packages/react-router/lib/dom/lib.tsx
@@ -66,7 +66,11 @@ import {
mergeRefs,
usePrefetchBehavior,
} from "./ssr/components";
-import { Router, mapRouteProperties } from "../components";
+import {
+ Router,
+ mapRouteProperties,
+ hydrationRouteProperties,
+} from "../components";
import type {
RouteObject,
NavigateOptions,
@@ -186,6 +190,7 @@ export function createBrowserRouter(
hydrationData: opts?.hydrationData || parseHydrationData(),
routes,
mapRouteProperties,
+ hydrationRouteProperties,
dataStrategy: opts?.dataStrategy,
patchRoutesOnNavigation: opts?.patchRoutesOnNavigation,
window: opts?.window,
@@ -209,6 +214,7 @@ export function createHashRouter(
hydrationData: opts?.hydrationData || parseHydrationData(),
routes,
mapRouteProperties,
+ hydrationRouteProperties,
dataStrategy: opts?.dataStrategy,
patchRoutesOnNavigation: opts?.patchRoutesOnNavigation,
window: opts?.window,
diff --git a/packages/react-router/lib/dom/ssr/routes.tsx b/packages/react-router/lib/dom/ssr/routes.tsx
index 27cb9feb2f..985b3246ac 100644
--- a/packages/react-router/lib/dom/ssr/routes.tsx
+++ b/packages/react-router/lib/dom/ssr/routes.tsx
@@ -4,12 +4,11 @@ import type { HydrationState } from "../../router/router";
import type {
ActionFunctionArgs,
LoaderFunctionArgs,
- unstable_MiddlewareFunction,
RouteManifest,
ShouldRevalidateFunction,
ShouldRevalidateFunctionArgs,
} from "../../router/utils";
-import { ErrorResponseImpl } from "../../router/utils";
+import { ErrorResponseImpl, compilePath } from "../../router/utils";
import type { RouteModule, RouteModules } from "./routeModules";
import { loadRouteModule } from "./routeModules";
import type { FutureConfig } from "./entry";
@@ -312,6 +311,7 @@ export function createClientRoutes(
unstable_middleware: routeModule.unstable_clientMiddleware,
handle: routeModule.handle,
shouldRevalidate: getShouldRevalidateFunction(
+ dataRoute.path,
routeModule,
route,
ssr,
@@ -524,6 +524,7 @@ export function createClientRoutes(
shouldRevalidate: async () => {
let lazyRoute = await getLazyRoute();
return getShouldRevalidateFunction(
+ dataRoute.path,
lazyRoute,
route,
ssr,
@@ -556,6 +557,7 @@ export function createClientRoutes(
}
function getShouldRevalidateFunction(
+ path: string | undefined,
route: Partial,
manifestRoute: Omit,
ssr: boolean,
@@ -572,17 +574,29 @@ function getShouldRevalidateFunction(
// When prerendering is enabled with `ssr:false`, any `loader` data is
// statically generated at build time so if we have a `loader` but not a
- // `clientLoader`, we disable revalidation by default since we can't be sure
- // if a `.data` file was pre-rendered. If users are somehow re-generating
- // updated versions of these on the backend they can still opt-into
- // revalidation which will make the `.data` request
+ // `clientLoader`, we only revalidate if the route's params changed since we
+ // can't be sure if a `.data` file was pre-rendered otherwise.
+ //
+ // I.e., If I have a parent and a child route and I only prerender `/parent`,
+ // we can't have parent revalidate when going from `/parent -> /parent/child`
+ // because `/parent/child.data` doesn't exist.
+ //
+ // If users are somehow re-generating updated versions of these on the backend
+ // they can still opt-into revalidation which will make the `.data` request
if (!ssr && manifestRoute.hasLoader && !manifestRoute.hasClientLoader) {
+ let myParams = path ? compilePath(path)[1].map((p) => p.paramName) : [];
+ const didParamsChange = (opts: ShouldRevalidateFunctionArgs) =>
+ myParams.some((p) => opts.currentParams[p] !== opts.nextParams[p]);
+
if (route.shouldRevalidate) {
let fn = route.shouldRevalidate;
return (opts: ShouldRevalidateFunctionArgs) =>
- fn({ ...opts, defaultShouldRevalidate: false });
+ fn({
+ ...opts,
+ defaultShouldRevalidate: didParamsChange(opts),
+ });
} else {
- return () => false;
+ return (opts: ShouldRevalidateFunctionArgs) => didParamsChange(opts);
}
}
diff --git a/packages/react-router/lib/dom/ssr/single-fetch.tsx b/packages/react-router/lib/dom/ssr/single-fetch.tsx
index 6471847f3e..a1cd20a27f 100644
--- a/packages/react-router/lib/dom/ssr/single-fetch.tsx
+++ b/packages/react-router/lib/dom/ssr/single-fetch.tsx
@@ -1,12 +1,11 @@
import * as React from "react";
import { decode } from "turbo-stream";
import type { Router as DataRouter } from "../../router/router";
-import { isResponse, runMiddlewarePipeline } from "../../router/router";
+import { isResponse } from "../../router/router";
import type {
DataStrategyFunction,
DataStrategyFunctionArgs,
DataStrategyResult,
- DataStrategyMatch,
} from "../../router/utils";
import {
ErrorResponseImpl,
@@ -16,11 +15,9 @@ import {
stripBasename,
} from "../../router/utils";
import { createRequestInit } from "./data";
-import type { AssetsManifest, EntryContext } from "./entry";
+import type { EntryContext } from "./entry";
import { escapeHtml } from "./markup";
-import type { RouteModule, RouteModules } from "./routeModules";
import invariant from "./invariant";
-import type { EntryRoute } from "./routes";
export const SingleFetchRedirectSymbol = Symbol("SingleFetchRedirect");
@@ -32,15 +29,22 @@ export type SingleFetchRedirectResult = {
replace: boolean;
};
+// Shared/serializable type used by both turbo-stream and RSC implementations
+type DecodedSingleFetchResults =
+ | { routes: { [key: string]: SingleFetchResult } }
+ | { redirect: SingleFetchRedirectResult };
+
+// This and SingleFetchResults are only used over the wire, and are converted to
+// DecodedSingleFetchResults in `fetchAndDecode`. This way turbo-stream/RSC
+// can use the same `unwrapSingleFetchResult` implementation.
export type SingleFetchResult =
| { data: unknown }
| { error: unknown }
| SingleFetchRedirectResult;
-export type SingleFetchResults = {
- [key: string]: SingleFetchResult;
- [SingleFetchRedirectSymbol]?: SingleFetchRedirectResult;
-};
+export type SingleFetchResults =
+ | { [key: string]: SingleFetchResult }
+ | { [SingleFetchRedirectSymbol]: SingleFetchRedirectResult };
interface StreamTransferProps {
context: EntryContext;
@@ -50,6 +54,16 @@ interface StreamTransferProps {
nonce?: string;
}
+// Some status codes are not permitted to have bodies, so we want to just
+// treat those as "no data" instead of throwing an exception:
+// https://datatracker.ietf.org/doc/html/rfc9110#name-informational-1xx
+// https://datatracker.ietf.org/doc/html/rfc9110#name-204-no-content
+// https://datatracker.ietf.org/doc/html/rfc9110#name-205-reset-content
+//
+// Note: 304 is not included here because the browser should fill those responses
+// with the cached body content.
+export const NO_BODY_STATUS_CODES = new Set([100, 101, 204, 205]);
+
// StreamTransfer recursively renders down chunks of the `serverHandoffStream`
// into the client-side `streamController`
export function StreamTransfer({
@@ -133,32 +147,55 @@ export function StreamTransfer({
}
}
-function handleMiddlewareError(error: unknown, routeId: string) {
- return { [routeId]: { type: "error", result: error } };
-}
+type GetRouteInfoFunction = (routeId: string) => {
+ hasLoader: boolean;
+ hasClientLoader: boolean; // TODO: Can this be read from match.route?
+ hasShouldRevalidate: boolean | undefined; // TODO: Can this be read from match.route?
+};
+
+type FetchAndDecodeFunction = (
+ args: DataStrategyFunctionArgs,
+ basename: string | undefined,
+ targetRoutes?: string[]
+) => Promise<{ status: number; data: DecodedSingleFetchResults }>;
export function getSingleFetchDataStrategy(
- manifest: AssetsManifest,
- routeModules: RouteModules,
+ getRouter: () => DataRouter,
+ getRouteInfo: GetRouteInfoFunction,
ssr: boolean,
- basename: string | undefined,
- getRouter: () => DataRouter
+ basename: string | undefined
+): DataStrategyFunction {
+ let dataStrategy = getSingleFetchDataStrategyImpl(
+ getRouter,
+ getRouteInfo,
+ fetchAndDecodeViaTurboStream,
+ ssr,
+ basename
+ );
+ return async (args) => args.unstable_runClientMiddleware(dataStrategy);
+}
+
+export function getSingleFetchDataStrategyImpl(
+ getRouter: () => DataRouter,
+ getRouteInfo: GetRouteInfoFunction,
+ fetchAndDecode: FetchAndDecodeFunction,
+ ssr: boolean,
+ basename: string | undefined
): DataStrategyFunction {
return async (args) => {
let { request, matches, fetcherKey } = args;
+ let router = getRouter();
// Actions are simple and behave the same for navigations and fetchers
if (request.method !== "GET") {
- return runMiddlewarePipeline(
- args,
- false,
- () => singleFetchActionStrategy(request, matches, basename),
- handleMiddlewareError
- ) as Promise>;
+ return singleFetchActionStrategy(args, fetchAndDecode, basename);
}
- // TODO: Enable middleware for this flow
- if (!ssr) {
+ let foundRevalidatingServerLoader = matches.some((m) => {
+ let { hasLoader, hasClientLoader } = getRouteInfo(m.route.id);
+ return m.unstable_shouldCallHandler() && hasLoader && !hasClientLoader;
+ });
+ if (!ssr && !foundRevalidatingServerLoader) {
// If this is SPA mode, there won't be any loaders below root and we'll
// disable single fetch. We have to keep the `dataStrategy` defined for
// SPA mode because we may load a SPA fallback page but then navigate into
@@ -191,71 +228,43 @@ export function getSingleFetchDataStrategy(
// errored otherwise
// - So it's safe to make the call knowing there will be a `.data` file on
// the other end
- let foundRevalidatingServerLoader = matches.some(
- (m) =>
- m.shouldLoad &&
- manifest.routes[m.route.id]?.hasLoader &&
- !manifest.routes[m.route.id]?.hasClientLoader
- );
- if (!foundRevalidatingServerLoader) {
- return runMiddlewarePipeline(
- args,
- false,
- () => nonSsrStrategy(manifest, request, matches, basename),
- handleMiddlewareError
- ) as Promise>;
- }
+ return nonSsrStrategy(args, getRouteInfo, fetchAndDecode, basename);
}
// Fetcher loads are singular calls to one loader
if (fetcherKey) {
- return runMiddlewarePipeline(
- args,
- false,
- () => singleFetchLoaderFetcherStrategy(request, matches, basename),
- handleMiddlewareError
- ) as Promise>;
+ return singleFetchLoaderFetcherStrategy(args, fetchAndDecode, basename);
}
// Navigational loads are more complex...
- return runMiddlewarePipeline(
+ return singleFetchLoaderNavigationStrategy(
args,
- false,
- () =>
- singleFetchLoaderNavigationStrategy(
- manifest,
- routeModules,
- ssr,
- getRouter(),
- request,
- matches,
- basename
- ),
- handleMiddlewareError
- ) as Promise>;
+ router,
+ getRouteInfo,
+ fetchAndDecode,
+ ssr,
+ basename
+ );
};
}
// Actions are simple since they're singular calls to the server for both
// navigations and fetchers)
async function singleFetchActionStrategy(
- request: Request,
- matches: DataStrategyFunctionArgs["matches"],
+ args: DataStrategyFunctionArgs,
+ fetchAndDecode: FetchAndDecodeFunction,
basename: string | undefined
) {
- let actionMatch = matches.find((m) => m.shouldLoad);
+ let actionMatch = args.matches.find((m) => m.unstable_shouldCallHandler());
invariant(actionMatch, "No action match found");
let actionStatus: number | undefined = undefined;
let result = await actionMatch.resolve(async (handler) => {
let result = await handler(async () => {
- let url = singleFetchUrl(request.url, basename);
- let init = await createRequestInit(request);
- let { data, status } = await fetchAndDecode(url, init);
+ let { data, status } = await fetchAndDecode(args, basename, [
+ actionMatch!.route.id,
+ ]);
actionStatus = status;
- return unwrapSingleFetchResult(
- data as SingleFetchResult,
- actionMatch!.route.id
- );
+ return unwrapSingleFetchResult(data, actionMatch!.route.id);
});
return result;
});
@@ -276,24 +285,29 @@ async function singleFetchActionStrategy(
// We want to opt-out of Single Fetch when we aren't in SSR mode
async function nonSsrStrategy(
- manifest: AssetsManifest,
- request: Request,
- matches: DataStrategyFunctionArgs["matches"],
+ args: DataStrategyFunctionArgs,
+ getRouteInfo: GetRouteInfoFunction,
+ fetchAndDecode: FetchAndDecodeFunction,
basename: string | undefined
) {
- let matchesToLoad = matches.filter((m) => m.shouldLoad);
- let url = stripIndexParam(singleFetchUrl(request.url, basename));
- let init = await createRequestInit(request);
+ let matchesToLoad = args.matches.filter((m) =>
+ m.unstable_shouldCallHandler()
+ );
let results: Record = {};
await Promise.all(
matchesToLoad.map((m) =>
m.resolve(async (handler) => {
try {
+ let { hasClientLoader } = getRouteInfo(m.route.id);
// Need to pass through a `singleFetch` override handler so
// clientLoader's can still call server loaders through `.data`
// requests
- let result = manifest.routes[m.route.id]?.hasClientLoader
- ? await fetchSingleLoader(handler, url, init, m.route.id)
+ let routeId = m.route.id;
+ let result = hasClientLoader
+ ? await handler(async () => {
+ let { data } = await fetchAndDecode(args, basename, [routeId]);
+ return unwrapSingleFetchResult(data, routeId);
+ })
: await handler();
results[m.route.id] = { type: "data", result };
} catch (e) {
@@ -308,121 +322,91 @@ async function nonSsrStrategy(
// Loaders are trickier since we only want to hit the server once, so we
// create a singular promise for all server-loader routes to latch onto.
async function singleFetchLoaderNavigationStrategy(
- manifest: AssetsManifest,
- routeModules: RouteModules,
- ssr: boolean,
+ args: DataStrategyFunctionArgs,
router: DataRouter,
- request: Request,
- matches: DataStrategyFunctionArgs["matches"],
+ getRouteInfo: GetRouteInfoFunction,
+ fetchAndDecode: FetchAndDecodeFunction,
+ ssr: boolean,
basename: string | undefined
) {
- // Track which routes need a server load - in case we need to tack on a
- // `_routes` param
+ // Track which routes need a server load for use in a `_routes` param
let routesParams = new Set();
- // We only add `_routes` when one or more routes opts out of a load via
- // `shouldRevalidate` or `clientLoader`
+ // Only add `_routes` when at least 1 route opts out via `shouldRevalidate`/`clientLoader`
let foundOptOutRoute = false;
- // Deferreds for each route so we can be sure they've all loaded via
- // `match.resolve()`, and a singular promise that can tell us all routes
- // have been resolved
- let routeDfds = matches.map(() => createDeferred());
- let routesLoadedPromise = Promise.all(routeDfds.map((d) => d.promise));
+ // Deferreds per-route so we can be sure they've all loaded via `match.resolve()`
+ let routeDfds = args.matches.map(() => createDeferred());
- // Deferred that we'll use for the call to the server that each match can
- // await and parse out it's specific result
- let singleFetchDfd = createDeferred();
-
- // Base URL and RequestInit for calls to the server
- let url = stripIndexParam(singleFetchUrl(request.url, basename));
- let init = await createRequestInit(request);
+ // Deferred we'll use for the singleular call to the server
+ let singleFetchDfd = createDeferred();
// We'll build up this results object as we loop through matches
let results: Record = {};
let resolvePromise = Promise.all(
- matches.map(async (m, i) =>
+ args.matches.map(async (m, i) =>
m.resolve(async (handler) => {
routeDfds[i].resolve();
-
- let manifestRoute = manifest.routes[m.route.id];
-
- // Note: If this logic changes for routes that should not participate
- // in Single Fetch, make sure you update getLowestLoadingIndex above
- // as well
- if (!m.shouldLoad) {
- // If we're not yet initialized and this is the initial load, respect
- // `shouldLoad` because we're only dealing with `clientLoader.hydrate`
- // routes which will fall into the `clientLoader` section below.
- if (!router.state.initialized) {
- return;
- }
-
- // Otherwise, we opt out if we currently have data and a
- // `shouldRevalidate` function. This implies that the user opted out
- // via `shouldRevalidate`
- if (
- m.route.id in router.state.loaderData &&
- manifestRoute &&
- m.route.shouldRevalidate
- ) {
- if (manifestRoute.hasLoader) {
- // If we have a server loader, make sure we don't include it in the
- // single fetch .data request
- foundOptOutRoute = true;
- }
- return;
- }
+ let routeId = m.route.id;
+ let { hasLoader, hasClientLoader, hasShouldRevalidate } =
+ getRouteInfo(routeId);
+
+ let defaultShouldRevalidate =
+ !m.unstable_shouldRevalidateArgs ||
+ m.unstable_shouldRevalidateArgs.actionStatus == null ||
+ m.unstable_shouldRevalidateArgs.actionStatus < 400;
+ let shouldCall = m.unstable_shouldCallHandler(defaultShouldRevalidate);
+
+ if (!shouldCall) {
+ // If this route opted out, don't include in the .data request
+ foundOptOutRoute ||=
+ m.unstable_shouldRevalidateArgs != null && // This is a revalidation,
+ hasLoader && // for a route with a server loader,
+ hasShouldRevalidate === true; // and a shouldRevalidate function
+ return;
}
// When a route has a client loader, it opts out of the singular call and
// calls it's server loader via `serverLoader()` using a `?_routes` param
- if (manifestRoute && manifestRoute.hasClientLoader) {
- if (manifestRoute.hasLoader) {
+ if (hasClientLoader) {
+ if (hasLoader) {
foundOptOutRoute = true;
}
try {
- let result = await fetchSingleLoader(
- handler,
- url,
- init,
- m.route.id
- );
- results[m.route.id] = { type: "data", result };
+ let result = await handler(async () => {
+ let { data } = await fetchAndDecode(args, basename, [routeId]);
+ return unwrapSingleFetchResult(data, routeId);
+ });
+
+ results[routeId] = { type: "data", result };
} catch (e) {
- results[m.route.id] = { type: "error", result: e };
+ results[routeId] = { type: "error", result: e };
}
return;
}
// Load this route on the server if it has a loader
- if (manifestRoute && manifestRoute.hasLoader) {
- routesParams.add(m.route.id);
+ if (hasLoader) {
+ routesParams.add(routeId);
}
// Lump this match in with the others on a singular promise
try {
let result = await handler(async () => {
let data = await singleFetchDfd.promise;
- return unwrapSingleFetchResults(data, m.route.id);
+ return unwrapSingleFetchResult(data, routeId);
});
- results[m.route.id] = {
- type: "data",
- result,
- };
+ results[routeId] = { type: "data", result };
} catch (e) {
- results[m.route.id] = {
- type: "error",
- result: e,
- };
+ results[routeId] = { type: "error", result: e };
}
})
)
);
// Wait for all routes to resolve above before we make the HTTP call
- await routesLoadedPromise;
+ await Promise.all(routeDfds.map((d) => d.promise));
// We can skip the server call:
// - On initial hydration - only clientLoaders can pass through via `clientLoader.hydrate`
@@ -437,24 +421,17 @@ async function singleFetchLoaderNavigationStrategy(
) {
singleFetchDfd.resolve({});
} else {
+ // When routes have opted out, add a `_routes` param to filter server loaders
+ // Skipped in `ssr:false` because we expect to be loading static `.data` files
+ let targetRoutes =
+ ssr && foundOptOutRoute && routesParams.size > 0
+ ? [...routesParams.keys()]
+ : undefined;
try {
- // When one or more routes have opted out, we add a _routes param to
- // limit the loaders to those that have a server loader and did not
- // opt out
- if (ssr && foundOptOutRoute && routesParams.size > 0) {
- url.searchParams.set(
- "_routes",
- matches
- .filter((m) => routesParams.has(m.route.id))
- .map((m) => m.route.id)
- .join(",")
- );
- }
-
- let data = await fetchAndDecode(url, init);
- singleFetchDfd.resolve(data.data as SingleFetchResults);
+ let data = await fetchAndDecode(args, basename, targetRoutes);
+ singleFetchDfd.resolve(data.data);
} catch (e) {
- singleFetchDfd.reject(e as Error);
+ singleFetchDfd.reject(e);
}
}
@@ -465,36 +442,22 @@ async function singleFetchLoaderNavigationStrategy(
// Fetcher loader calls are much simpler than navigational loader calls
async function singleFetchLoaderFetcherStrategy(
- request: Request,
- matches: DataStrategyFunctionArgs["matches"],
+ args: DataStrategyFunctionArgs,
+ fetchAndDecode: FetchAndDecodeFunction,
basename: string | undefined
) {
- let fetcherMatch = matches.find((m) => m.shouldLoad);
+ let fetcherMatch = args.matches.find((m) => m.unstable_shouldCallHandler());
invariant(fetcherMatch, "No fetcher match found");
- let result = await fetcherMatch.resolve(async (handler) => {
- let url = stripIndexParam(singleFetchUrl(request.url, basename));
- let init = await createRequestInit(request);
- return fetchSingleLoader(handler, url, init, fetcherMatch!.route.id);
- });
+ let routeId = fetcherMatch.route.id;
+ let result = await fetcherMatch.resolve(async (handler) =>
+ handler(async () => {
+ let { data } = await fetchAndDecode(args, basename, [routeId]);
+ return unwrapSingleFetchResult(data, routeId);
+ })
+ );
return { [fetcherMatch.route.id]: result };
}
-function fetchSingleLoader(
- handler: Parameters<
- NonNullable[0]>
- >[0],
- url: URL,
- init: RequestInit,
- routeId: string
-) {
- return handler(async () => {
- let singleLoaderUrl = new URL(url);
- singleLoaderUrl.searchParams.set("_routes", routeId);
- let { data } = await fetchAndDecode(singleLoaderUrl, init);
- return unwrapSingleFetchResults(data as SingleFetchResults, routeId);
- });
-}
-
function stripIndexParam(url: URL) {
let indexValues = url.searchParams.getAll("index");
url.searchParams.delete("index");
@@ -538,11 +501,21 @@ export function singleFetchUrl(
return url;
}
-async function fetchAndDecode(
- url: URL,
- init: RequestInit
-): Promise<{ status: number; data: unknown }> {
- let res = await fetch(url, init);
+async function fetchAndDecodeViaTurboStream(
+ args: DataStrategyFunctionArgs,
+ basename: string | undefined,
+ targetRoutes?: string[]
+): Promise<{ status: number; data: DecodedSingleFetchResults }> {
+ let { request } = args;
+ let url = singleFetchUrl(request.url, basename);
+ if (request.method === "GET") {
+ url = stripIndexParam(url);
+ if (targetRoutes) {
+ url.searchParams.set("_routes", targetRoutes.join(","));
+ }
+ }
+
+ let res = await fetch(url, await createRequestInit(request));
// If this 404'd without hitting the running server (most likely in a
// pre-rendered app using a CDN), then bubble a standard 404 ErrorResponse
@@ -550,27 +523,42 @@ async function fetchAndDecode(
throw new ErrorResponseImpl(404, "Not Found", true);
}
- // some status codes are not permitted to have bodies, so we want to just
- // treat those as "no data" instead of throwing an exception.
- // 304 is not included here because the browser should fill those responses
- // with the cached body content.
- const NO_BODY_STATUS_CODES = new Set([100, 101, 204, 205]);
if (NO_BODY_STATUS_CODES.has(res.status)) {
- if (!init.method || init.method === "GET") {
- // SingleFetchResults can just have no routeId keys which will result
- // in no data for all routes
- return { status: res.status, data: {} };
- } else {
- // SingleFetchResult is for a singular route and can specify no data
- return { status: res.status, data: { data: undefined } };
+ let routes: { [key: string]: SingleFetchResult } = {};
+ // We get back just a single result for action requests - normalize that
+ // to a DecodedSingleFetchResults shape here
+ if (targetRoutes && request.method !== "GET") {
+ routes[targetRoutes[0]] = { data: undefined };
}
+ return {
+ status: res.status,
+ data: { routes },
+ };
}
invariant(res.body, "No response body to decode");
try {
let decoded = await decodeViaTurboStream(res.body, window);
- return { status: res.status, data: decoded.value };
+ let data: DecodedSingleFetchResults;
+ if (request.method === "GET") {
+ let typed = decoded.value as SingleFetchResults;
+ if (SingleFetchRedirectSymbol in typed) {
+ data = { redirect: typed[SingleFetchRedirectSymbol] };
+ } else {
+ data = { routes: typed };
+ }
+ } else {
+ let typed = decoded.value as SingleFetchResult;
+ let routeId = targetRoutes?.[0];
+ invariant(routeId, "No routeId found for single fetch call decoding");
+ if ("redirect" in typed) {
+ data = { redirect: typed };
+ } else {
+ data = { routes: { [routeId]: typed } };
+ }
+ }
+ return { status: res.status, data };
} catch (e) {
// Can't clone after consuming the body via turbo-stream so we can't
// include the body here. In an ideal world we'd look for a turbo-stream
@@ -637,37 +625,34 @@ export function decodeViaTurboStream(
});
}
-function unwrapSingleFetchResults(
- results: SingleFetchResults,
+function unwrapSingleFetchResult(
+ result: DecodedSingleFetchResults,
routeId: string
) {
- let redirect = results[SingleFetchRedirectSymbol];
- if (redirect) {
- return unwrapSingleFetchResult(redirect, routeId);
+ if ("redirect" in result) {
+ let {
+ redirect: location,
+ revalidate,
+ reload,
+ replace,
+ status,
+ } = result.redirect;
+ throw redirect(location, {
+ status,
+ headers: {
+ // Three R's of redirecting (lol Veep)
+ ...(revalidate ? { "X-Remix-Revalidate": "yes" } : null),
+ ...(reload ? { "X-Remix-Reload-Document": "yes" } : null),
+ ...(replace ? { "X-Remix-Replace": "yes" } : null),
+ },
+ });
}
- return results[routeId] !== undefined
- ? unwrapSingleFetchResult(results[routeId], routeId)
- : null;
-}
-
-function unwrapSingleFetchResult(result: SingleFetchResult, routeId: string) {
- if ("error" in result) {
- throw result.error;
- } else if ("redirect" in result) {
- let headers: Record = {};
- if (result.revalidate) {
- headers["X-Remix-Revalidate"] = "yes";
- }
- if (result.reload) {
- headers["X-Remix-Reload-Document"] = "yes";
- }
- if (result.replace) {
- headers["X-Remix-Replace"] = "yes";
- }
- throw redirect(result.redirect, { status: result.status, headers });
- } else if ("data" in result) {
- return result.data;
+ let routeResult = result.routes[routeId];
+ if ("error" in routeResult) {
+ throw routeResult.error;
+ } else if ("data" in routeResult) {
+ return routeResult.data;
} else {
throw new Error(`No response found for routeId "${routeId}"`);
}
@@ -675,7 +660,7 @@ function unwrapSingleFetchResult(result: SingleFetchResult, routeId: string) {
function createDeferred() {
let resolve: (val?: any) => Promise;
- let reject: (error?: Error) => Promise;
+ let reject: (error?: unknown) => Promise;
let promise = new Promise((res, rej) => {
resolve = async (val: T) => {
res(val);
@@ -683,7 +668,7 @@ function createDeferred() {
await promise;
} catch (e) {}
};
- reject = async (error?: Error) => {
+ reject = async (error?: unknown) => {
rej(error);
try {
await promise;
diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts
index d785af7917..b5585cb978 100644
--- a/packages/react-router/lib/router/router.ts
+++ b/packages/react-router/lib/router/router.ts
@@ -1,3 +1,4 @@
+import type { DataRouteMatch } from "../context";
import type { History, Location, Path, To } from "./history";
import {
Action as NavigationType,
@@ -379,6 +380,7 @@ export interface RouterInit {
unstable_getContext?: () => MaybePromise;
mapRouteProperties?: MapRoutePropertiesFunction;
future?: Partial;
+ hydrationRouteProperties?: string[];
hydrationData?: HydrationState;
window?: Window;
dataStrategy?: DataStrategyFunction;
@@ -728,7 +730,8 @@ interface FetchLoadMatch {
interface RevalidatingFetcher extends FetchLoadMatch {
key: string;
match: AgnosticDataRouteMatch | null;
- matches: AgnosticDataRouteMatch[] | null;
+ matches: DataStrategyMatch[] | null;
+ request: Request | null;
controller: AbortController | null;
}
@@ -817,6 +820,7 @@ export function createRouter(init: RouterInit): Router {
"You must provide a non-empty routes array to createRouter"
);
+ let hydrationRouteProperties = init.hydrationRouteProperties || [];
let mapRouteProperties = init.mapRouteProperties || defaultMapRouteProperties;
// Routes keyed by ID
@@ -1628,6 +1632,7 @@ export function createRouter(init: RouterInit): Router {
matches,
scopedContext,
fogOfWar.active,
+ opts && opts.initialHydration === true,
{ replace: opts.replace, flushSync }
);
@@ -1719,6 +1724,7 @@ export function createRouter(init: RouterInit): Router {
matches: AgnosticDataRouteMatch[],
scopedContext: unstable_RouterContextProvider,
isFogOfWar: boolean,
+ initialHydration: boolean,
opts: { replace?: boolean; flushSync?: boolean } = {}
): Promise {
interruptActiveLoads();
@@ -1781,11 +1787,18 @@ export function createRouter(init: RouterInit): Router {
}),
};
} else {
- let results = await callDataStrategy(
- "action",
+ let dsMatches = getTargetedDataStrategyMatches(
+ mapRouteProperties,
+ manifest,
request,
- [actionMatch],
matches,
+ actionMatch,
+ initialHydration ? [] : hydrationRouteProperties,
+ scopedContext
+ );
+ let results = await callDataStrategy(
+ request,
+ dsMatches,
scopedContext,
null
);
@@ -1945,12 +1958,17 @@ export function createRouter(init: RouterInit): Router {
}
let routesToUse = inFlightDataRoutes || dataRoutes;
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
+ let { dsMatches, revalidatingFetchers } = getMatchesToLoad(
+ request,
+ scopedContext,
+ mapRouteProperties,
+ manifest,
init.history,
state,
matches,
activeSubmission,
location,
+ initialHydration ? [] : hydrationRouteProperties,
initialHydration === true,
isRevalidationRequired,
cancelledFetcherLoads,
@@ -1964,8 +1982,13 @@ export function createRouter(init: RouterInit): Router {
pendingNavigationLoadId = ++incrementingLoadId;
- // Short circuit if we have no loaders to run
- if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
+ // Short circuit if we have no loaders to run, unless there's a custom dataStrategy
+ // since they may have different revalidation rules (i.e., single fetch)
+ if (
+ !init.dataStrategy &&
+ !dsMatches.some((m) => m.shouldLoad) &&
+ revalidatingFetchers.length === 0
+ ) {
let updatedFetchers = markFetchRedirectsDone();
completeNavigation(
location,
@@ -2023,8 +2046,7 @@ export function createRouter(init: RouterInit): Router {
let { loaderResults, fetcherResults } =
await callLoadersAndMaybeResolveData(
- matches,
- matchesToLoad,
+ dsMatches,
revalidatingFetchers,
request,
scopedContext
@@ -2299,11 +2321,18 @@ export function createRouter(init: RouterInit): Router {
fetchControllers.set(key, abortController);
let originatingLoadId = incrementingLoadId;
- let actionResults = await callDataStrategy(
- "action",
+ let fetchMatches = getTargetedDataStrategyMatches(
+ mapRouteProperties,
+ manifest,
fetchRequest,
- [match],
requestMatches,
+ match,
+ hydrationRouteProperties,
+ scopedContext
+ );
+ let actionResults = await callDataStrategy(
+ fetchRequest,
+ fetchMatches,
scopedContext,
key
);
@@ -2375,12 +2404,17 @@ export function createRouter(init: RouterInit): Router {
let loadFetcher = getLoadingFetcher(submission, actionResult.data);
state.fetchers.set(key, loadFetcher);
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
+ let { dsMatches, revalidatingFetchers } = getMatchesToLoad(
+ revalidationRequest,
+ scopedContext,
+ mapRouteProperties,
+ manifest,
init.history,
state,
matches,
submission,
nextLocation,
+ hydrationRouteProperties,
false,
isRevalidationRequired,
cancelledFetcherLoads,
@@ -2423,8 +2457,7 @@ export function createRouter(init: RouterInit): Router {
let { loaderResults, fetcherResults } =
await callLoadersAndMaybeResolveData(
- matches,
- matchesToLoad,
+ dsMatches,
revalidatingFetchers,
revalidationRequest,
scopedContext
@@ -2581,11 +2614,18 @@ export function createRouter(init: RouterInit): Router {
fetchControllers.set(key, abortController);
let originatingLoadId = incrementingLoadId;
- let results = await callDataStrategy(
- "loader",
+ let dsMatches = getTargetedDataStrategyMatches(
+ mapRouteProperties,
+ manifest,
fetchRequest,
- [match],
matches,
+ match,
+ hydrationRouteProperties,
+ scopedContext
+ );
+ let results = await callDataStrategy(
+ fetchRequest,
+ dsMatches,
scopedContext,
key
);
@@ -2774,10 +2814,8 @@ export function createRouter(init: RouterInit): Router {
// Utility wrapper for calling dataStrategy client-side without having to
// pass around the manifest, mapRouteProperties, etc.
async function callDataStrategy(
- type: "loader" | "action",
request: Request,
- matchesToLoad: AgnosticDataRouteMatch[],
- matches: AgnosticDataRouteMatch[],
+ matches: DataStrategyMatch[],
scopedContext: unstable_RouterContextProvider,
fetcherKey: string | null
): Promise> {
@@ -2786,24 +2824,23 @@ export function createRouter(init: RouterInit): Router {
try {
results = await callDataStrategyImpl(
dataStrategyImpl as DataStrategyFunction,
- type,
request,
- matchesToLoad,
matches,
fetcherKey,
- manifest,
- mapRouteProperties,
- scopedContext
+ scopedContext,
+ false
);
} catch (e) {
// If the outer dataStrategy method throws, just return the error for all
// matches - and it'll naturally bubble to the root
- matchesToLoad.forEach((m) => {
- dataResults[m.route.id] = {
- type: ResultType.error,
- error: e,
- };
- });
+ matches
+ .filter((m) => m.shouldLoad)
+ .forEach((m) => {
+ dataResults[m.route.id] = {
+ type: ResultType.error,
+ error: e,
+ };
+ });
return dataResults;
}
@@ -2831,17 +2868,14 @@ export function createRouter(init: RouterInit): Router {
}
async function callLoadersAndMaybeResolveData(
- matches: AgnosticDataRouteMatch[],
- matchesToLoad: AgnosticDataRouteMatch[],
+ matches: DataStrategyMatch[],
fetchersToLoad: RevalidatingFetcher[],
request: Request,
scopedContext: unstable_RouterContextProvider
) {
// Kick off loaders and fetchers in parallel
let loaderResultsPromise = callDataStrategy(
- "loader",
request,
- matchesToLoad,
matches,
scopedContext,
null
@@ -2849,11 +2883,9 @@ export function createRouter(init: RouterInit): Router {
let fetcherResultsPromise = Promise.all(
fetchersToLoad.map(async (f) => {
- if (f.matches && f.match && f.controller) {
+ if (f.matches && f.match && f.request && f.controller) {
let results = await callDataStrategy(
- "loader",
- createClientSideRequest(init.history, f.path, f.controller.signal),
- [f.match],
+ f.request,
f.matches,
scopedContext,
f.key
@@ -3892,11 +3924,19 @@ export function createStaticHandler(
error,
};
} else {
- let results = await callDataStrategy(
- "action",
+ let dsMatches = getTargetedDataStrategyMatches(
+ mapRouteProperties,
+ manifest,
request,
- [actionMatch],
matches,
+ actionMatch,
+ [],
+ requestContext
+ );
+
+ let results = await callDataStrategy(
+ request,
+ dsMatches,
isRouteRequest,
requestContext,
dataStrategy
@@ -4077,26 +4117,56 @@ export function createStaticHandler(
});
}
- let requestMatches = routeMatch
- ? [routeMatch]
- : pendingActionResult && isErrorResult(pendingActionResult[1])
- ? getLoaderMatchesUntilBoundary(matches, pendingActionResult[0])
- : matches;
- let matchesToLoad = requestMatches.filter(
- (m) =>
- (m.route.loader || m.route.lazy) &&
- (!filterMatchesToLoad || filterMatchesToLoad(m))
- );
+ let dsMatches: DataStrategyMatch[];
+ if (routeMatch) {
+ dsMatches = getTargetedDataStrategyMatches(
+ mapRouteProperties,
+ manifest,
+ request,
+ matches,
+ routeMatch,
+ [],
+ requestContext
+ );
+ } else {
+ let maxIdx =
+ pendingActionResult && isErrorResult(pendingActionResult[1])
+ ? // Up to but not including the boundary
+ matches.findIndex((m) => m.route.id === pendingActionResult[0]) - 1
+ : undefined;
+
+ dsMatches = matches.map((match, index) => {
+ if (maxIdx != null && index > maxIdx) {
+ return getDataStrategyMatch(
+ mapRouteProperties,
+ manifest,
+ request,
+ match,
+ [],
+ requestContext,
+ false
+ );
+ }
- // Short circuit if we have no loaders to run (query())
- if (matchesToLoad.length === 0) {
+ return getDataStrategyMatch(
+ mapRouteProperties,
+ manifest,
+ request,
+ match,
+ [],
+ requestContext,
+ (match.route.loader || match.route.lazy) != null &&
+ (!filterMatchesToLoad || filterMatchesToLoad(match))
+ );
+ });
+ }
+
+ // Short circuit if we have no loaders to run, unless there's a custom dataStrategy
+ // since they may have different revalidation rules (i.e., single fetch)
+ if (!dataStrategy && !dsMatches.some((m) => m.shouldLoad)) {
return {
matches,
- // Add a null for all matched routes for proper revalidation on the client
- loaderData: matches.reduce(
- (acc, m) => Object.assign(acc, { [m.route.id]: null }),
- {}
- ),
+ loaderData: {},
errors:
pendingActionResult && isErrorResult(pendingActionResult[1])
? {
@@ -4109,10 +4179,8 @@ export function createStaticHandler(
}
let results = await callDataStrategy(
- "loader",
request,
- matchesToLoad,
- matches,
+ dsMatches,
isRouteRequest,
requestContext,
dataStrategy
@@ -4131,16 +4199,6 @@ export function createStaticHandler(
skipLoaderErrorBubbling
);
- // Add a null for any non-loader matches for proper revalidation on the client
- let executedLoaders = new Set(
- matchesToLoad.map((match) => match.route.id)
- );
- matches.forEach((match) => {
- if (!executedLoaders.has(match.route.id)) {
- handlerContext.loaderData[match.route.id] = null;
- }
- });
-
return {
...handlerContext,
matches,
@@ -4150,24 +4208,19 @@ export function createStaticHandler(
// Utility wrapper for calling dataStrategy server-side without having to
// pass around the manifest, mapRouteProperties, etc.
async function callDataStrategy(
- type: "loader" | "action",
request: Request,
- matchesToLoad: AgnosticDataRouteMatch[],
- matches: AgnosticDataRouteMatch[],
+ matches: DataStrategyMatch[],
isRouteRequest: boolean,
requestContext: unknown,
dataStrategy: DataStrategyFunction | null
): Promise> {
let results = await callDataStrategyImpl(
dataStrategy || defaultDataStrategy,
- type,
request,
- matchesToLoad,
matches,
null,
- manifest,
- mapRouteProperties,
- requestContext
+ requestContext,
+ true
);
let dataResults: Record = {};
@@ -4480,26 +4533,17 @@ function normalizeNavigateOptions(
return { path: createPath(parsedPath), submission };
}
-// Filter out all routes at/below any caught error as they aren't going to
-// render so we don't need to load them
-function getLoaderMatchesUntilBoundary(
- matches: AgnosticDataRouteMatch[],
- boundaryId: string,
- includeBoundary = false
-) {
- let index = matches.findIndex((m) => m.route.id === boundaryId);
- if (index >= 0) {
- return matches.slice(0, includeBoundary ? index + 1 : index);
- }
- return matches;
-}
-
function getMatchesToLoad(
+ request: Request,
+ scopedContext: unknown,
+ mapRouteProperties: MapRoutePropertiesFunction,
+ manifest: RouteManifest,
history: History,
state: RouterState,
matches: AgnosticDataRouteMatch[],
submission: Submission | undefined,
location: Location,
+ lazyRoutePropertiesToSkip: string[],
initialHydration: boolean,
isRevalidationRequired: boolean,
cancelledFetcherLoads: Set,
@@ -4509,7 +4553,10 @@ function getMatchesToLoad(
routesToUse: AgnosticDataRouteObject[],
basename: string | undefined,
pendingActionResult?: PendingActionResult
-): [AgnosticDataRouteMatch[], RevalidatingFetcher[]] {
+): {
+ dsMatches: DataStrategyMatch[];
+ revalidatingFetchers: RevalidatingFetcher[];
+} {
let actionResult = pendingActionResult
? isErrorResult(pendingActionResult[1])
? pendingActionResult[1].error
@@ -4519,25 +4566,20 @@ function getMatchesToLoad(
let nextUrl = history.createURL(location);
// Pick navigation matches that are net-new or qualify for revalidation
- let boundaryMatches = matches;
+ let maxIdx: number | undefined;
if (initialHydration && state.errors) {
// On initial hydration, only consider matches up to _and including_ the boundary.
// This is inclusive to handle cases where a server loader ran successfully,
// a child server loader bubbled up to this route, but this route has
// `clientLoader.hydrate` so we want to still run the `clientLoader` so that
// we have a complete version of `loaderData`
- boundaryMatches = getLoaderMatchesUntilBoundary(
- matches,
- Object.keys(state.errors)[0],
- true
- );
+ let boundaryId = Object.keys(state.errors)[0];
+ maxIdx = matches.findIndex((m) => m.route.id === boundaryId);
} else if (pendingActionResult && isErrorResult(pendingActionResult[1])) {
// If an action threw an error, we call loaders up to, but not including the
// boundary
- boundaryMatches = getLoaderMatchesUntilBoundary(
- matches,
- pendingActionResult[0]
- );
+ let boundaryId = pendingActionResult[0];
+ maxIdx = matches.findIndex((m) => m.route.id === boundaryId) - 1;
}
// Don't revalidate loaders by default after action 4xx/5xx responses
@@ -4548,51 +4590,84 @@ function getMatchesToLoad(
: undefined;
let shouldSkipRevalidation = actionStatus && actionStatus >= 400;
- let navigationMatches = boundaryMatches.filter((match, index) => {
+ let baseShouldRevalidateArgs = {
+ currentUrl,
+ currentParams: state.matches[0]?.params || {},
+ nextUrl,
+ nextParams: matches[0].params,
+ ...submission,
+ actionResult,
+ actionStatus,
+ };
+
+ let dsMatches: DataStrategyMatch[] = matches.map((match, index) => {
let { route } = match;
- if (route.lazy) {
- // We haven't loaded this route yet so we don't know if it's got a loader!
- return true;
- }
- if (route.loader == null) {
- return false;
- }
+ // For these cases we don't let the user have control via shouldRevalidate
+ // and we either force the loader to run or not run
+ let forceShouldLoad: boolean | null = null;
- if (initialHydration) {
- return shouldLoadRouteOnHydration(route, state.loaderData, state.errors);
+ if (maxIdx != null && index > maxIdx) {
+ // Don't call loaders below the boundary
+ forceShouldLoad = false;
+ } else if (route.lazy) {
+ // We haven't loaded this route yet so we don't know if it's got a loader!
+ forceShouldLoad = true;
+ } else if (route.loader == null) {
+ // Nothing to load!
+ forceShouldLoad = false;
+ } else if (initialHydration) {
+ // Only run on hydration if this is a hydrating `clientLoader`
+ forceShouldLoad = shouldLoadRouteOnHydration(
+ route,
+ state.loaderData,
+ state.errors
+ );
+ } else if (isNewLoader(state.loaderData, state.matches[index], match)) {
+ // Always call the loader on new route instances
+ forceShouldLoad = true;
}
- // Always call the loader on new route instances
- if (isNewLoader(state.loaderData, state.matches[index], match)) {
- return true;
+ if (forceShouldLoad !== null) {
+ return getDataStrategyMatch(
+ mapRouteProperties,
+ manifest,
+ request,
+ match,
+ lazyRoutePropertiesToSkip,
+ scopedContext,
+ forceShouldLoad
+ );
}
// This is the default implementation for when we revalidate. If the route
// provides it's own implementation, then we give them full control but
// provide this value so they can leverage it if needed after they check
// their own specific use cases
- let currentRouteMatch = state.matches[index];
- let nextRouteMatch = match;
-
- return shouldRevalidateLoader(match, {
- currentUrl,
- currentParams: currentRouteMatch.params,
- nextUrl,
- nextParams: nextRouteMatch.params,
- ...submission,
- actionResult,
- actionStatus,
- defaultShouldRevalidate: shouldSkipRevalidation
- ? false
- : // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
- isRevalidationRequired ||
- currentUrl.pathname + currentUrl.search ===
- nextUrl.pathname + nextUrl.search ||
- // Search params affect all loaders
- currentUrl.search !== nextUrl.search ||
- isNewRouteInstance(currentRouteMatch, nextRouteMatch),
- });
+ let defaultShouldRevalidate = shouldSkipRevalidation
+ ? false
+ : // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
+ isRevalidationRequired ||
+ currentUrl.pathname + currentUrl.search ===
+ nextUrl.pathname + nextUrl.search ||
+ // Search params affect all loaders
+ currentUrl.search !== nextUrl.search ||
+ isNewRouteInstance(state.matches[index], match);
+ let shouldRevalidateArgs = {
+ ...baseShouldRevalidateArgs,
+ defaultShouldRevalidate,
+ };
+ let shouldLoad = shouldRevalidateLoader(match, shouldRevalidateArgs);
+ return getDataStrategyMatch(
+ mapRouteProperties,
+ manifest,
+ request,
+ match,
+ lazyRoutePropertiesToSkip,
+ scopedContext,
+ shouldLoad,
+ shouldRevalidateArgs
+ );
});
// Pick fetcher.loads that need to be revalidated
@@ -4624,64 +4699,100 @@ function getMatchesToLoad(
path: f.path,
matches: null,
match: null,
+ request: null,
controller: null,
});
return;
}
+ if (fetchRedirectIds.has(key)) {
+ // Never trigger a revalidation of an actively redirecting fetcher
+ return;
+ }
+
// Revalidating fetchers are decoupled from the route matches since they
// load from a static href. They revalidate based on explicit revalidation
// (submission, useRevalidator, or X-Remix-Revalidate)
let fetcher = state.fetchers.get(key);
let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
- let shouldRevalidate = false;
- if (fetchRedirectIds.has(key)) {
- // Never trigger a revalidation of an actively redirecting fetcher
- shouldRevalidate = false;
- } else if (cancelledFetcherLoads.has(key)) {
+ let fetchController = new AbortController();
+ let fetchRequest = createClientSideRequest(
+ history,
+ f.path,
+ fetchController.signal
+ );
+
+ let fetcherDsMatches: DataStrategyMatch[] | null = null;
+
+ if (cancelledFetcherLoads.has(key)) {
// Always mark for revalidation if the fetcher was cancelled
cancelledFetcherLoads.delete(key);
- shouldRevalidate = true;
+ fetcherDsMatches = getTargetedDataStrategyMatches(
+ mapRouteProperties,
+ manifest,
+ fetchRequest,
+ fetcherMatches,
+ fetcherMatch,
+ lazyRoutePropertiesToSkip,
+ scopedContext
+ );
} else if (
fetcher &&
fetcher.state !== "idle" &&
fetcher.data === undefined
) {
- // If the fetcher hasn't ever completed loading yet, then this isn't a
- // revalidation, it would just be a brand new load if an explicit
- // revalidation is required
- shouldRevalidate = isRevalidationRequired;
+ if (isRevalidationRequired) {
+ // If the fetcher hasn't ever completed loading yet, then this isn't a
+ // revalidation, it would just be a brand new load if an explicit
+ // revalidation is required
+ fetcherDsMatches = getTargetedDataStrategyMatches(
+ mapRouteProperties,
+ manifest,
+ fetchRequest,
+ fetcherMatches,
+ fetcherMatch,
+ lazyRoutePropertiesToSkip,
+ scopedContext
+ );
+ }
} else {
// Otherwise fall back on any user-defined shouldRevalidate, defaulting
// to explicit revalidations only
- shouldRevalidate = shouldRevalidateLoader(fetcherMatch, {
- currentUrl,
- currentParams: state.matches[state.matches.length - 1].params,
- nextUrl,
- nextParams: matches[matches.length - 1].params,
- ...submission,
- actionResult,
- actionStatus,
+ let shouldRevalidateArgs: ShouldRevalidateFunctionArgs = {
+ ...baseShouldRevalidateArgs,
defaultShouldRevalidate: shouldSkipRevalidation
? false
: isRevalidationRequired,
- });
+ };
+ if (shouldRevalidateLoader(fetcherMatch, shouldRevalidateArgs)) {
+ fetcherDsMatches = getTargetedDataStrategyMatches(
+ mapRouteProperties,
+ manifest,
+ fetchRequest,
+ fetcherMatches,
+ fetcherMatch,
+ lazyRoutePropertiesToSkip,
+ scopedContext,
+ shouldRevalidateArgs
+ );
+ }
}
- if (shouldRevalidate) {
+ if (fetcherDsMatches) {
revalidatingFetchers.push({
key,
routeId: f.routeId,
path: f.path,
- matches: fetcherMatches,
+ matches: fetcherDsMatches,
match: fetcherMatch,
- controller: new AbortController(),
+ request: fetchRequest,
+ controller: fetchController,
});
}
});
- return [navigationMatches, revalidatingFetchers];
+ return { dsMatches, revalidatingFetchers };
}
function shouldLoadRouteOnHydration(
@@ -4944,7 +5055,8 @@ function loadLazyRoute(
route: AgnosticDataRouteObject,
type: "loader" | "action",
manifest: RouteManifest,
- mapRouteProperties: MapRoutePropertiesFunction
+ mapRouteProperties: MapRoutePropertiesFunction,
+ lazyRoutePropertiesToSkip?: string[]
): {
lazyRoutePromise: Promise | undefined;
lazyHandlerPromise: Promise | undefined;
@@ -5043,6 +5155,9 @@ function loadLazyRoute(
lazyRouteFunctionCache.set(routeToUpdate, lazyRoutePromise);
+ // Prevent unhandled rejection errors - handled inside of `callLoadOrAction`
+ lazyRoutePromise.catch(() => {});
+
return {
lazyRoutePromise,
lazyHandlerPromise: lazyRoutePromise,
@@ -5054,6 +5169,10 @@ function loadLazyRoute(
let lazyHandlerPromise: Promise | undefined = undefined;
for (let key of lazyKeys) {
+ if (lazyRoutePropertiesToSkip && lazyRoutePropertiesToSkip.includes(key)) {
+ continue;
+ }
+
let promise = loadLazyRouteProperty({
key,
route,
@@ -5068,9 +5187,16 @@ function loadLazyRoute(
}
}
- let lazyRoutePromise = Promise.all(lazyPropertyPromises)
- // Ensure type is Promise, not Promise
- .then(() => {});
+ let lazyRoutePromise =
+ lazyPropertyPromises.length > 0
+ ? Promise.all(lazyPropertyPromises)
+ // Ensure type is Promise, not Promise
+ .then(() => {})
+ : undefined;
+
+ // Prevent unhandled rejection errors - handled inside of `callLoadOrAction`
+ lazyRoutePromise?.catch(() => {});
+ lazyHandlerPromise?.catch(() => {});
return {
lazyRoutePromise,
@@ -5281,88 +5407,214 @@ async function callRouteMiddleware(
}
}
-async function callDataStrategyImpl(
- dataStrategyImpl: DataStrategyFunction,
- type: "loader" | "action",
- request: Request,
- matchesToLoad: AgnosticDataRouteMatch[],
- matches: AgnosticDataRouteMatch[],
- fetcherKey: string | null,
- manifest: RouteManifest,
+function getDataStrategyMatchLazyPromises(
mapRouteProperties: MapRoutePropertiesFunction,
- scopedContext: unknown
-): Promise> {
- // Ensure all lazy/lazyMiddleware async functions are kicked off in parallel
- // before we await them where needed below
- let loadMiddlewarePromise = loadLazyMiddlewareForMatches(
- matches,
+ manifest: RouteManifest,
+ request: Request,
+ match: DataRouteMatch,
+ lazyRoutePropertiesToSkip: string[]
+): DataStrategyMatch["_lazyPromises"] {
+ let lazyMiddlewarePromise = loadLazyRouteProperty({
+ key: "unstable_middleware",
+ route: match.route,
+ manifest,
+ mapRouteProperties,
+ });
+
+ let lazyRoutePromises = loadLazyRoute(
+ match.route,
+ isMutationMethod(request.method) ? "action" : "loader",
manifest,
- mapRouteProperties
+ mapRouteProperties,
+ lazyRoutePropertiesToSkip
);
- let lazyRoutePromises = matches.map((m) =>
- loadLazyRoute(m.route, type, manifest, mapRouteProperties)
+
+ return {
+ middleware: lazyMiddlewarePromise,
+ route: lazyRoutePromises.lazyRoutePromise,
+ handler: lazyRoutePromises.lazyHandlerPromise,
+ };
+}
+
+function getDataStrategyMatch(
+ mapRouteProperties: MapRoutePropertiesFunction,
+ manifest: RouteManifest,
+ request: Request,
+ match: DataRouteMatch,
+ lazyRoutePropertiesToSkip: string[],
+ scopedContext: unknown,
+ shouldLoad: boolean,
+ unstable_shouldRevalidateArgs: DataStrategyMatch["unstable_shouldRevalidateArgs"] = null
+): DataStrategyMatch {
+ // The hope here is to avoid a breaking change to the resolve behavior.
+ // Opt-ing into the `unstable_shouldCallHandler` API changes some nuanced behavior
+ // around when resolve calls through to the handler
+ let isUsingNewApi = false;
+
+ let _lazyPromises = getDataStrategyMatchLazyPromises(
+ mapRouteProperties,
+ manifest,
+ request,
+ match,
+ lazyRoutePropertiesToSkip
);
- // Ensure all middleware is loaded before we start executing routes
- if (loadMiddlewarePromise) {
- await loadMiddlewarePromise;
- }
+ return {
+ ...match,
+ _lazyPromises,
+ shouldLoad,
+ unstable_shouldRevalidateArgs,
+ unstable_shouldCallHandler(defaultShouldRevalidate) {
+ isUsingNewApi = true;
+ if (!unstable_shouldRevalidateArgs) {
+ return shouldLoad;
+ }
- let dsMatches = matches.map((match, i) => {
- let { lazyRoutePromise, lazyHandlerPromise } = lazyRoutePromises[i];
- let shouldLoad = matchesToLoad.some((m) => m.route.id === match.route.id);
- // `resolve` encapsulates route.lazy(), executing the loader/action,
- // and mapping return values/thrown errors to a `DataStrategyResult`. Users
- // can pass a callback to take fine-grained control over the execution
- // of the loader/action
- let resolve: DataStrategyMatch["resolve"] = async (handlerOverride) => {
+ if (typeof defaultShouldRevalidate === "boolean") {
+ return shouldRevalidateLoader(match, {
+ ...unstable_shouldRevalidateArgs,
+ defaultShouldRevalidate,
+ });
+ }
+ return shouldRevalidateLoader(match, unstable_shouldRevalidateArgs);
+ },
+ resolve(handlerOverride) {
if (
- handlerOverride &&
- request.method === "GET" &&
- (match.route.lazy || match.route.loader)
+ isUsingNewApi ||
+ shouldLoad ||
+ (handlerOverride &&
+ request.method === "GET" &&
+ (match.route.lazy || match.route.loader))
) {
- shouldLoad = true;
+ return callLoaderOrAction({
+ request,
+ match,
+ lazyHandlerPromise: _lazyPromises?.handler,
+ lazyRoutePromise: _lazyPromises?.route,
+ handlerOverride,
+ scopedContext,
+ });
}
- return shouldLoad
- ? callLoaderOrAction({
- type,
- request,
- match,
- lazyHandlerPromise,
- lazyRoutePromise,
- handlerOverride,
- scopedContext,
- })
- : Promise.resolve({ type: ResultType.data, result: undefined });
- };
+ return Promise.resolve({ type: ResultType.data, result: undefined });
+ },
+ };
+}
- return {
- ...match,
- shouldLoad,
- resolve,
- };
+function getTargetedDataStrategyMatches(
+ mapRouteProperties: MapRoutePropertiesFunction,
+ manifest: RouteManifest,
+ request: Request,
+ matches: AgnosticDataRouteMatch[],
+ targetMatch: AgnosticDataRouteMatch,
+ lazyRoutePropertiesToSkip: string[],
+ scopedContext: unknown,
+ shouldRevalidateArgs: DataStrategyMatch["unstable_shouldRevalidateArgs"] = null
+): DataStrategyMatch[] {
+ return matches.map((match) => {
+ if (match.route.id !== targetMatch.route.id) {
+ // We don't use getDataStrategyMatch here because these are for actions/fetchers
+ // where we should _never_ call the handler for any matches other than the target
+ return {
+ ...match,
+ shouldLoad: false,
+ unstable_shouldRevalidateArgs: shouldRevalidateArgs,
+ unstable_shouldCallHandler: () => false,
+ _lazyPromises: getDataStrategyMatchLazyPromises(
+ mapRouteProperties,
+ manifest,
+ request,
+ match,
+ lazyRoutePropertiesToSkip
+ ),
+ resolve: () => Promise.resolve({ type: "data", result: undefined }),
+ };
+ }
+
+ return getDataStrategyMatch(
+ mapRouteProperties,
+ manifest,
+ request,
+ match,
+ lazyRoutePropertiesToSkip,
+ scopedContext,
+ true,
+ shouldRevalidateArgs
+ );
});
+}
+
+async function callDataStrategyImpl(
+ dataStrategyImpl: DataStrategyFunction,
+ request: Request,
+ matches: DataStrategyMatch[],
+ fetcherKey: string | null,
+ scopedContext: unknown,
+ isStaticHandler: boolean
+): Promise> {
+ // Ensure all middleware is loaded before we start executing routes
+ if (matches.some((m) => m._lazyPromises?.middleware)) {
+ await Promise.all(matches.map((m) => m._lazyPromises?.middleware));
+ }
// Send all matches here to allow for a middleware-type implementation.
// handler will be a no-op for unneeded routes and we filter those results
// back out below.
- let results = await dataStrategyImpl({
- matches: dsMatches,
+ let dataStrategyArgs = {
request,
params: matches[0].params,
- fetcherKey,
context: scopedContext,
+ matches,
+ };
+ let unstable_runClientMiddleware = isStaticHandler
+ ? () => {
+ throw new Error(
+ "You cannot call `unstable_runClientMiddleware()` from a static handler " +
+ "`dataStrategy`. Middleware is run outside of `dataStrategy` during " +
+ "SSR in order to bubble up the Response. You can enable middleware " +
+ "via the `respond` API in `query`/`queryRoute`"
+ );
+ }
+ : (cb: DataStrategyFunction) => {
+ let typedDataStrategyArgs = dataStrategyArgs as (
+ | LoaderFunctionArgs
+ | ActionFunctionArgs
+ ) & {
+ matches: DataStrategyMatch[];
+ };
+ return runMiddlewarePipeline(
+ typedDataStrategyArgs,
+ false,
+ () =>
+ cb({
+ ...typedDataStrategyArgs,
+ fetcherKey,
+ unstable_runClientMiddleware: () => {
+ throw new Error(
+ "Cannot call `unstable_runClientMiddleware()` from within an " +
+ "`unstable_runClientMiddleware` handler"
+ );
+ },
+ }),
+ (error: unknown, routeId: string) => ({
+ [routeId]: { type: "error", result: error },
+ })
+ ) as Promise>;
+ };
+
+ let results = await dataStrategyImpl({
+ ...dataStrategyArgs,
+ fetcherKey,
+ unstable_runClientMiddleware,
});
// Wait for all routes to load here but swallow the error since we want
// it to bubble up from the `await loadRoutePromise` in `callLoaderOrAction` -
// called from `match.resolve()`. We also ensure that all promises are
// awaited so that we don't inadvertently leave any hanging promises.
- let allLazyRoutePromises: Promise[] = lazyRoutePromises.flatMap(
- (promiseMap) => Object.values(promiseMap).filter(isNonNullable)
- );
try {
- await Promise.all(allLazyRoutePromises);
+ await Promise.all(
+ matches.flatMap((m) => [m._lazyPromises?.handler, m._lazyPromises?.route])
+ );
} catch (e) {
// No-op
}
@@ -5372,7 +5624,6 @@ async function callDataStrategyImpl(
// Default logic for calling a loader/action is the user has no specified a dataStrategy
async function callLoaderOrAction({
- type,
request,
match,
lazyHandlerPromise,
@@ -5380,7 +5631,6 @@ async function callLoaderOrAction({
handlerOverride,
scopedContext,
}: {
- type: "loader" | "action";
request: Request;
match: AgnosticDataRouteMatch;
lazyHandlerPromise: Promise | undefined;
@@ -5390,7 +5640,8 @@ async function callLoaderOrAction({
}): Promise {
let result: DataStrategyResult;
let onReject: (() => void) | undefined;
-
+ let isAction = isMutationMethod(request.method);
+ let type = isAction ? "action" : "loader";
let runHandler = (
handler: boolean | LoaderFunction | ActionFunction
): Promise => {
@@ -5436,9 +5687,7 @@ async function callLoaderOrAction({
};
try {
- let handler = match.route[type] as
- | LoaderFunction
- | ActionFunction;
+ let handler = isAction ? match.route.action : match.route.loader;
// If we have a promise for a lazy route, await that first
if (lazyHandlerPromise || lazyRoutePromise) {
@@ -5464,9 +5713,7 @@ async function callLoaderOrAction({
// Load lazy loader/action before running it
await lazyHandlerPromise;
- handler = match.route[type] as
- | LoaderFunction
- | ActionFunction;
+ let handler = isAction ? match.route.action : match.route.loader;
if (handler) {
// Handler still runs even if we got interrupted to maintain consistency
// with un-abortable behavior of handler execution on non-lazy or
@@ -5833,33 +6080,37 @@ function processLoaderData(
);
// Process results from our revalidating fetchers
- revalidatingFetchers.forEach((rf) => {
- let { key, match, controller } = rf;
- let result = fetcherResults[key];
- invariant(result, "Did not find corresponding fetcher result");
-
- // Process fetcher non-redirect errors
- if (controller && controller.signal.aborted) {
- // Nothing to do for aborted fetchers
- return;
- } else if (isErrorResult(result)) {
- let boundaryMatch = findNearestBoundary(state.matches, match?.route.id);
- if (!(errors && errors[boundaryMatch.route.id])) {
- errors = {
- ...errors,
- [boundaryMatch.route.id]: result.error,
- };
+ revalidatingFetchers
+ // Keep those with no matches so we can bubble their 404's, otherwise only
+ // process fetchers that needed to load
+ .filter((f) => !f.matches || f.matches.some((m) => m.shouldLoad))
+ .forEach((rf) => {
+ let { key, match, controller } = rf;
+ let result = fetcherResults[key];
+ invariant(result, "Did not find corresponding fetcher result");
+
+ // Process fetcher non-redirect errors
+ if (controller && controller.signal.aborted) {
+ // Nothing to do for aborted fetchers
+ return;
+ } else if (isErrorResult(result)) {
+ let boundaryMatch = findNearestBoundary(state.matches, match?.route.id);
+ if (!(errors && errors[boundaryMatch.route.id])) {
+ errors = {
+ ...errors,
+ [boundaryMatch.route.id]: result.error,
+ };
+ }
+ state.fetchers.delete(key);
+ } else if (isRedirectResult(result)) {
+ // Should never get here, redirects should get processed above, but we
+ // keep this to type narrow to a success result in the else
+ invariant(false, "Unhandled fetcher revalidation redirect");
+ } else {
+ let doneFetcher = getDoneFetcher(result.data);
+ state.fetchers.set(key, doneFetcher);
}
- state.fetchers.delete(key);
- } else if (isRedirectResult(result)) {
- // Should never get here, redirects should get processed above, but we
- // keep this to type narrow to a success result in the else
- invariant(false, "Unhandled fetcher revalidation redirect");
- } else {
- let doneFetcher = getDoneFetcher(result.data);
- state.fetchers.set(key, doneFetcher);
- }
- });
+ });
return { loaderData, errors };
}
diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts
index c375d3c42c..cebea7406f 100644
--- a/packages/react-router/lib/router/utils.ts
+++ b/packages/react-router/lib/router/utils.ts
@@ -1,3 +1,4 @@
+import type { MiddlewareEnabled } from "../types/future";
import type { Equal, Expect } from "../types/utils";
import type { Location, Path, To } from "./history";
import { invariant, parsePath, warning } from "./history";
@@ -172,6 +173,10 @@ export class unstable_RouterContextProvider {
}
}
+type DefaultContext = MiddlewareEnabled extends true
+ ? unstable_RouterContextProvider
+ : any;
+
/**
* @private
* Arguments passed to route loader/action functions. Same for now but we keep
@@ -225,13 +230,13 @@ export type unstable_MiddlewareFunction = (
/**
* Arguments passed to loader functions
*/
-export interface LoaderFunctionArgs
+export interface LoaderFunctionArgs
extends DataFunctionArgs {}
/**
* Arguments passed to action functions
*/
-export interface ActionFunctionArgs
+export interface ActionFunctionArgs
extends DataFunctionArgs {}
/**
@@ -244,7 +249,7 @@ type DataFunctionReturnValue = MaybePromise;
/**
* Route loader function signature
*/
-export type LoaderFunction = {
+export type LoaderFunction = {
(
args: LoaderFunctionArgs,
handlerCtx?: unknown
@@ -254,7 +259,7 @@ export type LoaderFunction = {
/**
* Route action function signature
*/
-export interface ActionFunction {
+export interface ActionFunction {
(
args: ActionFunctionArgs,
handlerCtx?: unknown
@@ -331,7 +336,21 @@ export interface ShouldRevalidateFunction {
export interface DataStrategyMatch
extends AgnosticRouteMatch {
+ /**
+ * @private
+ */
+ _lazyPromises?: {
+ middleware: Promise | undefined;
+ handler: Promise | undefined;
+ route: Promise | undefined;
+ };
shouldLoad: boolean;
+ // This can be null for actions calls and for initial hydration calls
+ unstable_shouldRevalidateArgs: ShouldRevalidateFunctionArgs | null;
+ // This function will use a scoped version of `shouldRevalidateArgs` because
+ // they are read-only but let the user provide an optional override value for
+ // `defaultShouldRevalidate` if they choose
+ unstable_shouldCallHandler(defaultShouldRevalidate?: boolean): boolean;
resolve: (
handlerOverride?: (
handler: (ctx?: unknown) => DataFunctionReturnValue
@@ -339,9 +358,12 @@ export interface DataStrategyMatch
) => Promise;
}
-export interface DataStrategyFunctionArgs
+export interface DataStrategyFunctionArgs
extends DataFunctionArgs {
matches: DataStrategyMatch[];
+ unstable_runClientMiddleware: (
+ cb: DataStrategyFunction
+ ) => Promise>;
fetcherKey: string | null;
}
@@ -353,7 +375,7 @@ export interface DataStrategyResult {
result: unknown; // data, Error, Response, DeferredData, DataWithResponseInit
}
-export interface DataStrategyFunction {
+export interface DataStrategyFunction {
(args: DataStrategyFunctionArgs): Promise<
Record
>;
@@ -1206,7 +1228,7 @@ export function matchPath<
type CompiledPathParam = { paramName: string; isOptional?: boolean };
-function compilePath(
+export function compilePath(
path: string,
caseSensitive = false,
end = true
diff --git a/packages/react-router/lib/server-runtime/routes.ts b/packages/react-router/lib/server-runtime/routes.ts
index a49682f572..ab22c9c2e5 100644
--- a/packages/react-router/lib/server-runtime/routes.ts
+++ b/packages/react-router/lib/server-runtime/routes.ts
@@ -5,6 +5,7 @@ import type {
RouteManifest,
unstable_MiddlewareFunction,
} from "../router/utils";
+import { redirectDocument, replace, redirect } from "../router/utils";
import { callRouteHandler } from "./data";
import type { FutureConfig } from "../dom/ssr/entry";
import type { Route } from "../dom/ssr/routes";
@@ -12,7 +13,10 @@ import type {
SingleFetchResult,
SingleFetchResults,
} from "../dom/ssr/single-fetch";
-import { decodeViaTurboStream } from "../dom/ssr/single-fetch";
+import {
+ SingleFetchRedirectSymbol,
+ decodeViaTurboStream,
+} from "../dom/ssr/single-fetch";
import invariant from "./invariant";
import type { ServerRouteModule } from "../dom/ssr/routeModules";
@@ -99,13 +103,31 @@ export function createStaticHandlerDataRoutes(
});
let decoded = await decodeViaTurboStream(stream, global);
let data = decoded.value as SingleFetchResults;
- invariant(
- data && route.id in data,
- "Unable to decode prerendered data"
- );
- let result = data[route.id] as SingleFetchResult;
- invariant("data" in result, "Unable to process prerendered data");
- return result.data;
+
+ // If the loader returned a `.data` redirect, re-throw a normal
+ // Response here to trigger a document level SSG redirect
+ if (data && SingleFetchRedirectSymbol in data) {
+ let result = data[SingleFetchRedirectSymbol]!;
+ let init = { status: result.status };
+ if (result.reload) {
+ throw redirectDocument(result.redirect, init);
+ } else if (result.replace) {
+ throw replace(result.redirect, init);
+ } else {
+ throw redirect(result.redirect, init);
+ }
+ } else {
+ invariant(
+ data && route.id in data,
+ "Unable to decode prerendered data"
+ );
+ let result = data[route.id] as SingleFetchResult;
+ invariant(
+ "data" in result,
+ "Unable to process prerendered data"
+ );
+ return result.data;
+ }
}
let val = await callRouteHandler(route.module.loader!, args);
return val;
diff --git a/packages/react-router/lib/server-runtime/server.ts b/packages/react-router/lib/server-runtime/server.ts
index 0d1aa6a3e1..5cba00bcd4 100644
--- a/packages/react-router/lib/server-runtime/server.ts
+++ b/packages/react-router/lib/server-runtime/server.ts
@@ -24,18 +24,21 @@ import type { ServerRoute } from "./routes";
import { createStaticHandlerDataRoutes, createRoutes } from "./routes";
import { createServerHandoffString } from "./serverHandoff";
import { getDevServerHooks } from "./dev";
-import type { SingleFetchResult, SingleFetchResults } from "./single-fetch";
import {
encodeViaTurboStream,
getSingleFetchRedirect,
singleFetchAction,
singleFetchLoaders,
- SingleFetchRedirectSymbol,
SINGLE_FETCH_REDIRECT_STATUS,
- NO_BODY_STATUS_CODES,
+ SERVER_NO_BODY_STATUS_CODES,
} from "./single-fetch";
import { getDocumentHeaders } from "./headers";
import type { EntryRoute } from "../dom/ssr/routes";
+import type {
+ SingleFetchResult,
+ SingleFetchResults,
+} from "../dom/ssr/single-fetch";
+import { SingleFetchRedirectSymbol } from "../dom/ssr/single-fetch";
import type { MiddlewareEnabled } from "../types/future";
export type RequestHandler = (
@@ -90,12 +93,6 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
return async function requestHandler(request, initialContext) {
_build = typeof build === "function" ? await build() : build;
- let loadContext = _build.future.unstable_middleware
- ? new unstable_RouterContextProvider(
- initialContext as unknown as unstable_InitialContext
- )
- : initialContext || {};
-
if (typeof build === "function") {
let derived = derive(_build, mode);
routes = derived.routes;
@@ -110,6 +107,44 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
errorHandler = derived.errorHandler;
}
+ let params: RouteMatch["params"] = {};
+ let loadContext: AppLoadContext | unstable_RouterContextProvider;
+
+ let handleError = (error: unknown) => {
+ if (mode === ServerMode.Development) {
+ getDevServerHooks()?.processRequestError?.(error);
+ }
+
+ errorHandler(error, {
+ context: loadContext,
+ params,
+ request,
+ });
+ };
+
+ if (_build.future.unstable_middleware) {
+ if (initialContext == null) {
+ loadContext = new unstable_RouterContextProvider();
+ } else {
+ try {
+ loadContext = new unstable_RouterContextProvider(
+ initialContext as unknown as unstable_InitialContext
+ );
+ } catch (e) {
+ let error = new Error(
+ "Unable to create initial `unstable_RouterContextProvider` instance. " +
+ "Please confirm you are returning an instance of " +
+ "`Map` from your `getLoadContext` function." +
+ `\n\nError: ${e instanceof Error ? e.toString() : e}`
+ );
+ handleError(error);
+ return returnLastResortErrorResponse(error, serverMode);
+ }
+ }
+ } else {
+ loadContext = initialContext || {};
+ }
+
let url = new URL(request.url);
let normalizedBasename = _build.basename || "/";
@@ -127,19 +162,6 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
normalizedPath = normalizedPath.slice(0, -1);
}
- let params: RouteMatch["params"] = {};
- let handleError = (error: unknown) => {
- if (mode === ServerMode.Development) {
- getDevServerHooks()?.processRequestError?.(error);
- }
-
- errorHandler(error, {
- context: loadContext,
- params,
- request,
- });
- };
-
// When runtime SSR is disabled, make our dev server behave like the deployed
// pre-rendered site would
if (!_build.ssr) {
@@ -429,7 +451,7 @@ async function handleDocumentRequest(
let headers = getDocumentHeaders(build, context);
// Skip response body for unsupported status codes
- if (NO_BODY_STATUS_CODES.has(context.statusCode)) {
+ if (SERVER_NO_BODY_STATUS_CODES.has(context.statusCode)) {
return new Response(null, { status: context.statusCode, headers });
}
diff --git a/packages/react-router/lib/server-runtime/single-fetch.ts b/packages/react-router/lib/server-runtime/single-fetch.ts
index 145b09f4bc..21c2d84538 100644
--- a/packages/react-router/lib/server-runtime/single-fetch.ts
+++ b/packages/react-router/lib/server-runtime/single-fetch.ts
@@ -18,27 +18,16 @@ import type {
SingleFetchResult,
SingleFetchResults,
} from "../dom/ssr/single-fetch";
-import { SingleFetchRedirectSymbol } from "../dom/ssr/single-fetch";
+import {
+ NO_BODY_STATUS_CODES,
+ SingleFetchRedirectSymbol,
+} from "../dom/ssr/single-fetch";
import type { AppLoadContext } from "./data";
import { sanitizeError, sanitizeErrors } from "./errors";
import { ServerMode } from "./mode";
import { getDocumentHeaders } from "./headers";
import type { ServerBuild } from "./build";
-export type { SingleFetchResult, SingleFetchResults };
-export { SingleFetchRedirectSymbol };
-
-// Do not include a response body if the status code is one of these,
-// otherwise `undici` will throw an error when constructing the Response:
-// https://github.com/nodejs/undici/blob/bd98a6303e45d5e0d44192a93731b1defdb415f3/lib/web/fetch/response.js#L522-L528
-//
-// Specs:
-// https://datatracker.ietf.org/doc/html/rfc9110#name-informational-1xx
-// https://datatracker.ietf.org/doc/html/rfc9110#name-204-no-content
-// https://datatracker.ietf.org/doc/html/rfc9110#name-205-reset-content
-// https://datatracker.ietf.org/doc/html/rfc9110#name-304-not-modified
-export const NO_BODY_STATUS_CODES = new Set([100, 101, 204, 205, 304]);
-
// We can't use a 3xx status or else the `fetch()` would follow the redirect.
// We need to communicate the redirect back as data so we can act on it in the
// client side router. We use a 202 to avoid any automatic caching we might
@@ -46,6 +35,14 @@ export const NO_BODY_STATUS_CODES = new Set([100, 101, 204, 205, 304]);
// the user control cache behavior via Cache-Control
export const SINGLE_FETCH_REDIRECT_STATUS = 202;
+// Add 304 for server side - that is not included in the client side logic
+// because the browser should fill those responses with the cached data
+// https://datatracker.ietf.org/doc/html/rfc9110#name-304-not-modified
+export const SERVER_NO_BODY_STATUS_CODES = new Set([
+ ...NO_BODY_STATUS_CODES,
+ 304,
+]);
+
export async function singleFetchAction(
build: ServerBuild,
serverMode: ServerMode,
@@ -280,7 +277,7 @@ function generateSingleFetchResponse(
resultHeaders.set("X-Remix-Response", "yes");
// Skip response body for unsupported status codes
- if (NO_BODY_STATUS_CODES.has(status)) {
+ if (SERVER_NO_BODY_STATUS_CODES.has(status)) {
return new Response(null, { status, headers: resultHeaders });
}
diff --git a/packages/react-router/package.json b/packages/react-router/package.json
index 167debc207..add094b2db 100644
--- a/packages/react-router/package.json
+++ b/packages/react-router/package.json
@@ -1,6 +1,6 @@
{
"name": "react-router",
- "version": "7.5.0",
+ "version": "7.5.1",
"description": "Declarative routing for React",
"keywords": [
"react",
@@ -81,7 +81,6 @@
}
},
"dependencies": {
- "@types/cookie": "^0.6.0",
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0",
"turbo-stream": "2.4.0"
diff --git a/playground/framework-rolldown-vite/vite.config.ts b/playground/framework-rolldown-vite/vite.config.ts
index fac933f23c..bc98cf6730 100644
--- a/playground/framework-rolldown-vite/vite.config.ts
+++ b/playground/framework-rolldown-vite/vite.config.ts
@@ -1,11 +1,19 @@
import { reactRouter } from "@react-router/dev/vite";
-import { defineConfig } from "vite";
+import { defineConfig, type UserConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
-export default defineConfig({
- plugins: [
- // @ts-expect-error `dev` depends on Vite 6, Plugin type is mismatched.
- reactRouter(),
- tsconfigPaths(),
- ],
+export default defineConfig(({ isSsrBuild }) => {
+ const config: UserConfig = {
+ plugins: [
+ // @ts-expect-error `dev` depends on Vite 6, Plugin type is mismatched.
+ reactRouter(),
+ tsconfigPaths(),
+ ],
+ build: {
+ // Built-in minifier is still experimental
+ minify: isSsrBuild ? false : "esbuild",
+ },
+ };
+
+ return config;
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 771bb77ae1..ad1676be74 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -585,6 +585,76 @@ importers:
specifier: ^4.2.0
version: 4.2.0(@cloudflare/workers-types@4.20250317.0)
+ integration/helpers/vite-rolldown-template:
+ dependencies:
+ '@react-router/express':
+ specifier: workspace:*
+ version: link:../../../packages/react-router-express
+ '@react-router/node':
+ specifier: workspace:*
+ version: link:../../../packages/react-router-node
+ '@react-router/serve':
+ specifier: workspace:*
+ version: link:../../../packages/react-router-serve
+ '@vanilla-extract/css':
+ specifier: ^1.10.0
+ version: 1.14.2
+ '@vanilla-extract/vite-plugin':
+ specifier: ^3.9.2
+ version: 3.9.5(@types/node@20.11.30)(lightningcss@1.29.3)(rolldown-vite@6.3.0-beta.5(@types/node@20.11.30)(esbuild@0.25.0)(jiti@1.21.0)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.6.0))
+ express:
+ specifier: ^4.19.2
+ version: 4.19.2
+ isbot:
+ specifier: ^5.1.11
+ version: 5.1.11
+ react:
+ specifier: ^18.2.0
+ version: 18.2.0
+ react-dom:
+ specifier: ^18.2.0
+ version: 18.2.0(react@18.2.0)
+ react-router:
+ specifier: workspace:*
+ version: link:../../../packages/react-router
+ serialize-javascript:
+ specifier: ^6.0.1
+ version: 6.0.2
+ devDependencies:
+ '@react-router/dev':
+ specifier: workspace:*
+ version: link:../../../packages/react-router-dev
+ '@react-router/fs-routes':
+ specifier: workspace:*
+ version: link:../../../packages/react-router-fs-routes
+ '@react-router/remix-routes-option-adapter':
+ specifier: workspace:*
+ version: link:../../../packages/react-router-remix-routes-option-adapter
+ '@types/react':
+ specifier: ^18.2.18
+ version: 18.2.18
+ '@types/react-dom':
+ specifier: ^18.2.7
+ version: 18.2.7
+ cross-env:
+ specifier: ^7.0.3
+ version: 7.0.3
+ eslint:
+ specifier: ^8.38.0
+ version: 8.57.0
+ typescript:
+ specifier: ^5.1.6
+ version: 5.4.5
+ vite:
+ specifier: npm:rolldown-vite@6.3.0-beta.5
+ version: rolldown-vite@6.3.0-beta.5(@types/node@20.11.30)(esbuild@0.25.0)(jiti@1.21.0)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.6.0)
+ vite-env-only:
+ specifier: ^3.0.1
+ version: 3.0.1(rolldown-vite@6.3.0-beta.5(@types/node@20.11.30)(esbuild@0.25.0)(jiti@1.21.0)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.6.0))
+ vite-tsconfig-paths:
+ specifier: ^4.2.1
+ version: 4.3.2(rolldown-vite@6.3.0-beta.5(@types/node@20.11.30)(esbuild@0.25.0)(jiti@1.21.0)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.6.0))(typescript@5.4.5)
+
packages/create-react-router:
dependencies:
'@remix-run/web-fetch':
@@ -663,9 +733,6 @@ importers:
packages/react-router:
dependencies:
- '@types/cookie':
- specifier: ^0.6.0
- version: 0.6.0
cookie:
specifier: ^1.0.1
version: 1.0.1
@@ -3517,61 +3584,121 @@ packages:
cpu: [arm64]
os: [darwin]
+ '@rolldown/binding-darwin-arm64@1.0.0-beta.7-commit.e117288':
+ resolution: {integrity: sha512-aq6Y9OQl05bYUnzM4a7ZGF3+Du7cdrw3Ala1eCnvNqxgi2ksXKN+LHvgeaWDlyfLgX0jVQFZre4+kzgLSHEMog==}
+ cpu: [arm64]
+ os: [darwin]
+
'@rolldown/binding-darwin-x64@1.0.0-beta.7-commit.7452fa0':
resolution: {integrity: sha512-tA3K/yj2MDIKmpMjldEKkS/1k8o8MXIm+bMdLahZmFVRE7ODfQRe3aUaaxTm7wvHG8GKgE4DcqMJTwDeCqAt/g==}
cpu: [x64]
os: [darwin]
+ '@rolldown/binding-darwin-x64@1.0.0-beta.7-commit.e117288':
+ resolution: {integrity: sha512-GRxENhaf92Blo7TZz8C8vBFSt4pCRWDP45ElGATItWqzyM+ILtzNjkE5Wj1OyWPe7y0oWxps6YMxVxEdb3/BJQ==}
+ cpu: [x64]
+ os: [darwin]
+
'@rolldown/binding-freebsd-x64@1.0.0-beta.7-commit.7452fa0':
resolution: {integrity: sha512-Ps9e395Gmv3nR+WmOLGnN23Qc5R7GZ619QMnrsZZnrNjqts4pf2DAGoPnTY/dCT/z+rfcN3ku35hWh3HsI9XGA==}
cpu: [x64]
os: [freebsd]
+ '@rolldown/binding-freebsd-x64@1.0.0-beta.7-commit.e117288':
+ resolution: {integrity: sha512-3uibg1KMHT7c149YupfXICxKoO6K7q3MaMpvOdxUjTY9mY3+v96eHTfnq+dd6qD16ppKSVij7FfpEC+sCVDRmg==}
+ cpu: [x64]
+ os: [freebsd]
+
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.7-commit.7452fa0':
resolution: {integrity: sha512-/RKVSZGQyFpDWI2ksNV7/n2M1bbFvIoS4QvcETU+sMnDfhZQB6vP00dHMFsJS9J+y05XbsMnEgHslrLywFu4Ww==}
cpu: [arm]
os: [linux]
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.7-commit.e117288':
+ resolution: {integrity: sha512-oDFqE2fWx2sj0E9doRUmYxi5TKc9/vmD49NP0dUN578LQO8nJBwqOMvE8bM3a/T3or4g1mlJt2ebzGiKGzjHSw==}
+ cpu: [arm]
+ os: [linux]
+
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.7-commit.7452fa0':
resolution: {integrity: sha512-J6PeOqrX2QttacikU/CcIG2nlsnR9gDTcUQbwEbS1G/DaPrYEHXujiI4YY5Hmd+Sr1IYXI9i3z/RfzRI9XmcpQ==}
cpu: [arm64]
os: [linux]
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.7-commit.e117288':
+ resolution: {integrity: sha512-0Weogg1WiFNkmwznM4YS4AmDa55miGstb/I4zKquIsv1kSBLBkxboifgWTCPUnGFK7Wy1u/beRnxCY7UVL1oPw==}
+ cpu: [arm64]
+ os: [linux]
+
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.7-commit.7452fa0':
resolution: {integrity: sha512-lMUOKYcdDxpZMvkMbznjkqikPnvo3UIpdEfzEMp2/rOlYyC/2p3Trg3kGjhF4lbfRLbbuPEjLepGf67ot0I8oQ==}
cpu: [arm64]
os: [linux]
+ '@rolldown/binding-linux-arm64-musl@1.0.0-beta.7-commit.e117288':
+ resolution: {integrity: sha512-LwEN10APipzjxAHSreVTEnUAHRz3sq4/UR3IVD/AguV0s6yAbVAsIrvIoxXHKoci+RUlyY5FXICYZQKli8iU5w==}
+ cpu: [arm64]
+ os: [linux]
+
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.7-commit.7452fa0':
resolution: {integrity: sha512-ydsgeyhu3/AvB+I1/+uQ1+PSEQRmftkvJ1ewoXB0oJTozAKN6Ywx8jnmV8jA1g/IuMDzepR6/ixF0hbyYinWWQ==}
cpu: [x64]
os: [linux]
+ '@rolldown/binding-linux-x64-gnu@1.0.0-beta.7-commit.e117288':
+ resolution: {integrity: sha512-tgE2J4BAQfI0rfoPzS4r1LEHSNxdNSM8l1Ab5InnzE4dXzVw92BVQ/FLFE6L+nWy81O7uwd7yz0Jo+qByOPCXg==}
+ cpu: [x64]
+ os: [linux]
+
'@rolldown/binding-linux-x64-musl@1.0.0-beta.7-commit.7452fa0':
resolution: {integrity: sha512-prSpmuIoS6M1KLRd2Fzpz9n6K6K8g8/F5bN15iEpjRZCkCOI24+bVX6fDKbI0frstIMzFVvbGSxmHxt0pyphEA==}
cpu: [x64]
os: [linux]
+ '@rolldown/binding-linux-x64-musl@1.0.0-beta.7-commit.e117288':
+ resolution: {integrity: sha512-m78svPie3D5PIBxmexztDVHjrnHO5t6h3Lwtl6sqdrio1zhGYMY9FcPcaZZ40mXXWKHFoPmbueBZZLdttvOEIQ==}
+ cpu: [x64]
+ os: [linux]
+
'@rolldown/binding-wasm32-wasi@1.0.0-beta.7-commit.7452fa0':
resolution: {integrity: sha512-kRFr1jOfL4L627d1Bw/EPst3A2BwP+DV6CH/Myxl88DFzAeOAfQ04hFfCm8lBcRxzfrJNcFAMNrdIKgdUd7ddQ==}
engines: {node: '>=14.21.3'}
cpu: [wasm32]
+ '@rolldown/binding-wasm32-wasi@1.0.0-beta.7-commit.e117288':
+ resolution: {integrity: sha512-XbOcOWmdioNZ3hHBb5j96Y9S9pGyTeFZWR5ovMZggA9L7mWft2pMrbx4p5zUy2dCps3l1jaFQCjKuBXpwoCZug==}
+ engines: {node: '>=14.21.3'}
+ cpu: [wasm32]
+
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.7-commit.7452fa0':
resolution: {integrity: sha512-1l+vls3mjcKOxsrnwcwG1fX8/pL7URuZ+d+7WvKaXXIq3Id6HSdtCYuBwkUg3Bdm0mLDk7Qyv1QG3BwTcFahGQ==}
cpu: [arm64]
os: [win32]
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.7-commit.e117288':
+ resolution: {integrity: sha512-lnZ/wrat6UMWGbS9w5AUEH8WkPBA4EUSYp8cxlspdU16dISeD/KGpF2d0hS6Oa6ftbgZZrRLMEnQRiD8OupPsg==}
+ cpu: [arm64]
+ os: [win32]
+
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.7-commit.7452fa0':
resolution: {integrity: sha512-YJxvaPtH4sl5reLZCvNuqFHCgdsIRGG77LET+xng9CEWGaA1Epx2qcbeAAX8czU82tYrorx5Taxioo3GqvF53w==}
cpu: [ia32]
os: [win32]
+ '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.7-commit.e117288':
+ resolution: {integrity: sha512-F0N/6kAnCl9dOgqR09T60UjQSxKvRtlbImhiYxIdKBFxgYDDGsh8XzlSbMRUVQmMtNwKC8xi+i+SnamSqY6q8Q==}
+ cpu: [ia32]
+ os: [win32]
+
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.7-commit.7452fa0':
resolution: {integrity: sha512-OZok4v+44zYlSqo5pVyt5xPgruYcaPig9T0ieOh+O7f3BWqlkLI3ZFalznq2zFp4mJS7GtrqOAm6h7sgd+LTOw==}
cpu: [x64]
os: [win32]
+ '@rolldown/binding-win32-x64-msvc@1.0.0-beta.7-commit.e117288':
+ resolution: {integrity: sha512-T3qKMkSVemlVLLd5V7dCXnjt4Zda1UnUi45AQnmxIf3jH0/VP0J4aYAJiEEaRbhMoHc82j01+6MuZFZUVMeqng==}
+ cpu: [x64]
+ os: [win32]
+
'@rollup/pluginutils@5.1.0':
resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
engines: {node: '>=14.0.0'}
@@ -3856,9 +3983,6 @@ packages:
'@types/cookie@0.4.1':
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
- '@types/cookie@0.6.0':
- resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
-
'@types/cookiejar@2.1.5':
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
@@ -7821,6 +7945,46 @@ packages:
yaml:
optional: true
+ rolldown-vite@6.3.0-beta.5:
+ resolution: {integrity: sha512-/seCUlTV3pHNn0Y8qveGmHMNYxH/Z9xc65Ov0uaA/HtThaMZNTacWsMyDG4SA+S/c1RdpWIe85E5NeOmhywrGg==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ esbuild: ^0.25.0
+ jiti: '>=1.21.0'
+ less: '*'
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
rolldown@1.0.0-beta.7-commit.7452fa0:
resolution: {integrity: sha512-6/poOMpWJUy+MEd7qt6/f5lOOepR7vUXtMuK+J494yVA6jtkyXlCScvLVytpo13AKx+IhW/wt6qpCaZdFasd0g==}
hasBin: true
@@ -7830,6 +7994,15 @@ packages:
'@oxc-project/runtime':
optional: true
+ rolldown@1.0.0-beta.7-commit.e117288:
+ resolution: {integrity: sha512-3pjhtA9BV/q9cNdcz75ehvie3lgFfJZfzIT8A7aZJPvFCaWTj5AUAlcExXRWO/CIMMZ/49Y1x3MTwRC/Q/LuAw==}
+ hasBin: true
+ peerDependencies:
+ '@oxc-project/runtime': 0.61.2
+ peerDependenciesMeta:
+ '@oxc-project/runtime':
+ optional: true
+
rollup@4.24.0:
resolution: {integrity: sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -11194,41 +11367,79 @@ snapshots:
'@rolldown/binding-darwin-arm64@1.0.0-beta.7-commit.7452fa0':
optional: true
+ '@rolldown/binding-darwin-arm64@1.0.0-beta.7-commit.e117288':
+ optional: true
+
'@rolldown/binding-darwin-x64@1.0.0-beta.7-commit.7452fa0':
optional: true
+ '@rolldown/binding-darwin-x64@1.0.0-beta.7-commit.e117288':
+ optional: true
+
'@rolldown/binding-freebsd-x64@1.0.0-beta.7-commit.7452fa0':
optional: true
+ '@rolldown/binding-freebsd-x64@1.0.0-beta.7-commit.e117288':
+ optional: true
+
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.7-commit.7452fa0':
optional: true
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.7-commit.e117288':
+ optional: true
+
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.7-commit.7452fa0':
optional: true
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.7-commit.e117288':
+ optional: true
+
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.7-commit.7452fa0':
optional: true
+ '@rolldown/binding-linux-arm64-musl@1.0.0-beta.7-commit.e117288':
+ optional: true
+
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.7-commit.7452fa0':
optional: true
+ '@rolldown/binding-linux-x64-gnu@1.0.0-beta.7-commit.e117288':
+ optional: true
+
'@rolldown/binding-linux-x64-musl@1.0.0-beta.7-commit.7452fa0':
optional: true
+ '@rolldown/binding-linux-x64-musl@1.0.0-beta.7-commit.e117288':
+ optional: true
+
'@rolldown/binding-wasm32-wasi@1.0.0-beta.7-commit.7452fa0':
dependencies:
'@napi-rs/wasm-runtime': 0.2.7
optional: true
+ '@rolldown/binding-wasm32-wasi@1.0.0-beta.7-commit.e117288':
+ dependencies:
+ '@napi-rs/wasm-runtime': 0.2.7
+ optional: true
+
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.7-commit.7452fa0':
optional: true
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.7-commit.e117288':
+ optional: true
+
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.7-commit.7452fa0':
optional: true
+ '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.7-commit.e117288':
+ optional: true
+
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.7-commit.7452fa0':
optional: true
+ '@rolldown/binding-win32-x64-msvc@1.0.0-beta.7-commit.e117288':
+ optional: true
+
'@rollup/pluginutils@5.1.0(rollup@4.34.8)':
dependencies:
'@types/estree': 1.0.6
@@ -11477,8 +11688,6 @@ snapshots:
'@types/cookie@0.4.1': {}
- '@types/cookie@0.6.0': {}
-
'@types/cookiejar@2.1.5': {}
'@types/cross-spawn@6.0.6':
@@ -11954,6 +12163,24 @@ snapshots:
'@vanilla-extract/private@1.0.4': {}
+ '@vanilla-extract/vite-plugin@3.9.5(@types/node@20.11.30)(lightningcss@1.29.3)(rolldown-vite@6.3.0-beta.5(@types/node@20.11.30)(esbuild@0.25.0)(jiti@1.21.0)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.6.0))':
+ dependencies:
+ '@vanilla-extract/integration': 6.5.0(@types/node@20.11.30)(lightningcss@1.29.3)(terser@5.15.0)
+ outdent: 0.8.0
+ postcss: 8.4.49
+ postcss-load-config: 4.0.2(postcss@8.4.49)
+ vite: rolldown-vite@6.3.0-beta.5(@types/node@20.11.30)(esbuild@0.25.0)(jiti@1.21.0)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.6.0)
+ transitivePeerDependencies:
+ - '@types/node'
+ - less
+ - lightningcss
+ - sass
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ - ts-node
+
'@vanilla-extract/vite-plugin@3.9.5(@types/node@20.11.30)(lightningcss@1.29.3)(terser@5.15.0)(vite@5.1.3(@types/node@20.11.30)(lightningcss@1.29.3)(terser@5.15.0))':
dependencies:
'@vanilla-extract/integration': 6.5.0(@types/node@20.11.30)(lightningcss@1.29.3)(terser@5.15.0)
@@ -16627,6 +16854,24 @@ snapshots:
transitivePeerDependencies:
- typescript
+ rolldown-vite@6.3.0-beta.5(@types/node@20.11.30)(esbuild@0.25.0)(jiti@1.21.0)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.6.0):
+ dependencies:
+ '@oxc-project/runtime': 0.61.2
+ lightningcss: 1.29.3
+ picomatch: 4.0.2
+ postcss: 8.5.3
+ rolldown: 1.0.0-beta.7-commit.e117288(@oxc-project/runtime@0.61.2)(typescript@5.4.5)
+ tinyglobby: 0.2.12
+ optionalDependencies:
+ '@types/node': 20.11.30
+ esbuild: 0.25.0
+ fsevents: 2.3.3
+ jiti: 1.21.0
+ tsx: 4.19.3
+ yaml: 2.6.0
+ transitivePeerDependencies:
+ - typescript
+
rolldown@1.0.0-beta.7-commit.7452fa0(@oxc-project/runtime@0.61.2)(typescript@5.4.5):
dependencies:
'@oxc-project/types': 0.61.2
@@ -16649,6 +16894,28 @@ snapshots:
transitivePeerDependencies:
- typescript
+ rolldown@1.0.0-beta.7-commit.e117288(@oxc-project/runtime@0.61.2)(typescript@5.4.5):
+ dependencies:
+ '@oxc-project/types': 0.61.2
+ '@valibot/to-json-schema': 1.0.0(valibot@1.0.0(typescript@5.4.5))
+ valibot: 1.0.0(typescript@5.4.5)
+ optionalDependencies:
+ '@oxc-project/runtime': 0.61.2
+ '@rolldown/binding-darwin-arm64': 1.0.0-beta.7-commit.e117288
+ '@rolldown/binding-darwin-x64': 1.0.0-beta.7-commit.e117288
+ '@rolldown/binding-freebsd-x64': 1.0.0-beta.7-commit.e117288
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.7-commit.e117288
+ '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.7-commit.e117288
+ '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.7-commit.e117288
+ '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.7-commit.e117288
+ '@rolldown/binding-linux-x64-musl': 1.0.0-beta.7-commit.e117288
+ '@rolldown/binding-wasm32-wasi': 1.0.0-beta.7-commit.e117288
+ '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.7-commit.e117288
+ '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.7-commit.e117288
+ '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.7-commit.e117288
+ transitivePeerDependencies:
+ - typescript
+
rollup@4.24.0:
dependencies:
'@types/estree': 1.0.6
@@ -17648,6 +17915,19 @@ snapshots:
unist-util-stringify-position: 4.0.0
vfile-message: 4.0.2
+ vite-env-only@3.0.1(rolldown-vite@6.3.0-beta.5(@types/node@20.11.30)(esbuild@0.25.0)(jiti@1.21.0)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.6.0)):
+ dependencies:
+ '@babel/core': 7.24.3
+ '@babel/generator': 7.24.1
+ '@babel/parser': 7.24.1
+ '@babel/traverse': 7.24.1
+ '@babel/types': 7.24.0
+ babel-dead-code-elimination: 1.0.6
+ micromatch: 4.0.5
+ vite: rolldown-vite@6.3.0-beta.5(@types/node@20.11.30)(esbuild@0.25.0)(jiti@1.21.0)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.6.0)
+ transitivePeerDependencies:
+ - supports-color
+
vite-env-only@3.0.1(vite@5.1.3(@types/node@20.11.30)(lightningcss@1.29.3)(terser@5.15.0)):
dependencies:
'@babel/core': 7.24.3
@@ -17723,6 +18003,17 @@ snapshots:
- supports-color
- typescript
+ vite-tsconfig-paths@4.3.2(rolldown-vite@6.3.0-beta.5(@types/node@20.11.30)(esbuild@0.25.0)(jiti@1.21.0)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.6.0))(typescript@5.4.5):
+ dependencies:
+ debug: 4.3.7
+ globrex: 0.1.2
+ tsconfck: 3.0.3(typescript@5.4.5)
+ optionalDependencies:
+ vite: rolldown-vite@6.3.0-beta.5(@types/node@20.11.30)(esbuild@0.25.0)(jiti@1.21.0)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.6.0)
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
vite-tsconfig-paths@4.3.2(typescript@5.4.5)(vite@5.1.3(@types/node@20.11.30)(lightningcss@1.29.3)(terser@5.15.0)):
dependencies:
debug: 4.3.7
diff --git a/scripts/start-prerelease.sh b/scripts/start-prerelease.sh
new file mode 100755
index 0000000000..9309e8414d
--- /dev/null
+++ b/scripts/start-prerelease.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+set -x
+set -e
+
+if [[ -n $(git status --porcelain) ]]; then
+ echo "Error: Your git working directory is not clean. Please commit or stash your changes."
+ exit 1
+fi
+
+git checkout main
+git pull
+git checkout dev
+git pull
+git checkout -b release-next
+git merge main --no-edit
+pnpm changeset pre enter pre
+git add .changeset/pre.json
+git commit -m "Enter prerelease mode"
+git push --set-upstream origin release-next
+
+set +e
+set +x
\ No newline at end of file