Skip to content

Commit

Permalink
feat: add partition by new line and group kind in sort-exports
Browse files Browse the repository at this point in the history
  • Loading branch information
hugop95 authored Sep 19, 2024
1 parent 6f8cbd6 commit 4db2c5e
Show file tree
Hide file tree
Showing 3 changed files with 396 additions and 15 deletions.
32 changes: 32 additions & 0 deletions docs/content/rules/sort-exports.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,34 @@ Controls whether sorting should be case-sensitive or not.
- `true` — Ignore case when sorting alphabetically or naturally (e.g., “A” and “a” are the same).
- `false` — Consider case when sorting (e.g., “A” comes before “a”).

### partitionByNewLine

<sub>default: `false`</sub>

When `true`, the rule will not sort the exports if there is an empty line between them. This can be useful for keeping logically separated groups of exports in their defined order.

```js
// Group 1
export * from "./atoms";
export * from "./organisms";
export * from "./shared";

// Group 2
export { Named } from './folder';
export { AnotherNamed } from './second-folder';
```

### groupKind

<sub>default: `'mixed'`</sub>

Allows you to group exports by their kind, determining whether value exports should come before or after type exports.

- `mixed` — Do not group named exports by their kind; export statements are sorted together regardless of their type.
- `values-first` — Group all value exports before type exports.
- `types-first` — Group all type exports before value exports.


## Usage

<CodeTabs
Expand All @@ -127,6 +155,8 @@ Controls whether sorting should be case-sensitive or not.
type: 'alphabetical',
order: 'asc',
ignoreCase: true,
partitionByNewLine: false,
groupKind: 'mixed',
},
],
},
Expand All @@ -150,6 +180,8 @@ Controls whether sorting should be case-sensitive or not.
type: 'alphabetical',
order: 'asc',
ignoreCase: true,
partitionByNewLine: false,
groupKind: 'mixed',
},
],
},
Expand Down
79 changes: 64 additions & 15 deletions rules/sort-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,31 @@ import type { TSESTree } from '@typescript-eslint/types'
import type { SortingNode } from '../typings'

import { createEslintRule } from '../utils/create-eslint-rule'
import { getLinesBetween } from '../utils/get-lines-between'
import { getSourceCode } from '../utils/get-source-code'
import { rangeToDiff } from '../utils/range-to-diff'
import { getSettings } from '../utils/get-settings'
import { isPositive } from '../utils/is-positive'
import { sortNodes } from '../utils/sort-nodes'
import { makeFixes } from '../utils/make-fixes'
import { complete } from '../utils/complete'
import { pairwise } from '../utils/pairwise'
import { compare } from '../utils/compare'

type MESSAGE_ID = 'unexpectedExportsOrder'

type Options = [
Partial<{
groupKind: 'values-first' | 'types-first' | 'mixed'
type: 'alphabetical' | 'line-length' | 'natural'
partitionByNewLine: boolean
order: 'desc' | 'asc'
ignoreCase: boolean
}>,
]

type SortExportsSortingNode = SortingNode<
TSESTree.ExportNamedDeclarationWithSource | TSESTree.ExportAllDeclaration
>

export default createEslintRule<Options, MESSAGE_ID>({
name: 'sort-exports',
meta: {
Expand Down Expand Up @@ -51,6 +56,16 @@ export default createEslintRule<Options, MESSAGE_ID>({
'Controls whether sorting should be case-sensitive or not.',
type: 'boolean',
},
partitionByNewLine: {
description:
'Allows to use spaces to separate the nodes into logical groups.',
type: 'boolean',
},
groupKind: {
description: 'Specifies top-level groups.',
type: 'string',
enum: ['mixed', 'values-first', 'types-first'],
},
},
additionalProperties: false,
},
Expand All @@ -64,6 +79,8 @@ export default createEslintRule<Options, MESSAGE_ID>({
type: 'alphabetical',
order: 'asc',
ignoreCase: true,
partitionByNewLine: false,
groupKind: 'mixed',
},
],
create: context => {
Expand All @@ -73,20 +90,33 @@ export default createEslintRule<Options, MESSAGE_ID>({
type: 'alphabetical',
ignoreCase: true,
order: 'asc',
partitionByNewLine: false,
groupKind: 'mixed',
} as const)

let parts: SortingNode[][] = [[]]
let sourceCode = getSourceCode(context)

let parts: SortExportsSortingNode[][] = [[]]

let registerNode = (
node:
| TSESTree.ExportNamedDeclarationWithSource
| TSESTree.ExportAllDeclaration,
) => {
parts.at(-1)!.push({
let sortingNode: SortExportsSortingNode = {
size: rangeToDiff(node.range),
name: node.source.value,
node,
})
}
let lastNode = parts.at(-1)?.at(-1)
if (
options.partitionByNewLine &&
lastNode &&
getLinesBetween(sourceCode, lastNode, sortingNode)
) {
parts.push([])
}
parts.at(-1)!.push(sortingNode)
}

return {
Expand All @@ -97,25 +127,44 @@ export default createEslintRule<Options, MESSAGE_ID>({
}
},
'Program:exit': () => {
let sourceCode = getSourceCode(context)

for (let nodes of parts) {
let groupedByKind
if (options.groupKind !== 'mixed') {
groupedByKind = nodes.reduce<SortExportsSortingNode[][]>(
(accumulator, currentNode) => {
let exportTypeIndex =
options.groupKind === 'types-first' ? 0 : 1
let exportIndex = options.groupKind === 'types-first' ? 1 : 0
if (currentNode.node.exportKind === 'value') {
accumulator[exportIndex].push(currentNode)
} else {
accumulator[exportTypeIndex].push(currentNode)
}
return accumulator
},
[[], []],
)
} else {
groupedByKind = [nodes]
}

let sortedNodes: SortingNode[] = []
for (let nodesByKind of groupedByKind) {
sortedNodes = [...sortedNodes, ...sortNodes(nodesByKind, options)]
}

pairwise(nodes, (left, right) => {
if (isPositive(compare(left, right, options))) {
let indexOfLeft = sortedNodes.indexOf(left)
let indexOfRight = sortedNodes.indexOf(right)
if (indexOfLeft > indexOfRight) {
context.report({
messageId: 'unexpectedExportsOrder',
data: {
left: left.name,
right: right.name,
},
node: right.node,
fix: fixer =>
makeFixes(
fixer,
nodes,
sortNodes(nodes, options),
sourceCode,
),
fix: fixer => makeFixes(fixer, nodes, sortedNodes, sourceCode),
})
}
})
Expand Down
Loading

0 comments on commit 4db2c5e

Please sign in to comment.