Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sort-exports): #272 and #273: adds partitionByNewLine and groupKind #274

Merged
merged 2 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading