@mionjs/devtools 0.8.7 → 0.8.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/eslint/cjs/index.cjs.map +1 -1
- package/build/eslint/cjs/src/eslint/index.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/index.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/enforce-type-imports.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/enforce-type-imports.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/formatTypeNames.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/formatTypeNames.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-mixed-union-properties.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-mixed-union-properties.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-type-imports.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-type-imports.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-typeof-runtype.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-typeof-runtype.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-unreachable-union-types.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-unreachable-union-types.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-vite-client.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-vite-client.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/pure-functions.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/pure-functions.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/strong-typed-routes.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/strong-typed-routes.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/type-formats-imports.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/type-formats-imports.d.ts.map +1 -0
- package/build/eslint/cjs/src/pureFns/purityRules.d.ts +1 -0
- package/build/eslint/cjs/src/pureFns/purityRules.d.ts.map +1 -0
- package/build/eslint/esm/index.js.map +1 -1
- package/build/eslint/esm/src/eslint/index.d.ts +1 -0
- package/build/eslint/esm/src/eslint/index.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/enforce-type-imports.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/enforce-type-imports.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/formatTypeNames.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/formatTypeNames.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/no-mixed-union-properties.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/no-mixed-union-properties.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/no-type-imports.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/no-type-imports.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/no-typeof-runtype.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/no-typeof-runtype.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/no-unreachable-union-types.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/no-unreachable-union-types.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/no-vite-client.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/no-vite-client.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/pure-functions.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/pure-functions.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/strong-typed-routes.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/strong-typed-routes.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/type-formats-imports.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/type-formats-imports.d.ts.map +1 -0
- package/build/eslint/esm/src/pureFns/purityRules.d.ts +1 -0
- package/build/eslint/esm/src/pureFns/purityRules.d.ts.map +1 -0
- package/build/vite-plugin/cjs/index.cjs +0 -1
- package/build/vite-plugin/cjs/index.cjs.map +1 -1
- package/build/vite-plugin/cjs/src/pureFns/purityRules.d.ts +1 -0
- package/build/vite-plugin/cjs/src/pureFns/purityRules.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/aotCacheGenerator.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/aotCacheGenerator.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/aotDiskCache.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/aotDiskCache.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/cjsPackageJsonPlugin.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/cjsPackageJsonPlugin.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/constants.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/constants.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/extractPureFn.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/extractPureFn.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/index.cjs +0 -1
- package/build/vite-plugin/cjs/src/vite-plugin/index.cjs.map +1 -1
- package/build/vite-plugin/cjs/src/vite-plugin/index.d.ts +2 -1
- package/build/vite-plugin/cjs/src/vite-plugin/index.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/mionVitePlugin.cjs.map +1 -1
- package/build/vite-plugin/cjs/src/vite-plugin/mionVitePlugin.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/mionVitePlugin.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/resolveModule.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/resolveModule.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/transformers.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/transformers.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/types.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/types.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/virtualModule.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/virtualModule.d.ts.map +1 -0
- package/build/vite-plugin/esm/index.js +2 -3
- package/build/vite-plugin/esm/src/pureFns/purityRules.d.ts +1 -0
- package/build/vite-plugin/esm/src/pureFns/purityRules.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/aotCacheGenerator.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/aotCacheGenerator.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/aotDiskCache.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/aotDiskCache.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/cjsPackageJsonPlugin.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/cjsPackageJsonPlugin.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/constants.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/constants.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/extractPureFn.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/extractPureFn.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/index.d.ts +2 -1
- package/build/vite-plugin/esm/src/vite-plugin/index.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/index.js +2 -3
- package/build/vite-plugin/esm/src/vite-plugin/mionVitePlugin.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/mionVitePlugin.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/mionVitePlugin.js.map +1 -1
- package/build/vite-plugin/esm/src/vite-plugin/resolveModule.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/resolveModule.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/transformers.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/transformers.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/types.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/types.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/virtualModule.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/virtualModule.d.ts.map +1 -0
- package/package.json +5 -3
- package/src/eslint/index.ts +53 -0
- package/src/eslint/rules/enforce-type-imports.ts +157 -0
- package/src/eslint/rules/formatTypeNames.ts +92 -0
- package/src/eslint/rules/no-mixed-union-properties.ts +276 -0
- package/src/eslint/rules/no-type-imports.ts +386 -0
- package/src/eslint/rules/no-typeof-runtype.ts +112 -0
- package/src/eslint/rules/no-unreachable-union-types.ts +357 -0
- package/src/eslint/rules/no-vite-client.ts +101 -0
- package/src/eslint/rules/pure-functions.ts +523 -0
- package/src/eslint/rules/strong-typed-routes.ts +537 -0
- package/src/eslint/rules/type-formats-imports.ts +73 -0
- package/src/pureFns/purityRules.ts +76 -0
- package/src/vite-plugin/README.md +399 -0
- package/src/vite-plugin/aotCacheGenerator.ts +397 -0
- package/src/vite-plugin/aotDiskCache.ts +193 -0
- package/src/vite-plugin/cjsPackageJsonPlugin.ts +21 -0
- package/src/vite-plugin/constants.ts +50 -0
- package/src/vite-plugin/extractPureFn.ts +845 -0
- package/src/vite-plugin/index.ts +27 -0
- package/src/vite-plugin/mionVitePlugin.ts +738 -0
- package/src/vite-plugin/resolveModule.ts +50 -0
- package/src/vite-plugin/transformers.ts +209 -0
- package/src/vite-plugin/types.ts +182 -0
- 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;
|