Skip to content

Commit 2eea307

Browse files
committed
SSR Flight Fixture with CSS
1 parent bca7023 commit 2eea307

File tree

12 files changed

+237
-159
lines changed

12 files changed

+237
-159
lines changed

fixtures/flight/.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
# production
1212
/build
13-
/dist
1413
.eslintcache
1514

1615
# misc

fixtures/flight/config/paths.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ module.exports = {
5656
appPath: resolveApp('.'),
5757
appBuild: resolveApp(buildPath),
5858
appPublic: resolveApp('public'),
59-
appHtml: resolveApp('public/index.html'),
6059
appIndexJs: resolveModule(resolveApp, 'src/index'),
6160
appPackageJson: resolveApp('package.json'),
6261
appSrc: resolveApp('src'),

fixtures/flight/config/webpack.config.js

Lines changed: 36 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,10 @@ const {createHash} = require('crypto');
99
const path = require('path');
1010
const webpack = require('webpack');
1111
const resolve = require('resolve');
12-
const HtmlWebpackPlugin = require('html-webpack-plugin');
1312
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
14-
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
1513
const TerserPlugin = require('terser-webpack-plugin');
1614
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
1715
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
18-
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
1916
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
2017
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
2118
const ESLintPlugin = require('eslint-webpack-plugin');
@@ -28,6 +25,7 @@ const ForkTsCheckerWebpackPlugin =
2825
? require('react-dev-utils/ForkTsCheckerWarningWebpackPlugin')
2926
: require('react-dev-utils/ForkTsCheckerWebpackPlugin');
3027
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
28+
const {WebpackManifestPlugin} = require('webpack-manifest-plugin');
3129

3230
function createEnvironmentHash(env) {
3331
const hash = createHash('md5');
@@ -116,7 +114,7 @@ module.exports = function (webpackEnv) {
116114
const getStyleLoaders = (cssOptions, preProcessor) => {
117115
const loaders = [
118116
isEnvDevelopment && require.resolve('style-loader'),
119-
isEnvProduction && {
117+
{
120118
loader: MiniCssExtractPlugin.loader,
121119
// css is located in `static/css`, use '../../' to locate index.html folder
122120
// in production `paths.publicUrlOrPath` can be a relative path
@@ -578,44 +576,6 @@ module.exports = function (webpackEnv) {
578576
},
579577
plugins: [
580578
new webpack.HotModuleReplacementPlugin(),
581-
// Generates an `index.html` file with the <script> injected.
582-
new HtmlWebpackPlugin(
583-
Object.assign(
584-
{},
585-
{
586-
inject: true,
587-
template: paths.appHtml,
588-
},
589-
isEnvProduction
590-
? {
591-
minify: {
592-
removeComments: true,
593-
collapseWhitespace: true,
594-
removeRedundantAttributes: true,
595-
useShortDoctype: true,
596-
removeEmptyAttributes: true,
597-
removeStyleLinkTypeAttributes: true,
598-
keepClosingSlash: true,
599-
minifyJS: true,
600-
minifyCSS: true,
601-
minifyURLs: true,
602-
},
603-
}
604-
: undefined
605-
)
606-
),
607-
// Inlines the webpack runtime script. This script is too small to warrant
608-
// a network request.
609-
// https://github.com/facebook/create-react-app/issues/5358
610-
isEnvProduction &&
611-
shouldInlineRuntimeChunk &&
612-
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
613-
// Makes some environment variables available in index.html.
614-
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
615-
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
616-
// It will be an empty string unless you specify "homepage"
617-
// in `package.json`, in which case it will be the pathname of that URL.
618-
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
619579
// This gives some necessary context to module not found errors, such as
620580
// the requesting resource.
621581
new ModuleNotFoundPlugin(paths.appPath),
@@ -636,13 +596,40 @@ module.exports = function (webpackEnv) {
636596
// a plugin that prints an error when you attempt to do this.
637597
// See https://github.com/facebook/create-react-app/issues/240
638598
isEnvDevelopment && new CaseSensitivePathsPlugin(),
639-
isEnvProduction &&
640-
new MiniCssExtractPlugin({
641-
// Options similar to the same options in webpackOptions.output
642-
// both options are optional
643-
filename: 'static/css/[name].[contenthash:8].css',
644-
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
645-
}),
599+
new MiniCssExtractPlugin({
600+
// Options similar to the same options in webpackOptions.output
601+
// both options are optional
602+
filename: 'static/css/[name].[contenthash:8].css',
603+
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
604+
}),
605+
// Generate a manifest containing the required script / css for each entry.
606+
new WebpackManifestPlugin({
607+
fileName: 'entrypoint-manifest.json',
608+
publicPath: paths.publicUrlOrPath,
609+
generate: (seed, files, entrypoints) => {
610+
const entrypointFiles = entrypoints.main.filter(
611+
fileName => !fileName.endsWith('.map')
612+
);
613+
614+
const processedEntrypoints = {};
615+
for (let key in entrypoints) {
616+
processedEntrypoints[key] = {
617+
js: entrypoints[key].filter(
618+
filename =>
619+
// Include JS assets but ignore hot updates because they're not
620+
// safe to include as async script tags.
621+
filename.endsWith('.js') &&
622+
!filename.endsWith('.hot-update.js')
623+
),
624+
css: entrypoints[key].filter(filename =>
625+
filename.endsWith('.css')
626+
),
627+
};
628+
}
629+
630+
return processedEntrypoints;
631+
},
632+
}),
646633
// Moment.js is an extremely popular library that bundles large locale files
647634
// by default due to how webpack interprets its code. This is a practical
648635
// solution that requires the user to opt into importing specific locales.

fixtures/flight/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@
4343
"postcss-normalize": "^10.0.1",
4444
"postcss-preset-env": "^7.0.1",
4545
"prompts": "^2.4.2",
46-
"react": "^18.2.0",
46+
"react": "experimental",
4747
"react-app-polyfill": "^3.0.0",
4848
"react-dev-utils": "^12.0.1",
49-
"react-dom": "^18.2.0",
49+
"react-dom": "experimental",
5050
"react-refresh": "^0.11.0",
5151
"resolve": "^1.20.0",
5252
"resolve-url-loader": "^4.0.0",
@@ -59,7 +59,8 @@
5959
"undici": "^5.20.0",
6060
"webpack": "^5.64.4",
6161
"webpack-dev-middleware": "^5.3.1",
62-
"webpack-hot-middleware": "^2.25.3"
62+
"webpack-hot-middleware": "^2.25.3",
63+
"webpack-manifest-plugin": "^4.0.2"
6364
},
6465
"scripts": {
6566
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/",

fixtures/flight/public/index.html

Lines changed: 0 additions & 11 deletions
This file was deleted.

fixtures/flight/scripts/build.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
3838
const isInteractive = process.stdout.isTTY;
3939

4040
// Warn and crash if required files are missing
41-
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
41+
if (!checkRequiredFiles([paths.appIndexJs])) {
4242
process.exit(1);
4343
}
4444

@@ -204,6 +204,5 @@ function build(previousFileSizes) {
204204
function copyPublicFolder() {
205205
fs.copySync('public', 'build', {
206206
dereference: true,
207-
filter: file => file !== paths.appHtml,
208207
});
209208
}

fixtures/flight/server/global.js

Lines changed: 86 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,45 @@ babelRegister({
3232
// Ensure environment variables are read.
3333
require('../config/env');
3434

35-
const fs = require('fs');
35+
const fs = require('fs').promises;
3636
const compress = require('compression');
3737
const chalk = require('chalk');
3838
const express = require('express');
39-
const app = express();
40-
4139
const http = require('http');
4240

41+
const {renderToPipeableStream} = require('react-dom/server');
42+
const {createFromNodeStream} = require('react-server-dom-webpack/client');
43+
44+
const app = express();
45+
4346
app.use(compress());
4447

48+
if (process.env.NODE_ENV === 'development') {
49+
// In development we host the Webpack server for live bundling.
50+
const webpack = require('webpack');
51+
const webpackMiddleware = require('webpack-dev-middleware');
52+
const webpackHotMiddleware = require('webpack-hot-middleware');
53+
const paths = require('../config/paths');
54+
const configFactory = require('../config/webpack.config');
55+
const getClientEnvironment = require('../config/env');
56+
57+
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
58+
59+
const config = configFactory('development');
60+
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
61+
const appName = require(paths.appPackageJson).name;
62+
63+
// Create a webpack compiler that is configured with custom messages.
64+
const compiler = webpack(config);
65+
app.use(
66+
webpackMiddleware(compiler, {
67+
publicPath: paths.publicUrlOrPath.slice(0, -1),
68+
serverSideRender: true,
69+
})
70+
);
71+
app.use(webpackHotMiddleware(compiler));
72+
}
73+
4574
function request(options, body) {
4675
return new Promise((resolve, reject) => {
4776
const req = http.request(options, res => {
@@ -55,12 +84,6 @@ function request(options, body) {
5584
}
5685

5786
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-
6487
// Proxy the request to the regional server.
6588
const proxiedHeaders = {
6689
'X-Forwarded-Host': req.hostname,
@@ -85,52 +108,64 @@ app.all('/', async function (req, res, next) {
85108
req
86109
);
87110

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();
111+
if (req.accepts('text/html')) {
112+
try {
113+
const rscResponse = await promiseForData;
114+
115+
let virtualFs;
116+
let buildPath;
117+
if (process.env.NODE_ENV === 'development') {
118+
const {devMiddleware} = res.locals.webpack;
119+
virtualFs = devMiddleware.outputFileSystem.promises;
120+
buildPath = devMiddleware.stats.toJson().outputPath;
121+
} else {
122+
virtualFs = fs;
123+
buildPath = path.join(__dirname, '../build/');
124+
}
125+
// Read the module map from the virtual file system.
126+
const moduleMap = JSON.parse(
127+
await virtualFs.readFile(
128+
path.join(buildPath, 'react-ssr-manifest.json'),
129+
'utf8'
130+
)
131+
);
132+
// Read the initial
133+
const mainJSChunks = JSON.parse(
134+
await virtualFs.readFile(
135+
path.join(buildPath, 'entrypoint-manifest.json'),
136+
'utf8'
137+
)
138+
).main.js;
139+
// For HTML, we're a "client" emulator that runs the client code,
140+
// so we start by consuming the RSC payload. This needs a module
141+
// map that reverse engineers the client-side path to the SSR path.
142+
const root = await createFromNodeStream(rscResponse, moduleMap);
143+
// Render it into HTML by resolving the client components
144+
res.set('Content-type', 'text/html');
145+
const {pipe} = renderToPipeableStream(root, {
146+
bootstrapScripts: mainJSChunks,
147+
});
148+
pipe(res);
149+
} catch (e) {
150+
console.error(`Failed to SSR: ${e.stack}`);
151+
res.statusCode = 500;
152+
res.end();
153+
}
154+
} else {
155+
try {
156+
const rscResponse = await promiseForData;
157+
// For other request, we pass-through the RSC payload.
158+
res.set('Content-type', 'text/x-component');
159+
rscResponse.pipe(res);
160+
} catch (e) {
161+
console.error(`Failed to proxy request: ${e.stack}`);
162+
res.statusCode = 500;
163+
res.end();
164+
}
96165
}
97166
});
98167

99168
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');
107-
108-
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
109-
110-
const HOST = '0.0.0.0';
111-
const PORT = 3000;
112-
113-
const config = configFactory('development');
114-
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
115-
const appName = require(paths.appPackageJson).name;
116-
117-
// Create a webpack compiler that is configured with custom messages.
118-
const compiler = webpack(config);
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-
);
134169
app.use(express.static('public'));
135170
} else {
136171
// In production we host the static build output.

0 commit comments

Comments
 (0)