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 new file mode 100644 index 00000000000..3e8ec03d450 --- /dev/null +++ b/templates/arc/app/entry.client.tsx @@ -0,0 +1,28 @@ +/** + * 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"; + +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..0565fb5bc9a --- /dev/null +++ b/templates/arc/app/entry.server.tsx @@ -0,0 +1,119 @@ +/** + * 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 isbot from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; + +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) { + 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, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); + + 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 new file mode 100644 index 00000000000..3e8ec03d450 --- /dev/null +++ b/templates/cloudflare-pages/app/entry.client.tsx @@ -0,0 +1,28 @@ +/** + * 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"; + +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..361b4560500 --- /dev/null +++ b/templates/cloudflare-pages/app/entry.server.tsx @@ -0,0 +1,38 @@ +/** + * 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 isbot from "isbot"; +import { renderToReadableStream } from "react-dom/server"; + +export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + const body = await renderToReadableStream( + , + { + signal: request.signal, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); + + if (isbot(request.headers.get("user-agent"))) { + await body.allReady; + } + + responseHeaders.set("Content-Type", "text/html"); + 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 new file mode 100644 index 00000000000..3e8ec03d450 --- /dev/null +++ b/templates/cloudflare-workers/app/entry.client.tsx @@ -0,0 +1,28 @@ +/** + * 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"; + +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..361b4560500 --- /dev/null +++ b/templates/cloudflare-workers/app/entry.server.tsx @@ -0,0 +1,38 @@ +/** + * 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 isbot from "isbot"; +import { renderToReadableStream } from "react-dom/server"; + +export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + const body = await renderToReadableStream( + , + { + signal: request.signal, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); + + if (isbot(request.headers.get("user-agent"))) { + await body.allReady; + } + + responseHeaders.set("Content-Type", "text/html"); + return new Response(body, { + 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..3e8ec03d450 --- /dev/null +++ b/templates/deno/app/entry.client.tsx @@ -0,0 +1,28 @@ +/** + * 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"; + +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..5354bb7fb2e --- /dev/null +++ b/templates/deno/app/entry.server.tsx @@ -0,0 +1,38 @@ +/** + * 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 isbot from "isbot"; +import { renderToReadableStream } from "react-dom/server"; + +export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + const body = await renderToReadableStream( + , + { + signal: request.signal, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); + + if (isbot(request.headers.get("user-agent"))) { + await body.allReady; + } + + responseHeaders.set("Content-Type", "text/html"); + return new Response(body, { + 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..3e8ec03d450 --- /dev/null +++ b/templates/express/app/entry.client.tsx @@ -0,0 +1,28 @@ +/** + * 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"; + +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..0565fb5bc9a --- /dev/null +++ b/templates/express/app/entry.server.tsx @@ -0,0 +1,119 @@ +/** + * 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 isbot from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; + +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) { + 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, + }) + ); + + 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..3e8ec03d450 --- /dev/null +++ b/templates/fly/app/entry.client.tsx @@ -0,0 +1,28 @@ +/** + * 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"; + +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..0565fb5bc9a --- /dev/null +++ b/templates/fly/app/entry.server.tsx @@ -0,0 +1,119 @@ +/** + * 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 isbot from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; + +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) { + 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, + }) + ); + + 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..3e8ec03d450 --- /dev/null +++ b/templates/netlify/app/entry.client.tsx @@ -0,0 +1,28 @@ +/** + * 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"; + +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..0565fb5bc9a --- /dev/null +++ b/templates/netlify/app/entry.server.tsx @@ -0,0 +1,119 @@ +/** + * 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 isbot from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; + +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) { + 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, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); + + 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 new file mode 100644 index 00000000000..3e8ec03d450 --- /dev/null +++ b/templates/remix/app/entry.client.tsx @@ -0,0 +1,28 @@ +/** + * 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"; + +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..0565fb5bc9a --- /dev/null +++ b/templates/remix/app/entry.server.tsx @@ -0,0 +1,119 @@ +/** + * 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 isbot from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; + +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) { + 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, + }) + ); + + 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..3e8ec03d450 --- /dev/null +++ b/templates/vercel/app/entry.client.tsx @@ -0,0 +1,28 @@ +/** + * 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"; + +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..0565fb5bc9a --- /dev/null +++ b/templates/vercel/app/entry.server.tsx @@ -0,0 +1,119 @@ +/** + * 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 isbot from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; + +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) { + 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, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + } + ); + + 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" },