eslint-plugin-import-lite 0.1.0
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/LICENSE +21 -0
- package/README.md +40 -0
- package/dist/ast.js +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +46 -0
- package/dist/index2.d.ts +9 -0
- package/dist/rules/consistent-type-specifier-style.d.ts +8 -0
- package/dist/rules/consistent-type-specifier-style.js +5 -0
- package/dist/rules/consistent-type-specifier-style2.js +111 -0
- package/dist/rules/first.d.ts +8 -0
- package/dist/rules/first.js +4 -0
- package/dist/rules/first2.js +105 -0
- package/dist/rules/newline-after-import.d.ts +12 -0
- package/dist/rules/newline-after-import.js +4 -0
- package/dist/rules/newline-after-import2.js +172 -0
- package/dist/rules/no-default-export.d.ts +8 -0
- package/dist/rules/no-default-export.js +5 -0
- package/dist/rules/no-default-export2.js +50 -0
- package/dist/rules/no-duplicates.d.ts +18 -0
- package/dist/rules/no-duplicates.js +4 -0
- package/dist/rules/no-duplicates2.js +196 -0
- package/dist/rules/no-mutable-exports.d.ts +8 -0
- package/dist/rules/no-mutable-exports.js +4 -0
- package/dist/rules/no-mutable-exports2.js +41 -0
- package/dist/rules/no-named-default.d.ts +8 -0
- package/dist/rules/no-named-default.js +5 -0
- package/dist/rules/no-named-default2.js +29 -0
- package/dist/utils.js +30 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 vida xie
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# eslint-plugin-import-lite
|
|
2
|
+
|
|
3
|
+
[![npm version][npm-version-src]][npm-version-href]
|
|
4
|
+
[![npm bundle size][npm-bundle-size-src]][npm-bundle-size-href]
|
|
5
|
+
[![License][license-src]][license-href]
|
|
6
|
+
|
|
7
|
+
## Feature
|
|
8
|
+
|
|
9
|
+
- Port some useful rules from [`eslint-plugin-import-x`](https://github.com/un-ts/eslint-plugin-import-x).
|
|
10
|
+
- No need resolver and settings in [`eslint-plugin-import-x`](https://github.com/un-ts/eslint-plugin-import-x).
|
|
11
|
+
- Drop babel and flow support.
|
|
12
|
+
|
|
13
|
+
See all rules in [`src/rules`](./src/rules)
|
|
14
|
+
|
|
15
|
+
## Motivation
|
|
16
|
+
|
|
17
|
+
I extend [my own ESLint config](https://github.com/9romise/eslint-config) from [`@antfu/eslint-config`](https://github.com/antfu/eslint-config).
|
|
18
|
+
|
|
19
|
+
Recently this config dropped [`eslint-plugin-import-x`](https://github.com/un-ts/eslint-plugin-import-x) cause it introduce some binary packages and make it heavy.
|
|
20
|
+
|
|
21
|
+
In a [discussion]((https://github.com/9romise/eslint-import-resolver-oxc/issues/87#issuecomment-2945162572)) about built-in resolver, the maintainer plan to keep it as dependency, which makes it impossible to lightweight the package.
|
|
22
|
+
|
|
23
|
+
But there are some useful rules and [some people (include me) want to bring the plugin back](https://github.com/antfu/eslint-config/issues/720).
|
|
24
|
+
|
|
25
|
+
## Credits
|
|
26
|
+
|
|
27
|
+
[eslint-plugin-import-x](https://github.com/un-ts/eslint-plugin-import-x) - [MIT](https://github.com/un-ts/eslint-plugin-import-x/blob/master/LICENSE)
|
|
28
|
+
|
|
29
|
+
## License
|
|
30
|
+
|
|
31
|
+
[MIT](./LICENSE) License © 2025-PRESENT [Vida Xie](https://github.com/9romise)
|
|
32
|
+
|
|
33
|
+
<!-- Badges -->
|
|
34
|
+
|
|
35
|
+
[npm-version-src]: https://img.shields.io/npm/v/eslint-plugin-import-lite
|
|
36
|
+
[npm-version-href]: https://npmjs.com/package/eslint-plugin-import-lite
|
|
37
|
+
[npm-bundle-size-src]: https://img.shields.io/npm/unpacked-size/eslint-plugin-import-lite
|
|
38
|
+
[npm-bundle-size-href]: https://npmjs.com/package/eslint-plugin-import-lite
|
|
39
|
+
[license-src]: https://img.shields.io/npm/l/eslint-plugin-import-lite
|
|
40
|
+
[license-href]: https://opensource.org/licenses/MIT
|
package/dist/ast.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/ast.ts
|
|
4
|
+
function getValue(node) {
|
|
5
|
+
switch (node.type) {
|
|
6
|
+
case TSESTree.AST_NODE_TYPES.Identifier: return node.name;
|
|
7
|
+
case TSESTree.AST_NODE_TYPES.Literal: return node.value;
|
|
8
|
+
default: throw new Error(`Unsupported node type: ${node.type}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { getValue };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ESLintRuleModule } from "./index2.js";
|
|
2
|
+
import { Linter } from "eslint";
|
|
3
|
+
|
|
4
|
+
//#region src/index.d.ts
|
|
5
|
+
declare const _default: {
|
|
6
|
+
rules: Record<string, ESLintRuleModule<unknown[], string>>;
|
|
7
|
+
configs: {
|
|
8
|
+
recommended: Linter.Config<Linter.RulesRecord>;
|
|
9
|
+
all: Linter.Config<Linter.RulesRecord>;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { _default as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import "./utils.js";
|
|
2
|
+
import "./ast.js";
|
|
3
|
+
import { consistent_type_specifier_style_default } from "./rules/consistent-type-specifier-style2.js";
|
|
4
|
+
import { first_default } from "./rules/first2.js";
|
|
5
|
+
import { newline_after_import_default } from "./rules/newline-after-import2.js";
|
|
6
|
+
import { no_default_export_default } from "./rules/no-default-export2.js";
|
|
7
|
+
import { no_duplicates_default } from "./rules/no-duplicates2.js";
|
|
8
|
+
import { no_mutable_exports_default } from "./rules/no-mutable-exports2.js";
|
|
9
|
+
import { no_named_default_default } from "./rules/no-named-default2.js";
|
|
10
|
+
|
|
11
|
+
//#region src/rules/index.ts
|
|
12
|
+
const rules = {
|
|
13
|
+
"consistent-type-specifier-style": consistent_type_specifier_style_default,
|
|
14
|
+
"first": first_default,
|
|
15
|
+
"newline-after-import": newline_after_import_default,
|
|
16
|
+
"no-default-export": no_default_export_default,
|
|
17
|
+
"no-duplicates": no_duplicates_default,
|
|
18
|
+
"no-mutable-exports": no_mutable_exports_default,
|
|
19
|
+
"no-named-default": no_named_default_default
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/index.ts
|
|
24
|
+
const pluginName = "import-lite";
|
|
25
|
+
function generateConfig(name, filter) {
|
|
26
|
+
let ruleMeta = Object.entries(rules).filter(([_, rule]) => !rule.meta?.deprecated);
|
|
27
|
+
if (filter) ruleMeta = ruleMeta.filter(([ruleName, rule]) => filter(ruleName, rule));
|
|
28
|
+
return {
|
|
29
|
+
name: `${pluginName}/${name}`,
|
|
30
|
+
plugins: { [pluginName]: {
|
|
31
|
+
name: pluginName,
|
|
32
|
+
rules
|
|
33
|
+
} },
|
|
34
|
+
rules: Object.fromEntries(ruleMeta.map((ruleName) => [`${pluginName}/${ruleName}`, "error"]))
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
var src_default = {
|
|
38
|
+
rules,
|
|
39
|
+
configs: {
|
|
40
|
+
recommended: generateConfig("recommended", ([_, rule]) => rule.meta?.recommended),
|
|
41
|
+
all: generateConfig("all")
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
export { src_default as default };
|
package/dist/index2.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "@typescript-eslint/utils/eslint-utils";
|
|
2
|
+
import { Rule } from "eslint";
|
|
3
|
+
|
|
4
|
+
//#region src/utils/index.d.ts
|
|
5
|
+
interface ESLintRuleModule<T extends readonly unknown[], TMessageIds extends string> extends Rule.RuleModule {
|
|
6
|
+
defaultOptions: T;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { ESLintRuleModule };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ESLintRuleModule } from "../index2.js";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/consistent-type-specifier-style/consistent-type-specifier-style.d.ts
|
|
4
|
+
type Options = ['prefer-inline' | 'prefer-top-level'];
|
|
5
|
+
type MessageId = 'inline' | 'topLevel';
|
|
6
|
+
declare const _default: ESLintRuleModule<Options, MessageId>;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { Options, _default as default };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createRule } from "../utils.js";
|
|
2
|
+
import { getValue } from "../ast.js";
|
|
3
|
+
import { isCommaToken } from "@typescript-eslint/utils/ast-utils";
|
|
4
|
+
|
|
5
|
+
//#region src/rules/consistent-type-specifier-style/consistent-type-specifier-style.ts
|
|
6
|
+
function removeSpecifiers(fixes, fixer, sourceCode, specifiers) {
|
|
7
|
+
for (const specifier of specifiers) {
|
|
8
|
+
const token = sourceCode.getTokenAfter(specifier);
|
|
9
|
+
if (token && isCommaToken(token)) fixes.push(fixer.remove(token));
|
|
10
|
+
fixes.push(fixer.remove(specifier));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function getImportText(node, sourceCode, specifiers, kind) {
|
|
14
|
+
const sourceString = sourceCode.getText(node.source);
|
|
15
|
+
if (specifiers.length === 0) return "";
|
|
16
|
+
const names = specifiers.map((s) => {
|
|
17
|
+
const importedName = getValue(s.imported);
|
|
18
|
+
if (importedName === s.local.name) return importedName;
|
|
19
|
+
return `${importedName} as ${s.local.name}`;
|
|
20
|
+
});
|
|
21
|
+
return `import ${kind} {${names.join(", ")}} from ${sourceString};`;
|
|
22
|
+
}
|
|
23
|
+
var consistent_type_specifier_style_default = createRule({
|
|
24
|
+
name: "consistent-type-specifier-style",
|
|
25
|
+
meta: {
|
|
26
|
+
type: "suggestion",
|
|
27
|
+
docs: {
|
|
28
|
+
fixable: true,
|
|
29
|
+
description: "Enforce or ban the use of inline type-only markers for named imports."
|
|
30
|
+
},
|
|
31
|
+
fixable: "code",
|
|
32
|
+
schema: [{
|
|
33
|
+
type: "string",
|
|
34
|
+
enum: ["prefer-top-level", "prefer-inline"],
|
|
35
|
+
default: "prefer-top-level"
|
|
36
|
+
}],
|
|
37
|
+
messages: {
|
|
38
|
+
inline: "Prefer using inline {{kind}} specifiers instead of a top-level {{kind}}-only import.",
|
|
39
|
+
topLevel: "Prefer using a top-level {{kind}}-only import instead of inline {{kind}} specifiers."
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
defaultOptions: ["prefer-top-level"],
|
|
43
|
+
create(context) {
|
|
44
|
+
const { sourceCode } = context;
|
|
45
|
+
if (context.options[0] === "prefer-inline") return { ImportDeclaration(node) {
|
|
46
|
+
if (node.importKind === "value" || node.importKind == null) return;
|
|
47
|
+
if (node.specifiers.length === 0 || node.specifiers.length === 1 && (node.specifiers[0].type === "ImportDefaultSpecifier" || node.specifiers[0].type === "ImportNamespaceSpecifier")) return;
|
|
48
|
+
context.report({
|
|
49
|
+
node,
|
|
50
|
+
messageId: "inline",
|
|
51
|
+
data: { kind: node.importKind },
|
|
52
|
+
fix(fixer) {
|
|
53
|
+
const kindToken = sourceCode.getFirstToken(node, { skip: 1 });
|
|
54
|
+
return [kindToken ? fixer.remove(kindToken) : [], node.specifiers.map((specifier) => fixer.insertTextBefore(specifier, `${node.importKind} `))].flat();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
} };
|
|
58
|
+
return { ImportDeclaration(node) {
|
|
59
|
+
if (node.importKind === "type" || node.importKind === "typeof" || node.specifiers.length === 0 || node.specifiers.length === 1 && (node.specifiers[0].type === "ImportDefaultSpecifier" || node.specifiers[0].type === "ImportNamespaceSpecifier")) return;
|
|
60
|
+
const typeSpecifiers = [];
|
|
61
|
+
const typeofSpecifiers = [];
|
|
62
|
+
const valueSpecifiers = [];
|
|
63
|
+
let defaultSpecifier = null;
|
|
64
|
+
for (const specifier of node.specifiers) {
|
|
65
|
+
if (specifier.type === "ImportDefaultSpecifier") {
|
|
66
|
+
defaultSpecifier = specifier;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!("importKind" in specifier)) continue;
|
|
70
|
+
if (specifier.importKind === "type") typeSpecifiers.push(specifier);
|
|
71
|
+
else if (specifier.importKind === "typeof") typeofSpecifiers.push(specifier);
|
|
72
|
+
else if (specifier.importKind === "value" || specifier.importKind == null) valueSpecifiers.push(specifier);
|
|
73
|
+
}
|
|
74
|
+
const typeImport = getImportText(node, sourceCode, typeSpecifiers, "type");
|
|
75
|
+
const typeofImport = getImportText(node, sourceCode, typeofSpecifiers, "typeof");
|
|
76
|
+
const newImports = `${typeImport}\n${typeofImport}`.trim();
|
|
77
|
+
if (typeSpecifiers.length + typeofSpecifiers.length === node.specifiers.length) {
|
|
78
|
+
const kind = [typeSpecifiers.length > 0 ? "type" : [], typeofSpecifiers.length > 0 ? "typeof" : []].flat();
|
|
79
|
+
context.report({
|
|
80
|
+
node,
|
|
81
|
+
messageId: "topLevel",
|
|
82
|
+
data: { kind: kind.join("/") },
|
|
83
|
+
fix(fixer) {
|
|
84
|
+
return fixer.replaceText(node, newImports);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
} else for (const specifier of [...typeSpecifiers, ...typeofSpecifiers]) context.report({
|
|
88
|
+
node: specifier,
|
|
89
|
+
messageId: "topLevel",
|
|
90
|
+
data: { kind: specifier.importKind },
|
|
91
|
+
fix(fixer) {
|
|
92
|
+
const fixes = [];
|
|
93
|
+
if (valueSpecifiers.length > 0) {
|
|
94
|
+
removeSpecifiers(fixes, fixer, sourceCode, typeSpecifiers);
|
|
95
|
+
removeSpecifiers(fixes, fixer, sourceCode, typeofSpecifiers);
|
|
96
|
+
const maybeComma = sourceCode.getTokenAfter(valueSpecifiers[valueSpecifiers.length - 1]);
|
|
97
|
+
if (isCommaToken(maybeComma)) fixes.push(fixer.remove(maybeComma));
|
|
98
|
+
} else if (defaultSpecifier) {
|
|
99
|
+
const comma = sourceCode.getTokenAfter(defaultSpecifier, isCommaToken);
|
|
100
|
+
const closingBrace = sourceCode.getTokenAfter(node.specifiers[node.specifiers.length - 1], (token) => token.type === "Punctuator" && token.value === "}");
|
|
101
|
+
fixes.push(fixer.removeRange([comma.range[0], closingBrace.range[1]]));
|
|
102
|
+
}
|
|
103
|
+
return [...fixes, fixer.insertTextAfter(node, `\n${newImports}`)];
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
} };
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
//#endregion
|
|
111
|
+
export { consistent_type_specifier_style_default };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ESLintRuleModule } from "../index2.js";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/first/first.d.ts
|
|
4
|
+
type Options = 'absolute-first' | 'disable-absolute-first';
|
|
5
|
+
type MessageId = 'absolute' | 'order';
|
|
6
|
+
declare const _default: ESLintRuleModule<[(Options | undefined)?], MessageId>;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { MessageId, Options, _default as default };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createRule } from "../utils.js";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/first/first.ts
|
|
4
|
+
function getImportValue(node) {
|
|
5
|
+
return node.type === "ImportDeclaration" ? node.source.value : "moduleReference" in node && "expression" in node.moduleReference && "value" in node.moduleReference.expression && node.moduleReference.expression.value;
|
|
6
|
+
}
|
|
7
|
+
function isPossibleDirective(node) {
|
|
8
|
+
return node.type === "ExpressionStatement" && node.expression.type === "Literal" && typeof node.expression.value === "string";
|
|
9
|
+
}
|
|
10
|
+
var first_default = createRule({
|
|
11
|
+
name: "first",
|
|
12
|
+
meta: {
|
|
13
|
+
type: "suggestion",
|
|
14
|
+
docs: {
|
|
15
|
+
fixable: true,
|
|
16
|
+
description: "Ensure all imports appear before other statements."
|
|
17
|
+
},
|
|
18
|
+
fixable: "code",
|
|
19
|
+
schema: [{
|
|
20
|
+
type: "string",
|
|
21
|
+
enum: ["absolute-first", "disable-absolute-first"]
|
|
22
|
+
}],
|
|
23
|
+
messages: {
|
|
24
|
+
absolute: "Absolute imports should come before relative imports.",
|
|
25
|
+
order: "Import in body of module; reorder to top."
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
defaultOptions: [],
|
|
29
|
+
create(context, options) {
|
|
30
|
+
return { Program(n) {
|
|
31
|
+
const body = n.body;
|
|
32
|
+
if (!body?.length) return;
|
|
33
|
+
const absoluteFirst = options[0] === "absolute-first";
|
|
34
|
+
const { sourceCode } = context;
|
|
35
|
+
const originSourceCode = sourceCode.getText();
|
|
36
|
+
let nonImportCount = 0;
|
|
37
|
+
let anyExpressions = false;
|
|
38
|
+
let anyRelative = false;
|
|
39
|
+
let lastLegalImp = null;
|
|
40
|
+
const errorInfos = [];
|
|
41
|
+
let shouldSort = true;
|
|
42
|
+
let lastSortNodesIndex = 0;
|
|
43
|
+
for (const [index, node] of body.entries()) {
|
|
44
|
+
if (!anyExpressions && isPossibleDirective(node)) continue;
|
|
45
|
+
anyExpressions = true;
|
|
46
|
+
if (node.type === "ImportDeclaration" || node.type === "TSImportEqualsDeclaration") {
|
|
47
|
+
if (absoluteFirst) {
|
|
48
|
+
const importValue = getImportValue(node);
|
|
49
|
+
if (typeof importValue === "string" && /^\./.test(importValue)) anyRelative = true;
|
|
50
|
+
else if (anyRelative) context.report({
|
|
51
|
+
node: node.type === "ImportDeclaration" ? node.source : node.moduleReference,
|
|
52
|
+
messageId: "absolute"
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (nonImportCount > 0) {
|
|
56
|
+
/** @see https://eslint.org/docs/next/use/migrate-to-9.0.0#-removed-multiple-context-methods */
|
|
57
|
+
for (const variable of sourceCode.getDeclaredVariables(node)) {
|
|
58
|
+
if (!shouldSort) break;
|
|
59
|
+
for (const reference of variable.references) if (reference.identifier.range[0] < node.range[1]) {
|
|
60
|
+
shouldSort = false;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (shouldSort) lastSortNodesIndex = errorInfos.length;
|
|
65
|
+
errorInfos.push({
|
|
66
|
+
node,
|
|
67
|
+
range: [body[index - 1].range[1], node.range[1]]
|
|
68
|
+
});
|
|
69
|
+
} else lastLegalImp = node;
|
|
70
|
+
} else nonImportCount++;
|
|
71
|
+
}
|
|
72
|
+
if (errorInfos.length === 0) return;
|
|
73
|
+
for (const [index, { node }] of errorInfos.entries()) {
|
|
74
|
+
let fix;
|
|
75
|
+
if (index < lastSortNodesIndex) fix = (fixer) => fixer.insertTextAfter(node, "");
|
|
76
|
+
else if (index === lastSortNodesIndex) {
|
|
77
|
+
const sortNodes = errorInfos.slice(0, lastSortNodesIndex + 1);
|
|
78
|
+
fix = (fixer) => {
|
|
79
|
+
const removeFixers = sortNodes.map(({ range: range$1 }) => fixer.removeRange(range$1));
|
|
80
|
+
const range = [0, removeFixers[removeFixers.length - 1].range[1]];
|
|
81
|
+
let insertSourceCode = sortNodes.map(({ range: range$1 }) => {
|
|
82
|
+
const nodeSourceCode = originSourceCode.slice(...range$1);
|
|
83
|
+
if (/\S/.test(nodeSourceCode[0])) return `\n${nodeSourceCode}`;
|
|
84
|
+
return nodeSourceCode;
|
|
85
|
+
}).join("");
|
|
86
|
+
let replaceSourceCode = "";
|
|
87
|
+
if (!lastLegalImp) insertSourceCode = insertSourceCode.trim() + insertSourceCode.match(/^(\s+)/)[0];
|
|
88
|
+
const insertFixer = lastLegalImp ? fixer.insertTextAfter(lastLegalImp, insertSourceCode) : fixer.insertTextBefore(body[0], insertSourceCode);
|
|
89
|
+
const fixers = [insertFixer, ...removeFixers];
|
|
90
|
+
for (const [i, computedFixer] of fixers.entries()) replaceSourceCode += originSourceCode.slice(fixers[i - 1] ? fixers[i - 1].range[1] : 0, computedFixer.range[0]) + computedFixer.text;
|
|
91
|
+
return fixer.replaceTextRange(range, replaceSourceCode);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
context.report({
|
|
95
|
+
node,
|
|
96
|
+
messageId: "order",
|
|
97
|
+
fix
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} };
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
export { first_default };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ESLintRuleModule } from "../index2.js";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/newline-after-import/newline-after-import.d.ts
|
|
4
|
+
type RuleOptions = [{
|
|
5
|
+
count?: number;
|
|
6
|
+
exactCount?: boolean;
|
|
7
|
+
considerComments?: boolean;
|
|
8
|
+
}];
|
|
9
|
+
type MessageId = 'newline';
|
|
10
|
+
declare const _default: ESLintRuleModule<RuleOptions, "newline">;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { MessageId, RuleOptions, _default as default };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { createRule } from "../utils.js";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/newline-after-import/newline-after-import.ts
|
|
4
|
+
function isStaticRequire(node) {
|
|
5
|
+
return node && node.callee && node.callee.type === "Identifier" && node.callee.name === "require" && node.arguments.length === 1 && node.arguments[0].type === "Literal" && typeof node.arguments[0].value === "string";
|
|
6
|
+
}
|
|
7
|
+
function containsNodeOrEqual(outerNode, innerNode) {
|
|
8
|
+
return outerNode.range[0] <= innerNode.range[0] && outerNode.range[1] >= innerNode.range[1];
|
|
9
|
+
}
|
|
10
|
+
function getScopeBody(scope) {
|
|
11
|
+
if (scope.block.type === "SwitchStatement") {
|
|
12
|
+
console.log("SwitchStatement scopes not supported");
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const body = "body" in scope.block ? scope.block.body : null;
|
|
16
|
+
if (body && "type" in body && body.type === "BlockStatement") return body.body;
|
|
17
|
+
return Array.isArray(body) ? body : [];
|
|
18
|
+
}
|
|
19
|
+
function findNodeIndexInScopeBody(body, nodeToFind) {
|
|
20
|
+
return body.findIndex((node) => containsNodeOrEqual(node, nodeToFind));
|
|
21
|
+
}
|
|
22
|
+
function getLineDifference(node, nextNode) {
|
|
23
|
+
return nextNode.loc.start.line - node.loc.end.line;
|
|
24
|
+
}
|
|
25
|
+
function isClassWithDecorator(node) {
|
|
26
|
+
return node.type === "ClassDeclaration" && !!node.decorators?.length;
|
|
27
|
+
}
|
|
28
|
+
function isExportDefaultClass(node) {
|
|
29
|
+
return node.type === "ExportDefaultDeclaration" && node.declaration.type === "ClassDeclaration";
|
|
30
|
+
}
|
|
31
|
+
function isExportNameClass(node) {
|
|
32
|
+
return node.type === "ExportNamedDeclaration" && node.declaration?.type === "ClassDeclaration";
|
|
33
|
+
}
|
|
34
|
+
var newline_after_import_default = createRule({
|
|
35
|
+
name: "newline-after-import",
|
|
36
|
+
meta: {
|
|
37
|
+
type: "layout",
|
|
38
|
+
docs: {
|
|
39
|
+
fixable: true,
|
|
40
|
+
description: "Enforce a newline after import statements."
|
|
41
|
+
},
|
|
42
|
+
fixable: "whitespace",
|
|
43
|
+
schema: [{
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
count: {
|
|
47
|
+
type: "integer",
|
|
48
|
+
minimum: 1
|
|
49
|
+
},
|
|
50
|
+
exactCount: { type: "boolean" },
|
|
51
|
+
considerComments: { type: "boolean" }
|
|
52
|
+
},
|
|
53
|
+
additionalProperties: false
|
|
54
|
+
}],
|
|
55
|
+
messages: { newline: "Expected {{count}} empty line{{lineSuffix}} after {{type}} statement not followed by another {{type}}." }
|
|
56
|
+
},
|
|
57
|
+
defaultOptions: [{
|
|
58
|
+
count: 1,
|
|
59
|
+
exactCount: false,
|
|
60
|
+
considerComments: false
|
|
61
|
+
}],
|
|
62
|
+
create(context, [options]) {
|
|
63
|
+
let level = 0;
|
|
64
|
+
const requireCalls = [];
|
|
65
|
+
function checkForNewLine(node, nextNode, type) {
|
|
66
|
+
if (isExportDefaultClass(nextNode) || isExportNameClass(nextNode)) {
|
|
67
|
+
const classNode = nextNode.declaration;
|
|
68
|
+
if (isClassWithDecorator(classNode)) nextNode = classNode.decorators[0];
|
|
69
|
+
} else if (isClassWithDecorator(nextNode)) nextNode = nextNode.decorators[0];
|
|
70
|
+
const lineDifference = getLineDifference(node, nextNode);
|
|
71
|
+
const EXPECTED_LINE_DIFFERENCE = options.count + 1;
|
|
72
|
+
if (lineDifference < EXPECTED_LINE_DIFFERENCE || options.exactCount && lineDifference !== EXPECTED_LINE_DIFFERENCE) {
|
|
73
|
+
let column = node.loc.start.column;
|
|
74
|
+
if (node.loc.start.line !== node.loc.end.line) column = 0;
|
|
75
|
+
context.report({
|
|
76
|
+
loc: {
|
|
77
|
+
line: node.loc.end.line,
|
|
78
|
+
column
|
|
79
|
+
},
|
|
80
|
+
messageId: "newline",
|
|
81
|
+
data: {
|
|
82
|
+
count: options.count,
|
|
83
|
+
lineSuffix: options.count > 1 ? "s" : "",
|
|
84
|
+
type
|
|
85
|
+
},
|
|
86
|
+
fix: options.exactCount && EXPECTED_LINE_DIFFERENCE < lineDifference ? void 0 : (fixer) => fixer.insertTextAfter(node, "\n".repeat(EXPECTED_LINE_DIFFERENCE - lineDifference))
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function commentAfterImport(node, nextComment, type) {
|
|
91
|
+
const lineDifference = getLineDifference(node, nextComment);
|
|
92
|
+
const EXPECTED_LINE_DIFFERENCE = options.count + 1;
|
|
93
|
+
if (lineDifference < EXPECTED_LINE_DIFFERENCE) {
|
|
94
|
+
let column = node.loc.start.column;
|
|
95
|
+
if (node.loc.start.line !== node.loc.end.line) column = 0;
|
|
96
|
+
context.report({
|
|
97
|
+
loc: {
|
|
98
|
+
line: node.loc.end.line,
|
|
99
|
+
column
|
|
100
|
+
},
|
|
101
|
+
messageId: "newline",
|
|
102
|
+
data: {
|
|
103
|
+
count: options.count,
|
|
104
|
+
lineSuffix: options.count > 1 ? "s" : "",
|
|
105
|
+
type
|
|
106
|
+
},
|
|
107
|
+
fix: options.exactCount && EXPECTED_LINE_DIFFERENCE < lineDifference ? void 0 : (fixer) => fixer.insertTextAfter(node, "\n".repeat(EXPECTED_LINE_DIFFERENCE - lineDifference))
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function incrementLevel() {
|
|
112
|
+
level++;
|
|
113
|
+
}
|
|
114
|
+
function decrementLevel() {
|
|
115
|
+
level--;
|
|
116
|
+
}
|
|
117
|
+
function checkImport(node) {
|
|
118
|
+
const { parent } = node;
|
|
119
|
+
if (!parent || !("body" in parent) || !parent.body) return;
|
|
120
|
+
const root = parent;
|
|
121
|
+
const nodePosition = root.body.indexOf(node);
|
|
122
|
+
const nextNode = root.body[nodePosition + 1];
|
|
123
|
+
const endLine = node.loc.end.line;
|
|
124
|
+
let nextComment;
|
|
125
|
+
if (root.comments !== void 0 && options.considerComments) nextComment = root.comments.find((o) => o.loc.start.line >= endLine && o.loc.start.line <= endLine + options.count + 1);
|
|
126
|
+
if (node.type === "TSImportEqualsDeclaration" && node.isExport) return;
|
|
127
|
+
if (nextComment) commentAfterImport(node, nextComment, "import");
|
|
128
|
+
else if (nextNode && nextNode.type !== "ImportDeclaration" && (nextNode.type !== "TSImportEqualsDeclaration" || nextNode.isExport)) checkForNewLine(node, nextNode, "import");
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
"ImportDeclaration": checkImport,
|
|
132
|
+
"TSImportEqualsDeclaration": checkImport,
|
|
133
|
+
CallExpression(node) {
|
|
134
|
+
if (isStaticRequire(node) && level === 0) requireCalls.push(node);
|
|
135
|
+
},
|
|
136
|
+
"Program:exit": function(node) {
|
|
137
|
+
const scopeBody = getScopeBody(context.sourceCode.getScope(node));
|
|
138
|
+
for (const [index, node$1] of requireCalls.entries()) {
|
|
139
|
+
const nodePosition = findNodeIndexInScopeBody(scopeBody, node$1);
|
|
140
|
+
const statementWithRequireCall = scopeBody[nodePosition];
|
|
141
|
+
const nextStatement = scopeBody[nodePosition + 1];
|
|
142
|
+
const nextRequireCall = requireCalls[index + 1];
|
|
143
|
+
if (nextRequireCall && containsNodeOrEqual(statementWithRequireCall, nextRequireCall)) continue;
|
|
144
|
+
if (nextStatement && (!nextRequireCall || !containsNodeOrEqual(nextStatement, nextRequireCall))) {
|
|
145
|
+
let nextComment;
|
|
146
|
+
if ("comments" in statementWithRequireCall.parent && statementWithRequireCall.parent.comments !== void 0 && options.considerComments) {
|
|
147
|
+
const endLine = node$1.loc.end.line;
|
|
148
|
+
nextComment = statementWithRequireCall.parent.comments.find((o) => o.loc.start.line >= endLine && o.loc.start.line <= endLine + options.count + 1);
|
|
149
|
+
}
|
|
150
|
+
if (nextComment && nextComment !== void 0) commentAfterImport(statementWithRequireCall, nextComment, "require");
|
|
151
|
+
else checkForNewLine(statementWithRequireCall, nextStatement, "require");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"FunctionDeclaration": incrementLevel,
|
|
156
|
+
"FunctionExpression": incrementLevel,
|
|
157
|
+
"ArrowFunctionExpression": incrementLevel,
|
|
158
|
+
"BlockStatement": incrementLevel,
|
|
159
|
+
"ObjectExpression": incrementLevel,
|
|
160
|
+
"Decorator": incrementLevel,
|
|
161
|
+
"FunctionDeclaration:exit": decrementLevel,
|
|
162
|
+
"FunctionExpression:exit": decrementLevel,
|
|
163
|
+
"ArrowFunctionExpression:exit": decrementLevel,
|
|
164
|
+
"BlockStatement:exit": decrementLevel,
|
|
165
|
+
"ObjectExpression:exit": decrementLevel,
|
|
166
|
+
"Decorator:exit": decrementLevel
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
//#endregion
|
|
172
|
+
export { newline_after_import_default };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ESLintRuleModule } from "../index2.js";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/no-default-export/no-default-export.d.ts
|
|
4
|
+
type RuleOptions = [];
|
|
5
|
+
type MessageIds = 'preferNamed' | 'noAliasDefault';
|
|
6
|
+
declare const _default: ESLintRuleModule<never[], "preferNamed" | "noAliasDefault">;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { MessageIds, RuleOptions, _default as default };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createRule, sourceType } from "../utils.js";
|
|
2
|
+
import { getValue } from "../ast.js";
|
|
3
|
+
|
|
4
|
+
//#region src/rules/no-default-export/no-default-export.ts
|
|
5
|
+
var no_default_export_default = createRule({
|
|
6
|
+
name: "no-default-export",
|
|
7
|
+
meta: {
|
|
8
|
+
type: "suggestion",
|
|
9
|
+
docs: { description: "Forbid default exports." },
|
|
10
|
+
schema: [],
|
|
11
|
+
messages: {
|
|
12
|
+
preferNamed: "Prefer named exports.",
|
|
13
|
+
noAliasDefault: "Do not alias `{{local}}` as `default`. Just export `{{local}}` itself instead."
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
defaultOptions: [],
|
|
17
|
+
create(context) {
|
|
18
|
+
if (sourceType(context) !== "module") return {};
|
|
19
|
+
const { sourceCode } = context;
|
|
20
|
+
return {
|
|
21
|
+
ExportDefaultDeclaration(node) {
|
|
22
|
+
const { loc } = sourceCode.getFirstTokens(node)[1] || {};
|
|
23
|
+
context.report({
|
|
24
|
+
node,
|
|
25
|
+
messageId: "preferNamed",
|
|
26
|
+
loc
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
ExportNamedDeclaration(node) {
|
|
30
|
+
for (const specifier of node.specifiers.filter((specifier$1) => getValue(specifier$1.exported) === "default")) {
|
|
31
|
+
const { loc } = sourceCode.getFirstTokens(node)[1] || {};
|
|
32
|
+
if (specifier.type === "ExportDefaultSpecifier") context.report({
|
|
33
|
+
node,
|
|
34
|
+
messageId: "preferNamed",
|
|
35
|
+
loc
|
|
36
|
+
});
|
|
37
|
+
else if (specifier.type === "ExportSpecifier") context.report({
|
|
38
|
+
node,
|
|
39
|
+
messageId: "noAliasDefault",
|
|
40
|
+
data: { local: getValue(specifier.local) },
|
|
41
|
+
loc
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
export { no_default_export_default };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ESLintRuleModule } from "../index2.js";
|
|
2
|
+
import { TSESTree } from "@typescript-eslint/utils";
|
|
3
|
+
|
|
4
|
+
//#region src/rules/no-duplicates/no-duplicates.d.ts
|
|
5
|
+
type Options = [{
|
|
6
|
+
'prefer-inline'?: boolean;
|
|
7
|
+
}?];
|
|
8
|
+
type MessageId = 'duplicate';
|
|
9
|
+
interface ModuleMap {
|
|
10
|
+
imported: Map<string, TSESTree.ImportDeclaration[]>;
|
|
11
|
+
nsImported: Map<string, TSESTree.ImportDeclaration[]>;
|
|
12
|
+
defaultTypesImported: Map<string, TSESTree.ImportDeclaration[]>;
|
|
13
|
+
namespaceTypesImported: Map<string, TSESTree.ImportDeclaration[]>;
|
|
14
|
+
namedTypesImported: Map<string, TSESTree.ImportDeclaration[]>;
|
|
15
|
+
}
|
|
16
|
+
declare const _default: ESLintRuleModule<Options, "duplicate">;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { MessageId, ModuleMap, Options, _default as default };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { createRule } from "../utils.js";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/no-duplicates/no-duplicates.ts
|
|
4
|
+
function checkImports(imported, context) {
|
|
5
|
+
imported.forEach((nodes, module) => {
|
|
6
|
+
if (nodes.length <= 1) return;
|
|
7
|
+
for (let i = 0, len = nodes.length; i < len; i++) {
|
|
8
|
+
const node = nodes[i];
|
|
9
|
+
context.report({
|
|
10
|
+
node: node.source,
|
|
11
|
+
messageId: "duplicate",
|
|
12
|
+
data: { module },
|
|
13
|
+
fix: i === 0 ? getFix(nodes, context.sourceCode, context) : null
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function getFix(nodes, sourceCode, context) {
|
|
19
|
+
const first = nodes[0];
|
|
20
|
+
if (hasProblematicComments(first, sourceCode) || hasNamespace(first)) return null;
|
|
21
|
+
const defaultImportNames = new Set(nodes.flatMap((x) => getDefaultImportName(x) || []));
|
|
22
|
+
if (defaultImportNames.size > 1) return null;
|
|
23
|
+
const rest = nodes.slice(1);
|
|
24
|
+
const restWithoutCommentsAndNamespaces = rest.filter((node) => !hasProblematicComments(node, sourceCode) && !hasNamespace(node));
|
|
25
|
+
const restWithoutCommentsAndNamespacesHasSpecifiers = restWithoutCommentsAndNamespaces.map(hasSpecifiers);
|
|
26
|
+
const specifiers = restWithoutCommentsAndNamespaces.reduce((acc, node, nodeIndex) => {
|
|
27
|
+
const tokens = sourceCode.getTokens(node);
|
|
28
|
+
const openBrace = tokens.find((token) => isPunctuator(token, "{"));
|
|
29
|
+
const closeBrace = tokens.find((token) => isPunctuator(token, "}"));
|
|
30
|
+
if (openBrace == null || closeBrace == null) return acc;
|
|
31
|
+
acc.push({
|
|
32
|
+
importNode: node,
|
|
33
|
+
identifiers: sourceCode.text.slice(openBrace.range[1], closeBrace.range[0]).split(","),
|
|
34
|
+
isEmpty: !restWithoutCommentsAndNamespacesHasSpecifiers[nodeIndex]
|
|
35
|
+
});
|
|
36
|
+
return acc;
|
|
37
|
+
}, []);
|
|
38
|
+
const unnecessaryImports = restWithoutCommentsAndNamespaces.filter((node, nodeIndex) => !restWithoutCommentsAndNamespacesHasSpecifiers[nodeIndex] && !specifiers.some((specifier) => specifier.importNode === node));
|
|
39
|
+
const shouldAddSpecifiers = specifiers.length > 0;
|
|
40
|
+
const shouldRemoveUnnecessary = unnecessaryImports.length > 0;
|
|
41
|
+
const shouldAddDefault = getDefaultImportName(first) == null && defaultImportNames.size === 1;
|
|
42
|
+
if (!shouldAddSpecifiers && !shouldRemoveUnnecessary && !shouldAddDefault) return null;
|
|
43
|
+
const preferInline = context.options[0] && context.options[0]["prefer-inline"];
|
|
44
|
+
return (fixer) => {
|
|
45
|
+
const tokens = sourceCode.getTokens(first);
|
|
46
|
+
const openBrace = tokens.find((token) => isPunctuator(token, "{"));
|
|
47
|
+
const closeBrace = tokens.find((token) => isPunctuator(token, "}"));
|
|
48
|
+
const firstToken = sourceCode.getFirstToken(first);
|
|
49
|
+
const [defaultImportName] = defaultImportNames;
|
|
50
|
+
const firstHasTrailingComma = closeBrace != null && isPunctuator(sourceCode.getTokenBefore(closeBrace), ",");
|
|
51
|
+
const firstIsEmpty = !hasSpecifiers(first);
|
|
52
|
+
const firstExistingIdentifiers = firstIsEmpty ? /* @__PURE__ */ new Set() : new Set(sourceCode.text.slice(openBrace.range[1], closeBrace.range[0]).split(",").map((x) => x.split(" as ")[0].trim()));
|
|
53
|
+
const [specifiersText] = specifiers.reduce(([result, needsComma, existingIdentifiers], specifier) => {
|
|
54
|
+
const isTypeSpecifier = "importNode" in specifier && specifier.importNode.importKind === "type";
|
|
55
|
+
const [specifierText, updatedExistingIdentifiers] = specifier.identifiers.reduce(([text, set], cur) => {
|
|
56
|
+
const trimmed = cur.trim();
|
|
57
|
+
if (trimmed.length === 0 || existingIdentifiers.has(trimmed)) return [text, set];
|
|
58
|
+
const curWithType = preferInline && isTypeSpecifier ? cur.replace(/^(\s*)/, "$1type ") : cur;
|
|
59
|
+
return [text.length > 0 ? `${text},${curWithType}` : curWithType, set.add(trimmed)];
|
|
60
|
+
}, ["", existingIdentifiers]);
|
|
61
|
+
return [
|
|
62
|
+
needsComma && !specifier.isEmpty && specifierText.length > 0 ? `${result},${specifierText}` : `${result}${specifierText}`,
|
|
63
|
+
specifier.isEmpty ? needsComma : true,
|
|
64
|
+
updatedExistingIdentifiers
|
|
65
|
+
];
|
|
66
|
+
}, [
|
|
67
|
+
"",
|
|
68
|
+
!firstHasTrailingComma && !firstIsEmpty,
|
|
69
|
+
firstExistingIdentifiers
|
|
70
|
+
]);
|
|
71
|
+
const fixes = [];
|
|
72
|
+
if (shouldAddSpecifiers && preferInline && first.importKind === "type") {
|
|
73
|
+
const typeIdentifierToken = tokens.find((token) => token.type === "Identifier" && token.value === "type");
|
|
74
|
+
if (typeIdentifierToken) fixes.push(fixer.removeRange([typeIdentifierToken.range[0], typeIdentifierToken.range[1] + 1]));
|
|
75
|
+
for (const identifier of tokens.filter((token) => firstExistingIdentifiers.has(token.value))) fixes.push(fixer.replaceTextRange([identifier.range[0], identifier.range[1]], `type ${identifier.value}`));
|
|
76
|
+
}
|
|
77
|
+
if (openBrace == null && shouldAddSpecifiers && shouldAddDefault) fixes.push(fixer.insertTextAfter(firstToken, ` ${defaultImportName}, {${specifiersText}} from`));
|
|
78
|
+
else if (openBrace == null && !shouldAddSpecifiers && shouldAddDefault) fixes.push(fixer.insertTextAfter(firstToken, ` ${defaultImportName} from`));
|
|
79
|
+
else if (openBrace != null && closeBrace != null && shouldAddDefault) {
|
|
80
|
+
fixes.push(fixer.insertTextAfter(firstToken, ` ${defaultImportName},`));
|
|
81
|
+
if (shouldAddSpecifiers) fixes.push(fixer.insertTextBefore(closeBrace, specifiersText));
|
|
82
|
+
} else if (openBrace == null && shouldAddSpecifiers && !shouldAddDefault) if (first.specifiers.length === 0) fixes.push(fixer.insertTextAfter(firstToken, ` {${specifiersText}} from`));
|
|
83
|
+
else fixes.push(fixer.insertTextAfter(first.specifiers[0], `, {${specifiersText}}`));
|
|
84
|
+
else if (openBrace != null && closeBrace != null && !shouldAddDefault) {
|
|
85
|
+
const tokenBefore = sourceCode.getTokenBefore(closeBrace);
|
|
86
|
+
fixes.push(fixer.insertTextAfter(tokenBefore, specifiersText));
|
|
87
|
+
}
|
|
88
|
+
for (const specifier of specifiers) {
|
|
89
|
+
const importNode = specifier.importNode;
|
|
90
|
+
fixes.push(fixer.remove(importNode));
|
|
91
|
+
const charAfterImportRange = [importNode.range[1], importNode.range[1] + 1];
|
|
92
|
+
const charAfterImport = sourceCode.text.slice(charAfterImportRange[0], charAfterImportRange[1]);
|
|
93
|
+
if (charAfterImport === "\n") fixes.push(fixer.removeRange(charAfterImportRange));
|
|
94
|
+
}
|
|
95
|
+
for (const node of unnecessaryImports) {
|
|
96
|
+
fixes.push(fixer.remove(node));
|
|
97
|
+
const charAfterImportRange = [node.range[1], node.range[1] + 1];
|
|
98
|
+
const charAfterImport = sourceCode.text.slice(charAfterImportRange[0], charAfterImportRange[1]);
|
|
99
|
+
if (charAfterImport === "\n") fixes.push(fixer.removeRange(charAfterImportRange));
|
|
100
|
+
}
|
|
101
|
+
return fixes;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function isPunctuator(node, value) {
|
|
105
|
+
return node.type === "Punctuator" && node.value === value;
|
|
106
|
+
}
|
|
107
|
+
function getDefaultImportName(node) {
|
|
108
|
+
const defaultSpecifier = node.specifiers.find((specifier) => specifier.type === "ImportDefaultSpecifier");
|
|
109
|
+
return defaultSpecifier?.local.name;
|
|
110
|
+
}
|
|
111
|
+
function hasNamespace(node) {
|
|
112
|
+
return node.specifiers.some((specifier) => specifier.type === "ImportNamespaceSpecifier");
|
|
113
|
+
}
|
|
114
|
+
function hasSpecifiers(node) {
|
|
115
|
+
return node.specifiers.some((specifier) => specifier.type === "ImportSpecifier");
|
|
116
|
+
}
|
|
117
|
+
function hasProblematicComments(node, sourceCode) {
|
|
118
|
+
return hasCommentBefore(node, sourceCode) || hasCommentAfter(node, sourceCode) || hasCommentInsideNonSpecifiers(node, sourceCode);
|
|
119
|
+
}
|
|
120
|
+
function hasCommentBefore(node, sourceCode) {
|
|
121
|
+
return sourceCode.getCommentsBefore(node).some((comment) => comment.loc.end.line >= node.loc.start.line - 1);
|
|
122
|
+
}
|
|
123
|
+
function hasCommentAfter(node, sourceCode) {
|
|
124
|
+
return sourceCode.getCommentsAfter(node).some((comment) => comment.loc.start.line === node.loc.end.line);
|
|
125
|
+
}
|
|
126
|
+
function hasCommentInsideNonSpecifiers(node, sourceCode) {
|
|
127
|
+
const tokens = sourceCode.getTokens(node);
|
|
128
|
+
const openBraceIndex = tokens.findIndex((token) => isPunctuator(token, "{"));
|
|
129
|
+
const closeBraceIndex = tokens.findIndex((token) => isPunctuator(token, "}"));
|
|
130
|
+
const someTokens = openBraceIndex !== -1 && closeBraceIndex !== -1 ? [...tokens.slice(1, openBraceIndex + 1), ...tokens.slice(closeBraceIndex + 1)] : tokens.slice(1);
|
|
131
|
+
return someTokens.some((token) => sourceCode.getCommentsBefore(token).length > 0);
|
|
132
|
+
}
|
|
133
|
+
var no_duplicates_default = createRule({
|
|
134
|
+
name: "no-duplicates",
|
|
135
|
+
meta: {
|
|
136
|
+
type: "problem",
|
|
137
|
+
docs: {
|
|
138
|
+
recommended: true,
|
|
139
|
+
fixable: true,
|
|
140
|
+
description: "Forbid repeated import of the same module in multiple places."
|
|
141
|
+
},
|
|
142
|
+
fixable: "code",
|
|
143
|
+
schema: [{
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: { "prefer-inline": { type: "boolean" } },
|
|
146
|
+
additionalProperties: false
|
|
147
|
+
}],
|
|
148
|
+
messages: { duplicate: "'{{module}}' imported multiple times." }
|
|
149
|
+
},
|
|
150
|
+
defaultOptions: [],
|
|
151
|
+
create(context) {
|
|
152
|
+
const preferInline = context.options[0]?.["prefer-inline"];
|
|
153
|
+
const moduleMaps = /* @__PURE__ */ new Map();
|
|
154
|
+
function getImportMap(n) {
|
|
155
|
+
const parent = n.parent;
|
|
156
|
+
let map;
|
|
157
|
+
if (moduleMaps.has(parent)) map = moduleMaps.get(parent);
|
|
158
|
+
else {
|
|
159
|
+
map = {
|
|
160
|
+
imported: /* @__PURE__ */ new Map(),
|
|
161
|
+
nsImported: /* @__PURE__ */ new Map(),
|
|
162
|
+
defaultTypesImported: /* @__PURE__ */ new Map(),
|
|
163
|
+
namespaceTypesImported: /* @__PURE__ */ new Map(),
|
|
164
|
+
namedTypesImported: /* @__PURE__ */ new Map()
|
|
165
|
+
};
|
|
166
|
+
moduleMaps.set(parent, map);
|
|
167
|
+
}
|
|
168
|
+
if (n.importKind === "type") {
|
|
169
|
+
if (n.specifiers.length > 0 && n.specifiers[0].type === "ImportDefaultSpecifier") return map.defaultTypesImported;
|
|
170
|
+
if (n.specifiers.length > 0 && n.specifiers[0].type === "ImportNamespaceSpecifier") return map.namespaceTypesImported;
|
|
171
|
+
if (!preferInline) return map.namedTypesImported;
|
|
172
|
+
}
|
|
173
|
+
if (!preferInline && n.specifiers.some((spec) => "importKind" in spec && spec.importKind === "type")) return map.namedTypesImported;
|
|
174
|
+
return hasNamespace(n) ? map.nsImported : map.imported;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
ImportDeclaration(n) {
|
|
178
|
+
const resolvedPath = n.source.value;
|
|
179
|
+
const importMap = getImportMap(n);
|
|
180
|
+
if (importMap.has(resolvedPath)) importMap.get(resolvedPath).push(n);
|
|
181
|
+
else importMap.set(resolvedPath, [n]);
|
|
182
|
+
},
|
|
183
|
+
"Program:exit": function() {
|
|
184
|
+
for (const map of moduleMaps.values()) {
|
|
185
|
+
checkImports(map.imported, context);
|
|
186
|
+
checkImports(map.nsImported, context);
|
|
187
|
+
checkImports(map.defaultTypesImported, context);
|
|
188
|
+
checkImports(map.namedTypesImported, context);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
//#endregion
|
|
196
|
+
export { no_duplicates_default };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ESLintRuleModule } from "../index2.js";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/no-mutable-exports/no-mutable-exports.d.ts
|
|
4
|
+
type MessageId = 'noMutable';
|
|
5
|
+
type Options = [];
|
|
6
|
+
declare const _default: ESLintRuleModule<[], "noMutable">;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { MessageId, Options, _default as default };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createRule } from "../utils.js";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/no-mutable-exports/no-mutable-exports.ts
|
|
4
|
+
var no_mutable_exports_default = createRule({
|
|
5
|
+
name: "no-mutable-exports",
|
|
6
|
+
meta: {
|
|
7
|
+
type: "suggestion",
|
|
8
|
+
docs: { description: "Forbid the use of mutable exports with `var` or `let`." },
|
|
9
|
+
schema: [],
|
|
10
|
+
messages: { noMutable: "Exporting mutable '{{kind}}' binding, use 'const' instead." }
|
|
11
|
+
},
|
|
12
|
+
defaultOptions: [],
|
|
13
|
+
create(context) {
|
|
14
|
+
function checkDeclaration(node) {
|
|
15
|
+
if ("kind" in node && (node.kind === "var" || node.kind === "let")) context.report({
|
|
16
|
+
node,
|
|
17
|
+
messageId: "noMutable",
|
|
18
|
+
data: { kind: node.kind }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function checkDeclarationsInScope({ variables }, name) {
|
|
22
|
+
for (const variable of variables) if (variable.name === name) {
|
|
23
|
+
for (const def of variable.defs) if (def.type === "Variable" && def.parent) checkDeclaration(def.parent);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
ExportDefaultDeclaration(node) {
|
|
28
|
+
const scope = context.sourceCode.getScope(node);
|
|
29
|
+
if ("name" in node.declaration) checkDeclarationsInScope(scope, node.declaration.name);
|
|
30
|
+
},
|
|
31
|
+
ExportNamedDeclaration(node) {
|
|
32
|
+
const scope = context.sourceCode.getScope(node);
|
|
33
|
+
if (node.declaration) checkDeclaration(node.declaration);
|
|
34
|
+
else if (!node.source) for (const specifier of node.specifiers) checkDeclarationsInScope(scope, specifier.local.name);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { no_mutable_exports_default };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ESLintRuleModule } from "../index2.js";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/no-named-default/no-named-default.d.ts
|
|
4
|
+
type Options = [];
|
|
5
|
+
type MessageId = 'default';
|
|
6
|
+
declare const _default: ESLintRuleModule<[], "default">;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { MessageId, Options, _default as default };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createRule } from "../utils.js";
|
|
2
|
+
import { getValue } from "../ast.js";
|
|
3
|
+
|
|
4
|
+
//#region src/rules/no-named-default/no-named-default.ts
|
|
5
|
+
var no_named_default_default = createRule({
|
|
6
|
+
name: "no-named-default",
|
|
7
|
+
meta: {
|
|
8
|
+
type: "suggestion",
|
|
9
|
+
docs: { description: "Forbid named default exports." },
|
|
10
|
+
schema: [],
|
|
11
|
+
messages: { default: `Use default import syntax to import '{{importName}}'.` }
|
|
12
|
+
},
|
|
13
|
+
defaultOptions: [],
|
|
14
|
+
create(context) {
|
|
15
|
+
return { ImportDeclaration(node) {
|
|
16
|
+
for (const im of node.specifiers) {
|
|
17
|
+
if ("importKind" in im && (im.importKind === "type" || im.importKind === "typeof")) continue;
|
|
18
|
+
if (im.type === "ImportSpecifier" && getValue(im.imported) === "default") context.report({
|
|
19
|
+
node: im.local,
|
|
20
|
+
messageId: "default",
|
|
21
|
+
data: { importName: im.local.name }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
} };
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
export { no_named_default_default };
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { deepMerge, isObjectNotArray } from "@typescript-eslint/utils/eslint-utils";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/index.ts
|
|
4
|
+
function createRule({ name, create, defaultOptions = [], meta }) {
|
|
5
|
+
return {
|
|
6
|
+
create: (context) => {
|
|
7
|
+
const optionsCount = Math.max(context.options.length, defaultOptions.length);
|
|
8
|
+
const optionsWithDefault = Array.from({ length: optionsCount }, (_, i) => {
|
|
9
|
+
if (isObjectNotArray(context.options[i]) && isObjectNotArray(defaultOptions[i])) return deepMerge(defaultOptions[i], context.options[i]);
|
|
10
|
+
return context.options[i] ?? defaultOptions[i];
|
|
11
|
+
});
|
|
12
|
+
return create(context, optionsWithDefault);
|
|
13
|
+
},
|
|
14
|
+
defaultOptions,
|
|
15
|
+
meta: {
|
|
16
|
+
...meta,
|
|
17
|
+
docs: {
|
|
18
|
+
...meta.docs,
|
|
19
|
+
url: `https://github.com/9romise/eslint-plugin-import-lite/blob/main/src/rules/${name}/README.md`
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function sourceType(context) {
|
|
25
|
+
if ("sourceType" in context.parserOptions) return context.parserOptions.sourceType;
|
|
26
|
+
if ("languageOptions" in context && context.languageOptions) return context.languageOptions.sourceType;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { createRule, sourceType };
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-import-lite",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"packageManager": "pnpm@10.12.1",
|
|
6
|
+
"author": "Vida Xie<https://github.com/9romise>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/9romise/eslint-plugin-import-lite#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/9romise/eslint-plugin-import-lite.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/9romise/eslint-plugin-import-lite/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [],
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"require": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"module": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"dev": "tsdown --watch",
|
|
36
|
+
"build": "tsdown",
|
|
37
|
+
"test": "vitest",
|
|
38
|
+
"lint": "eslint .",
|
|
39
|
+
"update": "tsx scripts/update && eslint . --fix",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"prepare": "npm run build"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"eslint": ">=9.0.0",
|
|
45
|
+
"typescript": ">=4.5"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"typescript": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@typescript-eslint/utils": "catalog:prod"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "catalog:dev",
|
|
57
|
+
"@typescript-eslint/parser": "catalog:test",
|
|
58
|
+
"@vida0905/eslint-config": "catalog:dev",
|
|
59
|
+
"change-case": "catalog:dev",
|
|
60
|
+
"eslint": "catalog:dev",
|
|
61
|
+
"eslint-plugin-import-lite": "workspace:*",
|
|
62
|
+
"eslint-vitest-rule-tester": "catalog:test",
|
|
63
|
+
"nano-staged": "catalog:dev",
|
|
64
|
+
"simple-git-hooks": "catalog:dev",
|
|
65
|
+
"tinyglobby": "catalog:dev",
|
|
66
|
+
"tsdown": "catalog:dev",
|
|
67
|
+
"tsx": "catalog:dev",
|
|
68
|
+
"typescript": "catalog:dev",
|
|
69
|
+
"vitest": "catalog:test"
|
|
70
|
+
},
|
|
71
|
+
"simple-git-hooks": {
|
|
72
|
+
"pre-commit": "npx nano-staged"
|
|
73
|
+
},
|
|
74
|
+
"nano-staged": {
|
|
75
|
+
"*": "eslint --fix"
|
|
76
|
+
}
|
|
77
|
+
}
|