Skip to content

Commit ac7e597

Browse files
committed
feat: add sort-export-attributes rule
1 parent d147c56 commit ac7e597

File tree

6 files changed

+1815
-0
lines changed

6 files changed

+1815
-0
lines changed
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
---
2+
title: sort-export-attributes
3+
description: Keep export attributes consistently ordered for clearer, more maintainable exports. This rule sorts attributes defined with the export attributes syntax
4+
shortDescription: Enforce sorted export attributes
5+
keywords:
6+
- eslint
7+
- sort export attributes
8+
- export attributes
9+
- eslint rule
10+
- coding standards
11+
- code quality
12+
- javascript linting
13+
---
14+
15+
import CodeExample from '../../components/CodeExample.svelte'
16+
import CodeTabs from '../../components/CodeTabs.svelte'
17+
import dedent from 'dedent'
18+
19+
Enforce sorted export attributes.
20+
21+
This rule keeps attributes inside `export ... with { ... }` consistently ordered. It improves readability, makes diffs smaller, and keeps attribute groups tidy in larger files.
22+
23+
## Try it out
24+
25+
<CodeExample
26+
alphabetical={dedent`
27+
export { data } from 'lib' with {
28+
integrity: 'sha256-...',
29+
mode: 'no-cors',
30+
type: 'json',
31+
}
32+
`}
33+
lineLength={dedent`
34+
export { data } from 'lib' with {
35+
integrity: 'sha256-...',
36+
mode: 'no-cors',
37+
type: 'json',
38+
}
39+
`}
40+
initial={dedent`
41+
export { data } from 'lib' with {
42+
mode: 'no-cors',
43+
type: 'json',
44+
integrity: 'sha256-...',
45+
}
46+
`}
47+
client:load
48+
lang="tsx"
49+
/>
50+
51+
## Options
52+
53+
This rule accepts an options object with the following properties:
54+
55+
### type
56+
57+
<sub>default: `'alphabetical'`</sub>
58+
59+
Specifies the sorting method.
60+
61+
- `'alphabetical'` — Sort attributes alphabetically using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare).
62+
- `'natural'` — Sort by [natural](https://github.com/yobacca/natural-orderby) order (e.g., `link2` < `link10`).
63+
- `'line-length'` — Sort by attribute name length (shorter first by default).
64+
- `'custom'` — Sort using a custom alphabet specified in [`alphabet`](#alphabet).
65+
- `'unsorted'` — Do not sort items. [`grouping`](#groups) and [`newlines behavior`](#newlinesbetween) are still enforced.
66+
67+
### order
68+
69+
<sub>default: `'asc'`</sub>
70+
71+
Sort direction.
72+
73+
- `'asc'` — Ascending (A→Z, short→long).
74+
- `'desc'` — Descending (Z→A, long→short).
75+
76+
### fallbackSort
77+
78+
<sub>
79+
type:
80+
```ts
81+
{
82+
type: 'alphabetical' | 'natural' | 'line-length' | 'custom' | 'unsorted'
83+
order?: 'asc' | 'desc'
84+
}
85+
```
86+
</sub>
87+
<sub>default: `{ type: 'unsorted' }`</sub>
88+
89+
Fallback sorting used when the primary comparison results are equal.
90+
91+
Example: ensure alphabetical tiebreak for equal name lengths.
92+
```ts
93+
{
94+
type: 'line-length',
95+
order: 'desc',
96+
fallbackSort: { type: 'alphabetical', order: 'asc' }
97+
}
98+
```
99+
100+
### alphabet
101+
102+
<sub>default: `''`</sub>
103+
104+
Used only when [`type`](#type) is `'custom'`. Defines the custom character order.
105+
106+
### ignoreCase
107+
108+
<sub>default: `true`</sub>
109+
110+
Whether to perform case-insensitive comparison for string-based sorts.
111+
112+
### specialCharacters
113+
114+
<sub>default: `'keep'`</sub>
115+
116+
How to handle special characters before comparison.
117+
118+
- `'keep'` — Keep as-is.
119+
- `'trim'` — Trim leading special characters.
120+
- `'remove'` — Remove all special characters.
121+
122+
### locales
123+
124+
<sub>default: `'en-US'`</sub>
125+
126+
Locales passed to `localeCompare` for alphabetical/natural sorts.
127+
128+
### partitionByComment
129+
130+
<sub>default: `false`</sub>
131+
132+
Use comments to split attributes into independent partitions that are sorted separately.
133+
134+
- `true` — Any non-ESLint comment creates a partition.
135+
- `false` — Ignore comments for partitioning.
136+
- `RegExpPattern = string | { pattern: string; flags?: string }` — Pattern for matching comments.
137+
- `RegExpPattern[]` — List of patterns.
138+
- `{ block: boolean | RegExpPattern | RegExpPattern[]; line: boolean | RegExpPattern | RegExpPattern[] }` — Separate settings for block/line comments.
139+
140+
### partitionByNewLine
141+
142+
<sub>default: `false`</sub>
143+
144+
When `true`, an empty line between attributes creates a partition. Each partition is sorted independently.
145+
146+
### newlinesBetween
147+
148+
<sub>type: `number | 'ignore' | 'always' | 'never'`</sub>
149+
<sub>default: `'ignore'`</sub>
150+
151+
Controls newlines between groups:
152+
153+
- `'ignore'` — Do not report spacing issues.
154+
- `'always'` — Require a blank line between groups, forbid blank lines inside a group.
155+
- `'never'` — Forbid blank lines between groups.
156+
- `number` — Require exactly that many newlines between groups; forbid inside a group.
157+
158+
### groups
159+
160+
<sub>default: `[]`</sub>
161+
162+
Defines the order of attribute groups. Unknown attributes are placed after the last group.
163+
164+
You can mix predefined (none for this rule) and custom groups. Typical usage is with custom groups declared via `elementNamePattern`.
165+
166+
Example: put any attribute starting with `type` before others, and require a newline between groups.
167+
168+
```ts
169+
groups: ['type-attrs', 'unknown']
170+
customGroups: [
171+
{ groupName: 'type-attrs', elementNamePattern: '^type' },
172+
]
173+
newlinesBetween: 'always'
174+
```
175+
176+
### customGroups
177+
178+
Define custom attribute groups with optional per-group sort overrides.
179+
180+
```ts
181+
type CustomGroup = {
182+
groupName: string
183+
// Match by attribute name
184+
elementNamePattern?: string | string[] | { pattern: string; flags?: string } | { pattern: string; flags?: string }[]
185+
186+
// Optional per-group overrides:
187+
type?: 'alphabetical' | 'natural' | 'line-length' | 'custom' | 'unsorted'
188+
order?: 'asc' | 'desc'
189+
fallbackSort?: { type: string; order?: 'asc' | 'desc' }
190+
newlinesInside?: 'always' | 'never' | number
191+
}[]
192+
```
193+
194+
An attribute matches a custom group when its name satisfies `elementNamePattern`. The first matching definition wins.
195+
196+
## Usage
197+
198+
<CodeTabs
199+
code={[
200+
{
201+
source: dedent`
202+
// eslint.config.js
203+
import perfectionist from 'eslint-plugin-perfectionist'
204+
205+
export default [
206+
{
207+
plugins: { perfectionist },
208+
rules: {
209+
'perfectionist/sort-export-attributes': [
210+
'error',
211+
{
212+
type: 'alphabetical',
213+
order: 'asc',
214+
fallbackSort: { type: 'unsorted' },
215+
ignoreCase: true,
216+
specialCharacters: 'keep',
217+
locales: 'en-US',
218+
alphabet: '',
219+
partitionByComment: false,
220+
partitionByNewLine: false,
221+
newlinesBetween: 'ignore',
222+
groups: [],
223+
customGroups: [],
224+
},
225+
],
226+
},
227+
},
228+
]
229+
`,
230+
name: 'Flat Config',
231+
value: 'flat',
232+
},
233+
{
234+
source: dedent`
235+
// .eslintrc.js
236+
module.exports = {
237+
plugins: ['perfectionist'],
238+
rules: {
239+
'perfectionist/sort-export-attributes': [
240+
'error',
241+
{
242+
type: 'alphabetical',
243+
order: 'asc',
244+
fallbackSort: { type: 'unsorted' },
245+
ignoreCase: true,
246+
specialCharacters: 'keep',
247+
locales: 'en-US',
248+
alphabet: '',
249+
partitionByComment: false,
250+
partitionByNewLine: false,
251+
newlinesBetween: 'ignore',
252+
groups: [],
253+
customGroups: [],
254+
},
255+
],
256+
},
257+
}
258+
`,
259+
name: 'Legacy Config',
260+
value: 'legacy',
261+
},
262+
]}
263+
type="config-type"
264+
client:load
265+
lang="tsx"
266+
/>
267+
268+
## Version
269+
270+
This rule was introduced in [v5.0.0](https://github.com/azat-io/eslint-plugin-perfectionist/releases/tag/v5.0.0).
271+
272+
## Resources
273+
274+
- [Rule source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/rules/sort-export-attributes.ts)
275+
- [Test source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/test/rules/sort-export-attributes.test.ts)

index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { version as packageVersion, name as packageName } from './package.json'
44
import sortVariableDeclarations from './rules/sort-variable-declarations'
55
import sortIntersectionTypes from './rules/sort-intersection-types'
66
import sortImportAttributes from './rules/sort-import-attributes'
7+
import sortExportAttributes from './rules/sort-export-attributes'
78
import sortHeritageClauses from './rules/sort-heritage-clauses'
89
import sortArrayIncludes from './rules/sort-array-includes'
910
import sortNamedImports from './rules/sort-named-imports'
@@ -28,6 +29,7 @@ interface PluginConfig {
2829
'sort-variable-declarations': Rule.RuleModule
2930
'sort-intersection-types': Rule.RuleModule
3031
'sort-import-attributes': Rule.RuleModule
32+
'sort-export-attributes': Rule.RuleModule
3133
'sort-heritage-clauses': Rule.RuleModule
3234
'sort-array-includes': Rule.RuleModule
3335
'sort-named-imports': Rule.RuleModule
@@ -75,6 +77,7 @@ let plugin = {
7577
'sort-variable-declarations': sortVariableDeclarations,
7678
'sort-intersection-types': sortIntersectionTypes,
7779
'sort-import-attributes': sortImportAttributes,
80+
'sort-export-attributes': sortExportAttributes,
7881
'sort-heritage-clauses': sortHeritageClauses,
7982
'sort-array-includes': sortArrayIncludes,
8083
'sort-named-imports': sortNamedImports,

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ module.exports = {
179179
| [sort-classes](https://perfectionist.dev/rules/sort-classes) | Enforce sorted classes | 🔧 |
180180
| [sort-decorators](https://perfectionist.dev/rules/sort-decorators) | Enforce sorted decorators | 🔧 |
181181
| [sort-enums](https://perfectionist.dev/rules/sort-enums) | Enforce sorted TypeScript enums | 🔧 |
182+
| [sort-export-attributes](https://perfectionist.dev/rules/sort-export-attributes) | Enforce sorted export attributes | 🔧 |
182183
| [sort-exports](https://perfectionist.dev/rules/sort-exports) | Enforce sorted exports | 🔧 |
183184
| [sort-heritage-clauses](https://perfectionist.dev/rules/sort-heritage-clauses) | Enforce sorted `implements`/`extends` clauses | 🔧 |
184185
| [sort-import-attributes](https://perfectionist.dev/rules/sort-import-attributes) | Enforce sorted import attributes | 🔧 |

0 commit comments

Comments
 (0)