From 96b441bdd15ed6ace87d9c136fc98804780748f2 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 10 May 2024 17:37:25 -0400 Subject: [PATCH] Bring over Remix 2.9.2 logic for single fetch (#11552) --- .changeset/prerendering.md | 4 - integration/action-test.ts | 1 - integration/catch-boundary-data-test.ts | 1 - integration/catch-boundary-test.ts | 1 - integration/client-data-test.ts | 72 ++-- integration/defer-loader-test.ts | 1 - integration/defer-test.ts | 2 - integration/error-boundary-test.ts | 7 - integration/error-data-request-test.ts | 1 - integration/error-sanitization-test.ts | 3 - integration/fetcher-layout-test.ts | 1 - integration/fetcher-test.ts | 2 - integration/file-uploads-test.ts | 1 - integration/form-data-test.ts | 1 - integration/form-test.ts | 1 - integration/helpers/create-fixture.ts | 4 +- integration/helpers/vite.ts | 1 - integration/loader-test.ts | 2 - integration/navigation-state-test.ts | 1 - integration/prefetch-test.ts | 4 - integration/redirects-test.ts | 1 - integration/revalidate-test.ts | 1 - integration/set-cookie-revalidation-test.ts | 1 - integration/single-fetch-test.ts | 380 +++++++++++++++--- integration/vite-prerender-test.ts | 7 - packages/react-router/lib/dom/ssr/server.tsx | 3 + .../react-router/lib/dom/ssr/single-fetch.tsx | 5 + packages/remix-server-runtime/data.ts | 32 +- packages/remix-server-runtime/routeModules.ts | 20 +- packages/remix-server-runtime/routes.ts | 8 +- packages/remix-server-runtime/server.ts | 46 ++- packages/remix-server-runtime/single-fetch.ts | 60 ++- 32 files changed, 446 insertions(+), 229 deletions(-) diff --git a/.changeset/prerendering.md b/.changeset/prerendering.md index 21f4438e82..31a6883241 100644 --- a/.changeset/prerendering.md +++ b/.changeset/prerendering.md @@ -10,10 +10,6 @@ export default defineConfig({ plugins: [ reactRouter({ - // Single fetch is required for prerendering (which will be the default in v7) - future: { - unstable_singleFetch: true, - }, async prerender() { let slugs = await fakeGetSlugsFromCms(); // Prerender these paths into `.html` files at build time, and `.data` diff --git a/integration/action-test.ts b/integration/action-test.ts index 46464026f4..c88b295539 100644 --- a/integration/action-test.ts +++ b/integration/action-test.ts @@ -21,7 +21,6 @@ test.describe("actions", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/routes/urlencoded.tsx": js` import { Form, useActionData } from "react-router-dom"; diff --git a/integration/catch-boundary-data-test.ts b/integration/catch-boundary-data-test.ts index 4005cf279d..06a902d900 100644 --- a/integration/catch-boundary-data-test.ts +++ b/integration/catch-boundary-data-test.ts @@ -38,7 +38,6 @@ test.describe("ErrorBoundary (thrown responses)", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/root.tsx": js` import { json } from "@react-router/node"; diff --git a/integration/catch-boundary-test.ts b/integration/catch-boundary-test.ts index 283f54fc71..2e9c63264c 100644 --- a/integration/catch-boundary-test.ts +++ b/integration/catch-boundary-test.ts @@ -28,7 +28,6 @@ test.describe("ErrorBoundary (thrown responses)", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/root.tsx": js` import { json } from "@react-router/node"; diff --git a/integration/client-data-test.ts b/integration/client-data-test.ts index 4019a3e6c1..e79edb9a02 100644 --- a/integration/client-data-test.ts +++ b/integration/client-data-test.ts @@ -6,7 +6,7 @@ import { createFixture, js, } from "./helpers/create-fixture.js"; -import type { AppFixture, FixtureInit } from "./helpers/create-fixture.js"; +import type { AppFixture } from "./helpers/create-fixture.js"; import { PlaywrightFixture } from "./helpers/playwright-fixture.js"; function getFiles({ @@ -145,20 +145,10 @@ test.describe("Client Data", () => { appFixture.close(); }); - function createTestFixture(init: FixtureInit, serverMode?: ServerMode) { - return createFixture( - { - ...init, - singleFetch: true, - }, - serverMode - ); - } - test.describe("clientLoader - critical route module", () => { test("no client loaders or fallbacks", async ({ page }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: false, parentClientLoaderHydrate: false, @@ -178,7 +168,7 @@ test.describe("Client Data", () => { test("parent.clientLoader/child.clientLoader", async ({ page }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: true, parentClientLoaderHydrate: false, @@ -198,7 +188,7 @@ test.describe("Client Data", () => { test("parent.clientLoader.hydrate/child.clientLoader", async ({ page }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: true, parentClientLoaderHydrate: true, @@ -224,7 +214,7 @@ test.describe("Client Data", () => { test("parent.clientLoader/child.clientLoader.hydrate", async ({ page }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: true, parentClientLoaderHydrate: false, @@ -252,7 +242,7 @@ test.describe("Client Data", () => { page, }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: true, parentClientLoaderHydrate: true, @@ -279,7 +269,7 @@ test.describe("Client Data", () => { }); test("handles synchronous client loaders", async ({ page }) => { - let fixture = await createTestFixture({ + let fixture = await createFixture({ files: getFiles({ parentClientLoader: false, parentClientLoaderHydrate: false, @@ -318,7 +308,7 @@ test.describe("Client Data", () => { }); test("handles deferred data through client loaders", async ({ page }) => { - let fixture = await createTestFixture({ + let fixture = await createFixture({ files: { ...getFiles({ parentClientLoader: false, @@ -388,7 +378,7 @@ test.describe("Client Data", () => { test("allows hydration execution without rendering a fallback", async ({ page, }) => { - let fixture = await createTestFixture({ + let fixture = await createFixture({ files: getFiles({ parentClientLoader: false, parentClientLoaderHydrate: false, @@ -417,7 +407,7 @@ test.describe("Client Data", () => { test("HydrateFallback is not rendered if clientLoader.hydrate is not set (w/server loader)", async ({ page, }) => { - let fixture = await createTestFixture({ + let fixture = await createFixture({ files: { ...getFiles({ parentClientLoader: false, @@ -471,7 +461,7 @@ test.describe("Client Data", () => { page, }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: { ...getFiles({ parentClientLoader: false, @@ -514,7 +504,7 @@ test.describe("Client Data", () => { page, }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: { ...getFiles({ parentClientLoader: false, @@ -557,7 +547,7 @@ test.describe("Client Data", () => { page, }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: { ...getFiles({ parentClientLoader: false, @@ -600,7 +590,7 @@ test.describe("Client Data", () => { page, }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: { ...getFiles({ parentClientLoader: false, @@ -665,7 +655,7 @@ test.describe("Client Data", () => { page, }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: { ...getFiles({ parentClientLoader: false, @@ -742,7 +732,7 @@ test.describe("Client Data", () => { let _consoleError = console.error; console.error = () => {}; appFixture = await createAppFixture( - await createTestFixture( + await createFixture( { files: { ...getFiles({ @@ -795,7 +785,7 @@ test.describe("Client Data", () => { test.describe("clientLoader - lazy route module", () => { test("no client loaders or fallbacks", async ({ page }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: false, parentClientLoaderHydrate: false, @@ -817,7 +807,7 @@ test.describe("Client Data", () => { test("parent.clientLoader", async ({ page }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: true, parentClientLoaderHydrate: false, @@ -838,7 +828,7 @@ test.describe("Client Data", () => { test("child.clientLoader", async ({ page }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: false, parentClientLoaderHydrate: false, @@ -859,7 +849,7 @@ test.describe("Client Data", () => { test("parent.clientLoader/child.clientLoader", async ({ page }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: true, parentClientLoaderHydrate: false, @@ -882,7 +872,7 @@ test.describe("Client Data", () => { page, }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: { ...getFiles({ parentClientLoader: false, @@ -926,7 +916,7 @@ test.describe("Client Data", () => { test.describe("clientAction - critical route module", () => { test("child.clientAction", async ({ page }) => { appFixture = await createAppFixture( - await createTestFixture( + await createFixture( { files: getFiles({ parentClientLoader: false, @@ -964,7 +954,7 @@ test.describe("Client Data", () => { test("child.clientAction/parent.childLoader", async ({ page }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: true, parentClientLoaderHydrate: false, @@ -1006,7 +996,7 @@ test.describe("Client Data", () => { test("child.clientAction/child.clientLoader", async ({ page }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: false, parentClientLoaderHydrate: false, @@ -1050,7 +1040,7 @@ test.describe("Client Data", () => { page, }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: true, parentClientLoaderHydrate: false, @@ -1094,7 +1084,7 @@ test.describe("Client Data", () => { page, }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: { ...getFiles({ parentClientLoader: false, @@ -1139,7 +1129,7 @@ test.describe("Client Data", () => { test.describe("clientAction - lazy route module", () => { test("child.clientAction", async ({ page }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: false, parentClientLoaderHydrate: false, @@ -1175,7 +1165,7 @@ test.describe("Client Data", () => { test("child.clientAction/parent.childLoader", async ({ page }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: true, parentClientLoaderHydrate: false, @@ -1219,7 +1209,7 @@ test.describe("Client Data", () => { test("child.clientAction/child.clientLoader", async ({ page }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: false, parentClientLoaderHydrate: false, @@ -1265,7 +1255,7 @@ test.describe("Client Data", () => { page, }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: getFiles({ parentClientLoader: true, parentClientLoaderHydrate: false, @@ -1311,7 +1301,7 @@ test.describe("Client Data", () => { page, }) => { appFixture = await createAppFixture( - await createTestFixture({ + await createFixture({ files: { ...getFiles({ parentClientLoader: false, diff --git a/integration/defer-loader-test.ts b/integration/defer-loader-test.ts index 09575f3335..febc485b80 100644 --- a/integration/defer-loader-test.ts +++ b/integration/defer-loader-test.ts @@ -14,7 +14,6 @@ let appFixture: AppFixture; test.describe("deferred loaders", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/routes/_index.tsx": js` import { useLoaderData, Link } from "react-router-dom"; diff --git a/integration/defer-test.ts b/integration/defer-test.ts index 5cbefb05f0..170b222814 100644 --- a/integration/defer-test.ts +++ b/integration/defer-test.ts @@ -45,7 +45,6 @@ test.describe("non-aborted", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/components/counter.tsx": js` import { useState } from "react"; @@ -994,7 +993,6 @@ test.describe("aborted", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/entry.server.tsx": js` import { PassThrough } from "node:stream"; diff --git a/integration/error-boundary-test.ts b/integration/error-boundary-test.ts index 9ef26a308e..e768799dd1 100644 --- a/integration/error-boundary-test.ts +++ b/integration/error-boundary-test.ts @@ -46,7 +46,6 @@ test.describe("ErrorBoundary", () => { console.error = () => {}; fixture = await createFixture( { - singleFetch: true, files: { "app/root.tsx": js` import { Links, Meta, Outlet, Scripts } from "react-router-dom"; @@ -499,7 +498,6 @@ test.describe("ErrorBoundary", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/root.tsx": js` import { Links, Meta, Outlet, Scripts } from "react-router-dom"; @@ -665,7 +663,6 @@ test.describe("loaderData in ErrorBoundary", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/root.tsx": js` import { Links, Meta, Outlet, Scripts } from "react-router-dom"; @@ -1024,7 +1021,6 @@ test.describe("Default ErrorBoundary", () => { test.beforeAll(async () => { fixture = await createFixture( { - singleFetch: true, files: getFiles({ includeRootErrorBoundary: false }), }, ServerMode.Development @@ -1096,7 +1092,6 @@ test.describe("Default ErrorBoundary", () => { test.beforeAll(async () => { fixture = await createFixture( { - singleFetch: true, files: getFiles({ includeRootErrorBoundary: true }), }, ServerMode.Development @@ -1162,7 +1157,6 @@ test.describe("Default ErrorBoundary", () => { test.describe("When the root route has a boundary but it also throws 😦", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: getFiles({ includeRootErrorBoundary: true, rootErrorBoundaryThrows: true, @@ -1247,7 +1241,6 @@ test("Allows back-button out of an error boundary after a hard reload", async ({ console.error = () => {}; let fixture = await createFixture({ - singleFetch: true, files: { "app/root.tsx": js` import { Links, Meta, Outlet, Scripts, useRouteError } from "react-router-dom"; diff --git a/integration/error-data-request-test.ts b/integration/error-data-request-test.ts index 7e86572431..b1ad7e82cd 100644 --- a/integration/error-data-request-test.ts +++ b/integration/error-data-request-test.ts @@ -19,7 +19,6 @@ test.describe("ErrorBoundary", () => { console.error = (v) => errorLogs.push(v); fixture = await createFixture({ - singleFetch: true, files: { "app/root.tsx": js` import { Links, Meta, Outlet, Scripts } from "react-router-dom"; diff --git a/integration/error-sanitization-test.ts b/integration/error-sanitization-test.ts index 0d590ed9b1..071d8eba0a 100644 --- a/integration/error-sanitization-test.ts +++ b/integration/error-sanitization-test.ts @@ -157,7 +157,6 @@ test.describe("Error Sanitization", () => { test.beforeAll(async () => { fixture = await createFixture( { - singleFetch: true, files: routeFiles, }, ServerMode.Production @@ -322,7 +321,6 @@ test.describe("Error Sanitization", () => { test.beforeAll(async () => { fixture = await createFixture( { - singleFetch: true, files: routeFiles, }, ServerMode.Development @@ -490,7 +488,6 @@ test.describe("Error Sanitization", () => { test.beforeAll(async () => { fixture = await createFixture( { - singleFetch: true, files: { "app/entry.server.tsx": js` import { PassThrough } from "node:stream"; diff --git a/integration/fetcher-layout-test.ts b/integration/fetcher-layout-test.ts index b8ed4abe5e..6790210f2f 100644 --- a/integration/fetcher-layout-test.ts +++ b/integration/fetcher-layout-test.ts @@ -13,7 +13,6 @@ let appFixture: AppFixture; test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/routes/layout-action.tsx": js` import { json } from "@react-router/node"; diff --git a/integration/fetcher-test.ts b/integration/fetcher-test.ts index be7a164196..9a766df7f9 100644 --- a/integration/fetcher-test.ts +++ b/integration/fetcher-test.ts @@ -21,7 +21,6 @@ test.describe("useFetcher", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/routes/resource-route-action-only.ts": js` import { json } from "@react-router/node"; @@ -434,7 +433,6 @@ test.describe("fetcher aborts and adjacent forms", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/routes/_index.tsx": js` import * as React from "react"; diff --git a/integration/file-uploads-test.ts b/integration/file-uploads-test.ts index 0a045407bc..c49f763f6d 100644 --- a/integration/file-uploads-test.ts +++ b/integration/file-uploads-test.ts @@ -17,7 +17,6 @@ test.describe("file-uploads", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/fileUploadHandler.ts": js` import * as path from "node:path"; diff --git a/integration/form-data-test.ts b/integration/form-data-test.ts index b2b6121536..a7173a892e 100644 --- a/integration/form-data-test.ts +++ b/integration/form-data-test.ts @@ -7,7 +7,6 @@ let fixture: Fixture; test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/routes/_index.tsx": js` import { json } from "@react-router/node"; diff --git a/integration/form-test.ts b/integration/form-test.ts index 8f6cb536a4..acf9701214 100644 --- a/integration/form-test.ts +++ b/integration/form-test.ts @@ -60,7 +60,6 @@ test.describe("Forms", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/routes/get-submission.tsx": js` import { useLoaderData, Form } from "react-router-dom"; diff --git a/integration/helpers/create-fixture.ts b/integration/helpers/create-fixture.ts index ecff605f95..bff67f68a0 100644 --- a/integration/helpers/create-fixture.ts +++ b/integration/helpers/create-fixture.ts @@ -27,7 +27,6 @@ export interface FixtureInit { template?: "cf-template" | "deno-template" | "node-template"; useReactRouterServe?: boolean; spaMode?: boolean; - singleFetch?: boolean; port?: number; } @@ -312,7 +311,7 @@ export async function createFixtureProject( filename.startsWith("vite.config.") ); - let { singleFetch, spaMode } = init; + let { spaMode } = init; await writeTestFiles( { @@ -321,7 +320,6 @@ export async function createFixtureProject( : { "vite.config.js": await viteConfig.basic({ port, - singleFetch, spaMode, }), }), diff --git a/integration/helpers/vite.ts b/integration/helpers/vite.ts index 86c274fe8c..f41852c1a2 100644 --- a/integration/helpers/vite.ts +++ b/integration/helpers/vite.ts @@ -36,7 +36,6 @@ export const viteConfig = { basic: async (args: { port: number; fsAllow?: string[]; - singleFetch?: boolean; spaMode?: boolean; }) => { let pluginOptions: VitePluginConfig = { diff --git a/integration/loader-test.ts b/integration/loader-test.ts index 3866f84874..6da2c8c6e6 100644 --- a/integration/loader-test.ts +++ b/integration/loader-test.ts @@ -16,7 +16,6 @@ test.describe("loader", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/root.tsx": js` import { json } from "@react-router/node"; @@ -74,7 +73,6 @@ test.describe("loader in an app", () => { test.beforeAll(async () => { appFixture = await createAppFixture( await createFixture({ - singleFetch: true, files: { "app/root.tsx": js` import { Outlet } from 'react-router-dom' diff --git a/integration/navigation-state-test.ts b/integration/navigation-state-test.ts index 3cc96a2395..c2c30c2d87 100644 --- a/integration/navigation-state-test.ts +++ b/integration/navigation-state-test.ts @@ -35,7 +35,6 @@ test.describe("navigation states", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/root.tsx": js` import { useMemo, useRef } from "react"; diff --git a/integration/prefetch-test.ts b/integration/prefetch-test.ts index 781537fdd9..31b4509444 100644 --- a/integration/prefetch-test.ts +++ b/integration/prefetch-test.ts @@ -578,7 +578,6 @@ test.describe.skip("single fetch", () => { // Generate the test app using the given prefetch mode function fixtureFactory(mode: PrefetchType): FixtureInit { return { - singleFetch: true, files: { "app/root.tsx": js` import { @@ -842,7 +841,6 @@ test.describe.skip("single fetch", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/routes/_index.tsx": js` import { Link } from "react-router-dom"; @@ -917,7 +915,6 @@ test.describe.skip("single fetch", () => { page, }) => { fixture = await createFixture({ - singleFetch: true, files: { "app/root.tsx": js` import { Links, Meta, Scripts, useFetcher } from "react-router-dom"; @@ -996,7 +993,6 @@ test.describe.skip("single fetch", () => { test("dedupes prefetch tags", async ({ page }) => { fixture = await createFixture({ - singleFetch: true, files: { "app/root.tsx": js` import { diff --git a/integration/redirects-test.ts b/integration/redirects-test.ts index 74fd9d7486..10dec86ca6 100644 --- a/integration/redirects-test.ts +++ b/integration/redirects-test.ts @@ -14,7 +14,6 @@ test.describe("redirects", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/routes/absolute.tsx": js` import * as React from 'react'; diff --git a/integration/revalidate-test.ts b/integration/revalidate-test.ts index 9a6da99463..475ca8ea3a 100644 --- a/integration/revalidate-test.ts +++ b/integration/revalidate-test.ts @@ -14,7 +14,6 @@ test.describe("Revalidation", () => { test.beforeAll(async () => { appFixture = await createAppFixture( await createFixture({ - singleFetch: true, files: { "app/root.tsx": js` import { Link, Outlet, Scripts, useNavigation } from "react-router-dom"; diff --git a/integration/set-cookie-revalidation-test.ts b/integration/set-cookie-revalidation-test.ts index bffce7f2a2..5cd04017c1 100644 --- a/integration/set-cookie-revalidation-test.ts +++ b/integration/set-cookie-revalidation-test.ts @@ -16,7 +16,6 @@ let BANNER_MESSAGE = "you do not have permission to view /protected"; test.describe("set-cookie revalidation", () => { test.beforeAll(async () => { fixture = await createFixture({ - singleFetch: true, files: { "app/session.server.ts": js` import { createCookieSessionStorage } from "@react-router/node"; diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts index b98cae2cb2..7fa939a816 100644 --- a/integration/single-fetch-test.ts +++ b/integration/single-fetch-test.ts @@ -12,7 +12,7 @@ const ISO_DATE = "2024-03-12T12:00:00.000Z"; const files = { "app/root.tsx": js` - import { Form, Link, Links, Meta, Outlet, Scripts } from "react-router-dom"; + import { Form, Link, Links, Meta, Outlet, Scripts } from "react-router"; export function loader() { return { @@ -51,7 +51,7 @@ const files = { `, "app/routes/data.tsx": js` - import { useActionData, useLoaderData } from "react-router-dom"; + import { useActionData, useLoaderData } from "react-router"; export async function action({ request }) { let formData = await request.formData(); @@ -99,7 +99,6 @@ test.describe("single-fetch", () => { test("loads proper data on single fetch loader requests", async () => { let fixture = await createFixture( { - singleFetch: true, files, }, ServerMode.Development @@ -137,7 +136,6 @@ test.describe("single-fetch", () => { let fixture = await createFixture( { - singleFetch: true, files, }, ServerMode.Development @@ -161,7 +159,6 @@ test.describe("single-fetch", () => { test("loads proper data on single fetch action requests", async () => { let fixture = await createFixture( { - singleFetch: true, files, }, ServerMode.Development @@ -181,7 +178,6 @@ test.describe("single-fetch", () => { test("loads proper data on document request", async ({ page }) => { let fixture = await createFixture({ - singleFetch: true, files, }); let appFixture = await createAppFixture(fixture); @@ -194,7 +190,6 @@ test.describe("single-fetch", () => { test("loads proper data on client side navigation", async ({ page }) => { let fixture = await createFixture({ - singleFetch: true, files, }); let appFixture = await createAppFixture(fixture); @@ -211,7 +206,6 @@ test.describe("single-fetch", () => { page, }) => { let fixture = await createFixture({ - singleFetch: true, files, }); let appFixture = await createAppFixture(fixture); @@ -227,11 +221,10 @@ test.describe("single-fetch", () => { test("allows fine-grained revalidation", async ({ page }) => { let fixture = await createFixture({ - singleFetch: true, files: { ...files, "app/routes/no-revalidate.tsx": js` - import { Form, useActionData, useLoaderData, useNavigation } from "react-router-dom"; + import { Form, useActionData, useLoaderData, useNavigation } from "react-router"; export async function action({ request }) { let fd = await request.formData(); @@ -296,11 +289,10 @@ test.describe("single-fetch", () => { test("does not revalidate on 4xx/5xx action responses", async ({ page }) => { let fixture = await createFixture({ - singleFetch: true, files: { ...files, "app/routes/action.tsx": js` - import { Form, Link, useActionData, useLoaderData, useNavigation } from "react-router-dom"; + import { Form, Link, useActionData, useLoaderData, useNavigation } from "react-router"; export async function action({ request, response }) { let fd = await request.formData(); @@ -396,7 +388,6 @@ test.describe("single-fetch", () => { test("returns headers correctly for singular loader and action calls", async () => { let fixture = await createFixture({ - singleFetch: true, files: { ...files, "app/routes/headers.tsx": js` @@ -466,7 +457,6 @@ test.describe("single-fetch", () => { test("merges headers from nested routes", async () => { let fixture = await createFixture({ - singleFetch: true, files: { ...files, "app/routes/a.tsx": js` @@ -621,7 +611,6 @@ test.describe("single-fetch", () => { test("merges status codes from nested routes", async () => { let fixture = await createFixture({ - singleFetch: true, files: { ...files, "app/routes/a.tsx": js` @@ -719,7 +708,6 @@ test.describe("single-fetch", () => { test("merges headers from nested routes when raw Responses are returned", async () => { let fixture = await createFixture({ - singleFetch: true, files: { ...files, "app/routes/a.tsx": js` @@ -821,7 +809,6 @@ test.describe("single-fetch", () => { test("merges status codes from nested routes when raw Responses are used", async () => { let fixture = await createFixture({ - singleFetch: true, files: { ...files, "app/routes/a.tsx": js` @@ -917,7 +904,6 @@ test.describe("single-fetch", () => { page, }) => { let fixture = await createFixture({ - singleFetch: true, files: { ...files, "app/routes/data.tsx": js` @@ -958,7 +944,6 @@ test.describe("single-fetch", () => { page, }) => { let fixture = await createFixture({ - singleFetch: true, files: { ...files, "app/routes/data.tsx": js` @@ -995,7 +980,6 @@ test.describe("single-fetch", () => { test("processes thrown loader redirects via Response", async ({ page }) => { let fixture = await createFixture({ - singleFetch: true, files: { ...files, "app/routes/data.tsx": js` @@ -1032,7 +1016,6 @@ test.describe("single-fetch", () => { test("processes returned loader redirects via Response", async ({ page }) => { let fixture = await createFixture({ - singleFetch: true, files: { ...files, "app/routes/data.tsx": js` @@ -1069,7 +1052,6 @@ test.describe("single-fetch", () => { }) => { let fixture = await createFixture( { - singleFetch: true, files: { ...files, "app/routes/data.tsx": js` @@ -1116,7 +1098,6 @@ test.describe("single-fetch", () => { }) => { let fixture = await createFixture( { - singleFetch: true, files: { ...files, "app/routes/data.tsx": js` @@ -1159,7 +1140,6 @@ test.describe("single-fetch", () => { test("processes thrown action redirects via Response", async ({ page }) => { let fixture = await createFixture( { - singleFetch: true, files: { ...files, "app/routes/data.tsx": js` @@ -1202,7 +1182,6 @@ test.describe("single-fetch", () => { test("processes returned action redirects via Response", async ({ page }) => { let fixture = await createFixture( { - singleFetch: true, files: { ...files, "app/routes/data.tsx": js` @@ -1244,7 +1223,6 @@ test.describe("single-fetch", () => { page, }) => { let fixture = await createFixture({ - singleFetch: true, files: { ...files, "app/entry.server.tsx": js` @@ -1252,7 +1230,7 @@ test.describe("single-fetch", () => { import type { EntryContext } from "@react-router/node"; import { createReadableStreamFromReadable } from "@react-router/node"; - import { RemixServer } from "react-router-dom"; + import { RemixServer } from "react-router"; import { renderToPipeableStream } from "react-dom/server"; export default function handleRequest( @@ -1328,7 +1306,6 @@ test.describe("single-fetch", () => { page, }) => { let fixture = await createFixture({ - singleFetch: true, files: { ...files, "app/entry.server.tsx": js` @@ -1336,7 +1313,7 @@ test.describe("single-fetch", () => { import type { EntryContext } from "@react-router/node"; import { createReadableStreamFromReadable } from "@react-router/node"; - import { RemixServer } from "react-router-dom"; + import { RemixServer } from "react-router"; import { renderToPipeableStream } from "react-dom/server"; export default function handleRequest( @@ -1408,15 +1385,295 @@ test.describe("single-fetch", () => { expect(await app.getHtml("#target")).toContain("Target"); }); + test("throws an error on naked object returns from external resource route consumption", async () => { + let oldConsoleError = console.error; + console.error = () => {}; + + let fixture = await createFixture( + { + files: { + ...files, + "app/routes/resource.tsx": js` + export function loader() { + return { message: "RESOURCE" }; + } + `, + }, + }, + ServerMode.Development + ); + let res = await fixture.requestResource("/resource"); + expect(res.status).toBe(500); + expect(await res.text()).toContain( + "Expected a Response to be returned from resource route handler" + ); + + console.error = oldConsoleError; + }); + + test("processes response stub onto resource routes returning raw data", async () => { + let fixture = await createFixture( + { + files: { + ...files, + "app/routes/resource.tsx": js` + import { json } from '@remix-run/node'; + export function loader({ response }) { + response.status = 201; + response.headers.set('X-Stub', 'yes') + return { message: "RESOURCE" }; + } + `, + }, + }, + ServerMode.Development + ); + let res = await fixture.requestResource("/resource.data"); + expect(res.status).toBe(201); + expect(res.headers.get("X-Stub")).toBe("yes"); + expect(await res.text()).toContain("RESOURCE"); + }); + + test("processes response stub onto resource routes returning responses", async () => { + let fixture = await createFixture( + { + files: { + ...files, + "app/routes/resource.tsx": js` + import { json } from '@react-router/node'; + export function loader({ response }) { + // This will be ignored in favor of the returned Response status + response.status = 200; + response.headers.set('X-Stub', 'yes') + // This will overwrite the returned Response header + response.headers.set('X-Set', '2') + // This will append to the returned Response header + response.headers.append('X-Append', '2') + return json({ message: "RESOURCE" }, { + // This one takes precedence + status: 201, + headers: { + 'X-Response': 'yes', + 'X-Set': '1', + 'X-Append': '1', + }, + }); + } + `, + }, + }, + ServerMode.Development + ); + let res = await fixture.requestResource("/resource"); + expect(res.status).toBe(201); + expect(res.headers.get("X-Response")).toBe("yes"); + expect(res.headers.get("X-Stub")).toBe("yes"); + expect(res.headers.get("X-Set")).toBe("2"); + expect(res.headers.get("X-Append")).toBe("1, 2"); + expect(await res.json()).toEqual({ + message: "RESOURCE", + }); + }); + + test("allows fetcher to hit resource route and return via turbo stream", async ({ + page, + }) => { + let fixture = await createFixture({ + files: { + ...files, + "app/routes/_index.tsx": js` + import { useFetcher } from "react-router"; + export default function Component() { + let fetcher = useFetcher(); + return ( +
+ + {fetcher.data ?
{fetcher.data.message} {fetcher.data.date.toISOString()}
: null} +
+ ); + } + `, + "app/routes/resource.tsx": js` + export function loader() { + // Fetcher calls to resource routes will append ".data" and we'll go through + // the turbo-stream flow. If a user were to curl this endpoint they'd go + // through "handleResourceRoute" and it would be returned as "json()" + return { + message: "RESOURCE", + date: new Date("${ISO_DATE}"), + }; + } + `, + }, + }); + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickElement("#load"); + await page.waitForSelector("#fetcher-data"); + expect(await app.getHtml("#fetcher-data")).toContain( + "RESOURCE 2024-03-12T12:00:00.000Z" + ); + }); + + test("does not log thrown redirect response stubs via handleError", async () => { + let fixture = await createFixture({ + files: { + ...files, + "app/routes/redirect.tsx": js` + export function action({ response }) { + response.status = 301; + response.headers.set("Location", "/data"); + throw response; + } + export function loader({ response }) { + response.status = 301; + response.headers.set("Location", "/data"); + throw response; + } + export default function Component() { + return

Should not see me

; + } + `, + }, + }); + + let errorLogs = []; + console.error = (e) => errorLogs.push(e); + await fixture.requestDocument("/redirect"); + await fixture.requestSingleFetchData("/redirect.data"); + await fixture.requestSingleFetchData("/redirect.data", { + method: "post", + body: null, + }); + expect(errorLogs.length).toBe(0); + }); + + test("does not log thrown non-redirect response stubs via handleError", async () => { + let fixture = await createFixture({ + files: { + ...files, + "app/routes/redirect.tsx": js` + export function action({ response }) { + response.status = 400; + throw response; + } + export function loader({ response }) { + response.status = 400; + throw response; + } + export default function Component() { + return

Should not see me

; + } + `, + }, + }); + + let errorLogs = []; + console.error = (e) => errorLogs.push(e); + await fixture.requestDocument("/redirect"); + expect(errorLogs.length).toBe(1); // ErrorBoundary render logs this + await fixture.requestSingleFetchData("/redirect.data"); + await fixture.requestSingleFetchData("/redirect.data", { + method: "post", + body: null, + }); + expect(errorLogs.length).toBe(1); + }); + + test("supports nonce on streaming script tags", async ({ page }) => { + let fixture = await createFixture({ + files: { + ...files, + "app/root.tsx": js` + import { Links, Meta, Outlet, Scripts } from "react-router"; + export function loader() { + return { + message: "ROOT", + }; + } + export default function Root() { + return ( + + + + + + + + + + + ); + } + `, + "app/entry.server.tsx": js` + import { PassThrough } from "node:stream"; + import type { EntryContext } from "@react-router/node"; + import { createReadableStreamFromReadable } from "@react-router/node"; + import { RemixServer } from "react-router"; + import { renderToPipeableStream } from "react-dom/server"; + export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext + ) { + return new Promise((resolve, reject) => { + const { pipe } = renderToPipeableStream( + , + { + onShellReady() { + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + responseHeaders.set("Content-Type", "text/html"); + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + }, + } + ); + }); + } + `, + }, + }); + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/data", true); + let scripts = await page.$$("script"); + expect(scripts.length).toBe(6); + let remixScriptsCount = 0; + for (let script of scripts) { + let content = await script.innerHTML(); + if (content.includes("window.__remix")) { + remixScriptsCount++; + expect(await script.getAttribute("nonce")).toEqual("the-nonce"); + } + } + expect(remixScriptsCount).toBe(4); + }); + test.describe("client loaders", () => { test("when no routes have client loaders", async ({ page }) => { let fixture = await createFixture( { - singleFetch: true, files: { ...files, "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "A server loader" }; @@ -1434,7 +1691,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "B server loader" }; @@ -1452,7 +1709,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.c.tsx": js` - import { useLoaderData } from "react-router-dom"; + import { useLoaderData } from "react-router"; export function loader() { return { message: "C server loader" }; @@ -1496,11 +1753,10 @@ test.describe("single-fetch", () => { test("when one route has a client loader", async ({ page }) => { let fixture = await createFixture( { - singleFetch: true, files: { ...files, "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "A server loader" }; @@ -1518,7 +1774,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "B server loader" }; @@ -1536,7 +1792,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.c.tsx": js` - import { useLoaderData } from "react-router-dom"; + import { useLoaderData } from "react-router"; export function loader() { return { message: "C server loader" }; @@ -1592,11 +1848,10 @@ test.describe("single-fetch", () => { test("when multiple routes have client loaders", async ({ page }) => { let fixture = await createFixture( { - singleFetch: true, files: { ...files, "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "A server loader" }; @@ -1614,7 +1869,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "B server loader" }; @@ -1637,7 +1892,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.c.tsx": js` - import { useLoaderData } from "react-router-dom"; + import { useLoaderData } from "react-router"; export function loader() { return { message: "C server loader" }; @@ -1695,11 +1950,10 @@ test.describe("single-fetch", () => { test("when all routes have client loaders", async ({ page }) => { let fixture = await createFixture( { - singleFetch: true, files: { ...files, "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "A server loader" }; @@ -1722,7 +1976,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "B server loader" }; @@ -1745,7 +1999,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.c.tsx": js` - import { useLoaderData } from "react-router-dom"; + import { useLoaderData } from "react-router"; export function loader() { return { message: "C server loader" }; @@ -1806,11 +2060,10 @@ test.describe("single-fetch", () => { test("when no routes have client loaders", async ({ page }) => { let fixture = await createFixture( { - singleFetch: true, files: { ...files, "app/routes/_index.tsx": js` - import { Link } from "react-router-dom"; + import { Link } from "react-router"; export default function Index() { return ( @@ -1821,7 +2074,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "A server loader" }; @@ -1839,7 +2092,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "B server loader" }; @@ -1857,7 +2110,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.c.tsx": js` - import { useLoaderData } from "react-router-dom"; + import { useLoaderData } from "react-router"; export function loader() { return { message: "C server loader" }; @@ -1892,11 +2145,10 @@ test.describe("single-fetch", () => { test("when one route has a client loader", async ({ page }) => { let fixture = await createFixture( { - singleFetch: true, files: { ...files, "app/routes/_index.tsx": js` - import { Link } from "react-router-dom"; + import { Link } from "react-router"; export default function Index() { return ( @@ -1907,7 +2159,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "A server loader" }; @@ -1925,7 +2177,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "B server loader" }; @@ -1943,7 +2195,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.c.tsx": js` - import { useLoaderData } from "react-router-dom"; + import { useLoaderData } from "react-router"; export function loader() { return { message: "C server loader" }; @@ -1984,11 +2236,10 @@ test.describe("single-fetch", () => { test("when multiple routes have client loaders", async ({ page }) => { let fixture = await createFixture( { - singleFetch: true, files: { ...files, "app/routes/_index.tsx": js` - import { Link } from "react-router-dom"; + import { Link } from "react-router"; export default function Index() { return ( @@ -1999,7 +2250,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "A server loader" }; @@ -2017,7 +2268,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "B server loader" }; @@ -2040,7 +2291,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.c.tsx": js` - import { useLoaderData } from "react-router-dom"; + import { useLoaderData } from "react-router"; export function loader() { return { message: "C server loader" }; @@ -2081,11 +2332,10 @@ test.describe("single-fetch", () => { test("when all routes have client loaders", async ({ page }) => { let fixture = await createFixture( { - singleFetch: true, files: { ...files, "app/routes/_index.tsx": js` - import { Link } from "react-router-dom"; + import { Link } from "react-router"; export default function Index() { return ( @@ -2096,7 +2346,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "A server loader" }; @@ -2119,7 +2369,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; + import { Outlet, useLoaderData } from "react-router"; export function loader() { return { message: "B server loader" }; @@ -2142,7 +2392,7 @@ test.describe("single-fetch", () => { } `, "app/routes/a.b.c.tsx": js` - import { useLoaderData } from "react-router-dom"; + import { useLoaderData } from "react-router"; export function loader() { return { message: "C server loader" }; diff --git a/integration/vite-prerender-test.ts b/integration/vite-prerender-test.ts index aac8175fa1..5508a2536f 100644 --- a/integration/vite-prerender-test.ts +++ b/integration/vite-prerender-test.ts @@ -9,7 +9,6 @@ import { } from "./helpers/create-fixture.js"; import type { Fixture, AppFixture } from "./helpers/create-fixture.js"; import { PlaywrightFixture } from "./helpers/playwright-fixture.js"; -import { createProject, build } from "./helpers/vite.js"; let files = { "vite.config.ts": js` @@ -20,9 +19,6 @@ let files = { build: { manifest: true }, plugins: [ reactRouter({ - future: { - unstable_singleFetch: true, - }, prerender: ['/', '/about'], }) ], @@ -166,9 +162,6 @@ test.describe("Prerendering", () => { build: { manifest: true }, plugins: [ reactRouter({ - future: { - unstable_singleFetch: true, - }, async prerender() { await new Promise(r => setTimeout(r, 1)); return ['/', '/about']; diff --git a/packages/react-router/lib/dom/ssr/server.tsx b/packages/react-router/lib/dom/ssr/server.tsx index fd83438d12..57d95a12dd 100644 --- a/packages/react-router/lib/dom/ssr/server.tsx +++ b/packages/react-router/lib/dom/ssr/server.tsx @@ -12,6 +12,7 @@ export interface RemixServerProps { context: EntryContext; url: string | URL; abortDelay?: number; + nonce?: string; } /** @@ -23,6 +24,7 @@ export function RemixServer({ context, url, abortDelay, + nonce, }: RemixServerProps): ReactElement { if (typeof url === "string") { url = new URL(url); @@ -98,6 +100,7 @@ export function RemixServer({ identifier={0} reader={context.serverHandoffStream.getReader()} textDecoder={new TextDecoder()} + nonce={nonce} /> ) : null} diff --git a/packages/react-router/lib/dom/ssr/single-fetch.tsx b/packages/react-router/lib/dom/ssr/single-fetch.tsx index c072ba4f0f..3329d3b58e 100644 --- a/packages/react-router/lib/dom/ssr/single-fetch.tsx +++ b/packages/react-router/lib/dom/ssr/single-fetch.tsx @@ -41,6 +41,7 @@ interface StreamTransferProps { identifier: number; reader: ReadableStreamDefaultReader; textDecoder: TextDecoder; + nonce?: string; } // StreamTransfer recursively renders down chunks of the `serverHandoffStream` @@ -50,6 +51,7 @@ export function StreamTransfer({ identifier, reader, textDecoder, + nonce, }: StreamTransferProps) { // If the user didn't render the component then we don't have to // bother streaming anything in @@ -86,6 +88,7 @@ export function StreamTransfer({ let { done, value } = promise.result; let scriptTag = value ? (