Экосистема Create React App (CRA) изначально проектировалась как инструмент быстрого старта для React-приложений без необходимости ручной настройки сборщика. Внутри используется Webpack, Babel, ESLint и набор предустановленных конфигураций, скрытых от разработчика. Такой подход упрощает старт, но вводит жёсткие ограничения на изменение поведения сборки.
Основная проблема возникает в момент, когда требуется:
CRA не предоставляет доступа к внутреннему Webpack-конфигу без выполнения eject. Команда eject извлекает всю конфигурацию в проект, но после этого теряется поддержка обновлений CRA и увеличивается сложность сопровождения.
На этом фоне появились альтернативные подходы, позволяющие переопределять конфигурацию без eject: react-app-rewired и CRACO.
Внутри CRA используется скрытый пакет react-scripts, который содержит:
Запуск приложения происходит через команды:
Вся конфигурация инкапсулирована. Единственная официальная точка расширения — переменные окружения и ограниченный набор override-флагов.
Решение override-подхода основано на перехвате конфигурации перед её передачей в Webpack.
react-app-rewired реализует механизм без eject за счёт замены стандартных скриптов react-scripts на собственные обёртки.
В package.json происходит замена:
{
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test"
}
}
react-app-rewired перехватывает вызов CRA и предоставляет возможность модифицировать конфигурацию через файл:
config-overrides.js
Основная идея заключается в том, что CRA передаёт исходный Webpack-конфиг в функцию override:
module.exports = function override(config, env) {
return config;
};
const path = require('path');
module.exports = function override(config) {
config.resolve.alias = {
...config.resolve.alias,
"@components": path.resolve(__dirname, "src/components"),
"@utils": path.resolve(__dirname, "src/utils")
};
return config;
};
module.exports = function override(config) {
config.module.rules.push({
test: /\.md$/,
use: "raw-loader"
});
return config;
};
module.exports = function override(config) {
const babelLoader = config.module.rules.find(
rule => rule.oneOf
);
return config;
};
На практике работа с CRA-конфигом требует глубокого знания структуры
Webpack, поскольку он состоит из вложенных oneOf
правил.
Несмотря на гибкость, подход имеет ряд архитектурных проблем:
CRA не гарантирует стабильность внутреннего Webpack-конфига между версиями. Любое обновление react-scripts может изменить:
Это приводит к необходимости постоянного адаптирования override-логики.
Webpack-конфиг модифицируется напрямую как объект JavaScript. Это приводит к:
Некоторые плагины CRA создаются внутри react-scripts и не всегда доступны для переопределения без глубокого патчинга конфигурации.
CRACO (Create React App Configuration Override) решает те же задачи, но предоставляет более структурированный API для модификации CRA без eject.
CRACO также заменяет react-scripts, но вводит промежуточный слой конфигурации:
{
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test"
}
}
Основной файл:
craco.config.js
module.exports = {
webpack: {
alias: {
"@components": require("path").resolve(__dirname, "src/components")
}
}
};
CRACO предоставляет декларативный API поверх Webpack-конфига.
Одним из ключевых преимуществ является система плагинов.
module.exports = {
plugins: [
{
plugin: {
overrideWebpackConfig: ({ webpackConfig }) => {
webpackConfig.module.rules.push({
test: /\.svg$/,
use: ["@svgr/webpack"]
});
return webpackConfig;
}
}
}
]
};
Это позволяет:
CRACO поддерживает расширение TypeScript-конфигурации без eject.
module.exports = {
webpack: {
alias: {
"@app": "src/app",
"@shared": "src/shared"
}
}
};
В tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@app/*": ["src/app/*"],
"@shared/*": ["src/shared/*"]
}
}
}
CRACO позволяет изменять Babel-плагины:
module.exports = {
babel: {
plugins: [
["@babel/plugin-proposal-decorators", { legacy: true }]
]
}
};
Это важно для проектов, использующих:
react-app-rewired:
CRACO:
react-app-rewired:
CRACO:
react-app-rewired подходит для:
CRACO эффективен для:
Оба подхода работают поверх Webpack, который используется CRA как основа сборки.
Типичные расширения включают:
CRACO позволяет модифицировать devServer:
module.exports = {
devServer: {
proxy: {
"/api": "http://localhost:3001"
}
}
};
Это особенно важно для:
Через оба инструмента возможно вмешательство в production build:
Пример:
module.exports = {
webpack: {
configure: (webpackConfig) => {
webpackConfig.optimization.splitChunks = {
chunks: "all"
};
return webpackConfig;
}
}
};
Любой слой поверх CRA создаёт дополнительную зависимость от внутренней реализации react-scripts. Это приводит к следующим эффектам:
Особенно критично это проявляется при переходе CRA между major-версиями, когда структура Webpack полностью перестраивается.
Исторически развитие шло следующим образом:
Каждый следующий шаг уменьшает необходимость глубокого вмешательства в Webpack-конфигурацию, но сохраняет совместимость с CRA-проектами, которые уже существуют в продакшене.