This commit is contained in:
Tommy D. Rossi
2025-12-30 14:25:19 +01:00
parent b19b7ae72d
commit 3decc28242
16 changed files with 612 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
node_modules
/.cache
/build
.env
.react-router
.DS_Store
+22
View File
@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "website/src/components",
"utils": "website/src/lib/utils",
"ui": "website/src/components/ui",
"lib": "website/src/lib",
"hooks": "website/src/lib/hooks"
},
"iconLibrary": "lucide"
}
+46
View File
@@ -0,0 +1,46 @@
{
"name": "website",
"private": true,
"type": "module",
"scripts": {
"build": "react-router build",
"dev": "react-router dev --port 8044",
"start": "node ./build/server/server.js",
"typecheck": "react-router typegen && tsc"
},
"exports": {
"./*": "./*"
},
"dependencies": {
"@radix-ui/react-slot": "^1.2.4",
"@react-router/fs-routes": "^7.11.0",
"@react-router/node": "^7.11.0",
"@sentry/node": "^10.32.1",
"@tailwindcss/typography": "^0.5.19",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"isbot": "^5.1.32",
"lucide-react": "^0.562.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-router": "^7.11.0",
"sentries": "^0.2.2",
"tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@react-router/dev": "^7.11.0",
"@tailwindcss/vite": "^4.1.18",
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"@xmorse/deployment-utils": "^0.7.2",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.3.0",
"vite-plugin-environment": "^1.1.3",
"vite-tsconfig-paths": "^6.0.3",
"vitest": "^4.0.16"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+6
View File
@@ -0,0 +1,6 @@
import type { Config } from "@react-router/dev/config";
export default {
appDirectory: "src",
prerender: ["/", "/defer-example"],
} satisfies Config;
+7
View File
@@ -0,0 +1,7 @@
import { startTransition } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
startTransition(() => {
hydrateRoot(document, <HydratedRouter />);
});
+163
View File
@@ -0,0 +1,163 @@
import { PassThrough } from "node:stream";
import { createReadableStreamFromReadable } from "@react-router/node";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";
import type {
ActionFunctionArgs,
AppLoadContext,
EntryContext,
LoaderFunctionArgs,
} from "react-router";
import { isRouteErrorResponse, ServerRouter } from "react-router";
import { notifyError } from "./lib/errors";
export const streamTimeout = 60 * 20 * 1_000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
reactRouterContext: 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
) {
return isbot(request.headers.get("user-agent") || "")
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
reactRouterContext
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
reactRouterContext
);
}
function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
reactRouterContext: EntryContext
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
let timeoutId: NodeJS.Timeout;
const { pipe, abort } = renderToPipeableStream(
<ServerRouter
context={reactRouterContext}
url={request.url}
// abortDelay={ABORT_DELAY}
/>,
{
onAllReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
clearTimeout(timeoutId);
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
);
pipe(body);
},
onShellError(error: unknown) {
clearTimeout(timeoutId);
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
}
);
timeoutId = setTimeout(abort, streamTimeout);
});
}
function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
reactRouterContext: EntryContext
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
let timeoutId: NodeJS.Timeout;
const { pipe, abort } = renderToPipeableStream(
<ServerRouter
context={reactRouterContext}
url={request.url}
// abortDelay={ABORT_DELAY}
/>,
{
onShellReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
clearTimeout(timeoutId);
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
);
pipe(body);
},
onShellError(error: unknown) {
clearTimeout(timeoutId);
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
}
);
timeoutId = setTimeout(abort, streamTimeout);
});
}
export function handleError(
error: any,
{ request, params, context }: LoaderFunctionArgs | ActionFunctionArgs
) {
// https://github.com/remix-run/remix/discussions/8933
if (request.signal.aborted || isRouteErrorResponse(error)) {
return;
}
if (error?.["status"] === 404) {
return;
}
if (error instanceof Error) {
notifyError(error, "unhandled remix error");
} else {
console.error("error", error);
}
}
+34
View File
@@ -0,0 +1,34 @@
import { captureException, flush, init } from "sentries";
init({
dsn: "https://3e3f1075fec9ee2de1e0f79026b5f734@o4508014272446464.ingest.de.sentry.io/4508014292697168",
integrations: [],
tracesSampleRate: 0.01,
profilesSampleRate: 0.01,
beforeSend(event) {
if (process.env.NODE_ENV === "development") {
return null;
}
if (process.env.BYTECODE_RUN) {
return null;
}
if (event?.["name"] === "AbortError") {
return null;
}
return event;
},
});
export async function notifyError(error: any, msg?: string) {
console.error(msg, error);
captureException(error, { extra: { msg } });
await flush(1000);
}
export class AppError extends Error {
constructor(message: string) {
super(message);
this.name = "AppError";
}
}
+10
View File
@@ -0,0 +1,10 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export const sleep = (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+63
View File
@@ -0,0 +1,63 @@
import "website/src/styles/globals.css";
import { Route } from "./+types/root";
import {
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "react-router";
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html className="" lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (isRouteErrorResponse(error)) {
return (
<div className="flex flex-col items-center justify-center min-h-screen p-8 text-center">
<h1 className="text-4xl font-bold mb-4">
{error.status} {error.statusText}
</h1>
<p className="text-lg text-gray-600">{error.data}</p>
</div>
);
} else if (error instanceof Error) {
return (
<div className="flex flex-col items-center justify-center min-h-screen p-8 text-center max-w-4xl mx-auto">
<h1 className="text-4xl font-bold mb-4">Error</h1>
<p className="text-lg text-gray-600 mb-6">{error.message}</p>
<p className="text-sm text-gray-500 mb-2">The stack trace is:</p>
<pre className="bg-gray-100 p-4 rounded-lg text-xs text-left overflow-auto max-w-full border">
{error.stack}
</pre>
</div>
);
} else {
return (
<div className="flex items-center justify-center min-h-screen">
<h1 className="text-4xl font-bold text-center">Unknown Error</h1>
</div>
);
}
}
export default function App() {
return <Outlet />;
}
+4
View File
@@ -0,0 +1,4 @@
import type { RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export default flatRoutes() satisfies RouteConfig;
+9
View File
@@ -0,0 +1,9 @@
import { redirect } from 'react-router';
export const loader = () => {
return redirect('https://github.com/remorses/playwriter');
};
export default function Index() {
return null;
}
+24
View File
@@ -0,0 +1,24 @@
import React from "react";
import { sleep } from "../lib/utils";
import { Route } from "./+types/defer-example";
async function getProjectLocation() {
return Promise.resolve().then(() => sleep(1000).then(() => "hi"));
}
export async function loader({}: Route.LoaderArgs) {
return {
project: getProjectLocation(),
};
}
export default function ProjectRoute({ loaderData }: Route.ComponentProps) {
const location = React.use(loaderData.project);
return (
<main>
<h1>Let's locate your project</h1>
<p>Your project is at {location}.</p>
</main>
);
}
+143
View File
@@ -0,0 +1,143 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
@plugin "@tailwindcss/typography";
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.637 0.237 25.331);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.871 0.006 286.286);
--ring: oklch(0.871 0.006 286.286);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.871 0.006 286.286);
--sidebar-primary: oklch(0.37 0.013 285.805);
--sidebar-primary-foreground: oklch(0.871 0.006 286.286);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.871 0.006 286.286);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.871 0.006 286.286);
}
.dark {
--background: oklch(0.21 0.006 285.885);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.637 0.237 25.331);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.274 0.006 286.033);
--input: oklch(0.274 0.006 286.033);
--ring: oklch(0.442 0.017 285.786);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: #000;
--sidebar-foreground: oklch(0.871 0.006 286.286);
--sidebar-primary: oklch(0.37 0.013 285.805);
--sidebar-primary-foreground: oklch(0.871 0.006 286.286);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.871 0.006 286.286);
--sidebar-border: oklch(0.274 0.006 286.033);
--sidebar-ring: oklch(0.442 0.017 285.786);
}
@theme inline {
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
/* Custom scrollbar styles: shadcn-inspired */
::-webkit-scrollbar {
width: 6px;
height: 6px;
background: transparent;
border-radius: var(--radius-md);
}
::-webkit-scrollbar-thumb {
background: var(--color-muted);
border-radius: var(--radius-md);
border: none;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-muted-foreground);
}
+28
View File
@@ -0,0 +1,28 @@
{
"include": ["src"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["@react-router/node", "vite/client", "@types/node"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"target": "ES2022",
"strict": true,
"allowJs": true,
"skipLibCheck": true,
"allowArbitraryExtensions": true,
"allowImportingTsExtensions": true,
"noImplicitAny": false,
"useUnknownInCatchVariables": false,
"forceConsistentCasingInFileNames": true,
"rootDirs": [".", "./.react-router/types"],
"baseUrl": ".",
"paths": {
"website/*": ["./*"]
},
"noEmit": true
}
}
+47
View File
@@ -0,0 +1,47 @@
/// <reference types="vitest/config" />
import { reactRouter } from "@react-router/dev/vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import EnvironmentPlugin from "vite-plugin-environment";
import { defineConfig } from "vite";
import {
viteExternalsPlugin,
enablePreserveModulesPlugin,
} from "@xmorse/deployment-utils/dist/vite-externals-plugin.js";
import { reactRouterServerPlugin } from "@xmorse/deployment-utils/dist/react-router.js";
import tsconfigPaths from "vite-tsconfig-paths";
const NODE_ENV = JSON.stringify(process.env.NODE_ENV || "production");
export default defineConfig({
clearScreen: false,
define: {
"process.env.NODE_ENV": NODE_ENV,
},
test: {
pool: "threads",
exclude: ["**/dist/**", "**/esm/**", "**/node_modules/**", "**/e2e/**"],
poolOptions: {
threads: {
isolate: false,
},
},
},
plugins: [
EnvironmentPlugin("all", { prefix: "PUBLIC" }),
EnvironmentPlugin("all", { prefix: "NEXT_PUBLIC" }),
process.env.VITEST ? react() : reactRouter(),
tsconfigPaths(),
// viteExternalsPlugin({
// externals: ["@sentry/node"],
// }),
// enablePreserveModulesPlugin(),
reactRouterServerPlugin({ port: process.env.PORT || '8044' }),
tailwindcss(),
],
build: {
commonjsOptions: {
transformMixedEsModules: true,
},
},
});