Skip to content

Commit f286ad7

Browse files
Add playgrounds (remix-run#11464)
1 parent 0af95d4 commit f286ad7

34 files changed

+951
-40
lines changed

.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ node_modules/
33
pnpm-lock.yaml
44
/docs/api
55
examples/**/dist/
6+
/playground/
7+
/playground-local/
68
packages/**/dist/
79
packages/react-router-dom/server.d.ts
810
packages/react-router-dom/server.js

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ node_modules/
99
/examples/*/pnpm-lock.yaml
1010
/examples/*/dist
1111
/tutorial/dist
12+
/playground-local/
1213
/integration/playwright-report
1314

1415
# v5 build files

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"format": "prettier --ignore-path .eslintignore --write .",
99
"format:check": "prettier --ignore-path .eslintignore --check .",
1010
"lint": "eslint --cache .",
11+
"playground": "node ./scripts/playground.js",
1112
"prerelease": "pnpm build",
1213
"release": "changeset publish",
1314
"size": "filesize",
@@ -99,6 +100,7 @@
99100
"jest-environment-jsdom": "^29.6.2",
100101
"jsonfile": "^6.1.0",
101102
"prettier": "^2.8.8",
103+
"prompts": "^2.4.2",
102104
"react": "^18.2.0",
103105
"react-dom": "^18.2.0",
104106
"react-test-renderer": "^18.2.0",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
3+
/build
4+
.env
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { RouterProvider } from "react-router-dom";
2+
import { startTransition, StrictMode } from "react";
3+
import { hydrateRoot } from "react-dom/client";
4+
5+
startTransition(() => {
6+
hydrateRoot(
7+
document,
8+
<StrictMode>
9+
<RouterProvider />
10+
</StrictMode>
11+
);
12+
});
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { PassThrough } from "node:stream";
2+
3+
import type { AppLoadContext, EntryContext } from "@react-router/node";
4+
import { createReadableStreamFromReadable } from "@react-router/node";
5+
import { RemixServer } from "react-router-dom";
6+
import { isbot } from "isbot";
7+
import { renderToPipeableStream } from "react-dom/server";
8+
9+
const ABORT_DELAY = 5_000;
10+
11+
export default function handleRequest(
12+
request: Request,
13+
responseStatusCode: number,
14+
responseHeaders: Headers,
15+
reactRouterContext: EntryContext,
16+
loadContext: AppLoadContext
17+
) {
18+
return isbot(request.headers.get("user-agent") || "")
19+
? handleBotRequest(
20+
request,
21+
responseStatusCode,
22+
responseHeaders,
23+
reactRouterContext
24+
)
25+
: handleBrowserRequest(
26+
request,
27+
responseStatusCode,
28+
responseHeaders,
29+
reactRouterContext
30+
);
31+
}
32+
33+
function handleBotRequest(
34+
request: Request,
35+
responseStatusCode: number,
36+
responseHeaders: Headers,
37+
reactRouterContext: EntryContext
38+
) {
39+
return new Promise((resolve, reject) => {
40+
let shellRendered = false;
41+
const { pipe, abort } = renderToPipeableStream(
42+
<RemixServer
43+
context={reactRouterContext}
44+
url={request.url}
45+
abortDelay={ABORT_DELAY}
46+
/>,
47+
{
48+
onAllReady() {
49+
shellRendered = true;
50+
const body = new PassThrough();
51+
const stream = createReadableStreamFromReadable(body);
52+
53+
responseHeaders.set("Content-Type", "text/html");
54+
55+
resolve(
56+
new Response(stream, {
57+
headers: responseHeaders,
58+
status: responseStatusCode,
59+
})
60+
);
61+
62+
pipe(body);
63+
},
64+
onShellError(error: unknown) {
65+
reject(error);
66+
},
67+
onError(error: unknown) {
68+
responseStatusCode = 500;
69+
// Log streaming rendering errors from inside the shell. Don't log
70+
// errors encountered during initial shell rendering since they'll
71+
// reject and get logged in handleDocumentRequest.
72+
if (shellRendered) {
73+
console.error(error);
74+
}
75+
},
76+
}
77+
);
78+
79+
setTimeout(abort, ABORT_DELAY);
80+
});
81+
}
82+
83+
function handleBrowserRequest(
84+
request: Request,
85+
responseStatusCode: number,
86+
responseHeaders: Headers,
87+
reactRouterContext: EntryContext
88+
) {
89+
return new Promise((resolve, reject) => {
90+
let shellRendered = false;
91+
const { pipe, abort } = renderToPipeableStream(
92+
<RemixServer
93+
context={reactRouterContext}
94+
url={request.url}
95+
abortDelay={ABORT_DELAY}
96+
/>,
97+
{
98+
onShellReady() {
99+
shellRendered = true;
100+
const body = new PassThrough();
101+
const stream = createReadableStreamFromReadable(body);
102+
103+
responseHeaders.set("Content-Type", "text/html");
104+
105+
resolve(
106+
new Response(stream, {
107+
headers: responseHeaders,
108+
status: responseStatusCode,
109+
})
110+
);
111+
112+
pipe(body);
113+
},
114+
onShellError(error: unknown) {
115+
reject(error);
116+
},
117+
onError(error: unknown) {
118+
responseStatusCode = 500;
119+
// Log streaming rendering errors from inside the shell. Don't log
120+
// errors encountered during initial shell rendering since they'll
121+
// reject and get logged in handleDocumentRequest.
122+
if (shellRendered) {
123+
console.error(error);
124+
}
125+
},
126+
}
127+
);
128+
129+
setTimeout(abort, ABORT_DELAY);
130+
});
131+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
Links,
3+
Meta,
4+
Outlet,
5+
Scripts,
6+
ScrollRestoration,
7+
} from "react-router-dom";
8+
9+
export function Layout({ children }: { children: React.ReactNode }) {
10+
return (
11+
<html lang="en">
12+
<head>
13+
<meta charSet="utf-8" />
14+
<meta name="viewport" content="width=device-width, initial-scale=1" />
15+
<Meta />
16+
<Links />
17+
</head>
18+
<body>
19+
{children}
20+
<ScrollRestoration />
21+
<Scripts />
22+
</body>
23+
</html>
24+
);
25+
}
26+
27+
export default function App() {
28+
return <Outlet />;
29+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { MetaFunction } from "@react-router/node";
2+
3+
export const meta: MetaFunction = () => {
4+
return [
5+
{ title: "New React Router App" },
6+
{ name: "description", content: "Welcome to React Router!" },
7+
];
8+
};
9+
10+
export default function Index() {
11+
return (
12+
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
13+
<h1>Welcome to React Router</h1>
14+
</div>
15+
);
16+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"private": true,
3+
"sideEffects": false,
4+
"type": "module",
5+
"scripts": {
6+
"build": "react-router build",
7+
"dev": "node ./server.js",
8+
"start": "cross-env NODE_ENV=production node ./server.js",
9+
"typecheck": "tsc"
10+
},
11+
"dependencies": {
12+
"@react-router/express": "workspace:*",
13+
"@react-router/node": "workspace:*",
14+
"compression": "^1.7.4",
15+
"express": "^4.18.2",
16+
"isbot": "^4.1.0",
17+
"morgan": "^1.10.0",
18+
"react": "^18.2.0",
19+
"react-dom": "^18.2.0",
20+
"react-router": "workspace:*",
21+
"react-router-dom": "workspace:*"
22+
},
23+
"devDependencies": {
24+
"@react-router/dev": "workspace:*",
25+
"@types/compression": "^1.7.5",
26+
"@types/express": "^4.17.20",
27+
"@types/morgan": "^1.9.9",
28+
"@types/react": "^18.2.20",
29+
"@types/react-dom": "^18.2.7",
30+
"cross-env": "^7.0.3",
31+
"typescript": "^5.1.6",
32+
"vite": "^5.1.0",
33+
"vite-tsconfig-paths": "^4.2.1"
34+
},
35+
"engines": {
36+
"node": ">=18.0.0"
37+
}
38+
}
14.7 KB
Binary file not shown.

playground/compiler-express/server.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { createRequestHandler } from "@react-router/express";
2+
import { installGlobals } from "@react-router/node";
3+
import compression from "compression";
4+
import express from "express";
5+
import morgan from "morgan";
6+
7+
installGlobals();
8+
9+
const viteDevServer =
10+
process.env.NODE_ENV === "production"
11+
? undefined
12+
: await import("vite").then((vite) =>
13+
vite.createServer({
14+
server: { middlewareMode: true },
15+
})
16+
);
17+
18+
const reactRouterHandler = createRequestHandler({
19+
build: viteDevServer
20+
? () => viteDevServer.ssrLoadModule("virtual:react-router/server-build")
21+
: await import("./build/server/index.js"),
22+
});
23+
24+
const app = express();
25+
26+
app.use(compression());
27+
app.disable("x-powered-by");
28+
29+
if (viteDevServer) {
30+
app.use(viteDevServer.middlewares);
31+
} else {
32+
app.use(
33+
"/assets",
34+
express.static("build/client/assets", { immutable: true, maxAge: "1y" })
35+
);
36+
}
37+
38+
app.use(express.static("build/client", { maxAge: "1h" }));
39+
app.use(morgan("tiny"));
40+
41+
app.all("*", reactRouterHandler);
42+
43+
const port = process.env.PORT || 3000;
44+
app.listen(port, () =>
45+
console.log(`Express server listening at http://localhost:${port}`)
46+
);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"include": [
3+
"**/*.ts",
4+
"**/*.tsx",
5+
"**/.server/**/*.ts",
6+
"**/.server/**/*.tsx",
7+
"**/.client/**/*.ts",
8+
"**/.client/**/*.tsx"
9+
],
10+
"compilerOptions": {
11+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
12+
"types": ["@react-router/node", "vite/client"],
13+
"isolatedModules": true,
14+
"esModuleInterop": true,
15+
"jsx": "react-jsx",
16+
"module": "ESNext",
17+
"moduleResolution": "Bundler",
18+
"resolveJsonModule": true,
19+
"target": "ES2022",
20+
"strict": true,
21+
"allowJs": true,
22+
"skipLibCheck": true,
23+
"forceConsistentCasingInFileNames": true,
24+
"baseUrl": ".",
25+
"paths": {
26+
"~/*": ["./app/*"]
27+
},
28+
"noEmit": true
29+
}
30+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { vitePlugin as reactRouter } from "@react-router/dev";
2+
import { defineConfig } from "vite";
3+
import tsconfigPaths from "vite-tsconfig-paths";
4+
5+
export default defineConfig({
6+
plugins: [reactRouter(), tsconfigPaths()],
7+
});

playground/compiler-spa/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
3+
/build
4+
.env
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { RouterProvider } from "react-router-dom";
2+
import { startTransition, StrictMode } from "react";
3+
import { hydrateRoot } from "react-dom/client";
4+
5+
startTransition(() => {
6+
hydrateRoot(
7+
document,
8+
<StrictMode>
9+
<RouterProvider />
10+
</StrictMode>
11+
);
12+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { EntryContext } from "@react-router/node";
2+
import { RemixServer } from "react-router-dom";
3+
import { renderToString } from "react-dom/server";
4+
5+
export default function handleRequest(
6+
request: Request,
7+
responseStatusCode: number,
8+
responseHeaders: Headers,
9+
reactRouterContext: EntryContext
10+
) {
11+
let html = renderToString(
12+
<RemixServer context={reactRouterContext} url={request.url} />
13+
);
14+
html = "<!DOCTYPE html>\n" + html;
15+
return new Response(html, {
16+
headers: { "Content-Type": "text/html" },
17+
status: responseStatusCode,
18+
});
19+
}

0 commit comments

Comments
 (0)