@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.
Files changed (39) hide show
  1. package/dist/cjs/index.cjs +4 -2
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/rules/route-param-names/constants.cjs +13 -0
  4. package/dist/cjs/rules/route-param-names/constants.cjs.map +1 -0
  5. package/dist/cjs/rules/route-param-names/constants.d.cts +23 -0
  6. package/dist/cjs/rules/route-param-names/route-param-names.rule.cjs +98 -0
  7. package/dist/cjs/rules/route-param-names/route-param-names.rule.cjs.map +1 -0
  8. package/dist/cjs/rules/route-param-names/route-param-names.rule.d.cts +4 -0
  9. package/dist/cjs/rules/route-param-names/route-param-names.utils.cjs +61 -0
  10. package/dist/cjs/rules/route-param-names/route-param-names.utils.cjs.map +1 -0
  11. package/dist/cjs/rules/route-param-names/route-param-names.utils.d.cts +43 -0
  12. package/dist/cjs/rules.cjs +3 -1
  13. package/dist/cjs/rules.cjs.map +1 -1
  14. package/dist/cjs/utils/detect-router-imports.cjs +2 -1
  15. package/dist/cjs/utils/detect-router-imports.cjs.map +1 -1
  16. package/dist/esm/index.js +4 -2
  17. package/dist/esm/index.js.map +1 -1
  18. package/dist/esm/rules/route-param-names/constants.d.ts +23 -0
  19. package/dist/esm/rules/route-param-names/constants.js +13 -0
  20. package/dist/esm/rules/route-param-names/constants.js.map +1 -0
  21. package/dist/esm/rules/route-param-names/route-param-names.rule.d.ts +4 -0
  22. package/dist/esm/rules/route-param-names/route-param-names.rule.js +98 -0
  23. package/dist/esm/rules/route-param-names/route-param-names.rule.js.map +1 -0
  24. package/dist/esm/rules/route-param-names/route-param-names.utils.d.ts +43 -0
  25. package/dist/esm/rules/route-param-names/route-param-names.utils.js +61 -0
  26. package/dist/esm/rules/route-param-names/route-param-names.utils.js.map +1 -0
  27. package/dist/esm/rules.js +3 -1
  28. package/dist/esm/rules.js.map +1 -1
  29. package/dist/esm/utils/detect-router-imports.js +2 -1
  30. package/dist/esm/utils/detect-router-imports.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/__tests__/route-param-names.rule.test.ts +271 -0
  33. package/src/__tests__/route-param-names.utils.test.ts +174 -0
  34. package/src/index.ts +2 -0
  35. package/src/rules/route-param-names/constants.ts +36 -0
  36. package/src/rules/route-param-names/route-param-names.rule.ts +127 -0
  37. package/src/rules/route-param-names/route-param-names.utils.ts +122 -0
  38. package/src/rules.ts +2 -0
  39. package/src/utils/detect-router-imports.ts +2 -1
@@ -0,0 +1,61 @@
1
+ import { VALID_PARAM_NAME_REGEX } from "./constants.js";
2
+ function extractParamsFromSegment(segment) {
3
+ const params = [];
4
+ if (!segment || !segment.includes("$")) {
5
+ return params;
6
+ }
7
+ if (segment === "$" || segment === "{$}") {
8
+ return params;
9
+ }
10
+ if (segment.startsWith("$") && !segment.includes("{")) {
11
+ const paramName = segment.slice(1);
12
+ if (paramName) {
13
+ params.push({
14
+ fullParam: segment,
15
+ paramName,
16
+ isOptional: false,
17
+ isValid: VALID_PARAM_NAME_REGEX.test(paramName)
18
+ });
19
+ }
20
+ return params;
21
+ }
22
+ const bracePattern = /\{(-?\$)([^}]*)\}/g;
23
+ let match;
24
+ while ((match = bracePattern.exec(segment)) !== null) {
25
+ const prefix = match[1];
26
+ const paramName = match[2];
27
+ if (!paramName) {
28
+ continue;
29
+ }
30
+ const isOptional = prefix === "-$";
31
+ params.push({
32
+ fullParam: `${prefix}${paramName}`,
33
+ paramName,
34
+ isOptional,
35
+ isValid: VALID_PARAM_NAME_REGEX.test(paramName)
36
+ });
37
+ }
38
+ return params;
39
+ }
40
+ function extractParamsFromPath(path) {
41
+ if (!path || !path.includes("$")) {
42
+ return [];
43
+ }
44
+ const segments = path.split("/");
45
+ const allParams = [];
46
+ for (const segment of segments) {
47
+ const params = extractParamsFromSegment(segment);
48
+ allParams.push(...params);
49
+ }
50
+ return allParams;
51
+ }
52
+ function getInvalidParams(path) {
53
+ const params = extractParamsFromPath(path);
54
+ return params.filter((p) => !p.isValid);
55
+ }
56
+ export {
57
+ extractParamsFromPath,
58
+ extractParamsFromSegment,
59
+ getInvalidParams
60
+ };
61
+ //# sourceMappingURL=route-param-names.utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-param-names.utils.js","sources":["../../../../src/rules/route-param-names/route-param-names.utils.ts"],"sourcesContent":["import { VALID_PARAM_NAME_REGEX } from './constants'\n\nexport interface ExtractedParam {\n /** The full param string including $ prefix (e.g., \"$userId\", \"-$optional\") */\n fullParam: string\n /** The param name without $ prefix (e.g., \"userId\", \"optional\") */\n paramName: string\n /** Whether this is an optional param (prefixed with -$) */\n isOptional: boolean\n /** Whether this param name is valid */\n isValid: boolean\n}\n\n/**\n * Extracts param names from a route path segment.\n *\n * Handles these patterns:\n * - $paramName -> extract \"paramName\"\n * - {$paramName} -> extract \"paramName\"\n * - prefix{$paramName}suffix -> extract \"paramName\"\n * - {-$paramName} -> extract \"paramName\" (optional)\n * - prefix{-$paramName}suffix -> extract \"paramName\" (optional)\n * - $ or {$} -> wildcard, skip validation\n */\nexport function extractParamsFromSegment(\n segment: string,\n): Array<ExtractedParam> {\n const params: Array<ExtractedParam> = []\n\n // Skip empty segments\n if (!segment || !segment.includes('$')) {\n return params\n }\n\n // Check for wildcard ($ alone or {$})\n if (segment === '$' || segment === '{$}') {\n return params // Wildcard, no param name to validate\n }\n\n // Pattern 1: Simple $paramName (entire segment starts with $)\n if (segment.startsWith('$') && !segment.includes('{')) {\n const paramName = segment.slice(1)\n if (paramName) {\n params.push({\n fullParam: segment,\n paramName,\n isOptional: false,\n isValid: VALID_PARAM_NAME_REGEX.test(paramName),\n })\n }\n return params\n }\n\n // Pattern 2: Braces pattern {$paramName} or {-$paramName} with optional prefix/suffix\n // Match patterns like: prefix{$param}suffix, {$param}, {-$param}\n const bracePattern = /\\{(-?\\$)([^}]*)\\}/g\n let match\n\n while ((match = bracePattern.exec(segment)) !== null) {\n const prefix = match[1] // \"$\" or \"-$\"\n const paramName = match[2] // The param name after $ or -$\n\n if (!paramName) {\n // This is a wildcard {$} or {-$}, skip\n continue\n }\n\n const isOptional = prefix === '-$'\n\n params.push({\n fullParam: `${prefix}${paramName}`,\n paramName,\n isOptional,\n isValid: VALID_PARAM_NAME_REGEX.test(paramName),\n })\n }\n\n return params\n}\n\n/**\n * Extracts all params from a route path.\n *\n * @param path - The route path (e.g., \"/users/$userId/posts/$postId\")\n * @returns Array of extracted params with validation info\n */\nexport function extractParamsFromPath(path: string): Array<ExtractedParam> {\n if (!path || !path.includes('$')) {\n return []\n }\n\n const segments = path.split('/')\n const allParams: Array<ExtractedParam> = []\n\n for (const segment of segments) {\n const params = extractParamsFromSegment(segment)\n allParams.push(...params)\n }\n\n return allParams\n}\n\n/**\n * Validates a single param name.\n *\n * @param paramName - The param name to validate (without $ prefix)\n * @returns Whether the param name is valid\n */\nexport function isValidParamName(paramName: string): boolean {\n return VALID_PARAM_NAME_REGEX.test(paramName)\n}\n\n/**\n * Gets all invalid params from a route path.\n *\n * @param path - The route path\n * @returns Array of invalid param info\n */\nexport function getInvalidParams(path: string): Array<ExtractedParam> {\n const params = extractParamsFromPath(path)\n return params.filter((p) => !p.isValid)\n}\n"],"names":[],"mappings":";AAwBO,SAAS,yBACd,SACuB;AACvB,QAAM,SAAgC,CAAA;AAGtC,MAAI,CAAC,WAAW,CAAC,QAAQ,SAAS,GAAG,GAAG;AACtC,WAAO;AAAA,EACT;AAGA,MAAI,YAAY,OAAO,YAAY,OAAO;AACxC,WAAO;AAAA,EACT;AAGA,MAAI,QAAQ,WAAW,GAAG,KAAK,CAAC,QAAQ,SAAS,GAAG,GAAG;AACrD,UAAM,YAAY,QAAQ,MAAM,CAAC;AACjC,QAAI,WAAW;AACb,aAAO,KAAK;AAAA,QACV,WAAW;AAAA,QACX;AAAA,QACA,YAAY;AAAA,QACZ,SAAS,uBAAuB,KAAK,SAAS;AAAA,MAAA,CAC/C;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAIA,QAAM,eAAe;AACrB,MAAI;AAEJ,UAAQ,QAAQ,aAAa,KAAK,OAAO,OAAO,MAAM;AACpD,UAAM,SAAS,MAAM,CAAC;AACtB,UAAM,YAAY,MAAM,CAAC;AAEzB,QAAI,CAAC,WAAW;AAEd;AAAA,IACF;AAEA,UAAM,aAAa,WAAW;AAE9B,WAAO,KAAK;AAAA,MACV,WAAW,GAAG,MAAM,GAAG,SAAS;AAAA,MAChC;AAAA,MACA;AAAA,MACA,SAAS,uBAAuB,KAAK,SAAS;AAAA,IAAA,CAC/C;AAAA,EACH;AAEA,SAAO;AACT;AAQO,SAAS,sBAAsB,MAAqC;AACzE,MAAI,CAAC,QAAQ,CAAC,KAAK,SAAS,GAAG,GAAG;AAChC,WAAO,CAAA;AAAA,EACT;AAEA,QAAM,WAAW,KAAK,MAAM,GAAG;AAC/B,QAAM,YAAmC,CAAA;AAEzC,aAAW,WAAW,UAAU;AAC9B,UAAM,SAAS,yBAAyB,OAAO;AAC/C,cAAU,KAAK,GAAG,MAAM;AAAA,EAC1B;AAEA,SAAO;AACT;AAkBO,SAAS,iBAAiB,MAAqC;AACpE,QAAM,SAAS,sBAAsB,IAAI;AACzC,SAAO,OAAO,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO;AACxC;"}
package/dist/esm/rules.js CHANGED
@@ -1,5 +1,7 @@
1
- import { rule, name } from "./rules/create-route-property-order/create-route-property-order.rule.js";
1
+ import { rule as rule$1, name as name$1 } from "./rules/create-route-property-order/create-route-property-order.rule.js";
2
+ import { rule, name } from "./rules/route-param-names/route-param-names.rule.js";
2
3
  const rules = {
4
+ [name$1]: rule$1,
3
5
  [name]: rule
4
6
  };
5
7
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"rules.js","sources":["../../src/rules.ts"],"sourcesContent":["import * as createRoutePropertyOrder from './rules/create-route-property-order/create-route-property-order.rule'\nimport type { ESLintUtils } from '@typescript-eslint/utils'\nimport type { ExtraRuleDocs } from './types'\n\nexport const rules: Record<\n string,\n ESLintUtils.RuleModule<\n string,\n ReadonlyArray<unknown>,\n ExtraRuleDocs,\n ESLintUtils.RuleListener\n >\n> = {\n [createRoutePropertyOrder.name]: createRoutePropertyOrder.rule,\n}\n"],"names":["createRoutePropertyOrder.name","createRoutePropertyOrder.rule"],"mappings":";AAIO,MAAM,QAQT;AAAA,EACF,CAACA,IAA6B,GAAGC;AACnC;"}
1
+ {"version":3,"file":"rules.js","sources":["../../src/rules.ts"],"sourcesContent":["import * as createRoutePropertyOrder from './rules/create-route-property-order/create-route-property-order.rule'\nimport * as routeParamNames from './rules/route-param-names/route-param-names.rule'\nimport type { ESLintUtils } from '@typescript-eslint/utils'\nimport type { ExtraRuleDocs } from './types'\n\nexport const rules: Record<\n string,\n ESLintUtils.RuleModule<\n string,\n ReadonlyArray<unknown>,\n ExtraRuleDocs,\n ESLintUtils.RuleListener\n >\n> = {\n [createRoutePropertyOrder.name]: createRoutePropertyOrder.rule,\n [routeParamNames.name]: routeParamNames.rule,\n}\n"],"names":["createRoutePropertyOrder.name","createRoutePropertyOrder.rule","routeParamNames.name","routeParamNames.rule"],"mappings":";;AAKO,MAAM,QAQT;AAAA,EACF,CAACA,MAA6B,GAAGC;AAAAA,EACjC,CAACC,IAAoB,GAAGC;AAC1B;"}
@@ -22,7 +22,8 @@ function detectTanstackRouterImports(create) {
22
22
  };
23
23
  const detectionInstructions = {
24
24
  ImportDeclaration(node) {
25
- if (node.specifiers.length > 0 && node.importKind === "value" && node.source.value.startsWith("@tanstack/") && node.source.value.endsWith("-router")) {
25
+ if (node.specifiers.length > 0 && // `importKind` is parser-dependent and can be undefined (eg. Espree)
26
+ node.importKind !== "type" && node.source.value.startsWith("@tanstack/") && node.source.value.endsWith("-router")) {
26
27
  tanstackRouterImportSpecifiers.push(...node.specifiers);
27
28
  }
28
29
  }
@@ -1 +1 @@
1
- {"version":3,"file":"detect-router-imports.js","sources":["../../../src/utils/detect-router-imports.ts"],"sourcesContent":["import { TSESTree } from '@typescript-eslint/utils'\nimport type { ESLintUtils, TSESLint } from '@typescript-eslint/utils'\n\ntype Create = Parameters<\n ReturnType<typeof ESLintUtils.RuleCreator>\n>[0]['create']\n\ntype Context = Parameters<Create>[0]\ntype Options = Parameters<Create>[1]\ntype Helpers = {\n isSpecificTanstackRouterImport: (\n node: TSESTree.Identifier,\n source: string,\n ) => boolean\n isTanstackRouterImport: (node: TSESTree.Identifier) => boolean\n}\n\ntype EnhancedCreate = (\n context: Context,\n options: Options,\n helpers: Helpers,\n) => ReturnType<Create>\n\nexport function detectTanstackRouterImports(create: EnhancedCreate): Create {\n return (context, optionsWithDefault) => {\n const tanstackRouterImportSpecifiers: Array<TSESTree.ImportClause> = []\n\n const helpers: Helpers = {\n isSpecificTanstackRouterImport(node, source) {\n return !!tanstackRouterImportSpecifiers.find((specifier) => {\n if (\n specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier &&\n specifier.parent.type ===\n TSESTree.AST_NODE_TYPES.ImportDeclaration &&\n specifier.parent.source.value === source\n ) {\n return node.name === specifier.local.name\n }\n\n return false\n })\n },\n isTanstackRouterImport(node) {\n return !!tanstackRouterImportSpecifiers.find((specifier) => {\n if (specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier) {\n return node.name === specifier.local.name\n }\n\n return false\n })\n },\n }\n\n const detectionInstructions: TSESLint.RuleListener = {\n ImportDeclaration(node) {\n if (\n node.specifiers.length > 0 &&\n node.importKind === 'value' &&\n node.source.value.startsWith('@tanstack/') &&\n node.source.value.endsWith('-router')\n ) {\n tanstackRouterImportSpecifiers.push(...node.specifiers)\n }\n },\n }\n\n // Call original rule definition\n const ruleInstructions = create(context, optionsWithDefault, helpers)\n const enhancedRuleInstructions: TSESLint.RuleListener = {}\n\n const allKeys = new Set(\n Object.keys(detectionInstructions).concat(Object.keys(ruleInstructions)),\n )\n\n // Iterate over ALL instructions keys so we can override original rule instructions\n // to prevent their execution if conditions to report errors are not met.\n allKeys.forEach((instruction) => {\n enhancedRuleInstructions[instruction] = (node) => {\n if (instruction in detectionInstructions) {\n detectionInstructions[instruction]?.(node)\n }\n\n const ruleFunction = ruleInstructions[instruction]\n if (ruleFunction !== undefined) {\n return ruleFunction(node)\n }\n\n return undefined\n }\n })\n\n return enhancedRuleInstructions\n }\n}\n"],"names":[],"mappings":";AAuBO,SAAS,4BAA4B,QAAgC;AAC1E,SAAO,CAAC,SAAS,uBAAuB;AACtC,UAAM,iCAA+D,CAAA;AAErE,UAAM,UAAmB;AAAA,MACvB,+BAA+B,MAAM,QAAQ;AAC3C,eAAO,CAAC,CAAC,+BAA+B,KAAK,CAAC,cAAc;AAC1D,cACE,UAAU,SAAS,SAAS,eAAe,mBAC3C,UAAU,OAAO,SACf,SAAS,eAAe,qBAC1B,UAAU,OAAO,OAAO,UAAU,QAClC;AACA,mBAAO,KAAK,SAAS,UAAU,MAAM;AAAA,UACvC;AAEA,iBAAO;AAAA,QACT,CAAC;AAAA,MACH;AAAA,MACA,uBAAuB,MAAM;AAC3B,eAAO,CAAC,CAAC,+BAA+B,KAAK,CAAC,cAAc;AAC1D,cAAI,UAAU,SAAS,SAAS,eAAe,iBAAiB;AAC9D,mBAAO,KAAK,SAAS,UAAU,MAAM;AAAA,UACvC;AAEA,iBAAO;AAAA,QACT,CAAC;AAAA,MACH;AAAA,IAAA;AAGF,UAAM,wBAA+C;AAAA,MACnD,kBAAkB,MAAM;AACtB,YACE,KAAK,WAAW,SAAS,KACzB,KAAK,eAAe,WACpB,KAAK,OAAO,MAAM,WAAW,YAAY,KACzC,KAAK,OAAO,MAAM,SAAS,SAAS,GACpC;AACA,yCAA+B,KAAK,GAAG,KAAK,UAAU;AAAA,QACxD;AAAA,MACF;AAAA,IAAA;AAIF,UAAM,mBAAmB,OAAO,SAAS,oBAAoB,OAAO;AACpE,UAAM,2BAAkD,CAAA;AAExD,UAAM,UAAU,IAAI;AAAA,MAClB,OAAO,KAAK,qBAAqB,EAAE,OAAO,OAAO,KAAK,gBAAgB,CAAC;AAAA,IAAA;AAKzE,YAAQ,QAAQ,CAAC,gBAAgB;AAC/B,+BAAyB,WAAW,IAAI,CAAC,SAAS;AAChD,YAAI,eAAe,uBAAuB;AACxC,gCAAsB,WAAW,IAAI,IAAI;AAAA,QAC3C;AAEA,cAAM,eAAe,iBAAiB,WAAW;AACjD,YAAI,iBAAiB,QAAW;AAC9B,iBAAO,aAAa,IAAI;AAAA,QAC1B;AAEA,eAAO;AAAA,MACT;AAAA,IACF,CAAC;AAED,WAAO;AAAA,EACT;AACF;"}
1
+ {"version":3,"file":"detect-router-imports.js","sources":["../../../src/utils/detect-router-imports.ts"],"sourcesContent":["import { TSESTree } from '@typescript-eslint/utils'\nimport type { ESLintUtils, TSESLint } from '@typescript-eslint/utils'\n\ntype Create = Parameters<\n ReturnType<typeof ESLintUtils.RuleCreator>\n>[0]['create']\n\ntype Context = Parameters<Create>[0]\ntype Options = Parameters<Create>[1]\ntype Helpers = {\n isSpecificTanstackRouterImport: (\n node: TSESTree.Identifier,\n source: string,\n ) => boolean\n isTanstackRouterImport: (node: TSESTree.Identifier) => boolean\n}\n\ntype EnhancedCreate = (\n context: Context,\n options: Options,\n helpers: Helpers,\n) => ReturnType<Create>\n\nexport function detectTanstackRouterImports(create: EnhancedCreate): Create {\n return (context, optionsWithDefault) => {\n const tanstackRouterImportSpecifiers: Array<TSESTree.ImportClause> = []\n\n const helpers: Helpers = {\n isSpecificTanstackRouterImport(node, source) {\n return !!tanstackRouterImportSpecifiers.find((specifier) => {\n if (\n specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier &&\n specifier.parent.type ===\n TSESTree.AST_NODE_TYPES.ImportDeclaration &&\n specifier.parent.source.value === source\n ) {\n return node.name === specifier.local.name\n }\n\n return false\n })\n },\n isTanstackRouterImport(node) {\n return !!tanstackRouterImportSpecifiers.find((specifier) => {\n if (specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier) {\n return node.name === specifier.local.name\n }\n\n return false\n })\n },\n }\n\n const detectionInstructions: TSESLint.RuleListener = {\n ImportDeclaration(node) {\n if (\n node.specifiers.length > 0 &&\n // `importKind` is parser-dependent and can be undefined (eg. Espree)\n node.importKind !== 'type' &&\n node.source.value.startsWith('@tanstack/') &&\n node.source.value.endsWith('-router')\n ) {\n tanstackRouterImportSpecifiers.push(...node.specifiers)\n }\n },\n }\n\n // Call original rule definition\n const ruleInstructions = create(context, optionsWithDefault, helpers)\n const enhancedRuleInstructions: TSESLint.RuleListener = {}\n\n const allKeys = new Set(\n Object.keys(detectionInstructions).concat(Object.keys(ruleInstructions)),\n )\n\n // Iterate over ALL instructions keys so we can override original rule instructions\n // to prevent their execution if conditions to report errors are not met.\n allKeys.forEach((instruction) => {\n enhancedRuleInstructions[instruction] = (node) => {\n if (instruction in detectionInstructions) {\n detectionInstructions[instruction]?.(node)\n }\n\n const ruleFunction = ruleInstructions[instruction]\n if (ruleFunction !== undefined) {\n return ruleFunction(node)\n }\n\n return undefined\n }\n })\n\n return enhancedRuleInstructions\n }\n}\n"],"names":[],"mappings":";AAuBO,SAAS,4BAA4B,QAAgC;AAC1E,SAAO,CAAC,SAAS,uBAAuB;AACtC,UAAM,iCAA+D,CAAA;AAErE,UAAM,UAAmB;AAAA,MACvB,+BAA+B,MAAM,QAAQ;AAC3C,eAAO,CAAC,CAAC,+BAA+B,KAAK,CAAC,cAAc;AAC1D,cACE,UAAU,SAAS,SAAS,eAAe,mBAC3C,UAAU,OAAO,SACf,SAAS,eAAe,qBAC1B,UAAU,OAAO,OAAO,UAAU,QAClC;AACA,mBAAO,KAAK,SAAS,UAAU,MAAM;AAAA,UACvC;AAEA,iBAAO;AAAA,QACT,CAAC;AAAA,MACH;AAAA,MACA,uBAAuB,MAAM;AAC3B,eAAO,CAAC,CAAC,+BAA+B,KAAK,CAAC,cAAc;AAC1D,cAAI,UAAU,SAAS,SAAS,eAAe,iBAAiB;AAC9D,mBAAO,KAAK,SAAS,UAAU,MAAM;AAAA,UACvC;AAEA,iBAAO;AAAA,QACT,CAAC;AAAA,MACH;AAAA,IAAA;AAGF,UAAM,wBAA+C;AAAA,MACnD,kBAAkB,MAAM;AACtB,YACE,KAAK,WAAW,SAAS;AAAA,QAEzB,KAAK,eAAe,UACpB,KAAK,OAAO,MAAM,WAAW,YAAY,KACzC,KAAK,OAAO,MAAM,SAAS,SAAS,GACpC;AACA,yCAA+B,KAAK,GAAG,KAAK,UAAU;AAAA,QACxD;AAAA,MACF;AAAA,IAAA;AAIF,UAAM,mBAAmB,OAAO,SAAS,oBAAoB,OAAO;AACpE,UAAM,2BAAkD,CAAA;AAExD,UAAM,UAAU,IAAI;AAAA,MAClB,OAAO,KAAK,qBAAqB,EAAE,OAAO,OAAO,KAAK,gBAAgB,CAAC;AAAA,IAAA;AAKzE,YAAQ,QAAQ,CAAC,gBAAgB;AAC/B,+BAAyB,WAAW,IAAI,CAAC,SAAS;AAChD,YAAI,eAAe,uBAAuB;AACxC,gCAAsB,WAAW,IAAI,IAAI;AAAA,QAC3C;AAEA,cAAM,eAAe,iBAAiB,WAAW;AACjD,YAAI,iBAAiB,QAAW;AAC9B,iBAAO,aAAa,IAAI;AAAA,QAC1B;AAEA,eAAO;AAAA,MACT;AAAA,IACF,CAAC;AAED,WAAO;AAAA,EACT;AACF;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/eslint-plugin-router",
3
- "version": "1.154.7",
3
+ "version": "1.161.2",
4
4
  "description": "ESLint plugin for TanStack Router",
5
5
  "author": "Manuel Schiller",
6
6
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  "eslint": "^9.22.0"
46
46
  },
47
47
  "peerDependencies": {
48
- "eslint": "^8.57.0 || ^9.0.0"
48
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0"
49
49
  },
50
50
  "scripts": {
51
51
  "clean": "rimraf ./dist ./coverage",
@@ -0,0 +1,271 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester'
2
+
3
+ import { name, rule } from '../rules/route-param-names/route-param-names.rule'
4
+
5
+ const ruleTester = new RuleTester()
6
+
7
+ ruleTester.run(name, rule, {
8
+ valid: [
9
+ // Valid param names - simple $param format
10
+ {
11
+ name: 'valid simple param: $userId',
12
+ code: `
13
+ import { createFileRoute } from '@tanstack/react-router'
14
+ const Route = createFileRoute('/users/$userId')({})
15
+ `,
16
+ },
17
+ {
18
+ name: 'valid simple param: $id',
19
+ code: `
20
+ import { createFileRoute } from '@tanstack/react-router'
21
+ const Route = createFileRoute('/posts/$id')({})
22
+ `,
23
+ },
24
+ {
25
+ name: 'valid simple param: $_id (underscore prefix)',
26
+ code: `
27
+ import { createFileRoute } from '@tanstack/react-router'
28
+ const Route = createFileRoute('/items/$_id')({})
29
+ `,
30
+ },
31
+ {
32
+ name: 'valid simple param: $$var (dollar prefix)',
33
+ code: `
34
+ import { createFileRoute } from '@tanstack/react-router'
35
+ const Route = createFileRoute('/data/$$var')({})
36
+ `,
37
+ },
38
+ {
39
+ name: 'valid param with numbers: $user123',
40
+ code: `
41
+ import { createFileRoute } from '@tanstack/react-router'
42
+ const Route = createFileRoute('/users/$user123')({})
43
+ `,
44
+ },
45
+
46
+ // Valid param names - braces format {$param}
47
+ {
48
+ name: 'valid braces param: {$userName}',
49
+ code: `
50
+ import { createFileRoute } from '@tanstack/react-router'
51
+ const Route = createFileRoute('/users/{$userName}')({})
52
+ `,
53
+ },
54
+ {
55
+ name: 'valid braces param with prefix/suffix: prefix{$id}suffix',
56
+ code: `
57
+ import { createFileRoute } from '@tanstack/react-router'
58
+ const Route = createFileRoute('/items/item-{$id}-details')({})
59
+ `,
60
+ },
61
+
62
+ // Valid optional params - {-$param}
63
+ {
64
+ name: 'valid optional param: {-$optional}',
65
+ code: `
66
+ import { createFileRoute } from '@tanstack/react-router'
67
+ const Route = createFileRoute('/search/{-$query}')({})
68
+ `,
69
+ },
70
+ {
71
+ name: 'valid optional param with prefix/suffix: prefix{-$opt}suffix',
72
+ code: `
73
+ import { createFileRoute } from '@tanstack/react-router'
74
+ const Route = createFileRoute('/filter/by-{-$category}-items')({})
75
+ `,
76
+ },
77
+
78
+ // Wildcards - should be skipped (no validation)
79
+ {
80
+ name: 'wildcard: $ alone',
81
+ code: `
82
+ import { createFileRoute } from '@tanstack/react-router'
83
+ const Route = createFileRoute('/files/$')({})
84
+ `,
85
+ },
86
+ {
87
+ name: 'wildcard: {$}',
88
+ code: `
89
+ import { createFileRoute } from '@tanstack/react-router'
90
+ const Route = createFileRoute('/catch/{$}')({})
91
+ `,
92
+ },
93
+
94
+ // Multiple valid params
95
+ {
96
+ name: 'multiple valid params in path',
97
+ code: `
98
+ import { createFileRoute } from '@tanstack/react-router'
99
+ const Route = createFileRoute('/users/$userId/posts/$postId')({})
100
+ `,
101
+ },
102
+
103
+ // createRoute with path property
104
+ {
105
+ name: 'createRoute with valid param in path property',
106
+ code: `
107
+ import { createRoute } from '@tanstack/react-router'
108
+ const Route = createRoute({ path: '/users/$userId' })
109
+ `,
110
+ },
111
+
112
+ // createLazyFileRoute
113
+ {
114
+ name: 'createLazyFileRoute with valid param',
115
+ code: `
116
+ import { createLazyFileRoute } from '@tanstack/react-router'
117
+ const Route = createLazyFileRoute('/users/$userId')({})
118
+ `,
119
+ },
120
+
121
+ // createLazyRoute
122
+ {
123
+ name: 'createLazyRoute with valid param',
124
+ code: `
125
+ import { createLazyRoute } from '@tanstack/react-router'
126
+ const Route = createLazyRoute('/users/$userId')({})
127
+ `,
128
+ },
129
+
130
+ // No params - should pass
131
+ {
132
+ name: 'no params in path',
133
+ code: `
134
+ import { createFileRoute } from '@tanstack/react-router'
135
+ const Route = createFileRoute('/users/list')({})
136
+ `,
137
+ },
138
+
139
+ // Not from tanstack router - should be ignored
140
+ {
141
+ name: 'non-tanstack import should be ignored',
142
+ code: `
143
+ import { createFileRoute } from 'other-router'
144
+ const Route = createFileRoute('/users/$123invalid')({})
145
+ `,
146
+ },
147
+ ],
148
+
149
+ invalid: [
150
+ // Invalid param names - starts with number
151
+ {
152
+ name: 'invalid param starting with number: $123',
153
+ code: `
154
+ import { createFileRoute } from '@tanstack/react-router'
155
+ const Route = createFileRoute('/users/$123')({})
156
+ `,
157
+ errors: [{ messageId: 'invalidParamName', data: { paramName: '123' } }],
158
+ },
159
+ {
160
+ name: 'invalid param starting with number: $1user',
161
+ code: `
162
+ import { createFileRoute } from '@tanstack/react-router'
163
+ const Route = createFileRoute('/users/$1user')({})
164
+ `,
165
+ errors: [{ messageId: 'invalidParamName', data: { paramName: '1user' } }],
166
+ },
167
+
168
+ // Invalid param names - contains hyphen
169
+ {
170
+ name: 'invalid param with hyphen: $user-name',
171
+ code: `
172
+ import { createFileRoute } from '@tanstack/react-router'
173
+ const Route = createFileRoute('/users/$user-name')({})
174
+ `,
175
+ errors: [
176
+ { messageId: 'invalidParamName', data: { paramName: 'user-name' } },
177
+ ],
178
+ },
179
+
180
+ // Invalid param names - contains dot
181
+ {
182
+ name: 'invalid param with dot: {$my.param}',
183
+ code: `
184
+ import { createFileRoute } from '@tanstack/react-router'
185
+ const Route = createFileRoute('/users/{$my.param}')({})
186
+ `,
187
+ errors: [
188
+ { messageId: 'invalidParamName', data: { paramName: 'my.param' } },
189
+ ],
190
+ },
191
+
192
+ // Invalid param names - contains space
193
+ {
194
+ name: 'invalid param with space: {$param name}',
195
+ code: `
196
+ import { createFileRoute } from '@tanstack/react-router'
197
+ const Route = createFileRoute('/users/{$param name}')({})
198
+ `,
199
+ errors: [
200
+ { messageId: 'invalidParamName', data: { paramName: 'param name' } },
201
+ ],
202
+ },
203
+
204
+ // Invalid optional param
205
+ {
206
+ name: 'invalid optional param: {-$123invalid}',
207
+ code: `
208
+ import { createFileRoute } from '@tanstack/react-router'
209
+ const Route = createFileRoute('/search/{-$123invalid}')({})
210
+ `,
211
+ errors: [
212
+ { messageId: 'invalidParamName', data: { paramName: '123invalid' } },
213
+ ],
214
+ },
215
+
216
+ // Multiple invalid params
217
+ {
218
+ name: 'multiple invalid params in path',
219
+ code: `
220
+ import { createFileRoute } from '@tanstack/react-router'
221
+ const Route = createFileRoute('/users/$1id/posts/$post-id')({})
222
+ `,
223
+ errors: [
224
+ { messageId: 'invalidParamName', data: { paramName: '1id' } },
225
+ { messageId: 'invalidParamName', data: { paramName: 'post-id' } },
226
+ ],
227
+ },
228
+
229
+ // createRoute with invalid path property
230
+ {
231
+ name: 'createRoute with invalid param in path property',
232
+ code: `
233
+ import { createRoute } from '@tanstack/react-router'
234
+ const Route = createRoute({ path: '/users/$123' })
235
+ `,
236
+ errors: [{ messageId: 'invalidParamName', data: { paramName: '123' } }],
237
+ },
238
+
239
+ // createLazyFileRoute with invalid param
240
+ {
241
+ name: 'createLazyFileRoute with invalid param',
242
+ code: `
243
+ import { createLazyFileRoute } from '@tanstack/react-router'
244
+ const Route = createLazyFileRoute('/users/$user-id')({})
245
+ `,
246
+ errors: [
247
+ { messageId: 'invalidParamName', data: { paramName: 'user-id' } },
248
+ ],
249
+ },
250
+
251
+ // createLazyRoute with invalid param
252
+ {
253
+ name: 'createLazyRoute with invalid param',
254
+ code: `
255
+ import { createLazyRoute } from '@tanstack/react-router'
256
+ const Route = createLazyRoute('/users/$1abc')({})
257
+ `,
258
+ errors: [{ messageId: 'invalidParamName', data: { paramName: '1abc' } }],
259
+ },
260
+
261
+ // Invalid braces param with prefix/suffix
262
+ {
263
+ name: 'invalid braces param with prefix/suffix',
264
+ code: `
265
+ import { createFileRoute } from '@tanstack/react-router'
266
+ const Route = createFileRoute('/items/item-{$123}-details')({})
267
+ `,
268
+ errors: [{ messageId: 'invalidParamName', data: { paramName: '123' } }],
269
+ },
270
+ ],
271
+ })
@@ -0,0 +1,174 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ extractParamsFromPath,
4
+ extractParamsFromSegment,
5
+ getInvalidParams,
6
+ isValidParamName,
7
+ } from '../rules/route-param-names/route-param-names.utils'
8
+
9
+ describe('isValidParamName', () => {
10
+ it('should return true for valid param names', () => {
11
+ expect(isValidParamName('userId')).toBe(true)
12
+ expect(isValidParamName('id')).toBe(true)
13
+ expect(isValidParamName('_id')).toBe(true)
14
+ expect(isValidParamName('$var')).toBe(true)
15
+ expect(isValidParamName('user123')).toBe(true)
16
+ expect(isValidParamName('_')).toBe(true)
17
+ expect(isValidParamName('$')).toBe(true)
18
+ expect(isValidParamName('ABC')).toBe(true)
19
+ expect(isValidParamName('camelCase')).toBe(true)
20
+ expect(isValidParamName('PascalCase')).toBe(true)
21
+ expect(isValidParamName('snake_case')).toBe(true)
22
+ expect(isValidParamName('$$double')).toBe(true)
23
+ expect(isValidParamName('__double')).toBe(true)
24
+ })
25
+
26
+ it('should return false for invalid param names', () => {
27
+ expect(isValidParamName('123')).toBe(false)
28
+ expect(isValidParamName('1user')).toBe(false)
29
+ expect(isValidParamName('user-name')).toBe(false)
30
+ expect(isValidParamName('user.name')).toBe(false)
31
+ expect(isValidParamName('user name')).toBe(false)
32
+ expect(isValidParamName('')).toBe(false)
33
+ expect(isValidParamName('user@name')).toBe(false)
34
+ expect(isValidParamName('user#name')).toBe(false)
35
+ expect(isValidParamName('-user')).toBe(false)
36
+ })
37
+ })
38
+
39
+ describe('extractParamsFromSegment', () => {
40
+ it('should return empty array for segments without $', () => {
41
+ expect(extractParamsFromSegment('')).toEqual([])
42
+ expect(extractParamsFromSegment('users')).toEqual([])
43
+ expect(extractParamsFromSegment('static-segment')).toEqual([])
44
+ })
45
+
46
+ it('should skip wildcard segments', () => {
47
+ expect(extractParamsFromSegment('$')).toEqual([])
48
+ expect(extractParamsFromSegment('{$}')).toEqual([])
49
+ })
50
+
51
+ it('should extract simple $param format', () => {
52
+ const result = extractParamsFromSegment('$userId')
53
+ expect(result).toHaveLength(1)
54
+ expect(result[0]).toEqual({
55
+ fullParam: '$userId',
56
+ paramName: 'userId',
57
+ isOptional: false,
58
+ isValid: true,
59
+ })
60
+ })
61
+
62
+ it('should extract braces {$param} format', () => {
63
+ const result = extractParamsFromSegment('{$userId}')
64
+ expect(result).toHaveLength(1)
65
+ expect(result[0]).toEqual({
66
+ fullParam: '$userId',
67
+ paramName: 'userId',
68
+ isOptional: false,
69
+ isValid: true,
70
+ })
71
+ })
72
+
73
+ it('should extract braces with prefix/suffix', () => {
74
+ const result = extractParamsFromSegment('prefix{$id}suffix')
75
+ expect(result).toHaveLength(1)
76
+ expect(result[0]).toEqual({
77
+ fullParam: '$id',
78
+ paramName: 'id',
79
+ isOptional: false,
80
+ isValid: true,
81
+ })
82
+ })
83
+
84
+ it('should extract optional {-$param} format', () => {
85
+ const result = extractParamsFromSegment('{-$optional}')
86
+ expect(result).toHaveLength(1)
87
+ expect(result[0]).toEqual({
88
+ fullParam: '-$optional',
89
+ paramName: 'optional',
90
+ isOptional: true,
91
+ isValid: true,
92
+ })
93
+ })
94
+
95
+ it('should extract optional with prefix/suffix', () => {
96
+ const result = extractParamsFromSegment('pre{-$opt}post')
97
+ expect(result).toHaveLength(1)
98
+ expect(result[0]).toEqual({
99
+ fullParam: '-$opt',
100
+ paramName: 'opt',
101
+ isOptional: true,
102
+ isValid: true,
103
+ })
104
+ })
105
+
106
+ it('should mark invalid param names', () => {
107
+ const result = extractParamsFromSegment('$123invalid')
108
+ expect(result).toHaveLength(1)
109
+ expect(result[0]?.isValid).toBe(false)
110
+ expect(result[0]?.paramName).toBe('123invalid')
111
+ })
112
+
113
+ it('should mark hyphenated param names as invalid', () => {
114
+ const result = extractParamsFromSegment('$user-name')
115
+ expect(result).toHaveLength(1)
116
+ expect(result[0]?.isValid).toBe(false)
117
+ expect(result[0]?.paramName).toBe('user-name')
118
+ })
119
+ })
120
+
121
+ describe('extractParamsFromPath', () => {
122
+ it('should return empty array for paths without params', () => {
123
+ expect(extractParamsFromPath('')).toEqual([])
124
+ expect(extractParamsFromPath('/')).toEqual([])
125
+ expect(extractParamsFromPath('/users/list')).toEqual([])
126
+ })
127
+
128
+ it('should extract single param from path', () => {
129
+ const result = extractParamsFromPath('/users/$userId')
130
+ expect(result).toHaveLength(1)
131
+ expect(result[0]?.paramName).toBe('userId')
132
+ })
133
+
134
+ it('should extract multiple params from path', () => {
135
+ const result = extractParamsFromPath('/users/$userId/posts/$postId')
136
+ expect(result).toHaveLength(2)
137
+ expect(result[0]?.paramName).toBe('userId')
138
+ expect(result[1]?.paramName).toBe('postId')
139
+ })
140
+
141
+ it('should extract params with various formats', () => {
142
+ const result = extractParamsFromPath(
143
+ '/a/$simple/b/{$braces}/c/{-$optional}',
144
+ )
145
+ expect(result).toHaveLength(3)
146
+ expect(result[0]?.paramName).toBe('simple')
147
+ expect(result[0]?.isOptional).toBe(false)
148
+ expect(result[1]?.paramName).toBe('braces')
149
+ expect(result[1]?.isOptional).toBe(false)
150
+ expect(result[2]?.paramName).toBe('optional')
151
+ expect(result[2]?.isOptional).toBe(true)
152
+ })
153
+ })
154
+
155
+ describe('getInvalidParams', () => {
156
+ it('should return empty array for valid params', () => {
157
+ expect(getInvalidParams('/users/$userId')).toEqual([])
158
+ expect(getInvalidParams('/users/$_id')).toEqual([])
159
+ expect(getInvalidParams('/users/$$var')).toEqual([])
160
+ })
161
+
162
+ it('should return invalid params only', () => {
163
+ const result = getInvalidParams('/users/$123/posts/$validId')
164
+ expect(result).toHaveLength(1)
165
+ expect(result[0]?.paramName).toBe('123')
166
+ })
167
+
168
+ it('should return all invalid params', () => {
169
+ const result = getInvalidParams('/users/$1id/posts/$post-id')
170
+ expect(result).toHaveLength(2)
171
+ expect(result[0]?.paramName).toBe('1id')
172
+ expect(result[1]?.paramName).toBe('post-id')
173
+ })
174
+ })
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ Object.assign(plugin.configs, {
26
26
  plugins: ['@tanstack/eslint-plugin-router'],
27
27
  rules: {
28
28
  '@tanstack/router/create-route-property-order': 'warn',
29
+ '@tanstack/router/route-param-names': 'error',
29
30
  },
30
31
  },
31
32
  'flat/recommended': [
@@ -35,6 +36,7 @@ Object.assign(plugin.configs, {
35
36
  },
36
37
  rules: {
37
38
  '@tanstack/router/create-route-property-order': 'warn',
39
+ '@tanstack/router/route-param-names': 'error',
38
40
  },
39
41
  },
40
42
  ],
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Functions where the path is passed as the first argument (string literal)
3
+ * e.g., createFileRoute('/path/$param')(...)
4
+ */
5
+ export const pathAsFirstArgFunctions = [
6
+ 'createFileRoute',
7
+ 'createLazyFileRoute',
8
+ 'createLazyRoute',
9
+ ] as const
10
+
11
+ export type PathAsFirstArgFunction = (typeof pathAsFirstArgFunctions)[number]
12
+
13
+ /**
14
+ * Functions where the path is a property in the options object
15
+ * e.g., createRoute({ path: '/path/$param' })
16
+ */
17
+ export const pathAsPropertyFunctions = ['createRoute'] as const
18
+
19
+ export type PathAsPropertyFunction = (typeof pathAsPropertyFunctions)[number]
20
+
21
+ /**
22
+ * All route functions that need param name validation
23
+ */
24
+ export const allRouteFunctions = [
25
+ ...pathAsFirstArgFunctions,
26
+ ...pathAsPropertyFunctions,
27
+ ] as const
28
+
29
+ export type RouteFunction = (typeof allRouteFunctions)[number]
30
+
31
+ /**
32
+ * Regex for valid JavaScript identifier (param name)
33
+ * Must start with letter, underscore, or dollar sign
34
+ * Can contain letters, numbers, underscores, or dollar signs
35
+ */
36
+ export const VALID_PARAM_NAME_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/