1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource https://esm.sh/react */
import { hydrateRoot } from "https://esm.sh/react-dom@18.3.1/client";
import * as React from "https://esm.sh/react@18.3.1";
export { React };
export type RequestHandler = (request: Request) => Promise<Response>;
export type DataFetcher<T> = (request: Request) => Promise<T>;
// export type DataRequest<T> = Request & { data: T };
export type DataRequest = Request & { data: any };
export type NextCallback = () => Promise<Response>;
export type Middleware = (req: DataRequest, res: Response, callback: NextCallback) => Promise<Response>;
export interface ReactStreamProps {
url: URL;
pathname: string;
search: string;
searchParams: Record<string, string>;
headers: Headers;
cookie: string;
}
export function render<T>(
/** Root-level React component that renders an entire <html> element
* including the head and body tags.
*/
Component: React.ComponentType<ReactStreamProps>,
/** On Val Town, use `import.meta.url` for client-side hydration */
module: string | false,
/** Optional middleware */
opts: ReactStreamOptions | Middleware[] = [],
) {
const useMiddleware = Array.isArray(opts); // for backwards compat
const options: ReactStreamOptions = !Array.isArray(opts) ? opts : {};
const { api, getInitialProps } = options;
if (typeof document !== "undefined" && module) {
const props = window.__data;
hydrateRoot(document, <Component {...props} />);
}
return async function handler(request: Request): Promise<Response> {
const main = reactStream(Component, module);
const middleware: Middleware[] = [
parseURL,
parseHeaders,
// DEPRECATED (for backwards compat)
options.robots && robots(options.robots),
options.api && deprecatedCustomAPI(options.api),
options.getInitialProps && deprecatedGetInitiaProps(options.getInitialProps),
// New custom middleware
...(useMiddleware ? opts.slice() : []),
main,
].filter(Boolean);
let res: Response = new Response();
const req: DataRequest = Object.assign(request.clone(), { data: {} });
req.data = {};
const next = async (nextRes?: Response): Promise<Response> => {
const fn = middleware.shift();
if (nextRes) res = nextRes;
if (!fn) return main(req, res);
return fn(req, res, next);
};
return next();
};
}
export default render;
// main react response handler
const reactStream = (
Component: React.ComponentType<ReactStreamProps>,
module: string | false,
): Middleware =>
async function(req: DataRequest, res: Response): Promise<Response> {
const { renderToReadableStream } = await import("https://esm.sh/react-dom@18.3.1/server");
const stream = await renderToReadableStream(
<Component {...req.data} />,
module
? {
bootstrapModules: [module],
bootstrapScriptContent: `window.__data=${JSON.stringify(req.data)}`,
}
: {},
);
const headers = res.headers;
headers.set("Content-Type", "text/html");
console.log("react", res.status);
return new Response(stream, {
headers,
status: res.status,
statusText: res.statusText,
});
};