Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,104 changes: 3,013 additions & 91 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ packages:
- "node-custom-server"
- "node-postgres"
- "vercel"
- "unstable_rsc-parcel"
- "unstable_rsc-vite"
- "unstable_rsc-vite-spa"
4 changes: 4 additions & 0 deletions unstable_rsc-parcel/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.DS_Store
.parcel-cache
node_modules
dist
54 changes: 54 additions & 0 deletions unstable_rsc-parcel/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"private": true,
"targets": {
"react-server": {
"context": "react-server",
"source": "src/entry.rsc.tsx",
"scopeHoist": false,
"includeNodeModules": {
"@mjackson/node-fetch-server": false,
"compression": false,
"express": false
}
}
},
"postcss": {
"plugins": {
"@tailwindcss/postcss": {}
}
},
"scripts": {
"dev": "cross-env NODE_ENV=development parcel --no-autoinstall --no-cache",
"build": "parcel build --no-autoinstall",
"start": "cross-env NODE_ENV=production node dist/server/entry.rsc.js",
"typecheck": "tsc --noEmit"
},
"overrides": {
"react-router": "0.0.0-experimental-b3337fc38"
},
"dependencies": {
"@mjackson/node-fetch-server": "0.7.0",
"@parcel/runtime-rsc": "^2.15.2",
"buffer": "^6.0.3",
"compression": "^1.8.0",
"cross-env": "^7.0.3",
"express": "^5.1.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-router": "0.0.0-experimental-b3337fc38",
"react-server-dom-parcel": "19.1.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.10",
"@tailwindcss/typography": "0.5.16",
"@types/compression": "^1.8.1",
"@types/express": "^5.0.3",
"@types/node": "^24.0.3",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"parcel": "^2.15.2",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.10",
"typescript": "^5.8.3"
}
}
Binary file added unstable_rsc-parcel/public/favicon.ico
Binary file not shown.
48 changes: 48 additions & 0 deletions unstable_rsc-parcel/src/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client-entry";

import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import {
unstable_createCallServer as createCallServer,
unstable_getRSCStream as getRSCStream,
unstable_RSCHydratedRouter as RSCHydratedRouter,
type unstable_RSCPayload as RSCServerPayload,
} from "react-router";
import {
createFromReadableStream,
createTemporaryReferenceSet,
encodeReply,
setServerCallback,
// @ts-expect-error - no types for this yet
} from "react-server-dom-parcel/client";

// Create and set the callServer function to support post-hydration server actions.
setServerCallback(
createCallServer({
createFromReadableStream,
createTemporaryReferenceSet,
encodeReply,
}),
);

// Get and decode the initial server payload
createFromReadableStream(getRSCStream()).then((payload: RSCServerPayload) => {
startTransition(async () => {
const formState =
payload.type === "render" ? await payload.formState : undefined;

hydrateRoot(
document,
<StrictMode>
<RSCHydratedRouter
createFromReadableStream={createFromReadableStream}
payload={payload}
/>
</StrictMode>,
{
// @ts-expect-error - no types for this yet
formState,
},
);
});
});
74 changes: 74 additions & 0 deletions unstable_rsc-parcel/src/entry.rsc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { createRequestListener } from "@mjackson/node-fetch-server";
import compression from "compression";
import express from "express";
import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router";
import {
createTemporaryReferenceSet,
decodeAction,
decodeFormState,
decodeReply,
loadServerAction,
renderToReadableStream,
// @ts-expect-error - no types for this yet
} from "react-server-dom-parcel/server.edge";

// Import the generateHTML function from the client environment
import { generateHTML } from "./entry.ssr" with { env: "react-client" };
import { routes } from "./routes/config";

function fetchServer(request: Request) {
return matchRSCServerRequest({
// Provide the React Server touchpoints.
createTemporaryReferenceSet,
decodeAction,
decodeFormState,
decodeReply,
loadServerAction,
// The incoming request.
request,
// The app routes.
routes: routes(),
// Encode the match with the React Server implementation.
generateResponse(match) {
return new Response(renderToReadableStream(match.payload), {
status: match.statusCode,
headers: match.headers,
});
},
});
}

const app = express();

// Serve static assets with compression and long cache lifetime.
app.use(
"/client",
compression(),
express.static("dist/client", {
immutable: true,
maxAge: "1y",
})
);
app.use(compression(), express.static("public"));

// Ignore Chrome extension requests.
app.get("/.well-known/appspecific/com.chrome.devtools.json", (_, res) => {
res.status(404);
res.end();
});

// Hookup our application.
app.use(
createRequestListener((request) =>
generateHTML(
request,
fetchServer,
(routes as unknown as { bootstrapScript?: string }).bootstrapScript
)
)
);

const PORT = Number.parseInt(process.env.PORT || "3000");
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT} (http://localhost:${PORT})`);
});
37 changes: 37 additions & 0 deletions unstable_rsc-parcel/src/entry.ssr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { renderToReadableStream as renderHTMLToReadableStream } from "react-dom/server.edge";
import {
unstable_routeRSCServerRequest as routeRSCServerRequest,
unstable_RSCStaticRouter as RSCStaticRouter,
} from "react-router";
// @ts-expect-error - no types for this yet
import { createFromReadableStream } from "react-server-dom-parcel/client.edge";

export async function generateHTML(
request: Request,
fetchServer: (request: Request) => Promise<Response>,
bootstrapScriptContent: string | undefined,
): Promise<Response> {
return await routeRSCServerRequest({
// The incoming request.
request,
// How to call the React Server.
fetchServer,
// Provide the React Server touchpoints.
createFromReadableStream,
// Render the router to HTML.
async renderHTML(getPayload) {
const payload = await getPayload();
const formState =
payload.type === "render" ? await payload.formState : undefined;

return await renderHTMLToReadableStream(
<RSCStaticRouter getPayload={getPayload} />,
{
bootstrapScriptContent,
// @ts-expect-error - no types for this yet
formState,
},
);
},
});
}
10 changes: 10 additions & 0 deletions unstable_rsc-parcel/src/routes/about/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default function About() {
return (
<main className="mx-auto max-w-screen-xl px-4 py-8 lg:py-12">
<article className="prose mx-auto">
<h1>About Page</h1>
<p>This is the about page of our application.</p>
</article>
</main>
);
}
27 changes: 27 additions & 0 deletions unstable_rsc-parcel/src/routes/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use server-entry";

import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router";

import "../entry.client";

export function routes() {
return [
{
id: "root",
path: "",
lazy: () => import("./root/route"),
children: [
{
id: "home",
index: true,
lazy: () => import("./home/route"),
},
{
id: "about",
path: "about",
lazy: () => import("./about/route"),
},
],
},
] satisfies RSCRouteConfig;
}
14 changes: 14 additions & 0 deletions unstable_rsc-parcel/src/routes/home/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default function Home() {
return (
<main className="mx-auto max-w-screen-xl px-4 py-8 lg:py-12">
<article className="prose mx-auto">
<h1>Welcome to React Router RSC</h1>
<p>
This is a simple example of a React Router application using React
Server Components (RSC) with Parcel. It demonstrates how to set up a
basic routing structure and render components server-side.
</p>
</article>
</main>
);
}
73 changes: 73 additions & 0 deletions unstable_rsc-parcel/src/routes/root/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"use client";

import {
isRouteErrorResponse,
Link,
NavLink,
useNavigation,
useRouteError,
} from "react-router";

export function Layout({ children }: { children: React.ReactNode }) {
const navigation = useNavigation();
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
</head>
<body className="font-sans antialiased">
<header className="sticky inset-x-0 top-0 z-50 bg-background border-b">
<div className="mx-auto max-w-screen-xl px-4 relative flex h-16 items-center justify-between gap-4 sm:gap-8">
<div className="flex items-center gap-4">
<Link to="/">React Router 🚀</Link>
<nav>
<ul className="gap-4 flex">
<li>
<NavLink
to="/"
className="text-sm font-medium hover:opacity-75 aria-[current]:opacity-75"
>
Home
</NavLink>
</li>
<li>
<NavLink
to="/about"
className="text-sm font-medium hover:opacity-75 aria-[current]:opacity-75"
>
About
</NavLink>
</li>
</ul>
</nav>
<div>{navigation.state !== "idle" && <p>Loading...</p>}</div>
</div>
</div>
</header>
{children}
</body>
</html>
);
}

export function ErrorBoundary() {
const error = useRouteError();
let status = 500;
let message = "An unexpected error occurred.";

if (isRouteErrorResponse(error)) {
status = error.status;
message = status === 404 ? "Page not found." : error.statusText || message;
}

return (
<main className="mx-auto max-w-screen-xl px-4 py-8 lg:py-12">
<article className="prose mx-auto">
<h1>{status}</h1>
<p>{message}</p>
</article>
</main>
);
}
15 changes: 15 additions & 0 deletions unstable_rsc-parcel/src/routes/root/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Outlet } from "react-router";

import { Layout as ClientLayout } from "./client";
import "./styles.css";

export { ErrorBoundary } from "./client";

export function Layout({ children }: { children: React.ReactNode }) {
// This is necessary for the bundler to inject the needed CSS assets.
return <ClientLayout>{children}</ClientLayout>;
}

export default function Component() {
return <Outlet />;
}
2 changes: 2 additions & 0 deletions unstable_rsc-parcel/src/routes/root/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
16 changes: 16 additions & 0 deletions unstable_rsc-parcel/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"allowImportingTsExtensions": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"skipLibCheck": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"moduleResolution": "Bundler",
"module": "ESNext",
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"jsx": "react-jsx"
}
}
3 changes: 3 additions & 0 deletions unstable_rsc-vite-spa/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DS_Store
node_modules
dist
Loading