mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
This lets us ensure that we use the original V8 format and it lets us
skip source mapping. Source mapping every call can be expensive since we
do it eagerly for server components even if an error doesn't happen.
In the case of an error being thrown we don't actually always do this in
practice because if a try/catch before us touches it or if something in
onError touches it (which the default console.error does), it has
already been initialized. So we have to be resilient to thrown errors
having other formats.
These are not as perf sensitive since something actually threw but if
you want better perf in these cases, you can simply do something like
`onError(error) { console.error(error.message) }` instead.
The server has to be aware whether it's looking up original or compiled
output. I currently use the file:// check to determine if it's referring
to a source mapped file or compiled file in the fixture. A bundled app
can more easily check if it's a bundle or not.
269 lines
8.3 KiB
JavaScript
269 lines
8.3 KiB
JavaScript
'use strict';
|
|
|
|
// This is a server to host data-local resources like databases and RSC
|
|
|
|
const path = require('path');
|
|
|
|
const register = require('react-server-dom-webpack/node-register');
|
|
register();
|
|
|
|
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'],
|
|
plugins: ['@babel/transform-modules-commonjs'],
|
|
sourceMaps: process.env.NODE_ENV === 'development' ? 'inline' : false,
|
|
});
|
|
|
|
if (typeof fetch === 'undefined') {
|
|
// Patch fetch for earlier Node versions.
|
|
global.fetch = require('undici').fetch;
|
|
}
|
|
|
|
const express = require('express');
|
|
const bodyParser = require('body-parser');
|
|
const busboy = require('busboy');
|
|
const app = express();
|
|
const compress = require('compression');
|
|
const {Readable} = require('node:stream');
|
|
|
|
const nodeModule = require('node:module');
|
|
|
|
app.use(compress());
|
|
|
|
// Application
|
|
|
|
const {readFile} = require('fs').promises;
|
|
|
|
const React = require('react');
|
|
|
|
async function renderApp(res, returnValue, formState) {
|
|
const {renderToPipeableStream} = await import(
|
|
'react-server-dom-webpack/server'
|
|
);
|
|
// const m = require('../src/App.js');
|
|
const m = await import('../src/App.js');
|
|
|
|
let moduleMap;
|
|
let mainCSSChunks;
|
|
if (process.env.NODE_ENV === 'development') {
|
|
// Read the module map from the HMR server in development.
|
|
moduleMap = await (
|
|
await fetch('http://localhost:3000/react-client-manifest.json')
|
|
).json();
|
|
mainCSSChunks = (
|
|
await (
|
|
await fetch('http://localhost:3000/entrypoint-manifest.json')
|
|
).json()
|
|
).main.css;
|
|
} else {
|
|
// Read the module map from the static build in production.
|
|
moduleMap = JSON.parse(
|
|
await readFile(
|
|
path.resolve(__dirname, `../build/react-client-manifest.json`),
|
|
'utf8'
|
|
)
|
|
);
|
|
mainCSSChunks = JSON.parse(
|
|
await readFile(
|
|
path.resolve(__dirname, `../build/entrypoint-manifest.json`),
|
|
'utf8'
|
|
)
|
|
).main.css;
|
|
}
|
|
const App = m.default.default || m.default;
|
|
const root = React.createElement(
|
|
React.Fragment,
|
|
null,
|
|
// Prepend the App's tree with stylesheets required for this entrypoint.
|
|
mainCSSChunks.map(filename =>
|
|
React.createElement('link', {
|
|
rel: 'stylesheet',
|
|
href: filename,
|
|
precedence: 'default',
|
|
key: filename,
|
|
})
|
|
),
|
|
React.createElement(App)
|
|
);
|
|
// For client-invoked server actions we refresh the tree and return a return value.
|
|
const payload = {root, returnValue, formState};
|
|
const {pipe} = renderToPipeableStream(payload, moduleMap);
|
|
pipe(res);
|
|
}
|
|
|
|
app.get('/', async function (req, res) {
|
|
await renderApp(res, null, null);
|
|
});
|
|
|
|
app.post('/', bodyParser.text(), async function (req, res) {
|
|
const {decodeReply, decodeReplyFromBusboy, decodeAction, decodeFormState} =
|
|
await import('react-server-dom-webpack/server');
|
|
const serverReference = req.get('rsc-action');
|
|
if (serverReference) {
|
|
// This is the client-side case
|
|
const [filepath, name] = serverReference.split('#');
|
|
const action = (await import(filepath))[name];
|
|
// Validate that this is actually a function we intended to expose and
|
|
// not the client trying to invoke arbitrary functions. In a real app,
|
|
// you'd have a manifest verifying this before even importing it.
|
|
if (action.$$typeof !== Symbol.for('react.server.reference')) {
|
|
throw new Error('Invalid action');
|
|
}
|
|
|
|
let args;
|
|
if (req.is('multipart/form-data')) {
|
|
// Use busboy to streamingly parse the reply from form-data.
|
|
const bb = busboy({headers: req.headers});
|
|
const reply = decodeReplyFromBusboy(bb);
|
|
req.pipe(bb);
|
|
args = await reply;
|
|
} else {
|
|
args = await decodeReply(req.body);
|
|
}
|
|
const result = action.apply(null, args);
|
|
try {
|
|
// Wait for any mutations
|
|
await result;
|
|
} catch (x) {
|
|
// We handle the error on the client
|
|
}
|
|
// Refresh the client and return the value
|
|
renderApp(res, result, null);
|
|
} else {
|
|
// This is the progressive enhancement case
|
|
const UndiciRequest = require('undici').Request;
|
|
const fakeRequest = new UndiciRequest('http://localhost', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': req.headers['content-type']},
|
|
body: Readable.toWeb(req),
|
|
duplex: 'half',
|
|
});
|
|
const formData = await fakeRequest.formData();
|
|
const action = await decodeAction(formData);
|
|
try {
|
|
// Wait for any mutations
|
|
const result = await action();
|
|
const formState = decodeFormState(result, formData);
|
|
renderApp(res, null, formState);
|
|
} catch (x) {
|
|
const {setServerState} = await import('../src/ServerState.js');
|
|
setServerState('Error: ' + x.message);
|
|
renderApp(res, null, null);
|
|
}
|
|
}
|
|
});
|
|
|
|
app.get('/todos', function (req, res) {
|
|
res.json([
|
|
{
|
|
id: 1,
|
|
text: 'Shave yaks',
|
|
},
|
|
{
|
|
id: 2,
|
|
text: 'Eat kale',
|
|
},
|
|
]);
|
|
});
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
const rootDir = path.resolve(__dirname, '../');
|
|
|
|
app.get('/source-maps', async function (req, res, next) {
|
|
try {
|
|
res.set('Content-type', 'application/json');
|
|
let requestedFilePath = req.query.name;
|
|
|
|
let isCompiledOutput = false;
|
|
if (requestedFilePath.startsWith('file://')) {
|
|
// We assume that if it was prefixed with file:// it's referring to the compiled output
|
|
// and if it's a direct file path we assume it's source mapped back to original format.
|
|
isCompiledOutput = true;
|
|
requestedFilePath = requestedFilePath.slice(7);
|
|
}
|
|
|
|
const relativePath = path.relative(rootDir, requestedFilePath);
|
|
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
// This is outside the root directory of the app. Forbid it to be served.
|
|
res.status = 403;
|
|
res.write('{}');
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
const sourceMap = nodeModule.findSourceMap(requestedFilePath);
|
|
let map;
|
|
// There are two ways to return a source map depending on what we observe in error.stack.
|
|
// A real app will have a similar choice to make for which strategy to pick.
|
|
if (!sourceMap || !isCompiledOutput) {
|
|
// If a file doesn't have a source map, such as this file, then we generate a blank
|
|
// source map that just contains the original content and segments pointing to the
|
|
// original lines.
|
|
// Similarly
|
|
const sourceContent = await readFile(requestedFilePath, 'utf8');
|
|
const lines = sourceContent.split('\n').length;
|
|
map = {
|
|
version: 3,
|
|
sources: [requestedFilePath],
|
|
sourcesContent: [sourceContent],
|
|
// Note: This approach to mapping each line only lets you jump to each line
|
|
// not jump to a column within a line. To do that, you need a proper source map
|
|
// generated for each parsed segment or add a segment for each column.
|
|
mappings: 'AAAA' + ';AACA'.repeat(lines - 1),
|
|
sourceRoot: '',
|
|
};
|
|
} else {
|
|
// We always set prepareStackTrace before reading the stack so that we get the stack
|
|
// without source maps applied. Therefore we have to use the original source map.
|
|
// If something read .stack before we did, we might observe the line/column after
|
|
// source mapping back to the original file. We use the isCompiledOutput check above
|
|
// in that case.
|
|
map = sourceMap.payload;
|
|
}
|
|
res.write(JSON.stringify(map));
|
|
res.end();
|
|
} catch (x) {
|
|
res.status = 500;
|
|
res.write('{}');
|
|
res.end();
|
|
console.error(x);
|
|
}
|
|
});
|
|
}
|
|
|
|
app.listen(3001, () => {
|
|
console.log('Regional Flight Server listening on port 3001...');
|
|
});
|
|
|
|
app.on('error', function (error) {
|
|
if (error.syscall !== 'listen') {
|
|
throw error;
|
|
}
|
|
|
|
switch (error.code) {
|
|
case 'EACCES':
|
|
console.error('port 3001 requires elevated privileges');
|
|
process.exit(1);
|
|
break;
|
|
case 'EADDRINUSE':
|
|
console.error('Port 3001 is already in use');
|
|
process.exit(1);
|
|
break;
|
|
default:
|
|
throw error;
|
|
}
|
|
});
|