Skip to content

Commit fa563be

Browse files
committed
Add playground
1 parent 5132677 commit fa563be

File tree

14 files changed

+465
-0
lines changed

14 files changed

+465
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
3+
/build
4+
.env
5+
6+
.react-router/
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { startTransition, StrictMode } from "react";
2+
import { hydrateRoot } from "react-dom/client";
3+
import type {
4+
DataRouteObject,
5+
DataRouter,
6+
RouterNavigateOptions,
7+
} from "react-router";
8+
import { HydratedRouter } from "react-router/dom";
9+
import { getPattern, measure, startMeasure } from "./o11y";
10+
11+
startTransition(() => {
12+
hydrateRoot(
13+
document,
14+
<StrictMode>
15+
<HydratedRouter
16+
unstable_instrumentRoute={instrumentRoute}
17+
unstable_instrumentRouter={instrumentRouter}
18+
/>
19+
</StrictMode>,
20+
);
21+
});
22+
23+
function instrumentRouter(router: DataRouter) {
24+
let initialize = router.initialize;
25+
router.initialize = () => {
26+
let pattern = getPattern(router.routes, router.state.location.pathname);
27+
let end = startMeasure(["initialize", pattern]);
28+
29+
if (router.state.initialized) {
30+
end();
31+
} else {
32+
let unsubscribe = router.subscribe((state) => {
33+
if (state.initialized) {
34+
end();
35+
unsubscribe();
36+
}
37+
});
38+
}
39+
return initialize();
40+
};
41+
42+
let navigate = router.navigate;
43+
router.navigate = async (to, opts?: RouterNavigateOptions) => {
44+
let path =
45+
typeof to === "string"
46+
? to
47+
: typeof to === "number"
48+
? String(to)
49+
: (to?.pathname ?? "unknown");
50+
await measure([`navigate`, getPattern(router.routes, path)], () =>
51+
typeof to === "number" ? navigate(to) : navigate(to, opts),
52+
);
53+
};
54+
55+
return router;
56+
}
57+
58+
function instrumentRoute(route: DataRouteObject): DataRouteObject {
59+
if (typeof route.lazy === "function") {
60+
let lazy = route.lazy;
61+
route.lazy = () => measure(["lazy", route.id], () => lazy());
62+
}
63+
64+
if (
65+
route.middleware &&
66+
route.middleware.length > 0 &&
67+
// @ts-expect-error
68+
route.middleware.instrumented !== true
69+
) {
70+
route.middleware = route.middleware.map((mw, i) => {
71+
return ({ request, params, pattern, context }, next) =>
72+
measure(["middleware", route.id, i.toString(), pattern], async () =>
73+
mw({ request, params, pattern, context }, next),
74+
);
75+
});
76+
// When `route.lazy` is used alongside a statically defined `loader`, make
77+
// sure we don't double-instrument the `loader` after `route.lazy` completes
78+
// and we re-call `instrumentRoute` via `mapRouteProperties`
79+
// @ts-expect-error
80+
route.middleware.instrumented = true;
81+
}
82+
83+
// @ts-expect-error
84+
if (typeof route.loader === "function" && !route.loader.instrumented) {
85+
let loader = route.loader;
86+
route.loader = (...args) => {
87+
return measure([`loader:${route.id}`, args[0].pattern], async () =>
88+
loader(...args),
89+
);
90+
};
91+
// When `route.lazy` is used alongside a statically defined `loader`, make
92+
// sure we don't double-instrument the `loader` after `route.lazy` completes
93+
// and we re-call `instrumentRoute` via `mapRouteProperties`
94+
// @ts-expect-error
95+
route.loader.instrumented = true;
96+
}
97+
98+
return route;
99+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { matchRoutes, type DataRouteObject } from "react-router";
2+
3+
export function getPattern(routes: DataRouteObject[], path: string) {
4+
let matches = matchRoutes(routes, path);
5+
if (matches && matches.length > 0) {
6+
return matches
7+
?.map((m) => m.route.path)
8+
.filter(Boolean)
9+
.join("/")
10+
.replace(/\/\/+/g, "/");
11+
}
12+
return "unknown-pattern";
13+
}
14+
15+
export function startMeasure(label: string[]) {
16+
let strLabel = label.join("--");
17+
let now = Date.now().toString();
18+
let start = `start:${strLabel}:${now}`;
19+
console.log(new Date().toISOString(), "start", strLabel);
20+
start += `start:${strLabel}:${now}`;
21+
performance.mark(start);
22+
return () => {
23+
let end = `end:${strLabel}:${now}`;
24+
console.log(new Date().toISOString(), "end", strLabel);
25+
performance.mark(end);
26+
performance.measure(strLabel, start, end);
27+
};
28+
}
29+
30+
export async function measure<T>(
31+
label: string[],
32+
cb: () => Promise<T>,
33+
): Promise<T> {
34+
let end = startMeasure(label);
35+
try {
36+
return await cb();
37+
} finally {
38+
end();
39+
}
40+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
Link,
3+
Links,
4+
Meta,
5+
Outlet,
6+
Scripts,
7+
ScrollRestoration,
8+
type MiddlewareFunction,
9+
} from "react-router";
10+
11+
let sleep = (ms: number = Math.max(100, Math.round(Math.random() * 500))) =>
12+
new Promise((r) => setTimeout(r, ms));
13+
14+
export const middleware = [
15+
async (_: unknown, next: Parameters<MiddlewareFunction<Response>>[1]) => {
16+
await sleep();
17+
await next();
18+
await sleep();
19+
},
20+
];
21+
22+
export async function loader() {
23+
await sleep();
24+
}
25+
26+
export function Layout({ children }: { children: React.ReactNode }) {
27+
return (
28+
<html lang="en">
29+
<head>
30+
<meta charSet="utf-8" />
31+
<meta name="viewport" content="width=device-width, initial-scale=1" />
32+
<Meta />
33+
<Links />
34+
</head>
35+
<body>
36+
<nav>
37+
<Link to="/">Home</Link>
38+
<br />
39+
<Link to="/foo">Foo</Link>
40+
<br />
41+
<Link to="/bar">Bar</Link>
42+
<br />
43+
<Link to="/baz">Baz</Link>
44+
<br />
45+
</nav>
46+
{children}
47+
<ScrollRestoration />
48+
<Scripts />
49+
</body>
50+
</html>
51+
);
52+
}
53+
54+
export default function App() {
55+
return <Outlet />;
56+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { type RouteConfig, index, route } from "@react-router/dev/routes";
2+
3+
export default [
4+
index("routes/index.tsx"),
5+
route(":slug", "routes/slug.tsx"),
6+
] satisfies RouteConfig;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { MiddlewareFunction } from "react-router";
2+
3+
let sleep = (ms: number = Math.max(100, Math.round(Math.random() * 500))) =>
4+
new Promise((r) => setTimeout(r, ms));
5+
6+
export const middleware = [
7+
async (_: unknown, next: Parameters<MiddlewareFunction<Response>>[1]) => {
8+
await sleep();
9+
await next();
10+
await sleep();
11+
},
12+
];
13+
14+
export async function loader() {
15+
await sleep();
16+
}
17+
18+
export default function Index() {
19+
return (
20+
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
21+
<h1>Welcome to React Router</h1>
22+
</div>
23+
);
24+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { startMeasure } from "~/o11y";
2+
import { type Route } from "../../.react-router/types/app/routes/+types/slug";
3+
4+
let sleep = (ms: number = Math.max(100, Math.round(Math.random() * 500))) =>
5+
new Promise((r) => setTimeout(r, ms));
6+
7+
export const middleware: Route.MiddlewareFunction[] = [
8+
async (_, next) => {
9+
await sleep();
10+
await next();
11+
await sleep();
12+
},
13+
];
14+
15+
export const clientMiddleware: Route.ClientMiddlewareFunction[] = [
16+
async (_, next) => {
17+
await sleep();
18+
await next();
19+
await sleep();
20+
},
21+
];
22+
23+
export async function loader({ params }: Route.LoaderArgs) {
24+
await sleep();
25+
return params.slug;
26+
}
27+
28+
export async function clientLoader({
29+
serverLoader,
30+
pattern,
31+
}: Route.ClientLoaderArgs) {
32+
await sleep();
33+
let end = startMeasure(["serverLoader", pattern]);
34+
let value = await serverLoader();
35+
end();
36+
await sleep();
37+
return value;
38+
}
39+
40+
export default function Slug({ loaderData }: Route.ComponentProps) {
41+
return (
42+
<div>
43+
<h1>Slug: {loaderData}</h1>
44+
</div>
45+
);
46+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "@playground/framework-express",
3+
"version": "0.0.0",
4+
"private": true,
5+
"sideEffects": false,
6+
"type": "module",
7+
"scripts": {
8+
"build": "react-router build",
9+
"dev": "node ./server.js",
10+
"start": "cross-env NODE_ENV=production node ./server.js",
11+
"typecheck": "react-router typegen && tsc"
12+
},
13+
"dependencies": {
14+
"@react-router/express": "workspace:*",
15+
"@react-router/node": "workspace:*",
16+
"compression": "^1.7.4",
17+
"express": "^4.19.2",
18+
"isbot": "^5.1.11",
19+
"morgan": "^1.10.0",
20+
"react": "^19.1.0",
21+
"react-dom": "^19.1.0",
22+
"react-router": "workspace:*"
23+
},
24+
"devDependencies": {
25+
"@react-router/dev": "workspace:*",
26+
"@types/compression": "^1.7.5",
27+
"@types/express": "^4.17.20",
28+
"@types/morgan": "^1.9.9",
29+
"@types/react": "^18.2.20",
30+
"@types/react-dom": "^18.2.7",
31+
"cross-env": "^7.0.3",
32+
"typescript": "^5.1.6",
33+
"vite": "^6.1.0",
34+
"vite-tsconfig-paths": "^4.2.1"
35+
},
36+
"engines": {
37+
"node": ">=20.0.0"
38+
}
39+
}
14.7 KB
Binary file not shown.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { Config } from "@react-router/dev/config";
2+
3+
export default {
4+
future: {
5+
v8_middleware: true,
6+
},
7+
} satisfies Config;

0 commit comments

Comments
 (0)