From 14bd0808cf5115875a135ae2dc0282556fbaf8ae Mon Sep 17 00:00:00 2001 From: Gus Date: Sat, 23 Oct 2021 12:01:26 -0300 Subject: [PATCH 01/14] Display error message when no config.server_bundle_js_file is provided --- lib/react_on_rails/helper.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb index 9e638502d..13f25128a 100644 --- a/lib/react_on_rails/helper.rb +++ b/lib/react_on_rails/helper.rb @@ -54,6 +54,17 @@ module Helper # random_dom_id can be set to override the default from the config/initializers. That's only # used if you have multiple instance of the same component on the Rails view. def react_component(component_name, options = {}) + config_server_bundle_js = ReactOnRails.configuration.server_bundle_js_file + if options[:prerender] == true && (config_server_bundle_js.nil? || config_server_bundle_js == "") + msg = <<~MSG + The `prerender` option to allow Server Side Rendering is marked as true but the ReactOnRails configuration + for `server_bundle_js_file` is nil or not present in `config/initializers/react_on_rails.rb`. + Set `config.server_bundle_js_file` to your javascript bundle to allow server side rendering. + Read more at https://www.shakacode.com/react-on-rails/docs/guides/react-server-rendering/ + MSG + raise ReactOnRails::Error, msg + end + internal_result = internal_react_component(component_name, options) server_rendered_html = internal_result[:result]["html"] console_script = internal_result[:result]["consoleReplayScript"] From a75d7a0a62a95e8566ae9be7324bc1133ad5aa1c Mon Sep 17 00:00:00 2001 From: Gus Date: Sat, 23 Oct 2021 12:06:19 -0300 Subject: [PATCH 02/14] Add Webpack configuration files to the template on generator --- .../templates/base/base/babel.config.js | 103 +++++++++++++++ .../config/webpack/clientWebpackConfig.js | 15 +++ .../config/webpack/commonWebpackConfig.js | 14 ++ .../base/base/config/webpack/development.js | 40 ++++++ .../base/base/config/webpack/production.js | 11 ++ .../config/webpack/serverWebpackConfig.js | 120 ++++++++++++++++++ .../base/base/config/webpack/test.js | 9 ++ .../base/base/config/webpack/webpackConfig.js | 34 +++++ .../templates/base/base/config/webpacker.yml | 62 +++++++++ 9 files changed, 408 insertions(+) create mode 100644 lib/generators/react_on_rails/templates/base/base/babel.config.js create mode 100644 lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js create mode 100644 lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js create mode 100644 lib/generators/react_on_rails/templates/base/base/config/webpack/development.js create mode 100644 lib/generators/react_on_rails/templates/base/base/config/webpack/production.js create mode 100644 lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js create mode 100644 lib/generators/react_on_rails/templates/base/base/config/webpack/test.js create mode 100644 lib/generators/react_on_rails/templates/base/base/config/webpack/webpackConfig.js create mode 100644 lib/generators/react_on_rails/templates/base/base/config/webpacker.yml diff --git a/lib/generators/react_on_rails/templates/base/base/babel.config.js b/lib/generators/react_on_rails/templates/base/base/babel.config.js new file mode 100644 index 000000000..1515815ce --- /dev/null +++ b/lib/generators/react_on_rails/templates/base/base/babel.config.js @@ -0,0 +1,103 @@ +module.exports = function (api) { + var validEnv = ['development', 'test', 'production'] + var currentEnv = api.env() + // https://babeljs.io/docs/en/config-files#apienv + // api.env is almost the NODE_ENV + var isDevelopmentEnv = api.env('development') + var isProductionEnv = api.env('production') + var isTestEnv = api.env('test') + + if ( !validEnv.includes(currentEnv)) { + throw new Error( + 'Please specify a valid `NODE_ENV` or ' + + '`BABEL_ENV` environment variables. Valid values are "development", ' + + '"test", and "production". Instead, received: ' + + JSON.stringify(currentEnv) + + '.' + ) + } + + return { + presets: [ + isTestEnv && [ + '@babel/preset-env', + { + targets: { + node: 'current' + }, + modules: 'commonjs' + }, + '@babel/preset-react' + ], + (isProductionEnv || isDevelopmentEnv) && [ + '@babel/preset-env', + { + forceAllTransforms: true, + useBuiltIns: 'entry', + corejs: 3, + modules: false, + exclude: ['transform-typeof-symbol'] + } + ], + [ + '@babel/preset-react', + { + development: isDevelopmentEnv || isTestEnv, + useBuiltIns: true + } + ], + ['@babel/preset-typescript', {allExtensions: true, isTSX: true}] + ].filter(Boolean), + plugins: [ + 'babel-plugin-macros', + '@babel/plugin-syntax-dynamic-import', + isTestEnv && 'babel-plugin-dynamic-import-node', + '@babel/plugin-transform-destructuring', + [ + '@babel/plugin-proposal-class-properties', + { + loose: true + } + ], + [ + '@babel/plugin-proposal-object-rest-spread', + { + useBuiltIns: true + } + ], + [ + '@babel/plugin-transform-runtime', + { + helpers: false, + regenerator: true, + corejs: false + } + ], + [ + '@babel/plugin-transform-regenerator', + { + async: false + } + ], + [ + "@babel/plugin-proposal-private-property-in-object", + { + "loose": true + } + ], + [ + "@babel/plugin-proposal-private-methods", + { + loose: true + } + ], + process.env.WEBPACK_SERVE && 'react-refresh/babel', + isProductionEnv && [ + 'babel-plugin-transform-react-remove-prop-types', + { + removeImport: true + } + ] + ].filter(Boolean) + } +} diff --git a/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js b/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js new file mode 100644 index 000000000..ddc944160 --- /dev/null +++ b/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js @@ -0,0 +1,15 @@ +const commonWebpackConfig = require('./commonWebpackConfig') + +const configureClient = () => { + const clientConfig = commonWebpackConfig() + + // 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 diff --git a/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js b/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js new file mode 100644 index 000000000..e93fa5ba8 --- /dev/null +++ b/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js @@ -0,0 +1,14 @@ +// Common configuration applying to client and server configuration + +const { webpackConfig: baseClientWebpackConfig, merge } = require('@rails/webpacker') + +const commonOptions = { + resolve: { + extensions: ['.css', '.ts', '.tsx'] + } +} + +// Copy the object using merge b/c the baseClientWebpackConfig and commonOptions are mutable globals +const commonWebpackConfig = () => (merge({}, baseClientWebpackConfig, commonOptions)) + +module.exports = commonWebpackConfig diff --git a/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js b/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js new file mode 100644 index 000000000..cd5bb0eb8 --- /dev/null +++ b/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js @@ -0,0 +1,40 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'development' + +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin') +const path = require('path') +const { devServer, inliningCss } = require('@rails/webpacker') + +const webpackConfig = require('./webpackConfig') + +const developmentEnvOnly = (clientWebpackConfig, serverWebpackConfig) => { + + const isWebpackDevServer = process.env.WEBPACK_DEV_SERVER + + //plugins + if (inliningCss ) { + // Note, when this is run, we're building the server and client bundles in separate processes. + // Thus, this plugin is not applied. + const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin') + clientWebpackConfig.plugins.push( + new ReactRefreshWebpackPlugin({ + overlay:{ + sockPort: devServer.port + } + }) + ) + } + + // To support TypeScript type checker on a separate process uncomment the block below and add tsconfig.json + // to the root directory. + // As a reference visit https://github.com/shakacode/react_on_rails_tutorial_with_ssr_and_hmr_fast_refresh/blob/master/config/webpack/development.js + + // clientWebpackConfig.plugins.push( + // new ForkTsCheckerWebpackPlugin({ + // typescript: { + // configFile: path.resolve(__dirname, '../../tsconfig.json') + // }, + // async: false + // }) + // ) +} +module.exports = webpackConfig(developmentEnvOnly) diff --git a/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js b/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js new file mode 100644 index 000000000..86ee77315 --- /dev/null +++ b/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js @@ -0,0 +1,11 @@ +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 webpackConfig = require('./webpackConfig') + +const productionEnvOnly = (_clientWebpackConfig, _serverWebpackConfig) => { + // place any code here that is for production only +} + +module.exports = webpackConfig(productionEnvOnly) diff --git a/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js b/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js new file mode 100644 index 000000000..4edf3eeac --- /dev/null +++ b/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js @@ -0,0 +1,120 @@ +const { merge, config } = require('@rails/webpacker') +const commonWebpackConfig = require('./commonWebpackConfig') + +const webpack = require('webpack') + +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 = commonWebpackConfig() + + // We just want the single server bundle entry + const serverEntry = { + 'server-bundle': serverWebpackConfig.entry['server-bundle'] + } + + if (!serverEntry['server-bundle']) { + throw new Error('Create a pack with the file name \'server-bundle.js\' containing all the server rendering files') + } + + 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.publicPath, + // 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' + ) + + // Configure loader rules for SSR + // Remove the mini-css-extract-plugin from the style loaders because + // the client build will handle exporting CSS. + // replace file-loader with null-loader + const rules = serverWebpackConfig.module.rules; + rules.forEach((rule) => { + if (Array.isArray(rule.use)) { + // remove the mini-css-extract-plugin and style-loader + rule.use = rule.use.filter((item) => { + let testValue; + if (typeof item === 'string') { + testValue = item; + } else if (typeof item.loader === 'string') { + testValue = item.loader; + } + return !(testValue.match(/mini-css-extract-plugin/) || testValue === 'style-loader'); + }); + const cssLoader = rule.use.find((item) => { + let testValue; + + if (typeof item === 'string') { + testValue = item; + } else if (typeof item.loader === 'string') { + testValue = item.loader; + } + + return testValue.includes('css-loader'); + }); + if (cssLoader && cssLoader.options) { + cssLoader.options.modules = { exportOnlyLocals: true }; + } + + // Skip writing image files during SSR by setting emitFile to false + } else if (rule.use && (rule.use.loader === 'url-loader' || rule.use.loader === 'file-loader')) { + rule.use.options.emitFile = false; + } + }); + + // TODO: DELETE NEXT 2 LINES + // 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 diff --git a/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js b/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js new file mode 100644 index 000000000..065efa78c --- /dev/null +++ b/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js @@ -0,0 +1,9 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'development' + +const webpackConfig = require('./webpackConfig') + +const testOnly = (_clientWebpackConfig, _serverWebpackConfig) => { + // place any code here that is for test only +} + +module.exports = webpackConfig(testOnly) diff --git a/lib/generators/react_on_rails/templates/base/base/config/webpack/webpackConfig.js b/lib/generators/react_on_rails/templates/base/base/config/webpack/webpackConfig.js new file mode 100644 index 000000000..5f52597e2 --- /dev/null +++ b/lib/generators/react_on_rails/templates/base/base/config/webpack/webpackConfig.js @@ -0,0 +1,34 @@ +const clientWebpackConfig = require('./clientWebpackConfig') +const serverWebpackConfig = require('./serverWebpackConfig') + +const webpackConfig = (envSpecific) => { + const clientConfig = clientWebpackConfig() + const serverConfig = serverWebpackConfig() + + if (envSpecific) { + envSpecific(clientConfig, serverConfig) + } + + let result + // For HMR, need to separate the the client and server webpack configurations + if (process.env.WEBPACK_SERVE || 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 diff --git a/lib/generators/react_on_rails/templates/base/base/config/webpacker.yml b/lib/generators/react_on_rails/templates/base/base/config/webpacker.yml new file mode 100644 index 000000000..a3de9872b --- /dev/null +++ b/lib/generators/react_on_rails/templates/base/base/config/webpacker.yml @@ -0,0 +1,62 @@ +# Note: You must restart bin/webpack-dev-server for changes to take effect + +default: &default + source_path: app/javascript + source_entry_path: packs + public_root_path: public + public_output_path: packs + cache_path: tmp/webpacker + webpack_compile_output: true + + # Additional paths webpack should lookup modules + # ['app/assets', 'engine/foo/app/assets'] + additional_paths: [] + + # Reload manifest.json on all requests so we reload latest compiled packs + cache_manifest: false + +development: + <<: *default + # This is false since we're running `bin/webpack -w` in Procfile.dev-setic + compile: false + + # Reference: https://webpack.js.org/configuration/dev-server/ + dev_server: + https: false + host: localhost + port: 3035 + # Hot Module Replacement updates modules while the application is running without a full reload + hmr: true + client: + # Should we show a full-screen overlay in the browser when there are compiler errors or warnings? + overlay: true + # May also be a string + # webSocketURL: + # hostname: "0.0.0.0" + # pathname: "/ws" + # port: 8080 + compress: true + # Note that apps that do not check the host are vulnerable to DNS rebinding attacks + allowed_hosts: [ 'localhost' ] + pretty: true + headers: + 'Access-Control-Allow-Origin': '*' + static: + watch: + ignored: '**/node_modules/**' + +test: + <<: *default + compile: true + + # Compile test packs to a separate directory + public_output_path: packs-test + +production: + <<: *default + + # Production depends on precompilation of packs prior to booting for performance. + compile: false + + # Cache manifest.json for performance + cache_manifest: true From c8dae5fe8ca816a8e82af97dafae3bbc74a46941 Mon Sep 17 00:00:00 2001 From: Gus Date: Sat, 23 Oct 2021 12:11:03 -0300 Subject: [PATCH 03/14] Update hello world component files --- .../bundles/HelloWorld/components/HelloWorld.jsx | 3 ++- .../bundles/HelloWorld/components/HelloWorld.module.css | 3 +++ .../bundles/HelloWorld/components/HelloWorldServer.js | 5 +++++ .../base/base/app/javascript/packs/server-bundle.js | 8 ++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css create mode 100644 lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorldServer.js create mode 100644 lib/generators/react_on_rails/templates/base/base/app/javascript/packs/server-bundle.js diff --git a/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx b/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx index f7e0487e3..bc60ffebe 100644 --- a/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +++ b/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, { useState } from 'react'; +import style from './HelloWorld.module.css'; const HelloWorld = (props) => { const [name, setName] = useState(props.name); @@ -9,7 +10,7 @@ const HelloWorld = (props) => {

Hello, {name}!


-