mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 00:20:04 +01:00
399 lines
12 KiB
JavaScript
399 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
// This is a server to host data-local resources like databases and RSC
|
|
|
|
const path = require('path');
|
|
const url = require('url');
|
|
|
|
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');
|
|
|
|
const activeDebugChannels =
|
|
process.env.NODE_ENV === 'development' ? new Map() : null;
|
|
|
|
function filterStackFrame(sourceURL, functionName) {
|
|
return (
|
|
sourceURL !== '' &&
|
|
!sourceURL.startsWith('node:') &&
|
|
!sourceURL.includes('node_modules') &&
|
|
!sourceURL.endsWith('library.js') &&
|
|
!sourceURL.includes('/server/region.js')
|
|
);
|
|
}
|
|
|
|
function getDebugChannel(req) {
|
|
if (process.env.NODE_ENV !== 'development') {
|
|
return undefined;
|
|
}
|
|
const requestId = req.get('rsc-request-id');
|
|
if (!requestId) {
|
|
return undefined;
|
|
}
|
|
return activeDebugChannels.get(requestId);
|
|
}
|
|
|
|
async function renderApp(res, returnValue, formState, noCache, debugChannel) {
|
|
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, {noCache})
|
|
);
|
|
// For client-invoked server actions we refresh the tree and return a return value.
|
|
const payload = {root, returnValue, formState};
|
|
const {pipe} = renderToPipeableStream(payload, moduleMap, {
|
|
debugChannel,
|
|
filterStackFrame,
|
|
});
|
|
pipe(res);
|
|
}
|
|
|
|
async function prerenderApp(res, returnValue, formState, noCache) {
|
|
const {prerenderToNodeStream} = await import(
|
|
'react-server-dom-webpack/static'
|
|
);
|
|
// 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, {prerender: true, noCache})
|
|
);
|
|
// For client-invoked server actions we refresh the tree and return a return value.
|
|
const payload = {root, returnValue, formState};
|
|
const {prelude} = await prerenderToNodeStream(payload, moduleMap, {
|
|
filterStackFrame,
|
|
});
|
|
prelude.pipe(res);
|
|
}
|
|
|
|
app.get('/', async function (req, res) {
|
|
const noCache = req.get('cache-control') === 'no-cache';
|
|
|
|
if ('prerender' in req.query) {
|
|
await prerenderApp(res, null, null, noCache);
|
|
} else {
|
|
await renderApp(res, null, null, noCache, getDebugChannel(req));
|
|
}
|
|
});
|
|
|
|
app.post('/', bodyParser.text(), async function (req, res) {
|
|
const noCache = req.headers['cache-control'] === 'no-cache';
|
|
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, noCache, getDebugChannel(req));
|
|
} 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, noCache, undefined);
|
|
} catch (x) {
|
|
const {setServerState} = await import('../src/ServerState.js');
|
|
setServerState('Error: ' + x.message);
|
|
renderApp(res, null, null, noCache, undefined);
|
|
}
|
|
}
|
|
});
|
|
|
|
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 = url.fileURLToPath(requestedFilePath);
|
|
}
|
|
|
|
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;
|
|
if (requestedFilePath.startsWith('node:')) {
|
|
// This is a node internal. We don't include any source code for this but we still
|
|
// generate a source map for it so that we can add it to an ignoreList automatically.
|
|
map = {
|
|
version: 3,
|
|
// We use the node:// protocol convention to teach Chrome DevTools that this is
|
|
// on a different protocol and not part of the current page.
|
|
sources: ['node:///' + requestedFilePath.slice(5)],
|
|
sourcesContent: ['// Node Internals'],
|
|
mappings: 'AAAA',
|
|
ignoreList: [0],
|
|
sourceRoot: '',
|
|
};
|
|
} else 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. If a line number points to uncompiled output, like if source mapping
|
|
// was already applied we also use this path.
|
|
const sourceContent = await readFile(requestedFilePath, 'utf8');
|
|
const lines = sourceContent.split('\n').length;
|
|
// We ensure to absolute
|
|
const sourceURL = url.pathToFileURL(requestedFilePath);
|
|
map = {
|
|
version: 3,
|
|
sources: [sourceURL],
|
|
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: '',
|
|
// Add any node_modules to the ignore list automatically.
|
|
ignoreList: requestedFilePath.includes('node_modules')
|
|
? [0]
|
|
: undefined,
|
|
};
|
|
} 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);
|
|
}
|
|
});
|
|
}
|
|
|
|
const httpServer = 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;
|
|
}
|
|
});
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
// Open a websocket server for Debug information
|
|
const WebSocket = require('ws');
|
|
|
|
const webSocketServer = new WebSocket.Server({
|
|
server: httpServer,
|
|
path: '/debug-channel',
|
|
});
|
|
|
|
webSocketServer.on('connection', (ws, req) => {
|
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
const requestId = url.searchParams.get('id');
|
|
|
|
activeDebugChannels.set(requestId, ws);
|
|
|
|
ws.on('close', (code, reason) => {
|
|
activeDebugChannels.delete(requestId);
|
|
});
|
|
});
|
|
}
|