Skip to content

Commit 053fd6d

Browse files
authored
Revert "feat: add default web-streams based server entry (#14759)" (#15289)
This reverts commit 2cc25c7.
1 parent 1216fa9 commit 053fd6d

10 files changed

Lines changed: 179 additions & 107 deletions

File tree

docs/api/framework-conventions/entry.server.tsx.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,11 @@ _Note that you generally want to avoid logging when the request was aborted, sin
145145

146146
**Streaming Rendering Errors**
147147

148-
When you are streaming your HTML responses via [`renderToReadableStream`][rendertoreadablestream], your own `handleError` implementation will only handle errors encountered during the initial shell render. If you encounter a rendering error during subsequent streamed rendering you will need to handle these errors manually since the React Router server has already sent the Response by that point. You can handle these errors in the `onError` callback function.
148+
When you are streaming your HTML responses via [`renderToPipeableStream`][rendertopipeablestream] or [`renderToReadableStream`][rendertoreadablestream], your own `handleError` implementation will only handle errors encountered during the initial shell render. If you encounter a rendering error during subsequent streamed rendering you will need to handle these errors manually since the React Router server has already sent the Response by that point.
149149

150-
For an example, please refer to the default [`entry.server.tsx`][streaming-entry-server].
150+
For `renderToPipeableStream`, you can handle these errors in the `onError` callback function. You will need to toggle a boolean in `onShellReady` so you know if the error was a shell rendering error (and can be ignored) or an async
151+
152+
For an example, please refer to the default [`entry.server.tsx`][node-streaming-entry-server] for Node.
151153

152154
**Thrown Responses**
153155

@@ -158,5 +160,5 @@ Note that this does not handle thrown `Response` instances from your `loader`/`a
158160
[streaming]: ../../how-to/suspense
159161
[rendertopipeablestream]: https://react.dev/reference/react-dom/server/renderToPipeableStream
160162
[rendertoreadablestream]: https://react.dev/reference/react-dom/server/renderToReadableStream
161-
[streaming-entry-server]: https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/config/defaults/entry.server.tsx
163+
[node-streaming-entry-server]: https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/config/defaults/entry.server.node.tsx
162164
[templates-repo]: https://github.com/remix-run/react-router-templates

integration/defer-test.ts

Lines changed: 21 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function counterHtml(id: string, val: number) {
3535
return `<p id="count-${id}">${val}</p>`;
3636
}
3737

38-
const deferredHTMLStartString = '<script id="_R_">';
38+
const deferredHTMLStartString = "<template id=";
3939

4040
async function getHtmlSections(
4141
fixture: Fixture,
@@ -46,14 +46,7 @@ async function getHtmlSections(
4646
let html = await response.text();
4747
let deferredIndex = html.indexOf(deferredHTMLStartString);
4848

49-
// If nothing is deferred then we never get the streaming JS code
50-
if (deferredIndex === -1) {
51-
return {
52-
status: response.status,
53-
criticalHTML: html,
54-
deferredHTML: "",
55-
};
56-
}
49+
expect(deferredIndex).toBeGreaterThan(-1);
5750

5851
return {
5952
status: response.status,
@@ -98,7 +91,7 @@ test.describe("non-aborted", () => {
9891
}
9992
`,
10093
"app/root.tsx": js`
101-
import { Links, Meta, Outlet, Scripts, useLoaderData, useLocation } from "react-router";
94+
import { Links, Meta, Outlet, Scripts, useLoaderData } from "react-router";
10295
import Counter from "~/components/counter";
10396
import Interactive from "~/components/interactive";
10497
@@ -112,7 +105,6 @@ test.describe("non-aborted", () => {
112105
113106
export default function Root() {
114107
let { id } = useLoaderData();
115-
let location = useLocation();
116108
return (
117109
<html lang="en">
118110
<head>
@@ -122,13 +114,13 @@ test.describe("non-aborted", () => {
122114
<Links />
123115
</head>
124116
<body>
125-
<main id={id}>
117+
<div id={id}>
126118
<p>{id}</p>
127119
<Counter id={id} />
128120
<Outlet />
129121
<Interactive />
130-
</main>
131-
{location.pathname.startsWith("/deferred-noscript-") ? null : <Scripts />}
122+
</div>
123+
<Scripts />
132124
{/* Send arbitrary data so safari renders the initial shell before
133125
the document finishes downloading. */}
134126
{Array(1000).fill(null).map((_, i)=><p key={i}>YOOOOOOOOOO {i}</p>)}
@@ -579,10 +571,11 @@ test.describe("non-aborted", () => {
579571
expect(status).toBe(200);
580572
expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));
581573
expect(criticalHTML).toContain(counterHtml(INDEX_ID, 0));
582-
expect(deferredHTML).toBe("");
574+
expect(deferredHTML.replace("</body></html>", "")).not.toBe("");
575+
expect(deferredHTML).not.toContain('<p id="count-');
583576
});
584577

585-
test("resolved promises render in initial payload (noscript)", async () => {
578+
test("resolved promises do not render in initial payload", async () => {
586579
let { status, criticalHTML, deferredHTML } = await getHtmlSections(
587580
fixture,
588581
"/deferred-noscript-resolved",
@@ -591,14 +584,12 @@ test.describe("non-aborted", () => {
591584
expect(status).toBe(200);
592585
expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));
593586
expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));
594-
expect(criticalHTML).toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));
595-
expect(criticalHTML).not.toContain(FALLBACK_ID);
596-
expect(deferredHTML).not.toContain(FALLBACK_ID);
587+
expect(criticalHTML).not.toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));
588+
expect(deferredHTML).toContain(FALLBACK_ID);
589+
expect(deferredHTML).toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));
597590
});
598591

599-
test("unresolved promises render in subsequent payload (noscript)", async ({
600-
page,
601-
}) => {
592+
test("slow promises render in subsequent payload", async () => {
602593
let { status, criticalHTML, deferredHTML } = await getHtmlSections(
603594
fixture,
604595
"/deferred-noscript-unresolved",
@@ -607,23 +598,9 @@ test.describe("non-aborted", () => {
607598
expect(status).toBe(200);
608599
expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));
609600
expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));
610-
expect(criticalHTML).toContain(`<div id="${FALLBACK_ID}">`);
611601
expect(criticalHTML).not.toContain(RESOLVED_DEFERRED_ID);
602+
expect(deferredHTML).toContain(`<div id="${FALLBACK_ID}">`);
612603
expect(deferredHTML).toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));
613-
614-
// Hydrates out-of-order streamed content, but does not become interactive
615-
// because we didn't include Scripts
616-
let app = new PlaywrightFixture(appFixture, page);
617-
await app.goto("/deferred-noscript-unresolved", true);
618-
await page.waitForSelector(`main #${RESOLVED_DEFERRED_ID}`);
619-
await page.waitForSelector(
620-
`main #count-${RESOLVED_DEFERRED_ID}:has-text("0")`,
621-
);
622-
await page.locator(`main #increment-${RESOLVED_DEFERRED_ID}`).click();
623-
await new Promise((r) => setTimeout(r, 100));
624-
expect(
625-
await page.locator(`main #count-${RESOLVED_DEFERRED_ID}`).innerText(),
626-
).toBe("0");
627604
});
628605

629606
test("resolved promises render in initial payload", async () => {
@@ -635,12 +612,11 @@ test.describe("non-aborted", () => {
635612
expect(status).toBe(200);
636613
expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));
637614
expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));
638-
expect(criticalHTML).toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));
639-
expect(criticalHTML).not.toContain(FALLBACK_ID);
640-
expect(deferredHTML).not.toContain(FALLBACK_ID);
615+
expect(deferredHTML).toContain(FALLBACK_ID);
616+
expect(deferredHTML).toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));
641617
});
642618

643-
test("unresolved promises render in subsequent payload", async ({ page }) => {
619+
test("slow to resolve promises render in subsequent payload", async () => {
644620
let { status, criticalHTML, deferredHTML } = await getHtmlSections(
645621
fixture,
646622
"/deferred-script-unresolved",
@@ -649,22 +625,9 @@ test.describe("non-aborted", () => {
649625
expect(status).toBe(200);
650626
expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));
651627
expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));
652-
expect(criticalHTML).toContain(`<div id="${FALLBACK_ID}">`);
653628
expect(criticalHTML).not.toContain(RESOLVED_DEFERRED_ID);
629+
expect(deferredHTML).toContain(`<div id="${FALLBACK_ID}">`);
654630
expect(deferredHTML).toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));
655-
656-
// Hydrates out-of-order streamed content and becomes interactive
657-
let app = new PlaywrightFixture(appFixture, page);
658-
await app.goto("/deferred-script-unresolved", true);
659-
await page.waitForSelector(`main #${RESOLVED_DEFERRED_ID}`);
660-
await page.waitForSelector(
661-
`main #count-${RESOLVED_DEFERRED_ID}:has-text("0")`,
662-
);
663-
await page.locator(`main #increment-${RESOLVED_DEFERRED_ID}`).click();
664-
await new Promise((r) => setTimeout(r, 100));
665-
expect(
666-
await page.locator(`main #count-${RESOLVED_DEFERRED_ID}`).innerText(),
667-
).toBe("1");
668631
});
669632

670633
test("rejected promises render in initial payload", async () => {
@@ -676,9 +639,8 @@ test.describe("non-aborted", () => {
676639
expect(status).toBe(200);
677640
expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));
678641
expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));
679-
expect(criticalHTML).toContain(counterHtml(ERROR_ID, 0));
680-
expect(criticalHTML).not.toContain(FALLBACK_ID);
681-
expect(deferredHTML).not.toContain(FALLBACK_ID);
642+
expect(deferredHTML).toContain(FALLBACK_ID);
643+
expect(deferredHTML).toContain(counterHtml(ERROR_ID, 0));
682644
});
683645

684646
test("slow to reject promises render in subsequent payload", async () => {
@@ -690,8 +652,8 @@ test.describe("non-aborted", () => {
690652
expect(status).toBe(200);
691653
expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));
692654
expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));
693-
expect(criticalHTML).toContain(`<div id="${FALLBACK_ID}">`);
694655
expect(criticalHTML).not.toContain(ERROR_ID);
656+
expect(deferredHTML).toContain(`<div id="${FALLBACK_ID}">`);
695657
expect(deferredHTML).toContain(counterHtml(ERROR_ID, 0));
696658
});
697659

packages/react-router-dev/config/defaults/entry.server.tsx renamed to integration/helpers/vite-plugin-cloudflare-template/app/entry.server.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,22 @@ import { ServerRouter } from "react-router";
33
import { isbot } from "isbot";
44
import { renderToReadableStream } from "react-dom/server";
55

6-
export const streamTimeout = 5_000;
7-
86
export default async function handleRequest(
97
request: Request,
108
responseStatusCode: number,
119
responseHeaders: Headers,
1210
routerContext: EntryContext,
1311
_loadContext: RouterContextProvider,
1412
) {
15-
// https://httpwg.org/specs/rfc9110.html#HEAD
16-
if (request.method.toUpperCase() === "HEAD") {
17-
return new Response(null, {
18-
status: responseStatusCode,
19-
headers: responseHeaders,
20-
});
21-
}
22-
2313
let shellRendered = false;
24-
let userAgent = request.headers.get("user-agent");
14+
const userAgent = request.headers.get("user-agent");
2515

2616
const body = await renderToReadableStream(
2717
<ServerRouter context={routerContext} url={request.url} />,
2818
{
29-
signal: AbortSignal.timeout(streamTimeout + 1000),
3019
onError(error: unknown) {
3120
responseStatusCode = 500;
32-
// Log streaming rendering errors from inside the shell. Don't log
21+
// Log streaming rendering errors from inside the shell. Don't log
3322
// errors encountered during initial shell rendering since they'll
3423
// reject and get logged in handleDocumentRequest.
3524
if (shellRendered) {

integration/link-test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,9 +623,12 @@ test.describe("route module link export", () => {
623623
// Scripts:
624624
// RR: window.__reactRouterContext
625625
// RR: window.__reactRouterManifest/window.__reactRouterRouteModules
626+
// React: requestAnimationFrame(function(){$RT=performance.now()});
626627
// RR: window.__reactRouterContext.streamController.enqueue()
628+
// React: $RC=function(b,c,e){...
627629
// RR: window.__reactRouterContext.streamController.close();
628-
expect(scripts.length).toEqual(4);
630+
// React: $RC("B:1","S:1")
631+
expect(scripts.length).toEqual(7);
629632

630633
expect(await scripts[0].innerText()).toContain(
631634
"__reactRouterContext",

integration/vite-plugin-cloudflare-test.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,7 @@ import { expect } from "@playwright/test";
22
import dedent from "dedent";
33
import getPort from "get-port";
44

5-
import {
6-
type Files,
7-
test,
8-
viteConfig,
9-
createProject,
10-
build,
11-
} from "./helpers/vite.js";
5+
import { type Files, test, viteConfig } from "./helpers/vite.js";
126

137
const tsx = dedent;
148
const css = dedent;
@@ -183,15 +177,4 @@ test.describe("vite-plugin-cloudflare", () => {
183177
"20px",
184178
);
185179
});
186-
187-
test("builds project with default server entry", async () => {
188-
const files = defineFiles();
189-
const cwd = await createProject(
190-
await files({ port: 0 }),
191-
"vite-plugin-cloudflare-template",
192-
);
193-
const buildResult = build({ cwd });
194-
195-
expect(buildResult.status).toBe(0);
196-
});
197180
});

packages/react-router-dev/.changes/minor.web-streams-entry.md

Lines changed: 0 additions & 11 deletions
This file was deleted.

packages/react-router-dev/cli/commands.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { readFile, writeFile, copyFile } from "node:fs/promises";
33
import { createRequire } from "node:module";
44
import * as path from "node:path";
55
import exitHook from "exit-hook";
6+
import { readPackageJSON } from "pkg-types";
67
import colors from "picocolors";
78
// Workaround for "ERR_REQUIRE_CYCLE_MODULE" in Node 22.10.0+
89
import "react-router";
@@ -169,14 +170,22 @@ export async function generateEntry(
169170

170171
await copyFile(defaultEntry, outputFile);
171172
} else {
173+
let pkgJson = await readPackageJSON(rootDirectory);
174+
let deps = pkgJson.dependencies ?? {};
175+
176+
if (!deps["@react-router/node"]) {
177+
console.error(colors.red(`No default server entry detected.`));
178+
return;
179+
}
180+
172181
let defaultEntryClient = path.resolve(
173182
defaultsDirectory,
174183
"entry.client.tsx",
175184
);
176185

177186
let defaultEntryServer = path.resolve(
178187
defaultsDirectory,
179-
`entry.server.tsx`,
188+
`entry.server.node.tsx`,
180189
);
181190

182191
let isServerEntry = entry === "entry.server";

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,12 @@ export async function resolveEntryFiles({
10451045
let pkgJson = await readPackageJSON(packageJsonDirectory);
10461046
let deps = pkgJson.dependencies ?? {};
10471047

1048+
if (!deps["@react-router/node"]) {
1049+
throw new Error(
1050+
`Could not determine server runtime. Please install @react-router/node, or provide a custom entry.server.tsx/jsx file in your app directory.`,
1051+
);
1052+
}
1053+
10481054
if (!deps["isbot"]) {
10491055
console.log(
10501056
"adding `isbot@5` to your package.json, you should commit this change",
@@ -1064,7 +1070,7 @@ export async function resolveEntryFiles({
10641070
});
10651071
}
10661072

1067-
entryServerFile = `entry.server.tsx`;
1073+
entryServerFile = `entry.server.node.tsx`;
10681074
}
10691075

10701076
let entryClientFilePath = userEntryClientFile

0 commit comments

Comments
 (0)