eslint-plugin-import-next 2.3.3 → 2.3.6
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/CHANGELOG.md +24 -0
- package/README.md +80 -70
- package/package.json +15 -3
- package/src/index.d.ts +192 -0
- package/src/index.js +13 -1
- package/src/oxlint.d.ts +20 -0
- package/src/oxlint.js +21 -0
- package/src/rules/consistent-type-specifier-style.js +5 -1
- package/src/rules/default.js +1 -0
- package/src/rules/dynamic-import-chunkname.js +4 -1
- package/src/rules/enforce-dependency-direction.js +2 -1
- package/src/rules/enforce-import-order.js +4 -2
- package/src/rules/enforce-team-boundaries.js +27 -17
- package/src/rules/export.js +3 -0
- package/src/rules/exports-last.js +3 -0
- package/src/rules/extensions.js +1 -0
- package/src/rules/first.js +2 -0
- package/src/rules/group-exports.js +3 -0
- package/src/rules/max-dependencies.js +2 -1
- package/src/rules/named.js +1 -0
- package/src/rules/namespace.js +1 -0
- package/src/rules/newline-after-import.js +1 -0
- package/src/rules/no-absolute-path.js +3 -0
- package/src/rules/no-amd.js +2 -1
- package/src/rules/no-anonymous-default-export.js +2 -0
- package/src/rules/no-barrel-file.js +2 -1
- package/src/rules/no-barrel-import.js +1 -0
- package/src/rules/no-commonjs.js +2 -1
- package/src/rules/no-cross-domain-imports.js +2 -1
- package/src/rules/no-cycle.js +94 -73
- package/src/rules/no-default-export.js +2 -1
- package/src/rules/no-deprecated.js +2 -1
- package/src/rules/no-duplicates.js +1 -0
- package/src/rules/no-dynamic-require.js +3 -0
- package/src/rules/no-empty-named-blocks.js +4 -1
- package/src/rules/no-extraneous-dependencies.js +4 -1
- package/src/rules/no-full-package-import.js +1 -0
- package/src/rules/no-import-module-exports.js +3 -0
- package/src/rules/no-internal-modules.js +1 -0
- package/src/rules/no-legacy-imports.js +1 -0
- package/src/rules/no-mutable-exports.js +2 -1
- package/src/rules/no-named-as-default-member.js +3 -0
- package/src/rules/no-named-as-default.js +3 -0
- package/src/rules/no-named-default.js +3 -0
- package/src/rules/no-named-export.js +2 -1
- package/src/rules/no-namespace.js +12 -5
- package/src/rules/no-nodejs-modules.js +3 -1
- package/src/rules/no-relative-packages.js +4 -1
- package/src/rules/no-relative-parent-imports.js +1 -0
- package/src/rules/no-restricted-paths.js +1 -0
- package/src/rules/no-self-import.js +2 -1
- package/src/rules/no-unassigned-import.js +1 -0
- package/src/rules/no-unresolved.js +3 -2
- package/src/rules/no-unused-modules.js +1 -0
- package/src/rules/no-useless-path-segments.js +3 -0
- package/src/rules/prefer-default-export.js +2 -1
- package/src/rules/prefer-direct-import.js +1 -0
- package/src/rules/prefer-modern-api.js +1 -0
- package/src/rules/prefer-node-protocol.js +2 -0
- package/src/rules/prefer-tree-shakeable-imports.js +1 -0
- package/src/rules/require-import-approval.js +1 -0
- package/src/rules/unambiguous.js +3 -0
|
@@ -12,7 +12,10 @@ exports.dynamicImportChunkname = (0, eslint_devkit_1.createRule)({
|
|
|
12
12
|
meta: {
|
|
13
13
|
type: 'suggestion',
|
|
14
14
|
docs: {
|
|
15
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/dynamic-import-chunkname.md',
|
|
15
16
|
description: 'Enforce a leading comment with the webpackChunkName for dynamic imports',
|
|
17
|
+
cwe: 'CWE-1078',
|
|
18
|
+
cvss: 2.5,
|
|
16
19
|
},
|
|
17
20
|
hasSuggestions: true,
|
|
18
21
|
messages: {
|
|
@@ -62,7 +65,7 @@ exports.dynamicImportChunkname = (0, eslint_devkit_1.createRule)({
|
|
|
62
65
|
const options = context.options[0] || {};
|
|
63
66
|
const { allowEmpty = false, webpackChunknameFormat = '[a-zA-Z0-9-_/.]+' } = options;
|
|
64
67
|
const chunkNamePattern = new RegExp(`^${webpackChunknameFormat}$`);
|
|
65
|
-
const sourceCode = context.
|
|
68
|
+
const sourceCode = context.sourceCode;
|
|
66
69
|
function checkChunkName(node) {
|
|
67
70
|
// Get leading comments
|
|
68
71
|
const comments = sourceCode.getCommentsBefore(node.source);
|
|
@@ -48,6 +48,7 @@ exports.enforceDependencyDirection = (0, eslint_devkit_2.createRule)({
|
|
|
48
48
|
meta: {
|
|
49
49
|
type: 'problem',
|
|
50
50
|
docs: {
|
|
51
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/enforce-dependency-direction.md',
|
|
51
52
|
description: 'Ensures dependencies flow in the correct architectural direction',
|
|
52
53
|
},
|
|
53
54
|
messages: {
|
|
@@ -129,7 +130,7 @@ exports.enforceDependencyDirection = (0, eslint_devkit_2.createRule)({
|
|
|
129
130
|
},
|
|
130
131
|
],
|
|
131
132
|
create(context, [options = {}]) {
|
|
132
|
-
const filename = context.
|
|
133
|
+
const filename = context.filename;
|
|
133
134
|
const { layers: providedLayers, layerPatterns: providedLayerPatterns, allowSameLayer = true, } = options || {};
|
|
134
135
|
// Use provided layers or defaults
|
|
135
136
|
const layers = providedLayers || [
|
|
@@ -30,6 +30,7 @@ exports.enforceImportOrder = (0, eslint_devkit_1.createRule)({
|
|
|
30
30
|
meta: {
|
|
31
31
|
type: 'suggestion',
|
|
32
32
|
docs: {
|
|
33
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/enforce-import-order.md',
|
|
33
34
|
description: 'Enforces a specific order for import statements',
|
|
34
35
|
},
|
|
35
36
|
fixable: 'code',
|
|
@@ -121,7 +122,7 @@ exports.enforceImportOrder = (0, eslint_devkit_1.createRule)({
|
|
|
121
122
|
defaultOptions.alphabetize.caseInsensitive,
|
|
122
123
|
},
|
|
123
124
|
};
|
|
124
|
-
const sourceCode = context.
|
|
125
|
+
const sourceCode = context.sourceCode;
|
|
125
126
|
const imports = [];
|
|
126
127
|
function getImportType(node) {
|
|
127
128
|
const source = node.source.value;
|
|
@@ -237,7 +238,8 @@ exports.enforceImportOrder = (0, eslint_devkit_1.createRule)({
|
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
240
|
const originalImports = [...imports];
|
|
240
|
-
|
|
241
|
+
// oxlint-disable-next-line unicorn/no-array-sort
|
|
242
|
+
const sortedImports = Array.from(imports).sort((a, b) => {
|
|
241
243
|
const typeA = getImportType(a);
|
|
242
244
|
const typeB = getImportType(b);
|
|
243
245
|
const rankA = getGroupRank(typeA);
|
|
@@ -7,18 +7,35 @@
|
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.enforceTeamBoundaries = void 0;
|
|
9
9
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
/**
|
|
11
|
+
* Translate a user-supplied glob (`foo/**\/*.ts`) into a RegExp.
|
|
12
|
+
*
|
|
13
|
+
* Important: regex metacharacters in the input must be escaped BEFORE the
|
|
14
|
+
* glob tokens are translated. Without escaping, a pattern like `lib\foo` is
|
|
15
|
+
* interpreted as `\f` (form feed) and `lib.foo` matches `libXfoo` rather
|
|
16
|
+
* than the literal dot the user wrote. CodeQL flags the un-escaped backslash
|
|
17
|
+
* specifically (`js/incomplete-sanitization`).
|
|
18
|
+
*
|
|
19
|
+
* Algorithm: escape all regex metas (including `\`), then unescape and
|
|
20
|
+
* translate just the glob tokens `**` and `*` back to their regex form.
|
|
21
|
+
*/
|
|
22
|
+
function globToRegex(pattern) {
|
|
23
|
+
// Escape regex metacharacters INCLUDING `*`, so the subsequent replaces
|
|
24
|
+
// can find the now-escaped `\*\*` / `\*` and translate them back to their
|
|
25
|
+
// glob meanings. Without escaping `*`, the `**` in `src/platform/**`
|
|
26
|
+
// reaches `new RegExp()` raw and throws "Nothing to repeat".
|
|
27
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\*]/g, '\\$&');
|
|
28
|
+
return new RegExp(escaped
|
|
29
|
+
.replace(/\\\*\\\*/g, '.*')
|
|
30
|
+
.replace(/\\\*/g, '[^/]*'));
|
|
31
|
+
}
|
|
10
32
|
/**
|
|
11
33
|
* Determine which team owns this file based on path patterns
|
|
12
34
|
*/
|
|
13
35
|
function getFileTeam(filePath, teams) {
|
|
14
36
|
for (const team of teams) {
|
|
15
37
|
for (const pattern of team.paths) {
|
|
16
|
-
|
|
17
|
-
const regex = new RegExp(pattern
|
|
18
|
-
.replace(/\*\*/g, '.*')
|
|
19
|
-
.replace(/\*/g, '[^/]*')
|
|
20
|
-
.replace(/\//g, '\\/'));
|
|
21
|
-
if (regex.test(filePath)) {
|
|
38
|
+
if (globToRegex(pattern).test(filePath)) {
|
|
22
39
|
return team;
|
|
23
40
|
}
|
|
24
41
|
}
|
|
@@ -31,11 +48,7 @@ function getFileTeam(filePath, teams) {
|
|
|
31
48
|
function getImportTeam(importPath, teams) {
|
|
32
49
|
for (const team of teams) {
|
|
33
50
|
for (const pattern of team.paths) {
|
|
34
|
-
|
|
35
|
-
.replace(/\*\*/g, '.*')
|
|
36
|
-
.replace(/\*/g, '[^/]*')
|
|
37
|
-
.replace(/\//g, '\\/'));
|
|
38
|
-
if (regex.test(importPath)) {
|
|
51
|
+
if (globToRegex(pattern).test(importPath)) {
|
|
39
52
|
return team;
|
|
40
53
|
}
|
|
41
54
|
}
|
|
@@ -77,11 +90,7 @@ function isExternalImport(importPath) {
|
|
|
77
90
|
*/
|
|
78
91
|
function isSharedPath(importPath, sharedPaths) {
|
|
79
92
|
for (const shared of sharedPaths) {
|
|
80
|
-
|
|
81
|
-
.replace(/\*\*/g, '.*')
|
|
82
|
-
.replace(/\*/g, '[^/]*')
|
|
83
|
-
.replace(/\//g, '\\/'));
|
|
84
|
-
if (regex.test(importPath)) {
|
|
93
|
+
if (globToRegex(shared).test(importPath)) {
|
|
85
94
|
return true;
|
|
86
95
|
}
|
|
87
96
|
}
|
|
@@ -92,6 +101,7 @@ exports.enforceTeamBoundaries = (0, eslint_devkit_1.createRule)({
|
|
|
92
101
|
meta: {
|
|
93
102
|
type: 'problem',
|
|
94
103
|
docs: {
|
|
104
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/enforce-team-boundaries.md',
|
|
95
105
|
description: 'Prevent unauthorized cross-team imports in large codebases',
|
|
96
106
|
},
|
|
97
107
|
messages: {
|
|
@@ -154,7 +164,7 @@ exports.enforceTeamBoundaries = (0, eslint_devkit_1.createRule)({
|
|
|
154
164
|
create(context, [options]) {
|
|
155
165
|
const { teams, sharedPaths = [], allowExternalPackages = true } = options;
|
|
156
166
|
// Get the file's team ownership
|
|
157
|
-
const filename = context.filename
|
|
167
|
+
const filename = context.filename;
|
|
158
168
|
const sourceTeam = getFileTeam(filename, teams);
|
|
159
169
|
// If file doesn't belong to any team, skip checking
|
|
160
170
|
if (!sourceTeam) {
|
package/src/rules/export.js
CHANGED
|
@@ -12,7 +12,10 @@ exports.exportRule = (0, eslint_devkit_1.createRule)({
|
|
|
12
12
|
meta: {
|
|
13
13
|
type: 'problem',
|
|
14
14
|
docs: {
|
|
15
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/export.md',
|
|
15
16
|
description: 'Forbid any invalid exports, i.e. re-export of the same name',
|
|
17
|
+
cwe: 'CWE-694',
|
|
18
|
+
cvss: 7.5,
|
|
16
19
|
},
|
|
17
20
|
messages: {
|
|
18
21
|
duplicateExport: (0, eslint_devkit_1.formatLLMMessage)({
|
|
@@ -12,7 +12,10 @@ exports.exportsLast = (0, eslint_devkit_1.createRule)({
|
|
|
12
12
|
meta: {
|
|
13
13
|
type: 'suggestion',
|
|
14
14
|
docs: {
|
|
15
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/exports-last.md',
|
|
15
16
|
description: 'Ensure all exports appear after other statements',
|
|
17
|
+
cwe: 'CWE-1078',
|
|
18
|
+
cvss: 2.5,
|
|
16
19
|
},
|
|
17
20
|
messages: {
|
|
18
21
|
exportNotLast: (0, eslint_devkit_1.formatLLMMessage)({
|
package/src/rules/extensions.js
CHANGED
|
@@ -15,6 +15,7 @@ exports.extensions = (0, eslint_devkit_1.createRule)({
|
|
|
15
15
|
meta: {
|
|
16
16
|
type: 'suggestion',
|
|
17
17
|
docs: {
|
|
18
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/extensions.md',
|
|
18
19
|
description: 'Ensure consistent use of file extensions in imports',
|
|
19
20
|
},
|
|
20
21
|
fixable: 'code',
|
package/src/rules/first.js
CHANGED
|
@@ -13,6 +13,7 @@ exports.first = (0, eslint_devkit_1.createRule)({
|
|
|
13
13
|
meta: {
|
|
14
14
|
type: 'suggestion',
|
|
15
15
|
docs: {
|
|
16
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/first.md',
|
|
16
17
|
description: 'Ensure all imports appear before other statements',
|
|
17
18
|
},
|
|
18
19
|
fixable: 'code',
|
|
@@ -48,6 +49,7 @@ exports.first = (0, eslint_devkit_1.createRule)({
|
|
|
48
49
|
}
|
|
49
50
|
},
|
|
50
51
|
};
|
|
52
|
+
// oxlint-disable-next-line consistent-function-scoping
|
|
51
53
|
function isDirective(node) {
|
|
52
54
|
return (node.type === 'ExpressionStatement' &&
|
|
53
55
|
node.expression.type === 'Literal' &&
|
|
@@ -12,7 +12,10 @@ exports.groupExports = (0, eslint_devkit_1.createRule)({
|
|
|
12
12
|
meta: {
|
|
13
13
|
type: 'suggestion',
|
|
14
14
|
docs: {
|
|
15
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/group-exports.md',
|
|
15
16
|
description: 'Prefer named exports to be grouped together in a single export declaration',
|
|
17
|
+
cwe: 'CWE-1078',
|
|
18
|
+
cvss: 2.5,
|
|
16
19
|
},
|
|
17
20
|
messages: {
|
|
18
21
|
groupExports: (0, eslint_devkit_1.formatLLMMessage)({
|
|
@@ -13,6 +13,7 @@ exports.maxDependencies = (0, eslint_devkit_1.createRule)({
|
|
|
13
13
|
meta: {
|
|
14
14
|
type: 'suggestion',
|
|
15
15
|
docs: {
|
|
16
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/max-dependencies.md',
|
|
16
17
|
description: 'Enforce the maximum number of dependencies a module can have',
|
|
17
18
|
},
|
|
18
19
|
hasSuggestions: false,
|
|
@@ -87,7 +88,7 @@ exports.maxDependencies = (0, eslint_devkit_1.createRule)({
|
|
|
87
88
|
create(context) {
|
|
88
89
|
const [options] = context.options;
|
|
89
90
|
const { max = 10, ignoreTypeImports = true, ignoreImports = [], ignoreFiles = [], } = options || {};
|
|
90
|
-
const filename = context.
|
|
91
|
+
const filename = context.filename;
|
|
91
92
|
if (!filename) {
|
|
92
93
|
return {};
|
|
93
94
|
}
|
package/src/rules/named.js
CHANGED
|
@@ -15,6 +15,7 @@ exports.named = (0, eslint_devkit_1.createRule)({
|
|
|
15
15
|
meta: {
|
|
16
16
|
type: 'problem',
|
|
17
17
|
docs: {
|
|
18
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/named.md',
|
|
18
19
|
description: 'Ensure named imports correspond to a named export in the remote file',
|
|
19
20
|
},
|
|
20
21
|
fixable: 'code',
|
package/src/rules/namespace.js
CHANGED
|
@@ -15,6 +15,7 @@ exports.namespace = (0, eslint_devkit_1.createRule)({
|
|
|
15
15
|
meta: {
|
|
16
16
|
type: 'problem',
|
|
17
17
|
docs: {
|
|
18
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/namespace.md',
|
|
18
19
|
description: 'Ensure imported namespaces contain dereferenced properties as they are dereferenced',
|
|
19
20
|
},
|
|
20
21
|
messages: {
|
|
@@ -13,6 +13,7 @@ exports.newlineAfterImport = (0, eslint_devkit_1.createRule)({
|
|
|
13
13
|
meta: {
|
|
14
14
|
type: 'layout',
|
|
15
15
|
docs: {
|
|
16
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/newline-after-import.md',
|
|
16
17
|
description: 'Enforce a newline after import statements',
|
|
17
18
|
},
|
|
18
19
|
fixable: 'whitespace',
|
|
@@ -14,7 +14,10 @@ exports.noAbsolutePath = (0, eslint_devkit_1.createRule)({
|
|
|
14
14
|
meta: {
|
|
15
15
|
type: 'suggestion',
|
|
16
16
|
docs: {
|
|
17
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-absolute-path.md',
|
|
17
18
|
description: 'Forbid import of modules using absolute paths',
|
|
19
|
+
cwe: 'CWE-426',
|
|
20
|
+
cvss: 5,
|
|
18
21
|
},
|
|
19
22
|
fixable: 'code',
|
|
20
23
|
messages: {
|
package/src/rules/no-amd.js
CHANGED
|
@@ -13,6 +13,7 @@ exports.noAmd = (0, eslint_devkit_1.createRule)({
|
|
|
13
13
|
meta: {
|
|
14
14
|
type: 'problem',
|
|
15
15
|
docs: {
|
|
16
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-amd.md',
|
|
16
17
|
description: 'Prevents AMD require/define calls',
|
|
17
18
|
},
|
|
18
19
|
hasSuggestions: false,
|
|
@@ -58,7 +59,7 @@ exports.noAmd = (0, eslint_devkit_1.createRule)({
|
|
|
58
59
|
const { allow = [],
|
|
59
60
|
// suggestES6 = true // Reserved for future use when hasSuggestions is enabled
|
|
60
61
|
} = options || {};
|
|
61
|
-
const filename = context.
|
|
62
|
+
const filename = context.filename;
|
|
62
63
|
function shouldAllow() {
|
|
63
64
|
// Check if the call is in an allowed pattern
|
|
64
65
|
return allow.some((pattern) => {
|
|
@@ -13,6 +13,7 @@ exports.noAnonymousDefaultExport = (0, eslint_devkit_1.createRule)({
|
|
|
13
13
|
meta: {
|
|
14
14
|
type: 'suggestion',
|
|
15
15
|
docs: {
|
|
16
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-anonymous-default-export.md',
|
|
16
17
|
description: 'Forbid anonymous values as default exports',
|
|
17
18
|
},
|
|
18
19
|
messages: {
|
|
@@ -114,6 +115,7 @@ exports.noAnonymousDefaultExport = (0, eslint_devkit_1.createRule)({
|
|
|
114
115
|
return true;
|
|
115
116
|
}
|
|
116
117
|
}
|
|
118
|
+
// oxlint-disable-next-line consistent-function-scoping
|
|
117
119
|
function getSuggestionText(node) {
|
|
118
120
|
const declaration = node.declaration;
|
|
119
121
|
switch (declaration.type) {
|
|
@@ -74,6 +74,7 @@ exports.noBarrelFile = (0, eslint_devkit_1.createRule)({
|
|
|
74
74
|
meta: {
|
|
75
75
|
type: 'problem',
|
|
76
76
|
docs: {
|
|
77
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-barrel-file.md',
|
|
77
78
|
description: 'Disallow barrel files that harm build performance and tree-shaking efficiency',
|
|
78
79
|
},
|
|
79
80
|
hasSuggestions: true,
|
|
@@ -148,7 +149,7 @@ exports.noBarrelFile = (0, eslint_devkit_1.createRule)({
|
|
|
148
149
|
],
|
|
149
150
|
create(context, [options = {}]) {
|
|
150
151
|
const { threshold = 3, allowedPaths = [], barrelPatterns = DEFAULT_BARREL_PATTERNS, allowWithLocalExports = false, reexportRatio = 0.8, } = options || {};
|
|
151
|
-
const filename = context.filename
|
|
152
|
+
const filename = context.filename;
|
|
152
153
|
// Check if file matches barrel patterns
|
|
153
154
|
if (!matchesPattern(filename, barrelPatterns)) {
|
|
154
155
|
return {};
|
|
@@ -45,6 +45,7 @@ exports.noBarrelImport = (0, eslint_devkit_1.createRule)({
|
|
|
45
45
|
meta: {
|
|
46
46
|
type: 'suggestion',
|
|
47
47
|
docs: {
|
|
48
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-barrel-import.md',
|
|
48
49
|
description: 'Disallow imports from barrel files to improve build performance and tree-shaking',
|
|
49
50
|
},
|
|
50
51
|
fixable: 'code',
|
package/src/rules/no-commonjs.js
CHANGED
|
@@ -13,6 +13,7 @@ exports.noCommonjs = (0, eslint_devkit_1.createRule)({
|
|
|
13
13
|
meta: {
|
|
14
14
|
type: 'problem',
|
|
15
15
|
docs: {
|
|
16
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-commonjs.md',
|
|
16
17
|
description: 'Prevents CommonJS require/module.exports',
|
|
17
18
|
},
|
|
18
19
|
hasSuggestions: true,
|
|
@@ -98,7 +99,7 @@ exports.noCommonjs = (0, eslint_devkit_1.createRule)({
|
|
|
98
99
|
create(context) {
|
|
99
100
|
const [options] = context.options;
|
|
100
101
|
const { allowRequire = false, allowModuleExports = false, allowExports = false, allowInFiles = [], suggestES6 = true, } = options || {};
|
|
101
|
-
const filename = context.
|
|
102
|
+
const filename = context.filename;
|
|
102
103
|
function shouldAllow() {
|
|
103
104
|
if (!filename) {
|
|
104
105
|
return false;
|
|
@@ -130,6 +130,7 @@ exports.noCrossDomainImports = (0, eslint_devkit_2.createRule)({
|
|
|
130
130
|
meta: {
|
|
131
131
|
type: 'problem',
|
|
132
132
|
docs: {
|
|
133
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-cross-domain-imports.md',
|
|
133
134
|
description: 'Prevents imports across domain/feature boundaries',
|
|
134
135
|
},
|
|
135
136
|
messages: {
|
|
@@ -221,7 +222,7 @@ exports.noCrossDomainImports = (0, eslint_devkit_2.createRule)({
|
|
|
221
222
|
},
|
|
222
223
|
],
|
|
223
224
|
create(context, [options = {}]) {
|
|
224
|
-
const filename = context.
|
|
225
|
+
const filename = context.filename;
|
|
225
226
|
/**
|
|
226
227
|
* Check import declarations
|
|
227
228
|
*/
|
package/src/rules/no-cycle.js
CHANGED
|
@@ -57,7 +57,11 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
57
57
|
meta: {
|
|
58
58
|
type: 'problem',
|
|
59
59
|
docs: {
|
|
60
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-cycle.md',
|
|
60
61
|
description: 'Detect circular dependencies that cause bundle memory bloat and initialization issues',
|
|
62
|
+
cwe: 'CWE-407',
|
|
63
|
+
cvss: 9.5,
|
|
64
|
+
confidence: 'high',
|
|
61
65
|
},
|
|
62
66
|
messages: {
|
|
63
67
|
// 🎯 Token optimization: 45% reduction (~70→38 tokens per message) for architecture clarity
|
|
@@ -104,8 +108,8 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
104
108
|
properties: {
|
|
105
109
|
maxDepth: {
|
|
106
110
|
type: 'number',
|
|
107
|
-
default:
|
|
108
|
-
description: 'Maximum depth to traverse when detecting cycles (performance
|
|
111
|
+
default: Number.MAX_SAFE_INTEGER,
|
|
112
|
+
description: 'Maximum depth to traverse when detecting cycles. Default: unlimited (matches eslint-plugin-import and oxlint). Lower values are a performance escape hatch — but with our nonCyclicFiles cache, traversal cost is amortized, and a low cap silently misses cycles deeper than the limit. Set to a finite number only on huge graphs where the bench latency hurts you.',
|
|
109
113
|
},
|
|
110
114
|
ignorePatterns: {
|
|
111
115
|
type: 'array',
|
|
@@ -168,7 +172,12 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
168
172
|
},
|
|
169
173
|
defaultOptions: [
|
|
170
174
|
{
|
|
171
|
-
|
|
175
|
+
// Unlimited: matches eslint-plugin-import (Infinity) and oxlint
|
|
176
|
+
// (u32::MAX). The earlier default of 10 silently missed any cycle
|
|
177
|
+
// deeper than 10 hops — verified on next.js's webpack-config.ts
|
|
178
|
+
// (~12 hops). The nonCyclicFiles cache + the depth-limit-truncation
|
|
179
|
+
// fix in eslint-devkit's dependency-analysis make unlimited safe.
|
|
180
|
+
maxDepth: Number.MAX_SAFE_INTEGER,
|
|
172
181
|
ignorePatterns: [],
|
|
173
182
|
barrelExports: ['index.ts', 'index.tsx', 'index.js', 'index.jsx'],
|
|
174
183
|
reportAllCycles: true,
|
|
@@ -180,7 +189,7 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
180
189
|
],
|
|
181
190
|
create(context) {
|
|
182
191
|
const options = context.options[0] || {};
|
|
183
|
-
const maxDepth = options.maxDepth ??
|
|
192
|
+
const maxDepth = options.maxDepth ?? Number.MAX_SAFE_INTEGER;
|
|
184
193
|
const ignorePatterns = options.ignorePatterns ?? [
|
|
185
194
|
'**/*.test.ts',
|
|
186
195
|
'**/*.test.tsx',
|
|
@@ -201,13 +210,15 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
201
210
|
const moduleNamingConvention = options.moduleNamingConvention ?? 'semantic';
|
|
202
211
|
const coreModuleSuffix = options.coreModuleSuffix ?? 'core';
|
|
203
212
|
const extendedModuleSuffix = options.extendedModuleSuffix ?? 'extended';
|
|
204
|
-
const filename = context.
|
|
205
|
-
const workspaceRoot = context.
|
|
213
|
+
const filename = context.filename;
|
|
214
|
+
const workspaceRoot = context.cwd;
|
|
206
215
|
// Get resolver settings from ESLint settings (compatible with eslint-plugin-import)
|
|
207
|
-
// eslint-plugin-import uses settings['import/resolver']
|
|
216
|
+
// eslint-plugin-import uses settings['import/resolver']; allow either the
|
|
217
|
+
// upstream key or our drop-in `import-next/resolver` form for users
|
|
218
|
+
// migrating off eslint-plugin-import.
|
|
208
219
|
const settings = context.settings;
|
|
209
220
|
const resolverSettings = settings?.['import/resolver'] ||
|
|
210
|
-
settings?.['
|
|
221
|
+
settings?.['import-next/resolver'];
|
|
211
222
|
// Skip ignored files early
|
|
212
223
|
if ((0, eslint_devkit_3.shouldIgnoreFile)(filename, ignorePatterns, sharedCache.compiledPatterns)) {
|
|
213
224
|
return {};
|
|
@@ -257,9 +268,9 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
257
268
|
* Generate DIRECT IMPORT message
|
|
258
269
|
*/
|
|
259
270
|
function generateDirectImportMessage(cycle, sourceImport) {
|
|
260
|
-
const currentFile = (0, eslint_devkit_3.getBasename)(context.
|
|
271
|
+
const currentFile = (0, eslint_devkit_3.getBasename)(context.filename);
|
|
261
272
|
const targetFile = cycle[1];
|
|
262
|
-
const relativeImport = (0, eslint_devkit_3.getRelativeImportPath)(context.
|
|
273
|
+
const relativeImport = (0, eslint_devkit_3.getRelativeImportPath)(context.filename, targetFile);
|
|
263
274
|
return {
|
|
264
275
|
cycle: (0, eslint_devkit_3.formatCycleDisplay)(cycle, workspaceRoot),
|
|
265
276
|
currentFile,
|
|
@@ -333,83 +344,93 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
|
|
|
333
344
|
};
|
|
334
345
|
}
|
|
335
346
|
}
|
|
336
|
-
// Store found cycles to report on specific imports
|
|
337
|
-
let detectedCycles = [];
|
|
338
347
|
return {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
maxDepth,
|
|
345
|
-
reportAllCycles,
|
|
348
|
+
ImportDeclaration(node) {
|
|
349
|
+
const importSource = node.source.value;
|
|
350
|
+
// Resolve the import target to an absolute path
|
|
351
|
+
const resolved = (0, eslint_devkit_4.resolveImportPath)(importSource, {
|
|
352
|
+
fromFile: filename,
|
|
346
353
|
workspaceRoot,
|
|
347
354
|
barrelExports,
|
|
348
355
|
cache: sharedCache,
|
|
349
356
|
resolverSettings,
|
|
350
357
|
});
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
// Skip if we've already reported this cycle (shared across all files)
|
|
369
|
-
if (sharedCache.reportedCycles.has(cycleHash)) {
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
sharedCache.reportedCycles.add(cycleHash);
|
|
373
|
-
// Select the appropriate fix strategy
|
|
374
|
-
const strategy = selectFixStrategy(relevantCycle, fixStrategy);
|
|
375
|
-
// Find the target of the first import that causes the cycle
|
|
376
|
-
const cycleTarget = relevantCycle[1] || relevantCycle[0]; // The next file in the cycle
|
|
377
|
-
detectedCycles.push({
|
|
378
|
-
cycleTarget,
|
|
379
|
-
strategy,
|
|
380
|
-
relevantCycle,
|
|
381
|
-
cycleHash,
|
|
382
|
-
});
|
|
383
|
-
}
|
|
358
|
+
if (!resolved)
|
|
359
|
+
return;
|
|
360
|
+
// =====================================================================
|
|
361
|
+
// PER-IMPORT CYCLE DETECTION (Phase 3 optimization)
|
|
362
|
+
//
|
|
363
|
+
// Instead of computing all cycles upfront in Program() via full-graph
|
|
364
|
+
// BFS + Tarjan SCC (which reads ALL ~5K reachable files), we check
|
|
365
|
+
// each import individually with a targeted DFS.
|
|
366
|
+
//
|
|
367
|
+
// Performance advantage:
|
|
368
|
+
// - Only reads files along the actual DFS path (not all reachable)
|
|
369
|
+
// - nonCyclicFiles cache provides O(1) rejection after first visit
|
|
370
|
+
// - getFileImports cache reuses parsed results across files
|
|
371
|
+
// =====================================================================
|
|
372
|
+
// Fast path: if the target file is known to be non-cyclic, skip
|
|
373
|
+
if (sharedCache.nonCyclicFiles.has(resolved)) {
|
|
374
|
+
return;
|
|
384
375
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const resolved = (0, eslint_devkit_4.resolveImportPath)(importSource, {
|
|
390
|
-
fromFile: filename,
|
|
376
|
+
// Check if following this import leads back to the current file
|
|
377
|
+
const cycles = (0, eslint_devkit_4.detectCycleFromImport)(filename, resolved, {
|
|
378
|
+
maxDepth,
|
|
379
|
+
reportAllCycles,
|
|
391
380
|
workspaceRoot,
|
|
392
381
|
barrelExports,
|
|
393
382
|
cache: sharedCache,
|
|
394
383
|
resolverSettings,
|
|
395
384
|
});
|
|
396
|
-
if (
|
|
385
|
+
if (cycles.length === 0)
|
|
397
386
|
return;
|
|
398
|
-
for (const cycle of
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
};
|
|
405
|
-
// Generate the appropriate message based on strategy
|
|
406
|
-
const { messageId, data } = generateMessageData(cycle.relevantCycle, cycle.strategy, sourceImport);
|
|
407
|
-
context.report({
|
|
408
|
-
node,
|
|
409
|
-
messageId,
|
|
410
|
-
data,
|
|
411
|
-
});
|
|
387
|
+
for (const cycle of cycles) {
|
|
388
|
+
// Extract the minimal cycle (the actual loop)
|
|
389
|
+
const minimalCycle = (0, eslint_devkit_4.getMinimalCycle)(cycle);
|
|
390
|
+
// Only report if current file is part of the minimal cycle
|
|
391
|
+
if (!minimalCycle.includes(filename)) {
|
|
392
|
+
continue;
|
|
412
393
|
}
|
|
394
|
+
const cycleStart = minimalCycle.indexOf(filename);
|
|
395
|
+
if (cycleStart === -1)
|
|
396
|
+
continue;
|
|
397
|
+
const relevantCycle = [
|
|
398
|
+
...minimalCycle.slice(cycleStart),
|
|
399
|
+
...minimalCycle.slice(0, cycleStart),
|
|
400
|
+
];
|
|
401
|
+
// Dedupe by (file, cycle) instead of cycle alone. Without the
|
|
402
|
+
// file in the key, the cycle [A, B] reports only on whichever
|
|
403
|
+
// of A or B is linted first; the other gets silently skipped.
|
|
404
|
+
// Per-file keying matches oxlint's behavior — every file in a
|
|
405
|
+
// cycle gets its own diagnostic. The user can navigate from any
|
|
406
|
+
// entry point in the cycle and see the report there.
|
|
407
|
+
const cycleHash = `${filename}::${(0, eslint_devkit_4.getCycleHash)(relevantCycle)}`;
|
|
408
|
+
// Skip only if THIS file already reported THIS cycle (e.g. the
|
|
409
|
+
// same import statement appearing twice in the same file).
|
|
410
|
+
// (Earlier we experimented with a `pendingCycleReports` fan-out
|
|
411
|
+
// that emits on every cycle member regardless of which one's
|
|
412
|
+
// DFS discovered it — closes the with-router.tsx presentational
|
|
413
|
+
// gap but caused a 10× slowdown and 23× finding-count explosion
|
|
414
|
+
// because each cycle member iterates every other member of every
|
|
415
|
+
// cycle it touches. The cycle IS still reported on its other
|
|
416
|
+
// end; the one-finding loss isn't worth the perf hit.)
|
|
417
|
+
if (sharedCache.reportedCycles.has(cycleHash)) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
sharedCache.reportedCycles.add(cycleHash);
|
|
421
|
+
// Select the appropriate fix strategy
|
|
422
|
+
const strategy = selectFixStrategy(relevantCycle, fixStrategy);
|
|
423
|
+
// Generate the appropriate message based on strategy
|
|
424
|
+
const sourceImport = {
|
|
425
|
+
path: resolved,
|
|
426
|
+
source: importSource,
|
|
427
|
+
};
|
|
428
|
+
const { messageId, data } = generateMessageData(relevantCycle, strategy, sourceImport);
|
|
429
|
+
context.report({
|
|
430
|
+
node,
|
|
431
|
+
messageId,
|
|
432
|
+
data,
|
|
433
|
+
});
|
|
413
434
|
}
|
|
414
435
|
},
|
|
415
436
|
};
|
|
@@ -13,6 +13,7 @@ exports.noDefaultExport = (0, eslint_devkit_1.createRule)({
|
|
|
13
13
|
meta: {
|
|
14
14
|
type: 'problem',
|
|
15
15
|
docs: {
|
|
16
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-default-export.md',
|
|
16
17
|
description: 'Prevents default exports',
|
|
17
18
|
},
|
|
18
19
|
fixable: 'code',
|
|
@@ -80,7 +81,7 @@ exports.noDefaultExport = (0, eslint_devkit_1.createRule)({
|
|
|
80
81
|
create(context) {
|
|
81
82
|
const [options] = context.options;
|
|
82
83
|
const { allowInFiles = [], allowPatterns = [], suggestNamed = true, } = options || {};
|
|
83
|
-
const filename = context.
|
|
84
|
+
const filename = context.filename;
|
|
84
85
|
function shouldAllow() {
|
|
85
86
|
if (!filename) {
|
|
86
87
|
return false;
|
|
@@ -13,6 +13,7 @@ exports.noDeprecated = (0, eslint_devkit_1.createRule)({
|
|
|
13
13
|
meta: {
|
|
14
14
|
type: 'problem',
|
|
15
15
|
docs: {
|
|
16
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-deprecated.md',
|
|
16
17
|
description: 'Forbid imported names marked with @deprecated documentation tag',
|
|
17
18
|
},
|
|
18
19
|
hasSuggestions: true,
|
|
@@ -83,7 +84,7 @@ exports.noDeprecated = (0, eslint_devkit_1.createRule)({
|
|
|
83
84
|
create(context) {
|
|
84
85
|
const [options] = context.options;
|
|
85
86
|
const { allow = [], deprecationMarkers = ['@deprecated'], checkJSDoc = true, checkDecorators = true, } = options || {};
|
|
86
|
-
const filename = context.
|
|
87
|
+
const filename = context.filename;
|
|
87
88
|
if (!filename) {
|
|
88
89
|
return {};
|
|
89
90
|
}
|