| 1 | 'use strict'; |
| 2 | |
| 3 | // This is a server to host CDN distributed resources like module source files and SSR |
| 4 | |
| 5 | const path = require('path'); |
| 6 | const url = require('url'); |
| 7 | |
| 8 | const fs = require('fs').promises; |
| 9 | const compress = require('compression'); |
| 10 | const chalk = require('chalk'); |
| 11 | const express = require('express'); |
| 12 | const http = require('http'); |
| 13 | const React = require('react'); |
| 14 | |
| 15 | const {renderToPipeableStream} = require('react-dom/server'); |
| 16 | const {createFromNodeStream} = require('react-server-dom-esm/client'); |
| 17 | |
| 18 | const moduleBasePath = new URL('../src', url.pathToFileURL(__filename)).href; |
| 19 | |
| 20 | const app = express(); |
| 21 | |
| 22 | app.use(compress()); |
| 23 | |
| 24 | function request(options, body) { |
| 25 | return new Promise((resolve, reject) => { |
| 26 | const req = http.request(options, res => { |
| 27 | resolve(res); |
| 28 | }); |
| 29 | req.on('error', e => { |
| 30 | reject(e); |
| 31 | }); |
| 32 | body.pipe(req); |
| 33 | }); |
| 34 | } |
| 35 | |
| 36 | app.all('/', async function (req, res, next) { |
| 37 | // Proxy the request to the regional server. |
| 38 | const proxiedHeaders = { |
| 39 | 'X-Forwarded-Host': req.hostname, |
| 40 | 'X-Forwarded-For': req.ips, |
| 41 | 'X-Forwarded-Port': 3000, |
| 42 | 'X-Forwarded-Proto': req.protocol, |
| 43 | }; |
| 44 | // Proxy other headers as desired. |
| 45 | if (req.get('rsc-action')) { |
| 46 | proxiedHeaders['Content-type'] = req.get('Content-type'); |
| 47 | proxiedHeaders['rsc-action'] = req.get('rsc-action'); |
| 48 | } else if (req.get('Content-type')) { |
| 49 | proxiedHeaders['Content-type'] = req.get('Content-type'); |
| 50 | } |
| 51 | |
| 52 | const promiseForData = request( |
| 53 | { |
| 54 | host: '127.0.0.1', |
| 55 | port: 3001, |
| 56 | method: req.method, |
| 57 | path: '/', |
| 58 | headers: proxiedHeaders, |
| 59 | }, |
| 60 | req |
| 61 | ); |
| 62 | |
| 63 | if (req.accepts('text/html')) { |
| 64 | try { |
| 65 | const rscResponse = await promiseForData; |
| 66 | const moduleBaseURL = '/src'; |
| 67 | |
| 68 | // For HTML, we're a "client" emulator that runs the client code, |
| 69 | // so we start by consuming the RSC payload. This needs the local file path |
| 70 | // to load the source files from as well as the URL path for preloads. |
| 71 | |
| 72 | let root; |
| 73 | let Root = () => { |
| 74 | if (root) { |
| 75 | return React.use(root); |
| 76 | } |
| 77 | |
| 78 | return React.use( |
| 79 | (root = createFromNodeStream( |
| 80 | rscResponse, |
| 81 | moduleBasePath, |
| 82 | moduleBaseURL |
| 83 | )) |
| 84 | ); |
| 85 | }; |
| 86 | // Render it into HTML by resolving the client components |
| 87 | res.set('Content-type', 'text/html'); |
| 88 | const {pipe} = renderToPipeableStream(React.createElement(Root), { |
| 89 | importMap: { |
| 90 | imports: { |
| 91 | react: 'https://esm.sh/react@experimental?pin=v124&dev', |
| 92 | 'react-dom': 'https://esm.sh/react-dom@experimental?pin=v124&dev', |
| 93 | 'react-dom/': 'https://esm.sh/react-dom@experimental&pin=v124&dev/', |
| 94 | 'react-server-dom-esm/client': |
| 95 | '/node_modules/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js', |
| 96 | }, |
| 97 | }, |
| 98 | bootstrapModules: ['/src/index.js'], |
| 99 | }); |
| 100 | pipe(res); |
| 101 | } catch (e) { |
| 102 | console.error(`Failed to SSR: ${e.stack}`); |
| 103 | res.statusCode = 500; |
| 104 | res.end(); |
| 105 | } |
| 106 | } else { |
| 107 | try { |
| 108 | const rscResponse = await promiseForData; |
| 109 | |
| 110 | // For other request, we pass-through the RSC payload. |
| 111 | res.set('Content-type', 'text/x-component'); |
| 112 | rscResponse.on('data', data => { |
| 113 | res.write(data); |
| 114 | res.flush(); |
| 115 | }); |
| 116 | rscResponse.on('end', data => { |
| 117 | res.end(); |
| 118 | }); |
| 119 | } catch (e) { |
| 120 | console.error(`Failed to proxy request: ${e.stack}`); |
| 121 | res.statusCode = 500; |
| 122 | res.end(); |
| 123 | } |
| 124 | } |
| 125 | }); |
| 126 | |
| 127 | app.use(express.static('public')); |
| 128 | app.use('/src', express.static('src')); |
| 129 | app.use( |
| 130 | '/node_modules/react-server-dom-esm/esm', |
| 131 | express.static('node_modules/react-server-dom-esm/esm') |
| 132 | ); |
| 133 | |
| 134 | if (process.env.NODE_ENV === 'development') { |
| 135 | app.get('/source-maps', async function (req, res, next) { |
| 136 | // Proxy the request to the regional server. |
| 137 | const proxiedHeaders = { |
| 138 | 'X-Forwarded-Host': req.hostname, |
| 139 | 'X-Forwarded-For': req.ips, |
| 140 | 'X-Forwarded-Port': 3000, |
| 141 | 'X-Forwarded-Proto': req.protocol, |
| 142 | }; |
| 143 | |
| 144 | const promiseForData = request( |
| 145 | { |
| 146 | host: '127.0.0.1', |
| 147 | port: 3001, |
| 148 | method: req.method, |
| 149 | path: req.originalUrl, |
| 150 | headers: proxiedHeaders, |
| 151 | }, |
| 152 | req |
| 153 | ); |
| 154 | |
| 155 | try { |
| 156 | const rscResponse = await promiseForData; |
| 157 | res.set('Content-type', 'application/json'); |
| 158 | rscResponse.on('data', data => { |
| 159 | res.write(data); |
| 160 | res.flush(); |
| 161 | }); |
| 162 | rscResponse.on('end', data => { |
| 163 | res.end(); |
| 164 | }); |
| 165 | } catch (e) { |
| 166 | console.error(`Failed to proxy request: ${e.stack}`); |
| 167 | res.statusCode = 500; |
| 168 | res.end(); |
| 169 | } |
| 170 | }); |
| 171 | } |
| 172 | |
| 173 | app.listen(3000, () => { |
| 174 | console.log('Global Fizz/Webpack Server listening on port 3000...'); |
| 175 | }); |
| 176 | |
| 177 | app.on('error', function (error) { |
| 178 | if (error.syscall !== 'listen') { |
| 179 | throw error; |
| 180 | } |
| 181 | |
| 182 | switch (error.code) { |
| 183 | case 'EACCES': |
| 184 | console.error('port 3000 requires elevated privileges'); |
| 185 | process.exit(1); |
| 186 | break; |
| 187 | case 'EADDRINUSE': |
| 188 | console.error('Port 3000 is already in use'); |
| 189 | process.exit(1); |
| 190 | break; |
| 191 | default: |
| 192 | throw error; |
| 193 | } |
| 194 | }); |