Skip to content

Commit

Permalink
feat: render rsc into document
Browse files Browse the repository at this point in the history
  • Loading branch information
chenjun1011 committed Nov 16, 2023
1 parent 67dc6e9 commit 2fc0088
Show file tree
Hide file tree
Showing 6 changed files with 6,269 additions and 6,392 deletions.
19 changes: 0 additions & 19 deletions examples/with-rsc/src/components/RefreshButton.client.tsx

This file was deleted.

4 changes: 0 additions & 4 deletions examples/with-rsc/src/pages/about.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useAppContext } from 'ice';
import styles from './about.module.css';
import RefreshButton from '@/components/RefreshButton.client';
import Counter from '@/components/Counter.client';

if (!global.requestCount) {
Expand All @@ -18,9 +17,6 @@ export default function Home() {
<h2>About Page</h2>
<div>server request count: { global.requestCount++ }</div>
<Counter />
<RefreshButton>
Refresh Button
</RefreshButton>
</div>
);
}
3 changes: 1 addition & 2 deletions packages/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import type { RunClientAppOptions, CreateRoutes } from './runClientApp.js';
import { useAppContext as useInternalAppContext, useAppData, AppContextProvider } from './AppContext.js';
import { getAppData } from './appData.js';
import { useData, useConfig } from './RouteContext.js';
import { runRSCClientApp, useRefresh } from './runRSCClientApp.js';
import { runRSCClientApp } from './runRSCClientApp.js';
import {
Meta,
Title,
Expand Down Expand Up @@ -145,7 +145,6 @@ export {
RouteErrorComponent,

runRSCClientApp,
useRefresh,
};

export type {
Expand Down
103 changes: 67 additions & 36 deletions packages/runtime/src/runRSCClientApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,93 @@ import pkg from 'react-server-dom-webpack/client';
import type { AppConfig } from './types.js';

// @ts-ignore
const { Suspense, use, useState, createContext, useContext, startTransition } = React;
const { createFromFetch } = pkg;
const { Suspense, use } = React;
const { createFromReadableStream } = pkg;

export async function runRSCClientApp(appConfig: AppConfig) {
// It's possible that the DOM is already loaded.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', DOMContentLoaded, false);
} else {
DOMContentLoaded();
}

const rscData = (self as any).__rsc_data || [];
rscData.forEach(serverDataCallback);
rscData.push = serverDataCallback;

const rootId = appConfig.app.rootId || 'app';
const container = document.getElementById(rootId);
const root = ReactDOM.createRoot(container);
root.render(<Root />);
}

function Root() {
const response = useInitialServerResponse(window.location.href);

return (
<Router />
<Suspense>
{use(response)}
</Suspense>
);
}

const RouterContext = createContext(null);
const initialCache = new Map();
const rscCache = new Map();

function Router() {
const [cache, setCache] = useState(initialCache);
const [location] = useState(window.location.href);
function useInitialServerResponse(cacheKey: string): Promise<JSX.Element> {
const response = rscCache.get(cacheKey);
if (response) return response;

let content = cache.get(location);
if (!content) {
content = createFromFetch(
getReactTree(location),
);
cache.set(location, content);
}
const readable = new ReadableStream({
start(controller) {
nextServerDataRegisterWriter(controller);
},
});

const newResponse = createFromReadableStream(readable);

rscCache.set(cacheKey, newResponse);
return newResponse;
}

let initialServerDataBuffer: string[] | undefined;
let initialServerDataWriter: ReadableStreamDefaultController | undefined;
let initialServerDataLoaded = false;
let initialServerDataFlushed = false;
const encoder = new TextEncoder();

function refresh() {
startTransition(() => {
const nextCache = new Map();
const nextContent = createFromFetch(
getReactTree(location),
);
nextCache.set(location, nextContent);
setCache(nextCache);
function nextServerDataRegisterWriter(ctr: ReadableStreamDefaultController) {
if (initialServerDataBuffer) {
initialServerDataBuffer.forEach((val) => {
ctr.enqueue(encoder.encode(val));
});
if (initialServerDataLoaded && !initialServerDataFlushed) {
ctr.close();
initialServerDataFlushed = true;
initialServerDataBuffer = undefined;
}
}

return (
<RouterContext.Provider value={{ location, refresh }}>
<Suspense fallback={<h1>Loading...</h1>}>
{use(content)}
</Suspense>
</RouterContext.Provider>
);
initialServerDataWriter = ctr;
}

export function useRefresh() {
const router = useContext(RouterContext);
return router.refresh;
}
// When `DOMContentLoaded`, we can close all pending writers to finish hydration.
const DOMContentLoaded = function () {
if (initialServerDataWriter && !initialServerDataFlushed) {
initialServerDataWriter.close();
initialServerDataFlushed = true;
initialServerDataBuffer = undefined;
}
initialServerDataLoaded = true;
};

function getReactTree(location) {
return fetch(location + (location.indexOf('?') > -1 ? '&rsc=true' : '?rsc=true'));
function serverDataCallback(seg) {
if (initialServerDataWriter) {
initialServerDataWriter.enqueue(encoder.encode(seg));
} else {
if (!initialServerDataBuffer) {
initialServerDataBuffer = [];
}
initialServerDataBuffer.push(seg);
}
}
38 changes: 33 additions & 5 deletions packages/runtime/src/runRSCServerApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ import type {
ServerRenderOptions as RenderOptions,
} from './types.js';

// This utility is based on https://github.com/zertosh/htmlescape
// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE
const ESCAPE_LOOKUP: { [match: string]: string } = {
'&': '\\u0026',
'>': '\\u003e',
'<': '\\u003c',
'\u2028': '\\u2028',
'\u2029': '\\u2029',
};

const ESCAPE_REGEX = /[&><\u2028\u2029]/g;

function htmlEscapeJsonString(str: string): string {
return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
}

export async function runRSCServerApp(serverContext: ServerContext, renderOptions: RenderOptions) {
const { req, res } = serverContext;

Expand Down Expand Up @@ -49,9 +65,7 @@ export async function runRSCServerApp(serverContext: ServerContext, renderOption
matches: [],
};

if (req.url?.indexOf('rsc=true') === -1) {
return renderDocument(serverContext, renderOptions, appContext, matches);
}
renderDocument(serverContext, renderOptions, appContext, matches);

const routeModules = await loadRouteModules(matches.map(({ route: { id, lazy } }) => ({ id, lazy })));

Expand All @@ -71,7 +85,22 @@ export async function runRSCServerApp(serverContext: ServerContext, renderOption
}
});

res.setHeader('Content-Type', 'text/x-component; charset=utf-8');
const decoder = new TextDecoder();
const encoder = new TextEncoder();

res.write('<script>self.__rsc_data=self.__rsc_data||[];</script>');

function decorateWrite(write) {
return function (data) {
const chunk = decoder.decode(data, { stream: true });
const modifiedData = `<script>self.__rsc_data.push(${htmlEscapeJsonString(JSON.stringify([chunk]))})</script>`;

return write.call(this, encoder.encode(modifiedData));
};
}

res.write = decorateWrite(res.write);

const { pipe } = renderToPipeableStream(
element,
clientManifest,
Expand Down Expand Up @@ -116,6 +145,5 @@ function renderDocument(requestContext, renderOptions, appContext, matches) {

res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.write(`<!DOCTYPE html>${htmlStr}`);
res.end();
}

Loading

0 comments on commit 2fc0088

Please sign in to comment.