main
tsx 140 lines 3.97 KB
Raw
1 // Server dependencies.
2 import express, {
3 type Request as ExpressRequest,
4 type Response as ExpressResponse,
5 } from 'express';
6 import {Readable} from 'node:stream';
7 import type {ReadableStream as NodeReadableStream} from 'stream/web';
8 import {
9 renderToReadableStream,
10 loadServerAction,
11 decodeReply,
12 decodeAction,
13 } from 'react-server-dom-parcel/server.edge';
14 import {injectRSCPayload} from 'rsc-html-stream/server';
15
16 // Client dependencies, used for SSR.
17 // These must run in the same environment as client components (e.g. same instance of React).
18 import {createFromReadableStream} from 'react-server-dom-parcel/client.edge' with {env: 'react-client'};
19 import {renderToReadableStream as renderHTMLToReadableStream} from 'react-dom/server.edge' with {env: 'react-client'};
20 import ReactClient, {ReactElement} from 'react' with {env: 'react-client'};
21
22 // Page components. These must have "use server-entry" so they are treated as code splitting entry points.
23 import {Todos} from './Todos';
24
25 const app = express();
26
27 app.use(function (req, res, next) {
28 res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,POST');
29 res.setHeader('Access-Control-Allow-Origin', '*');
30 res.setHeader('Access-Control-Allow-Headers', 'rsc-action');
31 next();
32 });
33
34 app.use(express.static('dist'));
35
36 app.get('/', async (req, res) => {
37 await render(req, res, <Todos />);
38 });
39
40 app.post('/', async (req, res) => {
41 await handleAction(req, res, <Todos />);
42 });
43
44 app.get('/todos/:id', async (req, res) => {
45 await render(req, res, <Todos id={Number(req.params.id)} />);
46 });
47
48 app.post('/todos/:id', async (req, res) => {
49 await handleAction(req, res, <Todos id={Number(req.params.id)} />);
50 });
51
52 async function render(
53 req: ExpressRequest,
54 res: ExpressResponse,
55 component: ReactElement,
56 actionResult?: any,
57 ) {
58 // Render RSC payload.
59 let root: any = component;
60 if (actionResult) {
61 root = {result: actionResult, root};
62 }
63 let stream = renderToReadableStream(root);
64 if (req.accepts('text/html')) {
65 res.setHeader('Content-Type', 'text/html');
66
67 // Use client react to render the RSC payload to HTML.
68 let [s1, s2] = stream.tee();
69 let data: Promise<ReactElement>;
70 function Content() {
71 data ??= createFromReadableStream<ReactElement>(s1);
72 return ReactClient.use(data);
73 }
74
75 let htmlStream = await renderHTMLToReadableStream(<Content />, {
76 bootstrapScriptContent: (Todos as any).bootstrapScript,
77 });
78 let response = htmlStream.pipeThrough(injectRSCPayload(s2));
79 Readable.fromWeb(response as NodeReadableStream).pipe(res);
80 } else {
81 res.set('Content-Type', 'text/x-component');
82 Readable.fromWeb(stream as NodeReadableStream).pipe(res);
83 }
84 }
85
86 // Handle server actions.
87 async function handleAction(
88 req: ExpressRequest,
89 res: ExpressResponse,
90 component: ReactElement,
91 ) {
92 let id = req.get('rsc-action-id');
93 let request = new Request('http://localhost' + req.url, {
94 method: 'POST',
95 headers: req.headers as any,
96 body: Readable.toWeb(req) as ReadableStream,
97 // @ts-ignore
98 duplex: 'half',
99 });
100
101 if (id) {
102 let action = await loadServerAction(id);
103 let body = req.is('multipart/form-data')
104 ? await request.formData()
105 : await request.text();
106 let args = await decodeReply<any[]>(body);
107 let result = action.apply(null, args);
108 try {
109 // Wait for any mutations
110 await result;
111 } catch (x) {
112 // We handle the error on the client
113 }
114
115 await render(req, res, component, result);
116 } else {
117 // Form submitted by browser (progressive enhancement).
118 let formData = await request.formData();
119 let action = await decodeAction(formData);
120 try {
121 // Wait for any mutations
122 await action();
123 } catch (err) {
124 // TODO render error page?
125 }
126 await render(req, res, component);
127 }
128 }
129
130 let server = app.listen(3001);
131 console.log('Server listening on port 3001');
132
133 // Restart the server when it changes.
134 if (module.hot) {
135 module.hot.dispose(() => {
136 server.close();
137 });
138
139 module.hot.accept();
140 }