From eae81cc8a0a2e3b8f2e8ecbee885bd5cf5ff9f5d Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Thu, 23 Mar 2023 21:40:08 -0400 Subject: [PATCH 1/2] Revert "chore(templates): remove `entry.{client,server}` as they're now optional (#5455)" This reverts commit 4988875856ba4007d4e1e4219b117ce9c7012489. --- templates/arc/app/entry.client.tsx | 22 ++++ templates/arc/app/entry.server.tsx | 21 ++++ .../cloudflare-pages/app/entry.client.tsx | 22 ++++ .../cloudflare-pages/app/entry.server.tsx | 33 +++++ .../cloudflare-workers/app/entry.client.tsx | 22 ++++ .../cloudflare-workers/app/entry.server.tsx | 33 +++++ templates/deno/app/entry.client.tsx | 22 ++++ templates/deno/app/entry.server.tsx | 33 +++++ templates/express/app/entry.client.tsx | 22 ++++ templates/express/app/entry.server.tsx | 113 ++++++++++++++++++ templates/fly/app/entry.client.tsx | 22 ++++ templates/fly/app/entry.server.tsx | 113 ++++++++++++++++++ templates/netlify/app/entry.client.tsx | 22 ++++ templates/netlify/app/entry.server.tsx | 21 ++++ templates/remix/app/entry.client.tsx | 22 ++++ templates/remix/app/entry.server.tsx | 113 ++++++++++++++++++ templates/vercel/app/entry.client.tsx | 22 ++++ templates/vercel/app/entry.server.tsx | 21 ++++ 18 files changed, 699 insertions(+) create mode 100644 templates/arc/app/entry.client.tsx create mode 100644 templates/arc/app/entry.server.tsx create mode 100644 templates/cloudflare-pages/app/entry.client.tsx create mode 100644 templates/cloudflare-pages/app/entry.server.tsx create mode 100644 templates/cloudflare-workers/app/entry.client.tsx create mode 100644 templates/cloudflare-workers/app/entry.server.tsx create mode 100644 templates/deno/app/entry.client.tsx create mode 100644 templates/deno/app/entry.server.tsx create mode 100644 templates/express/app/entry.client.tsx create mode 100644 templates/express/app/entry.server.tsx create mode 100644 templates/fly/app/entry.client.tsx create mode 100644 templates/fly/app/entry.server.tsx create mode 100644 templates/netlify/app/entry.client.tsx create mode 100644 templates/netlify/app/entry.server.tsx create mode 100644 templates/remix/app/entry.client.tsx create mode 100644 templates/remix/app/entry.server.tsx create mode 100644 templates/vercel/app/entry.client.tsx create mode 100644 templates/vercel/app/entry.server.tsx diff --git a/templates/arc/app/entry.client.tsx b/templates/arc/app/entry.client.tsx new file mode 100644 index 00000000000..8338545d164 --- /dev/null +++ b/templates/arc/app/entry.client.tsx @@ -0,0 +1,22 @@ +import { RemixBrowser } from "@remix-run/react"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +function hydrate() { + startTransition(() => { + hydrateRoot( + document, + + + + ); + }); +} + +if (typeof requestIdleCallback === "function") { + requestIdleCallback(hydrate); +} else { + // Safari doesn't support requestIdleCallback + // https://caniuse.com/requestidlecallback + setTimeout(hydrate, 1); +} diff --git a/templates/arc/app/entry.server.tsx b/templates/arc/app/entry.server.tsx new file mode 100644 index 00000000000..fb8ea870984 --- /dev/null +++ b/templates/arc/app/entry.server.tsx @@ -0,0 +1,21 @@ +import type { EntryContext } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import { renderToString } from "react-dom/server"; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + const markup = renderToString( + + ); + + responseHeaders.set("Content-Type", "text/html"); + + return new Response("" + markup, { + headers: responseHeaders, + status: responseStatusCode, + }); +} diff --git a/templates/cloudflare-pages/app/entry.client.tsx b/templates/cloudflare-pages/app/entry.client.tsx new file mode 100644 index 00000000000..8338545d164 --- /dev/null +++ b/templates/cloudflare-pages/app/entry.client.tsx @@ -0,0 +1,22 @@ +import { RemixBrowser } from "@remix-run/react"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +function hydrate() { + startTransition(() => { + hydrateRoot( + document, + + + + ); + }); +} + +if (typeof requestIdleCallback === "function") { + requestIdleCallback(hydrate); +} else { + // Safari doesn't support requestIdleCallback + // https://caniuse.com/requestidlecallback + setTimeout(hydrate, 1); +} diff --git a/templates/cloudflare-pages/app/entry.server.tsx b/templates/cloudflare-pages/app/entry.server.tsx new file mode 100644 index 00000000000..ab0872209f7 --- /dev/null +++ b/templates/cloudflare-pages/app/entry.server.tsx @@ -0,0 +1,33 @@ +import type { EntryContext } from "@remix-run/cloudflare"; +import { RemixServer } from "@remix-run/react"; +import { renderToReadableStream } from "react-dom/server"; +import isbot from "isbot"; + +export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + const stream = await renderToReadableStream( + , + { + signal: request.signal, + onError(error: unknown) { + responseStatusCode = 500; + console.error(error); + }, + } + ); + + if (isbot(request.headers.get("user-agent"))) { + await stream.allReady; + } + + responseHeaders.set("Content-Type", "text/html"); + + return new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }); +} diff --git a/templates/cloudflare-workers/app/entry.client.tsx b/templates/cloudflare-workers/app/entry.client.tsx new file mode 100644 index 00000000000..8338545d164 --- /dev/null +++ b/templates/cloudflare-workers/app/entry.client.tsx @@ -0,0 +1,22 @@ +import { RemixBrowser } from "@remix-run/react"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +function hydrate() { + startTransition(() => { + hydrateRoot( + document, + + + + ); + }); +} + +if (typeof requestIdleCallback === "function") { + requestIdleCallback(hydrate); +} else { + // Safari doesn't support requestIdleCallback + // https://caniuse.com/requestidlecallback + setTimeout(hydrate, 1); +} diff --git a/templates/cloudflare-workers/app/entry.server.tsx b/templates/cloudflare-workers/app/entry.server.tsx new file mode 100644 index 00000000000..8ae6b6395a4 --- /dev/null +++ b/templates/cloudflare-workers/app/entry.server.tsx @@ -0,0 +1,33 @@ +import type { EntryContext } from "@remix-run/cloudflare"; +import { RemixServer } from "@remix-run/react"; +import { renderToReadableStream } from "react-dom/server"; +import isbot from "isbot"; + +export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + const stream = await renderToReadableStream( + , + { + signal: request.signal, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); + + if (isbot(request.headers.get("user-agent"))) { + await stream.allReady; + } + + responseHeaders.set("Content-Type", "text/html"); + + return new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }); +} diff --git a/templates/deno/app/entry.client.tsx b/templates/deno/app/entry.client.tsx new file mode 100644 index 00000000000..3ffb6c2ebc8 --- /dev/null +++ b/templates/deno/app/entry.client.tsx @@ -0,0 +1,22 @@ +import { RemixBrowser } from "@remix-run/react"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +function hydrate() { + startTransition(() => { + hydrateRoot( + document, + + + , + ); + }); +} + +if (typeof requestIdleCallback === "function") { + requestIdleCallback(hydrate); +} else { + // Safari doesn't support requestIdleCallback + // https://caniuse.com/requestidlecallback + setTimeout(hydrate, 1); +} diff --git a/templates/deno/app/entry.server.tsx b/templates/deno/app/entry.server.tsx new file mode 100644 index 00000000000..039c21c03a2 --- /dev/null +++ b/templates/deno/app/entry.server.tsx @@ -0,0 +1,33 @@ +import type { EntryContext } from "@remix-run/deno"; +import { RemixServer } from "@remix-run/react"; +import { renderToReadableStream } from "react-dom/server"; +import isbot from "isbot"; + +export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + const stream = await renderToReadableStream( + , + { + signal: request.signal, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + }, + ); + + if (isbot(request.headers.get("user-agent"))) { + await stream.allReady; + } + + responseHeaders.set("Content-Type", "text/html"); + + return new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }); +} diff --git a/templates/express/app/entry.client.tsx b/templates/express/app/entry.client.tsx new file mode 100644 index 00000000000..8338545d164 --- /dev/null +++ b/templates/express/app/entry.client.tsx @@ -0,0 +1,22 @@ +import { RemixBrowser } from "@remix-run/react"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +function hydrate() { + startTransition(() => { + hydrateRoot( + document, + + + + ); + }); +} + +if (typeof requestIdleCallback === "function") { + requestIdleCallback(hydrate); +} else { + // Safari doesn't support requestIdleCallback + // https://caniuse.com/requestidlecallback + setTimeout(hydrate, 1); +} diff --git a/templates/express/app/entry.server.tsx b/templates/express/app/entry.server.tsx new file mode 100644 index 00000000000..c78d5047bc9 --- /dev/null +++ b/templates/express/app/entry.server.tsx @@ -0,0 +1,113 @@ +import { PassThrough } from "stream"; +import type { EntryContext } from "@remix-run/node"; +import { Response } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import isbot from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return isbot(request.headers.get("user-agent")) + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + const body = new PassThrough(); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + const body = new PassThrough(); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/templates/fly/app/entry.client.tsx b/templates/fly/app/entry.client.tsx new file mode 100644 index 00000000000..8338545d164 --- /dev/null +++ b/templates/fly/app/entry.client.tsx @@ -0,0 +1,22 @@ +import { RemixBrowser } from "@remix-run/react"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +function hydrate() { + startTransition(() => { + hydrateRoot( + document, + + + + ); + }); +} + +if (typeof requestIdleCallback === "function") { + requestIdleCallback(hydrate); +} else { + // Safari doesn't support requestIdleCallback + // https://caniuse.com/requestidlecallback + setTimeout(hydrate, 1); +} diff --git a/templates/fly/app/entry.server.tsx b/templates/fly/app/entry.server.tsx new file mode 100644 index 00000000000..c78d5047bc9 --- /dev/null +++ b/templates/fly/app/entry.server.tsx @@ -0,0 +1,113 @@ +import { PassThrough } from "stream"; +import type { EntryContext } from "@remix-run/node"; +import { Response } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import isbot from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return isbot(request.headers.get("user-agent")) + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + const body = new PassThrough(); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + const body = new PassThrough(); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/templates/netlify/app/entry.client.tsx b/templates/netlify/app/entry.client.tsx new file mode 100644 index 00000000000..8338545d164 --- /dev/null +++ b/templates/netlify/app/entry.client.tsx @@ -0,0 +1,22 @@ +import { RemixBrowser } from "@remix-run/react"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +function hydrate() { + startTransition(() => { + hydrateRoot( + document, + + + + ); + }); +} + +if (typeof requestIdleCallback === "function") { + requestIdleCallback(hydrate); +} else { + // Safari doesn't support requestIdleCallback + // https://caniuse.com/requestidlecallback + setTimeout(hydrate, 1); +} diff --git a/templates/netlify/app/entry.server.tsx b/templates/netlify/app/entry.server.tsx new file mode 100644 index 00000000000..fb8ea870984 --- /dev/null +++ b/templates/netlify/app/entry.server.tsx @@ -0,0 +1,21 @@ +import type { EntryContext } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import { renderToString } from "react-dom/server"; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + const markup = renderToString( + + ); + + responseHeaders.set("Content-Type", "text/html"); + + return new Response("" + markup, { + headers: responseHeaders, + status: responseStatusCode, + }); +} diff --git a/templates/remix/app/entry.client.tsx b/templates/remix/app/entry.client.tsx new file mode 100644 index 00000000000..8338545d164 --- /dev/null +++ b/templates/remix/app/entry.client.tsx @@ -0,0 +1,22 @@ +import { RemixBrowser } from "@remix-run/react"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +function hydrate() { + startTransition(() => { + hydrateRoot( + document, + + + + ); + }); +} + +if (typeof requestIdleCallback === "function") { + requestIdleCallback(hydrate); +} else { + // Safari doesn't support requestIdleCallback + // https://caniuse.com/requestidlecallback + setTimeout(hydrate, 1); +} diff --git a/templates/remix/app/entry.server.tsx b/templates/remix/app/entry.server.tsx new file mode 100644 index 00000000000..c78d5047bc9 --- /dev/null +++ b/templates/remix/app/entry.server.tsx @@ -0,0 +1,113 @@ +import { PassThrough } from "stream"; +import type { EntryContext } from "@remix-run/node"; +import { Response } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import isbot from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return isbot(request.headers.get("user-agent")) + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + const body = new PassThrough(); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + const body = new PassThrough(); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/templates/vercel/app/entry.client.tsx b/templates/vercel/app/entry.client.tsx new file mode 100644 index 00000000000..8338545d164 --- /dev/null +++ b/templates/vercel/app/entry.client.tsx @@ -0,0 +1,22 @@ +import { RemixBrowser } from "@remix-run/react"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +function hydrate() { + startTransition(() => { + hydrateRoot( + document, + + + + ); + }); +} + +if (typeof requestIdleCallback === "function") { + requestIdleCallback(hydrate); +} else { + // Safari doesn't support requestIdleCallback + // https://caniuse.com/requestidlecallback + setTimeout(hydrate, 1); +} diff --git a/templates/vercel/app/entry.server.tsx b/templates/vercel/app/entry.server.tsx new file mode 100644 index 00000000000..fb8ea870984 --- /dev/null +++ b/templates/vercel/app/entry.server.tsx @@ -0,0 +1,21 @@ +import type { EntryContext } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import { renderToString } from "react-dom/server"; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + const markup = renderToString( + + ); + + responseHeaders.set("Content-Type", "text/html"); + + return new Response("" + markup, { + headers: responseHeaders, + status: responseStatusCode, + }); +} From 1f87327f3d2403a4899289ad22e2389b998e3276 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Fri, 24 Mar 2023 12:31:47 -0400 Subject: [PATCH 2/2] chore: add comment that you can delete entry files, update server entries to enable streaming, add isbot --- .../defaults/entry.server.cloudflare.tsx | 2 +- .../config/defaults/entry.server.deno.tsx | 2 +- .../config/defaults/entry.server.node.tsx | 2 +- templates/arc/app/entry.client.tsx | 6 + templates/arc/app/entry.server.tsx | 114 ++++++++++++++++-- templates/arc/package.json | 1 + .../cloudflare-pages/app/entry.client.tsx | 6 + .../cloudflare-pages/app/entry.server.tsx | 17 ++- .../cloudflare-workers/app/entry.client.tsx | 6 + .../cloudflare-workers/app/entry.server.tsx | 15 ++- templates/deno/app/entry.client.tsx | 8 +- templates/deno/app/entry.server.tsx | 19 +-- templates/express/app/entry.client.tsx | 6 + templates/express/app/entry.server.tsx | 10 +- templates/fly/app/entry.client.tsx | 6 + templates/fly/app/entry.server.tsx | 10 +- templates/netlify/app/entry.client.tsx | 6 + templates/netlify/app/entry.server.tsx | 114 ++++++++++++++++-- templates/netlify/package.json | 1 + templates/remix/app/entry.client.tsx | 6 + templates/remix/app/entry.server.tsx | 10 +- templates/vercel/app/entry.client.tsx | 6 + templates/vercel/app/entry.server.tsx | 114 ++++++++++++++++-- templates/vercel/package.json | 1 + 24 files changed, 436 insertions(+), 52 deletions(-) diff --git a/packages/remix-dev/config/defaults/entry.server.cloudflare.tsx b/packages/remix-dev/config/defaults/entry.server.cloudflare.tsx index b854691e5b8..3e08f46fe53 100644 --- a/packages/remix-dev/config/defaults/entry.server.cloudflare.tsx +++ b/packages/remix-dev/config/defaults/entry.server.cloudflare.tsx @@ -13,7 +13,7 @@ export default async function handleRequest( , { signal: request.signal, - onError(error) { + onError(error: unknown) { console.error(error); responseStatusCode = 500; }, diff --git a/packages/remix-dev/config/defaults/entry.server.deno.tsx b/packages/remix-dev/config/defaults/entry.server.deno.tsx index 96ff812aa2f..7be6c748061 100644 --- a/packages/remix-dev/config/defaults/entry.server.deno.tsx +++ b/packages/remix-dev/config/defaults/entry.server.deno.tsx @@ -13,7 +13,7 @@ export default async function handleRequest( , { signal: request.signal, - onError(error) { + onError(error: unknown) { console.error(error); responseStatusCode = 500; }, diff --git a/packages/remix-dev/config/defaults/entry.server.node.tsx b/packages/remix-dev/config/defaults/entry.server.node.tsx index 98b32eb3867..469de6d4329 100644 --- a/packages/remix-dev/config/defaults/entry.server.node.tsx +++ b/packages/remix-dev/config/defaults/entry.server.node.tsx @@ -5,7 +5,7 @@ import { RemixServer } from "@remix-run/react"; import isbot from "isbot"; import { renderToPipeableStream } from "react-dom/server"; -const ABORT_DELAY = 5000; +const ABORT_DELAY = 5_000; export default function handleRequest( request: Request, diff --git a/templates/arc/app/entry.client.tsx b/templates/arc/app/entry.client.tsx index 8338545d164..3e8ec03d450 100644 --- a/templates/arc/app/entry.client.tsx +++ b/templates/arc/app/entry.client.tsx @@ -1,3 +1,9 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.client + */ + import { RemixBrowser } from "@remix-run/react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; diff --git a/templates/arc/app/entry.server.tsx b/templates/arc/app/entry.server.tsx index fb8ea870984..0565fb5bc9a 100644 --- a/templates/arc/app/entry.server.tsx +++ b/templates/arc/app/entry.server.tsx @@ -1,6 +1,17 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.server + */ + +import { PassThrough } from "stream"; import type { EntryContext } from "@remix-run/node"; +import { Response } from "@remix-run/node"; import { RemixServer } from "@remix-run/react"; -import { renderToString } from "react-dom/server"; +import isbot from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; export default function handleRequest( request: Request, @@ -8,14 +19,101 @@ export default function handleRequest( responseHeaders: Headers, remixContext: EntryContext ) { - const markup = renderToString( - - ); + return isbot(request.headers.get("user-agent")) + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + const body = new PassThrough(); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + console.error(error); + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + const body = new PassThrough(); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); - responseHeaders.set("Content-Type", "text/html"); + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); - return new Response("" + markup, { - headers: responseHeaders, - status: responseStatusCode, + setTimeout(abort, ABORT_DELAY); }); } diff --git a/templates/arc/package.json b/templates/arc/package.json index cb0485a1664..c0697540150 100644 --- a/templates/arc/package.json +++ b/templates/arc/package.json @@ -14,6 +14,7 @@ "@remix-run/node": "*", "@remix-run/react": "*", "cross-env": "^7.0.3", + "isbot": "^3.6.5", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/templates/cloudflare-pages/app/entry.client.tsx b/templates/cloudflare-pages/app/entry.client.tsx index 8338545d164..3e8ec03d450 100644 --- a/templates/cloudflare-pages/app/entry.client.tsx +++ b/templates/cloudflare-pages/app/entry.client.tsx @@ -1,3 +1,9 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.client + */ + import { RemixBrowser } from "@remix-run/react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; diff --git a/templates/cloudflare-pages/app/entry.server.tsx b/templates/cloudflare-pages/app/entry.server.tsx index ab0872209f7..361b4560500 100644 --- a/templates/cloudflare-pages/app/entry.server.tsx +++ b/templates/cloudflare-pages/app/entry.server.tsx @@ -1,7 +1,13 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.server + */ + import type { EntryContext } from "@remix-run/cloudflare"; import { RemixServer } from "@remix-run/react"; -import { renderToReadableStream } from "react-dom/server"; import isbot from "isbot"; +import { renderToReadableStream } from "react-dom/server"; export default async function handleRequest( request: Request, @@ -9,24 +15,23 @@ export default async function handleRequest( responseHeaders: Headers, remixContext: EntryContext ) { - const stream = await renderToReadableStream( + const body = await renderToReadableStream( , { signal: request.signal, onError(error: unknown) { - responseStatusCode = 500; console.error(error); + responseStatusCode = 500; }, } ); if (isbot(request.headers.get("user-agent"))) { - await stream.allReady; + await body.allReady; } responseHeaders.set("Content-Type", "text/html"); - - return new Response(stream, { + return new Response(body, { headers: responseHeaders, status: responseStatusCode, }); diff --git a/templates/cloudflare-workers/app/entry.client.tsx b/templates/cloudflare-workers/app/entry.client.tsx index 8338545d164..3e8ec03d450 100644 --- a/templates/cloudflare-workers/app/entry.client.tsx +++ b/templates/cloudflare-workers/app/entry.client.tsx @@ -1,3 +1,9 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.client + */ + import { RemixBrowser } from "@remix-run/react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; diff --git a/templates/cloudflare-workers/app/entry.server.tsx b/templates/cloudflare-workers/app/entry.server.tsx index 8ae6b6395a4..361b4560500 100644 --- a/templates/cloudflare-workers/app/entry.server.tsx +++ b/templates/cloudflare-workers/app/entry.server.tsx @@ -1,7 +1,13 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.server + */ + import type { EntryContext } from "@remix-run/cloudflare"; import { RemixServer } from "@remix-run/react"; -import { renderToReadableStream } from "react-dom/server"; import isbot from "isbot"; +import { renderToReadableStream } from "react-dom/server"; export default async function handleRequest( request: Request, @@ -9,7 +15,7 @@ export default async function handleRequest( responseHeaders: Headers, remixContext: EntryContext ) { - const stream = await renderToReadableStream( + const body = await renderToReadableStream( , { signal: request.signal, @@ -21,12 +27,11 @@ export default async function handleRequest( ); if (isbot(request.headers.get("user-agent"))) { - await stream.allReady; + await body.allReady; } responseHeaders.set("Content-Type", "text/html"); - - return new Response(stream, { + return new Response(body, { headers: responseHeaders, status: responseStatusCode, }); diff --git a/templates/deno/app/entry.client.tsx b/templates/deno/app/entry.client.tsx index 3ffb6c2ebc8..3e8ec03d450 100644 --- a/templates/deno/app/entry.client.tsx +++ b/templates/deno/app/entry.client.tsx @@ -1,3 +1,9 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.client + */ + import { RemixBrowser } from "@remix-run/react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; @@ -8,7 +14,7 @@ function hydrate() { document, - , + ); }); } diff --git a/templates/deno/app/entry.server.tsx b/templates/deno/app/entry.server.tsx index 039c21c03a2..5354bb7fb2e 100644 --- a/templates/deno/app/entry.server.tsx +++ b/templates/deno/app/entry.server.tsx @@ -1,15 +1,21 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.server + */ + import type { EntryContext } from "@remix-run/deno"; import { RemixServer } from "@remix-run/react"; -import { renderToReadableStream } from "react-dom/server"; import isbot from "isbot"; +import { renderToReadableStream } from "react-dom/server"; export default async function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, - remixContext: EntryContext, + remixContext: EntryContext ) { - const stream = await renderToReadableStream( + const body = await renderToReadableStream( , { signal: request.signal, @@ -17,16 +23,15 @@ export default async function handleRequest( console.error(error); responseStatusCode = 500; }, - }, + } ); if (isbot(request.headers.get("user-agent"))) { - await stream.allReady; + await body.allReady; } responseHeaders.set("Content-Type", "text/html"); - - return new Response(stream, { + return new Response(body, { headers: responseHeaders, status: responseStatusCode, }); diff --git a/templates/express/app/entry.client.tsx b/templates/express/app/entry.client.tsx index 8338545d164..3e8ec03d450 100644 --- a/templates/express/app/entry.client.tsx +++ b/templates/express/app/entry.client.tsx @@ -1,3 +1,9 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.client + */ + import { RemixBrowser } from "@remix-run/react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; diff --git a/templates/express/app/entry.server.tsx b/templates/express/app/entry.server.tsx index c78d5047bc9..0565fb5bc9a 100644 --- a/templates/express/app/entry.server.tsx +++ b/templates/express/app/entry.server.tsx @@ -1,3 +1,9 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.server + */ + import { PassThrough } from "stream"; import type { EntryContext } from "@remix-run/node"; import { Response } from "@remix-run/node"; @@ -5,7 +11,7 @@ import { RemixServer } from "@remix-run/react"; import isbot from "isbot"; import { renderToPipeableStream } from "react-dom/server"; -const ABORT_DELAY = 5000; +const ABORT_DELAY = 5_000; export default function handleRequest( request: Request, @@ -60,8 +66,8 @@ function handleBotRequest( reject(error); }, onError(error: unknown) { - console.error(error); responseStatusCode = 500; + console.error(error); }, } ); diff --git a/templates/fly/app/entry.client.tsx b/templates/fly/app/entry.client.tsx index 8338545d164..3e8ec03d450 100644 --- a/templates/fly/app/entry.client.tsx +++ b/templates/fly/app/entry.client.tsx @@ -1,3 +1,9 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.client + */ + import { RemixBrowser } from "@remix-run/react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; diff --git a/templates/fly/app/entry.server.tsx b/templates/fly/app/entry.server.tsx index c78d5047bc9..0565fb5bc9a 100644 --- a/templates/fly/app/entry.server.tsx +++ b/templates/fly/app/entry.server.tsx @@ -1,3 +1,9 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.server + */ + import { PassThrough } from "stream"; import type { EntryContext } from "@remix-run/node"; import { Response } from "@remix-run/node"; @@ -5,7 +11,7 @@ import { RemixServer } from "@remix-run/react"; import isbot from "isbot"; import { renderToPipeableStream } from "react-dom/server"; -const ABORT_DELAY = 5000; +const ABORT_DELAY = 5_000; export default function handleRequest( request: Request, @@ -60,8 +66,8 @@ function handleBotRequest( reject(error); }, onError(error: unknown) { - console.error(error); responseStatusCode = 500; + console.error(error); }, } ); diff --git a/templates/netlify/app/entry.client.tsx b/templates/netlify/app/entry.client.tsx index 8338545d164..3e8ec03d450 100644 --- a/templates/netlify/app/entry.client.tsx +++ b/templates/netlify/app/entry.client.tsx @@ -1,3 +1,9 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.client + */ + import { RemixBrowser } from "@remix-run/react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; diff --git a/templates/netlify/app/entry.server.tsx b/templates/netlify/app/entry.server.tsx index fb8ea870984..0565fb5bc9a 100644 --- a/templates/netlify/app/entry.server.tsx +++ b/templates/netlify/app/entry.server.tsx @@ -1,6 +1,17 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.server + */ + +import { PassThrough } from "stream"; import type { EntryContext } from "@remix-run/node"; +import { Response } from "@remix-run/node"; import { RemixServer } from "@remix-run/react"; -import { renderToString } from "react-dom/server"; +import isbot from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; export default function handleRequest( request: Request, @@ -8,14 +19,101 @@ export default function handleRequest( responseHeaders: Headers, remixContext: EntryContext ) { - const markup = renderToString( - - ); + return isbot(request.headers.get("user-agent")) + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + const body = new PassThrough(); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + console.error(error); + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + const body = new PassThrough(); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); - responseHeaders.set("Content-Type", "text/html"); + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); - return new Response("" + markup, { - headers: responseHeaders, - status: responseStatusCode, + setTimeout(abort, ABORT_DELAY); }); } diff --git a/templates/netlify/package.json b/templates/netlify/package.json index b8f1f93ee16..72faa279bb1 100644 --- a/templates/netlify/package.json +++ b/templates/netlify/package.json @@ -13,6 +13,7 @@ "@remix-run/node": "*", "@remix-run/react": "*", "cross-env": "^7.0.3", + "isbot": "^3.6.5", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/templates/remix/app/entry.client.tsx b/templates/remix/app/entry.client.tsx index 8338545d164..3e8ec03d450 100644 --- a/templates/remix/app/entry.client.tsx +++ b/templates/remix/app/entry.client.tsx @@ -1,3 +1,9 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.client + */ + import { RemixBrowser } from "@remix-run/react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; diff --git a/templates/remix/app/entry.server.tsx b/templates/remix/app/entry.server.tsx index c78d5047bc9..0565fb5bc9a 100644 --- a/templates/remix/app/entry.server.tsx +++ b/templates/remix/app/entry.server.tsx @@ -1,3 +1,9 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.server + */ + import { PassThrough } from "stream"; import type { EntryContext } from "@remix-run/node"; import { Response } from "@remix-run/node"; @@ -5,7 +11,7 @@ import { RemixServer } from "@remix-run/react"; import isbot from "isbot"; import { renderToPipeableStream } from "react-dom/server"; -const ABORT_DELAY = 5000; +const ABORT_DELAY = 5_000; export default function handleRequest( request: Request, @@ -60,8 +66,8 @@ function handleBotRequest( reject(error); }, onError(error: unknown) { - console.error(error); responseStatusCode = 500; + console.error(error); }, } ); diff --git a/templates/vercel/app/entry.client.tsx b/templates/vercel/app/entry.client.tsx index 8338545d164..3e8ec03d450 100644 --- a/templates/vercel/app/entry.client.tsx +++ b/templates/vercel/app/entry.client.tsx @@ -1,3 +1,9 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.client + */ + import { RemixBrowser } from "@remix-run/react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; diff --git a/templates/vercel/app/entry.server.tsx b/templates/vercel/app/entry.server.tsx index fb8ea870984..0565fb5bc9a 100644 --- a/templates/vercel/app/entry.server.tsx +++ b/templates/vercel/app/entry.server.tsx @@ -1,6 +1,17 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/docs/en/main/file-conventions/entry.server + */ + +import { PassThrough } from "stream"; import type { EntryContext } from "@remix-run/node"; +import { Response } from "@remix-run/node"; import { RemixServer } from "@remix-run/react"; -import { renderToString } from "react-dom/server"; +import isbot from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; export default function handleRequest( request: Request, @@ -8,14 +19,101 @@ export default function handleRequest( responseHeaders: Headers, remixContext: EntryContext ) { - const markup = renderToString( - - ); + return isbot(request.headers.get("user-agent")) + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + const body = new PassThrough(); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + console.error(error); + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + const body = new PassThrough(); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); - responseHeaders.set("Content-Type", "text/html"); + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); - return new Response("" + markup, { - headers: responseHeaders, - status: responseStatusCode, + setTimeout(abort, ABORT_DELAY); }); } diff --git a/templates/vercel/package.json b/templates/vercel/package.json index 46ba5732c9f..5ae229ae25c 100644 --- a/templates/vercel/package.json +++ b/templates/vercel/package.json @@ -11,6 +11,7 @@ "@remix-run/react": "*", "@remix-run/vercel": "*", "@vercel/node": "^2.6.2", + "isbot": "^3.6.5", "react": "^18.2.0", "react-dom": "^18.2.0" },