Предупреждение CIRCULAR_DEPENDENCY в Rollup появляется
при обнаружении циклической зависимости в графе модулей. Под циклом
понимается ситуация, когда модуль A импортирует модуль B, а модуль B
(прямо или через цепочку импортов) возвращает зависимость обратно в
модуль A. Такая структура нарушает линейную направленность графа
зависимостей и усложняет статический анализ кода.
В контексте ES Modules циклы не всегда являются критической ошибкой, но они создают неопределённость порядка инициализации, влияют на tree-shaking и могут приводить к частично инициализированным значениям на этапе выполнения.
Rollup строит направленный граф модулей, начиная с входной точки и
рекурсивно анализируя import и export. При
построении графа фиксируются переходы между модулями. Если при обходе
графа обнаруживается повторное посещение уже находящегося в стеке
модуля, фиксируется цикл.
Упрощённо:
CIRCULAR_DEPENDENCYВажно, что Rollup не всегда считает цикл ошибкой сборки, поэтому выводится предупреждение, а не исключение.
// a.js
import { bValue } from './b.js';
export const aValue = 'A' + bValue;
// b.js
import { aValue } from './a.js';
export const bValue = 'B' + aValue;
Здесь a.js → b.js → a.js образует цикл.
На этапе выполнения значение aValue или
bValue может быть undefined в момент первого
обращения, поскольку модуль ещё не завершил инициализацию.
Одна из наиболее распространённых причин — реэкспорт через агрегирующие модули:
// index.js
export * from './user.js';
export * from './auth.js';
// user.js
import { login } from './index.js';
Проблема возникает, когда index.js становится точкой
пересечения множества модулей, а внутренние модули начинают
импортировать его обратно.
В архитектурах с насыщенной бизнес-логикой модули часто начинают ссылаться друг на друга:
В результате формируется кольцевая структура, которая не видна на уровне отдельных файлов, но проявляется на уровне графа зависимостей.
Модуль shared часто становится центральной точкой, в
которую постепенно добавляется логика:
// shared.js
export { formatDate } from './date.js';
export { apiClient } from './api.js';
Если apiClient или date.js начинают
импортировать что-то из shared.js, возникает замкнутый
цикл.
В React-проектах циклы часто возникают между компонентами:
или через общий модуль состояния.
ES Modules имеют специфику ленивой инициализации экспортов. При цикле:
undefinedПример:
console.log(aValue); // undefined
Даже если позже значение будет установлено, первый доступ может вернуть некорректное состояние.
Циклические зависимости ухудшают возможности статического анализа:
В сложных циклах Rollup может отказаться от агрессивного удаления неиспользуемого кода.
// api.js
import { getToken } from './auth.js';
export function request() {
return fetch('/data', {
headers: { Authorization: getToken() }
});
}
// auth.js
import { request } from './api.js';
export function getToken() {
return localStorage.getItem('token');
}
Даже если логически зависимости кажутся оправданными, граф остаётся циклическим.
// user.model.js
import { validateUser } from './user.service.js';
// user.service.js
import { UserModel } from './user.model.js';
Такой вариант часто появляется при попытке разделить ответственность без чёткой архитектурной границы.
Общая зависимость выносится в отдельный модуль:
// user.types.js
export const USER_ROLES = {
admin: 'admin',
user: 'user'
};
Оба модуля начинают зависеть от него, но не друг от друга напрямую.
Вместо прямого импорта используется передача зависимостей:
export function createApi(getToken) {
return function request() {
return fetch('/data', {
headers: { Authorization: getToken() }
});
};
}
Это разрывает цикл на уровне импорта.
export async function getService() {
const mod = await import('./service.js');
return mod.service;
}
Такой подход переносит зависимость из статического графа в runtime.
Классическая ошибка — смешивание уровней:
Перестройка слоёв позволяет убрать обратные связи.
Если цикл идёт через index.js, помогает:
Rollup позволяет подавлять предупреждение через конфигурацию:
export default {
onwarn(warning, warn) {
if (warning.code === 'CIRCULAR_DEPENDENCY') return;
warn(warning);
}
};
Такой подход применяется только в случаях, когда цикл осознанный и не влияет на runtime.
В монорепозиториях циклы часто возникают между пакетами:
Причина часто заключается в:
Rollup фиксирует такие циклы на уровне итогового бандла, даже если в отдельных пакетах они не видны.
Не все циклы очевидны. Возможны цепочки:
a → b → c → a
или более длинные:
a → utils → shared → core → a
Такие случаи сложнее диагностировать, так как прямой связи между
a и core может не быть.
Для анализа графа используются:
Rollup обычно выводит цепочку вида:
Circular dependency: a.js -> b.js -> a.js
или расширенную цепочку через несколько файлов.
Наличие CIRCULAR_DEPENDENCY не всегда означает ошибку,
но почти всегда сигнализирует о:
В крупных проектах количество таких предупреждений часто коррелирует с архитектурной связностью модулей.