-
Notifications
You must be signed in to change notification settings - Fork 61
/
server.js
164 lines (145 loc) · 4.75 KB
/
server.js
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { build as esbuild } from 'esbuild';
import { fileURLToPath } from 'node:url';
import { createElement } from 'react';
import { serveStatic } from '@hono/node-server/serve-static';
import * as ReactServerDom from 'react-server-dom-webpack/server.browser';
import { readFile, writeFile } from 'node:fs/promises';
import { parse } from 'es-module-lexer';
import { relative } from 'node:path';
const app = new Hono();
const clientComponentMap = {};
/**
* Endpoint to serve your index route.
* Includes the loader `/build/_client.js` to request your server component
* and stream results into `<div id="root">`
*/
app.get('/', async (c) => {
return c.html(`
<!DOCTYPE html>
<html>
<head>
<title>React Server Components from Scratch</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/build/_client.js"></script>
</body>
</html>
`);
});
/**
* Endpoint to render your server component to a stream.
* This uses `react-server-dom-webpack` to parse React elements
* into encoded virtual DOM elements for the client to read.
*/
app.get('/rsc', async (c) => {
// Note This will raise a type error until you build with `npm run dev`
const Page = await import('./build/page.js');
// @ts-expect-error `Type '() => Promise<any>' is not assignable to type 'FunctionComponent<{}>'`
const Comp = createElement(Page.default);
const stream = ReactServerDom.renderToReadableStream(Comp, clientComponentMap);
return new Response(stream);
});
/**
* Serve your `build/` folder as static assets.
* Allows you to serve built client components
* to import from your browser.
*/
app.use('/build/*', serveStatic());
/**
* Build both server and client components with esbuild
*/
async function build() {
const clientEntryPoints = new Set();
/** Build the server component tree */
await esbuild({
bundle: true,
format: 'esm',
logLevel: 'error',
entryPoints: [resolveApp('page.jsx')],
outdir: resolveBuild(),
// avoid bundling npm packages for server-side components
packages: 'external',
plugins: [
{
name: 'resolve-client-imports',
setup(build) {
// Intercept component imports to check for 'use client'
build.onResolve({ filter: reactComponentRegex }, async ({ path: relativePath }) => {
const path = resolveApp(relativePath);
const contents = await readFile(path, 'utf-8');
if (contents.startsWith("'use client'")) {
clientEntryPoints.add(path);
return {
// Avoid bundling client components into the server build.
external: true,
// Resolve the client import to the built `.js` file
// created by the client `esbuild` process below.
path: relativePath.replace(reactComponentRegex, '.js')
};
}
});
}
}
]
});
/** Build client components */
const { outputFiles } = await esbuild({
bundle: true,
format: 'esm',
logLevel: 'error',
entryPoints: [resolveApp('_client.jsx'), ...clientEntryPoints],
outdir: resolveBuild(),
splitting: true,
write: false
});
outputFiles.forEach(async (file) => {
// Parse file export names
const [, exports] = parse(file.text);
let newContents = file.text;
for (const exp of exports) {
// Create a unique lookup key for each exported component.
// Could be any identifier!
// We'll choose the file path + export name for simplicity.
const key = file.path + exp.n;
clientComponentMap[key] = {
// Have the browser import your component from your server
// at `/build/[component].js`
id: `/build/${relative(resolveBuild(), file.path)}`,
// Use the detected export name
name: exp.n,
// Turn off chunks. This is webpack-specific
chunks: [],
// Use an async import for the built resource in the browser
async: true
};
// Tag each component export with a special `react.client.reference` type
// and the map key to look up import information.
// This tells your stream renderer to avoid rendering the
// client component server-side. Instead, import the built component
// client-side at `clientComponentMap[key].id`
newContents += `
${exp.ln}.$$id = ${JSON.stringify(key)};
${exp.ln}.$$typeof = Symbol.for("react.client.reference");
`;
}
await writeFile(file.path, newContents);
});
}
serve(app, async (info) => {
await build();
console.log(`Listening on http://localhost:${info.port}`);
});
/** UTILS */
const appDir = new URL('./app/', import.meta.url);
const buildDir = new URL('./build/', import.meta.url);
function resolveApp(path = '') {
return fileURLToPath(new URL(path, appDir));
}
function resolveBuild(path = '') {
return fileURLToPath(new URL(path, buildDir));
}
const reactComponentRegex = /\.jsx$/;