CommonJS-зависимости и обходные пути
## Особенности CommonJS в экосистеме Vite
Vite ориентирован на современный стандарт ES Modules (ESM). Внутри dev-сервера используется нативная модульная система браузера, а сборка выполняется через Rollup. Из-за этого проекты, содержащие старые CommonJS-зависимости, регулярно сталкиваются с несовместимостью, ошибками импорта и нестабильным поведением модулей.
CommonJS был создан для Node.js задолго до появления ESM. Основные особенности CommonJS:
```js
const lib = require('./lib');
module.exports = {
value: 123
};
```
ESM использует другой синтаксис:
```js
import lib from './lib.js';
export const value = 123;
```
Главная проблема заключается не только в различии синтаксиса, но и в различной модели загрузки:
* CommonJS загружается синхронно
* ESM работает асинхронно
* CommonJS экспортирует объект `module.exports`
* ESM использует именованные и default-экспорты
* CommonJS динамичен
* ESM анализируется статически
Vite пытается автоматически преобразовывать CommonJS-пакеты в ESM, однако это работает не всегда.
---
## Почему CommonJS создаёт проблемы в Vite
Наиболее частые причины несовместимости:
### Использование `require`
```js
const lodash = require('lodash');
```
В браузере `require` отсутствует.
---
### Использование `module.exports`
```js
module.exports = function () {};
```
ESM ожидает:
```js
export default function () {}
```
---
### Динамические импорты через require
```js
const mod = require(name);
```
Vite не способен заранее проанализировать такой импорт.
---
### Зависимость от Node.js API
Многие старые CommonJS-библиотеки используют:
```js
const fs = require('fs');
const path = require('path');
```
В браузере этих API нет.
---
### Условные require
```js
if (process.env.NODE_ENV === 'development') {
require('./debug');
}
```
Такой код сложно оптимизировать и преобразовывать.
---
## Механизм обработки CommonJS в Vite
Vite использует несколько уровней преобразования.
### esbuild на этапе dev-сервера
Во время разработки Vite запускает предварительную оптимизацию зависимостей.
Основная задача:
* преобразование CommonJS в ESM
* ускорение загрузки
* объединение зависимостей
Пример:
```js
import react from 'react';
```
Даже если внутри React встречаются CommonJS-модули, esbuild пытается конвертировать их автоматически.
---
### Rollup во время production-сборки
При production-сборке Vite использует Rollup и плагин:
```txt
@rollup/plugin-commonjs
```
Именно он отвечает за преобразование CommonJS в финальном bundle.
---
## optimizeDeps и предварительная оптимизация
Один из главных инструментов работы с CommonJS — `optimizeDeps`.
### Принудительное включение зависимостей
Иногда Vite не может определить пакет автоматически.
Пример:
```js
// vite.config.js
export default {
optimizeDeps: {
include: ['legacy-library']
}
};
```
Это заставляет Vite заранее преобразовать пакет.
---
### Исключение проблемных зависимостей
Некоторые библиотеки ломаются после оптимизации.
```js
export default {
optimizeDeps: {
exclude: ['broken-package']
}
};
```
В этом случае пакет не будет обрабатываться esbuild.
---
### Полная конфигурация
```js
import { defineConfig } from 'vite';
export default defineConfig({
optimizeDeps: {
include: [
'lodash',
'uuid'
],
exclude: [
'problematic-lib'
]
}
});
```
---
## Ошибка “require is not defined”
Одна из самых распространённых ошибок.
### Причина
В браузере отсутствует CommonJS runtime.
Код:
```js
const axios = require('axios');
```
вызывает:
```txt
Uncaught ReferenceError: require is not defined
```
---
### Решение через import
Правильный вариант:
```js
import axios from 'axios';
```
---
### Частичная миграция legacy-кода
Иногда невозможно быстро переписать весь проект.
Допустим временный промежуточный слой:
```js
import pkg from 'legacy-lib';
const legacy = pkg.default || pkg;
```
---
## Ошибка default export у CommonJS-пакетов
Типичный пример:
```js
import moment from 'moment';
```
Иногда возникает:
```txt
does not provide an export named 'default'
```
---
## Причина
CommonJS не имеет настоящего default export.
Например:
```js
module.exports = fn;
```
может быть преобразован некорректно.
---
## Обходной путь
### Вариант с namespace import
```js
import * as moment from 'moment';
```
---
### Универсальный вариант
```js
import * as pkg from 'legacy-package';
const lib = pkg.default || pkg;
```
---
## CommonJS и named exports
Многие старые библиотеки экспортируют свойства динамически:
```js
module.exports.a = 1;
module.exports.b = 2;
```
Vite не всегда способен корректно построить именованные экспорты.
---
## Проблемный импорт
```js
import { a } from 'legacy-lib';
```
---
## Более безопасный подход
```js
import legacy from 'legacy-lib';
console.log(legacy.a);
```
---
## transformMixedEsModules
Некоторые библиотеки смешивают ESM и CommonJS в одном файле.
Пример:
```js
import x from './x';
const y = require('./y');
```
Подобный код может ломать Rollup.
---
## Решение
Настройка CommonJS-плагина:
```js
import { defineConfig } from 'vite';
export default defineConfig({
build: {
commonjsOptions: {
transformMixedEsModules: true
}
}
});
```
---
## dynamic require и ограничения Vite
Один из самых сложных сценариев:
```js
const moduleName = './langs/' + lang;
const locale = require(moduleName);
```
Vite не умеет статически анализировать такие конструкции.
---
## Почему это важно
Rollup строит граф зависимостей заранее.
Dynamic require:
* неизвестен во время сборки
* не может быть включён в bundle
* ломает tree-shaking
---
## Обходной путь через import.meta.glob
Vite предоставляет собственный механизм динамической загрузки.
### Вместо require
```js
const modules = import.meta.glob('./langs/*.js');
```
---
### Загрузка модуля
```js
const locale = await modules[`./langs/${lang}.js`]();
```
---
## Преимущества import.meta.glob
### Статический анализ
Vite заранее знает все возможные файлы.
---
### Поддержка code splitting
Каждый модуль может стать отдельным chunk.
---
### Совместимость с tree-shaking
Неиспользуемые модули исключаются.
---
## Работа с Node.js built-in модулями
Старые CommonJS-библиотеки часто используют:
```js
require('path');
require('crypto');
require('stream');
```
В браузере это вызывает ошибки.
---
## Типичные сообщения
```txt
Module "path" has been externalized
```
или:
```txt
Failed to resolve module fs
```
---
## Polyfill-подход
Иногда помогают browser-polyfills.
### Установка
```bash
npm install path-browserify
```
---
### Alias в Vite
```js
import { defineConfig } from 'vite';
export default defineConfig({
resolve: {
alias: {
path: 'path-browserify'
}
}
});
```
---
## Ограничения polyfill
Некоторые Node API невозможно полноценно реализовать в браузере:
* `fs`
* `child_process`
* `cluster`
* `net`
Подобные библиотеки часто непригодны для frontend-среды.
---
## CommonJS и SSR
В SSR-режиме ситуация отличается.
Node.js умеет работать с CommonJS напрямую, поэтому часть проблем исчезает.
---
## SSR externalization
По умолчанию Vite может не бандлить зависимости для SSR.
Иногда CommonJS-пакет вызывает ошибки.
---
## Настройка noExternal
```js
export default {
ssr: {
noExternal: ['legacy-lib']
}
};
```
Это заставляет Vite обработать пакет самостоятельно.
---
## Полная SSR-конфигурация
```js
import { defineConfig } from 'vite';
export default defineConfig({
ssr: {
noExternal: [
'legacy-lib',
'another-cjs-package'
]
}
});
```
---
## CommonJS и tree-shaking
CommonJS значительно хуже оптимизируется.
---
## Причина
ESM имеет статическую структуру:
```js
import { debounce } from 'lodash-es';
```
Bundler точно знает используемый экспорт.
---
## CommonJS менее предсказуем
```js
const _ = require('lodash');
```
Bundler часто вынужден включать всю библиотеку.
---
## Практическое последствие
Размер bundle существенно увеличивается.
---
## Предпочтение ESM-версий библиотек
Многие библиотеки выпускают две версии:
* CommonJS
* ESM
---
## Пример
Плохой вариант:
```js
import _ from 'lodash';
```
Лучший вариант:
```js
import debounce from 'lodash-es/debounce';
```
---
## Популярные ESM-альтернативы
| CommonJS | ESM |
| ------------------- | ----------------- |
| lodash | lodash-es |
| uuid старых версий | uuid новых версий |
| moment | dayjs |
| request | ky / fetch |
| chalk старых версий | chalk v5 |
---
## Проблемы dual packages
Некоторые библиотеки поддерживают одновременно:
* CommonJS
* ESM
Через поле:
```json
{
"exports": {
"import": "./esm/index.js",
"require": "./cjs/index.js"
}
}
```
---
## Возможные сложности
Иногда:
* dev-сервер использует ESM
* production — CommonJS
* SSR — другую сборку
Это приводит к различиям поведения.
---
## Устранение неоднозначности
Иногда полезно явно указывать entry:
```js
import pkg from 'library/dist/index.mjs';
```
---
## resolve.mainFields
Vite позволяет управлять приоритетом полей package.json.
### Пример
```js
export default {
resolve: {
mainFields: [
'module',
'jsnext:main',
'jsnext'
]
}
};
```
---
## Что это меняет
Vite будет предпочитать ESM-entry вместо CommonJS-entry.
---
## CommonJS-пакеты без package.json exports
Старые библиотеки иногда имеют структуру:
```txt
lib/
index.js
utils.js
internal.js
```
Без:
```json
exports
```
---
## Последствия
Возможны:
* нестабильные deep imports
* проблемы резолвинга
* различия между Node и Vite
---
## Deep imports
Например:
```js
import parser from 'legacy-lib/lib/parser';
```
Такие импорты:
* ломаются после обновлений
* могут конфликтовать с оптимизацией
* плохо совместимы с ESM
---
## Более безопасный подход
Использование официального публичного API:
```js
import { parser } from 'legacy-lib';
```
---
## CommonJS внутри монорепозиториев
В monorepo старые внутренние пакеты часто остаются CommonJS.
Пример:
```js
module.exports = {
utils
};
```
---
## Возможные проблемы
* HMR работает нестабильно
* зависимости кэшируются неправильно
* возникают дубли модулей
---
## Частичная миграция пакетов
Минимальная адаптация:
### Было
```js
const utils = require('./utils');
module.exports = {
utils
};
```
---
### Стало
```js
import utils from './utils.js';
export {
utils
};
```
---
## preserveSymlinks
В monorepo иногда помогает:
```js
export default {
resolve: {
preserveSymlinks: true
}
};
```
---
## CommonJS-плагины Rollup
Иногда требуется ручная настройка CommonJS-плагина.
### Пример
```js
export default {
build: {
commonjsOptions: {
include: [/node_modules/]
}
}
};
```
---
## ignoreDynamicRequires
Редкий, но полезный параметр:
```js
export default {
build: {
commonjsOptions: {
ignoreDynamicRequires: true
}
}
};
```
---
## Что делает ignoreDynamicRequires
Rollup перестаёт пытаться анализировать dynamic require.
Это не исправляет проблему полностью, но может позволить собрать проект.
---
## requireReturnsDefault
Некоторые библиотеки некорректно экспортируются после преобразования.
---
## Настройка
```js
export default {
build: {
commonjsOptions: {
requireReturnsDefault: 'auto'
}
}
};
```
---
## Возможные значения
| Значение | Назначение |
| --------- | ----------------------------------- |
| false | никогда не использовать default |
| true | всегда использовать default |
| auto | попытка автоматического определения |
| preferred | приоритет default |
---
## Анализ проблемных зависимостей
Наиболее частые признаки несовместимого CommonJS-пакета:
* `require is not defined`
* `exports is not defined`
* ошибки dynamic require
* отсутствие named export
* невозможность tree-shaking
* использование Node built-ins
* разные результаты dev/prod
---
## Практическая стратегия миграции
### Этап 1
Замена собственных CommonJS-модулей на ESM.
---
### Этап 2
Поиск ESM-аналогов старых библиотек.
---
### Этап 3
Настройка `optimizeDeps`.
---
### Этап 4
Корректировка `commonjsOptions`.
---
### Этап 5
Удаление dynamic require.
---
## Наиболее стабильный подход
На практике лучше всего работают проекты, которые:
* полностью используют ESM
* избегают legacy CommonJS-библиотек
* не используют dynamic require
* не зависят от Node built-ins во frontend
* применяют современные ESM-пакеты
* минимизируют deep imports
* используют import.meta.glob вместо require-каталогов