[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.
This commit is contained in:
Sebastian Markbåge
2023-02-28 19:24:16 -05:00
committed by GitHub
parent 1175c77cf2
commit e1afa557e7
8 changed files with 160 additions and 38 deletions
+8 -1
View File
@@ -208,7 +208,13 @@ module.exports = function (webpackEnv) {
: isEnvDevelopment && 'cheap-module-source-map',
// These are the "entry points" to our application.
// This means they will be the "root" imports that are included in JS bundle.
entry: paths.appIndexJs,
entry: isEnvProduction
? [paths.appIndexJs]
: [
paths.appIndexJs,
// HMR client
'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000',
],
output: {
// The build folder.
path: paths.appBuild,
@@ -571,6 +577,7 @@ module.exports = function (webpackEnv) {
].filter(Boolean),
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
+3 -1
View File
@@ -56,8 +56,10 @@
"style-loader": "^3.3.1",
"tailwindcss": "^3.0.2",
"terser-webpack-plugin": "^5.2.5",
"undici": "^5.20.0",
"webpack": "^5.64.4",
"webpack-dev-middleware": "^5.3.1"
"webpack-dev-middleware": "^5.3.1",
"webpack-hot-middleware": "^2.25.3"
},
"scripts": {
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/",
+72 -7
View File
@@ -38,12 +38,69 @@ const chalk = require('chalk');
const express = require('express');
const app = express();
const http = require('http');
app.use(compress());
function request(options, body) {
return new Promise((resolve, reject) => {
const req = http.request(options, res => {
resolve(res);
});
req.on('error', e => {
reject(e);
});
body.pipe(req);
});
}
app.all('/', async function (req, res, next) {
if (req.accepts('text/html')) {
// Pass-through to the html file
next();
return;
}
// Proxy the request to the regional server.
const proxiedHeaders = {
'X-Forwarded-Host': req.hostname,
'X-Forwarded-For': req.ips,
'X-Forwarded-Port': 3000,
'X-Forwarded-Proto': req.protocol,
};
// Proxy other headers as desired.
if (req.get('rsc-action')) {
proxiedHeaders['Content-type'] = req.get('Content-type');
proxiedHeaders['rsc-action'] = req.get('rsc-action');
}
const promiseForData = request(
{
host: '127.0.0.1',
port: 3001,
method: req.method,
path: '/',
headers: proxiedHeaders,
},
req
);
try {
const rscResponse = await promiseForData;
res.set('Content-type', 'text/x-component');
rscResponse.pipe(res);
} catch (e) {
console.error(`Failed to proxy request: ${e.stack}`);
res.statusCode = 500;
res.end();
}
});
if (process.env.NODE_ENV === 'development') {
// In development we host the Webpack server for live bundling.
const webpack = require('webpack');
const webpackMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
const getClientEnvironment = require('../config/env');
@@ -59,13 +116,21 @@ if (process.env.NODE_ENV === 'development') {
// Create a webpack compiler that is configured with custom messages.
const compiler = webpack(config);
const devMiddleware = {
writeToDisk: filePath => {
return /(react-client-manifest|react-ssr-manifest)\.json$/.test(filePath);
},
publicPath: paths.publicUrlOrPath.slice(0, -1),
};
app.use(webpackMiddleware(compiler, devMiddleware));
app.use(
webpackMiddleware(compiler, {
writeToDisk: filePath => {
return /(react-client-manifest|react-ssr-manifest)\.json$/.test(
filePath
);
},
publicPath: paths.publicUrlOrPath.slice(0, -1),
})
);
app.use(
webpackHotMiddleware(compiler, {
/* Options */
})
);
app.use(express.static('public'));
} else {
// In production we host the static build output.
+5 -10
View File
@@ -26,6 +26,11 @@ babelRegister({
plugins: ['@babel/transform-modules-commonjs'],
});
if (typeof fetch === 'undefined') {
// Patch fetch for earlier Node versions.
global.fetch = require('undici').fetch;
}
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
@@ -51,19 +56,11 @@ app.get('/', async function (req, res) {
'utf8'
);
const App = m.default.default || m.default;
res.setHeader('Access-Control-Allow-Origin', '*');
const moduleMap = JSON.parse(data);
const {pipe} = renderToPipeableStream(React.createElement(App), moduleMap);
pipe(res);
});
app.options('/', function (req, res) {
res.setHeader('Allow', 'Allow: GET,HEAD,POST');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', 'rsc-action');
res.end();
});
app.post('/', bodyParser.text(), async function (req, res) {
const {renderToPipeableStream} = await import(
'react-server-dom-webpack/server'
@@ -81,13 +78,11 @@ app.post('/', bodyParser.text(), async function (req, res) {
const args = JSON.parse(req.body);
const result = action.apply(null, args);
res.setHeader('Access-Control-Allow-Origin', '*');
const {pipe} = renderToPipeableStream(result, {});
pipe(res);
});
app.get('/todos', function (req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.json([
{
id: 1,
+7 -3
View File
@@ -4,13 +4,17 @@ import ReactDOM from 'react-dom/client';
import ReactServerDOMReader from 'react-server-dom-webpack/client';
let data = ReactServerDOMReader.createFromFetch(
fetch('http://localhost:3001'),
fetch('/', {
headers: {
Accept: 'text/x-component',
},
}),
{
callServer(id, args) {
const response = fetch('http://localhost:3001', {
const response = fetch('/', {
method: 'POST',
cors: 'cors',
headers: {
Accept: 'text/x-component',
'rsc-action': JSON.stringify({filepath: id.id, name: id.name}),
},
body: JSON.stringify(args),
+29 -1
View File
@@ -2651,7 +2651,7 @@ ansi-escapes@^4.3.1:
dependencies:
type-fest "^0.11.0"
ansi-html-community@^0.0.8:
[email protected], ansi-html-community@^0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41"
integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==
@@ -3061,6 +3061,13 @@ buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
busboy@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
dependencies:
streamsearch "^1.1.0"
[email protected]:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@@ -7903,6 +7910,11 @@ [email protected]:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
streamsearch@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
string-length@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1"
@@ -8388,6 +8400,13 @@ undefsafe@^2.0.5:
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
undici@^5.20.0:
version "5.20.0"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.20.0.tgz#6327462f5ce1d3646bcdac99da7317f455bcc263"
integrity sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==
dependencies:
busboy "^1.6.0"
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
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:
range-parser "^1.2.1"
schema-utils "^4.0.0"
webpack-hot-middleware@^2.25.3:
version "2.25.3"
resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.25.3.tgz#be343ce2848022cfd854dd82820cd730998c6794"
integrity sha512-IK/0WAHs7MTu1tzLTjio73LjS3Ov+VvBKQmE8WPlJutgG5zT6Urgq/BbAdRrHTRpyzK0dvAvFh1Qg98akxgZpA==
dependencies:
ansi-html-community "0.0.8"
html-entities "^2.1.0"
strip-ansi "^6.0.0"
webpack-sources@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
@@ -266,10 +266,17 @@ async function parseExportNamesInto(
if (typeof source !== 'string') {
throw new Error('Expected the transformed source to be a string.');
}
const {body: childBody} = acorn.parse(source, {
ecmaVersion: '2019',
sourceType: 'module',
});
let childBody;
try {
childBody = acorn.parse(source, {
ecmaVersion: '2024',
sourceType: 'module',
}).body;
} catch (x) {
// eslint-disable-next-line react-internal/no-production-logging
console.error('Error parsing %s %s', url, x.message);
continue;
}
await parseExportNamesInto(childBody, names, url, loader);
continue;
}
@@ -381,10 +388,17 @@ async function transformModuleIfNeeded(
return source;
}
const {body} = acorn.parse(source, {
ecmaVersion: '2019',
sourceType: 'module',
});
let body;
try {
body = acorn.parse(source, {
ecmaVersion: '2024',
sourceType: 'module',
}).body;
} catch (x) {
// eslint-disable-next-line react-internal/no-production-logging
console.error('Error parsing %s %s', url, x.message);
return source;
}
let useClient = false;
let useServer = false;
@@ -450,8 +464,8 @@ export async function load(
context: LoadContext,
defaultLoad: LoadFunction,
): Promise<{format: string, shortCircuit?: boolean, source: Source}> {
if (context.format === 'module') {
const result = await defaultLoad(url, context, defaultLoad);
const result = await defaultLoad(url, context, defaultLoad);
if (result.format === 'module') {
if (typeof result.source !== 'string') {
throw new Error('Expected source to have been loaded into a string.');
}
@@ -462,5 +476,5 @@ export async function load(
);
return {format: 'module', source: newSrc};
}
return defaultLoad(url, context, defaultLoad);
return result;
}
@@ -244,10 +244,17 @@ module.exports = function register() {
return originalCompile.apply(this, arguments);
}
const {body} = acorn.parse(content, {
ecmaVersion: '2019',
sourceType: 'source',
});
let body;
try {
body = acorn.parse(content, {
ecmaVersion: '2024',
sourceType: 'source',
}).body;
} catch (x) {
// eslint-disable-next-line react-internal/no-production-logging
console.error('Error parsing %s %s', url, x.message);
return originalCompile.apply(this, arguments);
}
let useClient = false;
let useServer = false;