Skip to content

Commit

Permalink
SSR Flight Fixture with CSS
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage committed Feb 28, 2023
1 parent 84422a7 commit 21fd7db
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 159 deletions.
1 change: 0 additions & 1 deletion fixtures/flight/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

# production
/build
/dist
.eslintcache

# misc
Expand Down
1 change: 0 additions & 1 deletion fixtures/flight/config/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ module.exports = {
appPath: resolveApp('.'),
appBuild: resolveApp(buildPath),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/index'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
Expand Down
85 changes: 36 additions & 49 deletions fixtures/flight/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,10 @@ const {createHash} = require('crypto');
const path = require('path');
const webpack = require('webpack');
const resolve = require('resolve');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const ESLintPlugin = require('eslint-webpack-plugin');
Expand All @@ -28,6 +25,7 @@ const ForkTsCheckerWebpackPlugin =
? require('react-dev-utils/ForkTsCheckerWarningWebpackPlugin')
: require('react-dev-utils/ForkTsCheckerWebpackPlugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const {WebpackManifestPlugin} = require('webpack-manifest-plugin');

function createEnvironmentHash(env) {
const hash = createHash('md5');
Expand Down Expand Up @@ -116,7 +114,7 @@ module.exports = function (webpackEnv) {
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
{
loader: MiniCssExtractPlugin.loader,
// css is located in `static/css`, use '../../' to locate index.html folder
// in production `paths.publicUrlOrPath` can be a relative path
Expand Down Expand Up @@ -578,44 +576,6 @@ module.exports = function (webpackEnv) {
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
// Inlines the webpack runtime script. This script is too small to warrant
// a network request.
// https://github.com/facebook/create-react-app/issues/5358
isEnvProduction &&
shouldInlineRuntimeChunk &&
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
// Makes some environment variables available in index.html.
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
// It will be an empty string unless you specify "homepage"
// in `package.json`, in which case it will be the pathname of that URL.
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
// This gives some necessary context to module not found errors, such as
// the requesting resource.
new ModuleNotFoundPlugin(paths.appPath),
Expand All @@ -636,13 +596,40 @@ module.exports = function (webpackEnv) {
// a plugin that prints an error when you attempt to do this.
// See https://github.com/facebook/create-react-app/issues/240
isEnvDevelopment && new CaseSensitivePathsPlugin(),
isEnvProduction &&
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}),
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}),
// Generate a manifest containing the required script / css for each entry.
new WebpackManifestPlugin({
fileName: 'entrypoint-manifest.json',
publicPath: paths.publicUrlOrPath,
generate: (seed, files, entrypoints) => {
const entrypointFiles = entrypoints.main.filter(
fileName => !fileName.endsWith('.map')
);

const processedEntrypoints = {};
for (let key in entrypoints) {
processedEntrypoints[key] = {
js: entrypoints[key].filter(
filename =>
// Include JS assets but ignore hot updates because they're not
// safe to include as async script tags.
filename.endsWith('.js') &&
!filename.endsWith('.hot-update.js')
),
css: entrypoints[key].filter(filename =>
filename.endsWith('.css')
),
};
}

return processedEntrypoints;
},
}),
// Moment.js is an extremely popular library that bundles large locale files
// by default due to how webpack interprets its code. This is a practical
// solution that requires the user to opt into importing specific locales.
Expand Down
7 changes: 4 additions & 3 deletions fixtures/flight/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@
"postcss-normalize": "^10.0.1",
"postcss-preset-env": "^7.0.1",
"prompts": "^2.4.2",
"react": "^18.2.0",
"react": "experimental",
"react-app-polyfill": "^3.0.0",
"react-dev-utils": "^12.0.1",
"react-dom": "^18.2.0",
"react-dom": "experimental",
"react-refresh": "^0.11.0",
"resolve": "^1.20.0",
"resolve-url-loader": "^4.0.0",
Expand All @@ -59,7 +59,8 @@
"undici": "^5.20.0",
"webpack": "^5.64.4",
"webpack-dev-middleware": "^5.3.1",
"webpack-hot-middleware": "^2.25.3"
"webpack-hot-middleware": "^2.25.3",
"webpack-manifest-plugin": "^4.0.2"
},
"scripts": {
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/",
Expand Down
11 changes: 0 additions & 11 deletions fixtures/flight/public/index.html

This file was deleted.

3 changes: 1 addition & 2 deletions fixtures/flight/scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
const isInteractive = process.stdout.isTTY;

// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
if (!checkRequiredFiles([paths.appIndexJs])) {
process.exit(1);
}

Expand Down Expand Up @@ -204,6 +204,5 @@ function build(previousFileSizes) {
function copyPublicFolder() {
fs.copySync('public', 'build', {
dereference: true,
filter: file => file !== paths.appHtml,
});
}
137 changes: 86 additions & 51 deletions fixtures/flight/server/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,45 @@ babelRegister({
// Ensure environment variables are read.
require('../config/env');

const fs = require('fs');
const fs = require('fs').promises;
const compress = require('compression');
const chalk = require('chalk');
const express = require('express');
const app = express();

const http = require('http');

const {renderToPipeableStream} = require('react-dom/server');
const {createFromNodeStream} = require('react-server-dom-webpack/client');

const app = express();

app.use(compress());

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');

const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));

const config = configFactory('development');
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const appName = require(paths.appPackageJson).name;

// Create a webpack compiler that is configured with custom messages.
const compiler = webpack(config);
app.use(
webpackMiddleware(compiler, {
publicPath: paths.publicUrlOrPath.slice(0, -1),
serverSideRender: true,
})
);
app.use(webpackHotMiddleware(compiler));
}

function request(options, body) {
return new Promise((resolve, reject) => {
const req = http.request(options, res => {
Expand All @@ -55,12 +84,6 @@ function request(options, body) {
}

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,
Expand All @@ -85,52 +108,64 @@ app.all('/', async function (req, res, next) {
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 (req.accepts('text/html')) {
try {
const rscResponse = await promiseForData;

let virtualFs;
let buildPath;
if (process.env.NODE_ENV === 'development') {
const {devMiddleware} = res.locals.webpack;
virtualFs = devMiddleware.outputFileSystem.promises;
buildPath = devMiddleware.stats.toJson().outputPath;
} else {
virtualFs = fs;
buildPath = path.join(__dirname, '../build/');
}
// Read the module map from the virtual file system.
const moduleMap = JSON.parse(
await virtualFs.readFile(
path.join(buildPath, 'react-ssr-manifest.json'),
'utf8'
)
);
// Read the initial
const mainJSChunks = JSON.parse(
await virtualFs.readFile(
path.join(buildPath, 'entrypoint-manifest.json'),
'utf8'
)
).main.js;
// For HTML, we're a "client" emulator that runs the client code,
// so we start by consuming the RSC payload. This needs a module
// map that reverse engineers the client-side path to the SSR path.
const root = await createFromNodeStream(rscResponse, moduleMap);
// Render it into HTML by resolving the client components
res.set('Content-type', 'text/html');
const {pipe} = renderToPipeableStream(root, {
bootstrapScripts: mainJSChunks,
});
pipe(res);
} catch (e) {
console.error(`Failed to SSR: ${e.stack}`);
res.statusCode = 500;
res.end();
}
} else {
try {
const rscResponse = await promiseForData;
// For other request, we pass-through the RSC payload.
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');

const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));

const HOST = '0.0.0.0';
const PORT = 3000;

const config = configFactory('development');
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const appName = require(paths.appPackageJson).name;

// Create a webpack compiler that is configured with custom messages.
const compiler = webpack(config);
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.
Expand Down
Loading

0 comments on commit 21fd7db

Please sign in to comment.