rollup-plugin-circular-dependencies
v2.0.1
Published
Detect circular dependencies in Rollup projects
Downloads
11,939
Maintainers
Readme
rollup-plugin-circular-dependencies
Detect and report circular dependencies in your Rollup & Vite projects.
A Rollup/Vite plugin that analyses your module graph using Tarjan's algorithm and reports every circular dependency it finds. It replaces Rollup's built-in circular dependency warnings with richer, configurable output — including pretty-printed console logs, JSON file reports, per-cycle filtering, lifecycle hooks, and detection metrics.
Table of Contents
- Installation
- Quick Start
- API Reference
- Configuration / Options
- Advanced Usage
- Compatibility & Peer Dependencies
- Troubleshooting / FAQ
- Contributing
- Changelog
- License
Installation
# npm
npm install -D rollup-plugin-circular-dependencies
# pnpm
pnpm add -D rollup-plugin-circular-dependencies
# yarn
yarn add -D rollup-plugin-circular-dependenciesQuick Start
// rollup.config.js
import { circularDependencies } from 'rollup-plugin-circular-dependencies';
export default {
input: 'src/index.js',
output: { dir: 'dist', format: 'esm' },
plugins: [circularDependencies()],
};If circular dependencies exist, the build will error by default. Set throwOnError: false to emit warnings instead.
API Reference
circularDependencies(options?)
Creates a Rollup plugin instance that detects circular dependencies in your project.
Signature:
function circularDependencies(options?: Options): Plugin;Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| options | Options | Plugin configuration. All fields are optional. |
Returns: Plugin — a Rollup plugin object.
Example:
import { circularDependencies } from 'rollup-plugin-circular-dependencies';
// Zero-config — errors on any cycle
circularDependencies();
// Warn instead of error
circularDependencies({ throwOnError: false });DefaultFormatters
An object containing built-in output formatters.
DefaultFormatters.JSON()
Creates a JSON formatter that serializes cycle data with 2-space indentation.
Signature:
function JSON(): (data: CircularDependenciesData) => string;Returns: A formatter function producing a JSON string.
Example:
import { circularDependencies, DefaultFormatters } from 'rollup-plugin-circular-dependencies';
circularDependencies({
formatOut: DefaultFormatters.JSON(),
});DefaultFormatters.Pretty(config?)
Creates a human-readable formatter with optional color support. Colors are enabled by default unless disabled via environment variables (NO_COLOR, NODE_DISABLE_COLORS, or FORCE_COLOR=0).
Signature:
function Pretty(config?: { colors?: boolean }): (data: CircularDependenciesData) => string;Parameters:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| config.colors | boolean | true (unless env-disabled) | Enable ANSI color output |
Returns: A formatter function producing a styled string.
Example:
import { circularDependencies, DefaultFormatters } from 'rollup-plugin-circular-dependencies';
// Disable colors explicitly
circularDependencies({
formatOut: DefaultFormatters.Pretty({ colors: false }),
});Options
Configuration object for the plugin. All fields are optional.
interface Options {
enabled?: boolean;
include?: FilterPattern;
exclude?: FilterPattern;
throwOnError?: boolean;
outputFilePath?: string;
debug?: boolean;
formatOutModulePath?: (path: string) => string;
formatOut?: (data: CircularDependenciesData) => unknown;
ignoreCycle?: (cyclePaths: string[]) => boolean;
onStart?: (pluginContext: PluginContext) => void;
onDetected?: (modulePath: string, pluginContext: PluginContext) => void;
onEnd?: (params: { rawOutput: CircularDependenciesData; formattedOutput: unknown; metrics: Metrics }, pluginContext: PluginContext) => void;
}See the Configuration / Options table for detailed descriptions.
Metrics
Metrics collected during circular dependency detection. Provided in the onEnd callback.
interface Metrics {
readonly modulesChecked: number;
readonly cyclesFound: number;
readonly largestCycleSize: number;
readonly detectionTimeMs: number;
}| Field | Type | Description |
|-------|------|-------------|
| modulesChecked | number | Total number of modules analyzed |
| cyclesFound | number | Number of unique cycles detected |
| largestCycleSize | number | Number of modules in the largest cycle |
| detectionTimeMs | number | Detection time in milliseconds |
CircularDependenciesData
The raw data structure representing detected cycles, grouped by entry module.
type CircularDependenciesData = Record<string, string[][]>;Each key is a module ID (file path). Each value is an array of cycles, where each cycle is an array of module IDs forming the circular chain.
Example value:
{
"src/a.ts": [
["src/a.ts", "src/b.ts"],
["src/a.ts", "src/c.ts", "src/d.ts"]
]
}Configuration / Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| enabled | boolean | true | Enable or disable the plugin entirely. |
| include | FilterPattern | [/\.(c\|m)?[jt]s(x)?$/] | Glob or RegExp patterns for files to include. |
| exclude | FilterPattern | [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/] | Glob or RegExp patterns for files to exclude. node_modules and .git are always excluded. |
| throwOnError | boolean | true | true = build error on detected cycles. false = warning only. |
| outputFilePath | string | "" | Path to write results to a file. Empty string = console output. |
| debug | boolean | false | Enable verbose debug logging (module count, timing, cycle details). |
| formatOutModulePath | (path: string) => string | path.relative(cwd, path) | Transform module paths in the output. |
| formatOut | (data: CircularDependenciesData) => unknown | Pretty() (console) / JSON() (file) | Custom formatter for the output data. |
| ignoreCycle | (cyclePaths: string[]) => boolean | () => false | Return true to ignore a specific cycle. Receives the array of module paths forming the cycle. |
| onStart | (pluginContext: PluginContext) => void | () => {} | Called before cycle detection starts. |
| onDetected | (modulePath: string, pluginContext: PluginContext) => void | () => {} | Called for each module that is part of a cycle. |
| onEnd | (params, pluginContext: PluginContext) => void | () => {} | Called after detection completes. Receives { rawOutput, formattedOutput, metrics }. |
Note:
FilterPatternis the type from@rollup/pluginutils— it acceptsstring | RegExp | Array<string | RegExp> | null.
Advanced Usage
Vite Integration
The plugin works with Vite out of the box:
// vite.config.ts
import { defineConfig } from 'vite';
import { circularDependencies } from 'rollup-plugin-circular-dependencies';
export default defineConfig({
plugins: [
circularDependencies({
throwOnError: false,
}),
],
});Output to File
Write detection results to a file instead of the console. When outputFilePath is set, the default formatter automatically switches to DefaultFormatters.JSON().
import { circularDependencies } from 'rollup-plugin-circular-dependencies';
circularDependencies({
outputFilePath: './reports/circular-deps.json',
throwOnError: false,
});To use the pretty formatter for file output instead:
import { circularDependencies, DefaultFormatters } from 'rollup-plugin-circular-dependencies';
circularDependencies({
outputFilePath: './reports/circular-deps.txt',
formatOut: DefaultFormatters.Pretty({ colors: false }),
});Custom Formatters
Replace the built-in formatters with your own:
import { circularDependencies } from 'rollup-plugin-circular-dependencies';
circularDependencies({
formatOut: (data) => {
const allCycles = Object.values(data).flat();
const summary = allCycles.map((cycle) => cycle.join(' → ')).join('\n');
return `Found ${allCycles.length} cycle(s):\n${summary}`;
},
});Lifecycle Hooks
Use lifecycle hooks to integrate with your CI pipeline, logging, or monitoring:
import { circularDependencies } from 'rollup-plugin-circular-dependencies';
circularDependencies({
throwOnError: false,
onStart: (pluginContext) => {
console.log('Scanning for circular dependencies...');
},
onDetected: (modulePath, pluginContext) => {
console.log(`Cycle member: ${modulePath}`);
},
onEnd: ({ rawOutput, formattedOutput, metrics }, pluginContext) => {
console.log(`Checked ${metrics.modulesChecked} modules`);
console.log(`Found ${metrics.cyclesFound} cycle(s)`);
console.log(`Largest cycle: ${metrics.largestCycleSize} modules`);
console.log(`Detection took ${metrics.detectionTimeMs.toFixed(1)}ms`);
// Example: fail CI if too many cycles
if (metrics.cyclesFound > 10) {
process.exit(1);
}
},
});Ignoring Specific Cycles
Filter out known or acceptable cycles using the ignoreCycle callback:
import { circularDependencies } from 'rollup-plugin-circular-dependencies';
circularDependencies({
ignoreCycle: (cyclePaths) => {
// Ignore cycles in generated code
if (cyclePaths.some((p) => p.includes('/generated/'))) {
return true;
}
// Ignore a specific known cycle
if (cyclePaths.some((p) => p.endsWith('store/index.ts'))) {
return true;
}
return false;
},
});Custom Module Path Formatting
By default, output paths are relative to the current working directory. Customize this behavior with formatOutModulePath:
import { circularDependencies } from 'rollup-plugin-circular-dependencies';
circularDependencies({
// Strip everything before 'src/'
formatOutModulePath: (modulePath) => {
const srcIndex = modulePath.indexOf('src/');
return srcIndex >= 0 ? modulePath.slice(srcIndex) : modulePath;
},
});Debug Mode
Enable verbose logging to see detection details in the Rollup info channel:
circularDependencies({
debug: true,
});Output example:
[circular-dependencies] Checked 142 modules in 3.2ms. Found 2 cycle(s), largest cycle size: 3.
[circular-dependencies] Cycle: src/a.ts → src/a.ts → src/b.ts
[circular-dependencies] Cycle: src/c.ts → src/c.ts → src/d.ts → src/e.tsWatch Mode
The plugin fully supports Rollup's watch mode. Internal state is automatically reset on each rebuild, ensuring accurate results without stale data or memory leaks.
// rollup.config.js
import { circularDependencies } from 'rollup-plugin-circular-dependencies';
export default {
input: 'src/index.js',
output: { dir: 'dist', format: 'esm' },
watch: { clearScreen: false },
plugins: [
circularDependencies({
throwOnError: false,
}),
],
};rollup -c --watchSee the watch-mode example for a complete demo.
Compatibility & Peer Dependencies
| Dependency | Version | Note |
|------------|---------|------|
| Node.js | >=20.12.0 | Required runtime |
| rollup | ^3.0.0 \|\| ^4.0.0 | Peer dependency |
The plugin ships both ESM and CJS builds:
| Format | Entry |
|--------|-------|
| ESM | dist/index.mjs |
| CJS | dist/index.cjs |
| Types | dist/index.d.cts |
Runtime dependency: @rollup/pluginutils ^5.3.0 — used for include/exclude file filtering.
Troubleshooting / FAQ
The plugin reports no cycles, but I know they exist
Make sure your files match the default include pattern [/\.(c|m)?[jt]s(x)?$/]. If your files use non-standard extensions, configure the include option explicitly:
circularDependencies({
include: [/\.vue$/, /\.[jt]sx?$/],
});I see "No files to check" in the console
This means no modules passed the include/exclude filters. Check your patterns:
// Debug by logging which modules are processed
circularDependencies({
debug: true,
onDetected: (modulePath) => {
console.log('Processing:', modulePath);
},
});I want warnings instead of errors
Set throwOnError to false:
circularDependencies({
throwOnError: false,
});Colors are not showing in my terminal
The plugin respects the NO_COLOR convention. Colors are disabled when any of these environment variables are set:
NO_COLOR=1/NO_COLOR=trueNODE_DISABLE_COLORS=1/NODE_DISABLE_COLORS=trueFORCE_COLOR=0/FORCE_COLOR=false
You can also disable colors explicitly:
import { circularDependencies, DefaultFormatters } from 'rollup-plugin-circular-dependencies';
circularDependencies({
formatOut: DefaultFormatters.Pretty({ colors: false }),
});How does the plugin differ from Rollup's built-in circular dependency warnings?
Rollup emits individual CIRCULAR_DEPENDENCY warnings for each cycle it finds. This plugin:
- Suppresses Rollup's built-in warnings to avoid duplicates
- Groups cycles by entry module for clearer output
- Provides structured data (
CircularDependenciesData) for programmatic use - Adds filtering (
ignoreCycle), lifecycle hooks, and metrics - Supports file output for CI integration
Can I use this with Vite?
Yes. Vite uses Rollup under the hood for production builds, so the plugin works as-is. See Vite Integration.
Changelog
See CHANGELOG.md for a detailed history of all releases.
