@tanstack/eslint-plugin-router 1.20.3-alpha.1
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.
- package/LICENSE +21 -0
- package/dist/cjs/index.cjs +29 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +13 -0
- package/dist/cjs/rules/create-route-property-order/constants.cjs +40 -0
- package/dist/cjs/rules/create-route-property-order/constants.cjs.map +1 -0
- package/dist/cjs/rules/create-route-property-order/constants.d.cts +7 -0
- package/dist/cjs/rules/create-route-property-order/create-route-property-order.rule.cjs +103 -0
- package/dist/cjs/rules/create-route-property-order/create-route-property-order.rule.cjs.map +1 -0
- package/dist/cjs/rules/create-route-property-order/create-route-property-order.rule.d.cts +4 -0
- package/dist/cjs/rules/create-route-property-order/create-route-property-order.utils.cjs +51 -0
- package/dist/cjs/rules/create-route-property-order/create-route-property-order.utils.cjs.map +1 -0
- package/dist/cjs/rules/create-route-property-order/create-route-property-order.utils.d.cts +2 -0
- package/dist/cjs/rules.cjs +8 -0
- package/dist/cjs/rules.cjs.map +1 -0
- package/dist/cjs/rules.d.cts +3 -0
- package/dist/cjs/types.d.cts +3 -0
- package/dist/cjs/utils/detect-router-imports.cjs +54 -0
- package/dist/cjs/utils/detect-router-imports.cjs.map +1 -0
- package/dist/cjs/utils/detect-router-imports.d.cts +11 -0
- package/dist/cjs/utils/get-docs-url.cjs +5 -0
- package/dist/cjs/utils/get-docs-url.cjs.map +1 -0
- package/dist/cjs/utils/get-docs-url.d.cts +1 -0
- package/dist/esm/index.d.ts +13 -0
- package/dist/esm/index.js +30 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/rules/create-route-property-order/constants.d.ts +7 -0
- package/dist/esm/rules/create-route-property-order/constants.js +40 -0
- package/dist/esm/rules/create-route-property-order/constants.js.map +1 -0
- package/dist/esm/rules/create-route-property-order/create-route-property-order.rule.d.ts +4 -0
- package/dist/esm/rules/create-route-property-order/create-route-property-order.rule.js +103 -0
- package/dist/esm/rules/create-route-property-order/create-route-property-order.rule.js.map +1 -0
- package/dist/esm/rules/create-route-property-order/create-route-property-order.utils.d.ts +2 -0
- package/dist/esm/rules/create-route-property-order/create-route-property-order.utils.js +51 -0
- package/dist/esm/rules/create-route-property-order/create-route-property-order.utils.js.map +1 -0
- package/dist/esm/rules.d.ts +3 -0
- package/dist/esm/rules.js +8 -0
- package/dist/esm/rules.js.map +1 -0
- package/dist/esm/types.d.ts +3 -0
- package/dist/esm/utils/detect-router-imports.d.ts +11 -0
- package/dist/esm/utils/detect-router-imports.js +54 -0
- package/dist/esm/utils/detect-router-imports.js.map +1 -0
- package/dist/esm/utils/get-docs-url.d.ts +1 -0
- package/dist/esm/utils/get-docs-url.js +5 -0
- package/dist/esm/utils/get-docs-url.js.map +1 -0
- package/package.json +51 -0
- package/src/__tests__/create-route-property-order.rule.test.ts +199 -0
- package/src/__tests__/create-route-property-order.utils.test.ts +179 -0
- package/src/__tests__/test-utils.test.ts +104 -0
- package/src/__tests__/test-utils.ts +108 -0
- package/src/index.ts +43 -0
- package/src/rules/create-route-property-order/constants.ts +40 -0
- package/src/rules/create-route-property-order/create-route-property-order.rule.ts +124 -0
- package/src/rules/create-route-property-order/create-route-property-order.utils.ts +73 -0
- package/src/rules.ts +15 -0
- package/src/types.ts +3 -0
- package/src/utils/detect-router-imports.ts +94 -0
- package/src/utils/get-docs-url.ts +2 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
getCheckedProperties,
|
|
4
|
+
sortDataByOrder,
|
|
5
|
+
} from '../rules/create-route-property-order/create-route-property-order.utils'
|
|
6
|
+
|
|
7
|
+
describe('create-route-property-order utils', () => {
|
|
8
|
+
describe('sortDataByOrder', () => {
|
|
9
|
+
const testCases = [
|
|
10
|
+
{
|
|
11
|
+
data: [{ key: 'a' }, { key: 'c' }, { key: 'b' }],
|
|
12
|
+
orderArray: [
|
|
13
|
+
[['a'], ['b']],
|
|
14
|
+
[['b'], ['c']],
|
|
15
|
+
],
|
|
16
|
+
key: 'key',
|
|
17
|
+
expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
data: [{ key: 'b' }, { key: 'a' }, { key: 'c' }],
|
|
21
|
+
orderArray: [
|
|
22
|
+
[['a'], ['b']],
|
|
23
|
+
[['b'], ['c']],
|
|
24
|
+
],
|
|
25
|
+
key: 'key',
|
|
26
|
+
expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
|
|
30
|
+
orderArray: [
|
|
31
|
+
[['a'], ['b']],
|
|
32
|
+
[['b'], ['c']],
|
|
33
|
+
],
|
|
34
|
+
key: 'key',
|
|
35
|
+
expected: null,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }, { key: 'd' }],
|
|
39
|
+
orderArray: [
|
|
40
|
+
[['a'], ['b']],
|
|
41
|
+
[['b'], ['c']],
|
|
42
|
+
],
|
|
43
|
+
key: 'key',
|
|
44
|
+
expected: null,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
data: [{ key: 'a' }, { key: 'b' }, { key: 'd' }, { key: 'c' }],
|
|
48
|
+
orderArray: [
|
|
49
|
+
[['a'], ['b']],
|
|
50
|
+
[['b'], ['c']],
|
|
51
|
+
],
|
|
52
|
+
key: 'key',
|
|
53
|
+
expected: null,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
data: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
|
|
57
|
+
orderArray: [
|
|
58
|
+
[['a'], ['b']],
|
|
59
|
+
[['b'], ['c']],
|
|
60
|
+
],
|
|
61
|
+
key: 'key',
|
|
62
|
+
expected: null,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
data: [{ key: 'd' }, { key: 'b' }, { key: 'a' }, { key: 'c' }],
|
|
66
|
+
orderArray: [
|
|
67
|
+
[['a'], ['b']],
|
|
68
|
+
[['b'], ['c']],
|
|
69
|
+
],
|
|
70
|
+
key: 'key',
|
|
71
|
+
expected: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
data: [{ key: 'd' }, { key: 'b' }, { key: 'a' }, { key: 'c' }],
|
|
75
|
+
orderArray: [
|
|
76
|
+
[['a', 'b'], ['d']],
|
|
77
|
+
[['d'], ['c']],
|
|
78
|
+
],
|
|
79
|
+
key: 'key',
|
|
80
|
+
expected: [{ key: 'b' }, { key: 'a' }, { key: 'd' }, { key: 'c' }],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
data: [
|
|
84
|
+
{ key: 'd' },
|
|
85
|
+
{ key: 'b' },
|
|
86
|
+
{ key: 'a' },
|
|
87
|
+
{ key: 'c' },
|
|
88
|
+
{ key: 'f' },
|
|
89
|
+
],
|
|
90
|
+
orderArray: [
|
|
91
|
+
[
|
|
92
|
+
['a', 'b'],
|
|
93
|
+
['d', 'f'],
|
|
94
|
+
],
|
|
95
|
+
[['d'], ['c']],
|
|
96
|
+
],
|
|
97
|
+
key: 'key',
|
|
98
|
+
expected: [
|
|
99
|
+
{ key: 'b' },
|
|
100
|
+
{ key: 'a' },
|
|
101
|
+
{ key: 'd' },
|
|
102
|
+
{ key: 'f' },
|
|
103
|
+
{ key: 'c' },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
data: [
|
|
108
|
+
{ key: 'd' },
|
|
109
|
+
{ key: 'b' },
|
|
110
|
+
{ key: 'a' },
|
|
111
|
+
{ key: 'c' },
|
|
112
|
+
{ key: 'f' },
|
|
113
|
+
],
|
|
114
|
+
orderArray: [
|
|
115
|
+
[
|
|
116
|
+
['a', 'b'],
|
|
117
|
+
['d', 'f'],
|
|
118
|
+
],
|
|
119
|
+
[['d', 'f'], ['c']],
|
|
120
|
+
],
|
|
121
|
+
key: 'key',
|
|
122
|
+
expected: [
|
|
123
|
+
{ key: 'b' },
|
|
124
|
+
{ key: 'a' },
|
|
125
|
+
{ key: 'd' },
|
|
126
|
+
{ key: 'f' },
|
|
127
|
+
{ key: 'c' },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
] as const
|
|
131
|
+
test.each(testCases)(
|
|
132
|
+
'$data $orderArray $key $expected',
|
|
133
|
+
({ data, orderArray, key, expected }) => {
|
|
134
|
+
const sortedData = sortDataByOrder(data, orderArray, key)
|
|
135
|
+
expect(sortedData).toEqual(expected)
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('getCheckedProperties', () => {
|
|
142
|
+
const testCases = [
|
|
143
|
+
{
|
|
144
|
+
orderRules: [
|
|
145
|
+
[['a', 'b'], ['c']],
|
|
146
|
+
[['c'], ['d']],
|
|
147
|
+
],
|
|
148
|
+
expected: ['a', 'b', 'c', 'd'],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
orderRules: [
|
|
152
|
+
[['a', 'b'], ['c']],
|
|
153
|
+
[['d'], ['e']],
|
|
154
|
+
],
|
|
155
|
+
expected: ['a', 'b', 'c', 'd', 'e'],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
orderRules: [
|
|
159
|
+
[['a', 'b'], ['c']],
|
|
160
|
+
[['d'], ['e']],
|
|
161
|
+
[['c'], ['f']],
|
|
162
|
+
],
|
|
163
|
+
expected: ['a', 'b', 'c', 'd', 'e', 'f'],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
orderRules: [
|
|
167
|
+
[['a', 'b'], ['c']],
|
|
168
|
+
[['d'], ['e']],
|
|
169
|
+
[['c'], ['f']],
|
|
170
|
+
[['f'], ['g']],
|
|
171
|
+
],
|
|
172
|
+
expected: ['a', 'b', 'c', 'd', 'e', 'f', 'g'],
|
|
173
|
+
},
|
|
174
|
+
] as const
|
|
175
|
+
test.each(testCases)('$orderRules $expected', ({ orderRules, expected }) => {
|
|
176
|
+
const checkedProperties = getCheckedProperties(orderRules)
|
|
177
|
+
expect(checkedProperties).toEqual(expected)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
expectArrayEqualIgnoreOrder,
|
|
4
|
+
generateInterleavedCombinations,
|
|
5
|
+
generatePartialCombinations,
|
|
6
|
+
generatePermutations,
|
|
7
|
+
} from './test-utils'
|
|
8
|
+
|
|
9
|
+
describe('test-utils', () => {
|
|
10
|
+
describe('generatePermutations', () => {
|
|
11
|
+
const testCases = [
|
|
12
|
+
{
|
|
13
|
+
input: ['a', 'b', 'c'],
|
|
14
|
+
expected: [
|
|
15
|
+
['a', 'b', 'c'],
|
|
16
|
+
['a', 'c', 'b'],
|
|
17
|
+
['b', 'a', 'c'],
|
|
18
|
+
['b', 'c', 'a'],
|
|
19
|
+
['c', 'a', 'b'],
|
|
20
|
+
['c', 'b', 'a'],
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
input: ['a', 'b'],
|
|
25
|
+
expected: [
|
|
26
|
+
['a', 'b'],
|
|
27
|
+
['b', 'a'],
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
input: ['a'],
|
|
32
|
+
expected: [['a']],
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
test.each(testCases)('$input $expected', ({ input, expected }) => {
|
|
36
|
+
const permutations = generatePermutations(input)
|
|
37
|
+
expect(permutations).toEqual(expected)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('generatePartialCombinations', () => {
|
|
42
|
+
const testCases = [
|
|
43
|
+
{
|
|
44
|
+
input: ['a', 'b', 'c'],
|
|
45
|
+
minLength: 2,
|
|
46
|
+
expected: [
|
|
47
|
+
['a', 'b'],
|
|
48
|
+
['a', 'c'],
|
|
49
|
+
['b', 'c'],
|
|
50
|
+
['a', 'b', 'c'],
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
input: ['a', 'b'],
|
|
55
|
+
expected: [['a', 'b']],
|
|
56
|
+
minLength: 2,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
input: ['a'],
|
|
60
|
+
expected: [],
|
|
61
|
+
minLength: 2,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
input: ['a'],
|
|
65
|
+
expected: [['a']],
|
|
66
|
+
minLength: 1,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
input: ['a'],
|
|
70
|
+
expected: [[], ['a']],
|
|
71
|
+
minLength: 0,
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
test.each(testCases)(
|
|
75
|
+
'$input $minLength $expected ',
|
|
76
|
+
({ input, minLength, expected }) => {
|
|
77
|
+
const combinations = generatePartialCombinations(input, minLength)
|
|
78
|
+
expectArrayEqualIgnoreOrder(combinations, expected)
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('generateInterleavedCombinations', () => {
|
|
84
|
+
const testCases = [
|
|
85
|
+
{
|
|
86
|
+
data: ['a', 'b'],
|
|
87
|
+
additional: ['x'],
|
|
88
|
+
expected: [
|
|
89
|
+
['a', 'b'],
|
|
90
|
+
['x', 'a', 'b'],
|
|
91
|
+
['a', 'x', 'b'],
|
|
92
|
+
['a', 'b', 'x'],
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
test.each(testCases)(
|
|
97
|
+
'$input $expected',
|
|
98
|
+
({ data, additional, expected }) => {
|
|
99
|
+
const combinations = generateInterleavedCombinations(data, additional)
|
|
100
|
+
expectArrayEqualIgnoreOrder(combinations, expected)
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
export function generatePermutations<T>(arr: Array<T>): Array<Array<T>> {
|
|
4
|
+
if (arr.length <= 1) {
|
|
5
|
+
return [arr]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const result: Array<Array<T>> = []
|
|
9
|
+
for (let i = 0; i < arr.length; i++) {
|
|
10
|
+
const rest = arr.slice(0, i).concat(arr.slice(i + 1))
|
|
11
|
+
const restPermutations = generatePermutations(rest)
|
|
12
|
+
for (const perm of restPermutations) {
|
|
13
|
+
result.push([arr[i]!, ...perm])
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return result
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function generatePartialCombinations<T>(
|
|
21
|
+
arr: ReadonlyArray<T>,
|
|
22
|
+
minLength: number,
|
|
23
|
+
): Array<Array<T>> {
|
|
24
|
+
const result: Array<Array<T>> = []
|
|
25
|
+
|
|
26
|
+
function backtrack(start: number, current: Array<T>) {
|
|
27
|
+
if (current.length > minLength - 1) {
|
|
28
|
+
result.push([...current])
|
|
29
|
+
}
|
|
30
|
+
for (let i = start; i < arr.length; i++) {
|
|
31
|
+
current.push(arr[i]!)
|
|
32
|
+
backtrack(i + 1, current)
|
|
33
|
+
current.pop()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
backtrack(0, [])
|
|
37
|
+
return result
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function expectArrayEqualIgnoreOrder<T>(a: Array<T>, b: Array<T>) {
|
|
41
|
+
expect([...a].sort()).toEqual([...b].sort())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function normalizeIndent(template: TemplateStringsArray) {
|
|
45
|
+
const codeLines = template[0]?.split('\n') ?? ['']
|
|
46
|
+
const leftPadding = codeLines[1]?.match(/\s+/)?.[0] ?? ''
|
|
47
|
+
return codeLines.map((line) => line.slice(leftPadding.length)).join('\n')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function generateInterleavedCombinations<
|
|
51
|
+
TData,
|
|
52
|
+
TAdditional,
|
|
53
|
+
TResult extends TData | TAdditional,
|
|
54
|
+
>(
|
|
55
|
+
data: Array<TData> | ReadonlyArray<TData>,
|
|
56
|
+
additional: Array<TAdditional> | ReadonlyArray<TAdditional>,
|
|
57
|
+
): Array<Array<TResult>> {
|
|
58
|
+
const result: Array<Array<TResult>> = []
|
|
59
|
+
|
|
60
|
+
function getSubsets(array: Array<TAdditional>): Array<Array<TAdditional>> {
|
|
61
|
+
return array.reduce(
|
|
62
|
+
(subsets, value) => {
|
|
63
|
+
return subsets.concat(subsets.map((set) => [...set, value]))
|
|
64
|
+
},
|
|
65
|
+
[[]] as Array<Array<TAdditional>>,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function insertAtPositions(
|
|
70
|
+
data: Array<TResult>,
|
|
71
|
+
subset: Array<TResult>,
|
|
72
|
+
): Array<Array<TResult>> {
|
|
73
|
+
const combinations: Array<Array<TResult>> = []
|
|
74
|
+
|
|
75
|
+
const recurse = (
|
|
76
|
+
currentData: Array<TResult>,
|
|
77
|
+
currentSubset: Array<TResult>,
|
|
78
|
+
start: number,
|
|
79
|
+
): void => {
|
|
80
|
+
if (currentSubset.length === 0) {
|
|
81
|
+
combinations.push([...currentData])
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (let i = start; i <= currentData.length; i++) {
|
|
86
|
+
const newData = [
|
|
87
|
+
...currentData.slice(0, i),
|
|
88
|
+
currentSubset[0]!,
|
|
89
|
+
...currentData.slice(i),
|
|
90
|
+
]
|
|
91
|
+
recurse(newData, currentSubset.slice(1), i + 1)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
recurse(data, subset, 0)
|
|
96
|
+
return combinations
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const subsets = getSubsets(additional as Array<TAdditional>)
|
|
100
|
+
|
|
101
|
+
subsets.forEach((subset) => {
|
|
102
|
+
result.push(
|
|
103
|
+
...insertAtPositions(data as Array<TResult>, subset as Array<TResult>),
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return result
|
|
108
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { rules } from './rules'
|
|
2
|
+
import type { ESLint, Linter } from 'eslint'
|
|
3
|
+
import type { RuleModule } from '@typescript-eslint/utils/ts-eslint'
|
|
4
|
+
|
|
5
|
+
type RuleKey = keyof typeof rules
|
|
6
|
+
|
|
7
|
+
interface Plugin extends Omit<ESLint.Plugin, 'rules'> {
|
|
8
|
+
rules: Record<RuleKey, RuleModule<any, any, any>>
|
|
9
|
+
configs: {
|
|
10
|
+
recommended: ESLint.ConfigData
|
|
11
|
+
'flat/recommended': Array<Linter.FlatConfig>
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const plugin: Plugin = {
|
|
16
|
+
meta: {
|
|
17
|
+
name: '@tanstack/eslint-plugin-router',
|
|
18
|
+
},
|
|
19
|
+
configs: {} as Plugin['configs'],
|
|
20
|
+
rules,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Assign configs here so we can reference `plugin`
|
|
24
|
+
Object.assign(plugin.configs, {
|
|
25
|
+
recommended: {
|
|
26
|
+
plugins: ['@tanstack/eslint-plugin-router'],
|
|
27
|
+
rules: {
|
|
28
|
+
'@tanstack/router/create-route-property-order': 'warn',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
'flat/recommended': [
|
|
32
|
+
{
|
|
33
|
+
plugins: {
|
|
34
|
+
'@tanstack/router': plugin,
|
|
35
|
+
},
|
|
36
|
+
rules: {
|
|
37
|
+
'@tanstack/router/create-route-property-order': 'warn',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export default plugin
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getCheckedProperties } from './create-route-property-order.utils'
|
|
2
|
+
|
|
3
|
+
export const createRouteFunctionsIndirect = [
|
|
4
|
+
'createFileRoute',
|
|
5
|
+
'createRootRouteWithContext',
|
|
6
|
+
] as const
|
|
7
|
+
export const createRouteFunctionsDirect = [
|
|
8
|
+
'createRootRoute',
|
|
9
|
+
'createRoute',
|
|
10
|
+
] as const
|
|
11
|
+
|
|
12
|
+
export const createRouteFunctions = [
|
|
13
|
+
...createRouteFunctionsDirect,
|
|
14
|
+
...createRouteFunctionsIndirect,
|
|
15
|
+
] as const
|
|
16
|
+
|
|
17
|
+
export type CreateRouteFunction = (typeof createRouteFunctions)[number]
|
|
18
|
+
|
|
19
|
+
export const sortRules = [
|
|
20
|
+
[['params', 'validateSearch'], ['search']],
|
|
21
|
+
[['search'], ['loaderDeps']],
|
|
22
|
+
[['loaderDeps'], ['context']],
|
|
23
|
+
[['context'], ['beforeLoad']],
|
|
24
|
+
[['beforeLoad'], ['loader']],
|
|
25
|
+
[
|
|
26
|
+
['loader'],
|
|
27
|
+
[
|
|
28
|
+
'onEnter',
|
|
29
|
+
'onStay',
|
|
30
|
+
'onLeave',
|
|
31
|
+
'head',
|
|
32
|
+
'scripts',
|
|
33
|
+
'headers',
|
|
34
|
+
'remountDeps',
|
|
35
|
+
],
|
|
36
|
+
],
|
|
37
|
+
] as const
|
|
38
|
+
|
|
39
|
+
export type CheckedProperties = (typeof sortRules)[number][number][number]
|
|
40
|
+
export const checkedProperties = getCheckedProperties(sortRules)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'
|
|
2
|
+
|
|
3
|
+
import { getDocsUrl } from '../../utils/get-docs-url'
|
|
4
|
+
import { detectTanstackRouterImports } from '../../utils/detect-router-imports'
|
|
5
|
+
import { sortDataByOrder } from './create-route-property-order.utils'
|
|
6
|
+
import {
|
|
7
|
+
createRouteFunctions,
|
|
8
|
+
createRouteFunctionsIndirect,
|
|
9
|
+
sortRules,
|
|
10
|
+
} from './constants'
|
|
11
|
+
import type { CreateRouteFunction } from './constants'
|
|
12
|
+
import type { ExtraRuleDocs } from '../../types'
|
|
13
|
+
|
|
14
|
+
const createRule = ESLintUtils.RuleCreator<ExtraRuleDocs>(getDocsUrl)
|
|
15
|
+
|
|
16
|
+
const createRouteFunctionSet = new Set(createRouteFunctions)
|
|
17
|
+
function isCreateRouteFunction(node: any): node is CreateRouteFunction {
|
|
18
|
+
return createRouteFunctionSet.has(node)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const name = 'create-route-property-order'
|
|
22
|
+
|
|
23
|
+
export const rule = createRule({
|
|
24
|
+
name,
|
|
25
|
+
meta: {
|
|
26
|
+
type: 'problem',
|
|
27
|
+
docs: {
|
|
28
|
+
description:
|
|
29
|
+
'Ensure correct order of inference sensitive properties for createRoute functions',
|
|
30
|
+
recommended: 'error',
|
|
31
|
+
},
|
|
32
|
+
messages: {
|
|
33
|
+
invalidOrder: 'Invalid order of properties for `{{function}}`.',
|
|
34
|
+
},
|
|
35
|
+
schema: [],
|
|
36
|
+
hasSuggestions: true,
|
|
37
|
+
fixable: 'code',
|
|
38
|
+
},
|
|
39
|
+
defaultOptions: [],
|
|
40
|
+
|
|
41
|
+
create: detectTanstackRouterImports((context) => {
|
|
42
|
+
return {
|
|
43
|
+
CallExpression(node) {
|
|
44
|
+
if (node.callee.type !== AST_NODE_TYPES.Identifier) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
const createRouteFunction = node.callee.name
|
|
48
|
+
if (!isCreateRouteFunction(createRouteFunction)) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
let args = node.arguments
|
|
52
|
+
if (createRouteFunctionsIndirect.includes(createRouteFunction as any)) {
|
|
53
|
+
if (node.parent.type === AST_NODE_TYPES.CallExpression) {
|
|
54
|
+
args = node.parent.arguments
|
|
55
|
+
} else {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const argument = args[0]
|
|
61
|
+
if (argument === undefined || argument.type !== 'ObjectExpression') {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const allProperties = argument.properties
|
|
66
|
+
// no need to sort if there is at max 1 property
|
|
67
|
+
if (allProperties.length < 2) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const properties = allProperties.flatMap((p) => {
|
|
72
|
+
if (
|
|
73
|
+
p.type === AST_NODE_TYPES.Property &&
|
|
74
|
+
p.key.type === AST_NODE_TYPES.Identifier
|
|
75
|
+
) {
|
|
76
|
+
return { name: p.key.name, property: p }
|
|
77
|
+
} else if (p.type === AST_NODE_TYPES.SpreadElement) {
|
|
78
|
+
if (p.argument.type === AST_NODE_TYPES.Identifier) {
|
|
79
|
+
return { name: p.argument.name, property: p }
|
|
80
|
+
} else {
|
|
81
|
+
throw new Error('Unsupported spread element')
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return []
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const sortedProperties = sortDataByOrder(properties, sortRules, 'name')
|
|
88
|
+
if (sortedProperties === null) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
context.report({
|
|
92
|
+
node: argument,
|
|
93
|
+
data: { function: node.callee.name },
|
|
94
|
+
messageId: 'invalidOrder',
|
|
95
|
+
fix(fixer) {
|
|
96
|
+
const sourceCode = context.sourceCode
|
|
97
|
+
|
|
98
|
+
const text = sortedProperties.reduce(
|
|
99
|
+
(sourceText, specifier, index) => {
|
|
100
|
+
let text = ''
|
|
101
|
+
if (index < allProperties.length - 1) {
|
|
102
|
+
text = sourceCode
|
|
103
|
+
.getText()
|
|
104
|
+
.slice(
|
|
105
|
+
allProperties[index]!.range[1],
|
|
106
|
+
allProperties[index + 1]!.range[0],
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
return (
|
|
110
|
+
sourceText + sourceCode.getText(specifier.property) + text
|
|
111
|
+
)
|
|
112
|
+
},
|
|
113
|
+
'',
|
|
114
|
+
)
|
|
115
|
+
return fixer.replaceTextRange(
|
|
116
|
+
[allProperties[0]!.range[0], allProperties.at(-1)!.range[1]],
|
|
117
|
+
text,
|
|
118
|
+
)
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
}),
|
|
124
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export function sortDataByOrder<T, TKey extends keyof T>(
|
|
2
|
+
data: Array<T> | ReadonlyArray<T>,
|
|
3
|
+
orderRules: ReadonlyArray<
|
|
4
|
+
Readonly<[ReadonlyArray<T[TKey]>, ReadonlyArray<T[TKey]>]>
|
|
5
|
+
>,
|
|
6
|
+
key: TKey,
|
|
7
|
+
): Array<T> | null {
|
|
8
|
+
const getSubsetIndex = (
|
|
9
|
+
item: T[TKey],
|
|
10
|
+
subsets: ReadonlyArray<ReadonlyArray<T[TKey]> | Array<T[TKey]>>,
|
|
11
|
+
): number | null => {
|
|
12
|
+
for (let i = 0; i < subsets.length; i++) {
|
|
13
|
+
if (subsets[i]?.includes(item)) {
|
|
14
|
+
return i
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const orderSets = orderRules.reduce(
|
|
21
|
+
(sets, [A, B]) => [...sets, A, B],
|
|
22
|
+
[] as Array<ReadonlyArray<T[TKey]> | Array<T[TKey]>>,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const inOrderArray = data.filter(
|
|
26
|
+
(item) => getSubsetIndex(item[key], orderSets) !== null,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
let wasResorted = false as boolean
|
|
30
|
+
|
|
31
|
+
// Sort by the relative order defined by the rules
|
|
32
|
+
const sortedArray = inOrderArray.sort((a, b) => {
|
|
33
|
+
const aKey = a[key],
|
|
34
|
+
bKey = b[key]
|
|
35
|
+
const aSubsetIndex = getSubsetIndex(aKey, orderSets)
|
|
36
|
+
const bSubsetIndex = getSubsetIndex(bKey, orderSets)
|
|
37
|
+
|
|
38
|
+
// If both items belong to different subsets, sort by their subset order
|
|
39
|
+
if (
|
|
40
|
+
aSubsetIndex !== null &&
|
|
41
|
+
bSubsetIndex !== null &&
|
|
42
|
+
aSubsetIndex !== bSubsetIndex
|
|
43
|
+
) {
|
|
44
|
+
return aSubsetIndex - bSubsetIndex
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If both items belong to the same subset or neither is in the subset, keep their relative order
|
|
48
|
+
return 0
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const inOrderIterator = sortedArray.values()
|
|
52
|
+
const result = data.map((item) => {
|
|
53
|
+
if (getSubsetIndex(item[key], orderSets) !== null) {
|
|
54
|
+
const sortedItem = inOrderIterator.next().value!
|
|
55
|
+
if (sortedItem[key] !== item[key]) {
|
|
56
|
+
wasResorted = true
|
|
57
|
+
}
|
|
58
|
+
return sortedItem
|
|
59
|
+
}
|
|
60
|
+
return item
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
if (!wasResorted) {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
return result
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getCheckedProperties<T>(
|
|
70
|
+
orderRules: ReadonlyArray<ReadonlyArray<ReadonlyArray<T>>>,
|
|
71
|
+
): ReadonlyArray<T> {
|
|
72
|
+
return [...new Set<T>(orderRules.flat(2))]
|
|
73
|
+
}
|
package/src/rules.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as createRoutePropertyOrder from './rules/create-route-property-order/create-route-property-order.rule'
|
|
2
|
+
import type { ESLintUtils } from '@typescript-eslint/utils'
|
|
3
|
+
import type { ExtraRuleDocs } from './types'
|
|
4
|
+
|
|
5
|
+
export const rules: Record<
|
|
6
|
+
string,
|
|
7
|
+
ESLintUtils.RuleModule<
|
|
8
|
+
string,
|
|
9
|
+
ReadonlyArray<unknown>,
|
|
10
|
+
ExtraRuleDocs,
|
|
11
|
+
ESLintUtils.RuleListener
|
|
12
|
+
>
|
|
13
|
+
> = {
|
|
14
|
+
[createRoutePropertyOrder.name]: createRoutePropertyOrder.rule,
|
|
15
|
+
}
|
package/src/types.ts
ADDED