Skip to content

Commit a47edb5

Browse files
committed
[no jira]: Full SSR, Hard Source plugin
1 parent 593a3ef commit a47edb5

File tree

12 files changed

+225
-40
lines changed

12 files changed

+225
-40
lines changed

packages/react-scripts/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# `backpack-react-scripts` Change Log
22

3+
## 8.1.0 (Pending)
4+
5+
- Added support for loadable components.
6+
- Added `start-ssr` command, to produce Node.js-compatible watched output. Several changes to SSR Webpack config to support.
7+
- Defined `typeof window` for browser and SSR environments, enabling dead code elimination (https://webpack.js.org/plugins/define-plugin/#usage)
8+
- SSR output always includes hash as part of filename
9+
- Added experimental support for https://github.com/mzgoddard/hard-source-webpack-plugin/, enabled by `USE_HARD_SOURCE_WEBPACK_PLUGIN=true` environment variable
10+
- `web` and `ssr` subpaths for each build's output
11+
- Output build 'status files' (`.build-status`, one for web, one for SSR), which can be watched by a Node.js server to know when builds are in progress or completed.
12+
313
## 8.0.0
414

515
### Breaking

packages/react-scripts/bin/react-scripts.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ const spawn = require('react-dev-utils/crossSpawn');
1919
const args = process.argv.slice(2);
2020

2121
const scriptIndex = args.findIndex(
22-
x => x === 'build' || x === 'eject' || x === 'start' || x === 'test'
22+
x => x === 'build' || x === 'eject' || x === 'start' || x === 'start-ssr' || x === 'test'
2323
);
2424
const script = scriptIndex === -1 ? args[0] : args[scriptIndex];
2525
const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : [];
2626

27-
if (['build', 'eject', 'start', 'test'].includes(script)) {
27+
if (['build', 'eject', 'start', 'start-ssr', 'test'].includes(script)) {
2828
const result = spawn.sync(
2929
'node',
3030
nodeArgs
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
6+
const reactScriptsRoot = path.resolve(__dirname, '..');
7+
const haveIsolatedDependencies =
8+
fs.existsSync(path.join(reactScriptsRoot, 'package-lock.json')) ||
9+
fs.existsSync(path.join(reactScriptsRoot, 'yarn.lock'));
10+
11+
module.exports = {
12+
root: haveIsolatedDependencies ? reactScriptsRoot : process.cwd(),
13+
directories: [],
14+
files: ['package-lock.json', 'yarn.lock'],
15+
};

packages/react-scripts/config/paths.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ const resolveModule = (resolveFn, filePath) => {
6060
module.exports = {
6161
dotenv: resolveApp('.env'),
6262
appPath: resolveApp('.'),
63-
appBuild: resolveApp('build'),
63+
appBuildWeb: resolveApp('build/web'),
64+
appBuildSsr: resolveApp('build/ssr'),
6465
appPublic: resolveApp('public'),
6566
appHtml: resolveApp('public/index.html'),
6667
appIndexJs: resolveModule(resolveApp, 'src/index'),
@@ -83,7 +84,8 @@ const resolveOwn = relativePath => path.resolve(__dirname, '..', relativePath);
8384
module.exports = {
8485
dotenv: resolveApp('.env'),
8586
appPath: resolveApp('.'),
86-
appBuild: resolveApp('build'),
87+
appBuildWeb: resolveApp('build/web'),
88+
appBuildSsr: resolveApp('build/ssr'),
8789
appPublic: resolveApp('public'),
8890
appHtml: resolveApp('public/index.html'),
8991
appIndexJs: resolveModule(resolveApp, 'src/index'),
@@ -119,7 +121,8 @@ if (
119121
module.exports = {
120122
dotenv: resolveOwn(`${templatePath}/.env`),
121123
appPath: resolveApp('.'),
122-
appBuild: resolveOwn('../../build'),
124+
appBuildWeb: resolveOwn('../../build/web'),
125+
appBuildSsr: resolveOwn('../../build/ssr'),
123126
appPublic: resolveOwn(`${templatePath}/public`),
124127
appHtml: resolveOwn(`${templatePath}/public/index.html`),
125128
appIndexJs: resolveModule(resolveOwn, `${templatePath}/src/index`),

packages/react-scripts/config/webpack.config.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ const postcssNormalize = require('postcss-normalize');
4040

4141
const appPackageJson = require(paths.appPackageJson);
4242

43+
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
44+
const LoadablePlugin = require('@loadable/webpack-plugin');
45+
4346
const sassFunctions = require('bpk-mixins/sass-functions');
4447
const camelCase = require('lodash/camelCase');
4548
const bpkReactScriptsConfig = appPackageJson['backpack-react-scripts'] || {};
@@ -58,6 +61,10 @@ const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
5861
// makes for a smoother build process.
5962
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
6063

64+
// We might not want to use the hard source plugin on environments that won't persist the cache for later
65+
const useHardSourceWebpackPlugin = process.env.USE_HARD_SOURCE_WEBPACK_PLUGIN === 'true';
66+
const environmentHash = require('./environmentHash');
67+
6168
// const isExtendingEslintConfig = process.env.EXTEND_ESLINT === 'true';
6269

6370
const imageInlineSizeLimit = parseInt(
@@ -195,7 +202,7 @@ module.exports = function(webpackEnv) {
195202
output: {
196203
crossOriginLoading: sriEnabled ? 'anonymous' : crossOriginLoading,
197204
// The build folder.
198-
path: isEnvProduction ? paths.appBuild : undefined,
205+
path: paths.appBuildWeb,
199206
// Add /* filename */ comments to generated require()s in the output.
200207
pathinfo: isEnvDevelopment,
201208
// There will be one main bundle, and one file per asynchronous chunk.
@@ -374,6 +381,7 @@ module.exports = function(webpackEnv) {
374381
],
375382
},
376383
module: {
384+
noParse: /iconv-loader\.js$/, // https://github.com/webpack/webpack/issues/3078#issuecomment-400697407
377385
strictExportPresence: true,
378386
rules: [
379387
// Disable require.ensure as it's not a standard language feature.
@@ -469,6 +477,7 @@ module.exports = function(webpackEnv) {
469477
),
470478
// @remove-on-eject-end
471479
plugins: [
480+
require.resolve('@loadable/babel-plugin'),
472481
[
473482
require.resolve('babel-plugin-named-asset-import'),
474483
{
@@ -646,6 +655,20 @@ module.exports = function(webpackEnv) {
646655
],
647656
},
648657
plugins: [
658+
useHardSourceWebpackPlugin &&
659+
new HardSourceWebpackPlugin({ environmentHash }),
660+
useHardSourceWebpackPlugin &&
661+
new HardSourceWebpackPlugin.ExcludeModulePlugin([
662+
{
663+
// HardSource works with mini-css-extract-plugin but due to how
664+
// mini-css emits assets, assets are not emitted on repeated builds with
665+
// mini-css and hard-source together. Ignoring the mini-css loader
666+
// modules, but not the other css loader modules, excludes the modules
667+
// that mini-css needs rebuilt to output assets every time.
668+
test: /mini-css-extract-plugin[\\/]dist[\\/]loader/,
669+
},
670+
]),
671+
new LoadablePlugin(),
649672
// Generates an `index.html` file with the <script> injected.
650673
new HtmlWebpackPlugin(
651674
Object.assign(
@@ -712,7 +735,10 @@ module.exports = function(webpackEnv) {
712735
// It is absolutely essential that NODE_ENV is set to production
713736
// during a production build.
714737
// Otherwise React will be compiled in the very slow development mode.
715-
new webpack.DefinePlugin(env.stringified),
738+
new webpack.DefinePlugin({
739+
...env.stringified,
740+
'typeof window': '"object"',
741+
}),
716742
// This is necessary to emit hot updates (currently CSS only):
717743
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
718744
// Watcher doesn't work well if you mistype casing in a path so we use

packages/react-scripts/config/webpack.config.ssr.js

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const PnpWebpackPlugin = require('pnp-webpack-plugin');
1717
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
1818
// const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
1919
// const TerserPlugin = require('terser-webpack-plugin');
20-
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
20+
// const MiniCssExtractPlugin = require('mini-css-extract-plugin');
2121
// const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
2222
// const safePostCssParser = require('postcss-safe-parser');
2323
// const ManifestPlugin = require('webpack-manifest-plugin');
@@ -39,6 +39,8 @@ const postcssNormalize = require('postcss-normalize');
3939

4040
const appPackageJson = require(paths.appPackageJson);
4141

42+
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
43+
const LoadablePlugin = require('@loadable/webpack-plugin');
4244
const sassFunctions = require('bpk-mixins/sass-functions');
4345
// const camelCase = require('lodash/camelCase');
4446
const bpkReactScriptsConfig = appPackageJson['backpack-react-scripts'] || {};
@@ -54,6 +56,9 @@ const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
5456
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
5557
// makes for a smoother build process.
5658
// const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
59+
// We might not want to use the hard source plugin on environments that won't persist the cache for later
60+
const useHardSourceWebpackPlugin = process.env.USE_HARD_SOURCE_WEBPACK_PLUGIN === 'true';
61+
const environmentHash = require('./environmentHash');
5762

5863
// const isExtendingEslintConfig = process.env.EXTEND_ESLINT === 'true';
5964

@@ -98,17 +103,17 @@ module.exports = function(webpackEnv) {
98103
preProcessorOptions = {}
99104
) => {
100105
const loaders = [
101-
isEnvDevelopment && require.resolve('style-loader'),
102-
isEnvProduction && {
103-
loader: MiniCssExtractPlugin.loader,
104-
// css is located in `static/css`, use '../../' to locate index.html folder
105-
// in production `paths.publicUrlOrPath` can be a relative path
106-
options: paths.publicUrlOrPath.startsWith('.')
107-
? { publicPath: '../../' }
108-
: {},
109-
},
106+
// isEnvDevelopment && require.resolve('style-loader'),
107+
// isEnvProduction && {
108+
// loader: MiniCssExtractPlugin.loader,
109+
// // css is located in `static/css`, use '../../' to locate index.html folder
110+
// // in production `paths.publicUrlOrPath` can be a relative path
111+
// options: paths.publicUrlOrPath.startsWith('.')
112+
// ? { publicPath: '../../' }
113+
// : {},
114+
// },
110115
{
111-
loader: require.resolve('css-loader'),
116+
loader: require.resolve('css-loader/locals'),
112117
options: cssOptions,
113118
},
114119
{
@@ -182,8 +187,8 @@ module.exports = function(webpackEnv) {
182187
// the line below with these two lines if you prefer the stock client:
183188
// require.resolve('webpack-dev-server/client') + '?/',
184189
// require.resolve('webpack/hot/dev-server'),
185-
isEnvDevelopment &&
186-
require.resolve('react-dev-utils/webpackHotDevClient'),
190+
// isEnvDevelopment &&
191+
// require.resolve('react-dev-utils/webpackHotDevClient'),
187192
// Finally, this is your app's code:
188193
// paths.appIndexJs,
189194
paths.appSsrJs,
@@ -193,7 +198,7 @@ module.exports = function(webpackEnv) {
193198
].filter(Boolean),
194199
output: {
195200
// The build folder.
196-
path: isEnvProduction ? paths.appBuild : undefined,
201+
path: paths.appBuildSsr,
197202
// Add /* filename */ comments to generated require()s in the output.
198203
pathinfo: isEnvDevelopment,
199204
// There will be one main bundle, and one file per asynchronous chunk.
@@ -203,12 +208,12 @@ module.exports = function(webpackEnv) {
203208
// : isEnvDevelopment && 'static/js/bundle.js',
204209
// TODO: remove this when upgrading to webpack 5
205210
futureEmitAssets: true,
206-
filename: 'ssr.js',
211+
filename: 'ssr.[hash:8].js',
207212
libraryTarget: 'commonjs2',
208213
// There are also additional JS chunk files if you use code splitting.
209214
chunkFilename: isEnvProduction
210215
? 'static/js/[name].[contenthash:8].chunk.js'
211-
: isEnvDevelopment && 'static/js/[name].chunk.js',
216+
: isEnvDevelopment && 'static/js/[name].[chunkhash:8].chunk.js',
212217
// webpack uses `publicPath` to determine where the app is being served from.
213218
// It requires a trailing slash, or the file assets will get an incorrect path.
214219
// We inferred the "public path" (such as / or /my-project) from homepage.
@@ -351,6 +356,7 @@ module.exports = function(webpackEnv) {
351356
],
352357
},
353358
module: {
359+
noParse: /iconv-loader\.js$/, // https://github.com/webpack/webpack/issues/3078#issuecomment-400697407
354360
strictExportPresence: true,
355361
rules: [
356362
// Disable require.ensure as it's not a standard language feature.
@@ -446,6 +452,7 @@ module.exports = function(webpackEnv) {
446452
),
447453
// @remove-on-eject-end
448454
plugins: [
455+
require.resolve('@loadable/babel-plugin'),
449456
[
450457
require.resolve('babel-plugin-named-asset-import'),
451458
{
@@ -623,6 +630,20 @@ module.exports = function(webpackEnv) {
623630
],
624631
},
625632
plugins: [
633+
useHardSourceWebpackPlugin &&
634+
new HardSourceWebpackPlugin({ environmentHash }),
635+
useHardSourceWebpackPlugin &&
636+
new HardSourceWebpackPlugin.ExcludeModulePlugin([
637+
{
638+
// HardSource works with mini-css-extract-plugin but due to how
639+
// mini-css emits assets, assets are not emitted on repeated builds with
640+
// mini-css and hard-source together. Ignoring the mini-css loader
641+
// modules, but not the other css loader modules, excludes the modules
642+
// that mini-css needs rebuilt to output assets every time.
643+
test: /mini-css-extract-plugin[\\/]dist[\\/]loader/,
644+
},
645+
]),
646+
new LoadablePlugin(),
626647
// Generates an `index.html` file with the <script> injected.
627648
// new HtmlWebpackPlugin(
628649
// Object.assign(
@@ -689,9 +710,12 @@ module.exports = function(webpackEnv) {
689710
// It is absolutely essential that NODE_ENV is set to production
690711
// during a production build.
691712
// Otherwise React will be compiled in the very slow development mode.
692-
new webpack.DefinePlugin(env.stringified),
713+
new webpack.DefinePlugin({
714+
...env.stringified,
715+
'typeof window': '"undefined"',
716+
}),
693717
// This is necessary to emit hot updates (currently CSS only):
694-
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
718+
// isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
695719
// Watcher doesn't work well if you mistype casing in a path so we use
696720
// a plugin that prints an error when you attempt to do this.
697721
// See https://github.com/facebook/create-react-app/issues/240
@@ -702,14 +726,14 @@ module.exports = function(webpackEnv) {
702726
// See https://github.com/facebook/create-react-app/issues/186
703727
isEnvDevelopment &&
704728
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
705-
isEnvProduction &&
706-
new MiniCssExtractPlugin({
707-
// Options similar to the same options in webpackOptions.output
708-
// both options are optional
709-
// filename: 'static/css/[name].[contenthash:8].css',
710-
filename: 'ssr.css',
711-
// chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
712-
}),
729+
// isEnvProduction &&
730+
// new MiniCssExtractPlugin({
731+
// // Options similar to the same options in webpackOptions.output
732+
// // both options are optional
733+
// // filename: 'static/css/[name].[contenthash:8].css',
734+
// filename: 'ssr.css',
735+
// // chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
736+
// }),
713737
// Generate an asset manifest file with the following content:
714738
// - "files" key: Mapping of all asset filenames to their corresponding
715739
// output file so that tools can pick it up without having to parse

packages/react-scripts/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
"types": "./lib/react-app.d.ts",
2727
"dependencies": {
2828
"@babel/core": "7.9.0",
29+
"@loadable/babel-plugin": "^5.6.0",
30+
"@loadable/webpack-plugin": "^5.5.0",
2931
"@svgr/webpack": "4.3.3",
3032
"@typescript-eslint/eslint-plugin": "^2.10.0",
3133
"@typescript-eslint/parser": "^2.10.0",
@@ -41,6 +43,7 @@
4143
"dotenv-expand": "5.1.0",
4244
"file-loader": "4.3.0",
4345
"fs-extra": "^8.1.0",
46+
"hard-source-webpack-plugin": "^0.13.1",
4447
"html-webpack-plugin": "4.0.0-beta.11",
4548
"identity-obj-proxy": "3.0.0",
4649
"jest": "24.9.0",
@@ -49,6 +52,7 @@
4952
"jest-watch-typeahead": "0.4.2",
5053
"lodash": "^4.17.15",
5154
"mini-css-extract-plugin": "0.9.0",
55+
"mkdirp": "^1.0.3",
5256
"node-sass": "^4.12.0",
5357
"optimize-css-assets-webpack-plugin": "5.0.3",
5458
"pnp-webpack-plugin": "1.6.4",

0 commit comments

Comments
 (0)