Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(rsbuild-rsc): tweak demo #59

Merged
merged 4 commits into from
Jul 28, 2024
Merged
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
1 change: 1 addition & 0 deletions rsbuild-rsc/misc/vercel/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ async function main() {
entryPoints: [join(import.meta.dirname, "entry.js")],
outfile: join(outDir, "functions/index.func/index.js"),
bundle: true,
minify: true,
format: "esm",
platform: "browser",
});
Expand Down
32 changes: 28 additions & 4 deletions rsbuild-rsc/src/entry-browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,45 @@ import ReactClient from "react-server-dom-webpack/client.browser";
import type { FlightData } from "./entry-server";
import "./lib/virtual-client-references-browser.js";
import "./style.css";
import { setupBrowserRouter } from "./lib/router/browser";

async function main() {
const url = new URL(window.location.href);
if (url.searchParams.has("__nojs")) {
return;
}

// TODO
const callServer = () => {};

// [flight => react node] react client
const initialFlight = await ReactClient.createFromReadableStream<FlightData>(
const initialFlight = ReactClient.createFromReadableStream<FlightData>(
(self as any).__flightStream,
// TODO
{ callServer: () => {} },
{ callServer },
);

let browserRoot = initialFlight.node;
function BrowserRoot() {
const [flight, setFlight] =
React.useState<Promise<FlightData>>(initialFlight);

React.useEffect(() => {
return setupBrowserRouter(() => {
const url = new URL(window.location.href);
url.searchParams.set("__f", "");
React.startTransition(() =>
setFlight(
ReactClient.createFromFetch<FlightData>(fetch(url), {
callServer,
}),
),
);
});
}, []);

return <>{React.use(flight).node}</>;
}

let browserRoot = <BrowserRoot />;
if (!url.searchParams.has("__nostrict")) {
browserRoot = <React.StrictMode>{browserRoot}</React.StrictMode>;
}
Expand Down
21 changes: 13 additions & 8 deletions rsbuild-rsc/src/entry-server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export async function handler(request: Request): Promise<ServerResult> {
}

// [react node -> flight] react server
const node = <Router />;
const node = <Router request={request} />;
const { browserManifest } = await getClientManifest();
const flightStream = ReactServer.renderToReadableStream<FlightData>(
{ node },
Expand All @@ -25,12 +25,17 @@ export async function handler(request: Request): Promise<ServerResult> {
return { flightStream };
}

async function Router() {
async function Router(props: { request: Request }) {
const url = new URL(props.request.url);
const { default: Layout } = await import("./routes/layout");
const { default: Page } = await import("./routes/page");
return (
<Layout>
<Page />
</Layout>
);
let page = <h1>Not Found</h1>; // TODO: 404 status
if (url.pathname === "/") {
const { default: Page } = await import("./routes/page");
page = <Page />;
}
if (url.pathname === "/stream") {
const { default: Page } = await import("./routes/stream/page");
page = <Page />;
}
return <Layout>{page}</Layout>;
}
41 changes: 41 additions & 0 deletions rsbuild-rsc/src/lib/router/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export function setupBrowserRouter(onNavigation: () => void) {
window.addEventListener("pushstate", onNavigation);
window.addEventListener("popstate", onNavigation);

const oldPushState = window.history.pushState;
window.history.pushState = function (...args) {
const res = oldPushState.apply(this, args);
onNavigation();
return res;
};

const oldReplaceState = window.history.replaceState;
window.history.replaceState = function (...args) {
const res = oldReplaceState.apply(this, args);
onNavigation();
return res;
};

function linkHandler(e: MouseEvent) {
const el = e.target;
if (
e.button === 0 &&
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) &&
el instanceof HTMLAnchorElement &&
(!el.target || el.target === "_self") &&
new URL(el.href).origin === window.location.origin
) {
e.preventDefault();
window.history.pushState({}, "", el.href);
}
}
document.addEventListener("click", linkHandler);

return () => {
document.removeEventListener("click", linkHandler);
window.removeEventListener("pushstate", onNavigation);
window.removeEventListener("popstate", onNavigation);
window.history.pushState = oldPushState;
window.history.replaceState = oldReplaceState;
};
}
19 changes: 18 additions & 1 deletion rsbuild-rsc/src/routes/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,25 @@ export default function Layout(props: React.PropsWithChildren) {
rel="icon"
href="https://assets.rspack.dev/rsbuild/favicon-128x128.png"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>{props.children}</body>
<body>
<div
style={{
display: "flex",
gap: "1rem",
alignItems: "center",
justifyContent: "center",
}}
>
Menu:
<a href="/">Home</a>
<a href="/stream">Stream</a>
</div>
<div id="root" style={{ display: "flex", placeContent: "center" }}>
{props.children}
</div>
</body>
</html>
);
}
2 changes: 1 addition & 1 deletion rsbuild-rsc/src/routes/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Counter, Hydrated } from "./_client";

export default function Page() {
return (
<div id="root">
<div>
<h1>Rsbuild RSC</h1>
<pre>{React.version}</pre>
<Hydrated />
Expand Down
5 changes: 5 additions & 0 deletions rsbuild-rsc/src/routes/stream/_client3.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"use client";

export function Client3() {
return <span style={{ display: "none" }}>test-client3</span>;
}
47 changes: 47 additions & 0 deletions rsbuild-rsc/src/routes/stream/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from "react";
import { Client3 } from "./_client3";

export default function Page() {
return (
<div
style={{
border: "1px solid #0004",
padding: "1rem",
}}
>
<h2>Stream</h2>
<Client3 />
<div
style={{
padding: "1rem",
}}
>
<div>Outer</div>
<pre>[rendered at {new Date().toISOString()}]</pre>
</div>
<div
style={{
padding: "1rem",
height: "5rem",
background: "#f002",
display: "grid",
placeContent: "center",
}}
>
<React.Suspense fallback={<div>Sleeping 1 sec...</div>}>
<Sleep />
</React.Suspense>
</div>
</div>
);
}

async function Sleep() {
await new Promise((r) => setTimeout(r, 1000));
return (
<div>
<div>Inner</div>
<pre>[rendered at {new Date().toISOString()}]</pre>
</div>
);
}
10 changes: 6 additions & 4 deletions rsbuild-rsc/src/style.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* based on https://github.com/vitejs/vite/blob/86cf1b4b497557f09a0d9a81dc304e7a081d6198/packages/create-vite/template-react/src/index.css */

:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
Expand Down Expand Up @@ -25,9 +27,10 @@ a:hover {
body {
margin: 0;
display: flex;
place-items: center;
flex-direction: column;
padding: 1rem 0;
gap: 1rem 0;
min-width: 320px;
min-height: 100vh;
}

h1 {
Expand Down Expand Up @@ -70,7 +73,6 @@ button:focus-visible {
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}

Expand Down Expand Up @@ -103,7 +105,7 @@ button:focus-visible {
}

.card {
padding: 2em;
padding: 1rem;
}

.read-the-docs {
Expand Down
7 changes: 7 additions & 0 deletions rsbuild-rsc/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ import { defineConfig } from "vite";
// use vite preview server for local build

export default defineConfig({
appType: "custom",
plugins: [
{
name: "preview-middleware",
async configurePreviewServer(server) {
server.middlewares.use((req, _res, next) => {
// disable compression entirely since it breaks streaming
delete req.headers["accept-encoding"];
next();
});

const mod = await import(path.resolve("./dist/ssr/index.cjs"));
return () => {
server.middlewares.use(webToNodeHandler(mod.default.default));
Expand Down