@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.
Files changed (67) hide show
  1. package/.github/workflows/ci.yml +82 -0
  2. package/README.md +67 -0
  3. package/dist/index.js +466 -38
  4. package/dist/index.js.map +1 -1
  5. package/package.json +8 -1
  6. package/src/index.ts +9 -0
  7. package/src/lib/compare.ts +3 -0
  8. package/src/rules/__tests__/importsAndReExportsAtTop.test.ts +118 -0
  9. package/src/rules/__tests__/individualReExports.test.ts +62 -0
  10. package/src/rules/__tests__/sortedImports.test.ts +3 -3
  11. package/src/rules/__tests__/sortedReExports.test.ts +151 -0
  12. package/src/rules/importsAndReExportsAtTop/CategorizedStatements.ts +8 -0
  13. package/src/rules/importsAndReExportsAtTop/ReExport.ts +5 -0
  14. package/src/rules/importsAndReExportsAtTop/StatementIndices.ts +5 -0
  15. package/src/rules/importsAndReExportsAtTop/categorizeStatements.ts +27 -0
  16. package/src/rules/importsAndReExportsAtTop/findFirstIndices.ts +25 -0
  17. package/src/rules/importsAndReExportsAtTop/generateSortedText.ts +18 -0
  18. package/src/rules/importsAndReExportsAtTop/getStatementType.ts +17 -0
  19. package/src/rules/importsAndReExportsAtTop/hasViolation.ts +29 -0
  20. package/src/rules/importsAndReExportsAtTop/index.ts +45 -0
  21. package/src/rules/importsAndReExportsAtTop/statementType.ts +4 -0
  22. package/src/rules/individualImports.ts +5 -14
  23. package/src/rules/individualReExports.ts +52 -0
  24. package/src/rules/sortedImports/CategorizedImport.ts +2 -2
  25. package/src/rules/sortedImports/ImportError.ts +2 -2
  26. package/src/rules/sortedImports/ImportGroup.ts +5 -1
  27. package/src/rules/sortedImports/ImportGroupOrder.ts +8 -0
  28. package/src/rules/sortedImports/areSpecifiersSorted.ts +4 -5
  29. package/src/rules/sortedImports/categorizeImport.ts +4 -0
  30. package/src/rules/sortedImports/checkAlphabeticalSorting.ts +4 -3
  31. package/src/rules/sortedImports/checkGroupOrdering.ts +2 -3
  32. package/src/rules/sortedImports/createFix/buildSortedCode.ts +2 -2
  33. package/src/rules/sortedImports/createFix/formatNamedImport.ts +5 -8
  34. package/src/rules/sortedImports/createFix/getReplacementRange.ts +1 -1
  35. package/src/rules/sortedImports/createFix/sortImportGroups.ts +5 -4
  36. package/src/rules/sortedImports/getNamedSpecifiers.ts +3 -4
  37. package/src/rules/sortedImports/getSortKey.ts +3 -3
  38. package/src/rules/sortedImports/getSpecifierName.ts +2 -2
  39. package/src/rules/sortedImports/sortSpecifiersText.ts +7 -6
  40. package/src/rules/sortedReExports/CategorizedNamedReExport.ts +6 -0
  41. package/src/rules/sortedReExports/CategorizedReExport.ts +15 -0
  42. package/src/rules/sortedReExports/ReExportDeclaration.ts +5 -0
  43. package/src/rules/sortedReExports/ReExportError.ts +6 -0
  44. package/src/rules/sortedReExports/ReExportGroup.ts +4 -0
  45. package/src/rules/sortedReExports/ReExportGroupOrder.ts +7 -0
  46. package/src/rules/sortedReExports/areSpecifiersSorted.ts +9 -0
  47. package/src/rules/sortedReExports/categorizeReExport.ts +17 -0
  48. package/src/rules/sortedReExports/categorizeReExports.ts +14 -0
  49. package/src/rules/sortedReExports/checkAlphabeticalSorting.ts +25 -0
  50. package/src/rules/sortedReExports/checkGroupOrdering.ts +21 -0
  51. package/src/rules/sortedReExports/checkSpecifiersSorting.ts +23 -0
  52. package/src/rules/sortedReExports/createFix/buildSortedCode.ts +28 -0
  53. package/src/rules/sortedReExports/createFix/findFirstExportIndex.ts +11 -0
  54. package/src/rules/sortedReExports/createFix/findLastExportIndex.ts +12 -0
  55. package/src/rules/sortedReExports/createFix/formatNamedReExport.ts +20 -0
  56. package/src/rules/sortedReExports/createFix/getReplacementRange.ts +22 -0
  57. package/src/rules/sortedReExports/createFix/groupReExportsByType.ts +17 -0
  58. package/src/rules/sortedReExports/createFix/index.ts +29 -0
  59. package/src/rules/sortedReExports/createFix/sortExportGroups.ts +11 -0
  60. package/src/rules/sortedReExports/getNamedSpecifiers.ts +9 -0
  61. package/src/rules/sortedReExports/getReExportDeclarations.ts +12 -0
  62. package/src/rules/sortedReExports/getSortKey.ts +16 -0
  63. package/src/rules/sortedReExports/getSpecifierName.ts +7 -0
  64. package/src/rules/sortedReExports/index.ts +54 -0
  65. package/src/rules/sortedReExports/isNamedReExport.ts +6 -0
  66. package/src/rules/sortedReExports/sortSpecifiersText.ts +15 -0
  67. /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
+ }
@@ -0,0 +1,4 @@
1
+ export type StatementType =
2
+ | 'import'
3
+ | 'other'
4
+ | 're-export'
@@ -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
- .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
- )
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,6 +1,6 @@
1
- import type {ImportDeclaration} from 'estree'
1
+ import type {TSESTree} from '@typescript-eslint/types'
2
2
 
3
3
  export interface ImportError {
4
- node: ImportDeclaration
4
+ node: TSESTree.ImportDeclaration
5
5
  messageId: 'sortedImports' | 'sortedNames' | 'wrongGroup'
6
6
  }
@@ -1 +1,5 @@
1
- export type ImportGroup = 'side-effect' | 'default' | 'named' | 'type'
1
+ export type ImportGroup =
2
+ | 'side-effect'
3
+ | 'default'
4
+ | 'named'
5
+ | 'type'
@@ -0,0 +1,8 @@
1
+ import {ImportGroup} from './ImportGroup'
2
+
3
+ export const importGroupOrder: ImportGroup[] = [
4
+ 'side-effect',
5
+ 'default',
6
+ 'named',
7
+ 'type',
8
+ ]
@@ -1,10 +1,9 @@
1
+ import {compare} from '../../lib/compare'
1
2
  import {getSpecifierName} from './getSpecifierName'
2
- import type {ImportSpecifier} from 'estree'
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 ['side-effect', 'default', 'named', 'type'] as ImportGroup[]) {
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.localeCompare(b.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 = groupOrder.indexOf(group)
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 groupOrder) {
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 {ImportDeclaration} from 'estree'
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
- return before + ' ' + sortedSpecifiers + ' ' + after
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 './ReplacementRange'
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.localeCompare(b.sortKey))
8
- grouped['default'].sort((a, b) => a.sortKey.localeCompare(b.sortKey))
9
- grouped['named'].sort((a, b) => a.sortKey.localeCompare(b.sortKey))
10
- grouped['type'].sort((a, b) => a.sortKey.localeCompare(b.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 {ImportDeclaration} from 'estree'
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 (declaration.source.value as string).toLowerCase()
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.toLowerCase() ?? ''
15
+ return defaultSpecifier?.local.name ?? ''
16
16
  }
17
17
 
18
18
  const specifier = declaration.specifiers[0]
19
- return specifier.local.name.toLowerCase()
19
+ return specifier.local.name
20
20
  }
@@ -1,6 +1,6 @@
1
- import type {ImportSpecifier} from 'estree'
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 {ImportSpecifier} from 'estree'
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 lowerA = getSpecifierName(a).toLowerCase()
10
- const lowerB = getSpecifierName(b).toLowerCase()
11
- return lowerA.localeCompare(lowerB)
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,6 @@
1
+ import type {CategorizedReExport} from './CategorizedReExport'
2
+
3
+ export type CategorizedNamedReExport = Extract<
4
+ CategorizedReExport,
5
+ {declaration: {type: 'ExportNamedDeclaration'}}
6
+ >
@@ -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,5 @@
1
+ import type {TSESTree} from '@typescript-eslint/types'
2
+
3
+ export type ReExportDeclaration =
4
+ | TSESTree.ExportAllDeclaration
5
+ | TSESTree.ExportNamedDeclaration
@@ -0,0 +1,6 @@
1
+ import type {ReExportDeclaration} from './ReExportDeclaration'
2
+
3
+ export interface ReExportError {
4
+ node: ReExportDeclaration
5
+ messageId: 'sortedReExports' | 'sortedNames' | 'wrongGroup'
6
+ }
@@ -0,0 +1,4 @@
1
+ export type ReExportGroup =
2
+ | 're-export-all'
3
+ | 're-export-named'
4
+ | 're-export-type'
@@ -0,0 +1,7 @@
1
+ import {ReExportGroup} from './ReExportGroup'
2
+
3
+ export const reExportGroupOrder: ReExportGroup[] = [
4
+ 're-export-all',
5
+ 're-export-named',
6
+ 're-export-type',
7
+ ]
@@ -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
+ }