@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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/index.cjs +29 -0
  3. package/dist/cjs/index.cjs.map +1 -0
  4. package/dist/cjs/index.d.cts +13 -0
  5. package/dist/cjs/rules/create-route-property-order/constants.cjs +27 -0
  6. package/dist/cjs/rules/create-route-property-order/constants.cjs.map +1 -0
  7. package/dist/cjs/rules/create-route-property-order/constants.d.cts +5 -0
  8. package/dist/cjs/rules/create-route-property-order/create-route-property-order.rule.cjs +104 -0
  9. package/dist/cjs/rules/create-route-property-order/create-route-property-order.rule.cjs.map +1 -0
  10. package/dist/cjs/rules/create-route-property-order/create-route-property-order.rule.d.cts +4 -0
  11. package/dist/cjs/rules/create-route-property-order/create-route-property-order.utils.cjs +28 -0
  12. package/dist/cjs/rules/create-route-property-order/create-route-property-order.utils.cjs.map +1 -0
  13. package/dist/cjs/rules/create-route-property-order/create-route-property-order.utils.d.cts +1 -0
  14. package/dist/cjs/rules.cjs +8 -0
  15. package/dist/cjs/rules.cjs.map +1 -0
  16. package/dist/cjs/rules.d.cts +3 -0
  17. package/dist/cjs/types.d.cts +3 -0
  18. package/dist/cjs/utils/detect-router-imports.cjs +54 -0
  19. package/dist/cjs/utils/detect-router-imports.cjs.map +1 -0
  20. package/dist/cjs/utils/detect-router-imports.d.cts +11 -0
  21. package/dist/cjs/utils/get-docs-url.cjs +5 -0
  22. package/dist/cjs/utils/get-docs-url.cjs.map +1 -0
  23. package/dist/cjs/utils/get-docs-url.d.cts +1 -0
  24. package/dist/esm/index.d.ts +13 -0
  25. package/dist/esm/index.js +30 -0
  26. package/dist/esm/index.js.map +1 -0
  27. package/dist/esm/rules/create-route-property-order/constants.d.ts +5 -0
  28. package/dist/esm/rules/create-route-property-order/constants.js +27 -0
  29. package/dist/esm/rules/create-route-property-order/constants.js.map +1 -0
  30. package/dist/esm/rules/create-route-property-order/create-route-property-order.rule.d.ts +4 -0
  31. package/dist/esm/rules/create-route-property-order/create-route-property-order.rule.js +104 -0
  32. package/dist/esm/rules/create-route-property-order/create-route-property-order.rule.js.map +1 -0
  33. package/dist/esm/rules/create-route-property-order/create-route-property-order.utils.d.ts +1 -0
  34. package/dist/esm/rules/create-route-property-order/create-route-property-order.utils.js +28 -0
  35. package/dist/esm/rules/create-route-property-order/create-route-property-order.utils.js.map +1 -0
  36. package/dist/esm/rules.d.ts +3 -0
  37. package/dist/esm/rules.js +8 -0
  38. package/dist/esm/rules.js.map +1 -0
  39. package/dist/esm/types.d.ts +3 -0
  40. package/dist/esm/utils/detect-router-imports.d.ts +11 -0
  41. package/dist/esm/utils/detect-router-imports.js +54 -0
  42. package/dist/esm/utils/detect-router-imports.js.map +1 -0
  43. package/dist/esm/utils/get-docs-url.d.ts +1 -0
  44. package/dist/esm/utils/get-docs-url.js +5 -0
  45. package/dist/esm/utils/get-docs-url.js.map +1 -0
  46. package/package.json +51 -0
  47. package/src/__tests__/create-route-property-order.rule.test.ts +153 -0
  48. package/src/__tests__/create-route-property-order.utils.test.ts +58 -0
  49. package/src/__tests__/test-utils.test.ts +104 -0
  50. package/src/__tests__/test-utils.ts +108 -0
  51. package/src/index.ts +43 -0
  52. package/src/rules/create-route-property-order/constants.ts +24 -0
  53. package/src/rules/create-route-property-order/create-route-property-order.rule.ts +125 -0
  54. package/src/rules/create-route-property-order/create-route-property-order.utils.ts +38 -0
  55. package/src/rules.ts +15 -0
  56. package/src/types.ts +3 -0
  57. package/src/utils/detect-router-imports.ts +94 -0
  58. 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,3 @@
1
+ export type ExtraRuleDocs = {
2
+ recommended: 'strict' | 'error' | 'warn'
3
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export const getDocsUrl = (ruleName: string): string =>
2
+ `https://tanstack.com/router/latest/docs/eslint/${ruleName}`