mirror of
https://github.com/facebook/react.git
synced 2026-02-26 07:25:30 +00:00
Prerendering in flight is similar to prerendering in Fizz. Instead of receiving a result (the stream) immediately a promise is returned which resolves to the stream when the prerender is complete. The promise will reject if the flight render fatally errors otherwise it will resolve when the render is completed or is aborted.
286 lines
8.1 KiB
JavaScript
286 lines
8.1 KiB
JavaScript
'use strict';
|
|
|
|
// This is a server to host CDN distributed resources like Webpack bundles and SSR
|
|
|
|
const path = require('path');
|
|
|
|
// Do this as the first thing so that any code reading it knows the right env.
|
|
process.env.BABEL_ENV = process.env.NODE_ENV;
|
|
|
|
const babelRegister = require('@babel/register');
|
|
babelRegister({
|
|
babelrc: false,
|
|
ignore: [
|
|
/\/(build|node_modules)\//,
|
|
function (file) {
|
|
if ((path.dirname(file) + '/').startsWith(__dirname + '/')) {
|
|
// Ignore everything in this folder
|
|
// because it's a mix of CJS and ESM
|
|
// and working with raw code is easier.
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
],
|
|
presets: ['@babel/preset-react'],
|
|
});
|
|
|
|
// Ensure environment variables are read.
|
|
require('../config/env');
|
|
|
|
const fs = require('fs').promises;
|
|
const compress = require('compression');
|
|
const chalk = require('chalk');
|
|
const express = require('express');
|
|
const http = require('http');
|
|
const React = require('react');
|
|
|
|
const {renderToPipeableStream} = require('react-dom/server');
|
|
const {createFromNodeStream} = require('react-server-dom-webpack/client');
|
|
const {PassThrough} = require('stream');
|
|
|
|
const app = express();
|
|
|
|
app.use(compress());
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
// In development we host the Webpack server for live bundling.
|
|
const webpack = require('webpack');
|
|
const webpackMiddleware = require('webpack-dev-middleware');
|
|
const webpackHotMiddleware = require('webpack-hot-middleware');
|
|
const paths = require('../config/paths');
|
|
const configFactory = require('../config/webpack.config');
|
|
const getClientEnvironment = require('../config/env');
|
|
|
|
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
|
|
|
|
const config = configFactory('development');
|
|
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
|
const appName = require(paths.appPackageJson).name;
|
|
|
|
// Create a webpack compiler that is configured with custom messages.
|
|
const compiler = webpack(config);
|
|
app.use(
|
|
webpackMiddleware(compiler, {
|
|
publicPath: paths.publicUrlOrPath.slice(0, -1),
|
|
serverSideRender: true,
|
|
headers: () => {
|
|
return {
|
|
'Cache-Control': 'no-store, must-revalidate',
|
|
};
|
|
},
|
|
})
|
|
);
|
|
app.use(webpackHotMiddleware(compiler));
|
|
}
|
|
|
|
function request(options, body) {
|
|
return new Promise((resolve, reject) => {
|
|
const req = http.request(options, res => {
|
|
resolve(res);
|
|
});
|
|
req.on('error', e => {
|
|
reject(e);
|
|
});
|
|
body.pipe(req);
|
|
});
|
|
}
|
|
|
|
async function renderApp(req, res, next) {
|
|
// Proxy the request to the regional server.
|
|
const proxiedHeaders = {
|
|
'X-Forwarded-Host': req.hostname,
|
|
'X-Forwarded-For': req.ips,
|
|
'X-Forwarded-Port': 3000,
|
|
'X-Forwarded-Proto': req.protocol,
|
|
};
|
|
// Proxy other headers as desired.
|
|
if (req.get('rsc-action')) {
|
|
proxiedHeaders['Content-type'] = req.get('Content-type');
|
|
proxiedHeaders['rsc-action'] = req.get('rsc-action');
|
|
} else if (req.get('Content-type')) {
|
|
proxiedHeaders['Content-type'] = req.get('Content-type');
|
|
}
|
|
|
|
const requestsPrerender = req.path === '/prerender';
|
|
|
|
const promiseForData = request(
|
|
{
|
|
host: '127.0.0.1',
|
|
port: 3001,
|
|
method: req.method,
|
|
path: requestsPrerender ? '/?prerender=1' : '/',
|
|
headers: proxiedHeaders,
|
|
},
|
|
req
|
|
);
|
|
|
|
if (req.accepts('text/html')) {
|
|
try {
|
|
const rscResponse = await promiseForData;
|
|
|
|
let virtualFs;
|
|
let buildPath;
|
|
if (process.env.NODE_ENV === 'development') {
|
|
const {devMiddleware} = res.locals.webpack;
|
|
virtualFs = devMiddleware.outputFileSystem.promises;
|
|
buildPath = devMiddleware.stats.toJson().outputPath;
|
|
} else {
|
|
virtualFs = fs;
|
|
buildPath = path.join(__dirname, '../build/');
|
|
}
|
|
// Read the module map from the virtual file system.
|
|
const ssrManifest = JSON.parse(
|
|
await virtualFs.readFile(
|
|
path.join(buildPath, 'react-ssr-manifest.json'),
|
|
'utf8'
|
|
)
|
|
);
|
|
|
|
// Read the entrypoints containing the initial JS to bootstrap everything.
|
|
// For other pages, the chunks in the RSC payload are enough.
|
|
const mainJSChunks = JSON.parse(
|
|
await virtualFs.readFile(
|
|
path.join(buildPath, 'entrypoint-manifest.json'),
|
|
'utf8'
|
|
)
|
|
).main.js;
|
|
// For HTML, we're a "client" emulator that runs the client code,
|
|
// so we start by consuming the RSC payload. This needs a module
|
|
// map that reverse engineers the client-side path to the SSR path.
|
|
|
|
// We need to get the formState before we start rendering but we also
|
|
// need to run the Flight client inside the render to get all the preloads.
|
|
// The API is ambivalent about what's the right one so we need two for now.
|
|
|
|
// Tee the response into two streams so that we can do both.
|
|
const rscResponse1 = new PassThrough();
|
|
const rscResponse2 = new PassThrough();
|
|
|
|
rscResponse.pipe(rscResponse1);
|
|
rscResponse.pipe(rscResponse2);
|
|
|
|
const {formState} = await createFromNodeStream(rscResponse1, ssrManifest);
|
|
rscResponse1.end();
|
|
|
|
let cachedResult;
|
|
let Root = () => {
|
|
if (!cachedResult) {
|
|
// Read this stream inside the render.
|
|
cachedResult = createFromNodeStream(rscResponse2, ssrManifest);
|
|
}
|
|
return React.use(cachedResult).root;
|
|
};
|
|
// Render it into HTML by resolving the client components
|
|
res.set('Content-type', 'text/html');
|
|
const {pipe} = renderToPipeableStream(React.createElement(Root), {
|
|
bootstrapScripts: mainJSChunks,
|
|
formState: formState,
|
|
onShellReady() {
|
|
pipe(res);
|
|
},
|
|
onShellError(error) {
|
|
const {pipe: pipeError} = renderToPipeableStream(
|
|
React.createElement('html', null, React.createElement('body')),
|
|
{
|
|
bootstrapScripts: mainJSChunks,
|
|
}
|
|
);
|
|
pipeError(res);
|
|
},
|
|
});
|
|
} catch (e) {
|
|
console.error(`Failed to SSR: ${e.stack}`);
|
|
res.statusCode = 500;
|
|
res.end();
|
|
}
|
|
} else {
|
|
try {
|
|
const rscResponse = await promiseForData;
|
|
// For other request, we pass-through the RSC payload.
|
|
res.set('Content-type', 'text/x-component');
|
|
rscResponse.on('data', data => {
|
|
res.write(data);
|
|
res.flush();
|
|
});
|
|
rscResponse.on('end', data => {
|
|
res.end();
|
|
});
|
|
} catch (e) {
|
|
console.error(`Failed to proxy request: ${e.stack}`);
|
|
res.statusCode = 500;
|
|
res.end();
|
|
}
|
|
}
|
|
}
|
|
|
|
app.all('/', renderApp);
|
|
app.all('/prerender', renderApp);
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
app.use(express.static('public'));
|
|
|
|
app.get('/source-maps', async function (req, res, next) {
|
|
// Proxy the request to the regional server.
|
|
const proxiedHeaders = {
|
|
'X-Forwarded-Host': req.hostname,
|
|
'X-Forwarded-For': req.ips,
|
|
'X-Forwarded-Port': 3000,
|
|
'X-Forwarded-Proto': req.protocol,
|
|
};
|
|
|
|
const promiseForData = request(
|
|
{
|
|
host: '127.0.0.1',
|
|
port: 3001,
|
|
method: req.method,
|
|
path: req.originalUrl,
|
|
headers: proxiedHeaders,
|
|
},
|
|
req
|
|
);
|
|
|
|
try {
|
|
const rscResponse = await promiseForData;
|
|
res.set('Content-type', 'application/json');
|
|
rscResponse.on('data', data => {
|
|
res.write(data);
|
|
res.flush();
|
|
});
|
|
rscResponse.on('end', data => {
|
|
res.end();
|
|
});
|
|
} catch (e) {
|
|
console.error(`Failed to proxy request: ${e.stack}`);
|
|
res.statusCode = 500;
|
|
res.end();
|
|
}
|
|
});
|
|
} else {
|
|
// In production we host the static build output.
|
|
app.use(express.static('build'));
|
|
}
|
|
|
|
app.listen(3000, () => {
|
|
console.log('Global Fizz/Webpack Server listening on port 3000...');
|
|
});
|
|
|
|
app.on('error', function (error) {
|
|
if (error.syscall !== 'listen') {
|
|
throw error;
|
|
}
|
|
|
|
switch (error.code) {
|
|
case 'EACCES':
|
|
console.error('port 3000 requires elevated privileges');
|
|
process.exit(1);
|
|
break;
|
|
case 'EADDRINUSE':
|
|
console.error('Port 3000 is already in use');
|
|
process.exit(1);
|
|
break;
|
|
default:
|
|
throw error;
|
|
}
|
|
});
|