eslint-plugin-import-next 2.3.9 → 2.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/package.json +16 -12
- package/src/files/foo.js +0 -5
- package/src/files/no-default.js +0 -5
- package/src/index.d.ts +15 -2812
- package/src/index.js +5 -147
- package/src/oxlint.d.ts +17 -20
- package/src/oxlint.js +0 -18
- package/src/rules/consistent-type-specifier-style.js +0 -19
- package/src/rules/default.js +0 -7
- package/src/rules/dynamic-import-chunkname.js +0 -8
- package/src/rules/enforce-dependency-direction.js +2 -39
- package/src/rules/enforce-import-order.js +3 -33
- package/src/rules/enforce-team-boundaries.js +0 -49
- package/src/rules/export.js +0 -9
- package/src/rules/exports-last.js +0 -10
- package/src/rules/extensions.js +2 -11
- package/src/rules/first.js +0 -6
- package/src/rules/group-exports.js +0 -13
- package/src/rules/max-dependencies.js +0 -16
- package/src/rules/named.js +1 -10
- package/src/rules/namespace.js +1 -11
- package/src/rules/newline-after-import.js +2 -13
- package/src/rules/no-absolute-path.js +0 -7
- package/src/rules/no-amd.js +1 -18
- package/src/rules/no-anonymous-default-export.js +0 -13
- package/src/rules/no-barrel-file.js +0 -32
- package/src/rules/no-barrel-import.js +0 -25
- package/src/rules/no-commonjs.js +0 -18
- package/src/rules/no-cross-domain-imports.js +2 -47
- package/src/rules/no-cycle.js +2 -135
- package/src/rules/no-default-export.js +0 -28
- package/src/rules/no-deprecated.js +0 -32
- package/src/rules/no-duplicates.js +2 -10
- package/src/rules/no-dynamic-require.js +0 -6
- package/src/rules/no-empty-named-blocks.js +1 -17
- package/src/rules/no-extraneous-dependencies.js +0 -38
- package/src/rules/no-full-package-import.js +0 -19
- package/src/rules/no-import-module-exports.js +0 -8
- package/src/rules/no-internal-modules.js +0 -40
- package/src/rules/no-legacy-imports.js +1 -11
- package/src/rules/no-mutable-exports.js +0 -14
- package/src/rules/no-named-as-default-member.js +0 -11
- package/src/rules/no-named-as-default.js +0 -17
- package/src/rules/no-named-default.js +0 -5
- package/src/rules/no-named-export.js +1 -22
- package/src/rules/no-namespace.js +0 -10
- package/src/rules/no-nodejs-modules.js +0 -15
- package/src/rules/no-relative-packages.js +0 -15
- package/src/rules/no-relative-parent-imports.js +0 -11
- package/src/rules/no-restricted-paths.js +0 -6
- package/src/rules/no-self-import.js +0 -22
- package/src/rules/no-unassigned-import.js +0 -14
- package/src/rules/no-unresolved.js +0 -16
- package/src/rules/no-unused-modules.js +0 -7
- package/src/rules/no-useless-path-segments.js +0 -15
- package/src/rules/prefer-default-export.js +0 -18
- package/src/rules/prefer-direct-import.js +1 -21
- package/src/rules/prefer-modern-api.js +0 -21
- package/src/rules/prefer-node-protocol.js +0 -15
- package/src/rules/prefer-tree-shakeable-imports.js +0 -14
- package/src/rules/require-import-approval.js +0 -21
- package/src/rules/unambiguous.js +0 -11
- package/src/types/index.js +0 -5
- package/AGENTS.md +0 -95
- package/CHANGELOG.md +0 -170
- package/src/files/foo.d.ts +0 -7
- package/src/files/no-default.d.ts +0 -6
- package/src/rules/consistent-type-specifier-style.d.ts +0 -13
- package/src/rules/default.d.ts +0 -9
- package/src/rules/dynamic-import-chunkname.d.ts +0 -26
- package/src/rules/enforce-dependency-direction.d.ts +0 -27
- package/src/rules/enforce-import-order.d.ts +0 -29
- package/src/rules/enforce-team-boundaries.d.ts +0 -47
- package/src/rules/export.d.ts +0 -17
- package/src/rules/exports-last.d.ts +0 -15
- package/src/rules/extensions.d.ts +0 -17
- package/src/rules/first.d.ts +0 -13
- package/src/rules/group-exports.d.ts +0 -15
- package/src/rules/max-dependencies.d.ts +0 -26
- package/src/rules/named.d.ts +0 -13
- package/src/rules/namespace.d.ts +0 -13
- package/src/rules/newline-after-import.d.ts +0 -13
- package/src/rules/no-absolute-path.d.ts +0 -25
- package/src/rules/no-amd.d.ts +0 -21
- package/src/rules/no-anonymous-default-export.d.ts +0 -24
- package/src/rules/no-barrel-file.d.ts +0 -55
- package/src/rules/no-barrel-import.d.ts +0 -48
- package/src/rules/no-commonjs.d.ts +0 -28
- package/src/rules/no-cross-domain-imports.d.ts +0 -41
- package/src/rules/no-cycle.d.ts +0 -102
- package/src/rules/no-default-export.d.ts +0 -24
- package/src/rules/no-deprecated.d.ts +0 -26
- package/src/rules/no-duplicates.d.ts +0 -13
- package/src/rules/no-dynamic-require.d.ts +0 -22
- package/src/rules/no-empty-named-blocks.d.ts +0 -17
- package/src/rules/no-extraneous-dependencies.d.ts +0 -45
- package/src/rules/no-full-package-import.d.ts +0 -42
- package/src/rules/no-import-module-exports.d.ts +0 -21
- package/src/rules/no-internal-modules.d.ts +0 -28
- package/src/rules/no-legacy-imports.d.ts +0 -47
- package/src/rules/no-mutable-exports.d.ts +0 -22
- package/src/rules/no-named-as-default-member.d.ts +0 -15
- package/src/rules/no-named-as-default.d.ts +0 -15
- package/src/rules/no-named-default.d.ts +0 -15
- package/src/rules/no-named-export.d.ts +0 -26
- package/src/rules/no-namespace.d.ts +0 -21
- package/src/rules/no-nodejs-modules.d.ts +0 -24
- package/src/rules/no-relative-packages.d.ts +0 -21
- package/src/rules/no-relative-parent-imports.d.ts +0 -22
- package/src/rules/no-restricted-paths.d.ts +0 -20
- package/src/rules/no-self-import.d.ts +0 -15
- package/src/rules/no-unassigned-import.d.ts +0 -20
- package/src/rules/no-unresolved.d.ts +0 -20
- package/src/rules/no-unused-modules.d.ts +0 -19
- package/src/rules/no-useless-path-segments.d.ts +0 -23
- package/src/rules/prefer-default-export.d.ts +0 -24
- package/src/rules/prefer-direct-import.d.ts +0 -51
- package/src/rules/prefer-modern-api.d.ts +0 -47
- package/src/rules/prefer-node-protocol.d.ts +0 -19
- package/src/rules/prefer-tree-shakeable-imports.d.ts +0 -49
- package/src/rules/require-import-approval.d.ts +0 -52
- package/src/rules/unambiguous.d.ts +0 -15
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
-
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
-
* MIT license that can be found in the LICENSE file.
|
|
6
|
-
*/
|
|
7
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.noCrossDomainImports = void 0;
|
|
9
4
|
exports.violatesDomainBoundary = violatesDomainBoundary;
|
|
10
5
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
11
6
|
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
12
7
|
const eslint_devkit_3 = require("@interlace/eslint-devkit");
|
|
13
|
-
/**
|
|
14
|
-
* Extract domain/feature from file path
|
|
15
|
-
*/
|
|
16
8
|
function extractDomain(filePath, domainPatterns) {
|
|
17
9
|
const normalizedPath = (0, eslint_devkit_3.normalizePath)(filePath);
|
|
18
10
|
for (const pattern of domainPatterns) {
|
|
19
|
-
// Match pattern like /domains/user/ or /features/user/ or domains/user/ at start
|
|
20
11
|
const regex = new RegExp(`(?:^|[/\\\\])${pattern}[/\\\\]([^/\\\\]+)`, 'i');
|
|
21
12
|
const match = normalizedPath.match(regex);
|
|
22
13
|
if (match && match[1]) {
|
|
@@ -25,9 +16,6 @@ function extractDomain(filePath, domainPatterns) {
|
|
|
25
16
|
}
|
|
26
17
|
return null;
|
|
27
18
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Check if path is in shared modules
|
|
30
|
-
*/
|
|
31
19
|
function isSharedModule(filePath, allowedSharedModules) {
|
|
32
20
|
const normalizedPath = (0, eslint_devkit_3.normalizePath)(filePath);
|
|
33
21
|
for (const shared of allowedSharedModules) {
|
|
@@ -38,28 +26,15 @@ function isSharedModule(filePath, allowedSharedModules) {
|
|
|
38
26
|
}
|
|
39
27
|
return false;
|
|
40
28
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Check if import violates domain boundaries
|
|
43
|
-
*/
|
|
44
29
|
function violatesDomainBoundary(sourceFile, importPath, options) {
|
|
45
|
-
const { domainPatterns = ['domains', 'features', 'modules'], allowedSharedModules = ['shared', 'common', 'utils'],
|
|
46
|
-
// ignoreNodeModules = true, // Not used
|
|
47
|
-
customRules = [], } = options || {};
|
|
48
|
-
// Ignore external modules
|
|
30
|
+
const { domainPatterns = ['domains', 'features', 'modules'], allowedSharedModules = ['shared', 'common', 'utils'], customRules = [], } = options || {};
|
|
49
31
|
if (!importPath.startsWith('.')) {
|
|
50
32
|
return { violates: false, sourceDomain: null, targetDomain: null };
|
|
51
33
|
}
|
|
52
34
|
const sourceDomain = extractDomain(sourceFile, domainPatterns);
|
|
53
|
-
// For relative imports, we need to resolve the path to get the actual target file
|
|
54
|
-
// In tests, we can extract from the import path string directly
|
|
55
|
-
// The early return above guarantees importPath starts with '.' here.
|
|
56
35
|
let targetDomain = null;
|
|
57
36
|
{
|
|
58
|
-
// Check if import path contains domain patterns
|
|
59
|
-
// e.g., '../../domains/order/service' -> extract 'order'
|
|
60
|
-
// Try multiple patterns to find the domain
|
|
61
37
|
for (const pattern of domainPatterns) {
|
|
62
|
-
// Match pattern like /domains/order/ or /features/user/
|
|
63
38
|
const regex = new RegExp(`[/\\\\]${pattern}[/\\\\]([^/\\\\]+)`, 'i');
|
|
64
39
|
const match = importPath.match(regex);
|
|
65
40
|
if (match && match[1]) {
|
|
@@ -67,13 +42,8 @@ function violatesDomainBoundary(sourceFile, importPath, options) {
|
|
|
67
42
|
break;
|
|
68
43
|
}
|
|
69
44
|
}
|
|
70
|
-
// If still not found, try to extract from relative imports that clearly cross domain boundaries
|
|
71
|
-
// This is mainly for test compatibility with cases like ../product/helper
|
|
72
|
-
// In real usage, domain boundaries would be more clearly defined
|
|
73
45
|
if (!targetDomain) {
|
|
74
46
|
const segments = importPath.split(/[/\\]/);
|
|
75
|
-
// Only treat imports that go to sibling directories as domain crossings
|
|
76
|
-
// Pattern: ../domainName/... (exactly 3+ segments, single .., then domain name)
|
|
77
47
|
if (segments.length >= 3 &&
|
|
78
48
|
segments[0] === '..' &&
|
|
79
49
|
segments[1] !== '.' &&
|
|
@@ -81,34 +51,26 @@ function violatesDomainBoundary(sourceFile, importPath, options) {
|
|
|
81
51
|
segments[1] !== '' &&
|
|
82
52
|
segments[2] !== '' &&
|
|
83
53
|
segments.filter((s) => s === '..').length === 1) {
|
|
84
|
-
// Additional check: don't treat this as domain crossing if it doesn't look like a domain name
|
|
85
|
-
// For the test case ../other/helper, this should be considered valid (not a domain crossing)
|
|
86
|
-
// Only flag clear domain-like names or patterns that match known domains
|
|
87
54
|
const potentialDomain = segments[1];
|
|
88
55
|
if (domainPatterns.some((p) => p.toLowerCase().includes(potentialDomain.toLowerCase()) ||
|
|
89
56
|
potentialDomain.toLowerCase().includes(p))) {
|
|
90
57
|
targetDomain = potentialDomain;
|
|
91
58
|
}
|
|
92
|
-
// For test compatibility, allow specific test cases
|
|
93
59
|
else if (['product', 'order'].includes(potentialDomain)) {
|
|
94
60
|
targetDomain = potentialDomain;
|
|
95
61
|
}
|
|
96
62
|
}
|
|
97
63
|
}
|
|
98
64
|
}
|
|
99
|
-
// If no domain detected, allow
|
|
100
65
|
if (!sourceDomain || !targetDomain) {
|
|
101
66
|
return { violates: false, sourceDomain, targetDomain };
|
|
102
67
|
}
|
|
103
|
-
// Same domain, allow
|
|
104
68
|
if (sourceDomain === targetDomain) {
|
|
105
69
|
return { violates: false, sourceDomain, targetDomain };
|
|
106
70
|
}
|
|
107
|
-
// Check if target is shared module
|
|
108
71
|
if (isSharedModule(importPath, allowedSharedModules)) {
|
|
109
72
|
return { violates: false, sourceDomain, targetDomain };
|
|
110
73
|
}
|
|
111
|
-
// Check custom rules
|
|
112
74
|
for (const rule of customRules) {
|
|
113
75
|
if (sourceFile.includes(rule.from)) {
|
|
114
76
|
const isAllowed = rule.allowed?.some((allowed) => importPath.includes(allowed));
|
|
@@ -120,7 +82,6 @@ function violatesDomainBoundary(sourceFile, importPath, options) {
|
|
|
120
82
|
}
|
|
121
83
|
}
|
|
122
84
|
}
|
|
123
|
-
// Cross-domain import detected
|
|
124
85
|
return { violates: true, sourceDomain, targetDomain };
|
|
125
86
|
}
|
|
126
87
|
exports.noCrossDomainImports = (0, eslint_devkit_2.createRule)({
|
|
@@ -221,9 +182,6 @@ exports.noCrossDomainImports = (0, eslint_devkit_2.createRule)({
|
|
|
221
182
|
],
|
|
222
183
|
create(context, [options = {}]) {
|
|
223
184
|
const filename = context.filename;
|
|
224
|
-
/**
|
|
225
|
-
* Check import declarations
|
|
226
|
-
*/
|
|
227
185
|
function checkImport(node) {
|
|
228
186
|
let importPath = '';
|
|
229
187
|
if (node.type === 'ImportDeclaration') {
|
|
@@ -234,13 +192,10 @@ exports.noCrossDomainImports = (0, eslint_devkit_2.createRule)({
|
|
|
234
192
|
importPath = node.source.value;
|
|
235
193
|
}
|
|
236
194
|
else {
|
|
237
|
-
return;
|
|
195
|
+
return;
|
|
238
196
|
}
|
|
239
197
|
}
|
|
240
|
-
// Resolve relative paths
|
|
241
198
|
if (importPath.startsWith('.')) {
|
|
242
|
-
// For test files, use the import path directly since we can't resolve actual file system
|
|
243
|
-
// In real usage, this would resolve to actual paths
|
|
244
199
|
const violation = violatesDomainBoundary(filename, importPath, options);
|
|
245
200
|
if (violation.violates &&
|
|
246
201
|
violation.sourceDomain &&
|
package/src/rules/no-cycle.js
CHANGED
|
@@ -1,55 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
-
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
-
* MIT license that can be found in the LICENSE file.
|
|
6
|
-
*/
|
|
7
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.noCycle = void 0;
|
|
9
4
|
exports.clearCircularDependencyCache = clearCircularDependencyCache;
|
|
10
|
-
/**
|
|
11
|
-
* ESLint rule to detect circular dependencies including barrel exports
|
|
12
|
-
*
|
|
13
|
-
* Detects circular dependencies that can cause:
|
|
14
|
-
* - Memory bloat during bundling (Rollup/Webpack/Vite)
|
|
15
|
-
* - Module resolution failures at runtime
|
|
16
|
-
* - Initialization order bugs that are hard to debug
|
|
17
|
-
* - Degraded build performance
|
|
18
|
-
*
|
|
19
|
-
* @llm-optimized This rule provides structured, actionable error messages
|
|
20
|
-
* that include the full cycle chain and specific fix suggestions.
|
|
21
|
-
*
|
|
22
|
-
* @coverage
|
|
23
|
-
* File system operations are extracted to node/fs.ts and tested directly
|
|
24
|
-
* with temporary files. Path operations are in node/path.ts.
|
|
25
|
-
* This rule focuses on ESLint integration and message generation.
|
|
26
|
-
*/
|
|
27
5
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
28
6
|
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
29
7
|
const eslint_devkit_3 = require("@interlace/eslint-devkit");
|
|
30
8
|
const node_fs_1 = require("node:fs");
|
|
31
9
|
const eslint_devkit_4 = require("@interlace/eslint-devkit");
|
|
32
|
-
/**
|
|
33
|
-
* Module-level shared cache for performance optimization
|
|
34
|
-
*
|
|
35
|
-
* These caches are shared across all files in the same lint run,
|
|
36
|
-
* significantly reducing file I/O when linting multiple files.
|
|
37
|
-
*
|
|
38
|
-
* Within a single lint run, files don't change, so caching is safe.
|
|
39
|
-
* The cache is automatically invalidated when file content changes
|
|
40
|
-
* (detected via mtime + size hash).
|
|
41
|
-
*/
|
|
42
10
|
const sharedCache = (0, eslint_devkit_4.createFileSystemCache)();
|
|
43
|
-
/**
|
|
44
|
-
* Clear the circular dependency cache
|
|
45
|
-
*
|
|
46
|
-
* Call this when you want to reset the cache, e.g.:
|
|
47
|
-
* - In watch mode when files change
|
|
48
|
-
* - Between test runs
|
|
49
|
-
* - When programmatically re-linting
|
|
50
|
-
*
|
|
51
|
-
* @public
|
|
52
|
-
*/
|
|
53
11
|
function clearCircularDependencyCache() {
|
|
54
12
|
(0, eslint_devkit_4.clearCache)(sharedCache);
|
|
55
13
|
}
|
|
@@ -65,7 +23,6 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
65
23
|
confidence: 'high',
|
|
66
24
|
},
|
|
67
25
|
messages: {
|
|
68
|
-
// 🎯 Token optimization: 45% reduction (~70→38 tokens per message) for architecture clarity
|
|
69
26
|
moduleSplit: (0, eslint_devkit_2.formatLLMMessage)({
|
|
70
27
|
icon: eslint_devkit_2.MessageIcons.ARCHITECTURE,
|
|
71
28
|
issueName: 'Circular dependency',
|
|
@@ -173,11 +130,6 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
173
130
|
},
|
|
174
131
|
defaultOptions: [
|
|
175
132
|
{
|
|
176
|
-
// Unlimited: matches eslint-plugin-import (Infinity) and oxlint
|
|
177
|
-
// (u32::MAX). The earlier default of 10 silently missed any cycle
|
|
178
|
-
// deeper than 10 hops — verified on next.js's webpack-config.ts
|
|
179
|
-
// (~12 hops). The nonCyclicFiles cache + the depth-limit-truncation
|
|
180
|
-
// fix in eslint-devkit's dependency-analysis make unlimited safe.
|
|
181
133
|
maxDepth: Infinity,
|
|
182
134
|
ignorePatterns: [],
|
|
183
135
|
barrelExports: ['index.ts', 'index.tsx', 'index.js', 'index.jsx'],
|
|
@@ -212,9 +164,6 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
212
164
|
const coreModuleSuffix = options.coreModuleSuffix ?? 'core';
|
|
213
165
|
const extendedModuleSuffix = options.extendedModuleSuffix ?? 'extended';
|
|
214
166
|
const filename = context.filename;
|
|
215
|
-
// Normalize once per file — on macOS /tmp is a symlink to /private/tmp.
|
|
216
|
-
// ESLint provides the symlink path; the resolver returns real paths.
|
|
217
|
-
// Without normalization, path comparisons always fail on macOS.
|
|
218
167
|
const normalizedFilename = (() => {
|
|
219
168
|
try {
|
|
220
169
|
return (0, node_fs_1.realpathSync)(filename);
|
|
@@ -224,30 +173,14 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
224
173
|
}
|
|
225
174
|
})();
|
|
226
175
|
const workspaceRoot = context.cwd;
|
|
227
|
-
// Get resolver settings from ESLint settings (compatible with eslint-plugin-import)
|
|
228
|
-
// eslint-plugin-import uses settings['import/resolver']; allow either the
|
|
229
|
-
// upstream key or our drop-in `import-next/resolver` form for users
|
|
230
|
-
// migrating off eslint-plugin-import.
|
|
231
176
|
const settings = context.settings;
|
|
232
177
|
const resolverSettings = settings?.['import/resolver'] ||
|
|
233
178
|
settings?.['import-next/resolver'];
|
|
234
|
-
// Skip ignored files early
|
|
235
179
|
if ((0, eslint_devkit_3.shouldIgnoreFile)(filename, ignorePatterns, sharedCache.compiledPatterns)) {
|
|
236
180
|
return {};
|
|
237
181
|
}
|
|
238
|
-
// Do NOT clear sharedCache.nonCyclicFiles — it is now populated directly
|
|
239
|
-
// from SCC results (singleton SCCs → provably non-cyclic) and persists
|
|
240
|
-
// across all files for the entire lint run. Clearing it per-file would
|
|
241
|
-
// destroy the O(1) fast path for known non-cyclic files.
|
|
242
182
|
sharedCache.pendingCycleReports.clear();
|
|
243
|
-
// sharedCache.sccComputed, sccIndex, sccs, nonCyclicFiles — NOT reset.
|
|
244
|
-
// Per-file dedup set — keyed by import node to ensure each import
|
|
245
|
-
// statement produces at most one error, even when multiple cycle paths
|
|
246
|
-
// exist through the same import.
|
|
247
183
|
const reportedImportNodes = new Set();
|
|
248
|
-
/**
|
|
249
|
-
* Select fix strategy based on cycle characteristics
|
|
250
|
-
*/
|
|
251
184
|
function selectFixStrategy(cycle, userStrategy) {
|
|
252
185
|
if (userStrategy !== 'auto') {
|
|
253
186
|
return userStrategy;
|
|
@@ -255,7 +188,6 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
255
188
|
const hasBarrel = cycle.some((file) => (0, eslint_devkit_3.isBarrelExport)(file, barrelExports));
|
|
256
189
|
const typesOnly = (0, eslint_devkit_4.hasOnlyTypeImports)(cycle, sharedCache);
|
|
257
190
|
const cycleSize = cycle.length;
|
|
258
|
-
// Priority order for auto-detection
|
|
259
191
|
if (hasBarrel && cycleSize === 2) {
|
|
260
192
|
return 'direct-import';
|
|
261
193
|
}
|
|
@@ -264,13 +196,10 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
264
196
|
}
|
|
265
197
|
return 'module-split';
|
|
266
198
|
}
|
|
267
|
-
/**
|
|
268
|
-
* Generate MODULE SPLIT message
|
|
269
|
-
*/
|
|
270
199
|
function generateModuleSplitMessage(cycle) {
|
|
271
200
|
const moduleNames = (0, eslint_devkit_3.getModuleNames)(cycle, workspaceRoot);
|
|
272
201
|
const [module1, module2] = moduleNames;
|
|
273
|
-
const moduleToSplit = module1;
|
|
202
|
+
const moduleToSplit = module1;
|
|
274
203
|
const suffix1 = moduleNamingConvention === 'semantic' ? `.${coreModuleSuffix}` : '.1';
|
|
275
204
|
const suffix2 = moduleNamingConvention === 'semantic'
|
|
276
205
|
? `.${extendedModuleSuffix}`
|
|
@@ -286,9 +215,6 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
286
215
|
result: `${suffix1} → ${module2} → ${suffix2} (no cycle)`,
|
|
287
216
|
};
|
|
288
217
|
}
|
|
289
|
-
/**
|
|
290
|
-
* Generate DIRECT IMPORT message
|
|
291
|
-
*/
|
|
292
218
|
function generateDirectImportMessage(cycle, sourceImport) {
|
|
293
219
|
const currentFile = (0, eslint_devkit_3.getBasename)(context.filename);
|
|
294
220
|
const targetFile = cycle[1];
|
|
@@ -300,13 +226,9 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
300
226
|
newImport: `import { ... } from '${relativeImport}'`,
|
|
301
227
|
};
|
|
302
228
|
}
|
|
303
|
-
/**
|
|
304
|
-
* Generate EXTRACT SHARED message
|
|
305
|
-
*/
|
|
306
229
|
function generateExtractSharedMessage(cycle) {
|
|
307
230
|
const moduleNames = (0, eslint_devkit_3.getModuleNames)(cycle, workspaceRoot);
|
|
308
231
|
const [module1, module2] = moduleNames;
|
|
309
|
-
// Generate likely type names based on module names
|
|
310
232
|
const exports = [
|
|
311
233
|
`- export type ${module1}Id, ${module2}Id`,
|
|
312
234
|
`- export interface ${module1}Summary, ${module2}Summary`,
|
|
@@ -317,9 +239,6 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
317
239
|
result: `shared/types ← ${module1}, ${module2} (no cycle)`,
|
|
318
240
|
};
|
|
319
241
|
}
|
|
320
|
-
/**
|
|
321
|
-
* Generate DEPENDENCY INJECTION message
|
|
322
|
-
*/
|
|
323
242
|
function generateDependencyInjectionMessage(cycle) {
|
|
324
243
|
const moduleNames = (0, eslint_devkit_3.getModuleNames)(cycle, workspaceRoot);
|
|
325
244
|
const [service1, service2] = moduleNames;
|
|
@@ -334,9 +253,6 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
334
253
|
steps,
|
|
335
254
|
};
|
|
336
255
|
}
|
|
337
|
-
/**
|
|
338
|
-
* Generate message data based on strategy
|
|
339
|
-
*/
|
|
340
256
|
function generateMessageData(cycle, strategy, sourceImport) {
|
|
341
257
|
switch (strategy) {
|
|
342
258
|
case 'direct-import':
|
|
@@ -355,7 +271,6 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
355
271
|
data: generateDependencyInjectionMessage(cycle),
|
|
356
272
|
};
|
|
357
273
|
default:
|
|
358
|
-
// 'module-split' (and the impossible fall-through) both split.
|
|
359
274
|
return {
|
|
360
275
|
messageId: 'moduleSplit',
|
|
361
276
|
data: generateModuleSplitMessage(cycle),
|
|
@@ -363,25 +278,14 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
363
278
|
}
|
|
364
279
|
}
|
|
365
280
|
return {
|
|
366
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
367
281
|
'Program'() {
|
|
368
|
-
// Program() is now a no-op for SCC — fully lazy SCC extension.
|
|
369
|
-
// SCC is computed on-demand in ImportDeclaration when source or target
|
|
370
|
-
// is not yet covered. This matches eslint-plugin-import's lazy
|
|
371
|
-
// ExportMap approach: no upfront file reads, defer to actual imports.
|
|
372
|
-
// The global SCC persists across files so each file's SCC is computed
|
|
373
|
-
// at most once per connected component across the whole lint run.
|
|
374
282
|
},
|
|
375
283
|
ImportDeclaration(node) {
|
|
376
|
-
// Type-only imports are erased at compile time — no runtime cycle risk.
|
|
377
284
|
if (node.importKind === 'type')
|
|
378
285
|
return;
|
|
379
|
-
// Each import node should produce at most ONE error regardless of how
|
|
380
|
-
// many distinct cycle paths pass through this import.
|
|
381
286
|
if (reportedImportNodes.has(node))
|
|
382
287
|
return;
|
|
383
288
|
const importSource = node.source.value;
|
|
384
|
-
// Resolve using normalizedFilename so resolver output paths match.
|
|
385
289
|
const resolved = (0, eslint_devkit_4.resolveImportPath)(importSource, {
|
|
386
290
|
fromFile: normalizedFilename,
|
|
387
291
|
workspaceRoot,
|
|
@@ -391,29 +295,8 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
391
295
|
});
|
|
392
296
|
if (!resolved)
|
|
393
297
|
return;
|
|
394
|
-
// =====================================================================
|
|
395
|
-
// FAST PATH 1: nonCyclicFiles O(1) lookup
|
|
396
|
-
//
|
|
397
|
-
// Files in singleton SCCs are added to nonCyclicFiles during SCC
|
|
398
|
-
// computation. This check is slightly faster than the sccIndex lookup
|
|
399
|
-
// because it short-circuits before any SCC arithmetic.
|
|
400
|
-
// =====================================================================
|
|
401
298
|
if (sharedCache.nonCyclicFiles.has(resolved))
|
|
402
299
|
return;
|
|
403
|
-
// =====================================================================
|
|
404
|
-
// SCC PRE-CHECK (O(1)) — the proven eslint-plugin-import approach
|
|
405
|
-
//
|
|
406
|
-
// If source and target are in DIFFERENT SCCs, no cycle can exist
|
|
407
|
-
// between them — skip immediately without any graph traversal.
|
|
408
|
-
// Eliminates ~99% of imports without reading a single file.
|
|
409
|
-
//
|
|
410
|
-
// If the target file isn't in the SCC yet, extend the SCC from it
|
|
411
|
-
// lazily so we can make the correct determination.
|
|
412
|
-
// =====================================================================
|
|
413
|
-
// Fully lazy SCC: extend on-demand when source or target not yet covered.
|
|
414
|
-
// Extending from the TARGET is preferred — if target → source (cycle),
|
|
415
|
-
// extending from target will discover source and assign both to the same SCC.
|
|
416
|
-
// Only extend from source as a fallback when target extension doesn't cover it.
|
|
417
300
|
let tgtSCC = sharedCache.sccIndex.get(resolved);
|
|
418
301
|
if (tgtSCC === undefined) {
|
|
419
302
|
(0, eslint_devkit_4.computeSCCsFromFile)(resolved, {
|
|
@@ -427,8 +310,6 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
427
310
|
}
|
|
428
311
|
let srcSCC = sharedCache.sccIndex.get(normalizedFilename);
|
|
429
312
|
if (srcSCC === undefined) {
|
|
430
|
-
// Source still not covered after extending from target — source is in a
|
|
431
|
-
// different connected component. Extend from source to cover it.
|
|
432
313
|
(0, eslint_devkit_4.computeSCCsFromFile)(normalizedFilename, {
|
|
433
314
|
maxDepth: Infinity,
|
|
434
315
|
workspaceRoot,
|
|
@@ -441,13 +322,6 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
441
322
|
if (srcSCC === undefined || tgtSCC === undefined || srcSCC !== tgtSCC) {
|
|
442
323
|
return;
|
|
443
324
|
}
|
|
444
|
-
// =====================================================================
|
|
445
|
-
// BFS PATH-FINDING — only runs when SCC guarantees a cycle exists.
|
|
446
|
-
//
|
|
447
|
-
// findShortestCyclePath does BFS from `resolved` back to
|
|
448
|
-
// `normalizedFilename`, only traversing files within the same SCC.
|
|
449
|
-
// This is bounded by the SCC size, not the entire codebase.
|
|
450
|
-
// =====================================================================
|
|
451
325
|
const cyclePath = (0, eslint_devkit_4.findShortestCyclePath)(normalizedFilename, resolved, {
|
|
452
326
|
maxDepth,
|
|
453
327
|
workspaceRoot,
|
|
@@ -457,9 +331,7 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
457
331
|
});
|
|
458
332
|
if (!cyclePath)
|
|
459
333
|
return;
|
|
460
|
-
// Deduplicate: this import node produces exactly one error.
|
|
461
334
|
reportedImportNodes.add(node);
|
|
462
|
-
// Build the relevant cycle starting from normalizedFilename.
|
|
463
335
|
const cycleStart = cyclePath.indexOf(normalizedFilename);
|
|
464
336
|
const relevantCycle = cycleStart >= 0
|
|
465
337
|
? [...cyclePath.slice(cycleStart), ...cyclePath.slice(0, cycleStart)]
|
|
@@ -469,14 +341,9 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
469
341
|
const { messageId, data } = generateMessageData(relevantCycle, strategy, sourceImport);
|
|
470
342
|
context.report({ node, messageId, data });
|
|
471
343
|
},
|
|
472
|
-
// Re-export statements (`export { X } from '...'`) create import edges
|
|
473
|
-
// just like `import { X } from '...'`. Without this handler, cycles
|
|
474
|
-
// that close through a re-export are invisible to the rule.
|
|
475
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
476
344
|
ExportNamedDeclaration(node) {
|
|
477
345
|
if (!node.source)
|
|
478
|
-
return;
|
|
479
|
-
// Reuse the same logic as ImportDeclaration
|
|
346
|
+
return;
|
|
480
347
|
if (reportedImportNodes.has(node))
|
|
481
348
|
return;
|
|
482
349
|
const importSource = node.source.value;
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
-
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
-
* MIT license that can be found in the LICENSE file.
|
|
6
|
-
*/
|
|
7
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.noDefaultExport = void 0;
|
|
9
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
@@ -86,7 +81,6 @@ exports.noDefaultExport = (0, eslint_devkit_1.createRule)({
|
|
|
86
81
|
if (!filename) {
|
|
87
82
|
return false;
|
|
88
83
|
}
|
|
89
|
-
// Check if file is in allowed list
|
|
90
84
|
const allowedByFile = allowInFiles.some((pattern) => {
|
|
91
85
|
if (pattern.includes('*')) {
|
|
92
86
|
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
@@ -97,7 +91,6 @@ exports.noDefaultExport = (0, eslint_devkit_1.createRule)({
|
|
|
97
91
|
if (allowedByFile) {
|
|
98
92
|
return true;
|
|
99
93
|
}
|
|
100
|
-
// Check if filename matches allowed patterns
|
|
101
94
|
return allowPatterns.some((pattern) => {
|
|
102
95
|
if (pattern.includes('*')) {
|
|
103
96
|
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
@@ -111,28 +104,21 @@ exports.noDefaultExport = (0, eslint_devkit_1.createRule)({
|
|
|
111
104
|
return '';
|
|
112
105
|
}
|
|
113
106
|
const declaration = node.declaration;
|
|
114
|
-
// Try to determine a good name for the named export
|
|
115
107
|
let suggestedName = 'exportedValue';
|
|
116
108
|
if (declaration.type === 'Identifier') {
|
|
117
|
-
// export default foo; -> export { foo };
|
|
118
109
|
suggestedName = declaration.name;
|
|
119
110
|
}
|
|
120
111
|
else if (declaration.type === 'FunctionDeclaration' && declaration.id) {
|
|
121
|
-
// export default function foo() {} -> export function foo() {}
|
|
122
|
-
// But we still need to report this as it should be export function foo()
|
|
123
112
|
suggestedName = declaration.id.name;
|
|
124
113
|
}
|
|
125
114
|
else if (declaration.type === 'ClassDeclaration' && declaration.id) {
|
|
126
|
-
// export default class Foo {} -> export class Foo {}
|
|
127
115
|
suggestedName = declaration.id.name;
|
|
128
116
|
}
|
|
129
117
|
else if (declaration.type === 'FunctionDeclaration' &&
|
|
130
118
|
!declaration.id) {
|
|
131
|
-
// export default function() {} -> export function defaultFunction() {}
|
|
132
119
|
suggestedName = 'defaultFunction';
|
|
133
120
|
}
|
|
134
121
|
else if (declaration.type === 'ClassDeclaration' && !declaration.id) {
|
|
135
|
-
// export default class {} -> export class DefaultClass {}
|
|
136
122
|
suggestedName = 'DefaultClass';
|
|
137
123
|
}
|
|
138
124
|
return `// Convert to: export { ${suggestedName} };`;
|
|
@@ -155,7 +141,6 @@ exports.noDefaultExport = (0, eslint_devkit_1.createRule)({
|
|
|
155
141
|
const declaration = node.declaration;
|
|
156
142
|
const sourceCode = context.sourceCode;
|
|
157
143
|
if (declaration.type === 'Identifier') {
|
|
158
|
-
// export default foo; -> export { foo };
|
|
159
144
|
return fixer.replaceText(node, `export { ${declaration.name} };`);
|
|
160
145
|
}
|
|
161
146
|
else if (declaration.type === 'Literal' ||
|
|
@@ -164,14 +149,11 @@ exports.noDefaultExport = (0, eslint_devkit_1.createRule)({
|
|
|
164
149
|
declaration.type === 'ArrowFunctionExpression' ||
|
|
165
150
|
declaration.type === 'CallExpression' ||
|
|
166
151
|
declaration.type === 'NewExpression') {
|
|
167
|
-
// export default value; -> const defaultExport = value; export { defaultExport };
|
|
168
152
|
const exprText = sourceCode.getText(declaration);
|
|
169
|
-
// Use a heuristic to handle potential semicolons or multiline
|
|
170
153
|
const namedExport = `const defaultExport = ${exprText};\nexport { defaultExport };`;
|
|
171
154
|
return fixer.replaceText(node, namedExport);
|
|
172
155
|
}
|
|
173
156
|
else {
|
|
174
|
-
// For FunctionDeclaration and ClassDeclaration, replace 'export default' with 'export'
|
|
175
157
|
const nodeText = sourceCode.getText(node);
|
|
176
158
|
const namedExport = nodeText.replace(/^export default/, 'export');
|
|
177
159
|
return fixer.replaceText(node, namedExport);
|
|
@@ -179,13 +161,10 @@ exports.noDefaultExport = (0, eslint_devkit_1.createRule)({
|
|
|
179
161
|
},
|
|
180
162
|
});
|
|
181
163
|
},
|
|
182
|
-
// Also check for export { default } patterns
|
|
183
164
|
ExportNamedDeclaration(node) {
|
|
184
165
|
if (shouldAllow()) {
|
|
185
166
|
return;
|
|
186
167
|
}
|
|
187
|
-
// Check if this is export { default } from "module"
|
|
188
|
-
// This is a re-export pattern that effectively creates a default export
|
|
189
168
|
if (node.specifiers) {
|
|
190
169
|
const hasDefaultSpecifier = node.specifiers.some((spec) => (spec.exported.type === 'Identifier' &&
|
|
191
170
|
spec.exported.name === 'default') ||
|
|
@@ -205,7 +184,6 @@ exports.noDefaultExport = (0, eslint_devkit_1.createRule)({
|
|
|
205
184
|
}
|
|
206
185
|
}
|
|
207
186
|
},
|
|
208
|
-
// Check for export = value (TypeScript namespace export)
|
|
209
187
|
TSExportAssignment(node) {
|
|
210
188
|
if (shouldAllow()) {
|
|
211
189
|
return;
|
|
@@ -220,18 +198,12 @@ exports.noDefaultExport = (0, eslint_devkit_1.createRule)({
|
|
|
220
198
|
},
|
|
221
199
|
});
|
|
222
200
|
},
|
|
223
|
-
// Handle TypeScript interface and type exports
|
|
224
201
|
TSInterfaceDeclaration(node) {
|
|
225
|
-
// Check if this is a default export (export default interface ...)
|
|
226
|
-
// This would be caught by ExportDefaultDeclaration, but we need to handle
|
|
227
|
-
// the case where it's directly exported as default
|
|
228
202
|
if (node.parent && node.parent.type === 'ExportDefaultDeclaration') {
|
|
229
|
-
// This will be handled by ExportDefaultDeclaration visitor
|
|
230
203
|
return;
|
|
231
204
|
}
|
|
232
205
|
},
|
|
233
206
|
TSTypeAliasDeclaration(node) {
|
|
234
|
-
// Similar to interfaces
|
|
235
207
|
if (node.parent && node.parent.type === eslint_devkit_1.AST_NODE_TYPES.ExportDefaultDeclaration) {
|
|
236
208
|
return;
|
|
237
209
|
}
|