Create React App: Customize Webpack Config Without Ejecting

By Dave Ceddia Comment

Updated August 16, 2017 – Fixed to work with Create React App 1.0.11

Create React App comes with a great config out of the box, and it has the “eject” feature for when you want to take the config into your own hands.

But what if you just want to add a couple little tweaks to the Webpack config without having to keep the entire config up to date by yourself? You might want to add SASS or SCSS support or use your own .eslintrc file.

We’ll go over how to do that here.

Warning!

First, a word of warning: if you are unfamiliar with how Webpack works, or are not comfortable maintaining the (small amount of) hacky code that we’ll produce here, I recommend skipping this one. This is rather advanced magic.

As Create React App (specifically its react-scripts package) changes, it is HIGHLY LIKELY that the code below will eventually need some repairs. If their Webpack config changes structure, or it’s exported differently, or any number of other things happens, this code will break, and it will be up to you to figure out how it works and fix it. So, again: if you’re not comfortable with that idea, don’t do this.

Hacking Create React App

Ok, with that scary disclaimer out of the way, let’s figure out how to hack Create React App. If you want to skip straight to the Example Project, that’s fine too. Before you get started, make sure you’re running the latest react-scripts, which at the time of writing is 1.0.11.

The underpinnings of Create React App are housed in the ‘react-scripts’ package, which you’ll see listed under “dependencies” in package.json.

We’re going to use rewire to monkey-patch react-scripts and allow us to customize the Webpack config before it executes.

This file here is the biggest piece of that puzzle. I suggest making a directory called “scripts” inside your CRA project and putting this code in scripts/customized-config.js. You can name it whatever you like, though (we’ll need the name again later).

scripts/customized-config.js
/*
  This module runs the scripts from react-scripts (Create React App)
  and gives an opportunity to override the Webpack config by creating
  "config-overrides.dev.js" and/or "config-overrides.prod.js" files in the
  root of the project.

  A config-overrides file should export a single function that takes a
  config and modifies it as necessary.

  module.exports = function(webpackConfig) {
    webpackConfig.module.rules[0].use[0].options.useEslintrc = true;
  };
*/
var rewire = require('rewire');
var proxyquire = require('proxyquire');

switch(process.argv[2]) {
  // The "start" script is run during development mode
  case 'start':
    rewireModule('react-scripts/scripts/start.js', loadCustomizer('../config-overrides.dev'));
    break;
  // The "build" script is run to produce a production bundle
  case 'build':
    rewireModule('react-scripts/scripts/build.js', loadCustomizer('../config-overrides.prod'));
    break;
  // The "test" script runs all the tests with Jest
  case 'test':
    // Load customizations from the config-overrides.testing file.
    // That file should export a single function that takes a config and returns a config
    let customizer = loadCustomizer('../config-overrides.testing');
    proxyquire('react-scripts/scripts/test.js', {
      // When test.js asks for '../utils/createJestConfig' it will get this instead:
      '../utils/createJestConfig': (...args) => {
        // Use the existing createJestConfig function to create a config, then pass
        // it through the customizer
        var createJestConfig = require('react-scripts/utils/createJestConfig');
        return customizer(createJestConfig(...args));
      }
    });
    break;
  default:
    console.log('customized-config only supports "start", "build", and "test" options.');
    process.exit(-1);
}

// Attempt to load the given module and return null if it fails.
function loadCustomizer(module) {
  try {
    return require(module);
  } catch(e) {
    if(e.code !== "MODULE_NOT_FOUND") {
      throw e;
    }
  }

  // If the module doesn't exist, return a
  // noop that simply returns the config it's given.
  return config => config;
}

function rewireModule(modulePath, customizer) {
  // Load the module with `rewire`, which allows modifying the
  // script's internal variables.
  let defaults = rewire(modulePath);

  // Reach into the module, grab its global 'config' variable,
  // and pass it through the customizer function.
  // The customizer should *mutate* the config object, because
  // react-scripts imports the config as a `const` and we can't
  // modify that reference.
  let config = defaults.__get__('config');
  customizer(config);
}

To make this work, you’ll need to install a few extra packages:

npm install --save rewire proxyquire

You can pretty much read the comments to figure out how it works. The interesting part is the rewireModule function at the bottom, which uses the rewire library to peek into another file and grab a reference to the config variable defined there.

Once you’ve got that in place, you can write the config-overrides files for dev, prod, and test. This part is really up to you – whatever changes you need to make to CRA’s Webpack config, go right ahead.

These files should go directly in the root of your CRA folder, and all 3 are optional. If you want to relocate them, just change the path in the “loadCustomizer” calls above. Just don’t put them in “src”.

Here’s an example of some dev overrides:

config-overrides.dev.js
const path = require('path');

module.exports = function(config) {
  // Use your own ESLint file
  let eslintLoader = config.module.rules[0];
  eslintLoader.use[0].options.useEslintrc = true;

  // Add the SASS loader second-to-last
  // (last one must remain as the "file-loader")
  let loaderList = config.module.rules[1].oneOf;
  loaderList.splice(loaderList.length - 1, 0, {
    test: /\.scss$/,
    use: ["style-loader", "css-loader", "sass-loader"]
  });
}

You would want to also create a config-overrides.prod.js file with the same contents. The dev file is used during development (e.g. npm start) and the prod file is used during the build (e.g. npm run build).

To make this work you’ll need to install the SASS loader, and its peer dependency, node-sass:

npm install --save sass-loader node-sass

Finally, to trigger all this new code, you’ll need to change package.json to call this new customized-config script instead of the default react-scripts. To do that, replace the “start”, “build”, and “test” lines with these:

package.json
"scripts": {
  "start": "node scripts/customized-config start",
  "build": "node scripts/customized-config build",
  "test": "node scripts/customized-config test --env=jsdom",
}

Example Project

A CRA-generated project with these mods applied is up on Github here.

comments powered by Disqus