-
Notifications
You must be signed in to change notification settings - Fork 65
Expand file tree
/
Copy pathserver.js
More file actions
159 lines (138 loc) · 4.47 KB
/
server.js
File metadata and controls
159 lines (138 loc) · 4.47 KB
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
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response';
import { build as esbuild } from 'esbuild';
import { fileURLToPath } from 'node:url';
import { createElement } from 'react';
import { serveStatic } from '@hono/node-server/serve-static';
import { renderToPipeableStream } from 'react-server-dom-esm/server';
import { readFile, writeFile } from 'node:fs/promises';
import { parse } from 'es-module-lexer';
import { relative, resolve } from 'node:path';
const app = new Hono();
/**
* 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');
const Comp = createElement(Page.default);
const stream = renderToPipeableStream(Comp, '');
// @ts-expect-error type of env is 'unknown'
stream.pipe(c.env.outgoing);
return RESPONSE_ALREADY_SENT;
});
/**
* 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, resolveDir }) => {
const path = resolveApp(resolve(resolveDir, 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 the id for each exported component
// React needs this in the format <file path>#<export name>
const relativeBuildPath = `/build/${relative(resolveBuild(), file.path)}`;
const key = `${relativeBuildPath}#${exp.n}`;
// 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$/;