@tanstack/eslint-plugin-router 1.154.7 → 1.161.2
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/dist/cjs/index.cjs +4 -2
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/rules/route-param-names/constants.cjs +13 -0
- package/dist/cjs/rules/route-param-names/constants.cjs.map +1 -0
- package/dist/cjs/rules/route-param-names/constants.d.cts +23 -0
- package/dist/cjs/rules/route-param-names/route-param-names.rule.cjs +98 -0
- package/dist/cjs/rules/route-param-names/route-param-names.rule.cjs.map +1 -0
- package/dist/cjs/rules/route-param-names/route-param-names.rule.d.cts +4 -0
- package/dist/cjs/rules/route-param-names/route-param-names.utils.cjs +61 -0
- package/dist/cjs/rules/route-param-names/route-param-names.utils.cjs.map +1 -0
- package/dist/cjs/rules/route-param-names/route-param-names.utils.d.cts +43 -0
- package/dist/cjs/rules.cjs +3 -1
- package/dist/cjs/rules.cjs.map +1 -1
- package/dist/cjs/utils/detect-router-imports.cjs +2 -1
- package/dist/cjs/utils/detect-router-imports.cjs.map +1 -1
- package/dist/esm/index.js +4 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/rules/route-param-names/constants.d.ts +23 -0
- package/dist/esm/rules/route-param-names/constants.js +13 -0
- package/dist/esm/rules/route-param-names/constants.js.map +1 -0
- package/dist/esm/rules/route-param-names/route-param-names.rule.d.ts +4 -0
- package/dist/esm/rules/route-param-names/route-param-names.rule.js +98 -0
- package/dist/esm/rules/route-param-names/route-param-names.rule.js.map +1 -0
- package/dist/esm/rules/route-param-names/route-param-names.utils.d.ts +43 -0
- package/dist/esm/rules/route-param-names/route-param-names.utils.js +61 -0
- package/dist/esm/rules/route-param-names/route-param-names.utils.js.map +1 -0
- package/dist/esm/rules.js +3 -1
- package/dist/esm/rules.js.map +1 -1
- package/dist/esm/utils/detect-router-imports.js +2 -1
- package/dist/esm/utils/detect-router-imports.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/route-param-names.rule.test.ts +271 -0
- package/src/__tests__/route-param-names.utils.test.ts +174 -0
- package/src/index.ts +2 -0
- package/src/rules/route-param-names/constants.ts +36 -0
- package/src/rules/route-param-names/route-param-names.rule.ts +127 -0
- package/src/rules/route-param-names/route-param-names.utils.ts +122 -0
- package/src/rules.ts +2 -0
- package/src/utils/detect-router-imports.ts +2 -1
|
@@ -0,0 +1,127 @@
|
|
|
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 { getInvalidParams } from './route-param-names.utils'
|
|
6
|
+
import { pathAsFirstArgFunctions, pathAsPropertyFunctions } from './constants'
|
|
7
|
+
import type { TSESTree } from '@typescript-eslint/utils'
|
|
8
|
+
import type { ExtraRuleDocs } from '../../types'
|
|
9
|
+
|
|
10
|
+
const createRule = ESLintUtils.RuleCreator<ExtraRuleDocs>(getDocsUrl)
|
|
11
|
+
|
|
12
|
+
const pathAsFirstArgSet = new Set<string>(pathAsFirstArgFunctions)
|
|
13
|
+
const pathAsPropertySet = new Set<string>(pathAsPropertyFunctions)
|
|
14
|
+
|
|
15
|
+
export const name = 'route-param-names'
|
|
16
|
+
|
|
17
|
+
export const rule = createRule({
|
|
18
|
+
name,
|
|
19
|
+
meta: {
|
|
20
|
+
type: 'problem',
|
|
21
|
+
docs: {
|
|
22
|
+
description: 'Ensure route param names are valid JavaScript identifiers',
|
|
23
|
+
recommended: 'error',
|
|
24
|
+
},
|
|
25
|
+
messages: {
|
|
26
|
+
invalidParamName:
|
|
27
|
+
'Invalid param name "{{paramName}}" in route path. Param names must be valid JavaScript identifiers (match /[a-zA-Z_$][a-zA-Z0-9_$]*/).',
|
|
28
|
+
},
|
|
29
|
+
schema: [],
|
|
30
|
+
},
|
|
31
|
+
defaultOptions: [],
|
|
32
|
+
|
|
33
|
+
create: detectTanstackRouterImports((context, _, helpers) => {
|
|
34
|
+
function reportInvalidParams(node: TSESTree.Node, path: string) {
|
|
35
|
+
const invalidParams = getInvalidParams(path)
|
|
36
|
+
|
|
37
|
+
for (const param of invalidParams) {
|
|
38
|
+
context.report({
|
|
39
|
+
node,
|
|
40
|
+
messageId: 'invalidParamName',
|
|
41
|
+
data: { paramName: param.paramName },
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getStringLiteralValue(node: TSESTree.Node): string | null {
|
|
47
|
+
if (
|
|
48
|
+
node.type === AST_NODE_TYPES.Literal &&
|
|
49
|
+
typeof node.value === 'string'
|
|
50
|
+
) {
|
|
51
|
+
return node.value
|
|
52
|
+
}
|
|
53
|
+
if (
|
|
54
|
+
node.type === AST_NODE_TYPES.TemplateLiteral &&
|
|
55
|
+
node.quasis.length === 1
|
|
56
|
+
) {
|
|
57
|
+
const cooked = node.quasis[0]?.value.cooked
|
|
58
|
+
if (cooked != null) {
|
|
59
|
+
return cooked
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
CallExpression(node) {
|
|
67
|
+
// Handle direct function call: createRoute({ path: '...' })
|
|
68
|
+
if (node.callee.type === AST_NODE_TYPES.Identifier) {
|
|
69
|
+
const funcName = node.callee.name
|
|
70
|
+
|
|
71
|
+
// Skip if not imported from TanStack Router
|
|
72
|
+
if (!helpers.isTanstackRouterImport(node.callee)) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Case: createRoute({ path: '/path/$param' }) or createRoute({ 'path': '/path/$param' })
|
|
77
|
+
if (pathAsPropertySet.has(funcName)) {
|
|
78
|
+
const arg = node.arguments[0]
|
|
79
|
+
if (arg?.type === AST_NODE_TYPES.ObjectExpression) {
|
|
80
|
+
for (const prop of arg.properties) {
|
|
81
|
+
if (prop.type === AST_NODE_TYPES.Property) {
|
|
82
|
+
const isPathKey =
|
|
83
|
+
(prop.key.type === AST_NODE_TYPES.Identifier &&
|
|
84
|
+
prop.key.name === 'path') ||
|
|
85
|
+
(prop.key.type === AST_NODE_TYPES.Literal &&
|
|
86
|
+
prop.key.value === 'path')
|
|
87
|
+
if (isPathKey) {
|
|
88
|
+
const pathValue = getStringLiteralValue(prop.value)
|
|
89
|
+
if (pathValue) {
|
|
90
|
+
reportInvalidParams(prop.value, pathValue)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Handle curried function call: createFileRoute('/path')({ ... })
|
|
101
|
+
if (node.callee.type === AST_NODE_TYPES.CallExpression) {
|
|
102
|
+
const innerCall = node.callee
|
|
103
|
+
|
|
104
|
+
if (innerCall.callee.type === AST_NODE_TYPES.Identifier) {
|
|
105
|
+
const funcName = innerCall.callee.name
|
|
106
|
+
|
|
107
|
+
// Skip if not imported from TanStack Router
|
|
108
|
+
if (!helpers.isTanstackRouterImport(innerCall.callee)) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Case: createFileRoute('/path/$param')(...) or similar
|
|
113
|
+
if (pathAsFirstArgSet.has(funcName)) {
|
|
114
|
+
const pathArg = innerCall.arguments[0]
|
|
115
|
+
if (pathArg) {
|
|
116
|
+
const pathValue = getStringLiteralValue(pathArg)
|
|
117
|
+
if (pathValue) {
|
|
118
|
+
reportInvalidParams(pathArg, pathValue)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
}),
|
|
127
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { VALID_PARAM_NAME_REGEX } from './constants'
|
|
2
|
+
|
|
3
|
+
export interface ExtractedParam {
|
|
4
|
+
/** The full param string including $ prefix (e.g., "$userId", "-$optional") */
|
|
5
|
+
fullParam: string
|
|
6
|
+
/** The param name without $ prefix (e.g., "userId", "optional") */
|
|
7
|
+
paramName: string
|
|
8
|
+
/** Whether this is an optional param (prefixed with -$) */
|
|
9
|
+
isOptional: boolean
|
|
10
|
+
/** Whether this param name is valid */
|
|
11
|
+
isValid: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extracts param names from a route path segment.
|
|
16
|
+
*
|
|
17
|
+
* Handles these patterns:
|
|
18
|
+
* - $paramName -> extract "paramName"
|
|
19
|
+
* - {$paramName} -> extract "paramName"
|
|
20
|
+
* - prefix{$paramName}suffix -> extract "paramName"
|
|
21
|
+
* - {-$paramName} -> extract "paramName" (optional)
|
|
22
|
+
* - prefix{-$paramName}suffix -> extract "paramName" (optional)
|
|
23
|
+
* - $ or {$} -> wildcard, skip validation
|
|
24
|
+
*/
|
|
25
|
+
export function extractParamsFromSegment(
|
|
26
|
+
segment: string,
|
|
27
|
+
): Array<ExtractedParam> {
|
|
28
|
+
const params: Array<ExtractedParam> = []
|
|
29
|
+
|
|
30
|
+
// Skip empty segments
|
|
31
|
+
if (!segment || !segment.includes('$')) {
|
|
32
|
+
return params
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check for wildcard ($ alone or {$})
|
|
36
|
+
if (segment === '$' || segment === '{$}') {
|
|
37
|
+
return params // Wildcard, no param name to validate
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Pattern 1: Simple $paramName (entire segment starts with $)
|
|
41
|
+
if (segment.startsWith('$') && !segment.includes('{')) {
|
|
42
|
+
const paramName = segment.slice(1)
|
|
43
|
+
if (paramName) {
|
|
44
|
+
params.push({
|
|
45
|
+
fullParam: segment,
|
|
46
|
+
paramName,
|
|
47
|
+
isOptional: false,
|
|
48
|
+
isValid: VALID_PARAM_NAME_REGEX.test(paramName),
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
return params
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Pattern 2: Braces pattern {$paramName} or {-$paramName} with optional prefix/suffix
|
|
55
|
+
// Match patterns like: prefix{$param}suffix, {$param}, {-$param}
|
|
56
|
+
const bracePattern = /\{(-?\$)([^}]*)\}/g
|
|
57
|
+
let match
|
|
58
|
+
|
|
59
|
+
while ((match = bracePattern.exec(segment)) !== null) {
|
|
60
|
+
const prefix = match[1] // "$" or "-$"
|
|
61
|
+
const paramName = match[2] // The param name after $ or -$
|
|
62
|
+
|
|
63
|
+
if (!paramName) {
|
|
64
|
+
// This is a wildcard {$} or {-$}, skip
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const isOptional = prefix === '-$'
|
|
69
|
+
|
|
70
|
+
params.push({
|
|
71
|
+
fullParam: `${prefix}${paramName}`,
|
|
72
|
+
paramName,
|
|
73
|
+
isOptional,
|
|
74
|
+
isValid: VALID_PARAM_NAME_REGEX.test(paramName),
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return params
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extracts all params from a route path.
|
|
83
|
+
*
|
|
84
|
+
* @param path - The route path (e.g., "/users/$userId/posts/$postId")
|
|
85
|
+
* @returns Array of extracted params with validation info
|
|
86
|
+
*/
|
|
87
|
+
export function extractParamsFromPath(path: string): Array<ExtractedParam> {
|
|
88
|
+
if (!path || !path.includes('$')) {
|
|
89
|
+
return []
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const segments = path.split('/')
|
|
93
|
+
const allParams: Array<ExtractedParam> = []
|
|
94
|
+
|
|
95
|
+
for (const segment of segments) {
|
|
96
|
+
const params = extractParamsFromSegment(segment)
|
|
97
|
+
allParams.push(...params)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return allParams
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validates a single param name.
|
|
105
|
+
*
|
|
106
|
+
* @param paramName - The param name to validate (without $ prefix)
|
|
107
|
+
* @returns Whether the param name is valid
|
|
108
|
+
*/
|
|
109
|
+
export function isValidParamName(paramName: string): boolean {
|
|
110
|
+
return VALID_PARAM_NAME_REGEX.test(paramName)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Gets all invalid params from a route path.
|
|
115
|
+
*
|
|
116
|
+
* @param path - The route path
|
|
117
|
+
* @returns Array of invalid param info
|
|
118
|
+
*/
|
|
119
|
+
export function getInvalidParams(path: string): Array<ExtractedParam> {
|
|
120
|
+
const params = extractParamsFromPath(path)
|
|
121
|
+
return params.filter((p) => !p.isValid)
|
|
122
|
+
}
|
package/src/rules.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as createRoutePropertyOrder from './rules/create-route-property-order/create-route-property-order.rule'
|
|
2
|
+
import * as routeParamNames from './rules/route-param-names/route-param-names.rule'
|
|
2
3
|
import type { ESLintUtils } from '@typescript-eslint/utils'
|
|
3
4
|
import type { ExtraRuleDocs } from './types'
|
|
4
5
|
|
|
@@ -12,4 +13,5 @@ export const rules: Record<
|
|
|
12
13
|
>
|
|
13
14
|
> = {
|
|
14
15
|
[createRoutePropertyOrder.name]: createRoutePropertyOrder.rule,
|
|
16
|
+
[routeParamNames.name]: routeParamNames.rule,
|
|
15
17
|
}
|
|
@@ -55,7 +55,8 @@ export function detectTanstackRouterImports(create: EnhancedCreate): Create {
|
|
|
55
55
|
ImportDeclaration(node) {
|
|
56
56
|
if (
|
|
57
57
|
node.specifiers.length > 0 &&
|
|
58
|
-
|
|
58
|
+
// `importKind` is parser-dependent and can be undefined (eg. Espree)
|
|
59
|
+
node.importKind !== 'type' &&
|
|
59
60
|
node.source.value.startsWith('@tanstack/') &&
|
|
60
61
|
node.source.value.endsWith('-router')
|
|
61
62
|
) {
|