@borela-tech/eslint-config 1.3.2 → 2.0.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/bin/lint +2 -1
- package/bin/test +12 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +322 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +22 -16
- package/src/rules/__tests__/dedent/countLeadingSpaces.ts +4 -0
- package/src/rules/__tests__/dedent/findMinIndent.ts +7 -0
- package/src/rules/__tests__/dedent/index.ts +17 -0
- package/src/rules/__tests__/dedent/interpolate.ts +11 -0
- package/src/rules/__tests__/dedent/removeEmptyPrefix.ts +6 -0
- package/src/rules/__tests__/dedent/removeEmptySuffix.ts +6 -0
- package/src/rules/__tests__/dedent/removeIndent.ts +3 -0
- package/src/rules/__tests__/individualImports.test.ts +42 -0
- package/src/rules/__tests__/sortedImports.test.ts +114 -0
- package/src/rules/individualImports.ts +46 -0
- package/src/rules/sortedImports/CategorizedImport.ts +8 -0
- package/src/rules/sortedImports/ImportError.ts +6 -0
- package/src/rules/sortedImports/ImportGroup.ts +1 -0
- package/src/rules/sortedImports/areSpecifiersSorted.ts +10 -0
- package/src/rules/sortedImports/categorizeImport.ts +12 -0
- package/src/rules/sortedImports/categorizeImports.ts +12 -0
- package/src/rules/sortedImports/checkAlphabeticalSorting.ts +22 -0
- package/src/rules/sortedImports/checkGroupOrdering.ts +22 -0
- package/src/rules/sortedImports/checkSpecifiersSorting.ts +21 -0
- package/src/rules/sortedImports/createFix/ReplacementRange.ts +4 -0
- package/src/rules/sortedImports/createFix/buildSortedCode.ts +22 -0
- package/src/rules/sortedImports/createFix/findLastImportIndex.ts +12 -0
- package/src/rules/sortedImports/createFix/formatNamedImport.ts +23 -0
- package/src/rules/sortedImports/createFix/getReplacementRange.ts +26 -0
- package/src/rules/sortedImports/createFix/groupImportsByType.ts +17 -0
- package/src/rules/sortedImports/createFix/index.ts +29 -0
- package/src/rules/sortedImports/createFix/sortImportGroups.ts +9 -0
- package/src/rules/sortedImports/getImportDeclarations.ts +9 -0
- package/src/rules/sortedImports/getNamedSpecifiers.ts +8 -0
- package/src/rules/sortedImports/getSortKey.ts +20 -0
- package/src/rules/sortedImports/getSpecifierName.ts +7 -0
- package/src/rules/sortedImports/index.ts +52 -0
- package/src/rules/sortedImports/sortSpecifiersText.ts +14 -0
package/bin/lint
CHANGED
package/bin/test
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as eslint_config from 'eslint/config';
|
|
2
2
|
|
|
3
|
-
declare const CONFIG:
|
|
3
|
+
declare const CONFIG: eslint_config.Config[];
|
|
4
4
|
|
|
5
5
|
export { CONFIG };
|
package/dist/index.js
CHANGED
|
@@ -4,13 +4,316 @@ import react from "eslint-plugin-react";
|
|
|
4
4
|
import reactHooks from "eslint-plugin-react-hooks";
|
|
5
5
|
import stylistic from "@stylistic/eslint-plugin";
|
|
6
6
|
import typescript from "typescript-eslint";
|
|
7
|
-
|
|
7
|
+
import { defineConfig } from "eslint/config";
|
|
8
|
+
|
|
9
|
+
// src/rules/individualImports.ts
|
|
10
|
+
var individualImports = {
|
|
11
|
+
meta: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Enforce individual imports instead of grouped imports",
|
|
14
|
+
recommended: true
|
|
15
|
+
},
|
|
16
|
+
fixable: "code",
|
|
17
|
+
messages: {
|
|
18
|
+
individualImports: "Use individual imports instead of grouped imports."
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
type: "suggestion"
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
ImportDeclaration(node) {
|
|
26
|
+
if (node.specifiers.length <= 1)
|
|
27
|
+
return;
|
|
28
|
+
context.report({
|
|
29
|
+
node,
|
|
30
|
+
messageId: "individualImports",
|
|
31
|
+
fix(fixer) {
|
|
32
|
+
const source = node.source.raw;
|
|
33
|
+
const specifiers = node.specifiers.map((importSpecifier) => {
|
|
34
|
+
if (importSpecifier.type === "ImportSpecifier")
|
|
35
|
+
return `import {${importSpecifier.local.name}} from ${source}`;
|
|
36
|
+
return null;
|
|
37
|
+
}).filter(Boolean);
|
|
38
|
+
if (specifiers.length !== node.specifiers.length)
|
|
39
|
+
return null;
|
|
40
|
+
return fixer.replaceText(
|
|
41
|
+
node,
|
|
42
|
+
specifiers.join("\n")
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// src/rules/sortedImports/categorizeImport.ts
|
|
52
|
+
function categorizeImport(declaration) {
|
|
53
|
+
if (declaration.specifiers.length === 0)
|
|
54
|
+
return "side-effect";
|
|
55
|
+
if (declaration.specifiers.some((s) => s.type === "ImportDefaultSpecifier"))
|
|
56
|
+
return "default";
|
|
57
|
+
return "named";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/rules/sortedImports/getSortKey.ts
|
|
61
|
+
function getSortKey(declaration) {
|
|
62
|
+
const group = categorizeImport(declaration);
|
|
63
|
+
if (group === "side-effect")
|
|
64
|
+
return declaration.source.value.toLowerCase();
|
|
65
|
+
if (group === "default") {
|
|
66
|
+
const defaultSpecifier = declaration.specifiers.find(
|
|
67
|
+
(s) => s.type === "ImportDefaultSpecifier"
|
|
68
|
+
);
|
|
69
|
+
return defaultSpecifier?.local.name.toLowerCase() ?? "";
|
|
70
|
+
}
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/rules/sortedImports/categorizeImports.ts
|
|
75
|
+
function categorizeImports(declarations) {
|
|
76
|
+
return declarations.map((declaration) => ({
|
|
77
|
+
declaration,
|
|
78
|
+
group: categorizeImport(declaration),
|
|
79
|
+
sortKey: getSortKey(declaration)
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/rules/sortedImports/checkAlphabeticalSorting.ts
|
|
84
|
+
function checkAlphabeticalSorting(categorized) {
|
|
85
|
+
const errors = [];
|
|
86
|
+
for (const group of ["side-effect", "default"]) {
|
|
87
|
+
const groupImports = categorized.filter((c) => c.group === group);
|
|
88
|
+
const sorted = [...groupImports].sort((a, b) => a.sortKey.localeCompare(b.sortKey));
|
|
89
|
+
for (let i = 0; i < groupImports.length; i++) {
|
|
90
|
+
if (groupImports[i] !== sorted[i]) {
|
|
91
|
+
errors.push({
|
|
92
|
+
node: groupImports[i].declaration,
|
|
93
|
+
messageId: "sortedImports"
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return errors;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/rules/sortedImports/checkGroupOrdering.ts
|
|
102
|
+
function checkGroupOrdering(categorized) {
|
|
103
|
+
const groupOrder = ["side-effect", "default", "named"];
|
|
104
|
+
const errors = [];
|
|
105
|
+
let currentGroupIndex = -1;
|
|
106
|
+
for (const { declaration, group } of categorized) {
|
|
107
|
+
const groupIndex = groupOrder.indexOf(group);
|
|
108
|
+
if (groupIndex < currentGroupIndex) {
|
|
109
|
+
errors.push({
|
|
110
|
+
node: declaration,
|
|
111
|
+
messageId: "wrongGroup"
|
|
112
|
+
});
|
|
113
|
+
} else
|
|
114
|
+
currentGroupIndex = groupIndex;
|
|
115
|
+
}
|
|
116
|
+
return errors;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/rules/sortedImports/getSpecifierName.ts
|
|
120
|
+
function getSpecifierName(specifier) {
|
|
121
|
+
return specifier.imported.type === "Identifier" ? specifier.imported.name : String(specifier.imported.value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/rules/sortedImports/areSpecifiersSorted.ts
|
|
125
|
+
function areSpecifiersSorted(specifiers) {
|
|
126
|
+
const names = specifiers.map((s) => getSpecifierName(s));
|
|
127
|
+
const sorted = [...names].sort(
|
|
128
|
+
(a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
|
|
129
|
+
);
|
|
130
|
+
return names.every((name, i) => name === sorted[i]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/rules/sortedImports/getNamedSpecifiers.ts
|
|
134
|
+
function getNamedSpecifiers(declaration) {
|
|
135
|
+
return declaration.specifiers.filter(
|
|
136
|
+
(s) => s.type === "ImportSpecifier"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/rules/sortedImports/checkSpecifiersSorting.ts
|
|
141
|
+
function checkSpecifiersSorting(categorized) {
|
|
142
|
+
const errors = [];
|
|
143
|
+
const namedImports = categorized.filter((c) => c.group === "named");
|
|
144
|
+
for (const { declaration } of namedImports) {
|
|
145
|
+
const specifiers = getNamedSpecifiers(declaration);
|
|
146
|
+
if (specifiers.length > 1 && !areSpecifiersSorted(specifiers)) {
|
|
147
|
+
errors.push({
|
|
148
|
+
node: declaration,
|
|
149
|
+
messageId: "sortedNames"
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return errors;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/rules/sortedImports/sortSpecifiersText.ts
|
|
157
|
+
function sortSpecifiersText(specifiers, sourceCode) {
|
|
158
|
+
const sorted = [...specifiers].sort((a, b) => {
|
|
159
|
+
const lowerA = getSpecifierName(a).toLowerCase();
|
|
160
|
+
const lowerB = getSpecifierName(b).toLowerCase();
|
|
161
|
+
return lowerA.localeCompare(lowerB);
|
|
162
|
+
});
|
|
163
|
+
return sorted.map((s) => sourceCode.getText(s)).join(", ");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/rules/sortedImports/createFix/formatNamedImport.ts
|
|
167
|
+
function formatNamedImport(declaration, sourceCode) {
|
|
168
|
+
const specifiers = getNamedSpecifiers(declaration);
|
|
169
|
+
if (specifiers.length > 1 && !areSpecifiersSorted(specifiers)) {
|
|
170
|
+
const importText = sourceCode.getText(declaration);
|
|
171
|
+
const specifiersStart = importText.indexOf("{");
|
|
172
|
+
const specifiersEnd = importText.lastIndexOf("}");
|
|
173
|
+
const before = importText.substring(0, specifiersStart + 1);
|
|
174
|
+
const after = importText.substring(specifiersEnd);
|
|
175
|
+
const sortedSpecifiers = sortSpecifiersText(specifiers, sourceCode);
|
|
176
|
+
return before + " " + sortedSpecifiers + " " + after;
|
|
177
|
+
}
|
|
178
|
+
return sourceCode.getText(declaration);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/rules/sortedImports/createFix/buildSortedCode.ts
|
|
182
|
+
function buildSortedCode(grouped, sourceCode) {
|
|
183
|
+
const groupOrder = ["side-effect", "default", "named"];
|
|
184
|
+
const sortedCode = [];
|
|
185
|
+
for (const group of groupOrder) {
|
|
186
|
+
for (const { declaration } of grouped[group]) {
|
|
187
|
+
if (group === "named")
|
|
188
|
+
sortedCode.push(formatNamedImport(declaration, sourceCode));
|
|
189
|
+
else
|
|
190
|
+
sortedCode.push(sourceCode.getText(declaration));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return sortedCode;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/rules/sortedImports/createFix/findLastImportIndex.ts
|
|
197
|
+
function findLastImportIndex(programBody) {
|
|
198
|
+
let lastIndex = 0;
|
|
199
|
+
for (let i = 0; i < programBody.length; i++) {
|
|
200
|
+
if (programBody[i].type === "ImportDeclaration")
|
|
201
|
+
lastIndex = i;
|
|
202
|
+
else
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
return lastIndex;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/rules/sortedImports/createFix/getReplacementRange.ts
|
|
209
|
+
function getReplacementRange(programBody, sourceCode) {
|
|
210
|
+
const fullText = sourceCode.getText();
|
|
211
|
+
const lastIndex = findLastImportIndex(programBody);
|
|
212
|
+
const firstImport = programBody[0];
|
|
213
|
+
const lastImport = programBody[lastIndex];
|
|
214
|
+
const start = firstImport.range[0];
|
|
215
|
+
let end = lastImport.range[1];
|
|
216
|
+
for (let i = end; i < fullText.length; i++) {
|
|
217
|
+
const char = fullText[i];
|
|
218
|
+
if (char === "\n" || char === " " || char === " ")
|
|
219
|
+
end++;
|
|
220
|
+
else
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
return { start, end };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/rules/sortedImports/createFix/groupImportsByType.ts
|
|
227
|
+
function groupImportsByType(categorized) {
|
|
228
|
+
const grouped = {
|
|
229
|
+
"side-effect": [],
|
|
230
|
+
default: [],
|
|
231
|
+
named: []
|
|
232
|
+
};
|
|
233
|
+
for (const item of categorized)
|
|
234
|
+
grouped[item.group].push(item);
|
|
235
|
+
return grouped;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/rules/sortedImports/createFix/sortImportGroups.ts
|
|
239
|
+
function sortImportGroups(grouped) {
|
|
240
|
+
grouped["side-effect"].sort((a, b) => a.sortKey.localeCompare(b.sortKey));
|
|
241
|
+
grouped["default"].sort((a, b) => a.sortKey.localeCompare(b.sortKey));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/rules/sortedImports/createFix/index.ts
|
|
245
|
+
function createFix(fixer, importDeclarations, sourceCode, programBody) {
|
|
246
|
+
const range = getReplacementRange(programBody, sourceCode);
|
|
247
|
+
const categorized = categorizeImports(importDeclarations);
|
|
248
|
+
const grouped = groupImportsByType(categorized);
|
|
249
|
+
sortImportGroups(grouped);
|
|
250
|
+
const sortedCode = buildSortedCode(grouped, sourceCode).join("\n");
|
|
251
|
+
return fixer.replaceTextRange(
|
|
252
|
+
[range.start, range.end],
|
|
253
|
+
sortedCode
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/rules/sortedImports/getImportDeclarations.ts
|
|
258
|
+
function getImportDeclarations(programBody) {
|
|
259
|
+
return programBody.filter(
|
|
260
|
+
(statement) => statement.type === "ImportDeclaration"
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/rules/sortedImports/index.ts
|
|
265
|
+
var sortedImports = {
|
|
266
|
+
meta: {
|
|
267
|
+
docs: {
|
|
268
|
+
description: "Enforce sorted imports alphabetically",
|
|
269
|
+
recommended: true
|
|
270
|
+
},
|
|
271
|
+
fixable: "code",
|
|
272
|
+
messages: {
|
|
273
|
+
sortedImports: "Imports should be sorted alphabetically",
|
|
274
|
+
sortedNames: "Named imports should be sorted alphabetically",
|
|
275
|
+
wrongGroup: "Import is in wrong group"
|
|
276
|
+
},
|
|
277
|
+
schema: [],
|
|
278
|
+
type: "suggestion"
|
|
279
|
+
},
|
|
280
|
+
create(context) {
|
|
281
|
+
return {
|
|
282
|
+
Program(node) {
|
|
283
|
+
const declarations = getImportDeclarations(node.body);
|
|
284
|
+
if (declarations.length === 0)
|
|
285
|
+
return;
|
|
286
|
+
const categorized = categorizeImports(declarations);
|
|
287
|
+
const errors = [
|
|
288
|
+
...checkGroupOrdering(categorized),
|
|
289
|
+
...checkAlphabeticalSorting(categorized),
|
|
290
|
+
...checkSpecifiersSorting(categorized)
|
|
291
|
+
];
|
|
292
|
+
for (const error of errors) {
|
|
293
|
+
context.report({
|
|
294
|
+
node: error.node,
|
|
295
|
+
messageId: error.messageId,
|
|
296
|
+
fix(fixer) {
|
|
297
|
+
const sourceCode = context.sourceCode;
|
|
298
|
+
return createFix(fixer, declarations, sourceCode, node.body);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// src/index.ts
|
|
308
|
+
var CONFIG = defineConfig([
|
|
8
309
|
{
|
|
9
310
|
ignores: [
|
|
10
311
|
"src/graphql/sdk.ts",
|
|
11
312
|
"**/node_modules/**",
|
|
12
313
|
"**/dist/**"
|
|
13
|
-
]
|
|
314
|
+
]
|
|
315
|
+
},
|
|
316
|
+
{
|
|
14
317
|
settings: {
|
|
15
318
|
react: {
|
|
16
319
|
version: "19"
|
|
@@ -28,6 +331,20 @@ var CONFIG = typescript.config(
|
|
|
28
331
|
},
|
|
29
332
|
rules: reactHooks.configs.recommended.rules
|
|
30
333
|
},
|
|
334
|
+
{
|
|
335
|
+
plugins: {
|
|
336
|
+
"@borela-tech": {
|
|
337
|
+
rules: {
|
|
338
|
+
"individual-imports": individualImports,
|
|
339
|
+
"sorted-imports": sortedImports
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
rules: {
|
|
344
|
+
"@borela-tech/individual-imports": "error",
|
|
345
|
+
"@borela-tech/sorted-imports": "error"
|
|
346
|
+
}
|
|
347
|
+
},
|
|
31
348
|
{
|
|
32
349
|
rules: {
|
|
33
350
|
"capitalized-comments": [
|
|
@@ -52,6 +369,7 @@ var CONFIG = typescript.config(
|
|
|
52
369
|
"error",
|
|
53
370
|
"consistent"
|
|
54
371
|
],
|
|
372
|
+
"@stylistic/block-spacing": "off",
|
|
55
373
|
"@stylistic/brace-style": [
|
|
56
374
|
"error",
|
|
57
375
|
"1tbs",
|
|
@@ -101,24 +419,10 @@ var CONFIG = typescript.config(
|
|
|
101
419
|
{ beforeStatementContinuationChars: "always" }
|
|
102
420
|
],
|
|
103
421
|
"@typescript-eslint/no-empty-function": "off",
|
|
104
|
-
"@typescript-eslint/consistent-indexed-object-style": "off"
|
|
105
|
-
"sort-imports": [
|
|
106
|
-
"error",
|
|
107
|
-
{
|
|
108
|
-
allowSeparatedGroups: true,
|
|
109
|
-
ignoreCase: true,
|
|
110
|
-
ignoreMemberSort: false,
|
|
111
|
-
memberSyntaxSortOrder: [
|
|
112
|
-
"none",
|
|
113
|
-
"all",
|
|
114
|
-
"single",
|
|
115
|
-
"multiple"
|
|
116
|
-
]
|
|
117
|
-
}
|
|
118
|
-
]
|
|
422
|
+
"@typescript-eslint/consistent-indexed-object-style": "off"
|
|
119
423
|
}
|
|
120
424
|
}
|
|
121
|
-
);
|
|
425
|
+
]);
|
|
122
426
|
export {
|
|
123
427
|
CONFIG
|
|
124
428
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import eslint from '@eslint/js'\nimport react from 'eslint-plugin-react'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport stylistic from '@stylistic/eslint-plugin'\nimport typescript from 'typescript-eslint'\n\nexport const CONFIG = typescript.config(\n {\n ignores: [\n 'src/graphql/sdk.ts',\n '**/node_modules/**',\n '**/dist/**',\n ],\n settings: {\n react: {\n version: '19',\n },\n },\n },\n eslint.configs.recommended,\n react.configs.flat.recommended,\n stylistic.configs.recommended,\n typescript.configs.recommended,\n typescript.configs.stylistic,\n {\n plugins: {\n 'react-hooks': reactHooks,\n },\n rules: reactHooks.configs.recommended.rules,\n },\n {\n rules: {\n 'capitalized-comments': [\n 'error',\n 'always',\n {ignoreConsecutiveComments: true},\n ],\n 'react/react-in-jsx-scope': 'off',\n '@stylistic/arrow-parens': [\n 'error',\n 'as-needed',\n ],\n '@stylistic/array-bracket-newline': [\n 'error',\n 'consistent',\n ],\n '@stylistic/array-bracket-spacing': [\n 'error',\n 'never',\n ],\n '@stylistic/array-element-newline': [\n 'error',\n 'consistent',\n ],\n '@stylistic/brace-style': [\n 'error',\n '1tbs',\n {allowSingleLine: true},\n ],\n '@stylistic/indent': [\n 'error',\n 2,\n {ignoredNodes: ['TSMappedType > *']},\n ],\n '@stylistic/jsx-tag-spacing': [\n 'error',\n {\n afterOpening: 'never',\n beforeClosing: 'never',\n beforeSelfClosing: 'never',\n closingSlash: 'never',\n },\n ],\n '@stylistic/jsx-wrap-multilines': 'off',\n '@stylistic/lines-between-class-members': 'off',\n '@stylistic/object-curly-newline': [\n 'error',\n {consistent: true},\n ],\n '@stylistic/object-curly-spacing': [\n 'error',\n 'never',\n ],\n '@stylistic/operator-linebreak': [\n 'error',\n 'before',\n {overrides: {'=': 'after'}},\n ],\n '@stylistic/quotes': [\n 'error',\n 'single',\n {avoidEscape: true},\n ],\n '@stylistic/quote-props': [\n 'error',\n 'as-needed',\n ],\n '@stylistic/semi': [\n 'error',\n 'never',\n {beforeStatementContinuationChars: 'always'},\n ],\n '@typescript-eslint/no-empty-function': 'off',\n '@typescript-eslint/consistent-indexed-object-style': 'off',\n 'sort-imports': [\n 'error',\n {\n allowSeparatedGroups: true,\n ignoreCase: true,\n ignoreMemberSort: false,\n memberSyntaxSortOrder: [\n 'none',\n 'all',\n 'single',\n 'multiple',\n ],\n },\n ],\n },\n },\n)\n"],"mappings":";AAAA,OAAO,YAAY;AACnB,OAAO,WAAW;AAClB,OAAO,gBAAgB;AACvB,OAAO,eAAe;AACtB,OAAO,gBAAgB;AAEhB,IAAM,SAAS,WAAW;AAAA,EAC/B;AAAA,IACE,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,UAAU;AAAA,MACR,OAAO;AAAA,QACL,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAAA,EACA,OAAO,QAAQ;AAAA,EACf,MAAM,QAAQ,KAAK;AAAA,EACnB,UAAU,QAAQ;AAAA,EAClB,WAAW,QAAQ;AAAA,EACnB,WAAW,QAAQ;AAAA,EACnB;AAAA,IACE,SAAS;AAAA,MACP,eAAe;AAAA,IACjB;AAAA,IACA,OAAO,WAAW,QAAQ,YAAY;AAAA,EACxC;AAAA,EACA;AAAA,IACE,OAAO;AAAA,MACL,wBAAwB;AAAA,QACtB;AAAA,QACA;AAAA,QACA,EAAC,2BAA2B,KAAI;AAAA,MAClC;AAAA,MACA,4BAA4B;AAAA,MAC5B,2BAA2B;AAAA,QACzB;AAAA,QACA;AAAA,MACF;AAAA,MACA,oCAAoC;AAAA,QAClC;AAAA,QACA;AAAA,MACF;AAAA,MACA,oCAAoC;AAAA,QAClC;AAAA,QACA;AAAA,MACF;AAAA,MACA,oCAAoC;AAAA,QAClC;AAAA,QACA;AAAA,MACF;AAAA,MACA,0BAA0B;AAAA,QACxB;AAAA,QACA;AAAA,QACA,EAAC,iBAAiB,KAAI;AAAA,MACxB;AAAA,MACA,qBAAqB;AAAA,QACnB;AAAA,QACA;AAAA,QACA,EAAC,cAAc,CAAC,kBAAkB,EAAC;AAAA,MACrC;AAAA,MACA,8BAA8B;AAAA,QAC5B;AAAA,QACA;AAAA,UACE,cAAc;AAAA,UACd,eAAe;AAAA,UACf,mBAAmB;AAAA,UACnB,cAAc;AAAA,QAChB;AAAA,MACF;AAAA,MACA,kCAAkC;AAAA,MAClC,0CAA0C;AAAA,MAC1C,mCAAmC;AAAA,QACjC;AAAA,QACA,EAAC,YAAY,KAAI;AAAA,MACnB;AAAA,MACA,mCAAmC;AAAA,QACjC;AAAA,QACA;AAAA,MACF;AAAA,MACA,iCAAiC;AAAA,QAC/B;AAAA,QACA;AAAA,QACA,EAAC,WAAW,EAAC,KAAK,QAAO,EAAC;AAAA,MAC5B;AAAA,MACA,qBAAqB;AAAA,QACnB;AAAA,QACA;AAAA,QACA,EAAC,aAAa,KAAI;AAAA,MACpB;AAAA,MACA,0BAA0B;AAAA,QACxB;AAAA,QACA;AAAA,MACF;AAAA,MACA,mBAAmB;AAAA,QACjB;AAAA,QACA;AAAA,QACA,EAAC,kCAAkC,SAAQ;AAAA,MAC7C;AAAA,MACA,wCAAwC;AAAA,MACxC,sDAAsD;AAAA,MACtD,gBAAgB;AAAA,QACd;AAAA,QACA;AAAA,UACE,sBAAsB;AAAA,UACtB,YAAY;AAAA,UACZ,kBAAkB;AAAA,UAClB,uBAAuB;AAAA,YACrB;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/rules/individualImports.ts","../src/rules/sortedImports/categorizeImport.ts","../src/rules/sortedImports/getSortKey.ts","../src/rules/sortedImports/categorizeImports.ts","../src/rules/sortedImports/checkAlphabeticalSorting.ts","../src/rules/sortedImports/checkGroupOrdering.ts","../src/rules/sortedImports/getSpecifierName.ts","../src/rules/sortedImports/areSpecifiersSorted.ts","../src/rules/sortedImports/getNamedSpecifiers.ts","../src/rules/sortedImports/checkSpecifiersSorting.ts","../src/rules/sortedImports/sortSpecifiersText.ts","../src/rules/sortedImports/createFix/formatNamedImport.ts","../src/rules/sortedImports/createFix/buildSortedCode.ts","../src/rules/sortedImports/createFix/findLastImportIndex.ts","../src/rules/sortedImports/createFix/getReplacementRange.ts","../src/rules/sortedImports/createFix/groupImportsByType.ts","../src/rules/sortedImports/createFix/sortImportGroups.ts","../src/rules/sortedImports/createFix/index.ts","../src/rules/sortedImports/getImportDeclarations.ts","../src/rules/sortedImports/index.ts"],"sourcesContent":["import eslint from '@eslint/js'\nimport react from 'eslint-plugin-react'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport stylistic from '@stylistic/eslint-plugin'\nimport typescript from 'typescript-eslint'\nimport {defineConfig} from 'eslint/config'\nimport {individualImports} from './rules/individualImports'\nimport {sortedImports} from './rules/sortedImports'\n\nexport const CONFIG = defineConfig([\n {\n ignores: [\n 'src/graphql/sdk.ts',\n '**/node_modules/**',\n '**/dist/**',\n ],\n },\n {\n settings: {\n react: {\n version: '19',\n },\n },\n },\n eslint.configs.recommended,\n react.configs.flat.recommended,\n stylistic.configs.recommended,\n typescript.configs.recommended,\n typescript.configs.stylistic,\n {\n plugins: {\n 'react-hooks': reactHooks,\n },\n rules: reactHooks.configs.recommended.rules,\n },\n {\n plugins: {\n '@borela-tech': {\n rules: {\n 'individual-imports': individualImports,\n 'sorted-imports': sortedImports,\n },\n },\n },\n rules: {\n '@borela-tech/individual-imports': 'error',\n '@borela-tech/sorted-imports': 'error',\n },\n },\n {\n rules: {\n 'capitalized-comments': [\n 'error',\n 'always',\n {ignoreConsecutiveComments: true},\n ],\n 'react/react-in-jsx-scope': 'off',\n '@stylistic/arrow-parens': [\n 'error',\n 'as-needed',\n ],\n '@stylistic/array-bracket-newline': [\n 'error',\n 'consistent',\n ],\n '@stylistic/array-bracket-spacing': [\n 'error',\n 'never',\n ],\n '@stylistic/array-element-newline': [\n 'error',\n 'consistent',\n ],\n '@stylistic/block-spacing': 'off',\n '@stylistic/brace-style': [\n 'error',\n '1tbs',\n {allowSingleLine: true},\n ],\n '@stylistic/indent': [\n 'error',\n 2,\n {ignoredNodes: ['TSMappedType > *']},\n ],\n '@stylistic/jsx-tag-spacing': [\n 'error',\n {\n afterOpening: 'never',\n beforeClosing: 'never',\n beforeSelfClosing: 'never',\n closingSlash: 'never',\n },\n ],\n '@stylistic/jsx-wrap-multilines': 'off',\n '@stylistic/lines-between-class-members': 'off',\n '@stylistic/object-curly-newline': [\n 'error',\n {consistent: true},\n ],\n '@stylistic/object-curly-spacing': [\n 'error',\n 'never',\n ],\n '@stylistic/operator-linebreak': [\n 'error',\n 'before',\n {overrides: {'=': 'after'}},\n ],\n '@stylistic/quotes': [\n 'error',\n 'single',\n {avoidEscape: true},\n ],\n '@stylistic/quote-props': [\n 'error',\n 'as-needed',\n ],\n '@stylistic/semi': [\n 'error',\n 'never',\n {beforeStatementContinuationChars: 'always'},\n ],\n '@typescript-eslint/no-empty-function': 'off',\n '@typescript-eslint/consistent-indexed-object-style': 'off',\n },\n },\n])\n","import type {Rule} from 'eslint'\n\nexport const individualImports: Rule.RuleModule = {\n meta: {\n docs: {\n description: 'Enforce individual imports instead of grouped imports',\n recommended: true,\n },\n fixable: 'code',\n messages: {\n individualImports: 'Use individual imports instead of grouped imports.',\n },\n schema: [],\n type: 'suggestion',\n },\n create(context) {\n return {\n ImportDeclaration(node) {\n if (node.specifiers.length <= 1)\n return\n context.report({\n node,\n messageId: 'individualImports',\n fix(fixer) {\n const source = node.source.raw\n const specifiers = node.specifiers\n .map(importSpecifier => {\n if (importSpecifier.type === 'ImportSpecifier')\n return `import {${importSpecifier.local.name}} from ${source}`\n return null\n })\n .filter(Boolean)\n\n if (specifiers.length !== node.specifiers.length)\n return null\n\n return fixer.replaceText(\n node,\n specifiers.join('\\n'),\n )\n },\n })\n },\n }\n },\n}\n","import type {ImportDeclaration} from 'estree'\nimport type {ImportGroup} from './ImportGroup'\n\nexport function categorizeImport(declaration: ImportDeclaration): ImportGroup {\n if (declaration.specifiers.length === 0)\n return 'side-effect'\n\n if (declaration.specifiers.some(s => s.type === 'ImportDefaultSpecifier'))\n return 'default'\n\n return 'named'\n}\n","import type {ImportDeclaration} from 'estree'\nimport type {ImportDefaultSpecifier} from 'estree'\nimport {categorizeImport} from './categorizeImport'\n\nexport function getSortKey(declaration: ImportDeclaration): string {\n const group = categorizeImport(declaration)\n\n if (group === 'side-effect')\n return (declaration.source.value as string).toLowerCase()\n\n if (group === 'default') {\n const defaultSpecifier = declaration.specifiers.find(\n s => s.type === 'ImportDefaultSpecifier',\n ) as ImportDefaultSpecifier | undefined\n\n return defaultSpecifier?.local.name.toLowerCase() ?? ''\n }\n\n return ''\n}\n","import type {ImportDeclaration} from 'estree'\nimport type {CategorizedImport} from './CategorizedImport'\nimport {categorizeImport} from './categorizeImport'\nimport {getSortKey} from './getSortKey'\n\nexport function categorizeImports(declarations: ImportDeclaration[]): CategorizedImport[] {\n return declarations.map(declaration => ({\n declaration,\n group: categorizeImport(declaration),\n sortKey: getSortKey(declaration),\n }))\n}\n","import type {CategorizedImport} from './CategorizedImport'\nimport type {ImportError} from './ImportError'\nimport type {ImportGroup} from './ImportGroup'\n\nexport function checkAlphabeticalSorting(categorized: CategorizedImport[]): ImportError[] {\n const errors: ImportError[] = []\n\n for (const group of ['side-effect', 'default'] as ImportGroup[]) {\n const groupImports = categorized.filter(c => c.group === group)\n const sorted = [...groupImports].sort((a, b) => a.sortKey.localeCompare(b.sortKey))\n for (let i = 0; i < groupImports.length; i++) {\n if (groupImports[i] !== sorted[i]) {\n errors.push({\n node: groupImports[i].declaration,\n messageId: 'sortedImports',\n })\n }\n }\n }\n\n return errors\n}\n","import type {CategorizedImport} from './CategorizedImport'\nimport type {ImportError} from './ImportError'\nimport type {ImportGroup} from './ImportGroup'\n\nexport function checkGroupOrdering(categorized: CategorizedImport[]): ImportError[] {\n const groupOrder: ImportGroup[] = ['side-effect', 'default', 'named']\n const errors: ImportError[] = []\n\n let currentGroupIndex = -1\n for (const {declaration, group} of categorized) {\n const groupIndex = groupOrder.indexOf(group)\n if (groupIndex < currentGroupIndex) {\n errors.push({\n node: declaration,\n messageId: 'wrongGroup',\n })\n } else\n currentGroupIndex = groupIndex\n }\n\n return errors\n}\n","import type {ImportSpecifier} from 'estree'\n\nexport function getSpecifierName(specifier: ImportSpecifier): string {\n return specifier.imported.type === 'Identifier'\n ? specifier.imported.name\n : String(specifier.imported.value)\n}\n","import type {ImportSpecifier} from 'estree'\nimport {getSpecifierName} from './getSpecifierName'\n\nexport function areSpecifiersSorted(specifiers: ImportSpecifier[]): boolean {\n const names = specifiers.map(s => getSpecifierName(s))\n const sorted = [...names].sort((a, b) =>\n a.toLowerCase().localeCompare(b.toLowerCase()),\n )\n return names.every((name, i) => name === sorted[i])\n}\n","import type {ImportDeclaration} from 'estree'\nimport type {ImportSpecifier} from 'estree'\n\nexport function getNamedSpecifiers(declaration: ImportDeclaration): ImportSpecifier[] {\n return declaration.specifiers.filter(\n (s): s is ImportSpecifier => s.type === 'ImportSpecifier',\n )\n}\n","import type {CategorizedImport} from './CategorizedImport'\nimport type {ImportError} from './ImportError'\nimport {areSpecifiersSorted} from './areSpecifiersSorted'\nimport {getNamedSpecifiers} from './getNamedSpecifiers'\n\nexport function checkSpecifiersSorting(categorized: CategorizedImport[]): ImportError[] {\n const errors: ImportError[] = []\n const namedImports = categorized.filter(c => c.group === 'named')\n\n for (const {declaration} of namedImports) {\n const specifiers = getNamedSpecifiers(declaration)\n if (specifiers.length > 1 && !areSpecifiersSorted(specifiers)) {\n errors.push({\n node: declaration,\n messageId: 'sortedNames',\n })\n }\n }\n\n return errors\n}\n","import type {ImportSpecifier} from 'estree'\nimport {getSpecifierName} from './getSpecifierName'\n\nexport function sortSpecifiersText(\n specifiers: ImportSpecifier[],\n sourceCode: {getText: (node: ImportSpecifier) => string},\n): string {\n const sorted = [...specifiers].sort((a, b) => {\n const lowerA = getSpecifierName(a).toLowerCase()\n const lowerB = getSpecifierName(b).toLowerCase()\n return lowerA.localeCompare(lowerB)\n })\n return sorted.map(s => sourceCode.getText(s)).join(', ')\n}\n","import type {ImportDeclaration} from 'estree'\nimport {areSpecifiersSorted} from '../areSpecifiersSorted'\nimport {getNamedSpecifiers} from '../getNamedSpecifiers'\nimport {sortSpecifiersText} from '../sortSpecifiersText'\n\nexport function formatNamedImport(\n declaration: ImportDeclaration,\n sourceCode: {getText: (node?: unknown) => string},\n): string {\n const specifiers = getNamedSpecifiers(declaration)\n\n if (specifiers.length > 1 && !areSpecifiersSorted(specifiers)) {\n const importText = sourceCode.getText(declaration)\n const specifiersStart = importText.indexOf('{')\n const specifiersEnd = importText.lastIndexOf('}')\n const before = importText.substring(0, specifiersStart + 1)\n const after = importText.substring(specifiersEnd)\n const sortedSpecifiers = sortSpecifiersText(specifiers, sourceCode)\n return before + ' ' + sortedSpecifiers + ' ' + after\n }\n\n return sourceCode.getText(declaration)\n}\n","import type {CategorizedImport} from '../CategorizedImport'\nimport type {ImportGroup} from '../ImportGroup'\nimport {formatNamedImport} from './formatNamedImport'\n\nexport function buildSortedCode(\n grouped: Record<ImportGroup, CategorizedImport[]>,\n sourceCode: {getText: (node?: unknown) => string},\n): string[] {\n const groupOrder: ImportGroup[] = ['side-effect', 'default', 'named']\n const sortedCode: string[] = []\n\n for (const group of groupOrder) {\n for (const {declaration} of grouped[group]) {\n if (group === 'named')\n sortedCode.push(formatNamedImport(declaration, sourceCode))\n else\n sortedCode.push(sourceCode.getText(declaration))\n }\n }\n\n return sortedCode\n}\n","import type {Program} from 'estree'\n\nexport function findLastImportIndex(programBody: Program['body']): number {\n let lastIndex = 0\n for (let i = 0; i < programBody.length; i++) {\n if (programBody[i].type === 'ImportDeclaration')\n lastIndex = i\n else\n break\n }\n return lastIndex\n}\n","import type {ImportDeclaration} from 'estree'\nimport type {Program} from 'estree'\nimport type {ReplacementRange} from './ReplacementRange'\nimport {findLastImportIndex} from './findLastImportIndex'\n\nexport function getReplacementRange(\n programBody: Program['body'],\n sourceCode: {getText: () => string},\n): ReplacementRange {\n const fullText = sourceCode.getText()\n const lastIndex = findLastImportIndex(programBody)\n const firstImport = programBody[0] as ImportDeclaration\n const lastImport = programBody[lastIndex] as ImportDeclaration\n const start = firstImport.range![0]\n let end = lastImport.range![1]\n\n for (let i = end; i < fullText.length; i++) {\n const char = fullText[i]\n if (char === '\\n' || char === ' ' || char === '\\t')\n end++\n else\n break\n }\n\n return {start, end}\n}\n","import type {CategorizedImport} from '../CategorizedImport'\nimport type {ImportGroup} from '../ImportGroup'\n\nexport function groupImportsByType(\n categorized: CategorizedImport[],\n): Record<ImportGroup, CategorizedImport[]> {\n const grouped: Record<ImportGroup, CategorizedImport[]> = {\n 'side-effect': [],\n default: [],\n named: [],\n }\n\n for (const item of categorized)\n grouped[item.group].push(item)\n\n return grouped\n}\n","import type {CategorizedImport} from '../CategorizedImport'\nimport type {ImportGroup} from '../ImportGroup'\n\nexport function sortImportGroups(\n grouped: Record<ImportGroup, CategorizedImport[]>,\n): void {\n grouped['side-effect'].sort((a, b) => a.sortKey.localeCompare(b.sortKey))\n grouped['default'].sort((a, b) => a.sortKey.localeCompare(b.sortKey))\n}\n","import type {Rule} from 'eslint'\nimport type {ImportDeclaration} from 'estree'\nimport type {Program} from 'estree'\nimport {buildSortedCode} from './buildSortedCode'\nimport {categorizeImports} from '../categorizeImports'\nimport {getReplacementRange} from './getReplacementRange'\nimport {groupImportsByType} from './groupImportsByType'\nimport {sortImportGroups} from './sortImportGroups'\n\nexport function createFix(\n fixer: Rule.RuleFixer,\n importDeclarations: ImportDeclaration[],\n sourceCode: {getText: (node?: unknown) => string},\n programBody: Program['body'],\n) {\n const range = getReplacementRange(programBody, sourceCode)\n const categorized = categorizeImports(importDeclarations)\n const grouped = groupImportsByType(categorized)\n\n sortImportGroups(grouped)\n\n const sortedCode = buildSortedCode(grouped, sourceCode)\n .join('\\n')\n\n return fixer.replaceTextRange(\n [range.start, range.end],\n sortedCode,\n )\n}\n","import type {ImportDeclaration} from 'estree'\nimport type {Program} from 'estree'\n\nexport function getImportDeclarations(programBody: Program['body']): ImportDeclaration[] {\n return programBody.filter(\n (statement): statement is ImportDeclaration =>\n statement.type === 'ImportDeclaration',\n )\n}\n","import type {Rule} from 'eslint'\nimport type {ImportError} from './ImportError'\nimport {categorizeImports} from './categorizeImports'\nimport {checkAlphabeticalSorting} from './checkAlphabeticalSorting'\nimport {checkGroupOrdering} from './checkGroupOrdering'\nimport {checkSpecifiersSorting} from './checkSpecifiersSorting'\nimport {createFix} from './createFix'\nimport {getImportDeclarations} from './getImportDeclarations'\n\nexport const sortedImports: Rule.RuleModule = {\n meta: {\n docs: {\n description: 'Enforce sorted imports alphabetically',\n recommended: true,\n },\n fixable: 'code',\n messages: {\n sortedImports: 'Imports should be sorted alphabetically',\n sortedNames: 'Named imports should be sorted alphabetically',\n wrongGroup: 'Import is in wrong group',\n },\n schema: [],\n type: 'suggestion',\n },\n create(context) {\n return {\n Program(node) {\n const declarations = getImportDeclarations(node.body)\n if (declarations.length === 0)\n return\n\n const categorized = categorizeImports(declarations)\n const errors: ImportError[] = [\n ...checkGroupOrdering(categorized),\n ...checkAlphabeticalSorting(categorized),\n ...checkSpecifiersSorting(categorized),\n ]\n\n for (const error of errors) {\n context.report({\n node: error.node,\n messageId: error.messageId,\n fix(fixer) {\n const sourceCode = context.sourceCode\n return createFix(fixer, declarations, sourceCode, node.body)\n },\n })\n }\n },\n }\n },\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@borela-tech/eslint-config",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "ESLint config used in Borela Tech projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@stylistic/eslint-plugin": "^4.2.0",
|
|
32
32
|
"eslint-plugin-react": "^7.37.5",
|
|
33
33
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
34
|
-
"typescript-eslint": "
|
|
34
|
+
"typescript-eslint": "8.56.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"eslint": "^9.24.0",
|
package/src/index.ts
CHANGED
|
@@ -3,14 +3,19 @@ import react from 'eslint-plugin-react'
|
|
|
3
3
|
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
4
|
import stylistic from '@stylistic/eslint-plugin'
|
|
5
5
|
import typescript from 'typescript-eslint'
|
|
6
|
+
import {defineConfig} from 'eslint/config'
|
|
7
|
+
import {individualImports} from './rules/individualImports'
|
|
8
|
+
import {sortedImports} from './rules/sortedImports'
|
|
6
9
|
|
|
7
|
-
export const CONFIG =
|
|
10
|
+
export const CONFIG = defineConfig([
|
|
8
11
|
{
|
|
9
12
|
ignores: [
|
|
10
13
|
'src/graphql/sdk.ts',
|
|
11
14
|
'**/node_modules/**',
|
|
12
15
|
'**/dist/**',
|
|
13
16
|
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
14
19
|
settings: {
|
|
15
20
|
react: {
|
|
16
21
|
version: '19',
|
|
@@ -28,6 +33,20 @@ export const CONFIG = typescript.config(
|
|
|
28
33
|
},
|
|
29
34
|
rules: reactHooks.configs.recommended.rules,
|
|
30
35
|
},
|
|
36
|
+
{
|
|
37
|
+
plugins: {
|
|
38
|
+
'@borela-tech': {
|
|
39
|
+
rules: {
|
|
40
|
+
'individual-imports': individualImports,
|
|
41
|
+
'sorted-imports': sortedImports,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
rules: {
|
|
46
|
+
'@borela-tech/individual-imports': 'error',
|
|
47
|
+
'@borela-tech/sorted-imports': 'error',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
31
50
|
{
|
|
32
51
|
rules: {
|
|
33
52
|
'capitalized-comments': [
|
|
@@ -52,6 +71,7 @@ export const CONFIG = typescript.config(
|
|
|
52
71
|
'error',
|
|
53
72
|
'consistent',
|
|
54
73
|
],
|
|
74
|
+
'@stylistic/block-spacing': 'off',
|
|
55
75
|
'@stylistic/brace-style': [
|
|
56
76
|
'error',
|
|
57
77
|
'1tbs',
|
|
@@ -102,20 +122,6 @@ export const CONFIG = typescript.config(
|
|
|
102
122
|
],
|
|
103
123
|
'@typescript-eslint/no-empty-function': 'off',
|
|
104
124
|
'@typescript-eslint/consistent-indexed-object-style': 'off',
|
|
105
|
-
'sort-imports': [
|
|
106
|
-
'error',
|
|
107
|
-
{
|
|
108
|
-
allowSeparatedGroups: true,
|
|
109
|
-
ignoreCase: true,
|
|
110
|
-
ignoreMemberSort: false,
|
|
111
|
-
memberSyntaxSortOrder: [
|
|
112
|
-
'none',
|
|
113
|
-
'all',
|
|
114
|
-
'single',
|
|
115
|
-
'multiple',
|
|
116
|
-
],
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
125
|
},
|
|
120
126
|
},
|
|
121
|
-
)
|
|
127
|
+
])
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import {countLeadingSpaces} from './countLeadingSpaces'
|
|
2
|
+
|
|
3
|
+
export function findMinIndent(lines: string[]): number {
|
|
4
|
+
const nonEmptyLines = lines.filter(line => line.trim().length > 0)
|
|
5
|
+
const indents = nonEmptyLines.map(line => countLeadingSpaces(line))
|
|
6
|
+
return Math.min(...indents)
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {findMinIndent} from './findMinIndent'
|
|
2
|
+
import {interpolate} from './interpolate'
|
|
3
|
+
import {removeEmptyPrefix} from './removeEmptyPrefix'
|
|
4
|
+
import {removeEmptySuffix} from './removeEmptySuffix'
|
|
5
|
+
import {removeIndent} from './removeIndent'
|
|
6
|
+
|
|
7
|
+
export function dedent(
|
|
8
|
+
strings: TemplateStringsArray,
|
|
9
|
+
...values: unknown[]
|
|
10
|
+
): string {
|
|
11
|
+
const raw = interpolate(strings, values)
|
|
12
|
+
const lines = raw.split('\n')
|
|
13
|
+
const withoutPrefix = removeEmptyPrefix(lines)
|
|
14
|
+
const trimmed = removeEmptySuffix(withoutPrefix)
|
|
15
|
+
const indentSize = findMinIndent(trimmed)
|
|
16
|
+
return removeIndent(trimmed, indentSize)
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function interpolate(
|
|
2
|
+
strings: TemplateStringsArray,
|
|
3
|
+
values: unknown[],
|
|
4
|
+
): string {
|
|
5
|
+
let result = ''
|
|
6
|
+
for (let i = 0; i < strings.length; i++) {
|
|
7
|
+
result += strings[i]
|
|
8
|
+
if (i < values.length) result += String(values[i])
|
|
9
|
+
}
|
|
10
|
+
return result
|
|
11
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import typescript from 'typescript-eslint'
|
|
2
|
+
import {RuleTester} from 'eslint'
|
|
3
|
+
import {individualImports} from '../individualImports'
|
|
4
|
+
import {dedent} from './dedent'
|
|
5
|
+
|
|
6
|
+
const ruleTester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser: typescript.parser,
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaVersion: 2020,
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
ruleTester.run('individual-imports', individualImports, {
|
|
17
|
+
valid: [{
|
|
18
|
+
code: "import {foo} from 'bar'",
|
|
19
|
+
}, {
|
|
20
|
+
code: "import foo from 'bar'",
|
|
21
|
+
}, {
|
|
22
|
+
code: "import * as foo from 'bar'",
|
|
23
|
+
}, {
|
|
24
|
+
code: "import 'bar'",
|
|
25
|
+
}],
|
|
26
|
+
invalid: [{
|
|
27
|
+
code: "import {foo, bar} from 'baz'",
|
|
28
|
+
errors: [{messageId: 'individualImports'}],
|
|
29
|
+
output: dedent`
|
|
30
|
+
import {foo} from 'baz'
|
|
31
|
+
import {bar} from 'baz'
|
|
32
|
+
`,
|
|
33
|
+
}, {
|
|
34
|
+
code: "import {foo, bar, baz} from 'qux'",
|
|
35
|
+
errors: [{messageId: 'individualImports'}],
|
|
36
|
+
output: dedent`
|
|
37
|
+
import {foo} from 'qux'
|
|
38
|
+
import {bar} from 'qux'
|
|
39
|
+
import {baz} from 'qux'
|
|
40
|
+
`,
|
|
41
|
+
}],
|
|
42
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import typescript from 'typescript-eslint'
|
|
2
|
+
import {RuleTester} from 'eslint'
|
|
3
|
+
import {sortedImports} from '../sortedImports'
|
|
4
|
+
import {dedent} from './dedent'
|
|
5
|
+
|
|
6
|
+
const ruleTester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser: typescript.parser,
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaVersion: 2020,
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
ruleTester.run('sorted-imports', sortedImports, {
|
|
17
|
+
valid: [{
|
|
18
|
+
code: "import {foo} from 'bar'",
|
|
19
|
+
}, {
|
|
20
|
+
code: "import foo from 'bar'",
|
|
21
|
+
}, {
|
|
22
|
+
code: "import 'bar'",
|
|
23
|
+
}, {
|
|
24
|
+
code: dedent`
|
|
25
|
+
import 'aaa'
|
|
26
|
+
import 'bbb'
|
|
27
|
+
import bar from 'bbb'
|
|
28
|
+
import foo from 'aaa'
|
|
29
|
+
import {a} from 'aaa'
|
|
30
|
+
import {b} from 'bbb'
|
|
31
|
+
`,
|
|
32
|
+
}, {
|
|
33
|
+
code: dedent`
|
|
34
|
+
import {a} from 'ccc'
|
|
35
|
+
import {b} from 'aaa'
|
|
36
|
+
import {c} from 'bbb'
|
|
37
|
+
`,
|
|
38
|
+
}, {
|
|
39
|
+
code: dedent`
|
|
40
|
+
import {a, b, c} from 'bar'
|
|
41
|
+
`,
|
|
42
|
+
}, {
|
|
43
|
+
code: '',
|
|
44
|
+
}, {
|
|
45
|
+
code: 'const x = 1',
|
|
46
|
+
}],
|
|
47
|
+
invalid: [{
|
|
48
|
+
code: dedent`
|
|
49
|
+
import {c, a, b} from 'bar'
|
|
50
|
+
`,
|
|
51
|
+
errors: [{messageId: 'sortedNames'}],
|
|
52
|
+
output: dedent`
|
|
53
|
+
import { a, b, c } from 'bar'
|
|
54
|
+
`,
|
|
55
|
+
}, {
|
|
56
|
+
code: dedent`
|
|
57
|
+
import {z, a} from 'bar'
|
|
58
|
+
`,
|
|
59
|
+
errors: [{messageId: 'sortedNames'}],
|
|
60
|
+
output: dedent`
|
|
61
|
+
import { a, z } from 'bar'
|
|
62
|
+
`,
|
|
63
|
+
}, {
|
|
64
|
+
code: dedent`
|
|
65
|
+
import foo from 'aaa'
|
|
66
|
+
import bar from 'bbb'
|
|
67
|
+
`,
|
|
68
|
+
errors: [{messageId: 'sortedImports'}, {messageId: 'sortedImports'}],
|
|
69
|
+
output: dedent`
|
|
70
|
+
import bar from 'bbb'
|
|
71
|
+
import foo from 'aaa'
|
|
72
|
+
`,
|
|
73
|
+
}, {
|
|
74
|
+
code: dedent`
|
|
75
|
+
import 'bbb'
|
|
76
|
+
import 'aaa'
|
|
77
|
+
`,
|
|
78
|
+
errors: [{messageId: 'sortedImports'}, {messageId: 'sortedImports'}],
|
|
79
|
+
output: dedent`
|
|
80
|
+
import 'aaa'
|
|
81
|
+
import 'bbb'
|
|
82
|
+
`,
|
|
83
|
+
}, {
|
|
84
|
+
code: dedent`
|
|
85
|
+
import foo from 'bar'
|
|
86
|
+
import 'baz'
|
|
87
|
+
`,
|
|
88
|
+
errors: [{messageId: 'wrongGroup'}],
|
|
89
|
+
output: dedent`
|
|
90
|
+
import 'baz'
|
|
91
|
+
import foo from 'bar'
|
|
92
|
+
`,
|
|
93
|
+
}, {
|
|
94
|
+
code: dedent`
|
|
95
|
+
import {a} from 'bar'
|
|
96
|
+
import foo from 'baz'
|
|
97
|
+
`,
|
|
98
|
+
errors: [{messageId: 'wrongGroup'}],
|
|
99
|
+
output: dedent`
|
|
100
|
+
import foo from 'baz'
|
|
101
|
+
import {a} from 'bar'
|
|
102
|
+
`,
|
|
103
|
+
}, {
|
|
104
|
+
code: dedent`
|
|
105
|
+
import {b, a} from 'bar'
|
|
106
|
+
import foo from 'baz'
|
|
107
|
+
`,
|
|
108
|
+
errors: [{messageId: 'sortedNames'}, {messageId: 'wrongGroup'}],
|
|
109
|
+
output: dedent`
|
|
110
|
+
import foo from 'baz'
|
|
111
|
+
import { a, b } from 'bar'
|
|
112
|
+
`,
|
|
113
|
+
}],
|
|
114
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type {Rule} from 'eslint'
|
|
2
|
+
|
|
3
|
+
export const individualImports: Rule.RuleModule = {
|
|
4
|
+
meta: {
|
|
5
|
+
docs: {
|
|
6
|
+
description: 'Enforce individual imports instead of grouped imports',
|
|
7
|
+
recommended: true,
|
|
8
|
+
},
|
|
9
|
+
fixable: 'code',
|
|
10
|
+
messages: {
|
|
11
|
+
individualImports: 'Use individual imports instead of grouped imports.',
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
type: 'suggestion',
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
ImportDeclaration(node) {
|
|
19
|
+
if (node.specifiers.length <= 1)
|
|
20
|
+
return
|
|
21
|
+
context.report({
|
|
22
|
+
node,
|
|
23
|
+
messageId: 'individualImports',
|
|
24
|
+
fix(fixer) {
|
|
25
|
+
const source = node.source.raw
|
|
26
|
+
const specifiers = node.specifiers
|
|
27
|
+
.map(importSpecifier => {
|
|
28
|
+
if (importSpecifier.type === 'ImportSpecifier')
|
|
29
|
+
return `import {${importSpecifier.local.name}} from ${source}`
|
|
30
|
+
return null
|
|
31
|
+
})
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
|
|
34
|
+
if (specifiers.length !== node.specifiers.length)
|
|
35
|
+
return null
|
|
36
|
+
|
|
37
|
+
return fixer.replaceText(
|
|
38
|
+
node,
|
|
39
|
+
specifiers.join('\n'),
|
|
40
|
+
)
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type ImportGroup = 'side-effect' | 'default' | 'named'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type {ImportSpecifier} from 'estree'
|
|
2
|
+
import {getSpecifierName} from './getSpecifierName'
|
|
3
|
+
|
|
4
|
+
export function areSpecifiersSorted(specifiers: ImportSpecifier[]): boolean {
|
|
5
|
+
const names = specifiers.map(s => getSpecifierName(s))
|
|
6
|
+
const sorted = [...names].sort((a, b) =>
|
|
7
|
+
a.toLowerCase().localeCompare(b.toLowerCase()),
|
|
8
|
+
)
|
|
9
|
+
return names.every((name, i) => name === sorted[i])
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type {ImportDeclaration} from 'estree'
|
|
2
|
+
import type {ImportGroup} from './ImportGroup'
|
|
3
|
+
|
|
4
|
+
export function categorizeImport(declaration: ImportDeclaration): ImportGroup {
|
|
5
|
+
if (declaration.specifiers.length === 0)
|
|
6
|
+
return 'side-effect'
|
|
7
|
+
|
|
8
|
+
if (declaration.specifiers.some(s => s.type === 'ImportDefaultSpecifier'))
|
|
9
|
+
return 'default'
|
|
10
|
+
|
|
11
|
+
return 'named'
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type {ImportDeclaration} from 'estree'
|
|
2
|
+
import type {CategorizedImport} from './CategorizedImport'
|
|
3
|
+
import {categorizeImport} from './categorizeImport'
|
|
4
|
+
import {getSortKey} from './getSortKey'
|
|
5
|
+
|
|
6
|
+
export function categorizeImports(declarations: ImportDeclaration[]): CategorizedImport[] {
|
|
7
|
+
return declarations.map(declaration => ({
|
|
8
|
+
declaration,
|
|
9
|
+
group: categorizeImport(declaration),
|
|
10
|
+
sortKey: getSortKey(declaration),
|
|
11
|
+
}))
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {CategorizedImport} from './CategorizedImport'
|
|
2
|
+
import type {ImportError} from './ImportError'
|
|
3
|
+
import type {ImportGroup} from './ImportGroup'
|
|
4
|
+
|
|
5
|
+
export function checkAlphabeticalSorting(categorized: CategorizedImport[]): ImportError[] {
|
|
6
|
+
const errors: ImportError[] = []
|
|
7
|
+
|
|
8
|
+
for (const group of ['side-effect', 'default'] as ImportGroup[]) {
|
|
9
|
+
const groupImports = categorized.filter(c => c.group === group)
|
|
10
|
+
const sorted = [...groupImports].sort((a, b) => a.sortKey.localeCompare(b.sortKey))
|
|
11
|
+
for (let i = 0; i < groupImports.length; i++) {
|
|
12
|
+
if (groupImports[i] !== sorted[i]) {
|
|
13
|
+
errors.push({
|
|
14
|
+
node: groupImports[i].declaration,
|
|
15
|
+
messageId: 'sortedImports',
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return errors
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {CategorizedImport} from './CategorizedImport'
|
|
2
|
+
import type {ImportError} from './ImportError'
|
|
3
|
+
import type {ImportGroup} from './ImportGroup'
|
|
4
|
+
|
|
5
|
+
export function checkGroupOrdering(categorized: CategorizedImport[]): ImportError[] {
|
|
6
|
+
const groupOrder: ImportGroup[] = ['side-effect', 'default', 'named']
|
|
7
|
+
const errors: ImportError[] = []
|
|
8
|
+
|
|
9
|
+
let currentGroupIndex = -1
|
|
10
|
+
for (const {declaration, group} of categorized) {
|
|
11
|
+
const groupIndex = groupOrder.indexOf(group)
|
|
12
|
+
if (groupIndex < currentGroupIndex) {
|
|
13
|
+
errors.push({
|
|
14
|
+
node: declaration,
|
|
15
|
+
messageId: 'wrongGroup',
|
|
16
|
+
})
|
|
17
|
+
} else
|
|
18
|
+
currentGroupIndex = groupIndex
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return errors
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type {CategorizedImport} from './CategorizedImport'
|
|
2
|
+
import type {ImportError} from './ImportError'
|
|
3
|
+
import {areSpecifiersSorted} from './areSpecifiersSorted'
|
|
4
|
+
import {getNamedSpecifiers} from './getNamedSpecifiers'
|
|
5
|
+
|
|
6
|
+
export function checkSpecifiersSorting(categorized: CategorizedImport[]): ImportError[] {
|
|
7
|
+
const errors: ImportError[] = []
|
|
8
|
+
const namedImports = categorized.filter(c => c.group === 'named')
|
|
9
|
+
|
|
10
|
+
for (const {declaration} of namedImports) {
|
|
11
|
+
const specifiers = getNamedSpecifiers(declaration)
|
|
12
|
+
if (specifiers.length > 1 && !areSpecifiersSorted(specifiers)) {
|
|
13
|
+
errors.push({
|
|
14
|
+
node: declaration,
|
|
15
|
+
messageId: 'sortedNames',
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return errors
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {CategorizedImport} from '../CategorizedImport'
|
|
2
|
+
import type {ImportGroup} from '../ImportGroup'
|
|
3
|
+
import {formatNamedImport} from './formatNamedImport'
|
|
4
|
+
|
|
5
|
+
export function buildSortedCode(
|
|
6
|
+
grouped: Record<ImportGroup, CategorizedImport[]>,
|
|
7
|
+
sourceCode: {getText: (node?: unknown) => string},
|
|
8
|
+
): string[] {
|
|
9
|
+
const groupOrder: ImportGroup[] = ['side-effect', 'default', 'named']
|
|
10
|
+
const sortedCode: string[] = []
|
|
11
|
+
|
|
12
|
+
for (const group of groupOrder) {
|
|
13
|
+
for (const {declaration} of grouped[group]) {
|
|
14
|
+
if (group === 'named')
|
|
15
|
+
sortedCode.push(formatNamedImport(declaration, sourceCode))
|
|
16
|
+
else
|
|
17
|
+
sortedCode.push(sourceCode.getText(declaration))
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return sortedCode
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type {Program} from 'estree'
|
|
2
|
+
|
|
3
|
+
export function findLastImportIndex(programBody: Program['body']): number {
|
|
4
|
+
let lastIndex = 0
|
|
5
|
+
for (let i = 0; i < programBody.length; i++) {
|
|
6
|
+
if (programBody[i].type === 'ImportDeclaration')
|
|
7
|
+
lastIndex = i
|
|
8
|
+
else
|
|
9
|
+
break
|
|
10
|
+
}
|
|
11
|
+
return lastIndex
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type {ImportDeclaration} from 'estree'
|
|
2
|
+
import {areSpecifiersSorted} from '../areSpecifiersSorted'
|
|
3
|
+
import {getNamedSpecifiers} from '../getNamedSpecifiers'
|
|
4
|
+
import {sortSpecifiersText} from '../sortSpecifiersText'
|
|
5
|
+
|
|
6
|
+
export function formatNamedImport(
|
|
7
|
+
declaration: ImportDeclaration,
|
|
8
|
+
sourceCode: {getText: (node?: unknown) => string},
|
|
9
|
+
): string {
|
|
10
|
+
const specifiers = getNamedSpecifiers(declaration)
|
|
11
|
+
|
|
12
|
+
if (specifiers.length > 1 && !areSpecifiersSorted(specifiers)) {
|
|
13
|
+
const importText = sourceCode.getText(declaration)
|
|
14
|
+
const specifiersStart = importText.indexOf('{')
|
|
15
|
+
const specifiersEnd = importText.lastIndexOf('}')
|
|
16
|
+
const before = importText.substring(0, specifiersStart + 1)
|
|
17
|
+
const after = importText.substring(specifiersEnd)
|
|
18
|
+
const sortedSpecifiers = sortSpecifiersText(specifiers, sourceCode)
|
|
19
|
+
return before + ' ' + sortedSpecifiers + ' ' + after
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return sourceCode.getText(declaration)
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {ImportDeclaration} from 'estree'
|
|
2
|
+
import type {Program} from 'estree'
|
|
3
|
+
import type {ReplacementRange} from './ReplacementRange'
|
|
4
|
+
import {findLastImportIndex} from './findLastImportIndex'
|
|
5
|
+
|
|
6
|
+
export function getReplacementRange(
|
|
7
|
+
programBody: Program['body'],
|
|
8
|
+
sourceCode: {getText: () => string},
|
|
9
|
+
): ReplacementRange {
|
|
10
|
+
const fullText = sourceCode.getText()
|
|
11
|
+
const lastIndex = findLastImportIndex(programBody)
|
|
12
|
+
const firstImport = programBody[0] as ImportDeclaration
|
|
13
|
+
const lastImport = programBody[lastIndex] as ImportDeclaration
|
|
14
|
+
const start = firstImport.range![0]
|
|
15
|
+
let end = lastImport.range![1]
|
|
16
|
+
|
|
17
|
+
for (let i = end; i < fullText.length; i++) {
|
|
18
|
+
const char = fullText[i]
|
|
19
|
+
if (char === '\n' || char === ' ' || char === '\t')
|
|
20
|
+
end++
|
|
21
|
+
else
|
|
22
|
+
break
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {start, end}
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type {CategorizedImport} from '../CategorizedImport'
|
|
2
|
+
import type {ImportGroup} from '../ImportGroup'
|
|
3
|
+
|
|
4
|
+
export function groupImportsByType(
|
|
5
|
+
categorized: CategorizedImport[],
|
|
6
|
+
): Record<ImportGroup, CategorizedImport[]> {
|
|
7
|
+
const grouped: Record<ImportGroup, CategorizedImport[]> = {
|
|
8
|
+
'side-effect': [],
|
|
9
|
+
default: [],
|
|
10
|
+
named: [],
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
for (const item of categorized)
|
|
14
|
+
grouped[item.group].push(item)
|
|
15
|
+
|
|
16
|
+
return grouped
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type {Rule} from 'eslint'
|
|
2
|
+
import type {ImportDeclaration} from 'estree'
|
|
3
|
+
import type {Program} from 'estree'
|
|
4
|
+
import {buildSortedCode} from './buildSortedCode'
|
|
5
|
+
import {categorizeImports} from '../categorizeImports'
|
|
6
|
+
import {getReplacementRange} from './getReplacementRange'
|
|
7
|
+
import {groupImportsByType} from './groupImportsByType'
|
|
8
|
+
import {sortImportGroups} from './sortImportGroups'
|
|
9
|
+
|
|
10
|
+
export function createFix(
|
|
11
|
+
fixer: Rule.RuleFixer,
|
|
12
|
+
importDeclarations: ImportDeclaration[],
|
|
13
|
+
sourceCode: {getText: (node?: unknown) => string},
|
|
14
|
+
programBody: Program['body'],
|
|
15
|
+
) {
|
|
16
|
+
const range = getReplacementRange(programBody, sourceCode)
|
|
17
|
+
const categorized = categorizeImports(importDeclarations)
|
|
18
|
+
const grouped = groupImportsByType(categorized)
|
|
19
|
+
|
|
20
|
+
sortImportGroups(grouped)
|
|
21
|
+
|
|
22
|
+
const sortedCode = buildSortedCode(grouped, sourceCode)
|
|
23
|
+
.join('\n')
|
|
24
|
+
|
|
25
|
+
return fixer.replaceTextRange(
|
|
26
|
+
[range.start, range.end],
|
|
27
|
+
sortedCode,
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type {CategorizedImport} from '../CategorizedImport'
|
|
2
|
+
import type {ImportGroup} from '../ImportGroup'
|
|
3
|
+
|
|
4
|
+
export function sortImportGroups(
|
|
5
|
+
grouped: Record<ImportGroup, CategorizedImport[]>,
|
|
6
|
+
): void {
|
|
7
|
+
grouped['side-effect'].sort((a, b) => a.sortKey.localeCompare(b.sortKey))
|
|
8
|
+
grouped['default'].sort((a, b) => a.sortKey.localeCompare(b.sortKey))
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type {ImportDeclaration} from 'estree'
|
|
2
|
+
import type {Program} from 'estree'
|
|
3
|
+
|
|
4
|
+
export function getImportDeclarations(programBody: Program['body']): ImportDeclaration[] {
|
|
5
|
+
return programBody.filter(
|
|
6
|
+
(statement): statement is ImportDeclaration =>
|
|
7
|
+
statement.type === 'ImportDeclaration',
|
|
8
|
+
)
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type {ImportDeclaration} from 'estree'
|
|
2
|
+
import type {ImportSpecifier} from 'estree'
|
|
3
|
+
|
|
4
|
+
export function getNamedSpecifiers(declaration: ImportDeclaration): ImportSpecifier[] {
|
|
5
|
+
return declaration.specifiers.filter(
|
|
6
|
+
(s): s is ImportSpecifier => s.type === 'ImportSpecifier',
|
|
7
|
+
)
|
|
8
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {ImportDeclaration} from 'estree'
|
|
2
|
+
import type {ImportDefaultSpecifier} from 'estree'
|
|
3
|
+
import {categorizeImport} from './categorizeImport'
|
|
4
|
+
|
|
5
|
+
export function getSortKey(declaration: ImportDeclaration): string {
|
|
6
|
+
const group = categorizeImport(declaration)
|
|
7
|
+
|
|
8
|
+
if (group === 'side-effect')
|
|
9
|
+
return (declaration.source.value as string).toLowerCase()
|
|
10
|
+
|
|
11
|
+
if (group === 'default') {
|
|
12
|
+
const defaultSpecifier = declaration.specifiers.find(
|
|
13
|
+
s => s.type === 'ImportDefaultSpecifier',
|
|
14
|
+
) as ImportDefaultSpecifier | undefined
|
|
15
|
+
|
|
16
|
+
return defaultSpecifier?.local.name.toLowerCase() ?? ''
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return ''
|
|
20
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type {Rule} from 'eslint'
|
|
2
|
+
import type {ImportError} from './ImportError'
|
|
3
|
+
import {categorizeImports} from './categorizeImports'
|
|
4
|
+
import {checkAlphabeticalSorting} from './checkAlphabeticalSorting'
|
|
5
|
+
import {checkGroupOrdering} from './checkGroupOrdering'
|
|
6
|
+
import {checkSpecifiersSorting} from './checkSpecifiersSorting'
|
|
7
|
+
import {createFix} from './createFix'
|
|
8
|
+
import {getImportDeclarations} from './getImportDeclarations'
|
|
9
|
+
|
|
10
|
+
export const sortedImports: Rule.RuleModule = {
|
|
11
|
+
meta: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Enforce sorted imports alphabetically',
|
|
14
|
+
recommended: true,
|
|
15
|
+
},
|
|
16
|
+
fixable: 'code',
|
|
17
|
+
messages: {
|
|
18
|
+
sortedImports: 'Imports should be sorted alphabetically',
|
|
19
|
+
sortedNames: 'Named imports should be sorted alphabetically',
|
|
20
|
+
wrongGroup: 'Import is in wrong group',
|
|
21
|
+
},
|
|
22
|
+
schema: [],
|
|
23
|
+
type: 'suggestion',
|
|
24
|
+
},
|
|
25
|
+
create(context) {
|
|
26
|
+
return {
|
|
27
|
+
Program(node) {
|
|
28
|
+
const declarations = getImportDeclarations(node.body)
|
|
29
|
+
if (declarations.length === 0)
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
const categorized = categorizeImports(declarations)
|
|
33
|
+
const errors: ImportError[] = [
|
|
34
|
+
...checkGroupOrdering(categorized),
|
|
35
|
+
...checkAlphabeticalSorting(categorized),
|
|
36
|
+
...checkSpecifiersSorting(categorized),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
for (const error of errors) {
|
|
40
|
+
context.report({
|
|
41
|
+
node: error.node,
|
|
42
|
+
messageId: error.messageId,
|
|
43
|
+
fix(fixer) {
|
|
44
|
+
const sourceCode = context.sourceCode
|
|
45
|
+
return createFix(fixer, declarations, sourceCode, node.body)
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type {ImportSpecifier} from 'estree'
|
|
2
|
+
import {getSpecifierName} from './getSpecifierName'
|
|
3
|
+
|
|
4
|
+
export function sortSpecifiersText(
|
|
5
|
+
specifiers: ImportSpecifier[],
|
|
6
|
+
sourceCode: {getText: (node: ImportSpecifier) => string},
|
|
7
|
+
): string {
|
|
8
|
+
const sorted = [...specifiers].sort((a, b) => {
|
|
9
|
+
const lowerA = getSpecifierName(a).toLowerCase()
|
|
10
|
+
const lowerB = getSpecifierName(b).toLowerCase()
|
|
11
|
+
return lowerA.localeCompare(lowerB)
|
|
12
|
+
})
|
|
13
|
+
return sorted.map(s => sourceCode.getText(s)).join(', ')
|
|
14
|
+
}
|