mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
This lets you click a stack frame on the client and see the Server source code inline. <img width="871" alt="Screenshot 2024-06-01 at 11 44 24 PM" src="https://github.com/facebook/react/assets/63648/581281ce-0dce-40c0-a084-4a6d53ba1682"> <img width="840" alt="Screenshot 2024-06-01 at 11 43 37 PM" src="https://github.com/facebook/react/assets/63648/00dc77af-07c1-4389-9ae0-cf1f45199efb"> We could do some logic on the server that sends a source map url for every stack frame in the RSC payload. That would make the client potentially config free. However regardless we need the config to describe what url scheme to use since that’s not built in to the bundler config. In practice you likely have a common pattern for your source maps so no need to send data over and over when we can just have a simple function configured on the client. The server must return a source map, even if the file is not actually compiled since the fake file is still compiled. The source mapping strategy can be one of two models depending on if the server’s stack traces (`new Error().stack`) are source mapped back to the original (`—enable-source-maps`) or represents the location in compiled code (like in the browser). If it represents the location in compiled code it’s actually easier. You just serve the source map generated for that file by the tooling. If it is already source mapped it has to generate a source map where everything points to the same location (as if not compiled) ideally with a segment per logical ast node.
281 lines
8.0 KiB
JavaScript
281 lines
8.0 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);
|
|
});
|
|
}
|
|
|
|
app.all('/', 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,
|
|
};
|
|
// 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 promiseForData = request(
|
|
{
|
|
host: '127.0.0.1',
|
|
port: 3001,
|
|
method: req.method,
|
|
path: '/',
|
|
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();
|
|
}
|
|
}
|
|
});
|
|
|
|
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;
|
|
}
|
|
});
|