@borela-tech/eslint-config 1.3.3 → 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.
Files changed (41) hide show
  1. package/bin/lint +2 -1
  2. package/bin/test +12 -0
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +321 -18
  5. package/dist/index.js.map +1 -1
  6. package/package.json +2 -2
  7. package/src/index.ts +21 -16
  8. package/src/rules/__tests__/dedent/countLeadingSpaces.ts +4 -0
  9. package/src/rules/__tests__/dedent/findMinIndent.ts +7 -0
  10. package/src/rules/__tests__/dedent/index.ts +17 -0
  11. package/src/rules/__tests__/dedent/interpolate.ts +11 -0
  12. package/src/rules/__tests__/dedent/removeEmptyPrefix.ts +6 -0
  13. package/src/rules/__tests__/dedent/removeEmptySuffix.ts +6 -0
  14. package/src/rules/__tests__/dedent/removeIndent.ts +3 -0
  15. package/src/rules/__tests__/individualImports.test.ts +42 -0
  16. package/src/rules/__tests__/sortedImports.test.ts +114 -0
  17. package/src/rules/individualImports.ts +3 -3
  18. package/src/rules/sortedImports/CategorizedImport.ts +8 -0
  19. package/src/rules/sortedImports/ImportError.ts +6 -0
  20. package/src/rules/sortedImports/ImportGroup.ts +1 -0
  21. package/src/rules/sortedImports/areSpecifiersSorted.ts +10 -0
  22. package/src/rules/sortedImports/categorizeImport.ts +12 -0
  23. package/src/rules/sortedImports/categorizeImports.ts +12 -0
  24. package/src/rules/sortedImports/checkAlphabeticalSorting.ts +22 -0
  25. package/src/rules/sortedImports/checkGroupOrdering.ts +22 -0
  26. package/src/rules/sortedImports/checkSpecifiersSorting.ts +21 -0
  27. package/src/rules/sortedImports/createFix/ReplacementRange.ts +4 -0
  28. package/src/rules/sortedImports/createFix/buildSortedCode.ts +22 -0
  29. package/src/rules/sortedImports/createFix/findLastImportIndex.ts +12 -0
  30. package/src/rules/sortedImports/createFix/formatNamedImport.ts +23 -0
  31. package/src/rules/sortedImports/createFix/getReplacementRange.ts +26 -0
  32. package/src/rules/sortedImports/createFix/groupImportsByType.ts +17 -0
  33. package/src/rules/sortedImports/createFix/index.ts +29 -0
  34. package/src/rules/sortedImports/createFix/sortImportGroups.ts +9 -0
  35. package/src/rules/sortedImports/getImportDeclarations.ts +9 -0
  36. package/src/rules/sortedImports/getNamedSpecifiers.ts +8 -0
  37. package/src/rules/sortedImports/getSortKey.ts +20 -0
  38. package/src/rules/sortedImports/getSpecifierName.ts +7 -0
  39. package/src/rules/sortedImports/index.ts +52 -0
  40. package/src/rules/sortedImports/sortSpecifiersText.ts +14 -0
  41. package/src/rules/sortedImports.ts +0 -83
package/bin/lint CHANGED
@@ -3,10 +3,11 @@
3
3
  SCRIPT_DIR=$(dirname -- "${BASH_SOURCE[0]}")
4
4
  pushd "$SCRIPT_DIR/.." > /dev/null
5
5
 
6
+ ./bin/build
6
7
  npx eslint \
7
8
  --ext ts \
8
9
  --max-warnings 0 \
9
10
  --report-unused-disable-directives \
10
- src
11
+ .
11
12
 
12
13
  popd > /dev/null
package/bin/test ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env -S bash -e -o pipefail
2
+
3
+ SCRIPT_DIR=$(dirname -- "${BASH_SOURCE[0]}")
4
+ pushd "$SCRIPT_DIR/.." > /dev/null
5
+
6
+ ./bin/build
7
+ node \
8
+ --import jiti/register \
9
+ --test \
10
+ src/rules/__tests__/*.test.ts
11
+
12
+ popd > /dev/null
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts-eslint';
1
+ import * as eslint_config from 'eslint/config';
2
2
 
3
- declare const CONFIG: _typescript_eslint_utils_ts_eslint.FlatConfig.ConfigArray;
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
- var CONFIG = typescript.config(
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": [
@@ -102,24 +419,10 @@ var CONFIG = typescript.config(
102
419
  { beforeStatementContinuationChars: "always" }
103
420
  ],
104
421
  "@typescript-eslint/no-empty-function": "off",
105
- "@typescript-eslint/consistent-indexed-object-style": "off",
106
- "sort-imports": [
107
- "error",
108
- {
109
- allowSeparatedGroups: true,
110
- ignoreCase: true,
111
- ignoreMemberSort: false,
112
- memberSyntaxSortOrder: [
113
- "none",
114
- "all",
115
- "single",
116
- "multiple"
117
- ]
118
- }
119
- ]
422
+ "@typescript-eslint/consistent-indexed-object-style": "off"
120
423
  }
121
424
  }
122
- );
425
+ ]);
123
426
  export {
124
427
  CONFIG
125
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/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 '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,4BAA4B;AAAA,MAC5B,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": "1.3.3",
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": "^8.29.1"
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 = typescript.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': [
@@ -103,20 +122,6 @@ export const CONFIG = typescript.config(
103
122
  ],
104
123
  '@typescript-eslint/no-empty-function': 'off',
105
124
  '@typescript-eslint/consistent-indexed-object-style': 'off',
106
- 'sort-imports': [
107
- 'error',
108
- {
109
- allowSeparatedGroups: true,
110
- ignoreCase: true,
111
- ignoreMemberSort: false,
112
- memberSyntaxSortOrder: [
113
- 'none',
114
- 'all',
115
- 'single',
116
- 'multiple',
117
- ],
118
- },
119
- ],
120
125
  },
121
126
  },
122
- )
127
+ ])
@@ -0,0 +1,4 @@
1
+ export function countLeadingSpaces(line: string): number {
2
+ const match = line.match(/^(\s*)/)
3
+ return match ? match[1].length : 0
4
+ }
@@ -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,6 @@
1
+ export function removeEmptyPrefix(lines: string[]): string[] {
2
+ const copy = [...lines]
3
+ while (copy.length > 0 && copy[0].trim() === '')
4
+ copy.shift()
5
+ return copy
6
+ }
@@ -0,0 +1,6 @@
1
+ export function removeEmptySuffix(lines: string[]): string[] {
2
+ const copy = [...lines]
3
+ while (copy.length > 0 && copy[copy.length - 1].trim() === '')
4
+ copy.pop()
5
+ return copy
6
+ }
@@ -0,0 +1,3 @@
1
+ export function removeIndent(lines: string[], indentSize: number): string {
2
+ return lines.map(line => line.slice(indentSize)).join('\n')
3
+ }
@@ -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
+ })
@@ -24,9 +24,9 @@ export const individualImports: Rule.RuleModule = {
24
24
  fix(fixer) {
25
25
  const source = node.source.raw
26
26
  const specifiers = node.specifiers
27
- .map(specifier => {
28
- if (specifier.type === 'ImportSpecifier')
29
- return `import {${specifier.local.name}} from ${source}`
27
+ .map(importSpecifier => {
28
+ if (importSpecifier.type === 'ImportSpecifier')
29
+ return `import {${importSpecifier.local.name}} from ${source}`
30
30
  return null
31
31
  })
32
32
  .filter(Boolean)
@@ -0,0 +1,8 @@
1
+ import type {ImportDeclaration} from 'estree'
2
+ import type {ImportGroup} from './ImportGroup'
3
+
4
+ export interface CategorizedImport {
5
+ declaration: ImportDeclaration
6
+ group: ImportGroup
7
+ sortKey: string
8
+ }
@@ -0,0 +1,6 @@
1
+ import type {ImportDeclaration} from 'estree'
2
+
3
+ export interface ImportError {
4
+ node: ImportDeclaration
5
+ messageId: 'sortedImports' | 'sortedNames' | 'wrongGroup'
6
+ }
@@ -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,4 @@
1
+ export interface ReplacementRange {
2
+ start: number
3
+ end: number
4
+ }
@@ -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,7 @@
1
+ import type {ImportSpecifier} from 'estree'
2
+
3
+ export function getSpecifierName(specifier: ImportSpecifier): string {
4
+ return specifier.imported.type === 'Identifier'
5
+ ? specifier.imported.name
6
+ : String(specifier.imported.value)
7
+ }
@@ -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
+ }
@@ -1,83 +0,0 @@
1
- import type {ImportDeclaration} from 'estree'
2
- import type {Rule} from 'eslint'
3
-
4
- export const sortedImports: Rule.RuleModule = {
5
- meta: {
6
- docs: {
7
- description: 'Enforce sorted imports alphabetically',
8
- recommended: true,
9
- },
10
- fixable: 'code',
11
- messages: {
12
- sortedImports: 'Imports should be sorted alphabetically',
13
- },
14
- schema: [],
15
- type: 'suggestion',
16
- },
17
- create(context) {
18
- return {
19
- Program(node) {
20
- const importDeclarations = node.body.filter(
21
- (statement): statement is ImportDeclaration =>
22
- statement.type === 'ImportDeclaration',
23
- )
24
-
25
- if (importDeclarations.length <= 1) return
26
-
27
- const sortedImports = importDeclarations.sort((a, b) => {
28
- const sourceA = (a.source.value as string).toLowerCase()
29
- const sourceB = (b.source.value as string).toLowerCase()
30
- return sourceA.localeCompare(sourceB)
31
- })
32
-
33
- const isSorted = importDeclarations.every((decl, index) => decl === sortedImports[index])
34
-
35
- if (!isSorted) {
36
- context.report({
37
- node: importDeclarations[0],
38
- messageId: 'sortedImports',
39
- fix(fixer) {
40
- const sourceCode = context.getSourceCode()
41
-
42
- // Find all consecutive import statements at the beginning
43
- const programBody = node.body
44
- let lastImportIndex = 0
45
- for (let i = 0; i < programBody.length; i++) {
46
- if (programBody[i].type === 'ImportDeclaration') {
47
- lastImportIndex = i
48
- } else {
49
- break
50
- }
51
- }
52
-
53
- const firstImport = programBody[0] as import('estree').ImportDeclaration
54
- const lastImport = programBody[lastImportIndex] as import('estree').ImportDeclaration
55
-
56
- // Get the range from start of first import to end of last import
57
- const startRange = firstImport.range![0]
58
- let endRange = lastImport.range![1]
59
-
60
- // Include any trailing whitespace/newlines after the last import
61
- const fullText = sourceCode.getText()
62
- for (let i = endRange; i < fullText.length; i++) {
63
- const char = fullText[i]
64
- if (char === '\n' || char === ' ' || char === '\t') {
65
- endRange++
66
- } else {
67
- break
68
- }
69
- }
70
-
71
- // Generate the sorted import code
72
- const sortedCode = sortedImports.map(decl => {
73
- return sourceCode.getText(decl)
74
- }).join('\n')
75
-
76
- return fixer.replaceTextRange([startRange, endRange], sortedCode)
77
- },
78
- })
79
- }
80
- },
81
- }
82
- },
83
- }