CRACO и react-app-rewired: переопределение без eject

Экосистема Create React App (CRA) изначально проектировалась как инструмент быстрого старта для React-приложений без необходимости ручной настройки сборщика. Внутри используется Webpack, Babel, ESLint и набор предустановленных конфигураций, скрытых от разработчика. Такой подход упрощает старт, но вводит жёсткие ограничения на изменение поведения сборки.

Основная проблема возникает в момент, когда требуется:

  • изменить конфигурацию Webpack (алиасы, лоадеры, плагины)
  • расширить Babel (декораторы, нестандартные трансформации)
  • настроить PostCSS или CSS Modules нестандартным образом
  • интегрировать монорепозитории или микрофронтенды
  • подключить нестандартные форматы файлов

CRA не предоставляет доступа к внутреннему Webpack-конфигу без выполнения eject. Команда eject извлекает всю конфигурацию в проект, но после этого теряется поддержка обновлений CRA и увеличивается сложность сопровождения.

На этом фоне появились альтернативные подходы, позволяющие переопределять конфигурацию без eject: react-app-rewired и CRACO.


Архитектурная модель CRA и точка расширения

Внутри CRA используется скрытый пакет react-scripts, который содержит:

  • webpack.config.js
  • babel.config.js (встроенный через babel-preset-react-app)
  • eslint-конфигурацию
  • dev-server настройки

Запуск приложения происходит через команды:

  • react-scripts start
  • react-scripts build
  • react-scripts test

Вся конфигурация инкапсулирована. Единственная официальная точка расширения — переменные окружения и ограниченный набор override-флагов.

Решение override-подхода основано на перехвате конфигурации перед её передачей в Webpack.


react-app-rewired: перехват конфигурации CRA

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

Структура config-overrides.js

Основная идея заключается в том, что CRA передаёт исходный Webpack-конфиг в функцию override:

module.exports = function override(config, env) {
  return config;
};

Типичные модификации

Добавление alias

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;
};

Добавление нового loader

module.exports = function override(config) {
  config.module.rules.push({
    test: /\.md$/,
    use: "raw-loader"
  });

  return config;
};

Изменение Babel конфигурации через loader

module.exports = function override(config) {
  const babelLoader = config.module.rules.find(
    rule => rule.oneOf
  );

  return config;
};

На практике работа с CRA-конфигом требует глубокого знания структуры Webpack, поскольку он состоит из вложенных oneOf правил.


Ограничения react-app-rewired

Несмотря на гибкость, подход имеет ряд архитектурных проблем:

Хрупкость структуры конфигурации

CRA не гарантирует стабильность внутреннего Webpack-конфига между версиями. Любое обновление react-scripts может изменить:

  • порядок rules
  • структуру oneOf
  • loader-цепочки

Это приводит к необходимости постоянного адаптирования override-логики.

Отсутствие типизации конфигурации

Webpack-конфиг модифицируется напрямую как объект JavaScript. Это приводит к:

  • отсутствию автодополнения
  • сложной отладке
  • высокой вероятности ошибок

Ограниченный контроль над плагинами

Некоторые плагины CRA создаются внутри react-scripts и не всегда доступны для переопределения без глубокого патчинга конфигурации.


CRACO: более современный слой абстракции

CRACO (Create React App Configuration Override) решает те же задачи, но предоставляет более структурированный API для модификации CRA без eject.

Принцип работы CRACO

CRACO также заменяет react-scripts, но вводит промежуточный слой конфигурации:

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

Основной файл:

craco.config.js

Структура craco.config.js

Базовая конфигурация

module.exports = {
  webpack: {
    alias: {
      "@components": require("path").resolve(__dirname, "src/components")
    }
  }
};

CRACO предоставляет декларативный API поверх Webpack-конфига.


Расширение через плагины CRACO

Одним из ключевых преимуществ является система плагинов.

Пример плагина

module.exports = {
  plugins: [
    {
      plugin: {
        overrideWebpackConfig: ({ webpackConfig }) => {
          webpackConfig.module.rules.push({
            test: /\.svg$/,
            use: ["@svgr/webpack"]
          });

          return webpackConfig;
        }
      }
    }
  ]
};

Это позволяет:

  • изолировать логику расширений
  • переиспользовать конфигурации
  • уменьшить хрупкость override-логики

Работа с TypeScript

CRACO поддерживает расширение TypeScript-конфигурации без eject.

Изменение tsconfig path mapping

module.exports = {
  webpack: {
    alias: {
      "@app": "src/app",
      "@shared": "src/shared"
    }
  }
};

В tsconfig.json:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@app/*": ["src/app/*"],
      "@shared/*": ["src/shared/*"]
    }
  }
}

Управление Babel через CRACO

CRACO позволяет изменять Babel-плагины:

module.exports = {
  babel: {
    plugins: [
      ["@babel/plugin-proposal-decorators", { legacy: true }]
    ]
  }
};

Это важно для проектов, использующих:

  • MobX с декораторами
  • legacy TypeScript decorators
  • экспериментальные синтаксические предложения

Сравнение react-app-rewired и CRACO

Архитектурная модель

react-app-rewired:

  • прямое изменение объекта config
  • минимальный слой абстракции
  • высокая гибкость, но высокая хрупкость

CRACO:

  • декларативный конфиг
  • плагинная архитектура
  • более стабильный API

Поддерживаемость

react-app-rewired:

  • сильная зависимость от внутренней структуры CRA
  • ручная работа с Webpack rules

CRACO:

  • изолированные расширения
  • предсказуемая структура конфигурации

Масштабируемость

react-app-rewired подходит для:

  • небольших проектов
  • точечных изменений конфигурации

CRACO эффективен для:

  • средних и крупных приложений
  • командной разработки
  • монорепозиториев

Интеграция с современным Webpack-стеком

Оба подхода работают поверх Webpack, который используется CRA как основа сборки.

Типичные расширения включают:

  • добавление новых loaders (raw-loader, svgr, url-loader)
  • настройка code splitting через dynamic import
  • оптимизация bundle analyzer
  • кастомизация dev-server proxy

Переопределение dev-server

CRACO позволяет модифицировать devServer:

module.exports = {
  devServer: {
    proxy: {
      "/api": "http://localhost:3001"
    }
  }
};

Это особенно важно для:

  • разделения frontend/backend
  • локальной разработки микросервисов
  • обхода CORS

Оптимизация сборки

Через оба инструмента возможно вмешательство в production build:

  • настройка TerserPlugin
  • управление splitChunks
  • удаление console.log
  • настройка asset compression

Пример:

module.exports = {
  webpack: {
    configure: (webpackConfig) => {
      webpackConfig.optimization.splitChunks = {
        chunks: "all"
      };

      return webpackConfig;
    }
  }
};

Типичные сценарии применения override-подхода

  • интеграция SVG как React компонентов
  • добавление монорепозитория (lerna, turborepo)
  • настройка абсолютных импортов
  • подключение legacy библиотек
  • внедрение нестандартных CSS препроцессоров
  • подключение WebAssembly модулей

Архитектурные риски override-слоя

Любой слой поверх CRA создаёт дополнительную зависимость от внутренней реализации react-scripts. Это приводит к следующим эффектам:

  • деградация при обновлении CRA
  • необходимость ручного тестирования конфигурации
  • рост сложности CI/CD пайплайнов
  • скрытая связность с Webpack версией

Особенно критично это проявляется при переходе CRA между major-версиями, когда структура Webpack полностью перестраивается.


Эволюция подходов к кастомизации CRA

Исторически развитие шло следующим образом:

  1. eject как единственный способ кастомизации
  2. react-app-rewired как минимальный override слой
  3. CRACO как структурированный конфигурационный слой
  4. переход части экосистемы на Vite и другие сборщики

Каждый следующий шаг уменьшает необходимость глубокого вмешательства в Webpack-конфигурацию, но сохраняет совместимость с CRA-проектами, которые уже существуют в продакшене.