Skip to content

Commit 6591eaf

Browse files
authored
Merge pull request #23 from mskelton/export-sort-groups
Export sort groups
2 parents 22b45bf + 73ef625 commit 6591eaf

File tree

4 files changed

+261
-13
lines changed

4 files changed

+261
-13
lines changed

docs/rules/exports.md

+74-4
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,83 @@ export { mark }
2727
export default React
2828
```
2929

30+
## Rule Options
31+
32+
This rule has an object with its properties as:
33+
34+
- `"groups"` (default: `[]`)
35+
3036
### Groups
3137

32-
Exports are sorted in the following groups top to bottom:
38+
By default, this rule will perform basic alphanumeric sorting but you can
39+
greatly customize your export sorting with sort groups.This allows you to
40+
separate common groups of exports to make it easier to scan your exports at a
41+
glance.
42+
43+
There are four built-in sort groups you can use:
44+
45+
1. `default`
46+
- Default exports (e.g. `export default a`).
47+
1. `sourceless`
48+
- Exports without a source (e.g. `export { a }`).
49+
1. `dependency`
50+
- Exports which do not throw an error when calling `require.resolve` on the
51+
source.
52+
- Useful for differentiating between path aliases (e.g. `components/Hello`)
53+
and dependencies (e.g. `react`).
54+
1. `other`
55+
- Catch all sort group for any exports which did not match other sort groups.
56+
57+
You can also define custom regex sort groups if the built-in sort groups aren't
58+
enough. The following configuration shows an example of using the built-in sort
59+
groups as well as a custom regex sort group.
60+
61+
```json
62+
{
63+
"sort/exports": [
64+
"warn",
65+
{
66+
"groups": [
67+
{ "type": "default", "order": 5 },
68+
{ "type": "sourceless", "order": 1 },
69+
{ "regex": "^~", "order": 3 },
70+
{ "type": "dependency", "order": 2 },
71+
{ "type": "other", "order": 4 }
72+
]
73+
}
74+
]
75+
}
76+
```
77+
78+
This configuration would result in the following output.
79+
80+
```js
81+
export { a }
82+
export { useState } from "react"
83+
export App from "~/components"
84+
export { b } from "./b"
85+
export default c
86+
```
87+
88+
#### Group Order
89+
90+
It's important to understand the difference between the order of the sort groups
91+
in the `groups` array, and the `order` property of each sort group. When sorting
92+
exports, this plugin will find the first sort group which the export would apply
93+
to and then assign it an order using the `order` property. This allows you to
94+
define a hierarchy of sort groups in descending specificity (e.g. dependency
95+
then regex) while still having full control over the order of the sort groups in
96+
the resulting code.
97+
98+
For example, the `other` sort group will match any export and thus should always
99+
be last in the list of sort groups. However, if you want to sort dependency
100+
exports (e.g. `react`) after the `other` sort group, you can use the `order`
101+
property to give the dependency exports a higher order than the `other` sort
102+
group.
33103

34-
- Exports with a source
35-
- Exports with no source
36-
- Default export
104+
The configuration example above shows how this works where default exports are
105+
the first sort group even though they have the highest order and are thus the
106+
last sort group in the resulting code.
37107

38108
## When Not To Use It
39109

src/__tests__/exports.spec.ts

+107-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
jest.mock("../resolver")
2+
13
import { RuleTester } from "eslint"
24
import rule from "../rules/exports"
35

@@ -33,6 +35,33 @@ ruleTester.run("sort/exports", rule, {
3335
// c
3436
export { c } from "./c"
3537
`.trim(),
38+
39+
// Sort groups
40+
{
41+
code: `
42+
const mark = ''
43+
44+
export default React
45+
export { relA } from './a'
46+
export { relB } from './b'
47+
export { depA } from 'dependency-a'
48+
export { depB } from 'dependency-b'
49+
export * from 'a'
50+
export { b } from 'b'
51+
export { mark }
52+
`.trim(),
53+
options: [
54+
{
55+
groups: [
56+
{ type: "default", order: 1 },
57+
{ type: "sourceless", order: 5 },
58+
{ regex: "^\\.+\\/", order: 2 },
59+
{ type: "dependency", order: 3 },
60+
{ type: "other", order: 4 },
61+
],
62+
},
63+
],
64+
},
3665
],
3766
invalid: [
3867
{
@@ -61,11 +90,11 @@ ruleTester.run("sort/exports", rule, {
6190
output: `
6291
const mark = ''
6392
93+
export default React
94+
export { mark }
6495
export { a } from 'a'
6596
export * from 'b'
6697
export { c } from 'c'
67-
export { mark }
68-
export default React
6998
`,
7099
errors: [{ messageId: "unsorted" }],
71100
},
@@ -90,5 +119,81 @@ ruleTester.run("sort/exports", rule, {
90119
`.trim(),
91120
errors: [{ messageId: "unsorted" }],
92121
},
122+
123+
// Sort groups
124+
{
125+
code: `
126+
const mark = ''
127+
128+
export { depB } from 'dependency-b'
129+
export { mark }
130+
export default React
131+
export { relB } from './b'
132+
export * from 'a'
133+
export { relA } from './a'
134+
export { depA } from 'dependency-a'
135+
export { b } from 'b'
136+
`.trim(),
137+
output: `
138+
const mark = ''
139+
140+
export { relA } from './a'
141+
export { relB } from './b'
142+
export * from 'a'
143+
export { b } from 'b'
144+
export { depA } from 'dependency-a'
145+
export { depB } from 'dependency-b'
146+
export { mark }
147+
export default React
148+
`.trim(),
149+
options: [
150+
{
151+
groups: [
152+
{ type: "default", order: 3 },
153+
{ type: "sourceless", order: 2 },
154+
{ type: "other", order: 1 },
155+
],
156+
},
157+
],
158+
errors: [{ messageId: "unsorted" }],
159+
},
160+
{
161+
code: `
162+
const mark = ''
163+
164+
export { depB } from 'dependency-b'
165+
export { mark }
166+
export default React
167+
export { relB } from './b'
168+
export * from 'a'
169+
export { relA } from './a'
170+
export { depA } from 'dependency-a'
171+
export { b } from 'b'
172+
`.trim(),
173+
output: `
174+
const mark = ''
175+
176+
export default React
177+
export { relA } from './a'
178+
export { relB } from './b'
179+
export { depA } from 'dependency-a'
180+
export { depB } from 'dependency-b'
181+
export * from 'a'
182+
export { b } from 'b'
183+
export { mark }
184+
`.trim(),
185+
options: [
186+
{
187+
groups: [
188+
{ type: "default", order: 1 },
189+
{ type: "sourceless", order: 5 },
190+
{ regex: "^\\.+\\/", order: 2 },
191+
{ type: "dependency", order: 3 },
192+
{ type: "other", order: 4 },
193+
],
194+
},
195+
],
196+
errors: [{ messageId: "unsorted" }],
197+
},
93198
],
94199
})

src/index.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,18 @@ module.exports = {
1212
plugins: ["sort"],
1313
rules: {
1414
"sort/destructuring-properties": "warn",
15-
"sort/exports": "warn",
15+
"sort/exports": [
16+
"warn",
17+
{
18+
groups: [
19+
{ type: "default", order: 5 },
20+
{ type: "sourceless", order: 4 },
21+
{ regex: "^\\.+\\/", order: 3 },
22+
{ type: "dependency", order: 1 },
23+
{ type: "other", order: 2 },
24+
],
25+
},
26+
],
1627
"sort/export-members": "warn",
1728
"sort/imports": [
1829
"warn",

src/rules/exports.ts

+68-6
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,53 @@
1+
import { ExportDefaultDeclaration } from "@typescript-eslint/types/dist/ast-spec"
12
import { Rule } from "eslint"
23
import { ImportDeclaration, ModuleDeclaration } from "estree"
4+
import { isResolved } from "../resolver"
35
import { docsURL, filterNodes, getName, report } from "../utils"
46

57
type Export = Exclude<ModuleDeclaration, ImportDeclaration>
68

9+
interface SortGroup {
10+
order: number
11+
type?: "default" | "sourceless" | "dependency" | "other"
12+
regex?: string
13+
}
14+
715
/**
8-
* Returns the node's sort weight. The sort weight is used to separate types
9-
* of nodes into groups and then sort in each individual group.
16+
* Returns the order of a given node based on the sort groups configured in the
17+
* rule options. If no sort groups are configured (default), the order returned
18+
* is always 0.
1019
*/
11-
function getWeight(node: Export) {
12-
return node.type === "ExportDefaultDeclaration" ? 2 : node.source ? 0 : 1
20+
function getSortGroup(
21+
sortGroups: SortGroup[],
22+
node: Exclude<Export, ExportDefaultDeclaration>
23+
) {
24+
const source = getSortValue(node)
25+
const isDefaultExport = node.type === "ExportDefaultDeclaration"
26+
27+
for (const { regex, type, order } of sortGroups) {
28+
switch (type) {
29+
case "default":
30+
if (isDefaultExport) return order
31+
break
32+
33+
case "sourceless":
34+
if (!isDefaultExport && !node.source) return order
35+
break
36+
37+
case "dependency":
38+
if (isResolved(source)) return order
39+
break
40+
41+
case "other":
42+
return order
43+
}
44+
45+
if (regex && new RegExp(regex).test(source)) {
46+
return order
47+
}
48+
}
49+
50+
return 0
1351
}
1452

1553
function getSortValue(node: Export) {
@@ -20,6 +58,8 @@ function getSortValue(node: Export) {
2058

2159
export default {
2260
create(context) {
61+
const groups = context.options[0]?.groups ?? []
62+
2363
return {
2464
Program(program) {
2565
const nodes = filterNodes(program.body, [
@@ -35,8 +75,8 @@ export default {
3575

3676
const sorted = nodes.slice().sort(
3777
(a, b) =>
38-
// First sort by weight
39-
getWeight(a) - getWeight(b) ||
78+
// First, sort by sort group
79+
getSortGroup(groups, a) - getSortGroup(groups, b) ||
4080
// Then sort by path
4181
getSortValue(a).localeCompare(getSortValue(b))
4282
)
@@ -54,5 +94,27 @@ export default {
5494
messages: {
5595
unsorted: "Exports should be sorted alphabetically.",
5696
},
97+
schema: [
98+
{
99+
type: "object",
100+
properties: {
101+
groups: {
102+
type: "array",
103+
items: {
104+
type: "object",
105+
properties: {
106+
type: {
107+
enum: ["default", "sourceless", "dependency", "other"],
108+
},
109+
regex: { type: "string" },
110+
order: { type: "number" },
111+
},
112+
required: ["order"],
113+
additionalProperties: false,
114+
},
115+
},
116+
},
117+
},
118+
],
57119
},
58120
} as Rule.RuleModule

0 commit comments

Comments
 (0)