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
96 changes: 82 additions & 14 deletions bun.lock

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions fixtures/react-router-rsc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# rsc react-router

https://vite-rsc-react-router.hiro18181.workers.dev

> [!NOTE]
> React Router now provides [official RSC support](https://reactrouter.com/how-to/react-server-components) for Vite. The example might not be kept up to date with the latest version. Please refer to React Router's official documentation for the latest integrations.
Vite RSC example based on demo made by React router team with Parcel:

- https://github.com/jacob-ebey/parcel-plugin-react-router/
- https://github.com/jacob-ebey/experimental-parcel-react-router-starter
- https://github.com/remix-run/react-router/tree/rsc/playground/rsc-vite

See also [`rsc-movies`](https://github.com/hi-ogawa/rsc-movies/).

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc/examples/react-router?file=src%2Froutes%2Froot.tsx)

Or try it locally by:

```sh
npx giget gh:vitejs/vite-plugin-react/packages/plugin-rsc/examples/react-router my-app
cd my-app
npm i
npm run dev
npm run build
npm run preview

# run on @cloudflare/vite-plugin and deploy.
# a separate configuration is found in ./cf/vite.config.ts
npm run cf-dev
npm run cf-build
npm run cf-preview
npm run cf-release
```
150 changes: 150 additions & 0 deletions fixtures/react-router-rsc/app/paper.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
@theme {
--default-font-family: "Patrick Hand SC", sans-serif;
--default-mono-font-family: "Patrick Hand SC", sans-serif;

--color-foreground: black;
--color-danger: rgb(167, 52, 45);
--color-secondary: rgb(11, 116, 213);
--color-success: rgb(134, 163, 97);
--color-warning: rgb(221, 205, 69);
--color-border: #cdcccb;
--color-border-active: rgba(0, 0, 0, 0.2);

--color-paper-background: white;
--color-paper-border: #cdcccb;
--shadow-paper: -1px 5px 35px -9px rgba(0, 0, 0, 0.2);

--shadow-btn: 15px 28px 25px -18px rgba(0, 0, 0, 0.2);
--shadow-btn-hover: 2px 8px 8px -5px rgba(0, 0, 0, 0.3);
--color-btn-border: black;
--btn-color-danger: var(--color-danger);
--btn-color-secondary: var(--color-secondary);
--btn-color-success: var(--color-success);
--btn-color-warning: var(--color-warning);
}

@utility paper-border {
@apply border-2 border-border;
border-bottom-left-radius: 25px 115px;
border-bottom-right-radius: 155px 25px;
border-top-left-radius: 15px 225px;
border-top-right-radius: 25px 150px;
}

@utility no-paper-border {
@apply border-0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
}

@utility paper-underline {
@apply border-b-3 border-[currentcolor];
border-bottom-left-radius: 15px 3px;
border-bottom-right-radius: 15px 5px;
border-bottom-style: solid;
}

@utility paper-underline-hover {
@apply paper-underline border-transparent;
@variant hover {
@apply border-[currentcolor];
}
}

@utility paper {
@apply border border-paper-border bg-paper-background p-8 shadow-paper;
}

@utility breadcrumbs {
@apply flex flex-wrap gap-2;
& > * {
@apply inline-block after:text-lg after:content-[""] not-last:after:ml-2 not-last:after:text-foreground not-last:after:content-["/"];
}
& > a {
@apply text-secondary;
}
}

@utility btn {
@apply inline-block cursor-pointer bg-paper-background paper-border px-4 py-2 text-lg shadow-btn transition-[shadow_transition];

@variant active {
@apply border-border-active;
}
@variant hover {
@apply translate-y-1 shadow-btn-hover;
}

&.btn-icon {
@apply aspect-square px-2 py-2;
& img,
& svg {
@apply h-7 w-7;
}
}
}

@utility btn-* {
border-color: --value(--btn-color-*);
color: --value(--btn-color-*);
}

@utility btn-sm {
@apply px-2 py-1 text-base;
}

@utility btn-lg {
@apply px-6 py-3 text-2xl;
}

@utility label {
@apply mb-1 block font-semibold;
}

@utility input {
@apply paper-border px-3 py-2;

@variant disabled {
@apply border-border-active;
}
}

@utility checkbox {
@apply h-6 w-6 paper-border;

@variant disabled {
@apply border-border-active;
}
}

@utility select {
@apply paper-border px-3 py-2;

@variant disabled {
@apply border-border-active;
}
}

@layer base {
body {
@apply text-foreground;
}

* {
@apply outline-secondary;
}
}

@layer utilities {
.prose {
:where(u):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
@apply paper-underline no-underline;
}

:where(a):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
@apply paper-underline-hover no-underline text-secondary;
}
}
}
55 changes: 55 additions & 0 deletions fixtures/react-router-rsc/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// oxlint-disable-next-line import/no-unassigned-import
import "./styles.css";
import { Link, Outlet } from "react-router";
import { TestClientState, TestHydrated } from "./routes/client";
import { DumpError, GlobalNavigationLoadingBar } from "./routes/root.client";

export function Layout({ children }: { children: React.ReactNode }) {
console.log("[debug] root - Layout");
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React Router Vite</title>
</head>
<body>
<header className="container px-8 my-8 mx-auto">
<nav className="paper paper-border">
<ul className="flex gap-4 flex-wrap">
<li className="flex gap-4 not-last:after:block not-last:after:content-['|']">
<Link to="/">Home</Link>
</li>
<li className="flex gap-4 not-last:after:block">
<Link to="/about">About</Link>
</li>
<li className="flex-1"></li>
<li className="flex items-center gap-2 text-gray-500">
<TestHydrated />
<TestClientState />
<span data-testid="root-style" className="text-[#0000ff]">
[style]
</span>
</li>
</ul>
</nav>
</header>
<GlobalNavigationLoadingBar />
{children}
</body>
</html>
);
}

export default function Component() {
console.log("[debug] root - Component");
return (
<>
<Outlet />
</>
);
}

export function ErrorBoundary() {
return <DumpError />;
}
21 changes: 21 additions & 0 deletions fixtures/react-router-rsc/app/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { unstable_RSCRouteConfigEntry } from "react-router";

export const routes: Array<unstable_RSCRouteConfigEntry> = [
{
id: "root",
path: "",
lazy: () => import("./root"),
children: [
{
id: "home",
index: true,
lazy: () => import("./routes/home"),
},
{
id: "about",
path: "about",
lazy: () => import("./routes/about"),
},
],
},
];
20 changes: 20 additions & 0 deletions fixtures/react-router-rsc/app/routes/about.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use client";

import React from "react";

export function Component() {
const [count, setCount] = React.useState(0);

return (
<main className="container my-8 px-8 mx-auto">
<article className="paper prose max-w-none">
<h1>About</h1>
<p>This is the about page.</p>
<p className="test-style-home">[test-style-home]</p>
<button className="btn" onClick={() => setCount((c) => c + 1)}>
Client counter: {count}
</button>
</article>
</main>
);
}
16 changes: 16 additions & 0 deletions fixtures/react-router-rsc/app/routes/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"use client";

import React from "react";

export function TestHydrated() {
const hydrated = React.useSyncExternalStore(
React.useCallback(() => () => {}, []),
() => true,
() => false,
);
return <span data-testid="hydrated">[hydrated: {hydrated ? 1 : 0}]</span>;
}

export function TestClientState() {
return <input className="input py-0" data-testid="client-state" placeholder="client-state" />;
}
7 changes: 7 additions & 0 deletions fixtures/react-router-rsc/app/routes/home.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use server";

export async function sayHello(defaultName: string, formData: FormData) {
await new Promise((resolve) => setTimeout(resolve, 500));
const name = formData.get("name") || defaultName;
console.log(`[debug] sayHello - ${name}`);
}
12 changes: 12 additions & 0 deletions fixtures/react-router-rsc/app/routes/home.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client";

import { useFormStatus } from "react-dom";

export function PendingButton() {
const status = useFormStatus();
return (
<button className="btn" type="submit" disabled={status.pending}>
{status.pending ? "Pending..." : "Log on server"}
</button>
);
}
3 changes: 3 additions & 0 deletions fixtures/react-router-rsc/app/routes/home.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.test-style-home {
color: rgb(250, 150, 0);
}
34 changes: 34 additions & 0 deletions fixtures/react-router-rsc/app/routes/home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { sayHello } from "./home.actions.ts";
import { PendingButton } from "./home.client.tsx";
// oxlint-disable-next-line import/no-unassigned-import
import "./home.css";
import { TestActionStateServer } from "./test-action-state/server.tsx";

const Component = () => {
return (
<main className="container my-8 px-8 mx-auto">
<article className="paper prose max-w-none">
<h1>Home</h1>
<p>This is the home page.</p>
<span className="test-style-home">[test-style-home]</span>
<h2>Server Action</h2>
<form className="no-prose grid gap-6" action={sayHello.bind(null, "Demo")}>
<div className="grid gap-1">
<label className="label" htmlFor="name">
Name
</label>
<input className="input" id="name" type="text" name="name" placeholder={"Demo"} />
</div>
<div>
<PendingButton />
</div>
</form>
<div className="mt-4">
<TestActionStateServer message={`${new Date().toISOString()}`} />
</div>
</article>
</main>
);
};

export default Component;
Loading