Skip to content

Commit 2fb1d62

Browse files
jacob-ebeybrookslybrandhi-ogawa
authored
feat: unstable RSC templates (#139)
Co-authored-by: Brooks Lybrand <[email protected]> Co-authored-by: Hiroshi Ogawa <[email protected]>
1 parent ad914e8 commit 2fb1d62

31 files changed

+3694
-92
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33

44
# https://github.com/netlify/cli/issues/6958
55
.netlify
6+
# Parcel cache
7+
.parcel-cache

pnpm-lock.yaml

Lines changed: 2950 additions & 92 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ packages:
99
- "node-custom-server"
1010
- "node-postgres"
1111
- "vercel"
12+
- "unstable_rsc-parcel"
13+
- "unstable_rsc-vite"

unstable_rsc-parcel/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
.parcel-cache
3+
node_modules
4+
dist

unstable_rsc-parcel/package.json

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"private": true,
3+
"targets": {
4+
"react-server": {
5+
"context": "react-server",
6+
"source": "src/entry.rsc.tsx",
7+
"scopeHoist": false,
8+
"includeNodeModules": {
9+
"@mjackson/node-fetch-server": false,
10+
"compression": false,
11+
"express": false
12+
}
13+
}
14+
},
15+
"postcss": {
16+
"plugins": {
17+
"@tailwindcss/postcss": {}
18+
}
19+
},
20+
"scripts": {
21+
"dev": "cross-env NODE_ENV=development parcel --no-autoinstall --no-cache",
22+
"build": "parcel build --no-autoinstall",
23+
"start": "cross-env NODE_ENV=production node dist/server/entry.rsc.js",
24+
"typecheck": "tsc --noEmit"
25+
},
26+
"dependencies": {
27+
"@mjackson/node-fetch-server": "0.7.0",
28+
"@parcel/runtime-rsc": "^2.15.4",
29+
"buffer": "^6.0.3",
30+
"compression": "^1.8.0",
31+
"cross-env": "^7.0.3",
32+
"express": "^5.1.0",
33+
"react": "19.1.0",
34+
"react-dom": "19.1.0",
35+
"react-router": "7.7.0",
36+
"react-server-dom-parcel": "19.1.0"
37+
},
38+
"devDependencies": {
39+
"@tailwindcss/postcss": "^4.1.10",
40+
"@tailwindcss/typography": "0.5.16",
41+
"@types/compression": "^1.8.1",
42+
"@types/express": "^5.0.3",
43+
"@types/node": "^24.0.3",
44+
"@types/react": "^19.1.8",
45+
"@types/react-dom": "^19.1.6",
46+
"parcel": "^2.15.4",
47+
"postcss": "^8.5.6",
48+
"tailwindcss": "^4.1.10",
49+
"typescript": "^5.8.3"
50+
}
51+
}
14.7 KB
Binary file not shown.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"use client-entry";
2+
3+
import { startTransition, StrictMode } from "react";
4+
import { hydrateRoot } from "react-dom/client";
5+
import {
6+
unstable_createCallServer as createCallServer,
7+
unstable_getRSCStream as getRSCStream,
8+
unstable_RSCHydratedRouter as RSCHydratedRouter,
9+
type unstable_RSCPayload as RSCServerPayload,
10+
} from "react-router";
11+
import {
12+
createFromReadableStream,
13+
createTemporaryReferenceSet,
14+
encodeReply,
15+
setServerCallback,
16+
// @ts-expect-error - no types for this yet
17+
} from "react-server-dom-parcel/client";
18+
19+
// Create and set the callServer function to support post-hydration server actions.
20+
setServerCallback(
21+
createCallServer({
22+
createFromReadableStream,
23+
createTemporaryReferenceSet,
24+
encodeReply,
25+
}),
26+
);
27+
28+
// Get and decode the initial server payload
29+
createFromReadableStream(getRSCStream()).then((payload: RSCServerPayload) => {
30+
startTransition(async () => {
31+
const formState =
32+
payload.type === "render" ? await payload.formState : undefined;
33+
34+
hydrateRoot(
35+
document,
36+
<StrictMode>
37+
<RSCHydratedRouter
38+
createFromReadableStream={createFromReadableStream}
39+
payload={payload}
40+
/>
41+
</StrictMode>,
42+
{
43+
// @ts-expect-error - no types for this yet
44+
formState,
45+
},
46+
);
47+
});
48+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { createRequestListener } from "@mjackson/node-fetch-server";
2+
import compression from "compression";
3+
import express from "express";
4+
import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router";
5+
import {
6+
createTemporaryReferenceSet,
7+
decodeAction,
8+
decodeFormState,
9+
decodeReply,
10+
loadServerAction,
11+
renderToReadableStream,
12+
// @ts-expect-error - no types for this yet
13+
} from "react-server-dom-parcel/server.edge";
14+
15+
// Import the generateHTML function from the client environment
16+
import { generateHTML } from "./entry.ssr" with { env: "react-client" };
17+
import { routes } from "./routes/config";
18+
19+
function fetchServer(request: Request) {
20+
return matchRSCServerRequest({
21+
// Provide the React Server touchpoints.
22+
createTemporaryReferenceSet,
23+
decodeAction,
24+
decodeFormState,
25+
decodeReply,
26+
loadServerAction,
27+
// The incoming request.
28+
request,
29+
// The app routes.
30+
routes: routes(),
31+
// Encode the match with the React Server implementation.
32+
generateResponse(match) {
33+
return new Response(renderToReadableStream(match.payload), {
34+
status: match.statusCode,
35+
headers: match.headers,
36+
});
37+
},
38+
});
39+
}
40+
41+
const app = express();
42+
43+
// Serve static assets with compression and long cache lifetime.
44+
app.use(
45+
"/client",
46+
compression(),
47+
express.static("dist/client", {
48+
immutable: true,
49+
maxAge: "1y",
50+
})
51+
);
52+
app.use(compression(), express.static("public"));
53+
54+
// Ignore Chrome extension requests.
55+
app.get("/.well-known/appspecific/com.chrome.devtools.json", (_, res) => {
56+
res.status(404);
57+
res.end();
58+
});
59+
60+
// Hookup our application.
61+
app.use(
62+
createRequestListener((request) =>
63+
generateHTML(
64+
request,
65+
fetchServer,
66+
(routes as unknown as { bootstrapScript?: string }).bootstrapScript
67+
)
68+
)
69+
);
70+
71+
const PORT = Number.parseInt(process.env.PORT || "3000");
72+
app.listen(PORT, () => {
73+
console.log(`Server listening on port ${PORT} (http://localhost:${PORT})`);
74+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { renderToReadableStream as renderHTMLToReadableStream } from "react-dom/server.edge";
2+
import {
3+
unstable_routeRSCServerRequest as routeRSCServerRequest,
4+
unstable_RSCStaticRouter as RSCStaticRouter,
5+
} from "react-router";
6+
// @ts-expect-error - no types for this yet
7+
import { createFromReadableStream } from "react-server-dom-parcel/client.edge";
8+
9+
export async function generateHTML(
10+
request: Request,
11+
fetchServer: (request: Request) => Promise<Response>,
12+
bootstrapScriptContent: string | undefined,
13+
): Promise<Response> {
14+
return await routeRSCServerRequest({
15+
// The incoming request.
16+
request,
17+
// How to call the React Server.
18+
fetchServer,
19+
// Provide the React Server touchpoints.
20+
createFromReadableStream,
21+
// Render the router to HTML.
22+
async renderHTML(getPayload) {
23+
const payload = await getPayload();
24+
const formState =
25+
payload.type === "render" ? await payload.formState : undefined;
26+
27+
return await renderHTMLToReadableStream(
28+
<RSCStaticRouter getPayload={getPayload} />,
29+
{
30+
bootstrapScriptContent,
31+
// @ts-expect-error - no types for this yet
32+
formState,
33+
},
34+
);
35+
},
36+
});
37+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export default function About() {
2+
return (
3+
<main className="mx-auto max-w-screen-xl px-4 py-8 lg:py-12">
4+
<article className="prose mx-auto">
5+
<h1>About Page</h1>
6+
<p>This is the about page of our application.</p>
7+
</article>
8+
</main>
9+
);
10+
}

0 commit comments

Comments
 (0)