Skip to content

Commit c21bc14

Browse files
committed
minimal app setup
1 parent 55cccfd commit c21bc14

File tree

16 files changed

+345
-0
lines changed

16 files changed

+345
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
# misc
15+
.DS_Store
16+
.env.local
17+
.env.development.local
18+
.env.test.local
19+
.env.production.local
20+
21+
npm-debug.log*
22+
yarn-debug.log*
23+
yarn-error.log*
24+
25+
/test-results/
26+
/playwright-report/
27+
/playwright/.cache/
28+
29+
!*.d.ts
30+
31+
# react router
32+
.react-router
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
4+
5+
html,
6+
body {
7+
@apply bg-white dark:bg-gray-950;
8+
9+
@media (prefers-color-scheme: dark) {
10+
color-scheme: dark;
11+
}
12+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as Sentry from '@sentry/react-router';
2+
import { startTransition, StrictMode } from 'react';
3+
import { hydrateRoot } from 'react-dom/client';
4+
import { HydratedRouter } from 'react-router/dom';
5+
6+
Sentry.init({
7+
dsn: process.env.E2E_TEST_DSN,
8+
integrations: [Sentry.browserTracingIntegration()],
9+
tracesSampleRate: 1.0,
10+
tracePropagationTargets: [/^\//],
11+
});
12+
13+
startTransition(() => {
14+
hydrateRoot(
15+
document,
16+
<StrictMode>
17+
<HydratedRouter />
18+
</StrictMode>,
19+
);
20+
});
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { PassThrough } from 'node:stream';
2+
3+
import type { AppLoadContext, EntryContext } from 'react-router';
4+
import { createReadableStreamFromReadable } from '@react-router/node';
5+
import { ServerRouter } from 'react-router';
6+
import { isbot } from 'isbot';
7+
import type { RenderToPipeableStreamOptions } from 'react-dom/server';
8+
import { renderToPipeableStream } from 'react-dom/server';
9+
import * as Sentry from '@sentry/react-router';
10+
const ABORT_DELAY = 5_000;
11+
12+
export default function handleRequest(
13+
request: Request,
14+
responseStatusCode: number,
15+
responseHeaders: Headers,
16+
routerContext: EntryContext,
17+
loadContext: AppLoadContext,
18+
) {
19+
return new Promise((resolve, reject) => {
20+
let shellRendered = false;
21+
let userAgent = request.headers.get('user-agent');
22+
23+
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
24+
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
25+
let readyOption: keyof RenderToPipeableStreamOptions =
26+
(userAgent && isbot(userAgent)) || routerContext.isSpaMode ? 'onAllReady' : 'onShellReady';
27+
28+
const { pipe, abort } = renderToPipeableStream(<ServerRouter context={routerContext} url={request.url} />, {
29+
[readyOption]() {
30+
shellRendered = true;
31+
const body = new PassThrough();
32+
const stream = createReadableStreamFromReadable(body);
33+
34+
responseHeaders.set('Content-Type', 'text/html');
35+
36+
resolve(
37+
new Response(stream, {
38+
headers: responseHeaders,
39+
status: responseStatusCode,
40+
}),
41+
);
42+
43+
pipe(body);
44+
},
45+
onShellError(error: unknown) {
46+
reject(error);
47+
},
48+
onError(error: unknown) {
49+
responseStatusCode = 500;
50+
// Log streaming rendering errors from inside the shell. Don't log
51+
// errors encountered during initial shell rendering since they'll
52+
// reject and get logged in handleDocumentRequest.
53+
if (shellRendered) {
54+
console.error(error);
55+
}
56+
},
57+
});
58+
59+
setTimeout(abort, ABORT_DELAY);
60+
});
61+
}
62+
63+
import { type HandleErrorFunction } from 'react-router';
64+
65+
export const handleError: HandleErrorFunction = (error, { request }) => {
66+
// React Router may abort some interrupted requests, don't log those
67+
if (!request.signal.aborted) {
68+
Sentry.captureException(error);
69+
70+
// make sure to still log the error so you can see it
71+
console.error(error);
72+
}
73+
};
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router';
2+
import * as Sentry from '@sentry/react-router';
3+
import type { Route } from './+types/root';
4+
import stylesheet from './app.css?url';
5+
6+
export const links: Route.LinksFunction = () => [
7+
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
8+
{
9+
rel: 'preconnect',
10+
href: 'https://fonts.gstatic.com',
11+
crossOrigin: 'anonymous',
12+
},
13+
{
14+
rel: 'stylesheet',
15+
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
16+
},
17+
{ rel: 'stylesheet', href: stylesheet },
18+
];
19+
20+
export function Layout({ children }: { children: React.ReactNode }) {
21+
return (
22+
<html lang="en">
23+
<head>
24+
<meta charSet="utf-8" />
25+
<meta name="viewport" content="width=device-width, initial-scale=1" />
26+
<Meta />
27+
<Links />
28+
</head>
29+
<body>
30+
{children}
31+
<ScrollRestoration />
32+
<Scripts />
33+
</body>
34+
</html>
35+
);
36+
}
37+
38+
export default function App() {
39+
return <Outlet />;
40+
}
41+
42+
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
43+
let message = 'Oops!';
44+
let details = 'An unexpected error occurred.';
45+
let stack: string | undefined;
46+
47+
if (isRouteErrorResponse(error)) {
48+
message = error.status === 404 ? '404' : 'Error';
49+
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
50+
} else if (error && error instanceof Error) {
51+
Sentry.captureException(error);
52+
if (import.meta.env.DEV) {
53+
details = error.message;
54+
stack = error.stack;
55+
}
56+
}
57+
58+
return (
59+
<main className="pt-16 p-4 container mx-auto">
60+
<h1>{message}</h1>
61+
<p>{details}</p>
62+
{stack && (
63+
<pre className="w-full p-4 overflow-x-auto">
64+
<code>{stack}</code>
65+
</pre>
66+
)}
67+
</main>
68+
);
69+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { type RouteConfig, index } from '@react-router/dev/routes';
2+
3+
export default [index('routes/home.tsx')] satisfies RouteConfig;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { Route } from './+types/home';
2+
3+
export function meta({}: Route.MetaArgs) {
4+
return [{ title: 'New React Router App' }, { name: 'description', content: 'Welcome to React Router!' }];
5+
}
6+
7+
export default function Home() {
8+
return <div>home</div>;
9+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import * as Sentry from '@sentry/react-router';
2+
3+
Sentry.init({
4+
dsn: process.env.E2E_TEST_DSN,
5+
tracesSampleRate: 1.0,
6+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"name": "react-router-7-framework",
3+
"version": "0.1.0",
4+
"type": "module",
5+
"private": true,
6+
"dependencies": {
7+
"react": "^18.3.1",
8+
"react-dom": "^18.3.1",
9+
"react-router": "^7.1.5",
10+
"@react-router/node": "^7.1.5",
11+
"@react-router/serve": "^7.1.5",
12+
"@sentry/react-router": "latest || *",
13+
"isbot": "^5.1.17"
14+
},
15+
"devDependencies": {
16+
"@types/react": "18.3.1",
17+
"@types/react-dom": "18.3.1",
18+
"@types/node": "^20",
19+
"@react-router/dev": "^7.1.5",
20+
"@playwright/test": "~1.50.0",
21+
"@sentry-internal/test-utils": "link:../../../test-utils",
22+
"autoprefixer": "^10.4.20",
23+
"postcss": "^8.4.49",
24+
"tailwindcss": "^3.4.15",
25+
"typescript": "^5.6.3",
26+
"vite": "^5.4.11"
27+
},
28+
"scripts": {
29+
"build": "react-router build",
30+
"dev": "NODE_OPTIONS='--import ./instrument.mjs' react-router dev",
31+
"start": "NODE_OPTIONS='--import ./instrument.mjs' react-router-serve ./build/server/index.js",
32+
"test": "pnpm typecheck && playwright test",
33+
"typecheck": "react-router typegen && tsc",
34+
"clean": "npx rimraf node_modules .react-router pnpm-lock.yaml",
35+
"test:build": "pnpm install && pnpm build",
36+
"test:assert": "pnpm test"
37+
},
38+
"eslintConfig": {
39+
"extends": [
40+
"react-app",
41+
"react-app/jest"
42+
]
43+
},
44+
"browserslist": {
45+
"production": [
46+
">0.2%",
47+
"not dead",
48+
"not op_mini all"
49+
],
50+
"development": [
51+
"last 1 chrome version",
52+
"last 1 firefox version",
53+
"last 1 safari version"
54+
]
55+
},
56+
"volta": {
57+
"extends": "../../package.json"
58+
}
59+
}

0 commit comments

Comments
 (0)