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-каталогов