Skip to content

Commit 8e3bad7

Browse files
committed
Convert to separate bundles for server vs. client rendering with HMR
1. Turned back on hmr and inline in webpacker.yml to support HMR. 2. Change config/initializers/react_on_rails.rb to have the correct server bundle name 3. Follow the flow from config/webpack/development.js to webpackConfig.js and consider uncommenting the debug line to see what happens when you run bin/webpack --debug
1 parent d4d5c94 commit 8e3bad7

13 files changed

+208
-61
lines changed

Procfile.dev-hmr

+4-25
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,5 @@
11
# Procfile for development using HMR
2-
3-
web: rails s -p 3000
4-
5-
# Note, hot and live reloading don't work with the default generator setup on
6-
# top of the rails/webpacker Webpack config with server rendering.
7-
# If you have server rendering enabled (prerender is true), you either need to
8-
# a. Ensure that you have dev_server.hmr and dev_server.inline BOTH set to false,
9-
# and you have this option in your config/initializers/react_on_rails.rb:
10-
# config.same_bundle_for_client_and_server = true
11-
# If you have either config/webpacker.yml option set to true, you'll see errors like
12-
# "ReferenceError: window is not defined" (if hmr is true)
13-
# "TypeError: Cannot read property 'prototype' of undefined" (if inline is true)
14-
# b. Skip using the webpack-dev-server. bin/webpack --watch is typically
15-
fast enough.
16-
# c. See the React on Rails README for a link to documentation for how to setup
17-
# SSR with HMR and React hot loading using the webpack-dev-server only for the
18-
# client bundles and a static file for the server bundle.
19-
20-
# Run the webpack-dev-server for client and maybe server files
21-
webpack-dev-server: bin/webpack-dev-server
22-
23-
# Keep the JS fresh for server rendering. Remove if not server rendering.
24-
# Especially if you have not configured generation of a server bundle without a hash.
25-
# as that will conflict with the manifest created by the bin/webpack-dev-server
26-
# rails-server-assets: SERVER_BUNDLE_ONLY=yes bin/webpack --watch
2+
# You can run these commands in separate shells
3+
rails: bundle exec rails s -p 3000
4+
wp-client: bin/webpack-dev-server
5+
wp-server: SERVER_BUNDLE_ONLY=yes bin/webpack --watch
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import HelloWorld from './HelloWorld';
2+
// This could be specialized for server rendering
3+
// For example, if using React-Router, we'd have the SSR setup here.
4+
5+
export default HelloWorld;

app/javascript/packs/server-bundle.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import ReactOnRails from 'react-on-rails';
2+
3+
import HelloWorld from '../bundles/HelloWorld/components/HelloWorldServer';
4+
5+
// This is how react_on_rails can see the HelloWorld in the browser.
6+
ReactOnRails.register({
7+
HelloWorld,
8+
});

config/initializers/react_on_rails.rb

+1-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,5 @@
4141
# different. You should have ONE server bundle which can create all of your server rendered
4242
# React components.
4343
#
44-
config.server_bundle_js_file = "hello-world-bundle.js"
45-
46-
config.same_bundle_for_client_and_server = true
44+
config.server_bundle_js_file = "server-bundle.js"
4745
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const { merge } = require('webpack-merge')
2+
const environment = require('./environment')
3+
4+
const configureClient = () => {
5+
const clientConfigObject = environment.toWebpackConfig()
6+
7+
// Copy the object using merge b/c the clientConfigObject is non-stop mutable
8+
// After calling toWebpackConfig, and then modifying the resulting object,
9+
// another call to `toWebpackConfig` on this same environment will overwrite
10+
// the next line.
11+
const clientConfig = merge({}, clientConfigObject)
12+
13+
// server-bundle is special and should ONLY be built by the serverConfig
14+
// In case this entry is not deleted, a very strange "window" not found
15+
// error shows referring to window["webpackJsonp"]. That is because the
16+
// client config is going to try to load chunks.
17+
delete clientConfig.entry['server-bundle']
18+
19+
return clientConfig
20+
}
21+
22+
module.exports = configureClient

config/webpack/development.js

+29-24
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
11
process.env.NODE_ENV = process.env.NODE_ENV || 'development'
22

3-
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
4-
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
5-
const path = require("path");
3+
const webpackConfig = require('./webpackConfig')
64

7-
const environment = require('./environment')
5+
module.exports = webpackConfig()
86

9-
const isWebpackDevServer = process.env.WEBPACK_DEV_SERVER;
7+
const developmentOnly = () => {
8+
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
9+
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
10+
const path = require("path");
11+
12+
const environment = require('./environment')
13+
14+
const isWebpackDevServer = process.env.WEBPACK_DEV_SERVER;
15+
16+
//plugins
17+
if (isWebpackDevServer) {
18+
environment.plugins.append(
19+
'ReactRefreshWebpackPlugin',
20+
new ReactRefreshWebpackPlugin({
21+
overlay: {
22+
sockPort: 3035
23+
}
24+
})
25+
);
26+
}
1027

11-
//plugins
12-
if (isWebpackDevServer) {
1328
environment.plugins.append(
14-
'ReactRefreshWebpackPlugin',
15-
new ReactRefreshWebpackPlugin({
16-
overlay: {
17-
sockPort: 3035
18-
}
29+
"ForkTsCheckerWebpackPlugin",
30+
new ForkTsCheckerWebpackPlugin({
31+
typescript: {
32+
configFile: path.resolve(__dirname, "../../tsconfig.json"),
33+
},
34+
async: false,
1935
})
2036
);
2137
}
2238

23-
24-
environment.plugins.append(
25-
"ForkTsCheckerWebpackPlugin",
26-
new ForkTsCheckerWebpackPlugin({
27-
typescript: {
28-
configFile: path.resolve(__dirname, "../../tsconfig.json"),
29-
},
30-
async: false,
31-
})
32-
);
33-
34-
module.exports = environment.toWebpackConfig()
39+
module.exports = webpackConfig(developmentOnly)

config/webpack/production.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
process.env.NODE_ENV = process.env.NODE_ENV || 'production'
2-
1+
// Below code should get refactored but the current way that rails/webpacker v5
2+
// does the globals, it's tricky
33
const environment = require('./environment')
4+
const webpackConfig = require('./webpackConfig')
5+
6+
const productionOnly = () => {
7+
// place any code here that is for production only
8+
}
49

5-
module.exports = environment.toWebpackConfig()
10+
module.exports = webpackConfig(productionOnly)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
const { config } = require('@rails/webpacker')
2+
const webpack = require('webpack')
3+
const { merge } = require('webpack-merge')
4+
const environment = require('./environment')
5+
6+
const clientConfigObject = environment.toWebpackConfig()
7+
8+
const configureServer = () => {
9+
// We need to use "merge" because the clientConfigObject, EVEN after running
10+
// toWebpackConfig() is a mutable GLOBAL. Thus any changes, like modifying the
11+
// entry value will result in changing the client config!
12+
// Using webpack-merge into an empty object avoids this issue.
13+
const serverWebpackConfig = merge({}, clientConfigObject)
14+
15+
// We just want the single server bundle entry
16+
const serverEntry = {
17+
'server-bundle': environment.entry.get('server-bundle'),
18+
}
19+
serverWebpackConfig.entry = serverEntry
20+
21+
// Remove the mini-css-extract-plugin from the style loaders because
22+
// the client build will handle exporting CSS.
23+
// replace file-loader with null-loader
24+
serverWebpackConfig.module.rules.forEach((loader) => {
25+
if (loader.use && loader.use.filter) {
26+
loader.use = loader.use.filter(
27+
(item) =>
28+
!(typeof item === 'string' && item.match(/mini-css-extract-plugin/))
29+
)
30+
}
31+
})
32+
33+
// No splitting of chunks for a server bundle
34+
serverWebpackConfig.optimization = {
35+
minimize: false,
36+
}
37+
serverWebpackConfig.plugins.unshift(
38+
new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })
39+
)
40+
41+
// Custom output for the server-bundle that matches the config in
42+
// config/initializers/react_on_rails.rb
43+
serverWebpackConfig.output = {
44+
filename: 'server-bundle.js',
45+
globalObject: 'this',
46+
// If using the React on Rails Pro node server renderer, uncomment the next line
47+
// libraryTarget: 'commonjs2',
48+
path: config.outputPath,
49+
publicPath: config.outputPath,
50+
// https://webpack.js.org/configuration/output/#outputglobalobject
51+
}
52+
53+
// Don't hash the server bundle b/c would conflict with the client manifest
54+
// And no need for the MiniCssExtractPlugin
55+
serverWebpackConfig.plugins = serverWebpackConfig.plugins.filter(
56+
(plugin) =>
57+
plugin.constructor.name !== 'WebpackAssetsManifest' &&
58+
plugin.constructor.name !== 'MiniCssExtractPlugin' &&
59+
plugin.constructor.name !== 'ForkTsCheckerWebpackPlugin'
60+
)
61+
62+
// Critical due to https://github.com/rails/webpacker/pull/2644
63+
delete serverWebpackConfig.devServer
64+
65+
// eval works well for the SSR bundle because it's the fastest and shows
66+
// lines in the server bundle which is good for debugging SSR
67+
// The default of cheap-module-source-map is slow and provides poor info.
68+
serverWebpackConfig.devtool = 'eval'
69+
70+
// If using the default 'web', then libraries like Emotion and loadable-components
71+
// break with SSR. The fix is to use a node renderer and change the target.
72+
// If using the React on Rails Pro node server renderer, uncomment the next line
73+
// serverWebpackConfig.target = 'node'
74+
75+
return serverWebpackConfig
76+
}
77+
78+
module.exports = configureServer

config/webpack/test.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
process.env.NODE_ENV = process.env.NODE_ENV || 'development'
1+
const webpackConfig = require('./webpackConfig')
22

3-
const environment = require('./environment')
3+
const testOnly = () => {
4+
// place any code here that is for test only
5+
}
46

5-
module.exports = environment.toWebpackConfig()
7+
module.exports = webpackConfig(testOnly)

config/webpack/webpackConfig.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const clientConfig = require('./clientWebpackConfiguration')
2+
const serverConfig = require('./serverWebpackConfiguration')
3+
4+
const webpackConfig = (envSpecific) => {
5+
if (envSpecific) {
6+
envSpecific()
7+
}
8+
9+
let result
10+
// For HMR, need to separate the the client and server webpack configurations
11+
if (process.env.WEBPACK_DEV_SERVER || process.env.CLIENT_BUNDLE_ONLY) {
12+
// eslint-disable-next-line no-console
13+
console.log('[React on Rails] Creating only the client bundles.')
14+
result = clientConfig()
15+
} else if (process.env.SERVER_BUNDLE_ONLY) {
16+
// eslint-disable-next-line no-console
17+
console.log('[React on Rails] Creating only the server bundle.')
18+
result = serverConfig()
19+
} else {
20+
// default is the standard client and server build
21+
// eslint-disable-next-line no-console
22+
console.log('[React on Rails] Creating both client and server bundles.')
23+
result = [clientConfig(), serverConfig()]
24+
}
25+
26+
// To debug, uncomment next line and inspect "result"
27+
// debugger
28+
return result
29+
}
30+
31+
module.exports = webpackConfig

config/webpacker.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ development:
6060
host: localhost
6161
port: 3035
6262
public: localhost:3035
63-
hmr: false
63+
hmr: true
6464
# Inline should be set to true if using HMR
65-
inline: false
65+
inline: true
6666
overlay: true
6767
compress: true
6868
disable_host_check: true

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"react": "^16.13.1",
1111
"react-dom": "^16.13.1",
1212
"react-on-rails": "12.0.1",
13-
"typescript": "^3.9.7"
13+
"typescript": "^3.9.7",
14+
"webpack-merge": "^5.0.9"
1415
},
1516
"devDependencies": {
1617
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.1",

yarn.lock

+13
Original file line numberDiff line numberDiff line change
@@ -7717,6 +7717,14 @@ webpack-log@^2.0.0:
77177717
ansi-colors "^3.0.0"
77187718
uuid "^3.3.2"
77197719

7720+
webpack-merge@^5.0.9:
7721+
version "5.0.9"
7722+
resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.0.9.tgz#d5e0e0ae564ae704836d747893bdd2741544bf31"
7723+
integrity sha512-P4teh6O26xIDPugOGX61wPxaeP918QOMjmzhu54zTVcLtOS28ffPWtnv+ilt3wscwBUCL2WNMnh97XkrKqt9Fw==
7724+
dependencies:
7725+
clone-deep "^4.0.1"
7726+
wildcard "^2.0.0"
7727+
77207728
webpack-sources@^1.0.0, webpack-sources@^1.0.1, webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
77217729
version "1.4.3"
77227730
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
@@ -7794,6 +7802,11 @@ wide-align@^1.1.0:
77947802
dependencies:
77957803
string-width "^1.0.2 || 2"
77967804

7805+
wildcard@^2.0.0:
7806+
version "2.0.0"
7807+
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
7808+
integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
7809+
77977810
worker-farm@^1.7.0:
77987811
version "1.7.0"
77997812
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"

0 commit comments

Comments
 (0)