Skip to content

Commit 67a61d5

Browse files
authored
[Flight Fixture] Show SSR Support with CSS (#26263)
Builds on #26257. To do this we need access to a manifest for which scripts and CSS are used for each "page" (entrypoint). The initial script to bootstrap the app is inserted with `bootstrapScripts`. Subsequent content are loaded using the chunks mechanism built-in. The stylesheets for each pages are prepended to each RSC payload and rendered using Float. This doesn't yet support styles imported in components that are also SSR:ed nor imported through Server Components. That's more complex and not implemented in the node loader. HMR doesn't work after reloads right now because the SSR renderer isn't hot reloaded because there's no idiomatic way to hot reload ESM modules in Node.js yet. Without killing the HMR server. This leads to hydration mismatches when reloading the page after a hot reload. Notably this doesn't show serializing the stream through the HTML like real implementations do. This will lead to possible hydration mismatches based on the data. However, manually serializing the stream as a string isn't exactly correct due to binary data. It's not the idiomatic way this is supposed to work. This will all be built-in which will make this automatic in the future.
1 parent 40755c0 commit 67a61d5

File tree

15 files changed

+312
-183
lines changed

15 files changed

+312
-183
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/loader/global.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import babel from '@babel/core';
2+
3+
const babelOptions = {
4+
babelrc: false,
5+
ignore: [/\/(build|node_modules)\//],
6+
plugins: [
7+
'@babel/plugin-syntax-import-meta',
8+
'@babel/plugin-transform-react-jsx',
9+
],
10+
};
11+
12+
export async function load(url, context, defaultLoad) {
13+
const {format} = context;
14+
const result = await defaultLoad(url, context, defaultLoad);
15+
if (result.format === 'module') {
16+
const opt = Object.assign({filename: url}, babelOptions);
17+
const newResult = await babel.transformAsync(result.source, opt);
18+
if (!newResult) {
19+
if (typeof result.source === 'string') {
20+
return result;
21+
}
22+
return {
23+
source: Buffer.from(result.source).toString('utf8'),
24+
format: 'module',
25+
};
26+
}
27+
return {source: newResult.code, format: 'module'};
28+
}
29+
return defaultLoad(url, context, defaultLoad);
30+
}
31+
32+
async function babelTransformSource(source, context, defaultTransformSource) {
33+
const {format} = context;
34+
if (format === 'module') {
35+
const opt = Object.assign({filename: context.url}, babelOptions);
36+
const newResult = await babel.transformAsync(source, opt);
37+
if (!newResult) {
38+
if (typeof source === 'string') {
39+
return {source};
40+
}
41+
return {
42+
source: Buffer.from(source).toString('utf8'),
43+
};
44+
}
45+
return {source: newResult.code};
46+
}
47+
return defaultTransformSource(source, context, defaultTransformSource);
48+
}
49+
50+
export const transformSource =
51+
process.version < 'v16' ? babelTransformSource : undefined;
File renamed without changes.

fixtures/flight/package.json

Lines changed: 8 additions & 7 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,17 +59,18 @@
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/",
6667
"prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/",
6768
"dev": "concurrently \"npm run dev:region\" \"npm run dev:global\"",
68-
"dev:global": "NODE_ENV=development BUILD_PATH=dist node server/global",
69-
"dev:region": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --experimental-loader ./loader/index.js --conditions=react-server server/region",
69+
"dev:global": "NODE_ENV=development BUILD_PATH=dist node --experimental-loader ./loader/global.js server/global",
70+
"dev:region": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --experimental-loader ./loader/region.js --conditions=react-server server/region",
7071
"start": "node scripts/build.js && concurrently \"npm run start:region\" \"npm run start:global\"",
71-
"start:global": "NODE_ENV=production node server/global",
72-
"start:region": "NODE_ENV=production node --experimental-loader ./loader/index.js --conditions=react-server server/region",
72+
"start:global": "NODE_ENV=production node --experimental-loader ./loader/global.js server/global",
73+
"start:region": "NODE_ENV=production node --experimental-loader ./loader/region.js --conditions=react-server server/region",
7374
"build": "node scripts/build.js",
7475
"test": "node scripts/test.js --env=jsdom"
7576
},

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
}

0 commit comments

Comments
 (0)