@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.
@@ -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
+ };