[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;