Skip to content

Commit f8d3ecc

Browse files
committed
adds eslint rules from cms/forms/deploy/workflow, run and fix.
1 parent 8cbab9a commit f8d3ecc

19 files changed

+419
-31
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
{
2+
"ignorePatterns": ["vite.*.ts", "src/generated/**", "devops/**"],
3+
"root": true,
4+
"plugins": ["eslint-plugin-local-rules"],
5+
"parserOptions": {
6+
"ecmaVersion": "latest"
7+
},
8+
"env": {
9+
"es6": true
10+
},
11+
"overrides": [
12+
{
13+
"files": ["**/*.ts"],
14+
"extends": [
15+
"eslint:recommended",
16+
"plugin:import/recommended",
17+
"plugin:import/typescript",
18+
"plugin:@typescript-eslint/eslint-recommended",
19+
"plugin:@typescript-eslint/recommended",
20+
"plugin:wc/recommended",
21+
"plugin:lit/recommended",
22+
"plugin:lit-a11y/recommended",
23+
"prettier"
24+
],
25+
"parser": "@typescript-eslint/parser",
26+
"parserOptions": {
27+
"project": "./tsconfig.json",
28+
"tsconfigRootDir": "./",
29+
"ecmaVersion": "latest",
30+
"sourceType": "module"
31+
},
32+
"env": {
33+
"browser": true,
34+
"es2021": true
35+
},
36+
"rules": {
37+
"no-var": "error",
38+
"import/no-unresolved": "off",
39+
"import/order": "warn",
40+
"import/no-duplicates": ["warn", {"prefer-inline": true}],
41+
"local-rules/bad-type-import": "error",
42+
"local-rules/no-direct-api-import": "warn",
43+
"local-rules/prefer-import-aliases": "error",
44+
"local-rules/enforce-element-suffix-on-element-class-name": "error",
45+
"local-rules/prefer-static-styles-last": "warn",
46+
"local-rules/ensure-relative-import-use-js-extension": "error",
47+
"@typescript-eslint/no-non-null-assertion": "off",
48+
"@typescript-eslint/no-explicit-any": "warn",
49+
"@typescript-eslint/no-unused-vars": "warn",
50+
"@typescript-eslint/consistent-type-exports": "error",
51+
"@typescript-eslint/consistent-type-imports": "error"
52+
},
53+
"settings": {
54+
"import/parsers": {
55+
"@typescript-eslint/parser": [".ts"]
56+
},
57+
"import/resolver": {
58+
"typescript": {
59+
"alwaysTryTypes": true,
60+
"project": "./tsconfig.json"
61+
}
62+
}
63+
}
64+
},
65+
{
66+
"files": ["**/*.js"],
67+
"extends": ["eslint:recommended", "plugin:import/recommended", "prettier"],
68+
"env": {
69+
"node": true,
70+
"browser": true,
71+
"es6": true
72+
},
73+
"parserOptions": {
74+
"sourceType": "module",
75+
"ecmaVersion": "latest"
76+
},
77+
"settings": {
78+
"import/resolver": {
79+
"node": {
80+
"extensions": [".js"],
81+
"moduleDirectory": ["node_modules"]
82+
}
83+
}
84+
}
85+
}
86+
]
87+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'problem',
5+
docs: {
6+
description: 'Ensures the use of the `import type` operator from the `src/core/models/index.ts` file.',
7+
category: 'Best Practices',
8+
recommended: true,
9+
},
10+
fixable: 'code',
11+
schema: [],
12+
},
13+
create: function (context) {
14+
return {
15+
ImportDeclaration: function (node) {
16+
if (
17+
node.source.parent.importKind !== 'type' &&
18+
(node.source.value.endsWith('/models') || node.source.value === 'router-slot/model')
19+
) {
20+
const sourceCode = context.getSourceCode();
21+
const nodeSource = sourceCode.getText(node);
22+
context.report({
23+
node,
24+
message: 'Use `import type` instead of `import`.',
25+
fix: (fixer) => fixer.replaceText(node, nodeSource.replace('import', 'import type')),
26+
});
27+
}
28+
},
29+
};
30+
},
31+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'suggestion',
5+
docs: {
6+
description: 'Enforce Element class name to end with "Element".',
7+
category: 'Naming',
8+
recommended: true,
9+
},
10+
schema: [],
11+
},
12+
create: function (context) {
13+
return {
14+
ClassDeclaration(node) {
15+
// check if the class extends HTMLElement, LitElement, or UmbLitElement
16+
const isExtendingElement =
17+
node.superClass && ['HTMLElement', 'LitElement', 'UmbLitElement'].includes(node.superClass.name);
18+
// check if the class name ends with 'Element'
19+
const isClassNameValid = node.id.name.endsWith('Element');
20+
21+
if (isExtendingElement && !isClassNameValid) {
22+
context.report({
23+
node,
24+
message: "Element class name should end with 'Element'.",
25+
// There us no fixer on purpose because it's not safe to rename the class. We want to do that trough the refactoring tool.
26+
});
27+
}
28+
},
29+
};
30+
},
31+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/** @type {import('eslint').Rule.RuleModule}*/
2+
module.exports = {
3+
meta: {
4+
type: 'problem',
5+
docs: {
6+
description: 'Ensures relative imports use the ".js" file extension.',
7+
category: 'Best Practices',
8+
recommended: true,
9+
},
10+
fixable: 'code',
11+
schema: [],
12+
},
13+
create: (context) => {
14+
function correctImport(value) {
15+
if (value === '.') {
16+
return './index.js';
17+
}
18+
19+
if (
20+
value &&
21+
value.startsWith('.') &&
22+
!value.endsWith('.js') &&
23+
!value.endsWith('.css') &&
24+
!value.endsWith('.json') &&
25+
!value.endsWith('.svg') &&
26+
!value.endsWith('.jpg') &&
27+
!value.endsWith('.png')
28+
) {
29+
return (value.endsWith('/') ? value + 'index' : value) + '.js';
30+
}
31+
32+
return null;
33+
}
34+
35+
return {
36+
ImportDeclaration: (node) => {
37+
const { source } = node;
38+
const { value } = source;
39+
40+
const fixedValue = correctImport(value);
41+
if (fixedValue) {
42+
context.report({
43+
node,
44+
message: 'Relative imports should use the ".js" file extension.',
45+
fix: (fixer) => fixer.replaceText(source, `'${fixedValue}'`),
46+
});
47+
}
48+
},
49+
ImportExpression: (node) => {
50+
const { source } = node;
51+
const { value } = source;
52+
53+
const fixedSource = correctImport(value);
54+
if (fixedSource) {
55+
context.report({
56+
node: source,
57+
message: 'Relative imports should use the ".js" file extension.',
58+
fix: (fixer) => fixer.replaceText(source, `'${fixedSource}'`),
59+
});
60+
}
61+
},
62+
ExportAllDeclaration: (node) => {
63+
const { source } = node;
64+
const { value } = source;
65+
66+
const fixedSource = correctImport(value);
67+
if (fixedSource) {
68+
context.report({
69+
node: source,
70+
message: 'Relative exports should use the ".js" file extension.',
71+
fix: (fixer) => fixer.replaceText(source, `'${fixedSource}'`),
72+
});
73+
}
74+
},
75+
ExportNamedDeclaration: (node) => {
76+
const { source } = node;
77+
if (!source) return;
78+
const { value } = source;
79+
80+
const fixedSource = correctImport(value);
81+
if (fixedSource) {
82+
context.report({
83+
node: source,
84+
message: 'Relative exports should use the ".js" file extension.',
85+
fix: (fixer) => fixer.replaceText(source, `'${fixedSource}'`),
86+
});
87+
}
88+
},
89+
};
90+
},
91+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
module.exports = {
2+
meta: {
3+
docs: {
4+
description:
5+
'Ensures that any API resources from the `@umbraco-cms/backoffice/backend-api` module are not used directly. Instead you should use the `tryExecuteAndNotify` function from the `@umbraco-cms/backoffice/resources` module.',
6+
category: 'Best Practices',
7+
recommended: true,
8+
},
9+
fixable: 'code',
10+
schema: [],
11+
},
12+
create: function (context) {
13+
return {
14+
// If methods called on *Resource classes are not already wrapped with `await tryExecuteAndNotify()`, then we should suggest to wrap them.
15+
CallExpression: function (node) {
16+
if (
17+
node.callee.type === 'MemberExpression' &&
18+
node.callee.object.type === 'Identifier' &&
19+
node.callee.object.name.endsWith('Resource') &&
20+
node.callee.property.type === 'Identifier' &&
21+
node.callee.property.name !== 'constructor'
22+
) {
23+
const hasTryExecuteAndNotify =
24+
node.parent &&
25+
node.parent.callee &&
26+
(node.parent.callee.name === 'tryExecute' || node.parent.callee.name === 'tryExecuteAndNotify');
27+
if (!hasTryExecuteAndNotify) {
28+
context.report({
29+
node,
30+
message: 'Wrap this call with `tryExecuteAndNotify()`. Make sure to `await` the result.',
31+
fix: (fixer) => [
32+
fixer.insertTextBefore(node, 'tryExecuteAndNotify(this, '),
33+
fixer.insertTextAfter(node, ')'),
34+
],
35+
});
36+
}
37+
}
38+
},
39+
};
40+
},
41+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'suggestion',
5+
docs: {
6+
description:
7+
'Ensures that the application does not rely on file system paths for imports. Instead, use import aliases or relative imports. This also solves a problem where GitHub fails on the test runner step.',
8+
category: 'Best Practices',
9+
recommended: true,
10+
},
11+
schema: [],
12+
},
13+
create: function (context) {
14+
return {
15+
ImportDeclaration: function (node) {
16+
if (node.source.value.startsWith('src/')) {
17+
context.report({
18+
node,
19+
message:
20+
'Prefer using import aliases or relative imports instead of absolute imports. Example: `import { MyComponent } from "src/components/MyComponent";` should be `import { MyComponent } from "@components/MyComponent";`',
21+
});
22+
}
23+
},
24+
};
25+
},
26+
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/** @type {import('eslint').Rule.RuleModule}*/
2+
module.exports = {
3+
meta: {
4+
type: 'suggestion',
5+
docs: {
6+
description:
7+
'Enforce the "styles" property with the static modifier to be the last property of a class that ends with "Element".',
8+
category: 'Best Practices',
9+
recommended: true,
10+
},
11+
fixable: 'code',
12+
schema: [],
13+
},
14+
create: function (context) {
15+
return {
16+
ClassDeclaration(node) {
17+
const className = node.id.name;
18+
if (className.endsWith('Element')) {
19+
const staticStylesProperty = node.body.body.find((bodyNode) => {
20+
return bodyNode.type === 'PropertyDefinition' && bodyNode.key.name === 'styles' && bodyNode.static;
21+
});
22+
if (staticStylesProperty) {
23+
const lastProperty = node.body.body[node.body.body.length - 1];
24+
if (lastProperty.key.name !== staticStylesProperty.key.name) {
25+
context.report({
26+
node: staticStylesProperty,
27+
message: 'The "styles" property should be the last property of a class declaration.',
28+
data: {
29+
className: className,
30+
},
31+
fix: function (fixer) {
32+
const sourceCode = context.getSourceCode();
33+
const staticStylesPropertyText = sourceCode.getText(staticStylesProperty);
34+
return [
35+
fixer.replaceTextRange(staticStylesProperty.range, ''),
36+
fixer.insertTextAfterRange(lastProperty.range, '\n \n ' + staticStylesPropertyText),
37+
];
38+
},
39+
});
40+
}
41+
}
42+
}
43+
},
44+
};
45+
},
46+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"use strict";
2+
3+
const badTypeImportRule = require("./devops/eslint/rules/bad-type-import.cjs");
4+
const enforceElementSuffixOnElementClassNameRule = require("./devops/eslint/rules/enforce-element-suffix-on-element-class-name.cjs");
5+
const ensureRelativeImportUseJsExtensionRule = require("./devops/eslint/rules/ensure-relative-import-use-js-extension.cjs");
6+
const noDirectApiImportRule = require("./devops/eslint/rules/no-direct-api-import.cjs");
7+
const preferImportAliasesRule = require("./devops/eslint/rules/prefer-import-aliases.cjs");
8+
const preferStaticStylesLastRule = require("./devops/eslint/rules/prefer-static-styles-last.cjs");
9+
10+
module.exports = {
11+
"bad-type-import": badTypeImportRule,
12+
"enforce-element-suffix-on-element-class-name":
13+
enforceElementSuffixOnElementClassNameRule,
14+
"ensure-relative-import-use-js-extension":
15+
ensureRelativeImportUseJsExtensionRule,
16+
"no-direct-api-import": noDirectApiImportRule,
17+
"prefer-import-aliases": preferImportAliasesRule,
18+
"prefer-static-styles-last": preferStaticStylesLastRule,
19+
};

0 commit comments

Comments
 (0)