@samitouri / QOS-React / commits / 40755c01a6

[Flight Fixture] Proxy requests through the global server instead of directly (#26257)

This proxies requests through the global server instead of requesting RSC responses from the regional server. This is a bit closer to idiomatic, and closer to SSR. This also wires up HMR using the Middleware technique instead of server. This will be an important part of RSC compatibility because there will be a `react-refresh` aspect to the integration. This convention uses `Accept` header to branch a URL between HTML/RSC but it could be anything really. Special headers, URLs etc. We might be more opinionated about this in the future but now it's up to the router. Some fixes for Node 16/17 support in the loader and fetch polyfill.

Sebastian Markbåge committed Feb 28, 2023 at 19:24 UTC 40755c01a68de4b44bc24cca7f03a46140f31d5e
8 files changed +160 -38
fixtures/flight/config/webpack.config.js
+8 -1
@@ -208,7 +208,13 @@ module.exports = function (webpackEnv) {
208 : isEnvDevelopment && 'cheap-module-source-map',
209 // These are the "entry points" to our application.
210 // This means they will be the "root" imports that are included in JS bundle.
211 - entry: paths.appIndexJs,
211 + entry: isEnvProduction
212 + ? [paths.appIndexJs]
213 + : [
214 + paths.appIndexJs,
215 + // HMR client
216 + 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000',
217 + ],
218 output: {
219 // The build folder.
220 path: paths.appBuild,
@@ -571,6 +577,7 @@ module.exports = function (webpackEnv) {
577 ].filter(Boolean),
578 },
579 plugins: [
580 + new webpack.HotModuleReplacementPlugin(),
581 // Generates an `index.html` file with the <script> injected.
582 new HtmlWebpackPlugin(
583 Object.assign(
fixtures/flight/package.json
+3 -1
@@ -56,8 +56,10 @@
56 "style-loader": "^3.3.1",
57 "tailwindcss": "^3.0.2",
58 "terser-webpack-plugin": "^5.2.5",
59 + "undici": "^5.20.0",
60 "webpack": "^5.64.4",
60 - "webpack-dev-middleware": "^5.3.1"
61 + "webpack-dev-middleware": "^5.3.1",
62 + "webpack-hot-middleware": "^2.25.3"
63 },
64 "scripts": {
65 "predev": "cp -r ../../build/oss-experimental/* ./node_modules/",
fixtures/flight/server/global.js
+72 -7
@@ -38,12 +38,69 @@ const chalk = require('chalk');
38 const express = require('express');
39 const app = express();
40
41 +const http = require('http');
42 +
43 app.use(compress());
44
45 +function request(options, body) {
46 + return new Promise((resolve, reject) => {
47 + const req = http.request(options, res => {
48 + resolve(res);
49 + });
50 + req.on('error', e => {
51 + reject(e);
52 + });
53 + body.pipe(req);
54 + });
55 +}
56 +
57 +app.all('/', async function (req, res, next) {
58 + if (req.accepts('text/html')) {
59 + // Pass-through to the html file
60 + next();
61 + return;
62 + }
63 +
64 + // Proxy the request to the regional server.
65 + const proxiedHeaders = {
66 + 'X-Forwarded-Host': req.hostname,
67 + 'X-Forwarded-For': req.ips,
68 + 'X-Forwarded-Port': 3000,
69 + 'X-Forwarded-Proto': req.protocol,
70 + };
71 + // Proxy other headers as desired.
72 + if (req.get('rsc-action')) {
73 + proxiedHeaders['Content-type'] = req.get('Content-type');
74 + proxiedHeaders['rsc-action'] = req.get('rsc-action');
75 + }
76 +
77 + const promiseForData = request(
78 + {
79 + host: '127.0.0.1',
80 + port: 3001,
81 + method: req.method,
82 + path: '/',
83 + headers: proxiedHeaders,
84 + },
85 + req
86 + );
87 +
88 + try {
89 + const rscResponse = await promiseForData;
90 + res.set('Content-type', 'text/x-component');
91 + rscResponse.pipe(res);
92 + } catch (e) {
93 + console.error(`Failed to proxy request: ${e.stack}`);
94 + res.statusCode = 500;
95 + res.end();
96 + }
97 +});
98 +
99 if (process.env.NODE_ENV === 'development') {
100 // In development we host the Webpack server for live bundling.
101 const webpack = require('webpack');
102 const webpackMiddleware = require('webpack-dev-middleware');
103 + const webpackHotMiddleware = require('webpack-hot-middleware');
104 const paths = require('../config/paths');
105 const configFactory = require('../config/webpack.config');
106 const getClientEnvironment = require('../config/env');
@@ -59,13 +116,21 @@ if (process.env.NODE_ENV === 'development') {
116
117 // Create a webpack compiler that is configured with custom messages.
118 const compiler = webpack(config);
62 - const devMiddleware = {
63 - writeToDisk: filePath => {
64 - return /(react-client-manifest|react-ssr-manifest)\.json$/.test(filePath);
65 - },
66 - publicPath: paths.publicUrlOrPath.slice(0, -1),
67 - };
68 - app.use(webpackMiddleware(compiler, devMiddleware));
119 + app.use(
120 + webpackMiddleware(compiler, {
121 + writeToDisk: filePath => {
122 + return /(react-client-manifest|react-ssr-manifest)\.json$/.test(
123 + filePath
124 + );
125 + },
126 + publicPath: paths.publicUrlOrPath.slice(0, -1),
127 + })
128 + );
129 + app.use(
130 + webpackHotMiddleware(compiler, {
131 + /* Options */
132 + })
133 + );
134 app.use(express.static('public'));
135 } else {
136 // In production we host the static build output.
fixtures/flight/server/region.js
+5 -10
@@ -26,6 +26,11 @@ babelRegister({
26 plugins: ['@babel/transform-modules-commonjs'],
27 });
28
29 +if (typeof fetch === 'undefined') {
30 + // Patch fetch for earlier Node versions.
31 + global.fetch = require('undici').fetch;
32 +}
33 +
34 const express = require('express');
35 const bodyParser = require('body-parser');
36 const app = express();
@@ -51,19 +56,11 @@ app.get('/', async function (req, res) {
56 'utf8'
57 );
58 const App = m.default.default || m.default;
54 - res.setHeader('Access-Control-Allow-Origin', '*');
59 const moduleMap = JSON.parse(data);
60 const {pipe} = renderToPipeableStream(React.createElement(App), moduleMap);
61 pipe(res);
62 });
63
60 -app.options('/', function (req, res) {
61 - res.setHeader('Allow', 'Allow: GET,HEAD,POST');
62 - res.setHeader('Access-Control-Allow-Origin', '*');
63 - res.setHeader('Access-Control-Allow-Headers', 'rsc-action');
64 - res.end();
65 -});
66 -
64 app.post('/', bodyParser.text(), async function (req, res) {
65 const {renderToPipeableStream} = await import(
66 'react-server-dom-webpack/server'
@@ -81,13 +78,11 @@ app.post('/', bodyParser.text(), async function (req, res) {
78 const args = JSON.parse(req.body);
79 const result = action.apply(null, args);
80
84 - res.setHeader('Access-Control-Allow-Origin', '*');
81 const {pipe} = renderToPipeableStream(result, {});
82 pipe(res);
83 });
84
85 app.get('/todos', function (req, res) {
90 - res.setHeader('Access-Control-Allow-Origin', '*');
86 res.json([
87 {
88 id: 1,
fixtures/flight/src/index.js
+7 -3
@@ -4,13 +4,17 @@ import ReactDOM from 'react-dom/client';
4 import ReactServerDOMReader from 'react-server-dom-webpack/client';
5
6 let data = ReactServerDOMReader.createFromFetch(
7 - fetch('http://localhost:3001'),
7 + fetch('/', {
8 + headers: {
9 + Accept: 'text/x-component',
10 + },
11 + }),
12 {
13 callServer(id, args) {
10 - const response = fetch('http://localhost:3001', {
14 + const response = fetch('/', {
15 method: 'POST',
12 - cors: 'cors',
16 headers: {
17 + Accept: 'text/x-component',
18 'rsc-action': JSON.stringify({filepath: id.id, name: id.name}),
19 },
20 body: JSON.stringify(args),
fixtures/flight/yarn.lock
+29 -1
@@ -2651,7 +2651,7 @@ ansi-escapes@^4.3.1:
2651 dependencies:
2652 type-fest "^0.11.0"
2653
2654 -ansi-html-community@^0.0.8:
2654 +ansi-html-community@0.0.8, ansi-html-community@^0.0.8:
2655 version "0.0.8"
2656 resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41"
2657 integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==
@@ -3061,6 +3061,13 @@ buffer-from@^1.0.0:
3061 version "1.1.1"
3062 resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
3063
3064 +busboy@^1.6.0:
3065 + version "1.6.0"
3066 + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
3067 + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
3068 + dependencies:
3069 + streamsearch "^1.1.0"
3070 +
3071 bytes@3.0.0:
3072 version "3.0.0"
3073 resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@@ -7903,6 +7910,11 @@ statuses@2.0.1:
7910 resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
7911 integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
7912
7913 +streamsearch@^1.1.0:
7914 + version "1.1.0"
7915 + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
7916 + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
7917 +
7918 string-length@^4.0.1:
7919 version "4.0.1"
7920 resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1"
@@ -8388,6 +8400,13 @@ undefsafe@^2.0.5:
8400 resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
8401 integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
8402
8403 +undici@^5.20.0:
8404 + version "5.20.0"
8405 + resolved "https://registry.yarnpkg.com/undici/-/undici-5.20.0.tgz#6327462f5ce1d3646bcdac99da7317f455bcc263"
8406 + integrity sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==
8407 + dependencies:
8408 + busboy "^1.6.0"
8409 +
8410 unicode-canonical-property-names-ecmascript@^1.0.4:
8411 version "1.0.4"
8412 resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
@@ -8548,6 +8567,15 @@ webpack-dev-middleware@^5.3.1:
8567 range-parser "^1.2.1"
8568 schema-utils "^4.0.0"
8569
8570 +webpack-hot-middleware@^2.25.3:
8571 + version "2.25.3"
8572 + resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.25.3.tgz#be343ce2848022cfd854dd82820cd730998c6794"
8573 + integrity sha512-IK/0WAHs7MTu1tzLTjio73LjS3Ov+VvBKQmE8WPlJutgG5zT6Urgq/BbAdRrHTRpyzK0dvAvFh1Qg98akxgZpA==
8574 + dependencies:
8575 + ansi-html-community "0.0.8"
8576 + html-entities "^2.1.0"
8577 + strip-ansi "^6.0.0"
8578 +
8579 webpack-sources@^3.2.3:
8580 version "3.2.3"
8581 resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js
+25 -11
@@ -266,10 +266,17 @@ async function parseExportNamesInto(
266 if (typeof source !== 'string') {
267 throw new Error('Expected the transformed source to be a string.');
268 }
269 - const {body: childBody} = acorn.parse(source, {
270 - ecmaVersion: '2019',
271 - sourceType: 'module',
272 - });
269 + let childBody;
270 + try {
271 + childBody = acorn.parse(source, {
272 + ecmaVersion: '2024',
273 + sourceType: 'module',
274 + }).body;
275 + } catch (x) {
276 + // eslint-disable-next-line react-internal/no-production-logging
277 + console.error('Error parsing %s %s', url, x.message);
278 + continue;
279 + }
280 await parseExportNamesInto(childBody, names, url, loader);
281 continue;
282 }
@@ -381,10 +388,17 @@ async function transformModuleIfNeeded(
388 return source;
389 }
390
384 - const {body} = acorn.parse(source, {
385 - ecmaVersion: '2019',
386 - sourceType: 'module',
387 - });
391 + let body;
392 + try {
393 + body = acorn.parse(source, {
394 + ecmaVersion: '2024',
395 + sourceType: 'module',
396 + }).body;
397 + } catch (x) {
398 + // eslint-disable-next-line react-internal/no-production-logging
399 + console.error('Error parsing %s %s', url, x.message);
400 + return source;
401 + }
402
403 let useClient = false;
404 let useServer = false;
@@ -450,8 +464,8 @@ export async function load(
464 context: LoadContext,
465 defaultLoad: LoadFunction,
466 ): Promise<{format: string, shortCircuit?: boolean, source: Source}> {
453 - if (context.format === 'module') {
454 - const result = await defaultLoad(url, context, defaultLoad);
467 + const result = await defaultLoad(url, context, defaultLoad);
468 + if (result.format === 'module') {
469 if (typeof result.source !== 'string') {
470 throw new Error('Expected source to have been loaded into a string.');
471 }
@@ -462,5 +476,5 @@ export async function load(
476 );
477 return {format: 'module', source: newSrc};
478 }
465 - return defaultLoad(url, context, defaultLoad);
479 + return result;
480 }
packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js
+11 -4
@@ -244,10 +244,17 @@ module.exports = function register() {
244 return originalCompile.apply(this, arguments);
245 }
246
247 - const {body} = acorn.parse(content, {
248 - ecmaVersion: '2019',
249 - sourceType: 'source',
250 - });
247 + let body;
248 + try {
249 + body = acorn.parse(content, {
250 + ecmaVersion: '2024',
251 + sourceType: 'source',
252 + }).body;
253 + } catch (x) {
254 + // eslint-disable-next-line react-internal/no-production-logging
255 + console.error('Error parsing %s %s', url, x.message);
256 + return originalCompile.apply(this, arguments);
257 + }
258
259 let useClient = false;
260 let useServer = false;