@mionjs/devtools 0.8.6 → 0.8.8

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 (131) hide show
  1. package/build/eslint/cjs/index.cjs.map +1 -1
  2. package/build/eslint/cjs/src/eslint/index.d.ts +1 -0
  3. package/build/eslint/cjs/src/eslint/index.d.ts.map +1 -0
  4. package/build/eslint/cjs/src/eslint/rules/enforce-type-imports.d.ts +1 -0
  5. package/build/eslint/cjs/src/eslint/rules/enforce-type-imports.d.ts.map +1 -0
  6. package/build/eslint/cjs/src/eslint/rules/formatTypeNames.d.ts +1 -0
  7. package/build/eslint/cjs/src/eslint/rules/formatTypeNames.d.ts.map +1 -0
  8. package/build/eslint/cjs/src/eslint/rules/no-mixed-union-properties.d.ts +1 -0
  9. package/build/eslint/cjs/src/eslint/rules/no-mixed-union-properties.d.ts.map +1 -0
  10. package/build/eslint/cjs/src/eslint/rules/no-type-imports.d.ts +1 -0
  11. package/build/eslint/cjs/src/eslint/rules/no-type-imports.d.ts.map +1 -0
  12. package/build/eslint/cjs/src/eslint/rules/no-typeof-runtype.d.ts +1 -0
  13. package/build/eslint/cjs/src/eslint/rules/no-typeof-runtype.d.ts.map +1 -0
  14. package/build/eslint/cjs/src/eslint/rules/no-unreachable-union-types.d.ts +1 -0
  15. package/build/eslint/cjs/src/eslint/rules/no-unreachable-union-types.d.ts.map +1 -0
  16. package/build/eslint/cjs/src/eslint/rules/no-vite-client.d.ts +1 -0
  17. package/build/eslint/cjs/src/eslint/rules/no-vite-client.d.ts.map +1 -0
  18. package/build/eslint/cjs/src/eslint/rules/pure-functions.d.ts +1 -0
  19. package/build/eslint/cjs/src/eslint/rules/pure-functions.d.ts.map +1 -0
  20. package/build/eslint/cjs/src/eslint/rules/strong-typed-routes.d.ts +1 -0
  21. package/build/eslint/cjs/src/eslint/rules/strong-typed-routes.d.ts.map +1 -0
  22. package/build/eslint/cjs/src/eslint/rules/type-formats-imports.d.ts +1 -0
  23. package/build/eslint/cjs/src/eslint/rules/type-formats-imports.d.ts.map +1 -0
  24. package/build/eslint/cjs/src/pureFns/purityRules.d.ts +1 -0
  25. package/build/eslint/cjs/src/pureFns/purityRules.d.ts.map +1 -0
  26. package/build/eslint/esm/index.js.map +1 -1
  27. package/build/eslint/esm/src/eslint/index.d.ts +1 -0
  28. package/build/eslint/esm/src/eslint/index.d.ts.map +1 -0
  29. package/build/eslint/esm/src/eslint/rules/enforce-type-imports.d.ts +1 -0
  30. package/build/eslint/esm/src/eslint/rules/enforce-type-imports.d.ts.map +1 -0
  31. package/build/eslint/esm/src/eslint/rules/formatTypeNames.d.ts +1 -0
  32. package/build/eslint/esm/src/eslint/rules/formatTypeNames.d.ts.map +1 -0
  33. package/build/eslint/esm/src/eslint/rules/no-mixed-union-properties.d.ts +1 -0
  34. package/build/eslint/esm/src/eslint/rules/no-mixed-union-properties.d.ts.map +1 -0
  35. package/build/eslint/esm/src/eslint/rules/no-type-imports.d.ts +1 -0
  36. package/build/eslint/esm/src/eslint/rules/no-type-imports.d.ts.map +1 -0
  37. package/build/eslint/esm/src/eslint/rules/no-typeof-runtype.d.ts +1 -0
  38. package/build/eslint/esm/src/eslint/rules/no-typeof-runtype.d.ts.map +1 -0
  39. package/build/eslint/esm/src/eslint/rules/no-unreachable-union-types.d.ts +1 -0
  40. package/build/eslint/esm/src/eslint/rules/no-unreachable-union-types.d.ts.map +1 -0
  41. package/build/eslint/esm/src/eslint/rules/no-vite-client.d.ts +1 -0
  42. package/build/eslint/esm/src/eslint/rules/no-vite-client.d.ts.map +1 -0
  43. package/build/eslint/esm/src/eslint/rules/pure-functions.d.ts +1 -0
  44. package/build/eslint/esm/src/eslint/rules/pure-functions.d.ts.map +1 -0
  45. package/build/eslint/esm/src/eslint/rules/strong-typed-routes.d.ts +1 -0
  46. package/build/eslint/esm/src/eslint/rules/strong-typed-routes.d.ts.map +1 -0
  47. package/build/eslint/esm/src/eslint/rules/type-formats-imports.d.ts +1 -0
  48. package/build/eslint/esm/src/eslint/rules/type-formats-imports.d.ts.map +1 -0
  49. package/build/eslint/esm/src/pureFns/purityRules.d.ts +1 -0
  50. package/build/eslint/esm/src/pureFns/purityRules.d.ts.map +1 -0
  51. package/build/vite-plugin/cjs/index.cjs +0 -1
  52. package/build/vite-plugin/cjs/index.cjs.map +1 -1
  53. package/build/vite-plugin/cjs/src/pureFns/purityRules.d.ts +1 -0
  54. package/build/vite-plugin/cjs/src/pureFns/purityRules.d.ts.map +1 -0
  55. package/build/vite-plugin/cjs/src/vite-plugin/aotCacheGenerator.d.ts +1 -0
  56. package/build/vite-plugin/cjs/src/vite-plugin/aotCacheGenerator.d.ts.map +1 -0
  57. package/build/vite-plugin/cjs/src/vite-plugin/aotDiskCache.d.ts +1 -0
  58. package/build/vite-plugin/cjs/src/vite-plugin/aotDiskCache.d.ts.map +1 -0
  59. package/build/vite-plugin/cjs/src/vite-plugin/cjsPackageJsonPlugin.d.ts +1 -0
  60. package/build/vite-plugin/cjs/src/vite-plugin/cjsPackageJsonPlugin.d.ts.map +1 -0
  61. package/build/vite-plugin/cjs/src/vite-plugin/constants.d.ts +1 -0
  62. package/build/vite-plugin/cjs/src/vite-plugin/constants.d.ts.map +1 -0
  63. package/build/vite-plugin/cjs/src/vite-plugin/extractPureFn.d.ts +1 -0
  64. package/build/vite-plugin/cjs/src/vite-plugin/extractPureFn.d.ts.map +1 -0
  65. package/build/vite-plugin/cjs/src/vite-plugin/index.cjs +0 -1
  66. package/build/vite-plugin/cjs/src/vite-plugin/index.cjs.map +1 -1
  67. package/build/vite-plugin/cjs/src/vite-plugin/index.d.ts +2 -1
  68. package/build/vite-plugin/cjs/src/vite-plugin/index.d.ts.map +1 -0
  69. package/build/vite-plugin/cjs/src/vite-plugin/mionVitePlugin.cjs.map +1 -1
  70. package/build/vite-plugin/cjs/src/vite-plugin/mionVitePlugin.d.ts +1 -0
  71. package/build/vite-plugin/cjs/src/vite-plugin/mionVitePlugin.d.ts.map +1 -0
  72. package/build/vite-plugin/cjs/src/vite-plugin/resolveModule.d.ts +1 -0
  73. package/build/vite-plugin/cjs/src/vite-plugin/resolveModule.d.ts.map +1 -0
  74. package/build/vite-plugin/cjs/src/vite-plugin/transformers.d.ts +1 -0
  75. package/build/vite-plugin/cjs/src/vite-plugin/transformers.d.ts.map +1 -0
  76. package/build/vite-plugin/cjs/src/vite-plugin/types.d.ts +1 -0
  77. package/build/vite-plugin/cjs/src/vite-plugin/types.d.ts.map +1 -0
  78. package/build/vite-plugin/cjs/src/vite-plugin/virtualModule.d.ts +1 -0
  79. package/build/vite-plugin/cjs/src/vite-plugin/virtualModule.d.ts.map +1 -0
  80. package/build/vite-plugin/esm/index.js +2 -3
  81. package/build/vite-plugin/esm/src/pureFns/purityRules.d.ts +1 -0
  82. package/build/vite-plugin/esm/src/pureFns/purityRules.d.ts.map +1 -0
  83. package/build/vite-plugin/esm/src/vite-plugin/aotCacheGenerator.d.ts +1 -0
  84. package/build/vite-plugin/esm/src/vite-plugin/aotCacheGenerator.d.ts.map +1 -0
  85. package/build/vite-plugin/esm/src/vite-plugin/aotDiskCache.d.ts +1 -0
  86. package/build/vite-plugin/esm/src/vite-plugin/aotDiskCache.d.ts.map +1 -0
  87. package/build/vite-plugin/esm/src/vite-plugin/cjsPackageJsonPlugin.d.ts +1 -0
  88. package/build/vite-plugin/esm/src/vite-plugin/cjsPackageJsonPlugin.d.ts.map +1 -0
  89. package/build/vite-plugin/esm/src/vite-plugin/constants.d.ts +1 -0
  90. package/build/vite-plugin/esm/src/vite-plugin/constants.d.ts.map +1 -0
  91. package/build/vite-plugin/esm/src/vite-plugin/extractPureFn.d.ts +1 -0
  92. package/build/vite-plugin/esm/src/vite-plugin/extractPureFn.d.ts.map +1 -0
  93. package/build/vite-plugin/esm/src/vite-plugin/index.d.ts +2 -1
  94. package/build/vite-plugin/esm/src/vite-plugin/index.d.ts.map +1 -0
  95. package/build/vite-plugin/esm/src/vite-plugin/index.js +2 -3
  96. package/build/vite-plugin/esm/src/vite-plugin/mionVitePlugin.d.ts +1 -0
  97. package/build/vite-plugin/esm/src/vite-plugin/mionVitePlugin.d.ts.map +1 -0
  98. package/build/vite-plugin/esm/src/vite-plugin/mionVitePlugin.js.map +1 -1
  99. package/build/vite-plugin/esm/src/vite-plugin/resolveModule.d.ts +1 -0
  100. package/build/vite-plugin/esm/src/vite-plugin/resolveModule.d.ts.map +1 -0
  101. package/build/vite-plugin/esm/src/vite-plugin/transformers.d.ts +1 -0
  102. package/build/vite-plugin/esm/src/vite-plugin/transformers.d.ts.map +1 -0
  103. package/build/vite-plugin/esm/src/vite-plugin/types.d.ts +1 -0
  104. package/build/vite-plugin/esm/src/vite-plugin/types.d.ts.map +1 -0
  105. package/build/vite-plugin/esm/src/vite-plugin/virtualModule.d.ts +1 -0
  106. package/build/vite-plugin/esm/src/vite-plugin/virtualModule.d.ts.map +1 -0
  107. package/package.json +6 -4
  108. package/src/eslint/index.ts +53 -0
  109. package/src/eslint/rules/enforce-type-imports.ts +157 -0
  110. package/src/eslint/rules/formatTypeNames.ts +92 -0
  111. package/src/eslint/rules/no-mixed-union-properties.ts +276 -0
  112. package/src/eslint/rules/no-type-imports.ts +386 -0
  113. package/src/eslint/rules/no-typeof-runtype.ts +112 -0
  114. package/src/eslint/rules/no-unreachable-union-types.ts +357 -0
  115. package/src/eslint/rules/no-vite-client.ts +101 -0
  116. package/src/eslint/rules/pure-functions.ts +523 -0
  117. package/src/eslint/rules/strong-typed-routes.ts +537 -0
  118. package/src/eslint/rules/type-formats-imports.ts +73 -0
  119. package/src/pureFns/purityRules.ts +76 -0
  120. package/src/vite-plugin/README.md +399 -0
  121. package/src/vite-plugin/aotCacheGenerator.ts +397 -0
  122. package/src/vite-plugin/aotDiskCache.ts +193 -0
  123. package/src/vite-plugin/cjsPackageJsonPlugin.ts +21 -0
  124. package/src/vite-plugin/constants.ts +50 -0
  125. package/src/vite-plugin/extractPureFn.ts +845 -0
  126. package/src/vite-plugin/index.ts +27 -0
  127. package/src/vite-plugin/mionVitePlugin.ts +738 -0
  128. package/src/vite-plugin/resolveModule.ts +50 -0
  129. package/src/vite-plugin/transformers.ts +209 -0
  130. package/src/vite-plugin/types.ts +182 -0
  131. package/src/vite-plugin/virtualModule.ts +94 -0
@@ -0,0 +1,112 @@
1
+ /* ########
2
+ * 2024 mion
3
+ * Author: Ma-jerez
4
+ * License: MIT
5
+ * The software is provided "as is", without warranty of any kind.
6
+ * ######## */
7
+
8
+ import {TSESTree, TSESLint, AST_NODE_TYPES} from '@typescript-eslint/utils';
9
+
10
+ /**
11
+ * Recursively checks if a type node contains `typeof`.
12
+ * @param typeNode The type node to check.
13
+ * @returns True if `typeof` is found, otherwise false.
14
+ */
15
+ function containsTypeof(typeNode: TSESTree.Node | null): boolean {
16
+ if (!typeNode) return false;
17
+
18
+ switch (typeNode.type) {
19
+ case AST_NODE_TYPES.TSTypeQuery:
20
+ return true;
21
+ case AST_NODE_TYPES.TSUnionType:
22
+ case AST_NODE_TYPES.TSIntersectionType:
23
+ return typeNode.types.some(containsTypeof);
24
+ case AST_NODE_TYPES.TSTupleType:
25
+ return typeNode.elementTypes.some(containsTypeof);
26
+ default:
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Checks if a call expression is calling runType or related functions from @mionjs/run-types
33
+ * @param node The call expression node
34
+ * @param context The ESLint context
35
+ * @returns True if it's a runType or related function call from @mionjs/run-types
36
+ */
37
+ function isRunTypeFromMionKit(node: TSESTree.CallExpression, context: TSESLint.RuleContext<any, any>): boolean {
38
+ // List of functions that should not use typeof
39
+ const runTypeFunctions = ['runType', 'isTypeFn', 'typeErrorsFn', 'mockTypeFn', 'toJavascriptFn'];
40
+
41
+ // Check if the callee is an identifier with one of the runType function names
42
+ if (node.callee.type !== AST_NODE_TYPES.Identifier || !runTypeFunctions.includes(node.callee.name)) {
43
+ return false;
44
+ }
45
+
46
+ // Get the source code to check imports
47
+ const sourceCode = context.sourceCode;
48
+ const program = sourceCode.ast;
49
+
50
+ // Look for import statements that import runType or related functions from @mionjs/run-types or relative paths
51
+ for (const statement of program.body) {
52
+ if (statement.type === AST_NODE_TYPES.ImportDeclaration) {
53
+ const source = statement.source.value;
54
+ // Check for @mionjs/run-types package or relative imports that end with runType or runTypeFunctions
55
+ if (
56
+ source === '@mionjs/run-types' ||
57
+ source === '@mionjs/run-types/' ||
58
+ (typeof source === 'string' && (source.endsWith('/runType') || source.endsWith('/runTypeFunctions')))
59
+ ) {
60
+ // Check if any of the runType functions are imported
61
+ for (const specifier of statement.specifiers) {
62
+ if (
63
+ specifier.type === AST_NODE_TYPES.ImportSpecifier &&
64
+ specifier.imported.type === AST_NODE_TYPES.Identifier &&
65
+ runTypeFunctions.includes(specifier.imported.name)
66
+ ) {
67
+ return true;
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ return false;
75
+ }
76
+
77
+ const rule: TSESLint.RuleModule<'noTypeof', []> = {
78
+ meta: {
79
+ type: 'problem',
80
+ docs: {
81
+ description: 'Disallow using `typeof` with run-type functions from @mionjs/run-types',
82
+ },
83
+ messages: {
84
+ noTypeof: 'Do not use `typeof` with `{{functionName}}()`. Use explicit type definitions instead.',
85
+ },
86
+ schema: [], // No options
87
+ },
88
+ defaultOptions: [],
89
+ create(context) {
90
+ return {
91
+ CallExpression(node: TSESTree.CallExpression) {
92
+ // Ensure the function being called is a run-type function from @mionjs/run-types
93
+ if (isRunTypeFromMionKit(node, context)) {
94
+ // Check if type arguments contain typeof
95
+ const typeArguments = (node as any).typeArguments || (node as any).typeParameters;
96
+ if (typeArguments?.params.some(containsTypeof)) {
97
+ const functionName = (node.callee as TSESTree.Identifier).name;
98
+ context.report({
99
+ node,
100
+ messageId: 'noTypeof',
101
+ data: {
102
+ functionName,
103
+ },
104
+ });
105
+ }
106
+ }
107
+ },
108
+ };
109
+ },
110
+ };
111
+
112
+ export default rule;
@@ -0,0 +1,357 @@
1
+ /* ########
2
+ * 2024 mion
3
+ * Author: Ma-jerez
4
+ * License: MIT
5
+ * The software is provided "as is", without warranty of any kind.
6
+ * ######## */
7
+
8
+ import {TSESTree, TSESLint, AST_NODE_TYPES} from '@typescript-eslint/utils';
9
+
10
+ type PropertyInfo = {
11
+ name: string;
12
+ isOptional: boolean;
13
+ };
14
+
15
+ /**
16
+ * Extracts property names from an object type (interface body, type literal)
17
+ */
18
+ function getObjectTypeProperties(node: TSESTree.TypeNode): PropertyInfo[] | null {
19
+ if (node.type === AST_NODE_TYPES.TSTypeLiteral) {
20
+ const props: PropertyInfo[] = [];
21
+ for (const member of node.members) {
22
+ if (member.type === AST_NODE_TYPES.TSPropertySignature && member.key.type === AST_NODE_TYPES.Identifier) {
23
+ props.push({name: member.key.name, isOptional: !!member.optional});
24
+ }
25
+ }
26
+ return props;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ /**
32
+ * Checks if typeB (earlier in union) blocks typeA (later in union), making typeA unreachable.
33
+ *
34
+ * TypeB blocks TypeA when:
35
+ * - All properties of TypeB (required + optional) exist in TypeA
36
+ * - TypeA has at least as many required properties as TypeB
37
+ * - TypeA is more specific than TypeB (more properties OR same properties but more required)
38
+ *
39
+ * Examples:
40
+ * - {a: string} blocks {a: string; b: number} - TypeB has 'a' (req), TypeA has 'a' (req) + 'b' (req)
41
+ * - {a?: string} does NOT block {b: number; c: string} - TypeB has 'a', TypeA doesn't have 'a'
42
+ * - {a: string; b?: number} blocks {a: string; b: number} - same props, but TypeA has more required
43
+ *
44
+ * @param typeAProps - Properties of the later type (potentially unreachable)
45
+ * @param typeBProps - Properties of the earlier type (potentially blocking)
46
+ * @returns true if typeB blocks typeA
47
+ */
48
+ function isSupersetOf(typeAProps: PropertyInfo[], typeBProps: PropertyInfo[]): boolean {
49
+ const typeARequired = typeAProps.filter((p) => !p.isOptional);
50
+ const typeBRequired = typeBProps.filter((p) => !p.isOptional);
51
+
52
+ // TypeA must have at least as many required properties as TypeB
53
+ if (typeARequired.length < typeBRequired.length) return false;
54
+
55
+ // TypeA must be more specific: either more properties OR same properties but more required
56
+ const isMoreSpecific =
57
+ typeAProps.length > typeBProps.length ||
58
+ (typeAProps.length === typeBProps.length && typeARequired.length > typeBRequired.length);
59
+
60
+ if (!isMoreSpecific) return false;
61
+
62
+ // Check if ALL properties of TypeB (required and optional) exist in TypeA
63
+ for (const propB of typeBProps) {
64
+ const propA = typeAProps.find((p) => p.name === propB.name);
65
+ // If TypeB has a prop that TypeA doesn't have, B doesn't block A
66
+ if (!propA) return false;
67
+ }
68
+
69
+ // TypeA has all properties of TypeB and is more specific, so TypeB blocks TypeA
70
+ return true;
71
+ }
72
+
73
+ /**
74
+ * Checks if a union type has interfaces where one is a superset of another
75
+ * and the subset comes before the superset (making the superset unreachable)
76
+ */
77
+ function findUnreachableTypes(unionNode: TSESTree.TSUnionType): {unreachable: TSESTree.TypeNode; blocker: TSESTree.TypeNode}[] {
78
+ const issues: {unreachable: TSESTree.TypeNode; blocker: TSESTree.TypeNode}[] = [];
79
+ const typesWithProps: {node: TSESTree.TypeNode; props: PropertyInfo[]}[] = [];
80
+
81
+ // Collect all object types with their properties
82
+ for (const typeNode of unionNode.types) {
83
+ const props = getObjectTypeProperties(typeNode);
84
+ if (props && props.length > 0) {
85
+ typesWithProps.push({node: typeNode, props});
86
+ }
87
+ }
88
+
89
+ // For each pair of types, check if one is a superset of another
90
+ for (let i = 0; i < typesWithProps.length; i++) {
91
+ for (let j = i + 1; j < typesWithProps.length; j++) {
92
+ const typeA = typesWithProps[i];
93
+ const typeB = typesWithProps[j];
94
+
95
+ // If typeA (earlier) is a subset of typeB (later), typeB is unreachable
96
+ if (isSupersetOf(typeB.props, typeA.props)) {
97
+ issues.push({unreachable: typeB.node, blocker: typeA.node});
98
+ }
99
+ }
100
+ }
101
+
102
+ return issues;
103
+ }
104
+
105
+ /**
106
+ * Gets a readable representation of an object type for error messages
107
+ */
108
+ function getTypeDescription(node: TSESTree.TypeNode): string {
109
+ if (node.type === AST_NODE_TYPES.TSTypeLiteral) {
110
+ const props = getObjectTypeProperties(node);
111
+ if (props) {
112
+ return `{${props.map((p) => (p.isOptional ? `${p.name}?` : p.name)).join(', ')}}`;
113
+ }
114
+ }
115
+ return 'object type';
116
+ }
117
+
118
+ /**
119
+ * Gets the router function name if the function is a handler for route/middleFn/headersFn
120
+ */
121
+ function getRouterFunctionName(
122
+ func: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration,
123
+ context: TSESLint.RuleContext<any, any>
124
+ ): string | null {
125
+ const parent = func.parent;
126
+ if (parent?.type === AST_NODE_TYPES.CallExpression) {
127
+ if (parent.callee.type === AST_NODE_TYPES.Identifier) {
128
+ const functionName = parent.callee.name;
129
+ if (['route', 'middleFn', 'headersFn'].includes(functionName) && isImportedFromMionRouter(functionName, context)) {
130
+ return functionName;
131
+ }
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+
137
+ /**
138
+ * Checks if the union type or type reference is in a parameter that should be checked
139
+ */
140
+ function isInCheckableParameter(
141
+ node: TSESTree.TSUnionType | TSESTree.TSTypeReference,
142
+ func: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration,
143
+ routerFunctionName: string
144
+ ): boolean {
145
+ // Find which parameter contains this union type
146
+ let current: TSESTree.Node | undefined = node.parent;
147
+ while (current && current !== func) {
148
+ // Check if we're in a parameter
149
+ if (
150
+ current.type === AST_NODE_TYPES.Identifier ||
151
+ current.type === AST_NODE_TYPES.ArrayPattern ||
152
+ current.type === AST_NODE_TYPES.ObjectPattern
153
+ ) {
154
+ const paramIndex = func.params.indexOf(current as TSESTree.Parameter);
155
+ if (paramIndex !== -1) {
156
+ // For route and middleFn: skip first parameter (context)
157
+ if ((routerFunctionName === 'route' || routerFunctionName === 'middleFn') && paramIndex >= 1) {
158
+ return true;
159
+ }
160
+ // For headersFn: skip first two parameters (context and headers)
161
+ if (routerFunctionName === 'headersFn' && paramIndex >= 2) {
162
+ return true;
163
+ }
164
+ return false;
165
+ }
166
+ }
167
+ current = current.parent;
168
+ }
169
+ return false;
170
+ }
171
+
172
+ /**
173
+ * Checks if the union type or type reference is a return type or parameter type of route/middleFn/headersFn
174
+ */
175
+ function isRouterUnionType(
176
+ node: TSESTree.TSUnionType | TSESTree.TSTypeReference,
177
+ context: TSESLint.RuleContext<any, any>
178
+ ): boolean {
179
+ // Check if we're in a return type annotation or parameter type annotation
180
+ let current: TSESTree.Node | undefined = node.parent;
181
+ while (current) {
182
+ // Check if we're in a function that's used in route/middleFn/headersFn
183
+ if (
184
+ current.type === AST_NODE_TYPES.ArrowFunctionExpression ||
185
+ current.type === AST_NODE_TYPES.FunctionExpression ||
186
+ current.type === AST_NODE_TYPES.FunctionDeclaration
187
+ ) {
188
+ const routerFunctionName = getRouterFunctionName(current, context);
189
+ if (routerFunctionName) {
190
+ // Check if we're in the return type
191
+ if (current.returnType?.typeAnnotation === node || isDescendantOf(node, current.returnType?.typeAnnotation)) {
192
+ return true;
193
+ }
194
+ // Check if we're in a parameter type (excluding context and headers params)
195
+ if (isInCheckableParameter(node, current, routerFunctionName)) {
196
+ return true;
197
+ }
198
+ }
199
+ }
200
+ // Check if we're in a type annotation for Handler/HeaderHandler
201
+ if (current.type === AST_NODE_TYPES.TSTypeAnnotation) {
202
+ const typeAnnotationParent = current.parent;
203
+ if (typeAnnotationParent?.type === AST_NODE_TYPES.Identifier) {
204
+ const declarator = typeAnnotationParent.parent;
205
+ if (declarator?.type === AST_NODE_TYPES.VariableDeclarator) {
206
+ // Check if the type annotation references Handler/HeaderHandler
207
+ if (current.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference) {
208
+ const typeName = current.typeAnnotation.typeName;
209
+ if (typeName.type === AST_NODE_TYPES.Identifier) {
210
+ if (
211
+ (typeName.name === 'Handler' || typeName.name === 'HeaderHandler') &&
212
+ isImportedFromMionRouter(typeName.name, context)
213
+ ) {
214
+ return true;
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ current = current.parent;
222
+ }
223
+ return false;
224
+ }
225
+
226
+ /**
227
+ * Checks if a node is a descendant of another node
228
+ */
229
+ function isDescendantOf(node: TSESTree.Node | undefined, ancestor: TSESTree.Node | undefined): boolean {
230
+ if (!node || !ancestor) return false;
231
+ let current: TSESTree.Node | undefined = node;
232
+ while (current) {
233
+ if (current === ancestor) return true;
234
+ current = current.parent;
235
+ }
236
+ return false;
237
+ }
238
+
239
+ /**
240
+ * Checks if a name is imported from @mionjs/router
241
+ */
242
+ function isImportedFromMionRouter(name: string, context: TSESLint.RuleContext<any, any>): boolean {
243
+ const sourceCode = context.sourceCode;
244
+ const program = sourceCode.ast;
245
+
246
+ for (const statement of program.body) {
247
+ if (statement.type === AST_NODE_TYPES.ImportDeclaration) {
248
+ const source = statement.source.value;
249
+ if (source === '@mionjs/router' || source === '@mionjs/router/') {
250
+ for (const specifier of statement.specifiers) {
251
+ if (
252
+ specifier.type === AST_NODE_TYPES.ImportSpecifier &&
253
+ specifier.imported.type === AST_NODE_TYPES.Identifier &&
254
+ specifier.imported.name === name
255
+ ) {
256
+ return true;
257
+ }
258
+ }
259
+ }
260
+ }
261
+ }
262
+ return false;
263
+ }
264
+
265
+ /**
266
+ * Resolves a type reference to its actual union type definition
267
+ */
268
+ function resolveTypeReference(node: TSESTree.TypeNode, context: TSESLint.RuleContext<any, any>): TSESTree.TSUnionType | null {
269
+ if (node.type !== AST_NODE_TYPES.TSTypeReference) {
270
+ return null;
271
+ }
272
+
273
+ // Get the type name
274
+ if (node.typeName.type !== AST_NODE_TYPES.Identifier) {
275
+ return null;
276
+ }
277
+
278
+ const typeName = node.typeName.name;
279
+ const sourceCode = context.sourceCode;
280
+ const program = sourceCode.ast;
281
+
282
+ // Find the type alias declaration
283
+ for (const statement of program.body) {
284
+ if (statement.type === AST_NODE_TYPES.TSTypeAliasDeclaration) {
285
+ if (statement.id.name === typeName && statement.typeAnnotation.type === AST_NODE_TYPES.TSUnionType) {
286
+ return statement.typeAnnotation;
287
+ }
288
+ }
289
+ }
290
+
291
+ return null;
292
+ }
293
+
294
+ const rule: TSESLint.RuleModule<'unreachableUnionType', []> = {
295
+ meta: {
296
+ type: 'problem',
297
+ docs: {
298
+ description:
299
+ 'Detect union types where one interface is unreachable at runtime when using isType function because a subset type comes before it',
300
+ },
301
+ messages: {
302
+ unreachableUnionType:
303
+ 'Union type {{unreachableType}} is unreachable at runtime when doing type checking because {{blockerType}} will always match first. ' +
304
+ 'To fix this move the more specific type {{unreachableType}} first within the union, ie: {{unreachableType}} | {{blockerType}} ',
305
+ },
306
+ schema: [],
307
+ },
308
+ defaultOptions: [],
309
+ create(context) {
310
+ return {
311
+ TSUnionType(node: TSESTree.TSUnionType) {
312
+ // Only check unions in router context
313
+ if (!isRouterUnionType(node, context)) {
314
+ return;
315
+ }
316
+
317
+ const issues = findUnreachableTypes(node);
318
+ for (const issue of issues) {
319
+ context.report({
320
+ node: node,
321
+ messageId: 'unreachableUnionType',
322
+ data: {
323
+ unreachableType: getTypeDescription(issue.unreachable),
324
+ blockerType: getTypeDescription(issue.blocker),
325
+ },
326
+ });
327
+ }
328
+ },
329
+ TSTypeReference(node: TSESTree.TSTypeReference) {
330
+ // Try to resolve the type reference to a union type
331
+ const unionType = resolveTypeReference(node, context);
332
+ if (!unionType) {
333
+ return;
334
+ }
335
+
336
+ // Only check if this type reference is used in a router context
337
+ if (!isRouterUnionType(node, context)) {
338
+ return;
339
+ }
340
+
341
+ const issues = findUnreachableTypes(unionType);
342
+ for (const issue of issues) {
343
+ context.report({
344
+ node: node,
345
+ messageId: 'unreachableUnionType',
346
+ data: {
347
+ unreachableType: getTypeDescription(issue.unreachable),
348
+ blockerType: getTypeDescription(issue.blocker),
349
+ },
350
+ });
351
+ }
352
+ },
353
+ };
354
+ },
355
+ };
356
+
357
+ export default rule;
@@ -0,0 +1,101 @@
1
+ /* ########
2
+ * 2026 mion
3
+ * Author: Ma-jerez
4
+ * License: MIT
5
+ * The software is provided "as is", without warranty of any kind.
6
+ * ######## */
7
+
8
+ import {TSESTree, TSESLint, AST_NODE_TYPES} from '@typescript-eslint/utils';
9
+ import {PURE_FN_SOURCE_PACKAGES} from '../../pureFns/purityRules.ts';
10
+
11
+ type MessageIds = 'missingPureFnName' | 'missingMapFromName' | 'nameNotStringLiteral' | 'registerPureFnFactoryNotAllowed';
12
+
13
+ /** Enforces non-Vite client constraints: requires names for pureServerFn/mapFrom and disallows registerPureFnFactory */
14
+ const rule: TSESLint.RuleModule<MessageIds, []> = {
15
+ meta: {
16
+ type: 'problem',
17
+ docs: {
18
+ description:
19
+ 'Enforce non-Vite client constraints: require name/bodyHash string literal for pureServerFn() and mapFrom(), and disallow registerPureFnFactory() which requires Vite transforms.',
20
+ },
21
+ messages: {
22
+ missingPureFnName:
23
+ 'pureServerFn() requires a name as the second argument (string literal) for non-Vite environments.',
24
+ missingMapFromName: 'mapFrom() requires a name as the third argument (string literal) for non-Vite environments.',
25
+ nameNotStringLiteral: '{{callee}}() name argument must be a string literal, not a variable or expression.',
26
+ registerPureFnFactoryNotAllowed:
27
+ 'registerPureFnFactory() is not supported in non-Vite environments. It requires Vite build-time transforms.',
28
+ },
29
+ schema: [],
30
+ },
31
+ defaultOptions: [],
32
+ create(context) {
33
+ let pureFnNames: Map<string, string> | null = null;
34
+
35
+ return {
36
+ Program(node: TSESTree.Program) {
37
+ pureFnNames = new Map();
38
+ for (const statement of node.body) {
39
+ if (statement.type !== AST_NODE_TYPES.ImportDeclaration) continue;
40
+ const source = statement.source.value;
41
+ if (!PURE_FN_SOURCE_PACKAGES.includes(source as (typeof PURE_FN_SOURCE_PACKAGES)[number])) continue;
42
+
43
+ for (const specifier of statement.specifiers) {
44
+ if (
45
+ specifier.type === AST_NODE_TYPES.ImportSpecifier &&
46
+ specifier.imported.type === AST_NODE_TYPES.Identifier
47
+ ) {
48
+ const importedName = specifier.imported.name;
49
+ if (
50
+ importedName === 'pureServerFn' ||
51
+ importedName === 'mapFrom' ||
52
+ importedName === 'registerPureFnFactory'
53
+ ) {
54
+ pureFnNames.set(specifier.local.name, importedName);
55
+ }
56
+ }
57
+ }
58
+ }
59
+ },
60
+
61
+ CallExpression(node: TSESTree.CallExpression) {
62
+ if (!pureFnNames || pureFnNames.size === 0) return;
63
+ if (node.callee.type !== AST_NODE_TYPES.Identifier) return;
64
+ const importedName = pureFnNames.get(node.callee.name);
65
+ if (!importedName) return;
66
+
67
+ if (importedName === 'pureServerFn') {
68
+ if (node.arguments.length < 2) {
69
+ context.report({node, messageId: 'missingPureFnName'});
70
+ } else if (
71
+ node.arguments[1].type !== AST_NODE_TYPES.Literal ||
72
+ typeof (node.arguments[1] as TSESTree.Literal).value !== 'string'
73
+ ) {
74
+ context.report({
75
+ node: node.arguments[1],
76
+ messageId: 'nameNotStringLiteral',
77
+ data: {callee: 'pureServerFn'},
78
+ });
79
+ }
80
+ } else if (importedName === 'mapFrom') {
81
+ if (node.arguments.length < 3) {
82
+ context.report({node, messageId: 'missingMapFromName'});
83
+ } else if (
84
+ node.arguments[2].type !== AST_NODE_TYPES.Literal ||
85
+ typeof (node.arguments[2] as TSESTree.Literal).value !== 'string'
86
+ ) {
87
+ context.report({
88
+ node: node.arguments[2],
89
+ messageId: 'nameNotStringLiteral',
90
+ data: {callee: 'mapFrom'},
91
+ });
92
+ }
93
+ } else if (importedName === 'registerPureFnFactory') {
94
+ context.report({node, messageId: 'registerPureFnFactoryNotAllowed'});
95
+ }
96
+ },
97
+ };
98
+ },
99
+ };
100
+
101
+ export default rule;