@borela-tech/eslint-config 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +82 -0
- package/README.md +67 -0
- package/dist/index.js +466 -38
- package/dist/index.js.map +1 -1
- package/package.json +8 -1
- package/src/index.ts +9 -0
- package/src/lib/compare.ts +3 -0
- package/src/rules/__tests__/importsAndReExportsAtTop.test.ts +118 -0
- package/src/rules/__tests__/individualReExports.test.ts +62 -0
- package/src/rules/__tests__/sortedImports.test.ts +3 -3
- package/src/rules/__tests__/sortedReExports.test.ts +151 -0
- package/src/rules/importsAndReExportsAtTop/CategorizedStatements.ts +8 -0
- package/src/rules/importsAndReExportsAtTop/ReExport.ts +5 -0
- package/src/rules/importsAndReExportsAtTop/StatementIndices.ts +5 -0
- package/src/rules/importsAndReExportsAtTop/categorizeStatements.ts +27 -0
- package/src/rules/importsAndReExportsAtTop/findFirstIndices.ts +25 -0
- package/src/rules/importsAndReExportsAtTop/generateSortedText.ts +18 -0
- package/src/rules/importsAndReExportsAtTop/getStatementType.ts +17 -0
- package/src/rules/importsAndReExportsAtTop/hasViolation.ts +29 -0
- package/src/rules/importsAndReExportsAtTop/index.ts +45 -0
- package/src/rules/importsAndReExportsAtTop/statementType.ts +4 -0
- package/src/rules/individualImports.ts +5 -14
- package/src/rules/individualReExports.ts +52 -0
- package/src/rules/sortedImports/CategorizedImport.ts +2 -2
- package/src/rules/sortedImports/ImportError.ts +2 -2
- package/src/rules/sortedImports/ImportGroup.ts +5 -1
- package/src/rules/sortedImports/ImportGroupOrder.ts +8 -0
- package/src/rules/sortedImports/areSpecifiersSorted.ts +4 -5
- package/src/rules/sortedImports/categorizeImport.ts +4 -0
- package/src/rules/sortedImports/checkAlphabeticalSorting.ts +4 -3
- package/src/rules/sortedImports/checkGroupOrdering.ts +2 -3
- package/src/rules/sortedImports/createFix/buildSortedCode.ts +2 -2
- package/src/rules/sortedImports/createFix/formatNamedImport.ts +5 -8
- package/src/rules/sortedImports/createFix/getReplacementRange.ts +1 -1
- package/src/rules/sortedImports/createFix/sortImportGroups.ts +5 -4
- package/src/rules/sortedImports/getNamedSpecifiers.ts +3 -4
- package/src/rules/sortedImports/getSortKey.ts +3 -3
- package/src/rules/sortedImports/getSpecifierName.ts +2 -2
- package/src/rules/sortedImports/sortSpecifiersText.ts +7 -6
- package/src/rules/sortedReExports/CategorizedNamedReExport.ts +6 -0
- package/src/rules/sortedReExports/CategorizedReExport.ts +15 -0
- package/src/rules/sortedReExports/ReExportDeclaration.ts +5 -0
- package/src/rules/sortedReExports/ReExportError.ts +6 -0
- package/src/rules/sortedReExports/ReExportGroup.ts +4 -0
- package/src/rules/sortedReExports/ReExportGroupOrder.ts +7 -0
- package/src/rules/sortedReExports/areSpecifiersSorted.ts +9 -0
- package/src/rules/sortedReExports/categorizeReExport.ts +17 -0
- package/src/rules/sortedReExports/categorizeReExports.ts +14 -0
- package/src/rules/sortedReExports/checkAlphabeticalSorting.ts +25 -0
- package/src/rules/sortedReExports/checkGroupOrdering.ts +21 -0
- package/src/rules/sortedReExports/checkSpecifiersSorting.ts +23 -0
- package/src/rules/sortedReExports/createFix/buildSortedCode.ts +28 -0
- package/src/rules/sortedReExports/createFix/findFirstExportIndex.ts +11 -0
- package/src/rules/sortedReExports/createFix/findLastExportIndex.ts +12 -0
- package/src/rules/sortedReExports/createFix/formatNamedReExport.ts +20 -0
- package/src/rules/sortedReExports/createFix/getReplacementRange.ts +22 -0
- package/src/rules/sortedReExports/createFix/groupReExportsByType.ts +17 -0
- package/src/rules/sortedReExports/createFix/index.ts +29 -0
- package/src/rules/sortedReExports/createFix/sortExportGroups.ts +11 -0
- package/src/rules/sortedReExports/getNamedSpecifiers.ts +9 -0
- package/src/rules/sortedReExports/getReExportDeclarations.ts +12 -0
- package/src/rules/sortedReExports/getSortKey.ts +16 -0
- package/src/rules/sortedReExports/getSpecifierName.ts +7 -0
- package/src/rules/sortedReExports/index.ts +54 -0
- package/src/rules/sortedReExports/isNamedReExport.ts +6 -0
- package/src/rules/sortedReExports/sortSpecifiersText.ts +15 -0
- /package/src/{rules/sortedImports/createFix → lib}/ReplacementRange.ts +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {getStatementType} from './getStatementType'
|
|
2
|
+
import {ReExport} from './ReExport'
|
|
3
|
+
import type {CategorizedStatements} from './CategorizedStatements'
|
|
4
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
5
|
+
|
|
6
|
+
export function categorizeStatements(
|
|
7
|
+
statements: TSESTree.Statement[],
|
|
8
|
+
): CategorizedStatements {
|
|
9
|
+
const result: CategorizedStatements = {
|
|
10
|
+
imports: [],
|
|
11
|
+
reExports: [],
|
|
12
|
+
other: [],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
for (const statement of statements) {
|
|
16
|
+
const type = getStatementType(statement)
|
|
17
|
+
|
|
18
|
+
if (type === 'import')
|
|
19
|
+
result.imports.push(statement as TSESTree.ImportDeclaration)
|
|
20
|
+
else if (type === 're-export')
|
|
21
|
+
result.reExports.push(statement as ReExport)
|
|
22
|
+
else
|
|
23
|
+
result.other.push(statement)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return result
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {getStatementType} from './getStatementType'
|
|
2
|
+
import type {StatementIndices} from './StatementIndices'
|
|
3
|
+
import type {StatementType} from './statementType'
|
|
4
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
5
|
+
|
|
6
|
+
export function findFirstIndices(
|
|
7
|
+
statements: TSESTree.Statement[],
|
|
8
|
+
): StatementIndices {
|
|
9
|
+
let firstImport = Infinity
|
|
10
|
+
let firstReExport = Infinity
|
|
11
|
+
let firstOther = -1
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < statements.length; i++) {
|
|
14
|
+
const type: StatementType = getStatementType(statements[i])
|
|
15
|
+
|
|
16
|
+
if (type === 'import' && firstImport === Infinity)
|
|
17
|
+
firstImport = i
|
|
18
|
+
else if (type === 're-export' && firstReExport === Infinity)
|
|
19
|
+
firstReExport = i
|
|
20
|
+
else if (type === 'other' && firstOther === -1)
|
|
21
|
+
firstOther = i
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {firstImport, firstReExport, firstOther}
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {Rule} from 'eslint'
|
|
2
|
+
import type {CategorizedStatements} from './CategorizedStatements'
|
|
3
|
+
import type {Node as ESTreeNode} from 'estree'
|
|
4
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
5
|
+
|
|
6
|
+
export function generateSortedText(
|
|
7
|
+
context: Rule.RuleContext,
|
|
8
|
+
categories: CategorizedStatements,
|
|
9
|
+
): string {
|
|
10
|
+
const allStatements: TSESTree.Node[] = [
|
|
11
|
+
...categories.imports,
|
|
12
|
+
...categories.reExports,
|
|
13
|
+
...categories.other,
|
|
14
|
+
]
|
|
15
|
+
return allStatements.map(
|
|
16
|
+
node => context.sourceCode.getText(node as ESTreeNode),
|
|
17
|
+
).join('\n')
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {StatementType} from './statementType'
|
|
2
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
3
|
+
|
|
4
|
+
export function getStatementType(statement: TSESTree.Statement): StatementType {
|
|
5
|
+
if (statement.type === 'ImportDeclaration')
|
|
6
|
+
return 'import'
|
|
7
|
+
|
|
8
|
+
if (statement.type === 'ExportAllDeclaration')
|
|
9
|
+
return 're-export'
|
|
10
|
+
|
|
11
|
+
if (statement.type === 'ExportNamedDeclaration') {
|
|
12
|
+
if (statement.source !== null)
|
|
13
|
+
return 're-export'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return 'other'
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type {CategorizedStatements} from './CategorizedStatements'
|
|
2
|
+
import type {StatementIndices} from './StatementIndices'
|
|
3
|
+
|
|
4
|
+
export function hasViolation(
|
|
5
|
+
indices: StatementIndices,
|
|
6
|
+
categories: CategorizedStatements,
|
|
7
|
+
): boolean {
|
|
8
|
+
const {
|
|
9
|
+
firstImport,
|
|
10
|
+
firstReExport,
|
|
11
|
+
firstOther,
|
|
12
|
+
} = indices
|
|
13
|
+
|
|
14
|
+
// No imports or no re-exports.
|
|
15
|
+
if (categories.imports.length === 0 || categories.reExports.length === 0)
|
|
16
|
+
return false
|
|
17
|
+
|
|
18
|
+
const firstImportOrReExport = Math.min(firstImport, firstReExport)
|
|
19
|
+
const hasOtherBeforeImportOrReExport =
|
|
20
|
+
firstOther !== -1 && firstOther < firstImportOrReExport
|
|
21
|
+
|
|
22
|
+
// Violation if:
|
|
23
|
+
// 1. Other statements appear before imports/re-exports.
|
|
24
|
+
// 2. Re-exports appear before imports.
|
|
25
|
+
if (hasOtherBeforeImportOrReExport || firstImport > firstReExport)
|
|
26
|
+
return true
|
|
27
|
+
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {categorizeStatements} from './categorizeStatements'
|
|
2
|
+
import {findFirstIndices} from './findFirstIndices'
|
|
3
|
+
import {generateSortedText} from './generateSortedText'
|
|
4
|
+
import {hasViolation} from './hasViolation'
|
|
5
|
+
import type {Rule} from 'eslint'
|
|
6
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
7
|
+
|
|
8
|
+
export const importsAndReExportsAtTop: Rule.RuleModule = {
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'suggestion',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Enforce imports and re-exports at the top of the file',
|
|
13
|
+
recommended: false,
|
|
14
|
+
},
|
|
15
|
+
fixable: 'code',
|
|
16
|
+
messages: {
|
|
17
|
+
importsAndReExportsAtTop:
|
|
18
|
+
'Imports and re-exports should be at the top of the file.',
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
Program(node) {
|
|
26
|
+
const statements = node.body as TSESTree.Statement[]
|
|
27
|
+
const categories = categorizeStatements(statements)
|
|
28
|
+
const indices = findFirstIndices(statements)
|
|
29
|
+
|
|
30
|
+
if (!hasViolation(indices, categories))
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
messageId: 'importsAndReExportsAtTop',
|
|
36
|
+
|
|
37
|
+
fix(fixer) {
|
|
38
|
+
const sortedText = generateSortedText(context, categories)
|
|
39
|
+
return fixer.replaceText(node, sortedText)
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}
|
|
@@ -18,26 +18,17 @@ export const individualImports: Rule.RuleModule = {
|
|
|
18
18
|
ImportDeclaration(node) {
|
|
19
19
|
if (node.specifiers.length <= 1)
|
|
20
20
|
return
|
|
21
|
+
|
|
21
22
|
context.report({
|
|
22
23
|
node,
|
|
23
24
|
messageId: 'individualImports',
|
|
24
25
|
fix(fixer) {
|
|
25
26
|
const source = node.source.raw
|
|
26
27
|
const specifiers = node.specifiers
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
)
|
|
28
|
+
.filter(s => s.type === 'ImportSpecifier')
|
|
29
|
+
.map(s => `import {${s.local.name}} from ${source}`)
|
|
30
|
+
.join('\n')
|
|
31
|
+
return fixer.replaceText(node, specifiers)
|
|
41
32
|
},
|
|
42
33
|
})
|
|
43
34
|
},
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type {Rule} from 'eslint'
|
|
2
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
3
|
+
|
|
4
|
+
export const individualReExports: Rule.RuleModule = {
|
|
5
|
+
meta: {
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'Enforce individual exports instead of grouped exports',
|
|
8
|
+
recommended: true,
|
|
9
|
+
},
|
|
10
|
+
fixable: 'code',
|
|
11
|
+
messages: {
|
|
12
|
+
individualReExports: 'Use individual exports instead of grouped exports.',
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
type: 'suggestion',
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
ExportNamedDeclaration(node) {
|
|
20
|
+
const exportNode = node as TSESTree.ExportNamedDeclaration
|
|
21
|
+
if (!exportNode.source || exportNode.specifiers.length <= 1)
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
context.report({
|
|
25
|
+
node,
|
|
26
|
+
messageId: 'individualReExports',
|
|
27
|
+
fix(fixer) {
|
|
28
|
+
const source = exportNode.source!.value
|
|
29
|
+
const typeKeyword = exportNode.exportKind === 'type'
|
|
30
|
+
? 'type '
|
|
31
|
+
: ''
|
|
32
|
+
const specifiers = exportNode.specifiers
|
|
33
|
+
.map(s => {
|
|
34
|
+
const localName = s.local.type === 'Identifier'
|
|
35
|
+
? s.local.name
|
|
36
|
+
: s.local.value
|
|
37
|
+
const exportedName = s.exported.type === 'Identifier'
|
|
38
|
+
? s.exported.name
|
|
39
|
+
: s.exported.value
|
|
40
|
+
const name = localName === exportedName
|
|
41
|
+
? localName
|
|
42
|
+
: `${localName} as ${exportedName}`
|
|
43
|
+
return `export ${typeKeyword}{${name}} from '${source}'`
|
|
44
|
+
})
|
|
45
|
+
.join('\n')
|
|
46
|
+
return fixer.replaceText(node, specifiers)
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {ImportDeclaration} from 'estree'
|
|
2
1
|
import type {ImportGroup} from './ImportGroup'
|
|
2
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
3
3
|
|
|
4
4
|
export interface CategorizedImport {
|
|
5
|
-
declaration: ImportDeclaration
|
|
5
|
+
declaration: TSESTree.ImportDeclaration
|
|
6
6
|
group: ImportGroup
|
|
7
7
|
sortKey: string
|
|
8
8
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import {compare} from '../../lib/compare'
|
|
1
2
|
import {getSpecifierName} from './getSpecifierName'
|
|
2
|
-
import type {
|
|
3
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
3
4
|
|
|
4
|
-
export function areSpecifiersSorted(specifiers: ImportSpecifier[]): boolean {
|
|
5
|
+
export function areSpecifiersSorted(specifiers: TSESTree.ImportSpecifier[]): boolean {
|
|
5
6
|
const names = specifiers.map(s => getSpecifierName(s))
|
|
6
|
-
const sorted = [...names].sort((a, b) =>
|
|
7
|
-
a.toLowerCase().localeCompare(b.toLowerCase()),
|
|
8
|
-
)
|
|
7
|
+
const sorted = [...names].sort((a, b) => compare(a, b))
|
|
9
8
|
return names.every((name, i) => name === sorted[i])
|
|
10
9
|
}
|
|
@@ -2,14 +2,18 @@ import type {ImportGroup} from './ImportGroup'
|
|
|
2
2
|
import type {TSESTree} from '@typescript-eslint/types'
|
|
3
3
|
|
|
4
4
|
export function categorizeImport(declaration: TSESTree.ImportDeclaration): ImportGroup {
|
|
5
|
+
// Example: import type {Type} from 'module'
|
|
5
6
|
if (declaration.importKind === 'type')
|
|
6
7
|
return 'type'
|
|
7
8
|
|
|
9
|
+
// Example: import 'module'
|
|
8
10
|
if (declaration.specifiers.length === 0)
|
|
9
11
|
return 'side-effect'
|
|
10
12
|
|
|
13
|
+
// Example: import value from 'module'
|
|
11
14
|
if (declaration.specifiers.some(s => s.type === 'ImportDefaultSpecifier'))
|
|
12
15
|
return 'default'
|
|
13
16
|
|
|
17
|
+
// Example: import {value} from 'module'
|
|
14
18
|
return 'named'
|
|
15
19
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import {compare} from '../../lib/compare'
|
|
2
|
+
import {importGroupOrder} from './ImportGroupOrder'
|
|
1
3
|
import type {CategorizedImport} from './CategorizedImport'
|
|
2
4
|
import type {ImportError} from './ImportError'
|
|
3
|
-
import type {ImportGroup} from './ImportGroup'
|
|
4
5
|
|
|
5
6
|
export function checkAlphabeticalSorting(categorized: CategorizedImport[]): ImportError[] {
|
|
6
7
|
const errors: ImportError[] = []
|
|
7
8
|
|
|
8
|
-
for (const group of
|
|
9
|
+
for (const group of importGroupOrder) {
|
|
9
10
|
const groupImports = categorized.filter(c => c.group === group)
|
|
10
|
-
const sorted = [...groupImports].sort((a, b) => a.sortKey
|
|
11
|
+
const sorted = [...groupImports].sort((a, b) => compare(a.sortKey, b.sortKey))
|
|
11
12
|
for (let i = 0; i < groupImports.length; i++) {
|
|
12
13
|
if (groupImports[i] !== sorted[i]) {
|
|
13
14
|
errors.push({
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
import {importGroupOrder} from './ImportGroupOrder'
|
|
1
2
|
import type {CategorizedImport} from './CategorizedImport'
|
|
2
3
|
import type {ImportError} from './ImportError'
|
|
3
|
-
import type {ImportGroup} from './ImportGroup'
|
|
4
4
|
|
|
5
5
|
export function checkGroupOrdering(categorized: CategorizedImport[]): ImportError[] {
|
|
6
|
-
const groupOrder: ImportGroup[] = ['side-effect', 'default', 'named', 'type']
|
|
7
6
|
const errors: ImportError[] = []
|
|
8
7
|
|
|
9
8
|
let currentGroupIndex = -1
|
|
10
9
|
for (const {declaration, group} of categorized) {
|
|
11
|
-
const groupIndex =
|
|
10
|
+
const groupIndex = importGroupOrder.indexOf(group)
|
|
12
11
|
if (groupIndex < currentGroupIndex) {
|
|
13
12
|
errors.push({
|
|
14
13
|
node: declaration,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {formatNamedImport} from './formatNamedImport'
|
|
2
|
+
import {importGroupOrder} from '../ImportGroupOrder'
|
|
2
3
|
import type {CategorizedImport} from '../CategorizedImport'
|
|
3
4
|
import type {ImportGroup} from '../ImportGroup'
|
|
4
5
|
|
|
@@ -6,10 +7,9 @@ export function buildSortedCode(
|
|
|
6
7
|
grouped: Record<ImportGroup, CategorizedImport[]>,
|
|
7
8
|
sourceCode: {getText: (node?: unknown) => string},
|
|
8
9
|
): string[] {
|
|
9
|
-
const groupOrder: ImportGroup[] = ['side-effect', 'default', 'named', 'type']
|
|
10
10
|
const sortedCode: string[] = []
|
|
11
11
|
|
|
12
|
-
for (const group of
|
|
12
|
+
for (const group of importGroupOrder) {
|
|
13
13
|
for (const {declaration} of grouped[group]) {
|
|
14
14
|
if (group === 'named' || group === 'type')
|
|
15
15
|
sortedCode.push(formatNamedImport(declaration, sourceCode))
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import {areSpecifiersSorted} from '../areSpecifiersSorted'
|
|
2
2
|
import {getNamedSpecifiers} from '../getNamedSpecifiers'
|
|
3
3
|
import {sortSpecifiersText} from '../sortSpecifiersText'
|
|
4
|
-
import type {
|
|
4
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
5
5
|
|
|
6
6
|
export function formatNamedImport(
|
|
7
|
-
declaration: ImportDeclaration,
|
|
7
|
+
declaration: TSESTree.ImportDeclaration,
|
|
8
8
|
sourceCode: {getText: (node?: unknown) => string},
|
|
9
9
|
): string {
|
|
10
10
|
const specifiers = getNamedSpecifiers(declaration)
|
|
11
11
|
|
|
12
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
13
|
const sortedSpecifiers = sortSpecifiersText(specifiers, sourceCode)
|
|
19
|
-
|
|
14
|
+
const source = declaration.source.value
|
|
15
|
+
const prefix = declaration.importKind === 'type' ? 'import type ' : 'import '
|
|
16
|
+
return `${prefix}{${sortedSpecifiers}} from '${source}'`
|
|
20
17
|
}
|
|
21
18
|
|
|
22
19
|
return sourceCode.getText(declaration)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {findLastImportIndex} from './findLastImportIndex'
|
|
2
|
-
import type {ReplacementRange} from '
|
|
2
|
+
import type {ReplacementRange} from '../../../lib/ReplacementRange'
|
|
3
3
|
import type {TSESTree} from '@typescript-eslint/types'
|
|
4
4
|
|
|
5
5
|
export function getReplacementRange(
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import {compare} from '../../../lib/compare'
|
|
1
2
|
import type {CategorizedImport} from '../CategorizedImport'
|
|
2
3
|
import type {ImportGroup} from '../ImportGroup'
|
|
3
4
|
|
|
4
5
|
export function sortImportGroups(
|
|
5
6
|
grouped: Record<ImportGroup, CategorizedImport[]>,
|
|
6
7
|
): void {
|
|
7
|
-
grouped['side-effect'].sort((a, b) => a.sortKey
|
|
8
|
-
grouped['default'].sort((a, b) => a.sortKey
|
|
9
|
-
grouped['named'].sort((a, b) => a.sortKey
|
|
10
|
-
grouped['type'].sort((a, b) => a.sortKey
|
|
8
|
+
grouped['side-effect'].sort((a, b) => compare(a.sortKey, b.sortKey))
|
|
9
|
+
grouped['default'].sort((a, b) => compare(a.sortKey, b.sortKey))
|
|
10
|
+
grouped['named'].sort((a, b) => compare(a.sortKey, b.sortKey))
|
|
11
|
+
grouped['type'].sort((a, b) => compare(a.sortKey, b.sortKey))
|
|
11
12
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {ImportSpecifier} from 'estree'
|
|
1
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
3
2
|
|
|
4
|
-
export function getNamedSpecifiers(declaration: ImportDeclaration): ImportSpecifier[] {
|
|
3
|
+
export function getNamedSpecifiers(declaration: TSESTree.ImportDeclaration): TSESTree.ImportSpecifier[] {
|
|
5
4
|
return declaration.specifiers.filter(
|
|
6
|
-
(s): s is ImportSpecifier => s.type === 'ImportSpecifier',
|
|
5
|
+
(s): s is TSESTree.ImportSpecifier => s.type === 'ImportSpecifier',
|
|
7
6
|
)
|
|
8
7
|
}
|
|
@@ -5,16 +5,16 @@ export function getSortKey(declaration: TSESTree.ImportDeclaration): string {
|
|
|
5
5
|
const group = categorizeImport(declaration)
|
|
6
6
|
|
|
7
7
|
if (group === 'side-effect')
|
|
8
|
-
return
|
|
8
|
+
return declaration.source.value
|
|
9
9
|
|
|
10
10
|
if (group === 'default') {
|
|
11
11
|
const defaultSpecifier = declaration.specifiers.find(
|
|
12
12
|
s => s.type === 'ImportDefaultSpecifier',
|
|
13
13
|
) as TSESTree.ImportDefaultSpecifier | undefined
|
|
14
14
|
|
|
15
|
-
return defaultSpecifier?.local.name
|
|
15
|
+
return defaultSpecifier?.local.name ?? ''
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const specifier = declaration.specifiers[0]
|
|
19
|
-
return specifier.local.name
|
|
19
|
+
return specifier.local.name
|
|
20
20
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
2
2
|
|
|
3
|
-
export function getSpecifierName(specifier: ImportSpecifier): string {
|
|
3
|
+
export function getSpecifierName(specifier: TSESTree.ImportSpecifier): string {
|
|
4
4
|
return specifier.imported.type === 'Identifier'
|
|
5
5
|
? specifier.imported.name
|
|
6
6
|
: String(specifier.imported.value)
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import {compare} from '../../lib/compare'
|
|
1
2
|
import {getSpecifierName} from './getSpecifierName'
|
|
2
|
-
import type {
|
|
3
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
3
4
|
|
|
4
5
|
export function sortSpecifiersText(
|
|
5
|
-
specifiers: ImportSpecifier[],
|
|
6
|
-
sourceCode: {getText: (node: ImportSpecifier) => string},
|
|
6
|
+
specifiers: TSESTree.ImportSpecifier[],
|
|
7
|
+
sourceCode: {getText: (node: TSESTree.ImportSpecifier) => string},
|
|
7
8
|
): string {
|
|
8
9
|
const sorted = [...specifiers].sort((a, b) => {
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
return
|
|
10
|
+
const nameA = getSpecifierName(a)
|
|
11
|
+
const nameB = getSpecifierName(b)
|
|
12
|
+
return compare(nameA, nameB)
|
|
12
13
|
})
|
|
13
14
|
return sorted.map(s => sourceCode.getText(s)).join(', ')
|
|
14
15
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {TSESTree} from '@typescript-eslint/types'
|
|
2
|
+
|
|
3
|
+
interface NamedReExport {
|
|
4
|
+
declaration: TSESTree.ExportNamedDeclaration
|
|
5
|
+
group: 're-export-named' | 're-export-type'
|
|
6
|
+
sortKey: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ReExportAll {
|
|
10
|
+
declaration: TSESTree.ExportAllDeclaration
|
|
11
|
+
group: 're-export-all'
|
|
12
|
+
sortKey: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type CategorizedReExport = NamedReExport | ReExportAll
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import {compare} from '../../lib/compare'
|
|
2
|
+
import {getSpecifierName} from './getSpecifierName'
|
|
3
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
4
|
+
|
|
5
|
+
export function areSpecifiersSorted(specifiers: TSESTree.ExportSpecifier[]): boolean {
|
|
6
|
+
const names = specifiers.map(s => getSpecifierName(s))
|
|
7
|
+
const sorted = [...names].sort((a, b) => compare(a, b))
|
|
8
|
+
return names.every((name, i) => name === sorted[i])
|
|
9
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type {ReExportGroup} from './ReExportGroup'
|
|
2
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
3
|
+
|
|
4
|
+
export function categorizeReExport(
|
|
5
|
+
declaration: TSESTree.ExportNamedDeclaration | TSESTree.ExportAllDeclaration,
|
|
6
|
+
): ReExportGroup {
|
|
7
|
+
// Example: export * from 'module'
|
|
8
|
+
if (declaration.type === 'ExportAllDeclaration')
|
|
9
|
+
return 're-export-all'
|
|
10
|
+
|
|
11
|
+
// Example: export type {Type} from 'module'
|
|
12
|
+
if (declaration.exportKind === 'type')
|
|
13
|
+
return 're-export-type'
|
|
14
|
+
|
|
15
|
+
// Example: export {value} from 'module'
|
|
16
|
+
return 're-export-named'
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {categorizeReExport} from './categorizeReExport'
|
|
2
|
+
import {getSortKey} from './getSortKey'
|
|
3
|
+
import {ReExportDeclaration} from './ReExportDeclaration'
|
|
4
|
+
import type {CategorizedReExport} from './CategorizedReExport'
|
|
5
|
+
|
|
6
|
+
export function categorizeReExports(declarations: ReExportDeclaration[]): CategorizedReExport[] {
|
|
7
|
+
return declarations.map(declaration => {
|
|
8
|
+
return {
|
|
9
|
+
declaration,
|
|
10
|
+
group: categorizeReExport(declaration),
|
|
11
|
+
sortKey: getSortKey(declaration),
|
|
12
|
+
} as CategorizedReExport
|
|
13
|
+
})
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {compare} from '../../lib/compare'
|
|
2
|
+
import {reExportGroupOrder} from './ReExportGroupOrder'
|
|
3
|
+
import type {CategorizedReExport} from './CategorizedReExport'
|
|
4
|
+
import type {ReExportError} from './ReExportError'
|
|
5
|
+
|
|
6
|
+
export function checkAlphabeticalSorting(categorized: CategorizedReExport[]): ReExportError[] {
|
|
7
|
+
const errors: ReExportError[] = []
|
|
8
|
+
|
|
9
|
+
for (const group of reExportGroupOrder) {
|
|
10
|
+
const groupReExports = categorized.filter(c => c.group === group)
|
|
11
|
+
const sorted = [...groupReExports].sort((a, b) =>
|
|
12
|
+
compare(a.sortKey, b.sortKey),
|
|
13
|
+
)
|
|
14
|
+
for (let i = 0; i < groupReExports.length; i++) {
|
|
15
|
+
if (groupReExports[i] !== sorted[i]) {
|
|
16
|
+
errors.push({
|
|
17
|
+
node: groupReExports[i].declaration,
|
|
18
|
+
messageId: 'sortedReExports',
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return errors
|
|
25
|
+
}
|