@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,537 @@
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
+ // List of router functions that should have strongly typed handlers
11
+ const ROUTER_FUNCTIONS = ['route', 'middleFn', 'headersFn'] as const;
12
+ // List of handler types that can be used with type annotations
13
+ const HANDLER_TYPES = ['Handler', 'HeaderHandler'] as const;
14
+
15
+ /**
16
+ * Cache for imports from @mionjs/router - computed once per file
17
+ */
18
+ interface MionRouterImports {
19
+ /** Set of function names imported from @mionjs/router (route, middleFn, headersFn) */
20
+ routerFunctions: Set<string>;
21
+ /** Set of type names imported from @mionjs/router (Handler, HeaderHandler) */
22
+ handlerTypes: Set<string>;
23
+ }
24
+
25
+ /**
26
+ * Builds a cache of all imports from @mionjs/router
27
+ * This is called once per file in the Program visitor
28
+ */
29
+ function buildImportCache(program: TSESTree.Program): MionRouterImports {
30
+ const routerFunctions = new Set<string>();
31
+ const handlerTypes = new Set<string>();
32
+
33
+ for (const statement of program.body) {
34
+ if (statement.type === AST_NODE_TYPES.ImportDeclaration) {
35
+ const source = statement.source.value;
36
+ // Check for @mionjs/router package
37
+ if (source === '@mionjs/router' || source === '@mionjs/router/') {
38
+ for (const specifier of statement.specifiers) {
39
+ if (
40
+ specifier.type === AST_NODE_TYPES.ImportSpecifier &&
41
+ specifier.imported.type === AST_NODE_TYPES.Identifier
42
+ ) {
43
+ const name = specifier.imported.name;
44
+ if (ROUTER_FUNCTIONS.includes(name as (typeof ROUTER_FUNCTIONS)[number])) {
45
+ routerFunctions.add(name);
46
+ }
47
+ if (HANDLER_TYPES.includes(name as (typeof HANDLER_TYPES)[number])) {
48
+ handlerTypes.add(name);
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ return {routerFunctions, handlerTypes};
57
+ }
58
+
59
+ /**
60
+ * Checks if a call expression is calling router functions from @mionjs/router
61
+ * Uses the cached import information for performance
62
+ */
63
+ function getRouterFunctionName(node: TSESTree.CallExpression, importCache: MionRouterImports): string | null {
64
+ // Check if the callee is an identifier with one of the router function names
65
+ if (node.callee.type !== AST_NODE_TYPES.Identifier) {
66
+ return null;
67
+ }
68
+
69
+ const functionName = node.callee.name;
70
+
71
+ // Check if this function is imported from @mionjs/router
72
+ if (importCache.routerFunctions.has(functionName)) {
73
+ return functionName;
74
+ }
75
+
76
+ return null;
77
+ }
78
+
79
+ /**
80
+ * Cache for top-level function declarations and variable function expressions
81
+ * This avoids iterating through the program body for each function reference
82
+ */
83
+ type FunctionCache = Map<string, TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration>;
84
+
85
+ /**
86
+ * Builds a cache of all top-level function declarations and function variable assignments
87
+ * This is called once per file in the Program visitor
88
+ */
89
+ function buildFunctionCache(program: TSESTree.Program): FunctionCache {
90
+ const cache: FunctionCache = new Map();
91
+
92
+ for (const statement of program.body) {
93
+ // Check function declarations
94
+ if (statement.type === AST_NODE_TYPES.FunctionDeclaration && statement.id?.name) {
95
+ cache.set(statement.id.name, statement);
96
+ }
97
+
98
+ // Check variable declarations
99
+ if (statement.type === AST_NODE_TYPES.VariableDeclaration) {
100
+ for (const declarator of statement.declarations) {
101
+ if (declarator.id.type === AST_NODE_TYPES.Identifier) {
102
+ if (
103
+ declarator.init?.type === AST_NODE_TYPES.ArrowFunctionExpression ||
104
+ declarator.init?.type === AST_NODE_TYPES.FunctionExpression
105
+ ) {
106
+ cache.set(declarator.id.name, declarator.init);
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ return cache;
114
+ }
115
+
116
+ /**
117
+ * Gets the handler function from a router function call
118
+ * Uses the cached function information for performance
119
+ */
120
+ function getHandlerFunction(
121
+ node: TSESTree.CallExpression,
122
+ functionCache: FunctionCache
123
+ ): TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration | null {
124
+ // Handler is always the first parameter for route, middleFn, and headersFn
125
+ const handlerIndex = 0;
126
+
127
+ if (node.arguments.length <= handlerIndex) {
128
+ return null;
129
+ }
130
+
131
+ const handlerArg = node.arguments[handlerIndex];
132
+
133
+ // Handle inline function expressions/arrow functions
134
+ if (handlerArg.type === AST_NODE_TYPES.ArrowFunctionExpression || handlerArg.type === AST_NODE_TYPES.FunctionExpression) {
135
+ return handlerArg;
136
+ }
137
+
138
+ // Handle function references (identifiers) - use cached function lookup
139
+ if (handlerArg.type === AST_NODE_TYPES.Identifier) {
140
+ return functionCache.get(handlerArg.name) ?? null;
141
+ }
142
+
143
+ return null;
144
+ }
145
+
146
+ /**
147
+ * Checks if a function has an explicit return type annotation
148
+ * @param func The function node
149
+ * @returns True if it has explicit return type, false otherwise
150
+ */
151
+ function hasExplicitReturnType(
152
+ func: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration
153
+ ): boolean {
154
+ return func.returnType !== undefined;
155
+ }
156
+
157
+ /**
158
+ * Checks if a node represents a primitive literal value
159
+ * @param node The node to check
160
+ * @returns True if the node is a primitive literal (boolean, string, number, null, undefined, bigint)
161
+ */
162
+ function isPrimitiveLiteral(node: TSESTree.Node): boolean {
163
+ // Check for boolean, string, number, null, or bigint literals
164
+ if (node.type === AST_NODE_TYPES.Literal) {
165
+ const value = node.value;
166
+ // BigInt literals have a 'bigint' property and value is null
167
+ if ('bigint' in node) {
168
+ return true;
169
+ }
170
+ return typeof value === 'boolean' || typeof value === 'string' || typeof value === 'number' || value === null;
171
+ }
172
+ // Check for undefined identifier
173
+ if (node.type === AST_NODE_TYPES.Identifier && node.name === 'undefined') {
174
+ return true;
175
+ }
176
+ // Check for negative numbers (UnaryExpression with -)
177
+ if (node.type === AST_NODE_TYPES.UnaryExpression && node.operator === '-' && node.argument.type === AST_NODE_TYPES.Literal) {
178
+ return typeof node.argument.value === 'number';
179
+ }
180
+ return false;
181
+ }
182
+
183
+ /**
184
+ * Checks if all parameters (except the first) have explicit type annotations
185
+ * @param func The function node
186
+ * @returns Object with validation results including the actual parameter nodes
187
+ */
188
+ function validateParameterTypes(
189
+ func: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration
190
+ ): {
191
+ valid: boolean;
192
+ missingTypeParams: string[];
193
+ missingParamNodes: TSESTree.Parameter[];
194
+ } {
195
+ const missingTypeParams: string[] = [];
196
+ const missingParamNodes: TSESTree.Parameter[] = [];
197
+
198
+ // Skip the first parameter (context), check all others
199
+ for (let i = 1; i < func.params.length; i++) {
200
+ const param = func.params[i];
201
+
202
+ if (param.type === AST_NODE_TYPES.Identifier) {
203
+ if (!param.typeAnnotation) {
204
+ missingTypeParams.push(param.name);
205
+ missingParamNodes.push(param);
206
+ }
207
+ } else if (param.type === AST_NODE_TYPES.RestElement) {
208
+ if (param.argument.type === AST_NODE_TYPES.Identifier && !param.typeAnnotation) {
209
+ missingTypeParams.push(`...${param.argument.name}`);
210
+ missingParamNodes.push(param);
211
+ }
212
+ } else if (param.type === AST_NODE_TYPES.ArrayPattern || param.type === AST_NODE_TYPES.ObjectPattern) {
213
+ // For destructuring patterns, check if they have type annotations
214
+ if (!param.typeAnnotation) {
215
+ missingTypeParams.push(`parameter ${i + 1}`);
216
+ missingParamNodes.push(param);
217
+ }
218
+ } else if (param.type === AST_NODE_TYPES.AssignmentPattern) {
219
+ // For default parameters (e.g., `name = 'default'` or `name: string = 'default'`), check if:
220
+ // 1. There's an explicit type annotation on the AssignmentPattern itself, OR
221
+ // 2. There's an explicit type annotation on the left side (for `name: Type = value` pattern), OR
222
+ // 3. The default value is a primitive literal (type can be inferred)
223
+ const hasTypeAnnotationOnPattern = param.typeAnnotation !== undefined;
224
+ const hasTypeAnnotationOnLeft =
225
+ param.left.type === AST_NODE_TYPES.Identifier && param.left.typeAnnotation !== undefined;
226
+ const hasTypeAnnotation = hasTypeAnnotationOnPattern || hasTypeAnnotationOnLeft;
227
+ const hasPrimitiveDefault = isPrimitiveLiteral(param.right);
228
+
229
+ if (!hasTypeAnnotation && !hasPrimitiveDefault) {
230
+ // Need explicit type annotation since type cannot be inferred from default value
231
+ const paramName = param.left.type === AST_NODE_TYPES.Identifier ? param.left.name : `parameter ${i + 1}`;
232
+ missingTypeParams.push(paramName);
233
+ missingParamNodes.push(param);
234
+ }
235
+ }
236
+ // For other parameter types, we assume they need type annotations too
237
+ else if (!('typeAnnotation' in param) || !param.typeAnnotation) {
238
+ missingTypeParams.push(`parameter ${i + 1}`);
239
+ missingParamNodes.push(param);
240
+ }
241
+ }
242
+
243
+ return {
244
+ valid: missingTypeParams.length === 0,
245
+ missingTypeParams,
246
+ missingParamNodes,
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Extracts a readable parameter name from a parameter node
252
+ */
253
+ function getParameterName(param: TSESTree.Parameter): string {
254
+ if (param.type === AST_NODE_TYPES.Identifier) {
255
+ return param.name;
256
+ } else if (param.type === AST_NODE_TYPES.RestElement && param.argument.type === AST_NODE_TYPES.Identifier) {
257
+ return `...${param.argument.name}`;
258
+ } else if (param.type === AST_NODE_TYPES.ArrayPattern) {
259
+ return '[...]';
260
+ } else if (param.type === AST_NODE_TYPES.ObjectPattern) {
261
+ return '{...}';
262
+ } else if (param.type === AST_NODE_TYPES.AssignmentPattern) {
263
+ if (param.left.type === AST_NODE_TYPES.Identifier) {
264
+ return param.left.name;
265
+ }
266
+ return 'param';
267
+ }
268
+ return 'param';
269
+ }
270
+
271
+ /**
272
+ * Gets the node to report for missing return type
273
+ * For arrow functions, this is the arrow token area; for regular functions, the function keyword
274
+ */
275
+ function getReturnTypeReportNode(
276
+ func: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration
277
+ ): TSESTree.Node {
278
+ // For function declarations/expressions with an id, report on the id
279
+ if ((func.type === AST_NODE_TYPES.FunctionDeclaration || func.type === AST_NODE_TYPES.FunctionExpression) && func.id) {
280
+ return func.id;
281
+ }
282
+ // Otherwise report on the function itself (will highlight just the signature area)
283
+ return func;
284
+ }
285
+
286
+ /**
287
+ * Checks if a type annotation references Handler or HeaderHandler from @mionjs/router
288
+ * Uses the cached import information for performance
289
+ */
290
+ function getHandlerTypeFromAnnotation(
291
+ typeAnnotation: TSESTree.TSTypeAnnotation,
292
+ importCache: MionRouterImports
293
+ ): 'Handler' | 'HeaderHandler' | null {
294
+ if (typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference) {
295
+ const typeName = typeAnnotation.typeAnnotation.typeName;
296
+ if (typeName.type === AST_NODE_TYPES.Identifier) {
297
+ const name = typeName.name;
298
+ if ((name === 'Handler' || name === 'HeaderHandler') && importCache.handlerTypes.has(name)) {
299
+ return name as 'Handler' | 'HeaderHandler';
300
+ }
301
+ }
302
+ }
303
+ return null;
304
+ }
305
+
306
+ /**
307
+ * Checks if a satisfies expression references Handler or HeaderHandler from @mionjs/router
308
+ * Uses the cached import information for performance
309
+ */
310
+ function getHandlerTypeFromSatisfies(
311
+ satisfiesExpression: TSESTree.TSSatisfiesExpression,
312
+ importCache: MionRouterImports
313
+ ): 'Handler' | 'HeaderHandler' | null {
314
+ if (satisfiesExpression.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference) {
315
+ const typeName = satisfiesExpression.typeAnnotation.typeName;
316
+ if (typeName.type === AST_NODE_TYPES.Identifier) {
317
+ const name = typeName.name;
318
+ if ((name === 'Handler' || name === 'HeaderHandler') && importCache.handlerTypes.has(name)) {
319
+ return name as 'Handler' | 'HeaderHandler';
320
+ }
321
+ }
322
+ }
323
+ return null;
324
+ }
325
+
326
+ /**
327
+ * Checks if a function has JSDoc tags indicating it should be type-checked
328
+ * @param node The node to check for JSDoc comments
329
+ * @param context The ESLint context
330
+ * @returns The handler type if JSDoc tag is found, null otherwise
331
+ */
332
+ function getHandlerTypeFromJSDoc(
333
+ node:
334
+ | TSESTree.ArrowFunctionExpression
335
+ | TSESTree.FunctionExpression
336
+ | TSESTree.FunctionDeclaration
337
+ | TSESTree.VariableDeclaration,
338
+ context: TSESLint.RuleContext<any, any>
339
+ ): 'Handler' | 'HeaderHandler' | 'MiddleFnHandler' | null {
340
+ const sourceCode = context.sourceCode;
341
+ const comments = sourceCode.getCommentsBefore(node);
342
+
343
+ for (const comment of comments) {
344
+ if (comment.type === 'Block') {
345
+ const commentText = comment.value;
346
+ if (commentText.includes('@mion:route')) {
347
+ return 'Handler';
348
+ }
349
+ if (commentText.includes('@mion:middleFn')) {
350
+ return 'MiddleFnHandler';
351
+ }
352
+ if (commentText.includes('@mion:headersFn')) {
353
+ return 'HeaderHandler';
354
+ }
355
+ }
356
+ }
357
+ return null;
358
+ }
359
+
360
+ const rule: TSESLint.RuleModule<
361
+ 'missingReturnType' | 'missingParamTypes' | 'missingReturnTypeRouter' | 'missingParamTypesRouter',
362
+ []
363
+ > = {
364
+ meta: {
365
+ type: 'problem',
366
+ docs: {
367
+ description: 'Enforce explicit parameters and return type annotations for router handler functions',
368
+ },
369
+ messages: {
370
+ missingReturnType: 'mion {{handlerType}}() handler must define a return type.',
371
+ missingParamTypes: 'mion parameter "{{paramName}}" must have an explicit type definition.',
372
+ missingReturnTypeRouter: 'mion {{routerFunction}}() handler must define a return type.',
373
+ missingParamTypesRouter: 'mion parameter "{{paramName}}" must have an explicit type definition.',
374
+ },
375
+ schema: [], // No options
376
+ },
377
+ defaultOptions: [],
378
+ create(context) {
379
+ // Caches built once per file for performance
380
+ let importCache: MionRouterImports | null = null;
381
+ let functionCache: FunctionCache | null = null;
382
+
383
+ return {
384
+ // Build caches once when we start processing the file
385
+ Program(node: TSESTree.Program) {
386
+ importCache = buildImportCache(node);
387
+ functionCache = buildFunctionCache(node);
388
+ },
389
+ CallExpression(node: TSESTree.CallExpression) {
390
+ if (!importCache || !functionCache) return;
391
+
392
+ const functionName = getRouterFunctionName(node, importCache);
393
+ if (!functionName) {
394
+ return;
395
+ }
396
+
397
+ const handlerFunc = getHandlerFunction(node, functionCache);
398
+ if (!handlerFunc) {
399
+ return;
400
+ }
401
+
402
+ const hasReturnType = hasExplicitReturnType(handlerFunc);
403
+ const paramValidation = validateParameterTypes(handlerFunc);
404
+
405
+ // Report errors on specific nodes for better highlighting
406
+ if (!hasReturnType) {
407
+ context.report({
408
+ node: getReturnTypeReportNode(handlerFunc),
409
+ messageId: 'missingReturnTypeRouter',
410
+ data: {
411
+ routerFunction: functionName,
412
+ },
413
+ });
414
+ }
415
+
416
+ // Report each missing parameter type separately
417
+ for (const paramNode of paramValidation.missingParamNodes) {
418
+ context.report({
419
+ node: paramNode,
420
+ messageId: 'missingParamTypesRouter',
421
+ data: {
422
+ paramName: getParameterName(paramNode),
423
+ },
424
+ });
425
+ }
426
+ },
427
+ // Check variable declarations with type annotations or JSDoc tags
428
+ VariableDeclarator(node: TSESTree.VariableDeclarator) {
429
+ if (!importCache) return;
430
+
431
+ if (node.id.type === AST_NODE_TYPES.Identifier) {
432
+ let handlerType: 'Handler' | 'HeaderHandler' | 'MiddleFnHandler' | null = null;
433
+
434
+ // Check for type annotation
435
+ if (node.id.typeAnnotation) {
436
+ handlerType = getHandlerTypeFromAnnotation(node.id.typeAnnotation, importCache);
437
+ }
438
+
439
+ // Check for JSDoc tags on the variable declaration
440
+ if (!handlerType && node.parent?.type === AST_NODE_TYPES.VariableDeclaration) {
441
+ handlerType = getHandlerTypeFromJSDoc(node.parent, context);
442
+ }
443
+
444
+ if (
445
+ handlerType &&
446
+ (node.init?.type === AST_NODE_TYPES.ArrowFunctionExpression ||
447
+ node.init?.type === AST_NODE_TYPES.FunctionExpression)
448
+ ) {
449
+ checkHandlerFunction(node.init, handlerType, context);
450
+ }
451
+ }
452
+ },
453
+ // Check satisfies expressions
454
+ TSSatisfiesExpression(node: TSESTree.TSSatisfiesExpression) {
455
+ if (!importCache) return;
456
+
457
+ const handlerType = getHandlerTypeFromSatisfies(node, importCache);
458
+ if (
459
+ handlerType &&
460
+ (node.expression.type === AST_NODE_TYPES.ArrowFunctionExpression ||
461
+ node.expression.type === AST_NODE_TYPES.FunctionExpression)
462
+ ) {
463
+ checkHandlerFunction(node.expression, handlerType, context);
464
+ }
465
+ },
466
+ // Check function declarations with JSDoc tags
467
+ FunctionDeclaration(node: TSESTree.FunctionDeclaration) {
468
+ const handlerType = getHandlerTypeFromJSDoc(node, context);
469
+ if (handlerType) {
470
+ checkHandlerFunction(node, handlerType, context);
471
+ }
472
+ },
473
+ // Check arrow functions and function expressions with JSDoc tags
474
+ ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression) {
475
+ const handlerType = getHandlerTypeFromJSDoc(node, context);
476
+ if (handlerType) {
477
+ checkHandlerFunction(node, handlerType, context);
478
+ }
479
+ },
480
+ FunctionExpression(node: TSESTree.FunctionExpression) {
481
+ const handlerType = getHandlerTypeFromJSDoc(node, context);
482
+ if (handlerType) {
483
+ checkHandlerFunction(node, handlerType, context);
484
+ }
485
+ },
486
+ };
487
+ },
488
+ };
489
+
490
+ /**
491
+ * Maps Handler/HeaderHandler/MiddleFnHandler type names to their corresponding router function names
492
+ */
493
+ function handlerTypeToFunctionName(handlerType: 'Handler' | 'HeaderHandler' | 'MiddleFnHandler'): string {
494
+ if (handlerType === 'HeaderHandler') return 'headersFn';
495
+ if (handlerType === 'MiddleFnHandler') return 'middleFn';
496
+ return 'route';
497
+ }
498
+
499
+ /**
500
+ * Helper function to check a handler function for type annotations
501
+ * @param func The function node to check
502
+ * @param handlerType The expected handler type
503
+ * @param context The ESLint context
504
+ */
505
+ function checkHandlerFunction(
506
+ func: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration,
507
+ handlerType: 'Handler' | 'HeaderHandler' | 'MiddleFnHandler',
508
+ context: TSESLint.RuleContext<any, any>
509
+ ) {
510
+ const hasReturnType = hasExplicitReturnType(func);
511
+ const paramValidation = validateParameterTypes(func);
512
+ const functionName = handlerTypeToFunctionName(handlerType);
513
+
514
+ // Report errors on specific nodes for better highlighting
515
+ if (!hasReturnType) {
516
+ context.report({
517
+ node: getReturnTypeReportNode(func),
518
+ messageId: 'missingReturnType',
519
+ data: {
520
+ handlerType: functionName,
521
+ },
522
+ });
523
+ }
524
+
525
+ // Report each missing parameter type separately
526
+ for (const paramNode of paramValidation.missingParamNodes) {
527
+ context.report({
528
+ node: paramNode,
529
+ messageId: 'missingParamTypes',
530
+ data: {
531
+ paramName: getParameterName(paramNode),
532
+ },
533
+ });
534
+ }
535
+ }
536
+
537
+ export default rule;
@@ -0,0 +1,73 @@
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 {FORMAT_TYPES_BY_PACKAGE} from './formatTypeNames.ts';
10
+
11
+ /** Checks if source matches one of the format type packages (exact or subpath) */
12
+ function getFormatTypePackage(source: string): string | null {
13
+ for (const [pkg] of FORMAT_TYPES_BY_PACKAGE) {
14
+ if (source === pkg || source.startsWith(pkg + '/')) return pkg;
15
+ }
16
+ return null;
17
+ }
18
+
19
+ const rule: TSESLint.RuleModule<'typeFormatsImports', []> = {
20
+ meta: {
21
+ type: 'problem',
22
+ docs: {
23
+ description:
24
+ 'Disallow type-only imports for TypeFormat types. Type-only imports strip type metadata needed by Deepkit for runtime type reflection, causing silent validation/serialization failures.',
25
+ },
26
+ messages: {
27
+ typeFormatsImports:
28
+ 'Type "{{typeName}}" is imported as type-only from "{{source}}". Remove the "type" keyword from the import to preserve runtime type metadata for Deepkit reflection.',
29
+ },
30
+ schema: [],
31
+ },
32
+ defaultOptions: [],
33
+ create(context) {
34
+ return {
35
+ ImportDeclaration(node: TSESTree.ImportDeclaration) {
36
+ const source = node.source.value;
37
+ const matchedPkg = getFormatTypePackage(source);
38
+ if (!matchedPkg) return;
39
+
40
+ const formatTypes = FORMAT_TYPES_BY_PACKAGE.get(matchedPkg);
41
+ if (!formatTypes) return;
42
+
43
+ const isTypeOnlyImport = node.importKind === 'type';
44
+
45
+ for (const specifier of node.specifiers) {
46
+ if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) continue;
47
+
48
+ const importedName =
49
+ specifier.imported.type === AST_NODE_TYPES.Identifier
50
+ ? specifier.imported.name
51
+ : specifier.imported.value;
52
+
53
+ if (!formatTypes.has(importedName)) continue;
54
+
55
+ const isTypeOnlySpecifier = specifier.importKind === 'type';
56
+
57
+ if (isTypeOnlyImport || isTypeOnlySpecifier) {
58
+ context.report({
59
+ node: specifier,
60
+ messageId: 'typeFormatsImports',
61
+ data: {
62
+ typeName: importedName,
63
+ source,
64
+ },
65
+ });
66
+ }
67
+ }
68
+ },
69
+ };
70
+ },
71
+ };
72
+
73
+ export default rule;
@@ -0,0 +1,76 @@
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
+ /** Globals and built-ins that are allowed inside pure functions */
9
+ export const ALLOWED_GLOBALS = new Set([
10
+ // Value types
11
+ 'undefined',
12
+ 'null',
13
+ 'NaN',
14
+ 'Infinity',
15
+ 'true',
16
+ 'false',
17
+ // Built-in constructors/objects
18
+ 'Object',
19
+ 'Array',
20
+ 'String',
21
+ 'Number',
22
+ 'Boolean',
23
+ 'Math',
24
+ 'JSON',
25
+ 'Date',
26
+ 'RegExp',
27
+ 'Map',
28
+ 'Set',
29
+ 'WeakMap',
30
+ 'WeakSet',
31
+ 'Symbol',
32
+ 'BigInt',
33
+ 'Promise',
34
+ 'Error',
35
+ 'TypeError',
36
+ 'RangeError',
37
+ 'parseInt',
38
+ 'parseFloat',
39
+ 'isNaN',
40
+ 'isFinite',
41
+ 'encodeURIComponent',
42
+ 'decodeURIComponent',
43
+ 'encodeURI',
44
+ 'decodeURI',
45
+ // Common safe globals
46
+ 'console',
47
+ 'globalThis',
48
+ 'Bun',
49
+ ]);
50
+
51
+ /** Forbidden identifiers that indicate impure operations in pure functions */
52
+ export const FORBIDDEN_IDENTIFIERS = new Set([
53
+ 'eval',
54
+ 'Function',
55
+ 'fetch',
56
+ 'setTimeout',
57
+ 'setInterval',
58
+ 'clearTimeout',
59
+ 'clearInterval',
60
+ 'process',
61
+ 'window',
62
+ 'document',
63
+ 'global',
64
+ 'require',
65
+ 'XMLHttpRequest',
66
+ 'WebSocket',
67
+ 'localStorage',
68
+ 'sessionStorage',
69
+ 'indexedDB',
70
+ ]);
71
+
72
+ /** The target function names that purity validation applies to */
73
+ export const PURE_FN_CALL_NAMES = ['pureServerFn', 'registerPureFnFactory', 'mapFrom'] as const;
74
+
75
+ /** The packages that export the target pure function APIs */
76
+ export const PURE_FN_SOURCE_PACKAGES = ['@mionjs/core', '@mionjs/client'] as const;