main
js 297 lines 8.41 KB
Raw
1 'use strict';
2
3 // This is a server to host CDN distributed resources like Webpack bundles and SSR
4
5 const path = require('path');
6
7 // Do this as the first thing so that any code reading it knows the right env.
8 process.env.BABEL_ENV = process.env.NODE_ENV;
9
10 const babelRegister = require('@babel/register');
11 babelRegister({
12 babelrc: false,
13 ignore: [
14 /\/(build|node_modules)\//,
15 function (file) {
16 if ((path.dirname(file) + '/').startsWith(__dirname + '/')) {
17 // Ignore everything in this folder
18 // because it's a mix of CJS and ESM
19 // and working with raw code is easier.
20 return true;
21 }
22 return false;
23 },
24 ],
25 presets: ['@babel/preset-react'],
26 });
27
28 // Ensure environment variables are read.
29 require('../config/env');
30
31 const fs = require('fs').promises;
32 const compress = require('compression');
33 const chalk = require('chalk');
34 const express = require('express');
35 const http = require('http');
36 const React = require('react');
37
38 const {renderToPipeableStream} = require('react-dom/server');
39 const {createFromNodeStream} = require('react-server-dom-unbundled/client');
40 const {PassThrough} = require('stream');
41
42 const app = express();
43
44 app.use(compress());
45
46 if (process.env.NODE_ENV === 'development') {
47 // In development we host the Webpack server for live bundling.
48 const webpack = require('webpack');
49 const webpackMiddleware = require('webpack-dev-middleware');
50 const webpackHotMiddleware = require('webpack-hot-middleware');
51 const paths = require('../config/paths');
52 const configFactory = require('../config/webpack.config');
53 const getClientEnvironment = require('../config/env');
54
55 const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
56
57 const config = configFactory('development');
58 const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
59 const appName = require(paths.appPackageJson).name;
60
61 // Create a webpack compiler that is configured with custom messages.
62 const compiler = webpack(config);
63 app.use(
64 webpackMiddleware(compiler, {
65 publicPath: paths.publicUrlOrPath.slice(0, -1),
66 serverSideRender: true,
67 headers: () => {
68 return {
69 'Cache-Control': 'no-store, must-revalidate',
70 };
71 },
72 })
73 );
74 app.use(webpackHotMiddleware(compiler));
75 }
76
77 function request(options, body) {
78 return new Promise((resolve, reject) => {
79 const req = http.request(options, res => {
80 resolve(res);
81 });
82 req.on('error', e => {
83 reject(e);
84 });
85 body.pipe(req);
86 });
87 }
88
89 async function renderApp(req, res, next) {
90 // Proxy the request to the regional server.
91 const proxiedHeaders = {
92 'X-Forwarded-Host': req.hostname,
93 'X-Forwarded-For': req.ips,
94 'X-Forwarded-Port': 3000,
95 'X-Forwarded-Proto': req.protocol,
96 };
97 // Proxy other headers as desired.
98 if (req.get('rsc-action')) {
99 proxiedHeaders['Content-type'] = req.get('Content-type');
100 proxiedHeaders['rsc-action'] = req.get('rsc-action');
101 } else if (req.get('Content-type')) {
102 proxiedHeaders['Content-type'] = req.get('Content-type');
103 }
104 if (req.headers['cache-control']) {
105 proxiedHeaders['Cache-Control'] = req.get('cache-control');
106 }
107 if (req.get('rsc-request-id')) {
108 proxiedHeaders['rsc-request-id'] = req.get('rsc-request-id');
109 }
110
111 const requestsPrerender = req.path === '/prerender';
112
113 const promiseForData = request(
114 {
115 host: '127.0.0.1',
116 port: 3001,
117 method: req.method,
118 path: requestsPrerender ? '/?prerender=1' : '/',
119 headers: proxiedHeaders,
120 },
121 req
122 );
123
124 if (req.accepts('text/html')) {
125 try {
126 const rscResponse = await promiseForData;
127
128 let virtualFs;
129 let buildPath;
130 if (process.env.NODE_ENV === 'development') {
131 const {devMiddleware} = res.locals.webpack;
132 virtualFs = devMiddleware.outputFileSystem.promises;
133 buildPath = devMiddleware.stats.toJson().outputPath;
134 } else {
135 virtualFs = fs;
136 buildPath = path.join(__dirname, '../build/');
137 }
138 // Read the module map from the virtual file system.
139 const serverConsumerManifest = JSON.parse(
140 await virtualFs.readFile(
141 path.join(buildPath, 'react-ssr-manifest.json'),
142 'utf8'
143 )
144 );
145
146 // Read the entrypoints containing the initial JS to bootstrap everything.
147 // For other pages, the chunks in the RSC payload are enough.
148 const mainJSChunks = JSON.parse(
149 await virtualFs.readFile(
150 path.join(buildPath, 'entrypoint-manifest.json'),
151 'utf8'
152 )
153 ).main.js;
154 // For HTML, we're a "client" emulator that runs the client code,
155 // so we start by consuming the RSC payload. This needs a module
156 // map that reverse engineers the client-side path to the SSR path.
157
158 // We need to get the formState before we start rendering but we also
159 // need to run the Flight client inside the render to get all the preloads.
160 // The API is ambivalent about what's the right one so we need two for now.
161
162 // Tee the response into two streams so that we can do both.
163 const rscResponse1 = new PassThrough();
164 const rscResponse2 = new PassThrough();
165
166 rscResponse.pipe(rscResponse1);
167 rscResponse.pipe(rscResponse2);
168
169 const {formState} = await createFromNodeStream(
170 rscResponse1,
171 serverConsumerManifest
172 );
173 rscResponse1.end();
174
175 let cachedResult;
176 let Root = () => {
177 if (!cachedResult) {
178 // Read this stream inside the render.
179 cachedResult = createFromNodeStream(
180 rscResponse2,
181 serverConsumerManifest
182 );
183 }
184 return React.use(cachedResult).root;
185 };
186 // Render it into HTML by resolving the client components
187 res.set('Content-type', 'text/html');
188 const {pipe} = renderToPipeableStream(React.createElement(Root), {
189 bootstrapScripts: mainJSChunks,
190 formState: formState,
191 onShellReady() {
192 pipe(res);
193 },
194 onShellError(error) {
195 const {pipe: pipeError} = renderToPipeableStream(
196 React.createElement('html', null, React.createElement('body')),
197 {
198 bootstrapScripts: mainJSChunks,
199 }
200 );
201 pipeError(res);
202 },
203 });
204 } catch (e) {
205 console.error(`Failed to SSR: ${e.stack}`);
206 res.statusCode = 500;
207 res.end();
208 }
209 } else {
210 try {
211 const rscResponse = await promiseForData;
212 // For other request, we pass-through the RSC payload.
213 res.set('Content-type', 'text/x-component');
214 rscResponse.on('data', data => {
215 res.write(data);
216 res.flush();
217 });
218 rscResponse.on('end', data => {
219 res.end();
220 });
221 } catch (e) {
222 console.error(`Failed to proxy request: ${e.stack}`);
223 res.statusCode = 500;
224 res.end();
225 }
226 }
227 }
228
229 app.all('/', renderApp);
230 app.all('/prerender', renderApp);
231
232 if (process.env.NODE_ENV === 'development') {
233 app.use(express.static('public'));
234
235 app.get('/source-maps', async function (req, res, next) {
236 // Proxy the request to the regional server.
237 const proxiedHeaders = {
238 'X-Forwarded-Host': req.hostname,
239 'X-Forwarded-For': req.ips,
240 'X-Forwarded-Port': 3000,
241 'X-Forwarded-Proto': req.protocol,
242 };
243
244 const promiseForData = request(
245 {
246 host: '127.0.0.1',
247 port: 3001,
248 method: req.method,
249 path: req.originalUrl,
250 headers: proxiedHeaders,
251 },
252 req
253 );
254
255 try {
256 const rscResponse = await promiseForData;
257 res.set('Content-type', 'application/json');
258 rscResponse.on('data', data => {
259 res.write(data);
260 res.flush();
261 });
262 rscResponse.on('end', data => {
263 res.end();
264 });
265 } catch (e) {
266 console.error(`Failed to proxy request: ${e.stack}`);
267 res.statusCode = 500;
268 res.end();
269 }
270 });
271 } else {
272 // In production we host the static build output.
273 app.use(express.static('build'));
274 }
275
276 app.listen(3000, () => {
277 console.log('Global Fizz/Webpack Server listening on port 3000...');
278 });
279
280 app.on('error', function (error) {
281 if (error.syscall !== 'listen') {
282 throw error;
283 }
284
285 switch (error.code) {
286 case 'EACCES':
287 console.error('port 3000 requires elevated privileges');
288 process.exit(1);
289 break;
290 case 'EADDRINUSE':
291 console.error('Port 3000 is already in use');
292 process.exit(1);
293 break;
294 default:
295 throw error;
296 }
297 });