mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
We use the stack of a Promise as the start of the I/O instead of the actual I/O since that can symbolize the start of the operation even if the actual I/O is batched, deduped or pooled. It can also group multiple I/O operations into one. We want the deepest possible Promise since otherwise it would just be the Component's Promise. However, we don't really need deeper than the boundary between first party and third party. We can't just take the outer most that has third party things on the stack though because third party can have callbacks into first party and then we want the inner one. So we take the inner most Promise that depends on I/O that has a first party stack on it. The realization is that for the purposes of determining whether we have a first party stack we need to ignore async stack frames. They can appear on the stack when we resume third party code inside a resumption frame of a first party stack. <img width="832" alt="Screenshot 2025-07-08 at 6 34 25 PM" src="https://github.com/user-attachments/assets/1636f980-be4c-4340-ad49-8d2b31953436" /> --------- Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
407 lines
13 KiB
JavaScript
407 lines
13 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')
|
|
);
|
|
}
|
|
|
|
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,
|
|
promiseForDebugChannel
|
|
) {
|
|
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: await promiseForDebugChannel,
|
|
filterStackFrame,
|
|
});
|
|
pipe(res);
|
|
}
|
|
|
|
async function prerenderApp(res, returnValue, formState, noCache) {
|
|
const {unstable_prerenderToNodeStream: 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({noServer: true});
|
|
|
|
httpServer.on('upgrade', (request, socket, head) => {
|
|
const DEBUG_CHANNEL_PATH = '/debug-channel?';
|
|
if (request.url.startsWith(DEBUG_CHANNEL_PATH)) {
|
|
const requestId = request.url.slice(DEBUG_CHANNEL_PATH.length);
|
|
const promiseForWs = new Promise(resolve => {
|
|
webSocketServer.handleUpgrade(request, socket, head, ws => {
|
|
ws.on('close', () => {
|
|
activeDebugChannels.delete(requestId);
|
|
});
|
|
resolve(ws);
|
|
});
|
|
});
|
|
activeDebugChannels.set(requestId, promiseForWs);
|
|
} else {
|
|
socket.destroy();
|
|
}
|
|
});
|
|
}
|