How to Add Custom Webpack Configuration to Create React App Without Ejecting

You cannot modify the internal packages/react-scripts/config/webpack.config.js directly because it is protected by @remove-on-eject markers, so you must use a wrapper tool like react-app-rewired or craco to programmatically extend the webpack configuration while keeping the benefits of a managed environment.

Create React App (CRA) intentionally shields its build pipeline to provide a stable, zero-configuration environment. However, when you need to modify the underlying webpack configuration—such as adding aliases, custom loaders, or analysis plugins—you must work around the internal packages/react-scripts/config/webpack.config.js file that is locked behind @remove-on-eject markers. Fortunately, you can inject custom webpack configuration into Create React App without ejecting by using community-maintained wrapper tools that programmatically extend the default setup.

Why the Internal Webpack Config Is Protected

The canonical webpack configuration lives in packages/react-scripts/config/webpack.config.js within the react-scripts package. This file contains the @remove-on-eject annotation pattern, which signals that the file is ephemeral and will be replaced during react-scripts upgrades. According to the facebook/create-react-app source code, any manual edits to this file would be overwritten on the next dependency update, making direct modification unsustainable.

The packages/react-scripts/config/paths.js module defines critical filesystem references like paths.appSrc and paths.appBuild that the webpack configuration consumes. While you can inspect these files to understand the default behavior, the architecture deliberately prevents runtime extension points—unlike packages/babel-preset-react-app/webpack-overrides.js, which exposes a minimal macro detection override but does not provide a user-level API for general webpack customization.

Method 1: Override with react-app-rewired and customize-cra

The react-app-rewired package replaces the react-scripts binary with a thin wrapper that loads a user-provided config-overrides.js. The wrapper calls the original CRA config function, lets you mutate the returned configuration object, and then hands it back to webpack.

Installation and Setup

Install the wrapper and helper utilities as development dependencies:

npm install -D react-app-rewired customize-cra

Update your package.json scripts to use the rewired binary instead of the default react-scripts:

{
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test"
  }
}

Adding Plugins and Aliases

Create a config-overrides.js file at the project root. This file exports a function that receives the default webpack configuration and returns the modified object:

// config-overrides.js
const { override, addWebpackPlugin, addWebpackAlias } = require('customize-cra');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const path = require('path');

module.exports = override(
  // Add a new plugin
  addWebpackPlugin(
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false,
      reportFilename: 'bundle-report.html',
    })
  ),

  // Add a custom alias to resolve "~components" to src/components
  addWebpackAlias({
    '~components': path.resolve(__dirname, 'src/components'),
  })
);

Running npm run build now generates a bundle-report.html file and enables imports from ~components/... without ejecting.

Method 2: Use CRACO for Structured Configuration

CRACO (Create React App Configuration Override) provides a richer API than react-app-rewired and integrates with TypeScript, ESLint, and other CRA subsystems out-of-the-box. It uses a craco.config.js file that merges with CRA’s internal configuration via a structured API rather than imperative mutation.

Installation and Configuration

Install the package:

npm install -D @craco/craco

Replace the default scripts in package.json:

{
  "scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test"
  }
}

Modifying Loaders and Rules

Create a craco.config.js file to add aliases, plugins, or modify existing loader rules:

// craco.config.js
const path = require('path');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  webpack: {
    alias: {
      '@utils': path.resolve(__dirname, 'src/utils/'),
    },
    plugins: {
      add: [
        new BundleAnalyzerPlugin({
          analyzerMode: process.env.NODE_ENV === 'production' ? 'static' : 'disabled',
        }),
      ],
    },
    // Modify existing rule to support .svg as React components
    configure: (webpackConfig) => {
      const fileLoaderRule = webpackConfig.module.rules.find(
        (rule) => rule.oneOf && rule.oneOf.find((r) => r.loader && r.loader.includes('file-loader'))
      );
      if (fileLoaderRule) {
        const svgRule = {
          test: /\.svg$/,
          use: ['@svgr/webpack', 'file-loader'],
        };
        // Insert before the file-loader rule to intercept SVGs
        fileLoaderRule.oneOf.unshift(svgRule);
      }
      return webpackConfig;
    },
  },
};

This configuration enables importing SVGs as React components (import { ReactComponent as Logo } from './logo.svg') and uses @utils as a path alias without touching the internal webpack.config.js.

Method 3: Fork react-scripts for Deep Customization

For scenarios requiring changes that wrapper tools cannot express—such as replacing the entire module resolution pipeline or modifying the paths.js logic—you can maintain a custom react-scripts fork. Clone the react-scripts package, apply your changes to its internal packages/react-scripts/config/webpack.config.js, and reference the fork in your project’s package.json:

{
  "react-scripts": "github:yourusername/react-scripts#your-branch"
}

This approach provides complete control but requires manual synchronization with upstream CRA updates to receive security patches and new features.

Summary

  • packages/react-scripts/config/webpack.config.js is protected by @remove-on-eject markers and must never be edited directly in node_modules.
  • react-app-rewired with customize-cra allows imperative mutation of the webpack config via config-overrides.js for simple plugin and alias additions.
  • @craco/craco offers a declarative API via craco.config.js for complex environment-specific overrides and TypeScript integration.
  • A custom react-scripts fork is the only path for deep architectural changes that wrappers cannot accommodate.
  • All methods allow you to run standard commands (npm start, npm run build, npm test) while transparently injecting your custom webpack settings.

Frequently Asked Questions

Can I directly edit the webpack.config.js file in node_modules without ejecting?

No. The file located at packages/react-scripts/config/webpack.config.js contains @remove-on-eject markers and is regenerated on every react-scripts upgrade. Any manual changes would be overwritten, breaking your build pipeline and preventing clean updates.

What is the difference between react-app-rewired and CRACO?

react-app-rewired provides a minimal wrapper that calls a user-supplied function to mutate the webpack config object directly. CRACO offers a structured configuration API with dedicated sections for webpack, babel, eslint, and devServer, making it better suited for complex, multi-faceted customizations that need to coordinate across different build tools.

Will using a wrapper tool prevent me from updating CRA?

No. Both react-app-rewired and CRACO act as transparent proxies to the underlying react-scripts commands. They intercept the configuration at runtime but do not modify the react-scripts package itself, allowing you to upgrade CRA versions normally. However, you should verify wrapper compatibility with major CRA releases in the wrapper's changelog.

How do I add a simple path alias without ejecting?

Use either the addWebpackAlias helper from customize-cra in a config-overrides.js file, or define the webpack.alias property in a craco.config.js file. Both methods inject the alias into the resolve configuration of the internal webpack setup without requiring ejection.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →