So you have your custom frontend project with a huge webpack configuration file, while enviously looking at some newer code bases based on Create React App from Facebook? Or, you've started out with a vanilla Create React App,
but felt that you needed to eject (even though many advised you not to)?
Either way, wouldn't it be nice to be able to re-use some of that magic in a dependency-kind-of-way? After all, Facebook's configuration is probably more tested and thought-through than yours! :)
Well, you can! All that Webpack magic, incl. Jest and Webpack Dev Server configuration, is packaged by Facebook in react-scripts. Internally, Create React App is using it, but that doesn't help us, right? In this post, I'll show you how.
In order to be able to tap into react-scripts we first need to install them as a dependency using
This is where the fun begins!
In essence, you create a
Next thing is to use this shared config for your production build by adding a
Next up is to handle the Webpack Dev Server configuration, which can be achieved pretty much the same way - create a
Even though the setup is a little bit tricky, you'll reap all the benefits of relying on Facebook to do the configuration for you. And even better, when they update it, you only have to bump the version number of react-scripts!
I understand that this isn't for everyone and there are absolutely many ways to skin a cat - if you like it, use it. If not - don't!
A complete example project can be found here.
Either way, wouldn't it be nice to be able to re-use some of that magic in a dependency-kind-of-way? After all, Facebook's configuration is probably more tested and thought-through than yours! :)
Well, you can! All that Webpack magic, incl. Jest and Webpack Dev Server configuration, is packaged by Facebook in react-scripts. Internally, Create React App is using it, but that doesn't help us, right? In this post, I'll show you how.
In order to be able to tap into react-scripts we first need to install them as a dependency using
npm install react-scripts --save-dev
.This is where the fun begins!
In essence, you create a
webpack.shared.config.js
and import the base config from react-scripts and merge that with any additional config you might need. For things in the base config that you want to remove,
you'll have to do some additional clean-up after the merge, see the example config with comments below:
const webpack = require("webpack"); // note that react-scripts uses it's own webpack dependency (and hence version) from react-scripts/node_modules/webpack const reactScriptsWebpack = require("react-scripts/node_modules/webpack"); const webpackVersion = require("react-scripts/node_modules/webpack/package.json"); const chalk = require("chalk"); const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const merge = require("webpack-merge"); const createBaseConfig = require("react-scripts/config/webpack.config"); const WorkboxWebpackPlugin = require("workbox-webpack-plugin"); const path = require("path"); const paths = require("./paths"); module.exports = (env, argv) => { // eslint-disable-next-line no-console console.log( chalk.cyan.bold( "Creating webpack config based on webpack v" + webpackVersion.version ) ); const baseConfig = createBaseConfig(process.env.NODE_ENV); const sharedConfig = merge(baseConfig, { entry: { // replace entire entry-array, as the webpack-dev-server CLI adds client- and hot entries by default, see https://github.com/webpack/webpack-dev-server/blob/143762596682d8da4fdc73555880be05255734d7/lib/utils/addEntries.js#L22 // react-scripts uses the Node API and adds them manually (i.e. we get them twice) which we don't want, see https://github.com/facebook/create-react-app/blob/2c6dd45cf4cb81f3cbd2e4073713647ebd033bbf/packages/react-scripts/config/webpack.config.js#L145 home: [path.resolve(__dirname, "../src/index")] }, output: { path: path.resolve(__dirname, paths.bundleOutputDir) }, plugins: [ new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: [ path.resolve(__dirname, paths.bundleOutputDir), path.resolve(__dirname, paths.reportsOutputDir) ] }) ] }); // remove WorkboxWebpackPlugin sharedConfig.plugins = sharedConfig.plugins.filter( plugin => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW) ); // make it possible to extend eslint rules const eslintLoader = sharedConfig.module.rules.find( rule => rule.enforce === "pre" && rule.use[0].loader === require.resolve("eslint-loader") ); eslintLoader.use[0].options.baseConfig = { extends: require.resolve("../.eslintrc") }; // replace react-scripts/node_modules/webpack version of HotModuleReplacementPlugin with our version (or we'll get a mismatch of the actual webpack reference) const index = sharedConfig.plugins.findIndex( plugin => plugin instanceof reactScriptsWebpack.HotModuleReplacementPlugin ); if (index >= 0) { sharedConfig.plugins.splice( index, 1, new webpack.HotModuleReplacementPlugin() ); } // create less-rule by using sass rule as template const oneOf = sharedConfig.module.rules.find(rule => "oneOf" in rule).oneOf; const sassRule = oneOf.find(rule => RegExp(rule.test).test(".sass")); const sassLoader = sassRule.use.find(loader => /sass-loader/.test(loader.loader) ); const otherLoaders = sassRule.use.filter( loader => loader.loader !== sassLoader.loader ); const lessRule = { test: /\.less$/, // use all loaders but sassLoader and end off with less-loader use: otherLoaders.concat({ loader: require.resolve("less-loader"), // reuse options from sassLoader options: sassLoader.options }), sideEffects: true }; // insert less rule before css-rules oneOf.splice( oneOf.findIndex(rule => RegExp(rule.test).test(".css")), 0, lessRule ); return sharedConfig; };Notice e.g. the addition of handling of .less-files.
Next thing is to use this shared config for your production build by adding a
webpack.prod.config.js
where you put production specific config, e.g. process.env.NODE_ENV = "production"
to minimize the build.
process.env.NODE_ENV = "production"; process.env.BABEL_ENV = "production"; const chalk = require("chalk"); const merge = require("webpack-merge"); const getSharedConfig = require("./webpack.shared.config"); module.exports = (env, argv) => { // eslint-disable-next-line no-console console.log(chalk.cyan.bold("Starting prod build...")); const prodConfig = merge(getSharedConfig(env, argv), { // add extra configs here }); // eslint-disable-next-line no-console console.log(prodConfig); return prodConfig; };Now, to use it, add
"build": "webpack --config ./config/webpack.prod.config.js"
into your package.json
and fire away!Next up is to handle the Webpack Dev Server configuration, which can be achieved pretty much the same way - create a
webpackDevServer.js
and import the base configuration from react-scripts. The base config is really
a function requiring proxy information and allowed hosts, which can be added through the prepareUrls utility function of WebpackDevServerUtils,
see example below:const merge = require("webpack-merge"); const createBaseConfig = require("react-scripts/config/webpackDevServer.config"); const { prepareUrls } = require("react-dev-utils/WebpackDevServerUtils"); const DEFAULT_PORT = 8080; const HOST = process.env.HOST || "0.0.0.0"; module.exports = () => { const baseConfig = createBaseConfig( undefined, prepareUrls("http", HOST, DEFAULT_PORT).lanUrlForConfig ); const webpackDevServerConfig = merge(baseConfig, { // add extra config here }); return webpackDevServerConfig; };Add the configuration above to a
webpack.dev.config.js
and use it by adding "start": "webpack-dev-server --config ./config/webpack.dev.config.js"
into your package.json
.
process.env.NODE_ENV = "development"; process.env.BABEL_ENV = "development"; const chalk = require("chalk"); const merge = require("webpack-merge"); const getSharedConfig = require("./webpack.shared.config"); const webpackDevServerConfig = require("./webpackDevServer"); module.exports = (env, argv) => { // eslint-disable-next-line no-console console.log(chalk.cyan.bold("Starting local build...")); const devConfig = merge(getSharedConfig(env, argv), { devServer: webpackDevServerConfig() }); // eslint-disable-next-line no-console console.log(devConfig); return devConfig; };Finally, if you're running unit tests using Jest, you can also get that configuration for free - just import the base configuration like previously and extend it with whatever you like. The configuration functions requires some handling to resolve relative paths, see example below:
process.env.BABEL_ENV = "test"; process.env.NODE_ENV = "test"; const merge = require("webpack-merge"); const chalk = require("chalk"); const path = require("path"); const createJestConfig = require("react-scripts/scripts/utils/createJestConfig"); const sharedJestConfig = createJestConfig( relativePath => path.resolve( require.resolve("react-scripts/scripts/utils/createJestConfig"), "..", "..", "..", relativePath ), path.resolve(path.resolve(__dirname, "..")), false ); module.exports = (argv => { // eslint-disable-next-line no-console console.log(chalk.cyan.bold("Starting test run...")); const config = merge(sharedJestConfig, { setupFilesAfterEnv: ["Use it by adding/config/jest.setup.js"] // https://facebook.github.io/create-react-app/docs/running-tests#initializing-test-environment, }); if (!argv.includes("--watch")) { config.collectCoverage = true; config.coverageDirectory = " /reports/coverage"; config.collectCoverageFrom = [ "src/**/*.{ts,tsx}", "!src/**/index.{ts,tsx}", "!src/**/*.d.ts" ]; config.coverageReporters = ["lcov", "html", "cobertura"]; } // eslint-disable-next-line no-console console.log(config); return config; })(process.argv); // don't mind these, something the formatter added...
"test": "jest --config ./config/jest.config.js"
to your package.json
.Even though the setup is a little bit tricky, you'll reap all the benefits of relying on Facebook to do the configuration for you. And even better, when they update it, you only have to bump the version number of react-scripts!
I understand that this isn't for everyone and there are absolutely many ways to skin a cat - if you like it, use it. If not - don't!
A complete example project can be found here.
Comments
Post a Comment