Skip to content

Commit

Permalink
Convert to separate bundles for server vs. client rendering with HMR
Browse files Browse the repository at this point in the history
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
  • Loading branch information
justin808 committed Aug 3, 2020
1 parent d4d5c94 commit 8e3bad7
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 61 deletions.
29 changes: 4 additions & 25 deletions Procfile.dev-hmr
Original file line number Diff line number Diff line change
@@ -1,26 +1,5 @@
# Procfile for development using HMR

web: rails s -p 3000

# Note, hot and live reloading don't work with the default generator setup on
# top of the rails/webpacker Webpack config with server rendering.
# If you have server rendering enabled (prerender is true), you either need to
# a. Ensure that you have dev_server.hmr and dev_server.inline BOTH set to false,
# and you have this option in your config/initializers/react_on_rails.rb:
# config.same_bundle_for_client_and_server = true
# If you have either config/webpacker.yml option set to true, you'll see errors like
# "ReferenceError: window is not defined" (if hmr is true)
# "TypeError: Cannot read property 'prototype' of undefined" (if inline is true)
# b. Skip using the webpack-dev-server. bin/webpack --watch is typically
fast enough.
# c. See the React on Rails README for a link to documentation for how to setup
# SSR with HMR and React hot loading using the webpack-dev-server only for the
# client bundles and a static file for the server bundle.

# Run the webpack-dev-server for client and maybe server files
webpack-dev-server: bin/webpack-dev-server

# Keep the JS fresh for server rendering. Remove if not server rendering.
# Especially if you have not configured generation of a server bundle without a hash.
# as that will conflict with the manifest created by the bin/webpack-dev-server
# rails-server-assets: SERVER_BUNDLE_ONLY=yes bin/webpack --watch
# You can run these commands in separate shells
rails: bundle exec rails s -p 3000
wp-client: bin/webpack-dev-server
wp-server: SERVER_BUNDLE_ONLY=yes bin/webpack --watch
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import HelloWorld from './HelloWorld';
// This could be specialized for server rendering
// For example, if using React-Router, we'd have the SSR setup here.

export default HelloWorld;
8 changes: 8 additions & 0 deletions app/javascript/packs/server-bundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ReactOnRails from 'react-on-rails';

import HelloWorld from '../bundles/HelloWorld/components/HelloWorldServer';

// This is how react_on_rails can see the HelloWorld in the browser.
ReactOnRails.register({
HelloWorld,
});
4 changes: 1 addition & 3 deletions config/initializers/react_on_rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,5 @@
# different. You should have ONE server bundle which can create all of your server rendered
# React components.
#
config.server_bundle_js_file = "hello-world-bundle.js"

config.same_bundle_for_client_and_server = true
config.server_bundle_js_file = "server-bundle.js"
end
22 changes: 22 additions & 0 deletions config/webpack/clientWebpackConfiguration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const { merge } = require('webpack-merge')
const environment = require('./environment')

const configureClient = () => {
const clientConfigObject = environment.toWebpackConfig()

// Copy the object using merge b/c the clientConfigObject is non-stop mutable
// After calling toWebpackConfig, and then modifying the resulting object,
// another call to `toWebpackConfig` on this same environment will overwrite
// the next line.
const clientConfig = merge({}, clientConfigObject)

// server-bundle is special and should ONLY be built by the serverConfig
// In case this entry is not deleted, a very strange "window" not found
// error shows referring to window["webpackJsonp"]. That is because the
// client config is going to try to load chunks.
delete clientConfig.entry['server-bundle']

return clientConfig
}

module.exports = configureClient
53 changes: 29 additions & 24 deletions config/webpack/development.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
process.env.NODE_ENV = process.env.NODE_ENV || 'development'

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

const environment = require('./environment')
module.exports = webpackConfig()

const isWebpackDevServer = process.env.WEBPACK_DEV_SERVER;
const developmentOnly = () => {
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const path = require("path");

const environment = require('./environment')

const isWebpackDevServer = process.env.WEBPACK_DEV_SERVER;

//plugins
if (isWebpackDevServer) {
environment.plugins.append(
'ReactRefreshWebpackPlugin',
new ReactRefreshWebpackPlugin({
overlay: {
sockPort: 3035
}
})
);
}

//plugins
if (isWebpackDevServer) {
environment.plugins.append(
'ReactRefreshWebpackPlugin',
new ReactRefreshWebpackPlugin({
overlay: {
sockPort: 3035
}
"ForkTsCheckerWebpackPlugin",
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: path.resolve(__dirname, "../../tsconfig.json"),
},
async: false,
})
);
}


environment.plugins.append(
"ForkTsCheckerWebpackPlugin",
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: path.resolve(__dirname, "../../tsconfig.json"),
},
async: false,
})
);

module.exports = environment.toWebpackConfig()
module.exports = webpackConfig(developmentOnly)
11 changes: 8 additions & 3 deletions config/webpack/production.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
process.env.NODE_ENV = process.env.NODE_ENV || 'production'

// Below code should get refactored but the current way that rails/webpacker v5
// does the globals, it's tricky
const environment = require('./environment')
const webpackConfig = require('./webpackConfig')

const productionOnly = () => {
// place any code here that is for production only
}

module.exports = environment.toWebpackConfig()
module.exports = webpackConfig(productionOnly)
78 changes: 78 additions & 0 deletions config/webpack/serverWebpackConfiguration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const { config } = require('@rails/webpacker')
const webpack = require('webpack')
const { merge } = require('webpack-merge')
const environment = require('./environment')

const clientConfigObject = environment.toWebpackConfig()

const configureServer = () => {
// We need to use "merge" because the clientConfigObject, EVEN after running
// toWebpackConfig() is a mutable GLOBAL. Thus any changes, like modifying the
// entry value will result in changing the client config!
// Using webpack-merge into an empty object avoids this issue.
const serverWebpackConfig = merge({}, clientConfigObject)

// We just want the single server bundle entry
const serverEntry = {
'server-bundle': environment.entry.get('server-bundle'),
}
serverWebpackConfig.entry = serverEntry

// Remove the mini-css-extract-plugin from the style loaders because
// the client build will handle exporting CSS.
// replace file-loader with null-loader
serverWebpackConfig.module.rules.forEach((loader) => {
if (loader.use && loader.use.filter) {
loader.use = loader.use.filter(
(item) =>
!(typeof item === 'string' && item.match(/mini-css-extract-plugin/))
)
}
})

// No splitting of chunks for a server bundle
serverWebpackConfig.optimization = {
minimize: false,
}
serverWebpackConfig.plugins.unshift(
new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })
)

// Custom output for the server-bundle that matches the config in
// config/initializers/react_on_rails.rb
serverWebpackConfig.output = {
filename: 'server-bundle.js',
globalObject: 'this',
// If using the React on Rails Pro node server renderer, uncomment the next line
// libraryTarget: 'commonjs2',
path: config.outputPath,
publicPath: config.outputPath,
// https://webpack.js.org/configuration/output/#outputglobalobject
}

// Don't hash the server bundle b/c would conflict with the client manifest
// And no need for the MiniCssExtractPlugin
serverWebpackConfig.plugins = serverWebpackConfig.plugins.filter(
(plugin) =>
plugin.constructor.name !== 'WebpackAssetsManifest' &&
plugin.constructor.name !== 'MiniCssExtractPlugin' &&
plugin.constructor.name !== 'ForkTsCheckerWebpackPlugin'
)

// Critical due to https://github.com/rails/webpacker/pull/2644
delete serverWebpackConfig.devServer

// eval works well for the SSR bundle because it's the fastest and shows
// lines in the server bundle which is good for debugging SSR
// The default of cheap-module-source-map is slow and provides poor info.
serverWebpackConfig.devtool = 'eval'

// If using the default 'web', then libraries like Emotion and loadable-components
// break with SSR. The fix is to use a node renderer and change the target.
// If using the React on Rails Pro node server renderer, uncomment the next line
// serverWebpackConfig.target = 'node'

return serverWebpackConfig
}

module.exports = configureServer
8 changes: 5 additions & 3 deletions config/webpack/test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
process.env.NODE_ENV = process.env.NODE_ENV || 'development'
const webpackConfig = require('./webpackConfig')

const environment = require('./environment')
const testOnly = () => {
// place any code here that is for test only
}

module.exports = environment.toWebpackConfig()
module.exports = webpackConfig(testOnly)
31 changes: 31 additions & 0 deletions config/webpack/webpackConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const clientConfig = require('./clientWebpackConfiguration')
const serverConfig = require('./serverWebpackConfiguration')

const webpackConfig = (envSpecific) => {
if (envSpecific) {
envSpecific()
}

let result
// For HMR, need to separate the the client and server webpack configurations
if (process.env.WEBPACK_DEV_SERVER || process.env.CLIENT_BUNDLE_ONLY) {
// eslint-disable-next-line no-console
console.log('[React on Rails] Creating only the client bundles.')
result = clientConfig()
} else if (process.env.SERVER_BUNDLE_ONLY) {
// eslint-disable-next-line no-console
console.log('[React on Rails] Creating only the server bundle.')
result = serverConfig()
} else {
// default is the standard client and server build
// eslint-disable-next-line no-console
console.log('[React on Rails] Creating both client and server bundles.')
result = [clientConfig(), serverConfig()]
}

// To debug, uncomment next line and inspect "result"
// debugger
return result
}

module.exports = webpackConfig
4 changes: 2 additions & 2 deletions config/webpacker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ development:
host: localhost
port: 3035
public: localhost:3035
hmr: false
hmr: true
# Inline should be set to true if using HMR
inline: false
inline: true
overlay: true
compress: true
disable_host_check: true
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-on-rails": "12.0.1",
"typescript": "^3.9.7"
"typescript": "^3.9.7",
"webpack-merge": "^5.0.9"
},
"devDependencies": {
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.1",
Expand Down
13 changes: 13 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7717,6 +7717,14 @@ webpack-log@^2.0.0:
ansi-colors "^3.0.0"
uuid "^3.3.2"

webpack-merge@^5.0.9:
version "5.0.9"
resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.0.9.tgz#d5e0e0ae564ae704836d747893bdd2741544bf31"
integrity sha512-P4teh6O26xIDPugOGX61wPxaeP918QOMjmzhu54zTVcLtOS28ffPWtnv+ilt3wscwBUCL2WNMnh97XkrKqt9Fw==
dependencies:
clone-deep "^4.0.1"
wildcard "^2.0.0"

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:
version "1.4.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
Expand Down Expand Up @@ -7794,6 +7802,11 @@ wide-align@^1.1.0:
dependencies:
string-width "^1.0.2 || 2"

wildcard@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==

worker-farm@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
Expand Down

0 comments on commit 8e3bad7

Please sign in to comment.