no-cycle
Detect circular dependencies that cause bundle memory bloat and initialization issues
💼 This rule is enabled in the following configs: recommended, typescript.
Rule Details
Reports an import whose target imports the current module back, directly or
through other modules, by runtime edges. Such a cycle makes the module loader
hand one module to another while it is still initializing (an undefined
binding or a TDZ error), and keeps both modules in every bundle that loads
either.
An edge is a runtime edge when it survives compilation:
- value imports (default, named value, namespace), side-effect imports,
export { x } from,export * from,export * as ns from,require()and TSimport x = require()are runtime edges; import typeandexport type { T } fromare erased;- an inline
import { type T }is erased, unlessverbatimModuleSyntaxistrue(tsc then emitsimport {} from, which still loads the module); - a plain
import { T }that takes only types (interfaces, type aliases) is elided by TypeScript, unlessverbatimModuleSyntaxistrue; import()loads lazily, after both modules have initialized, and does not form an initialization cycle.
Options:
ignorePatterns(default: test, spec and story files,__tests__,__mocks__): files that are not checked;maxDepth(default unlimited): how many imports deep the edge back may be;1finds onlyA -> B -> A;verbatimModuleSyntax: see above;barrelExports,fixStrategy,moduleNamingConvention,coreModuleSuffix,extendedModuleSuffixshape the suggested fix in the message;reportAllCyclesis accepted for compatibility. None of them changes what is reported.
Generated files (marked by a @generated header) are not checked.
// ❌ Incorrect
// a.ts
import { b } from './b';
export const a = () => b();
// b.ts
import { a } from './a';
export const b = () => a();
// ✅ Correct
// b.ts
import type { A } from './a'; // erased: no runtime edge
export const lazyA = () => import('./a');Options
[
{
"type": "object",
"properties": {
"maxDepth": {
"type": "number",
"default": 9007199254740991,
"description": "Maximum depth to traverse when detecting cycles (performance optimization)"
},
"ignorePatterns": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"**/*.test.ts",
"**/*.test.tsx",
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.stories.tsx",
"**/__tests__/**",
"**/__mocks__/**"
],
"description": "File patterns to ignore (glob patterns)"
},
"barrelExports": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"index.ts",
"index.tsx",
"index.js",
"index.jsx"
],
"description": "Files considered barrel exports"
},
"reportAllCycles": {
"type": "boolean",
"default": true,
"description": "Report all circular dependencies found (not just the first one)"
},
"verbatimModuleSyntax": {
"type": "boolean",
"default": false,
"description": "Treat an inline `import { type Foo }` specifier as a runtime edge. Set this when the project compiles with TypeScript's `verbatimModuleSyntax`, under which the inline form is emitted as `import {} from './foo.js'` rather than erased — so the target module is still evaluated and the cycle is real. A statement-level `import type` is erased under every setting and stays skipped either way."
},
"fixStrategy": {
"type": "string",
"enum": [
"module-split",
"direct-import",
"extract-shared",
"dependency-injection",
"auto"
],
"default": "auto",
"description": "Strategy for fixing circular dependencies (auto = smart detection)"
},
"moduleNamingConvention": {
"type": "string",
"enum": [
"semantic",
"numbered"
],
"default": "semantic",
"description": "Naming convention for split modules (semantic: .core, .api | numbered: .1, .2)"
},
"coreModuleSuffix": {
"type": "string",
"default": "core",
"description": "Suffix for core module when splitting (e.g., \"core\", \"base\", \"main\")"
},
"extendedModuleSuffix": {
"type": "string",
"default": "extended",
"description": "Suffix for extended module when splitting (e.g., \"extended\", \"api\", \"helpers\")"
}
},
"additionalProperties": false
}
]Implementation
OWASP Foundation
- Category: A05:2021 - Security Misconfiguration
Use this rule
npm install -D oxlint-plugin-import-next{ "jsPlugins": ["oxlint-plugin-import-next"], "rules": { "import-next/no-cycle": "error" }}import { defineConfig } from 'vite-plus';export default defineConfig({ lint: { jsPlugins: ['oxlint-plugin-import-next'], rules: { 'import-next/no-cycle': 'error', }, },});oxlint-plugin-import-next re-exports eslint-plugin-import-next/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.
Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.