1. 'use strict';
    
  2. 
    
  3. // This is a server to host CDN distributed resources like Webpack bundles and SSR
    
  4. 
    
  5. const path = require('path');
    
  6. 
    
  7. // Do this as the first thing so that any code reading it knows the right env.
    
  8. process.env.BABEL_ENV = process.env.NODE_ENV;
    
  9. 
    
  10. const babelRegister = require('@babel/register');
    
  11. babelRegister({
    
  12.   babelrc: false,
    
  13.   ignore: [
    
  14.     /\/(build|node_modules)\//,
    
  15.     function (file) {
    
  16.       if ((path.dirname(file) + '/').startsWith(__dirname + '/')) {
    
  17.         // Ignore everything in this folder
    
  18.         // because it's a mix of CJS and ESM
    
  19.         // and working with raw code is easier.
    
  20.         return true;
    
  21.       }
    
  22.       return false;
    
  23.     },
    
  24.   ],
    
  25.   presets: ['@babel/preset-react'],
    
  26. });
    
  27. 
    
  28. // Ensure environment variables are read.
    
  29. require('../config/env');
    
  30. 
    
  31. const fs = require('fs').promises;
    
  32. const compress = require('compression');
    
  33. const chalk = require('chalk');
    
  34. const express = require('express');
    
  35. const http = require('http');
    
  36. const React = require('react');
    
  37. 
    
  38. const {renderToPipeableStream} = require('react-dom/server');
    
  39. const {createFromNodeStream} = require('react-server-dom-webpack/client');
    
  40. 
    
  41. const app = express();
    
  42. 
    
  43. app.use(compress());
    
  44. 
    
  45. if (process.env.NODE_ENV === 'development') {
    
  46.   // In development we host the Webpack server for live bundling.
    
  47.   const webpack = require('webpack');
    
  48.   const webpackMiddleware = require('webpack-dev-middleware');
    
  49.   const webpackHotMiddleware = require('webpack-hot-middleware');
    
  50.   const paths = require('../config/paths');
    
  51.   const configFactory = require('../config/webpack.config');
    
  52.   const getClientEnvironment = require('../config/env');
    
  53. 
    
  54.   const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
    
  55. 
    
  56.   const config = configFactory('development');
    
  57.   const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
    
  58.   const appName = require(paths.appPackageJson).name;
    
  59. 
    
  60.   // Create a webpack compiler that is configured with custom messages.
    
  61.   const compiler = webpack(config);
    
  62.   app.use(
    
  63.     webpackMiddleware(compiler, {
    
  64.       publicPath: paths.publicUrlOrPath.slice(0, -1),
    
  65.       serverSideRender: true,
    
  66.       headers: () => {
    
  67.         return {
    
  68.           'Cache-Control': 'no-store, must-revalidate',
    
  69.         };
    
  70.       },
    
  71.     })
    
  72.   );
    
  73.   app.use(webpackHotMiddleware(compiler));
    
  74. }
    
  75. 
    
  76. function request(options, body) {
    
  77.   return new Promise((resolve, reject) => {
    
  78.     const req = http.request(options, res => {
    
  79.       resolve(res);
    
  80.     });
    
  81.     req.on('error', e => {
    
  82.       reject(e);
    
  83.     });
    
  84.     body.pipe(req);
    
  85.   });
    
  86. }
    
  87. 
    
  88. app.all('/', async function (req, res, next) {
    
  89.   // Proxy the request to the regional server.
    
  90.   const proxiedHeaders = {
    
  91.     'X-Forwarded-Host': req.hostname,
    
  92.     'X-Forwarded-For': req.ips,
    
  93.     'X-Forwarded-Port': 3000,
    
  94.     'X-Forwarded-Proto': req.protocol,
    
  95.   };
    
  96.   // Proxy other headers as desired.
    
  97.   if (req.get('rsc-action')) {
    
  98.     proxiedHeaders['Content-type'] = req.get('Content-type');
    
  99.     proxiedHeaders['rsc-action'] = req.get('rsc-action');
    
  100.   } else if (req.get('Content-type')) {
    
  101.     proxiedHeaders['Content-type'] = req.get('Content-type');
    
  102.   }
    
  103. 
    
  104.   const promiseForData = request(
    
  105.     {
    
  106.       host: '127.0.0.1',
    
  107.       port: 3001,
    
  108.       method: req.method,
    
  109.       path: '/',
    
  110.       headers: proxiedHeaders,
    
  111.     },
    
  112.     req
    
  113.   );
    
  114. 
    
  115.   if (req.accepts('text/html')) {
    
  116.     try {
    
  117.       const rscResponse = await promiseForData;
    
  118. 
    
  119.       let virtualFs;
    
  120.       let buildPath;
    
  121.       if (process.env.NODE_ENV === 'development') {
    
  122.         const {devMiddleware} = res.locals.webpack;
    
  123.         virtualFs = devMiddleware.outputFileSystem.promises;
    
  124.         buildPath = devMiddleware.stats.toJson().outputPath;
    
  125.       } else {
    
  126.         virtualFs = fs;
    
  127.         buildPath = path.join(__dirname, '../build/');
    
  128.       }
    
  129.       // Read the module map from the virtual file system.
    
  130.       const ssrManifest = JSON.parse(
    
  131.         await virtualFs.readFile(
    
  132.           path.join(buildPath, 'react-ssr-manifest.json'),
    
  133.           'utf8'
    
  134.         )
    
  135.       );
    
  136. 
    
  137.       // Read the entrypoints containing the initial JS to bootstrap everything.
    
  138.       // For other pages, the chunks in the RSC payload are enough.
    
  139.       const mainJSChunks = JSON.parse(
    
  140.         await virtualFs.readFile(
    
  141.           path.join(buildPath, 'entrypoint-manifest.json'),
    
  142.           'utf8'
    
  143.         )
    
  144.       ).main.js;
    
  145.       // For HTML, we're a "client" emulator that runs the client code,
    
  146.       // so we start by consuming the RSC payload. This needs a module
    
  147.       // map that reverse engineers the client-side path to the SSR path.
    
  148. 
    
  149.       // This is a bad hack to set the form state after SSR has started. It works
    
  150.       // because we block the root component until we have the form state and
    
  151.       // any form that reads it necessarily will come later. It also only works
    
  152.       // because the formstate type is an object which may change in the future
    
  153.       const lazyFormState = [];
    
  154. 
    
  155.       let cachedResult = null;
    
  156.       async function getRootAndFormState() {
    
  157.         const {root, formState} = await createFromNodeStream(
    
  158.           rscResponse,
    
  159.           ssrManifest
    
  160.         );
    
  161.         // We shouldn't be assuming formState is an object type but at the moment
    
  162.         // we have no way of setting the form state from within the render
    
  163.         Object.assign(lazyFormState, formState);
    
  164.         return root;
    
  165.       }
    
  166.       let Root = () => {
    
  167.         if (!cachedResult) {
    
  168.           cachedResult = getRootAndFormState();
    
  169.         }
    
  170.         return React.use(cachedResult);
    
  171.       };
    
  172.       // Render it into HTML by resolving the client components
    
  173.       res.set('Content-type', 'text/html');
    
  174.       const {pipe} = renderToPipeableStream(React.createElement(Root), {
    
  175.         bootstrapScripts: mainJSChunks,
    
  176.         formState: lazyFormState,
    
  177.       });
    
  178.       pipe(res);
    
  179.     } catch (e) {
    
  180.       console.error(`Failed to SSR: ${e.stack}`);
    
  181.       res.statusCode = 500;
    
  182.       res.end();
    
  183.     }
    
  184.   } else {
    
  185.     try {
    
  186.       const rscResponse = await promiseForData;
    
  187.       // For other request, we pass-through the RSC payload.
    
  188.       res.set('Content-type', 'text/x-component');
    
  189.       rscResponse.on('data', data => {
    
  190.         res.write(data);
    
  191.         res.flush();
    
  192.       });
    
  193.       rscResponse.on('end', data => {
    
  194.         res.end();
    
  195.       });
    
  196.     } catch (e) {
    
  197.       console.error(`Failed to proxy request: ${e.stack}`);
    
  198.       res.statusCode = 500;
    
  199.       res.end();
    
  200.     }
    
  201.   }
    
  202. });
    
  203. 
    
  204. if (process.env.NODE_ENV === 'development') {
    
  205.   app.use(express.static('public'));
    
  206. } else {
    
  207.   // In production we host the static build output.
    
  208.   app.use(express.static('build'));
    
  209. }
    
  210. 
    
  211. app.listen(3000, () => {
    
  212.   console.log('Global Fizz/Webpack Server listening on port 3000...');
    
  213. });
    
  214. 
    
  215. app.on('error', function (error) {
    
  216.   if (error.syscall !== 'listen') {
    
  217.     throw error;
    
  218.   }
    
  219. 
    
  220.   switch (error.code) {
    
  221.     case 'EACCES':
    
  222.       console.error('port 3000 requires elevated privileges');
    
  223.       process.exit(1);
    
  224.       break;
    
  225.     case 'EADDRINUSE':
    
  226.       console.error('Port 3000 is already in use');
    
  227.       process.exit(1);
    
  228.       break;
    
  229.     default:
    
  230.       throw error;
    
  231.   }
    
  232. });