@tanstack/eslint-plugin-router 1.58.0
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 +27 -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 +5 -0
- package/dist/cjs/rules/create-route-property-order/create-route-property-order.rule.cjs +104 -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 +28 -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 +1 -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 +5 -0
- package/dist/esm/rules/create-route-property-order/constants.js +27 -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 +104 -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 +1 -0
- package/dist/esm/rules/create-route-property-order/create-route-property-order.utils.js +28 -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 +153 -0
- package/src/__tests__/create-route-property-order.utils.test.ts +58 -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 +24 -0
- package/src/rules/create-route-property-order/create-route-property-order.rule.ts +125 -0
- package/src/rules/create-route-property-order/create-route-property-order.utils.ts +38 -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,24 @@
|
|
|
1
|
+
export const createRouteFunctionsIndirect = [
|
|
2
|
+
'createFileRoute',
|
|
3
|
+
'createRootRouteWithContext',
|
|
4
|
+
] as const
|
|
5
|
+
export const createRouteFunctionsDirect = [
|
|
6
|
+
'createRootRoute',
|
|
7
|
+
'createRoute',
|
|
8
|
+
] as const
|
|
9
|
+
|
|
10
|
+
export const createRouteFunctions = [
|
|
11
|
+
...createRouteFunctionsDirect,
|
|
12
|
+
...createRouteFunctionsIndirect,
|
|
13
|
+
] as const
|
|
14
|
+
|
|
15
|
+
export type CreateRouteFunction = (typeof createRouteFunctions)[number]
|
|
16
|
+
|
|
17
|
+
export const checkedProperties = [
|
|
18
|
+
'params',
|
|
19
|
+
'validateSearch',
|
|
20
|
+
'context',
|
|
21
|
+
'beforeLoad',
|
|
22
|
+
'loaderDeps',
|
|
23
|
+
'loader',
|
|
24
|
+
] as const
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
checkedProperties,
|
|
8
|
+
createRouteFunctions,
|
|
9
|
+
createRouteFunctionsIndirect,
|
|
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
|
+
|
|
67
|
+
// TODO we need to support spread elements, they would be discarded here
|
|
68
|
+
const properties = allProperties.flatMap((p) => {
|
|
69
|
+
if (
|
|
70
|
+
p.type === AST_NODE_TYPES.Property &&
|
|
71
|
+
p.key.type === AST_NODE_TYPES.Identifier
|
|
72
|
+
) {
|
|
73
|
+
return { name: p.key.name, property: p }
|
|
74
|
+
} else if (p.type === AST_NODE_TYPES.SpreadElement) {
|
|
75
|
+
if (p.argument.type === AST_NODE_TYPES.Identifier) {
|
|
76
|
+
return { name: p.argument.name, property: p }
|
|
77
|
+
} else {
|
|
78
|
+
throw new Error('Unsupported spread element')
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return []
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const sortedProperties = sortDataByOrder(
|
|
85
|
+
properties,
|
|
86
|
+
checkedProperties,
|
|
87
|
+
'name',
|
|
88
|
+
)
|
|
89
|
+
if (sortedProperties === null) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
context.report({
|
|
93
|
+
node: argument,
|
|
94
|
+
data: { function: node.callee.name },
|
|
95
|
+
messageId: 'invalidOrder',
|
|
96
|
+
fix(fixer) {
|
|
97
|
+
const sourceCode = context.sourceCode
|
|
98
|
+
|
|
99
|
+
const text = sortedProperties.reduce(
|
|
100
|
+
(sourceText, specifier, index) => {
|
|
101
|
+
let text = ''
|
|
102
|
+
if (index < allProperties.length - 1) {
|
|
103
|
+
text = sourceCode
|
|
104
|
+
.getText()
|
|
105
|
+
.slice(
|
|
106
|
+
allProperties[index]!.range[1],
|
|
107
|
+
allProperties[index + 1]!.range[0],
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
return (
|
|
111
|
+
sourceText + sourceCode.getText(specifier.property) + text
|
|
112
|
+
)
|
|
113
|
+
},
|
|
114
|
+
'',
|
|
115
|
+
)
|
|
116
|
+
return fixer.replaceTextRange(
|
|
117
|
+
[allProperties[0]!.range[0], allProperties.at(-1)!.range[1]],
|
|
118
|
+
text,
|
|
119
|
+
)
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
}),
|
|
125
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function sortDataByOrder<T, TKey extends keyof T>(
|
|
2
|
+
data: Array<T> | ReadonlyArray<T>,
|
|
3
|
+
orderArray: Array<T[TKey]> | ReadonlyArray<T[TKey]>,
|
|
4
|
+
key: TKey,
|
|
5
|
+
): Array<T> | null {
|
|
6
|
+
const orderMap = new Map(orderArray.map((item, index) => [item, index]))
|
|
7
|
+
|
|
8
|
+
// Separate items that are in orderArray from those that are not
|
|
9
|
+
const inOrderArray = data
|
|
10
|
+
.filter((item) => orderMap.has(item[key]))
|
|
11
|
+
.sort((a, b) => {
|
|
12
|
+
const indexA = orderMap.get(a[key])!
|
|
13
|
+
const indexB = orderMap.get(b[key])!
|
|
14
|
+
|
|
15
|
+
return indexA - indexB
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const inOrderIterator = inOrderArray.values()
|
|
19
|
+
|
|
20
|
+
// `as boolean` is needed to avoid TS incorrectly inferring that wasResorted is always `true`
|
|
21
|
+
let wasResorted = false as boolean
|
|
22
|
+
|
|
23
|
+
const result = data.map((item) => {
|
|
24
|
+
if (orderMap.has(item[key])) {
|
|
25
|
+
const sortedItem = inOrderIterator.next().value!
|
|
26
|
+
if (sortedItem[key] !== item[key]) {
|
|
27
|
+
wasResorted = true
|
|
28
|
+
}
|
|
29
|
+
return sortedItem
|
|
30
|
+
}
|
|
31
|
+
return item
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (!wasResorted) {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
return result
|
|
38
|
+
}
|
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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/utils'
|
|
2
|
+
import type { ESLintUtils, TSESLint } from '@typescript-eslint/utils'
|
|
3
|
+
|
|
4
|
+
type Create = Parameters<
|
|
5
|
+
ReturnType<typeof ESLintUtils.RuleCreator>
|
|
6
|
+
>[0]['create']
|
|
7
|
+
|
|
8
|
+
type Context = Parameters<Create>[0]
|
|
9
|
+
type Options = Parameters<Create>[1]
|
|
10
|
+
type Helpers = {
|
|
11
|
+
isSpecificTanstackRouterImport: (
|
|
12
|
+
node: TSESTree.Identifier,
|
|
13
|
+
source: string,
|
|
14
|
+
) => boolean
|
|
15
|
+
isTanstackRouterImport: (node: TSESTree.Identifier) => boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type EnhancedCreate = (
|
|
19
|
+
context: Context,
|
|
20
|
+
options: Options,
|
|
21
|
+
helpers: Helpers,
|
|
22
|
+
) => ReturnType<Create>
|
|
23
|
+
|
|
24
|
+
export function detectTanstackRouterImports(create: EnhancedCreate): Create {
|
|
25
|
+
return (context, optionsWithDefault) => {
|
|
26
|
+
const tanstackRouterImportSpecifiers: Array<TSESTree.ImportClause> = []
|
|
27
|
+
|
|
28
|
+
const helpers: Helpers = {
|
|
29
|
+
isSpecificTanstackRouterImport(node, source) {
|
|
30
|
+
return !!tanstackRouterImportSpecifiers.find((specifier) => {
|
|
31
|
+
if (
|
|
32
|
+
specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier &&
|
|
33
|
+
specifier.parent.type ===
|
|
34
|
+
TSESTree.AST_NODE_TYPES.ImportDeclaration &&
|
|
35
|
+
specifier.parent.source.value === source
|
|
36
|
+
) {
|
|
37
|
+
return node.name === specifier.local.name
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return false
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
isTanstackRouterImport(node) {
|
|
44
|
+
return !!tanstackRouterImportSpecifiers.find((specifier) => {
|
|
45
|
+
if (specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier) {
|
|
46
|
+
return node.name === specifier.local.name
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return false
|
|
50
|
+
})
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const detectionInstructions: TSESLint.RuleListener = {
|
|
55
|
+
ImportDeclaration(node) {
|
|
56
|
+
if (
|
|
57
|
+
node.specifiers.length > 0 &&
|
|
58
|
+
node.importKind === 'value' &&
|
|
59
|
+
node.source.value.startsWith('@tanstack/') &&
|
|
60
|
+
node.source.value.endsWith('-router')
|
|
61
|
+
) {
|
|
62
|
+
tanstackRouterImportSpecifiers.push(...node.specifiers)
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Call original rule definition
|
|
68
|
+
const ruleInstructions = create(context, optionsWithDefault, helpers)
|
|
69
|
+
const enhancedRuleInstructions: TSESLint.RuleListener = {}
|
|
70
|
+
|
|
71
|
+
const allKeys = new Set(
|
|
72
|
+
Object.keys(detectionInstructions).concat(Object.keys(ruleInstructions)),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
// Iterate over ALL instructions keys so we can override original rule instructions
|
|
76
|
+
// to prevent their execution if conditions to report errors are not met.
|
|
77
|
+
allKeys.forEach((instruction) => {
|
|
78
|
+
enhancedRuleInstructions[instruction] = (node) => {
|
|
79
|
+
if (instruction in detectionInstructions) {
|
|
80
|
+
detectionInstructions[instruction]?.(node)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ruleFunction = ruleInstructions[instruction]
|
|
84
|
+
if (ruleFunction !== undefined) {
|
|
85
|
+
return ruleFunction(node)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return enhancedRuleInstructions
|
|
93
|
+
}
|
|
94
|
+
}
|