diff --git a/.eslintrc.js b/.eslintrc.js index 3d360f2cf..6f44cdff2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,6 +30,7 @@ module.exports = { "@typescript-eslint/no-unsafe-argument": 0, "@typescript-eslint/no-non-null-assertion": 0, "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/unbound-method": 0, "react/prop-types": 0, "@typescript-eslint/no-restricted-imports": [ "error", diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 3e5ce2426..b782b92c4 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -1,32 +1,125 @@ +import { PassThrough } from "stream"; + import type { EntryContext } from "@remix-run/node"; +import { Response } from "@remix-run/node"; import { RemixServer } from "@remix-run/react"; -import { I18nextProvider } from "react-i18next"; -import { renderToString } from "react-dom/server"; +import isbot from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; import cron from "node-cron"; import { updatePatreonData } from "./modules/patreon"; import { i18Instance } from "./modules/i18n"; +import { I18nextProvider } from "react-i18next"; -export default async function handleRequest( +const ABORT_DELAY = 5000; + +const handleRequest = ( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext -) { - const i18n = await i18Instance(request, remixContext); +) => + isbot(request.headers.get("user-agent")) + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ); +export default handleRequest; - const markup = renderToString( - - - - ); +const handleBotRequest = ( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) => + new Promise((resolve, reject) => { + let didError = false; - responseHeaders.set("Content-Type", "text/html"); + void i18Instance(request, remixContext).then((i18n) => { + const { pipe, abort } = renderToPipeableStream( + + + , + { + onAllReady: () => { + const body = new PassThrough(); - return new Response("" + markup, { - status: responseStatusCode, - headers: responseHeaders, + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(body, { + headers: responseHeaders, + status: didError ? 500 : responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError: (error: unknown) => { + reject(error); + }, + onError: (error: unknown) => { + didError = true; + + console.error(error); + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); + }); + +const handleBrowserRequest = ( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) => + new Promise((resolve, reject) => { + let didError = false; + + void i18Instance(request, remixContext).then((i18n) => { + const { pipe, abort } = renderToPipeableStream( + + + , + { + onShellReady: () => { + const body = new PassThrough(); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(body, { + headers: responseHeaders, + status: didError ? 500 : responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError: (error: unknown) => { + reject(error); + }, + onError: (error: unknown) => { + didError = true; + + console.error(error); + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); }); -} // example from https://github.com/BenMcH/remix-rss/blob/main/app/entry.server.tsx declare global { diff --git a/app/routes/plans.tsx b/app/routes/plans.tsx index c2a999228..3ddf539f3 100644 --- a/app/routes/plans.tsx +++ b/app/routes/plans.tsx @@ -2,6 +2,7 @@ import { lazy, Suspense } from "react"; import type { LinksFunction } from "@remix-run/node"; import styles from "~/styles/plans.css"; import type { SendouRouteHandle } from "~/utils/remix"; +import { useIsMounted } from "~/hooks/useIsMounted"; export const handle: SendouRouteHandle = { i18n: ["weapons"], @@ -14,9 +15,9 @@ export const links: LinksFunction = () => { const Planner = lazy(() => import("~/components/Planner")); export default function MapPlannerPage() { - return ( - }> - - - ); + const isMounted = useIsMounted(); + + if (!isMounted) return
; + + return ; } diff --git a/package-lock.json b/package-lock.json index 4d0db64f4..6c3dd577b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "i18next-browser-languagedetector": "^6.1.5", "i18next-fs-backend": "^1.1.5", "i18next-http-backend": "^1.4.4", + "isbot": "^3.6.5", "just-capitalize": "^3.1.1", "just-clone": "^6.1.1", "just-random-integer": "^4.1.1", @@ -10050,6 +10051,14 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "node_modules/isbot": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-3.6.5.tgz", + "integrity": "sha512-BchONELXt6yMad++BwGpa0oQxo/uD0keL7N15cYVf0A1oMIoNQ79OqeYdPMFWDrNhCqCbRuw9Y9F3QBjvAxZ5g==", + "engines": { + "node": ">=12" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -24062,6 +24071,11 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "isbot": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-3.6.5.tgz", + "integrity": "sha512-BchONELXt6yMad++BwGpa0oQxo/uD0keL7N15cYVf0A1oMIoNQ79OqeYdPMFWDrNhCqCbRuw9Y9F3QBjvAxZ5g==" + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", diff --git a/package.json b/package.json index f9dee72ca..575111954 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "i18next-browser-languagedetector": "^6.1.5", "i18next-fs-backend": "^1.1.5", "i18next-http-backend": "^1.4.4", + "isbot": "^3.6.5", "just-capitalize": "^3.1.1", "just-clone": "^6.1.1", "just-random-integer": "^4.1.1",