@orion.ui/orion-linter 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/configs/eslint.config.mjs +256 -0
- package/dist/configs/stylelint.config.mjs +78 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +75 -0
- package/dist/rules/async-suffix.js +121 -0
- package/dist/rules/class-name-match-filename.js +34 -0
- package/dist/rules/default-props-are-static-readonly.js +48 -0
- package/dist/rules/events-are-in-camel-case.js +140 -0
- package/dist/rules/force-dynamic-vue-imports-in-router.js +80 -0
- package/dist/rules/force-dynamic-vue-imports-in-services.js +160 -0
- package/dist/rules/get-set-adjacent.js +154 -0
- package/dist/rules/get-set-one-liner.js +156 -0
- package/dist/rules/no-api-in-entity.js +32 -0
- package/dist/rules/no-api-in-setup.js +31 -0
- package/dist/rules/no-entity-in-service.js +31 -0
- package/dist/rules/no-export-type-in-ts.js +36 -0
- package/dist/rules/popables-are-readonly.js +52 -0
- package/dist/rules/private-property-if-only-in-template.js +192 -0
- package/dist/rules/state-are-private-readonly.js +89 -0
- package/dist/rules/template-refs-are-readonly.js +52 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.js +17 -0
- package/dist/utils.d.ts +29 -0
- package/dist/utils.js +66 -0
- package/package.json +66 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
type: 'suggestion',
|
|
3
|
+
meta: {
|
|
4
|
+
docs: {
|
|
5
|
+
description: `enforce async function naming ends with 'Async'`,
|
|
6
|
+
category: `Possible Errors`,
|
|
7
|
+
recommended: true,
|
|
8
|
+
url: '',
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
create: function (context) {
|
|
13
|
+
const MISSING_ASYNC = `Names should end with 'Async' for async functions. Rename '{{name}}' to '{{name}}Async'`;
|
|
14
|
+
const EXTRA_ASYNC = `Non async names should not end with 'Async'. Rename '{{name}}Async' to '{{name}}'`;
|
|
15
|
+
|
|
16
|
+
const endsWithAsync = name => name.endsWith('Async');
|
|
17
|
+
|
|
18
|
+
const isReturningPromise = (node) => {
|
|
19
|
+
return node.value?.body?.body?.[0]?.argument?.callee?.name === 'Promise';
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check a node is valid
|
|
24
|
+
* @param {Object} node The root node that is being checked
|
|
25
|
+
* @param {Object} identifier The identifier of the root node
|
|
26
|
+
* @param {Boolean} isAsync Whether the node is marked as async
|
|
27
|
+
*/
|
|
28
|
+
const check = (node, identifier, isAsync) => {
|
|
29
|
+
// Unknown case that we don't handle
|
|
30
|
+
if (!identifier || !identifier.name) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const nameEndsWithAsync = endsWithAsync(identifier.name);
|
|
35
|
+
|
|
36
|
+
if (isAsync && !nameEndsWithAsync) {
|
|
37
|
+
context.report({
|
|
38
|
+
node: identifier,
|
|
39
|
+
message: MISSING_ASYNC,
|
|
40
|
+
data: { name: identifier.name },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!isAsync && nameEndsWithAsync) {
|
|
45
|
+
const lastIndex = identifier.name.lastIndexOf('Async');
|
|
46
|
+
const noneAsyncName = identifier.name.substring(0, lastIndex);
|
|
47
|
+
|
|
48
|
+
context.report({
|
|
49
|
+
node: identifier,
|
|
50
|
+
message: EXTRA_ASYNC,
|
|
51
|
+
data: { name: noneAsyncName },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const VariableDeclarator = (node) => {
|
|
57
|
+
const init = node.init;
|
|
58
|
+
const identifier = node.id;
|
|
59
|
+
|
|
60
|
+
if (init && identifier) {
|
|
61
|
+
check(node, identifier, init.async);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const MethodDefinition = (node) => {
|
|
66
|
+
const identifier = node.key;
|
|
67
|
+
const functionExpression = node.value;
|
|
68
|
+
|
|
69
|
+
if (identifier && functionExpression) {
|
|
70
|
+
check(
|
|
71
|
+
node,
|
|
72
|
+
identifier,
|
|
73
|
+
functionExpression.async || isReturningPromise(node),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const FunctionDeclaration = (node) => {
|
|
79
|
+
const identifier = node.id;
|
|
80
|
+
|
|
81
|
+
if (identifier) {
|
|
82
|
+
check(node, identifier, node.async || isReturningPromise(node));
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const AssignmentExpression = (node) => {
|
|
87
|
+
const identifier = node.left;
|
|
88
|
+
const functionExpression = node.right;
|
|
89
|
+
if (
|
|
90
|
+
identifier
|
|
91
|
+
&& functionExpression
|
|
92
|
+
&& functionExpression.async !== undefined
|
|
93
|
+
) {
|
|
94
|
+
check(node, identifier, functionExpression.async);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const Property = (node) => {
|
|
99
|
+
// Only care for Methods
|
|
100
|
+
const identifier = node.key;
|
|
101
|
+
const functionExpression = node.value;
|
|
102
|
+
|
|
103
|
+
// Ignore shorthand node definitions
|
|
104
|
+
if (node.shorthand) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (identifier && functionExpression) {
|
|
109
|
+
check(node, identifier, functionExpression.async);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
VariableDeclarator,
|
|
115
|
+
FunctionDeclaration,
|
|
116
|
+
MethodDefinition,
|
|
117
|
+
Property,
|
|
118
|
+
AssignmentExpression,
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { sep } from 'node:path';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem',
|
|
6
|
+
docs: { description: `check that entity name matches its file name` },
|
|
7
|
+
fixable: 'code',
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
create: function (context) {
|
|
11
|
+
return {
|
|
12
|
+
ClassDeclaration(node) {
|
|
13
|
+
const fileName = context.getPhysicalFilename().split(sep).reverse()[0].replace(/\.ts$/, '');
|
|
14
|
+
const fileShouldBeChecked = /(Entity|Service|Api([A-Z]+\w*)?)$/.test(fileName);
|
|
15
|
+
|
|
16
|
+
if (!fileShouldBeChecked) return;
|
|
17
|
+
|
|
18
|
+
const className = node.id.name;
|
|
19
|
+
|
|
20
|
+
if (fileName === className) return;
|
|
21
|
+
|
|
22
|
+
context.report({
|
|
23
|
+
node,
|
|
24
|
+
message: `Oops !
|
|
25
|
+
The class name ${className} do not correspond with its file name ${fileName}.
|
|
26
|
+
Either rename the file or the class`,
|
|
27
|
+
fix: (fixer) => {
|
|
28
|
+
return fixer.replaceTextRange(node.id.range, fileName);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function getClassProperties(classNode) {
|
|
2
|
+
return classNode.body.body.filter(x => x.type === 'PropertyDefinition');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'problem',
|
|
8
|
+
docs: { description: `check that default props in Setup are static and readonly` },
|
|
9
|
+
fixable: 'code',
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
create: function (context) {
|
|
13
|
+
return {
|
|
14
|
+
ClassDeclaration(node) {
|
|
15
|
+
if (/^Base[A-Z]/.test(node.id.name)) return;
|
|
16
|
+
|
|
17
|
+
const propsDeclaration = getClassProperties(node).filter(propertyNode => propertyNode.key.name === 'defaultProps')[0];
|
|
18
|
+
|
|
19
|
+
if (!propsDeclaration) return;
|
|
20
|
+
if (propsDeclaration.static && propsDeclaration.readonly) return;
|
|
21
|
+
|
|
22
|
+
const messages = [];
|
|
23
|
+
const fixers = [];
|
|
24
|
+
|
|
25
|
+
if (!propsDeclaration.static) {
|
|
26
|
+
messages.push(`\`defaultProps\` declaration should be \`static\`.`);
|
|
27
|
+
fixers.push('static');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!propsDeclaration.readonly) {
|
|
31
|
+
messages.push(`\`defaultProps\` declaration should be \`readonly\`.`);
|
|
32
|
+
fixers.push('readonly');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
context.report({
|
|
36
|
+
node: propsDeclaration,
|
|
37
|
+
message: `Oops !
|
|
38
|
+
${messages.join('\n')}`,
|
|
39
|
+
fix: (fixer) => {
|
|
40
|
+
return propsDeclaration.readonly
|
|
41
|
+
? fixer.insertTextBefore(propsDeclaration, fixers.join(' ') + ' ')
|
|
42
|
+
: fixer.insertTextBeforeRange(propsDeclaration.key.range, fixers.join(' ') + ' ');
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: { description: 'Event name must be in camelCase' },
|
|
5
|
+
fixable: 'code',
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
create: function (context) {
|
|
9
|
+
return {
|
|
10
|
+
Program(node) {
|
|
11
|
+
const sourceCode = context.getSourceCode();
|
|
12
|
+
|
|
13
|
+
const isInCamelCase = (str) => {
|
|
14
|
+
if (str.includes(':')) {
|
|
15
|
+
const strParts = str.split(':');
|
|
16
|
+
return strParts.every(part => isInCamelCase(part));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Allow full template variables
|
|
20
|
+
if (str.match(/^\$\{.*\}$/)) return true;
|
|
21
|
+
|
|
22
|
+
// Allow Setup
|
|
23
|
+
if (/^[A-Z][a-zA-Z0-9]*Setup(Service)?$/.test(str)) return true;
|
|
24
|
+
|
|
25
|
+
// Handle template variables within the string
|
|
26
|
+
if (str.match(/\$\{.*\}/)) {
|
|
27
|
+
// Split by template variables and check each part
|
|
28
|
+
const parts = str.split(/\$\{[^}]+\}/g).filter(p => p !== '');
|
|
29
|
+
if (parts.length === 0) return true;
|
|
30
|
+
// First part should start with lowercase, others can start with uppercase
|
|
31
|
+
return parts.every((part, index) => {
|
|
32
|
+
if (index === 0 && str.startsWith(part)) {
|
|
33
|
+
// First part at the beginning of string
|
|
34
|
+
return /^[a-z][a-z0-9]*([A-Z][a-z0-9]*)*$/.test(part);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// Part after a template variable can start with uppercase
|
|
38
|
+
return /^[A-Z]?[a-z0-9]*([A-Z][a-z0-9]*)*$/.test(part);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return /^[a-z][a-z0-9]*([A-Z][a-z0-9]*)*$/.test(str);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const kebabToCamelCaseStrict = (str) => {
|
|
47
|
+
if (str.includes(':')) {
|
|
48
|
+
const strParts = str.split(':');
|
|
49
|
+
return strParts
|
|
50
|
+
.map((part) => {
|
|
51
|
+
return isInCamelCase(part)
|
|
52
|
+
? part
|
|
53
|
+
: kebabToCamelCaseStrict(part);
|
|
54
|
+
})
|
|
55
|
+
.join(':');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Handle template variables
|
|
59
|
+
const templateVarRegex = /\$\{[^}]+\}/g;
|
|
60
|
+
const parts = str.split(templateVarRegex);
|
|
61
|
+
const templateVars = str.match(templateVarRegex) || [];
|
|
62
|
+
|
|
63
|
+
if (templateVars.length === 0) {
|
|
64
|
+
// No template variables, simple conversion
|
|
65
|
+
return str
|
|
66
|
+
.toLowerCase()
|
|
67
|
+
.replace(/-([a-z0-9])/g, (_, letter) => letter.toUpperCase());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Convert each part
|
|
71
|
+
const convertedParts = parts.map((part, index) => {
|
|
72
|
+
// Remove leading and trailing dashes
|
|
73
|
+
part = part.replace(/^-+/, '').replace(/-+$/, '');
|
|
74
|
+
|
|
75
|
+
if (!part) return '';
|
|
76
|
+
|
|
77
|
+
// Convert to camelCase
|
|
78
|
+
const camelCase = part.toLowerCase().replace(/-([a-z0-9])/g, (_, letter) => letter.toUpperCase());
|
|
79
|
+
|
|
80
|
+
// If this part comes after a template variable (index > 0 and previous part exists or is at position 0 in original string)
|
|
81
|
+
// then capitalize first letter
|
|
82
|
+
if (index > 0) {
|
|
83
|
+
return camelCase.charAt(0).toUpperCase() + camelCase.substring(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return camelCase;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Reassemble with template variables
|
|
90
|
+
let result = '';
|
|
91
|
+
for (let i = 0; i < convertedParts.length; i++) {
|
|
92
|
+
result += convertedParts[i];
|
|
93
|
+
if (i < templateVars.length) {
|
|
94
|
+
result += templateVars[i];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return result;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const busEventsRegex = /(registerBusEvent|((Bus\.|useBusService\(\)\.)on|once|emit|off)|((modal|aside|notif)\??\.trigger))\(['`"](?<event>[^'`"]+)['`"]/gm;
|
|
102
|
+
const emitEventsRegex = /Emits\s*=\s*\{\s*\(\w+:\s*['`"](?<event>[^'`"]+)['`"]/gm;
|
|
103
|
+
const matches = Array.from(sourceCode.getText().matchAll(busEventsRegex));
|
|
104
|
+
matches.push(...Array.from(sourceCode.getText().matchAll(emitEventsRegex)));
|
|
105
|
+
|
|
106
|
+
if (!matches.length) return;
|
|
107
|
+
|
|
108
|
+
matches.forEach((match) => {
|
|
109
|
+
const event = match.groups?.event;
|
|
110
|
+
if (!event) return;
|
|
111
|
+
|
|
112
|
+
if (!isInCamelCase(event)) {
|
|
113
|
+
const index = match.index + match[0].indexOf(event);
|
|
114
|
+
const loc = context.getSourceCode().getLocFromIndex(index);
|
|
115
|
+
|
|
116
|
+
context.report({
|
|
117
|
+
message: `Oops! Event "${event}" should be in camelCase (":" allowed).`,
|
|
118
|
+
loc: {
|
|
119
|
+
start: loc,
|
|
120
|
+
end: {
|
|
121
|
+
line: loc.line,
|
|
122
|
+
column: loc.column + event.length,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
fix: (fixer) => {
|
|
126
|
+
const start = match.index + match[0].indexOf(event);
|
|
127
|
+
const end = start + event.length;
|
|
128
|
+
|
|
129
|
+
return fixer.replaceTextRange(
|
|
130
|
+
[start, end],
|
|
131
|
+
kebabToCamelCaseStrict(event),
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: { description: 'Enforce dynamic imports for Vue components in Router files and disallow static imports.' },
|
|
5
|
+
fixable: 'code',
|
|
6
|
+
},
|
|
7
|
+
create(context) {
|
|
8
|
+
const ROUTER_FILE_REGEX = /router\/.*\.ts$/;
|
|
9
|
+
|
|
10
|
+
// Check if the current file is a router file
|
|
11
|
+
if (!ROUTER_FILE_REGEX.test(context.getFilename())) {
|
|
12
|
+
return {}; // Not a Router file, so don't apply this rule
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const importedComponents = []; // Define importedComponents inside create function
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
// Check Property for 'component' key in route objects
|
|
19
|
+
Property(node) {
|
|
20
|
+
if (node.key.type === 'Identifier' && node.key.name === 'component') {
|
|
21
|
+
const componentValue = node.value;
|
|
22
|
+
const sourceCode = context.getSourceCode();
|
|
23
|
+
|
|
24
|
+
const isCorrectDynamicImport = componentValue.type === 'ArrowFunctionExpression'
|
|
25
|
+
&& componentValue.body.type === 'ImportExpression';
|
|
26
|
+
|
|
27
|
+
if (isCorrectDynamicImport) {
|
|
28
|
+
return; // This is valid, do nothing.
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Attempt to extract the import path if it's a static import or a different dynamic import
|
|
32
|
+
let importPath = '';
|
|
33
|
+
if (componentValue.type === 'Identifier') {
|
|
34
|
+
const componentIsImported = importedComponents.find(comp => comp.name === componentValue.name && comp.source.endsWith('.vue'));
|
|
35
|
+
if (componentIsImported) {
|
|
36
|
+
importPath = componentIsImported.source;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (componentValue.type === 'ImportExpression') { // Direct ImportExpression as component value
|
|
40
|
+
importPath = componentValue.source.value;
|
|
41
|
+
}
|
|
42
|
+
else if (componentValue.type === 'CallExpression' && componentValue.callee.type === 'ImportExpression') { // This case is for `import(...)` directly, not wrapped in arrow function
|
|
43
|
+
importPath = componentValue.arguments[0].value;
|
|
44
|
+
}
|
|
45
|
+
else if (componentValue.type === 'MemberExpression' && componentValue.object.type === 'AwaitExpression' && componentValue.object.argument.type === 'ImportExpression') {
|
|
46
|
+
importPath = componentValue.object.argument.source.value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If it's not the correct dynamic import, report it.
|
|
50
|
+
context.report({
|
|
51
|
+
node: componentValue,
|
|
52
|
+
message: 'Vue components in router files must use dynamic imports in the format `() => import(\'path/to/Component.vue\')`.',
|
|
53
|
+
fix(fixer) {
|
|
54
|
+
if (importPath) {
|
|
55
|
+
return fixer.replaceText(componentValue, `() => import('${importPath}')`);
|
|
56
|
+
}
|
|
57
|
+
return null; // Cannot fix automatically if path is not found
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
ImportDeclaration(node) {
|
|
63
|
+
importedComponents.push(...node.specifiers.map(s => ({
|
|
64
|
+
name: s.local.name,
|
|
65
|
+
source: node.source.value,
|
|
66
|
+
})));
|
|
67
|
+
|
|
68
|
+
if (node.source.value.endsWith('.vue')) {
|
|
69
|
+
context.report({
|
|
70
|
+
node,
|
|
71
|
+
message: 'Static import of Vue components is not allowed in Router files. Use dynamic import instead.',
|
|
72
|
+
fix(fixer) {
|
|
73
|
+
return fixer.remove(node);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: { description: 'Enforce dynamic imports for Vue components in Service and Setup files and disallow static imports.' },
|
|
5
|
+
fixable: 'code',
|
|
6
|
+
},
|
|
7
|
+
create(context) {
|
|
8
|
+
const SERVICE_OR_SETUP_FILE_REGEX = /(Service|Setup)\.ts$/;
|
|
9
|
+
|
|
10
|
+
// Check if the current file is a Service.ts or Setup.ts file
|
|
11
|
+
if (!SERVICE_OR_SETUP_FILE_REGEX.test(context.getFilename())) {
|
|
12
|
+
return {}; // Not a Service or Setup file, so don't apply this rule
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const importedComponents = [];
|
|
16
|
+
let hasDefineAsyncComponent = false;
|
|
17
|
+
let vueImportNode = null;
|
|
18
|
+
|
|
19
|
+
// Helper function to ensure defineAsyncComponent is imported
|
|
20
|
+
const ensureDefineAsyncComponentImport = (fixer) => {
|
|
21
|
+
const sourceCode = context.getSourceCode();
|
|
22
|
+
|
|
23
|
+
if (hasDefineAsyncComponent) {
|
|
24
|
+
return null; // Already imported
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (vueImportNode) {
|
|
28
|
+
// Add defineAsyncComponent to existing vue import
|
|
29
|
+
const importSpecifiers = vueImportNode.specifiers;
|
|
30
|
+
if (importSpecifiers.length > 0) {
|
|
31
|
+
const lastSpecifier = importSpecifiers[importSpecifiers.length - 1];
|
|
32
|
+
return fixer.insertTextAfter(lastSpecifier, ', defineAsyncComponent');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Add new import for defineAsyncComponent from vue at the beginning
|
|
37
|
+
const firstNode = sourceCode.ast.body[0];
|
|
38
|
+
if (firstNode) {
|
|
39
|
+
return fixer.insertTextBefore(firstNode, 'import { defineAsyncComponent } from \'vue\';\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Helper function to check and fix Nested property in useModal/useAside
|
|
46
|
+
const checkNestedProperty = (node, functionName) => {
|
|
47
|
+
const options = node.arguments[0];
|
|
48
|
+
|
|
49
|
+
if (options.type === 'ObjectExpression') {
|
|
50
|
+
const nestedProperty = options.properties.find(
|
|
51
|
+
prop => prop.type === 'Property' && prop.key.type === 'Identifier' && prop.key.name === 'Nested',
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (nestedProperty) {
|
|
55
|
+
const nestedValue = nestedProperty.value;
|
|
56
|
+
|
|
57
|
+
const componentIsImported = importedComponents.find(
|
|
58
|
+
comp => nestedValue.type === 'Identifier' && comp.name === nestedValue.name && comp.source.endsWith('.vue'),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Check if it's already using defineAsyncComponent
|
|
62
|
+
const isDefineAsyncComponent = nestedValue.type === 'CallExpression'
|
|
63
|
+
&& nestedValue.callee.type === 'Identifier'
|
|
64
|
+
&& nestedValue.callee.name === 'defineAsyncComponent';
|
|
65
|
+
|
|
66
|
+
if (isDefineAsyncComponent) {
|
|
67
|
+
return; // Already correct, do nothing
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if it's using the old pattern: (await import('...')).default
|
|
71
|
+
const isOldDynamicImportPattern = nestedValue.type === 'MemberExpression'
|
|
72
|
+
&& nestedValue.property.type === 'Identifier'
|
|
73
|
+
&& nestedValue.property.name === 'default'
|
|
74
|
+
&& nestedValue.object.type === 'AwaitExpression'
|
|
75
|
+
&& nestedValue.object.argument.type === 'ImportExpression';
|
|
76
|
+
|
|
77
|
+
if (isOldDynamicImportPattern) {
|
|
78
|
+
const importPath = nestedValue.object.argument.source.value;
|
|
79
|
+
context.report({
|
|
80
|
+
node: nestedValue,
|
|
81
|
+
message: `The \`Nested\` property in \`${functionName}\` should use \`defineAsyncComponent(() => import('...'))\` instead of \`(await import('...')).default\`.`,
|
|
82
|
+
fix(fixer) {
|
|
83
|
+
const fixes = [
|
|
84
|
+
fixer.replaceText(nestedValue, `defineAsyncComponent(() => import('${importPath}'))`),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const importFix = ensureDefineAsyncComponentImport(fixer);
|
|
88
|
+
if (importFix) {
|
|
89
|
+
fixes.push(importFix);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return fixes;
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (componentIsImported) {
|
|
99
|
+
// Component is statically imported, suggest dynamic import with defineAsyncComponent
|
|
100
|
+
context.report({
|
|
101
|
+
node: nestedValue,
|
|
102
|
+
message: `The \`Nested\` property in \`${functionName}\` must use \`defineAsyncComponent\` with dynamic import instead of a static import.`,
|
|
103
|
+
fix(fixer) {
|
|
104
|
+
const componentImportPath = componentIsImported.source;
|
|
105
|
+
const fixes = [
|
|
106
|
+
fixer.replaceText(nestedValue, `defineAsyncComponent(() => import('${componentImportPath}'))`),
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const importFix = ensureDefineAsyncComponentImport(fixer);
|
|
110
|
+
if (importFix) {
|
|
111
|
+
fixes.push(importFix);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return fixes;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
|
|
124
|
+
// Check useModal and useAside calls
|
|
125
|
+
CallExpression(node) {
|
|
126
|
+
if (node.callee.type === 'Identifier' && (node.callee.name === 'useModal' || node.callee.name === 'useAside') && node.arguments.length > 0) {
|
|
127
|
+
checkNestedProperty(node, node.callee.name);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
ImportDeclaration(node) {
|
|
132
|
+
// Track imported components
|
|
133
|
+
importedComponents.push(...node.specifiers.map(s => ({
|
|
134
|
+
name: s.local.name,
|
|
135
|
+
source: node.source.value,
|
|
136
|
+
})));
|
|
137
|
+
|
|
138
|
+
// Track vue imports to check for defineAsyncComponent
|
|
139
|
+
if (node.source.value === 'vue') {
|
|
140
|
+
vueImportNode = node;
|
|
141
|
+
hasDefineAsyncComponent = node.specifiers.some(
|
|
142
|
+
spec => spec.type === 'ImportSpecifier' && spec.imported.name === 'defineAsyncComponent',
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Report static imports of .vue files
|
|
147
|
+
if (node.source.value.endsWith('.vue')) {
|
|
148
|
+
context.report({
|
|
149
|
+
node,
|
|
150
|
+
message: 'Static import of Vue components is not allowed in Service files. Use \`defineAsyncComponent\` with dynamic import instead.',
|
|
151
|
+
fix(fixer) {
|
|
152
|
+
return fixer.remove(node);
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
};
|