@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,28 @@
1
+ function sortDataByOrder(data, orderArray, key) {
2
+ const orderMap = new Map(orderArray.map((item, index) => [item, index]));
3
+ const inOrderArray = data.filter((item) => orderMap.has(item[key])).sort((a, b) => {
4
+ const indexA = orderMap.get(a[key]);
5
+ const indexB = orderMap.get(b[key]);
6
+ return indexA - indexB;
7
+ });
8
+ const inOrderIterator = inOrderArray.values();
9
+ let wasResorted = false;
10
+ const result = data.map((item) => {
11
+ if (orderMap.has(item[key])) {
12
+ const sortedItem = inOrderIterator.next().value;
13
+ if (sortedItem[key] !== item[key]) {
14
+ wasResorted = true;
15
+ }
16
+ return sortedItem;
17
+ }
18
+ return item;
19
+ });
20
+ if (!wasResorted) {
21
+ return null;
22
+ }
23
+ return result;
24
+ }
25
+ export {
26
+ sortDataByOrder
27
+ };
28
+ //# sourceMappingURL=create-route-property-order.utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-route-property-order.utils.js","sources":["../../../../src/rules/create-route-property-order/create-route-property-order.utils.ts"],"sourcesContent":["export function sortDataByOrder<T, TKey extends keyof T>(\n data: Array<T> | ReadonlyArray<T>,\n orderArray: Array<T[TKey]> | ReadonlyArray<T[TKey]>,\n key: TKey,\n): Array<T> | null {\n const orderMap = new Map(orderArray.map((item, index) => [item, index]))\n\n // Separate items that are in orderArray from those that are not\n const inOrderArray = data\n .filter((item) => orderMap.has(item[key]))\n .sort((a, b) => {\n const indexA = orderMap.get(a[key])!\n const indexB = orderMap.get(b[key])!\n\n return indexA - indexB\n })\n\n const inOrderIterator = inOrderArray.values()\n\n // `as boolean` is needed to avoid TS incorrectly inferring that wasResorted is always `true`\n let wasResorted = false as boolean\n\n const result = data.map((item) => {\n if (orderMap.has(item[key])) {\n const sortedItem = inOrderIterator.next().value!\n if (sortedItem[key] !== item[key]) {\n wasResorted = true\n }\n return sortedItem\n }\n return item\n })\n\n if (!wasResorted) {\n return null\n }\n return result\n}\n"],"names":[],"mappings":"AAAgB,SAAA,gBACd,MACA,YACA,KACiB;AACjB,QAAM,WAAW,IAAI,IAAI,WAAW,IAAI,CAAC,MAAM,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC;AAGvE,QAAM,eAAe,KAClB,OAAO,CAAC,SAAS,SAAS,IAAI,KAAK,GAAG,CAAC,CAAC,EACxC,KAAK,CAAC,GAAG,MAAM;AACd,UAAM,SAAS,SAAS,IAAI,EAAE,GAAG,CAAC;AAClC,UAAM,SAAS,SAAS,IAAI,EAAE,GAAG,CAAC;AAElC,WAAO,SAAS;AAAA,EAAA,CACjB;AAEG,QAAA,kBAAkB,aAAa;AAGrC,MAAI,cAAc;AAElB,QAAM,SAAS,KAAK,IAAI,CAAC,SAAS;AAChC,QAAI,SAAS,IAAI,KAAK,GAAG,CAAC,GAAG;AACrB,YAAA,aAAa,gBAAgB,KAAA,EAAO;AAC1C,UAAI,WAAW,GAAG,MAAM,KAAK,GAAG,GAAG;AACnB,sBAAA;AAAA,MAChB;AACO,aAAA;AAAA,IACT;AACO,WAAA;AAAA,EAAA,CACR;AAED,MAAI,CAAC,aAAa;AACT,WAAA;AAAA,EACT;AACO,SAAA;AACT;"}
@@ -0,0 +1,3 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ import { ExtraRuleDocs } from './types.js';
3
+ export declare const rules: Record<string, ESLintUtils.RuleModule<string, ReadonlyArray<unknown>, ExtraRuleDocs, ESLintUtils.RuleListener>>;
@@ -0,0 +1,8 @@
1
+ import { name, rule } from "./rules/create-route-property-order/create-route-property-order.rule.js";
2
+ const rules = {
3
+ [name]: rule
4
+ };
5
+ export {
6
+ rules
7
+ };
8
+ //# sourceMappingURL=rules.js.map
@@ -0,0 +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;"}
@@ -0,0 +1,3 @@
1
+ export type ExtraRuleDocs = {
2
+ recommended: 'strict' | 'error' | 'warn';
3
+ };
@@ -0,0 +1,11 @@
1
+ import { TSESTree, ESLintUtils } from '@typescript-eslint/utils';
2
+ type Create = Parameters<ReturnType<typeof ESLintUtils.RuleCreator>>[0]['create'];
3
+ type Context = Parameters<Create>[0];
4
+ type Options = Parameters<Create>[1];
5
+ type Helpers = {
6
+ isSpecificTanstackRouterImport: (node: TSESTree.Identifier, source: string) => boolean;
7
+ isTanstackRouterImport: (node: TSESTree.Identifier) => boolean;
8
+ };
9
+ type EnhancedCreate = (context: Context, options: Options, helpers: Helpers) => ReturnType<Create>;
10
+ export declare function detectTanstackRouterImports(create: EnhancedCreate): Create;
11
+ export {};
@@ -0,0 +1,54 @@
1
+ import { TSESTree } from "@typescript-eslint/utils";
2
+ function detectTanstackRouterImports(create) {
3
+ return (context, optionsWithDefault) => {
4
+ const tanstackRouterImportSpecifiers = [];
5
+ const helpers = {
6
+ isSpecificTanstackRouterImport(node, source) {
7
+ return !!tanstackRouterImportSpecifiers.find((specifier) => {
8
+ if (specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier && specifier.parent.type === TSESTree.AST_NODE_TYPES.ImportDeclaration && specifier.parent.source.value === source) {
9
+ return node.name === specifier.local.name;
10
+ }
11
+ return false;
12
+ });
13
+ },
14
+ isTanstackRouterImport(node) {
15
+ return !!tanstackRouterImportSpecifiers.find((specifier) => {
16
+ if (specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier) {
17
+ return node.name === specifier.local.name;
18
+ }
19
+ return false;
20
+ });
21
+ }
22
+ };
23
+ const detectionInstructions = {
24
+ ImportDeclaration(node) {
25
+ if (node.specifiers.length > 0 && node.importKind === "value" && node.source.value.startsWith("@tanstack/") && node.source.value.endsWith("-router")) {
26
+ tanstackRouterImportSpecifiers.push(...node.specifiers);
27
+ }
28
+ }
29
+ };
30
+ const ruleInstructions = create(context, optionsWithDefault, helpers);
31
+ const enhancedRuleInstructions = {};
32
+ const allKeys = new Set(
33
+ Object.keys(detectionInstructions).concat(Object.keys(ruleInstructions))
34
+ );
35
+ allKeys.forEach((instruction) => {
36
+ enhancedRuleInstructions[instruction] = (node) => {
37
+ var _a;
38
+ if (instruction in detectionInstructions) {
39
+ (_a = detectionInstructions[instruction]) == null ? void 0 : _a.call(detectionInstructions, node);
40
+ }
41
+ const ruleFunction = ruleInstructions[instruction];
42
+ if (ruleFunction !== void 0) {
43
+ return ruleFunction(node);
44
+ }
45
+ return void 0;
46
+ };
47
+ });
48
+ return enhancedRuleInstructions;
49
+ };
50
+ }
51
+ export {
52
+ detectTanstackRouterImports
53
+ };
54
+ //# sourceMappingURL=detect-router-imports.js.map
@@ -0,0 +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;AACnE,SAAA,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;AACO,mBAAA,KAAK,SAAS,UAAU,MAAM;AAAA,UACvC;AAEO,iBAAA;AAAA,QAAA,CACR;AAAA,MACH;AAAA,MACA,uBAAuB,MAAM;AAC3B,eAAO,CAAC,CAAC,+BAA+B,KAAK,CAAC,cAAc;AAC1D,cAAI,UAAU,SAAS,SAAS,eAAe,iBAAiB;AACvD,mBAAA,KAAK,SAAS,UAAU,MAAM;AAAA,UACvC;AAEO,iBAAA;AAAA,QAAA,CACR;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;AAC+B,yCAAA,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;AAKjE,YAAA,QAAQ,CAAC,gBAAgB;AACN,+BAAA,WAAW,IAAI,CAAC,SAAS;;AAChD,YAAI,eAAe,uBAAuB;AAClB,sCAAA,iBAAA,+CAAe;AAAA,QACvC;AAEM,cAAA,eAAe,iBAAiB,WAAW;AACjD,YAAI,iBAAiB,QAAW;AAC9B,iBAAO,aAAa,IAAI;AAAA,QAC1B;AAEO,eAAA;AAAA,MAAA;AAAA,IACT,CACD;AAEM,WAAA;AAAA,EAAA;AAEX;"}
@@ -0,0 +1 @@
1
+ export declare const getDocsUrl: (ruleName: string) => string;
@@ -0,0 +1,5 @@
1
+ const getDocsUrl = (ruleName) => `https://tanstack.com/router/latest/docs/eslint/${ruleName}`;
2
+ export {
3
+ getDocsUrl
4
+ };
5
+ //# sourceMappingURL=get-docs-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-docs-url.js","sources":["../../../src/utils/get-docs-url.ts"],"sourcesContent":["export const getDocsUrl = (ruleName: string): string =>\n `https://tanstack.com/router/latest/docs/eslint/${ruleName}`\n"],"names":[],"mappings":"AAAO,MAAM,aAAa,CAAC,aACzB,kDAAkD,QAAQ;"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@tanstack/eslint-plugin-router",
3
+ "version": "1.58.0",
4
+ "description": "ESLint plugin for TanStack Router",
5
+ "author": "Manuel Schiller",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/TanStack/router.git",
10
+ "directory": "packages/eslint-plugin-router"
11
+ },
12
+ "homepage": "https://tanstack.com/router",
13
+ "funding": {
14
+ "type": "github",
15
+ "url": "https://github.com/sponsors/tannerlinsley"
16
+ },
17
+ "type": "module",
18
+ "types": "dist/esm/index.d.ts",
19
+ "main": "dist/cjs/index.cjs",
20
+ "module": "dist/esm/index.js",
21
+ "exports": {
22
+ ".": {
23
+ "import": {
24
+ "types": "./dist/esm/index.d.ts",
25
+ "default": "./dist/esm/index.js"
26
+ },
27
+ "require": {
28
+ "types": "./dist/cjs/index.d.cts",
29
+ "default": "./dist/cjs/index.cjs"
30
+ }
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "sideEffects": false,
35
+ "files": [
36
+ "dist",
37
+ "src"
38
+ ],
39
+ "dependencies": {
40
+ "@typescript-eslint/utils": "^8.3.0"
41
+ },
42
+ "devDependencies": {
43
+ "@typescript-eslint/rule-tester": "^8.3.0",
44
+ "combinate": "^1.1.11",
45
+ "eslint": "^9.9.1"
46
+ },
47
+ "peerDependencies": {
48
+ "eslint": "^8.57.0 || ^9.0.0"
49
+ },
50
+ "scripts": {}
51
+ }
@@ -0,0 +1,153 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester'
2
+ import combinate from 'combinate'
3
+ import {
4
+ name,
5
+ rule,
6
+ } from '../rules/create-route-property-order/create-route-property-order.rule'
7
+ import {
8
+ checkedProperties,
9
+ createRouteFunctionsDirect,
10
+ createRouteFunctionsIndirect,
11
+ } from '../rules/create-route-property-order/constants'
12
+ import {
13
+ generateInterleavedCombinations,
14
+ generatePartialCombinations,
15
+ generatePermutations,
16
+ normalizeIndent,
17
+ } from './test-utils'
18
+
19
+ const ruleTester = new RuleTester()
20
+
21
+ // reduce the number of test cases by only testing a subset of the checked properties
22
+ const testedCheckedProperties = [
23
+ checkedProperties[0],
24
+ checkedProperties[1],
25
+ checkedProperties[2],
26
+ ]
27
+ type TestedCheckedProperties = (typeof testedCheckedProperties)[number]
28
+ const orderIndependentProps = ['gcTime', '...foo'] as const
29
+ type OrderIndependentProps = (typeof orderIndependentProps)[number]
30
+
31
+ // reduce the number of test cases by only testing the first function of createRouteFunctionsDirect
32
+ const testedCreateRouteFunctions = [
33
+ ...createRouteFunctionsIndirect,
34
+ createRouteFunctionsDirect[0],
35
+ ]
36
+ type TestedCreateRouteFunction = (typeof testedCreateRouteFunctions)[number]
37
+
38
+ interface TestCase {
39
+ createRouteFunction: TestedCreateRouteFunction
40
+ properties: Array<TestedCheckedProperties | OrderIndependentProps>
41
+ }
42
+
43
+ const validTestMatrix = combinate({
44
+ createRouteFunction: testedCreateRouteFunctions,
45
+ properties: generatePartialCombinations(testedCheckedProperties, 2),
46
+ })
47
+
48
+ export function generateInvalidPermutations<T>(
49
+ arr: ReadonlyArray<T>,
50
+ ): Array<{ invalid: Array<T>; valid: Array<T> }> {
51
+ const combinations = generatePartialCombinations(arr, 2)
52
+ const allPermutations: Array<{ invalid: Array<T>; valid: Array<T> }> = []
53
+
54
+ for (const combination of combinations) {
55
+ const permutations = generatePermutations(combination)
56
+ // skip the first permutation as it matches the original combination
57
+ const invalidPermutations = permutations.slice(1)
58
+ allPermutations.push(
59
+ ...invalidPermutations.map((p) => ({ invalid: p, valid: combination })),
60
+ )
61
+ }
62
+
63
+ return allPermutations
64
+ }
65
+
66
+ const invalidPermutations = generateInvalidPermutations(testedCheckedProperties)
67
+
68
+ type Interleaved = TestedCheckedProperties | OrderIndependentProps
69
+ const interleavedInvalidPermutations: Array<{
70
+ invalid: Array<Interleaved>
71
+ valid: Array<Interleaved>
72
+ }> = []
73
+ for (const invalidPermutation of invalidPermutations) {
74
+ const invalid = generateInterleavedCombinations(
75
+ invalidPermutation.invalid,
76
+ orderIndependentProps,
77
+ )
78
+ const valid = generateInterleavedCombinations(
79
+ invalidPermutation.valid,
80
+ orderIndependentProps,
81
+ )
82
+
83
+ for (let i = 0; i < invalid.length; i++) {
84
+ interleavedInvalidPermutations.push({
85
+ invalid: invalid[i]!,
86
+ valid: valid[i]!,
87
+ })
88
+ }
89
+ }
90
+
91
+ const invalidTestMatrix = combinate({
92
+ createRouteFunction: testedCreateRouteFunctions,
93
+ properties: interleavedInvalidPermutations,
94
+ })
95
+
96
+ function getCode({ createRouteFunction, properties }: TestCase) {
97
+ let invocation = ''
98
+ switch (createRouteFunction) {
99
+ case 'createFileRoute': {
100
+ invocation = `('/_layout/hello/foo/$id')`
101
+ break
102
+ }
103
+ case 'createRootRouteWithContext': {
104
+ invocation = normalizeIndent`
105
+ <{
106
+ queryClient: QueryClient
107
+ }>()`
108
+ break
109
+ }
110
+ }
111
+ function getPropertyCode(
112
+ property: TestedCheckedProperties | OrderIndependentProps,
113
+ ) {
114
+ if (property.startsWith('...')) {
115
+ return property
116
+ }
117
+ return `${property}: () => null`
118
+ }
119
+ return `
120
+ import { ${createRouteFunction} } from '@tanstack/react-router'
121
+
122
+ const Route = ${createRouteFunction}${invocation}({
123
+ ${properties.map(getPropertyCode).join(',\n ')}
124
+ })
125
+ `
126
+ }
127
+
128
+ const validTestCases = validTestMatrix.map(
129
+ ({ createRouteFunction, properties }) => ({
130
+ name: `should pass when order is correct for ${createRouteFunction} with order: ${properties.join(', ')}`,
131
+ code: getCode({ createRouteFunction, properties }),
132
+ }),
133
+ )
134
+
135
+ const invalidTestCases = invalidTestMatrix.map(
136
+ ({ createRouteFunction, properties }) => ({
137
+ name: `incorrect property order is detected for ${createRouteFunction} with order: ${properties.invalid.join(', ')}`,
138
+ code: getCode({
139
+ createRouteFunction,
140
+ properties: properties.invalid,
141
+ }),
142
+ errors: [{ messageId: 'invalidOrder' }],
143
+ output: getCode({
144
+ createRouteFunction,
145
+ properties: properties.valid,
146
+ }),
147
+ }),
148
+ )
149
+
150
+ ruleTester.run(name, rule, {
151
+ valid: validTestCases,
152
+ invalid: invalidTestCases,
153
+ })
@@ -0,0 +1,58 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { sortDataByOrder } from '../rules/create-route-property-order/create-route-property-order.utils'
3
+
4
+ describe('create-route-property-order utils', () => {
5
+ describe('sortDataByOrder', () => {
6
+ const testCases = [
7
+ {
8
+ data: [{ key: 'a' }, { key: 'c' }, { key: 'b' }],
9
+ orderArray: ['a', 'b', 'c'],
10
+ key: 'key',
11
+ expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
12
+ },
13
+ {
14
+ data: [{ key: 'b' }, { key: 'a' }, { key: 'c' }],
15
+ orderArray: ['a', 'b', 'c'],
16
+ key: 'key',
17
+ expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
18
+ },
19
+ {
20
+ data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
21
+ orderArray: ['a', 'b', 'c'],
22
+ key: 'key',
23
+ expected: null,
24
+ },
25
+ {
26
+ data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }, { key: 'd' }],
27
+ orderArray: ['a', 'b', 'c'],
28
+ key: 'key',
29
+ expected: null,
30
+ },
31
+ {
32
+ data: [{ key: 'a' }, { key: 'b' }, { key: 'd' }, { key: 'c' }],
33
+ orderArray: ['a', 'b', 'c'],
34
+ key: 'key',
35
+ expected: null,
36
+ },
37
+ {
38
+ data: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
39
+ orderArray: ['a', 'b', 'c'],
40
+ key: 'key',
41
+ expected: null,
42
+ },
43
+ {
44
+ data: [{ key: 'd' }, { key: 'b' }, { key: 'a' }, { key: 'c' }],
45
+ orderArray: ['a', 'b', 'c'],
46
+ key: 'key',
47
+ expected: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
48
+ },
49
+ ] as const
50
+ test.each(testCases)(
51
+ '$data $orderArray $key $expected',
52
+ ({ data, orderArray, key, expected }) => {
53
+ const sortedData = sortDataByOrder(data, orderArray, key)
54
+ expect(sortedData).toEqual(expected)
55
+ },
56
+ )
57
+ })
58
+ })
@@ -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