diff --git a/.github/workflows/quality-gateway-pull-request.yml b/.github/workflows/quality-gateway-pull-request.yml index b9759c8e..d153c2cf 100644 --- a/.github/workflows/quality-gateway-pull-request.yml +++ b/.github/workflows/quality-gateway-pull-request.yml @@ -17,7 +17,7 @@ jobs: bun-version: latest - run: bun install --frozen-lockfile - run: bun lint - - run: bun build:static + - run: bunx react-router build biome: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index d81b981b..5476a5ef 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ node_modules # playwright test-results playwright-report + +# React Router +/.react-router/ +/build/ diff --git a/README.md b/README.md index e3cce4ca..7b9ac013 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,16 @@ This WebApp is the main project of PodCodar, a learning community about programm - 📖 [Remix docs](https://remix.run/docs) +## Features + +- 🚀 Server-side rendering +- ⚡️ Hot Module Replacement (HMR) +- 📦 Asset bundling and optimization +- 🔄 Data loading and mutations +- 🔒 TypeScript by default +- 🎉 TailwindCSS for styling +- 📖 [React Router docs](https://reactrouter.com/) + ## Development ### System Dependencies diff --git a/app/cookies.server.ts b/app/cookies.server.ts index 6d92ac22..d23cd861 100644 --- a/app/cookies.server.ts +++ b/app/cookies.server.ts @@ -1,5 +1,5 @@ import { MAX_COOKIE_AGE } from "@packages/contants"; -import { createCookie } from "@remix-run/cloudflare"; // or cloudflare/deno +import { createCookie } from "react-router"; export const selectedTheme = createCookie("selected-theme", { path: "/", diff --git a/app/entry.client.tsx b/app/entry.client.tsx deleted file mode 100644 index c56bc718..00000000 --- a/app/entry.client.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * 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/file-conventions/entry.client - */ - -import { RemixBrowser } from "@remix-run/react"; -import { StrictMode, startTransition } from "react"; -import { hydrateRoot } from "react-dom/client"; - -startTransition(() => { - hydrateRoot( - document, - - - , - ); -}); diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 36edf1f4..1701765b 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -1,35 +1,27 @@ -/** - * 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/file-conventions/entry.server - */ - -import type { AppLoadContext, EntryContext } from "@remix-run/cloudflare"; -import { RemixServer } from "@remix-run/react"; import { isbot } from "isbot"; import { renderToReadableStream } from "react-dom/server"; +import { + type AppLoadContext, + type EntryContext, + ServerRouter, +} from "react-router"; const ABORT_DELAY = 5000; export default async function handleRequest( request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext, + status: number, + headers: Headers, + routerContext: EntryContext, // This is ignored so we can keep it in the template for visibility. Feel // free to delete this parameter in your app if you're not using it! - // eslint-disable-next-line @typescript-eslint/no-unused-vars - loadContext: AppLoadContext, + _loadContext: AppLoadContext, ) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), ABORT_DELAY); const body = await renderToReadableStream( - , + , { signal: controller.signal, onError(error: unknown) { @@ -37,7 +29,7 @@ export default async function handleRequest( // Log streaming rendering errors from inside the shell console.error(error); } - responseStatusCode = 500; + status = 500; }, }, ); @@ -48,9 +40,6 @@ export default async function handleRequest( await body.allReady; } - responseHeaders.set("Content-Type", "text/html"); - return new Response(body, { - headers: responseHeaders, - status: responseStatusCode, - }); + headers.set("Content-Type", "text/html"); + return new Response(body, { headers, status }); } diff --git a/app/root.tsx b/app/root.tsx index eba83df3..c51edcaf 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,11 +1,9 @@ import type { Theme } from "@packages/utils/theme"; -import type { - ActionFunctionArgs, - LinksFunction, - LoaderFunctionArgs, -} from "@remix-run/cloudflare"; import { + type ActionFunctionArgs, Links, + type LinksFunction, + type LoaderFunctionArgs, Meta, Outlet, Scripts, @@ -14,7 +12,7 @@ import { redirect, useLoaderData, useRouteError, -} from "@remix-run/react"; +} from "react-router"; import "./tailwind.css"; import Metadata from "@packages/components/Metadata"; diff --git a/app/routes.ts b/app/routes.ts new file mode 100644 index 00000000..a4c86c3b --- /dev/null +++ b/app/routes.ts @@ -0,0 +1,6 @@ +import { type RouteConfig, route } from "@react-router/dev/routes"; +import { flatRoutes } from "@react-router/fs-routes"; + +const routes = await flatRoutes(); + +export default [...routes, route("/*", "./catchall.tsx")] satisfies RouteConfig; diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 664c5e30..a2b0f1ae 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -5,11 +5,14 @@ import RoadmapSection from "@packages/components/RoadmapSection"; import TechSection from "@packages/components/TechSection"; import TestimonialSection from "@packages/components/TestimonialSection"; import WhyItWorksSection from "@packages/components/WhyItWorksSection"; -import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/cloudflare"; -import { useLoaderData } from "@remix-run/react"; import { description, title } from "@packages/config/site"; import { Database } from "@packages/repositories/db"; +import { + type LoaderFunctionArgs, + type MetaFunction, + useLoaderData, +} from "react-router"; export const meta: MetaFunction = () => { return [{ title }, { name: "description", content: description }]; diff --git a/app/routes/team.tsx b/app/routes/team.tsx index 382d0f25..7d155f60 100644 --- a/app/routes/team.tsx +++ b/app/routes/team.tsx @@ -1,10 +1,8 @@ import TeamPage from "@packages/components/TeamPage"; import { Database } from "@packages/repositories/db"; -import type { LoaderFunctionArgs } from "@remix-run/cloudflare"; -import { useLoaderData } from "@remix-run/react"; +import { type LoaderFunctionArgs, useLoaderData } from "react-router"; export async function loader({ context }: LoaderFunctionArgs) { - console.log(context); const db = new Database( context.cloudflare.env.TURSO_CONNECTION_URL, context.cloudflare.env.TURSO_AUTH_TOKEN, diff --git a/bun.lockb b/bun.lockb index 3d5f4ddf..f1d480a3 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/functions/[[path]].ts b/functions/[[path]].ts index 41877c61..324175ce 100644 --- a/functions/[[path]].ts +++ b/functions/[[path]].ts @@ -1,10 +1,9 @@ -import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages"; +import { createRequestHandler } from "@react-router/cloudflare"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore - the server build file is generated by `remix vite:build` -// eslint-disable-next-line import/no-unresolved +// @ts-ignore - the server build file is generated by `react-router build` import * as build from "../build/server"; -export const onRequest = createPagesFunctionHandler({ +export const onRequest = createRequestHandler({ build, }); diff --git a/load-context.ts b/load-context.ts index 6a0eb6bf..96c09202 100644 --- a/load-context.ts +++ b/load-context.ts @@ -1,9 +1,9 @@ -import type { AppLoadContext } from "@remix-run/cloudflare"; +import type { AppLoadContext } from "react-router"; import type { PlatformProxy } from "wrangler"; type Cloudflare = Omit, "dispose">; -declare module "@remix-run/cloudflare" { +declare module "react-router" { interface AppLoadContext { cloudflare: Cloudflare; extra: string; // augmented diff --git a/package.json b/package.json index 9b51534a..d8223ecd 100644 --- a/package.json +++ b/package.json @@ -6,62 +6,64 @@ "sideEffects": false, "type": "module", "scripts": { - "dev": "bun run typegen && remix vite:dev", "docs:dev": "vitepress dev docs", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs", + "dev": "bun run typegen && react-router dev", "prebuild": "bun run typegen && bun run db:migrate", - "build": "remix vite:build", + "build": "react-router build", "deploy": "bun run build && wrangler pages deploy", "lint": "biome lint", "fmt": "biome check --write ", "encrypt": "gpg -c .env", - "decrypt": "gpg -d .env.gpg > .env && cp .env .dev.vars", + "decrypt": "gpg -d .env.gpg > .env && cp .env .dev.vars && wrangler types", "test": "bun test packages app", "e2e": "playwright test", "db:migrate": "drizzle-kit migrate", "db:studio": "drizzle-kit studio", - "typegen": "wrangler types && drizzle-kit generate", "preview": "bun run build && wrangler pages dev", "start": "wrangler pages dev ./build/client", + "typegen": "drizzle-kit generate && react-router typegen", "typecheck": "tsc" }, "dependencies": { "@libsql/client": "^0.14.0", - "@remix-run/cloudflare": "^2.15.2", - "@remix-run/cloudflare-pages": "^2.15.2", - "@remix-run/react": "^2.15.2", - "@remix-run/serve": "^2.15.2", - "daisyui": "^4.12.22", + "@react-router/cloudflare": "^7.1.1", + "@react-router/fs-routes": "^7.1.1", + "@react-router/node": "^7.1.1", + "@react-router/serve": "^7.1.1", + "daisyui": "^4.12.23", "dotenv": "^16.4.7", - "drizzle-orm": "^0.38.2", - "i18next": "^24.2.0", - "isbot": "^4.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-i18next": "^15.2.0" + "drizzle-orm": "^0.38.3", + "i18next": "^24.2.1", + "isbot": "^5.1.21", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-i18next": "^15.4.0", + "react-router": "^7.1.1" }, "devDependencies": { "@biomejs/biome": "^1.9.4", "@modyfi/vite-plugin-yaml": "^1.1.0", "@playwright/test": "^1.49.1", - "@remix-run/dev": "^2.15.2", - "@types/react": "^18.2.20", - "@types/react-dom": "^18.2.7", - "autoprefixer": "^10.4.19", + "@react-router/dev": "^7.1.1", + "@types/node": "^20.17.13", + "@types/react": "^19.0.7", + "@types/react-dom": "^19.0.3", + "autoprefixer": "^10.4.20", "drizzle-kit": "^0.30.1", - "lefthook": "^1.10.0", - "lint-staged": "^15.2.11", + "lefthook": "^1.10.7", + "lint-staged": "^15.3.0", "mermaid": "^11.4.1", - "postcss": "^8.4.38", - "tailwindcss": "^3.4.4", - "typescript": "^5.1.6", - "vite": "^5.1.0", - "vite-tsconfig-paths": "^4.2.1", + "postcss": "^8.5.1", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3", + "vite": "^5.4.11", + "vite-tsconfig-paths": "^4.3.2", "vitepress": "^1.5.0", "vitepress-carbon": "^1.5.0", "vitepress-plugin-mermaid": "^2.0.17", - "wrangler": "^3.99.0" + "wrangler": "^3.102.0" }, "lint-staged": { "*.{md,mdx}": "bunx prettier --write", diff --git a/packages/components/Link.tsx b/packages/components/Link.tsx index 5449e828..bfb18d7c 100644 --- a/packages/components/Link.tsx +++ b/packages/components/Link.tsx @@ -1,4 +1,4 @@ -import { Link as DefaulLink, type LinkProps } from "@remix-run/react"; +import { Link as DefaulLink, type LinkProps } from "react-router"; type Props = Omit & { children: React.ReactNode; diff --git a/packages/components/NavBar.tsx b/packages/components/NavBar.tsx index ac21c4e0..cdf1689f 100644 --- a/packages/components/NavBar.tsx +++ b/packages/components/NavBar.tsx @@ -1,5 +1,3 @@ -"use client"; - import { useState } from "react"; import { Logo } from "@packages/components/icons"; diff --git a/packages/components/ToggleThemeButton.tsx b/packages/components/ToggleThemeButton.tsx index abedd4b9..7073d056 100644 --- a/packages/components/ToggleThemeButton.tsx +++ b/packages/components/ToggleThemeButton.tsx @@ -1,8 +1,6 @@ -"use client"; - import { type Theme, strToTheme, toggleTheme } from "@packages/utils/theme"; -import { Form } from "@remix-run/react"; import { useState } from "react"; +import { Form } from "react-router"; export default function ToggleThemeButton() { const [colorMode, setColorMode] = useState( diff --git a/react-router.config.ts b/react-router.config.ts new file mode 100644 index 00000000..504439d9 --- /dev/null +++ b/react-router.config.ts @@ -0,0 +1,7 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + // Config options... + // Server-side render by default, to enable SPA mode set this to `false` + ssr: true, +} satisfies Config; diff --git a/tsconfig.json b/tsconfig.json index c615d4ac..c2ef94d4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,38 +1,28 @@ { "include": [ - "**/*.ts", - "**/*.tsx", - "**/.server/**/*.ts", - "**/.server/**/*.tsx", - "**/.client/**/*.ts", - "**/.client/**/*.tsx" + "**/*", + "**/.server/**/*", + "**/.client/**/*", + ".react-router/types/**/*" ], "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2022"], - "types": [ - "vite/client", - "@remix-run/cloudflare", - "@cloudflare/workers-types/2023-07-01" - ], - "isolatedModules": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, + "types": ["node", "vite/client"], "target": "ES2022", - "strict": true, - "allowJs": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], "baseUrl": ".", "paths": { "@packages/*": ["./packages/*"], - "f/*": ["./functions/*"], "~/*": ["./app/*"] }, - - // Vite takes care of building everything, not tsc. - "noEmit": true + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true } } diff --git a/vite.config.ts b/vite.config.ts index 0022448f..7f32a878 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,6 @@ import viteYaml from "@modyfi/vite-plugin-yaml"; -import { - vitePlugin as remix, - cloudflareDevProxyVitePlugin as remixCloudflareDevProxy, -} from "@remix-run/dev"; +import { reactRouter } from "@react-router/dev/vite"; +import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; @@ -10,24 +8,9 @@ import { getLoadContext } from "./load-context"; export default defineConfig({ plugins: [ - remixCloudflareDevProxy({ - getLoadContext, - }), + cloudflareDevProxy({ getLoadContext }), viteYaml(), - remix({ - future: { - v3_fetcherPersist: true, - v3_relativeSplatPath: true, - v3_throwAbortReason: true, - v3_singleFetch: true, - v3_lazyRouteDiscovery: true, - }, - routes(defineRoutes) { - return defineRoutes((route) => { - route("/*", "catchall.tsx"); - }); - }, - }), + reactRouter(), tsconfigPaths(), ], }); diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 2ae67591..6a207551 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -3,4 +3,6 @@ interface Env { TURSO_CONNECTION_URL: string; TURSO_AUTH_TOKEN: string; + FLY_API_TOKEN: string; + CR_PAT: string; }