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.jsis protected by@remove-on-ejectmarkers and must never be edited directly innode_modules.react-app-rewiredwithcustomize-craallows imperative mutation of the webpack config viaconfig-overrides.jsfor simple plugin and alias additions.@craco/cracooffers a declarative API viacraco.config.jsfor complex environment-specific overrides and TypeScript integration.- A custom
react-scriptsfork 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →