'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); }); }); }