From a3a2bc9dc1c339e26cf93e3b510f280acaab5027 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Mon, 10 Mar 2025 03:01:44 +0900 Subject: feat(fontend): migrate from Remix to React Router --- frontend/app/.server/auth.ts | 59 ++++++----- frontend/app/.server/cookie.ts | 2 +- frontend/app/.server/session.ts | 2 +- .../components/GolfPlayApps/GolfPlayAppGaming.tsx | 2 +- frontend/app/components/NavigateLink.tsx | 2 +- frontend/app/entry.client.tsx | 12 --- frontend/app/entry.server.tsx | 116 --------------------- frontend/app/root.tsx | 10 +- frontend/app/routes.ts | 4 + frontend/app/routes/_index.tsx | 2 +- frontend/app/routes/dashboard.tsx | 4 +- frontend/app/routes/golf.$gameId.play.tsx | 4 +- frontend/app/routes/golf.$gameId.watch.tsx | 4 +- frontend/app/routes/login.tsx | 8 +- frontend/app/routes/logout.tsx | 2 +- 15 files changed, 54 insertions(+), 179 deletions(-) delete mode 100644 frontend/app/entry.client.tsx delete mode 100644 frontend/app/entry.server.tsx create mode 100644 frontend/app/routes.ts (limited to 'frontend/app') diff --git a/frontend/app/.server/auth.ts b/frontend/app/.server/auth.ts index 386eb70..cbeb141 100644 --- a/frontend/app/.server/auth.ts +++ b/frontend/app/.server/auth.ts @@ -1,6 +1,5 @@ -import { redirect } from "@remix-run/node"; -import type { Session } from "@remix-run/server-runtime"; import { jwtDecode } from "jwt-decode"; +import { redirect } from "react-router"; import { Authenticator } from "remix-auth"; import { FormStrategy } from "remix-auth-form"; import { apiPostLogin } from "../api/client"; @@ -8,7 +7,7 @@ import { components } from "../api/schema"; import { createUnstructuredCookie } from "./cookie"; import { cookieOptions, sessionStorage } from "./session"; -const authenticator = new Authenticator(sessionStorage); +const authenticator = new Authenticator(); authenticator.use( new FormStrategy(async ({ form }) => { @@ -29,14 +28,12 @@ const tokenCookie = createUnstructuredCookie("albatross_token", cookieOptions); * @throws Error on failure */ export async function login(request: Request): Promise { - const jwt = await authenticator.authenticate("default", request, { - throwOnError: true, - }); + const jwt = await authenticator.authenticate("default", request); const session = await sessionStorage.getSession( request.headers.get("cookie"), ); - session.set(authenticator.sessionKey, jwt); + session.set("user", jwt); throw redirect("/dashboard", { headers: [ @@ -46,34 +43,42 @@ export async function login(request: Request): Promise { }); } -export async function logout(request: Request | Session): Promise { - try { - return await authenticator.logout(request, { redirectTo: "/" }); - } catch (response) { - if (response instanceof Response) { - response.headers.append( +export async function logout(request: Request): Promise { + const session = await sessionStorage.getSession( + request.headers.get("cookie"), + ); + throw redirect("/", { + headers: [ + ["Set-Cookie", await sessionStorage.destroySession(session)], + [ "Set-Cookie", await tokenCookie.serialize("", { maxAge: 0, expires: new Date(0) }), - ); - } - throw response; - } + ], + ], + }); } export async function ensureUserLoggedIn( - request: Request | Session, + request: Request, ): Promise<{ user: User; token: string }> { - const token = await authenticator.isAuthenticated(request, { - failureRedirect: "/login", - }); + const session = await sessionStorage.getSession( + request.headers.get("cookie"), + ); + const token = session.get("user"); + if (!token) { + throw redirect("/login"); + } const user = jwtDecode(token); return { user, token }; } -export async function ensureUserNotLoggedIn( - request: Request | Session, -): Promise { - return await authenticator.isAuthenticated(request, { - successRedirect: "/dashboard", - }); +export async function ensureUserNotLoggedIn(request: Request): Promise { + const session = await sessionStorage.getSession( + request.headers.get("cookie"), + ); + const token = session.get("user"); + if (token) { + throw redirect("/dashboard"); + } + return null; } diff --git a/frontend/app/.server/cookie.ts b/frontend/app/.server/cookie.ts index 4552081..c8365eb 100644 --- a/frontend/app/.server/cookie.ts +++ b/frontend/app/.server/cookie.ts @@ -1,5 +1,5 @@ -import { Cookie, CookieOptions } from "@remix-run/server-runtime"; import { parse as parseCookie, serialize as serializeCookie } from "cookie"; +import { Cookie, CookieOptions } from "react-router"; // Remix's createCookie() returns "structured" cookies, which are cookies that hold a JSON-encoded object. // This is not suitable for interoperation with other systems that expect a simple string value. diff --git a/frontend/app/.server/session.ts b/frontend/app/.server/session.ts index 4730305..2d9a652 100644 --- a/frontend/app/.server/session.ts +++ b/frontend/app/.server/session.ts @@ -1,4 +1,4 @@ -import { createCookieSessionStorage } from "@remix-run/node"; +import { createCookieSessionStorage } from "react-router"; export const cookieOptions = { sameSite: "lax" as const, diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx index 76ffcb8..ec92556 100644 --- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx +++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx @@ -1,6 +1,6 @@ -import { Link } from "@remix-run/react"; import { useAtomValue } from "jotai"; import React, { useRef } from "react"; +import { Link } from "react-router"; import SubmitButton from "../../components/SubmitButton"; import { gamingLeftTimeSecondsAtom, diff --git a/frontend/app/components/NavigateLink.tsx b/frontend/app/components/NavigateLink.tsx index 95c3bcf..c4ee7aa 100644 --- a/frontend/app/components/NavigateLink.tsx +++ b/frontend/app/components/NavigateLink.tsx @@ -1,4 +1,4 @@ -import { Link, LinkProps } from "@remix-run/react"; +import { Link, LinkProps } from "react-router"; export default function NavigateLink(props: LinkProps) { return ( diff --git a/frontend/app/entry.client.tsx b/frontend/app/entry.client.tsx deleted file mode 100644 index 92d1585..0000000 --- a/frontend/app/entry.client.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { RemixBrowser } from "@remix-run/react"; -import { StrictMode, startTransition } from "react"; -import { hydrateRoot } from "react-dom/client"; - -startTransition(() => { - hydrateRoot( - document, - - - , - ); -}); diff --git a/frontend/app/entry.server.tsx b/frontend/app/entry.server.tsx deleted file mode 100644 index 6234421..0000000 --- a/frontend/app/entry.server.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { PassThrough } from "node:stream"; - -import type { EntryContext } from "@remix-run/node"; -import { createReadableStreamFromReadable } from "@remix-run/node"; -import { RemixServer } from "@remix-run/react"; -import { isbot } from "isbot"; -import { renderToPipeableStream } from "react-dom/server"; - -export const streamTimeout = 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) => { - let shellRendered = false; - const { pipe, abort } = renderToPipeableStream( - , - { - onAllReady() { - shellRendered = true; - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); - - responseHeaders.set("Content-Type", "text/html"); - - resolve( - new Response(stream, { - headers: responseHeaders, - status: responseStatusCode, - }), - ); - - pipe(body); - }, - onShellError(error: unknown) { - reject(error); - }, - onError(error: unknown) { - responseStatusCode = 500; - if (shellRendered) { - console.error(error); - } - }, - }, - ); - - +setTimeout(abort, streamTimeout + 1000); - }); -} - -function handleBrowserRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext, -) { - return new Promise((resolve, reject) => { - let shellRendered = false; - const { pipe, abort } = renderToPipeableStream( - , - { - onShellReady() { - shellRendered = true; - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); - - responseHeaders.set("Content-Type", "text/html"); - - resolve( - new Response(stream, { - headers: responseHeaders, - status: responseStatusCode, - }), - ); - - pipe(body); - }, - onShellError(error: unknown) { - reject(error); - }, - onError(error: unknown) { - responseStatusCode = 500; - if (shellRendered) { - console.error(error); - } - }, - }, - ); - - setTimeout(abort, streamTimeout + 1000); - }); -} diff --git a/frontend/app/root.tsx b/frontend/app/root.tsx index 20aa910..5721a2b 100644 --- a/frontend/app/root.tsx +++ b/frontend/app/root.tsx @@ -1,13 +1,7 @@ import { config } from "@fortawesome/fontawesome-svg-core"; import "@fortawesome/fontawesome-svg-core/styles.css"; -import type { LinksFunction } from "@remix-run/node"; -import { - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, -} from "@remix-run/react"; +import type { LinksFunction } from "react-router"; +import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; import "./tailwind.css"; import "./shiki.css"; diff --git a/frontend/app/routes.ts b/frontend/app/routes.ts new file mode 100644 index 0000000..4c05936 --- /dev/null +++ b/frontend/app/routes.ts @@ -0,0 +1,4 @@ +import { type RouteConfig } from "@react-router/dev/routes"; +import { flatRoutes } from "@react-router/fs-routes"; + +export default flatRoutes() satisfies RouteConfig; diff --git a/frontend/app/routes/_index.tsx b/frontend/app/routes/_index.tsx index 06cca78..651a61c 100644 --- a/frontend/app/routes/_index.tsx +++ b/frontend/app/routes/_index.tsx @@ -1,4 +1,4 @@ -import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; +import type { LoaderFunctionArgs, MetaFunction } from "react-router"; import { ensureUserNotLoggedIn } from "../.server/auth"; import BorderedContainer from "../components/BorderedContainer"; import NavigateLink from "../components/NavigateLink"; diff --git a/frontend/app/routes/dashboard.tsx b/frontend/app/routes/dashboard.tsx index ab170b5..78ed531 100644 --- a/frontend/app/routes/dashboard.tsx +++ b/frontend/app/routes/dashboard.tsx @@ -1,5 +1,5 @@ -import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { Form, useLoaderData } from "@remix-run/react"; +import type { LoaderFunctionArgs, MetaFunction } from "react-router"; +import { Form, useLoaderData } from "react-router"; import { ensureUserLoggedIn } from "../.server/auth"; import { apiGetGames } from "../api/client"; import BorderedContainer from "../components/BorderedContainer"; diff --git a/frontend/app/routes/golf.$gameId.play.tsx b/frontend/app/routes/golf.$gameId.play.tsx index e523187..dc8eb38 100644 --- a/frontend/app/routes/golf.$gameId.play.tsx +++ b/frontend/app/routes/golf.$gameId.play.tsx @@ -1,6 +1,6 @@ -import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { useLoaderData } from "@remix-run/react"; import { useHydrateAtoms } from "jotai/utils"; +import type { LoaderFunctionArgs, MetaFunction } from "react-router"; +import { useLoaderData } from "react-router"; import { ensureUserLoggedIn } from "../.server/auth"; import { ApiAuthTokenContext, diff --git a/frontend/app/routes/golf.$gameId.watch.tsx b/frontend/app/routes/golf.$gameId.watch.tsx index fed06aa..674abc4 100644 --- a/frontend/app/routes/golf.$gameId.watch.tsx +++ b/frontend/app/routes/golf.$gameId.watch.tsx @@ -1,6 +1,6 @@ -import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { useLoaderData } from "@remix-run/react"; import { useHydrateAtoms } from "jotai/utils"; +import type { LoaderFunctionArgs, MetaFunction } from "react-router"; +import { useLoaderData } from "react-router"; import { ensureUserLoggedIn } from "../.server/auth"; import { ApiAuthTokenContext, diff --git a/frontend/app/routes/login.tsx b/frontend/app/routes/login.tsx index 5ca6217..dd5b9e7 100644 --- a/frontend/app/routes/login.tsx +++ b/frontend/app/routes/login.tsx @@ -2,8 +2,8 @@ import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction, -} from "@remix-run/node"; -import { Form, json, useActionData } from "@remix-run/react"; +} from "react-router"; +import { Form, data, useActionData } from "react-router"; import { ensureUserNotLoggedIn, login } from "../.server/auth"; import BorderedContainer from "../components/BorderedContainer"; import InputText from "../components/InputText"; @@ -22,7 +22,7 @@ export async function action({ request }: ActionFunctionArgs) { const username = String(formData.get("username")); const password = String(formData.get("password")); if (username === "" || password === "") { - return json( + return data( { message: "ユーザー名またはパスワードが誤っています", errors: { @@ -40,7 +40,7 @@ export async function action({ request }: ActionFunctionArgs) { await login(request); } catch (error) { if (error instanceof Error) { - return json( + return data( { message: error.message, errors: { diff --git a/frontend/app/routes/logout.tsx b/frontend/app/routes/logout.tsx index d697be2..9616b4d 100644 --- a/frontend/app/routes/logout.tsx +++ b/frontend/app/routes/logout.tsx @@ -1,4 +1,4 @@ -import type { ActionFunctionArgs } from "@remix-run/node"; +import type { ActionFunctionArgs } from "react-router"; import { logout } from "../.server/auth"; export async function action({ request }: ActionFunctionArgs) { -- cgit v1.2.3-70-g09d2