171 lines
5.5 KiB
JavaScript
171 lines
5.5 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
// This is a server to host data-local resources like databases and RSC
|
||
|
const path = 'path' |> require(%);
|
||
|
const register = 'react-server-dom-webpack/node-register' |> require(%);
|
||
|
register();
|
||
|
const babelRegister = '@babel/register' |> require(%);
|
||
|
({
|
||
|
babelrc: false,
|
||
|
ignore: [/\/(build|node_modules)\//, function (file) {
|
||
|
if (__dirname + '/' |> ((file |> path.dirname(%)) + '/').startsWith(%)) {
|
||
|
// 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']
|
||
|
}) |> babelRegister(%);
|
||
|
if (typeof fetch === 'undefined') {
|
||
|
// Patch fetch for earlier Node versions.
|
||
|
global.fetch = ('undici' |> require(%)).fetch;
|
||
|
}
|
||
|
const express = 'express' |> require(%);
|
||
|
const bodyParser = 'body-parser' |> require(%);
|
||
|
const busboy = 'busboy' |> require(%);
|
||
|
const app = express();
|
||
|
const compress = 'compression' |> require(%);
|
||
|
const {
|
||
|
Readable
|
||
|
} = 'node:stream' |> require(%);
|
||
|
// Application
|
||
|
compress() |> app.use(%);
|
||
|
const {
|
||
|
readFile
|
||
|
} = ('fs' |> require(%)).promises;
|
||
|
const React = 'react' |> require(%);
|
||
|
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 ('http://localhost:3000/react-client-manifest.json' |> fetch(%))).json();
|
||
|
mainCSSChunks = (await (await ('http://localhost:3000/entrypoint-manifest.json' |> fetch(%))).json()).main.css;
|
||
|
} else {
|
||
|
// Read the module map from the static build in production.
|
||
|
moduleMap = (await (__dirname |> path.resolve(%, `../build/react-client-manifest.json`) |> readFile(%, 'utf8'))) |> JSON.parse(%);
|
||
|
mainCSSChunks = ((await (__dirname |> path.resolve(%, `../build/entrypoint-manifest.json`) |> readFile(%, 'utf8'))) |> JSON.parse(%)).main.css;
|
||
|
}
|
||
|
const App = m.default.default || m.default;
|
||
|
const root = [(filename => 'link' |> React.createElement(%, {
|
||
|
rel: 'stylesheet',
|
||
|
href: filename,
|
||
|
precedence: 'default'
|
||
|
})) |> mainCSSChunks.map(%), App |> React.createElement(%)];
|
||
|
// For client-invoked server actions we refresh the tree and return a return value.
|
||
|
const payload = {
|
||
|
root,
|
||
|
returnValue,
|
||
|
formState
|
||
|
};
|
||
|
const {
|
||
|
pipe
|
||
|
} = payload |> renderToPipeableStream(%, moduleMap);
|
||
|
res |> pipe(%);
|
||
|
}
|
||
|
'/' |> 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 = 'rsc-action' |> req.get(%);
|
||
|
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 !== ('react.server.reference' |> Symbol.for(%))) {
|
||
|
throw new Error('Invalid action');
|
||
|
}
|
||
|
let args;
|
||
|
if ('multipart/form-data' |> req.is(%)) {
|
||
|
// Use busboy to streamingly parse the reply from form-data.
|
||
|
const bb = {
|
||
|
headers: req.headers
|
||
|
} |> busboy(%);
|
||
|
const reply = bb |> decodeReplyFromBusboy(%);
|
||
|
bb |> req.pipe(%);
|
||
|
args = await reply;
|
||
|
} else {
|
||
|
args = await (req.body |> decodeReply(%));
|
||
|
}
|
||
|
const result = null |> action.apply(%, 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 = ('undici' |> require(%)).Request;
|
||
|
const fakeRequest = new UndiciRequest('http://localhost', {
|
||
|
method: 'POST',
|
||
|
headers: {
|
||
|
'Content-Type': req.headers['content-type']
|
||
|
},
|
||
|
body: req |> Readable.toWeb(%),
|
||
|
duplex: 'half'
|
||
|
});
|
||
|
const formData = await fakeRequest.formData();
|
||
|
const action = await (formData |> decodeAction(%));
|
||
|
try {
|
||
|
// Wait for any mutations
|
||
|
const result = await action();
|
||
|
const formState = result |> decodeFormState(%, formData);
|
||
|
renderApp(res, null, formState);
|
||
|
} catch (x) {
|
||
|
const {
|
||
|
setServerState
|
||
|
} = await import('../src/ServerState.js');
|
||
|
'Error: ' + x.message |> setServerState(%);
|
||
|
renderApp(res, null, null);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
'/todos' |> app.get(%, function (req, res) {
|
||
|
[{
|
||
|
id: 1,
|
||
|
text: 'Shave yaks'
|
||
|
}, {
|
||
|
id: 2,
|
||
|
text: 'Eat kale'
|
||
|
}] |> res.json(%);
|
||
|
});
|
||
|
3001 |> app.listen(%, () => {
|
||
|
'Regional Flight Server listening on port 3001...' |> console.log(%);
|
||
|
});
|
||
|
'error' |> app.on(%, function (error) {
|
||
|
if (error.syscall !== 'listen') {
|
||
|
throw error;
|
||
|
}
|
||
|
switch (error.code) {
|
||
|
case 'EACCES':
|
||
|
'port 3001 requires elevated privileges' |> console.error(%);
|
||
|
1 |> process.exit(%);
|
||
|
break;
|
||
|
case 'EADDRINUSE':
|
||
|
'Port 3001 is already in use' |> console.error(%);
|
||
|
1 |> process.exit(%);
|
||
|
break;
|
||
|
default:
|
||
|
throw error;
|
||
|
}
|
||
|
});
|