390 lines
11 KiB
JavaScript
390 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
/*
|
|
* A eslint rule that ensures the use of the `import type` operator from the `src/core/models/index.ts` file.
|
|
*/
|
|
// eslint-disable-next-line no-undef
|
|
module.exports = {
|
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
'bad-type-import': {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Ensures the use of the `import type` operator from the `src/core/models/index.ts` file.',
|
|
category: 'Best Practices',
|
|
recommended: true,
|
|
},
|
|
fixable: 'code',
|
|
schema: [],
|
|
},
|
|
create: function (context) {
|
|
return {
|
|
ImportDeclaration: function (node) {
|
|
if (
|
|
node.source.parent.importKind !== 'type' &&
|
|
(node.source.value.endsWith('/models') || node.source.value === 'router-slot/model')
|
|
) {
|
|
const sourceCode = context.getSourceCode();
|
|
const nodeSource = sourceCode.getText(node);
|
|
context.report({
|
|
node,
|
|
message: 'Use `import type` instead of `import`.',
|
|
fix: (fixer) => fixer.replaceText(node, nodeSource.replace('import', 'import type')),
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
},
|
|
|
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
'no-direct-api-import': {
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: {
|
|
description:
|
|
'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.',
|
|
category: 'Best Practices',
|
|
recommended: true,
|
|
},
|
|
fixable: 'code',
|
|
schema: [],
|
|
},
|
|
create: function (context) {
|
|
return {
|
|
// If methods called on *Resource classes are not already wrapped with `await tryExecuteAndNotify()`, then we should suggest to wrap them.
|
|
CallExpression: function (node) {
|
|
if (
|
|
node.callee.type === 'MemberExpression' &&
|
|
node.callee.object.type === 'Identifier' &&
|
|
node.callee.object.name.endsWith('Resource') &&
|
|
node.callee.property.type === 'Identifier' &&
|
|
node.callee.property.name !== 'constructor'
|
|
) {
|
|
const hasTryExecuteAndNotify =
|
|
node.parent &&
|
|
node.parent.callee &&
|
|
(node.parent.callee.name === 'tryExecute' || node.parent.callee.name === 'tryExecuteAndNotify');
|
|
if (!hasTryExecuteAndNotify) {
|
|
context.report({
|
|
node,
|
|
message: 'Wrap this call with `tryExecuteAndNotify()`. Make sure to `await` the result.',
|
|
fix: (fixer) => [
|
|
fixer.insertTextBefore(node, 'tryExecuteAndNotify(this, '),
|
|
fixer.insertTextAfter(node, ')'),
|
|
],
|
|
});
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
},
|
|
|
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
'prefer-import-aliases': {
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: {
|
|
description:
|
|
'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.',
|
|
category: 'Best Practices',
|
|
recommended: true,
|
|
},
|
|
schema: [],
|
|
},
|
|
create: function (context) {
|
|
return {
|
|
ImportDeclaration: function (node) {
|
|
if (node.source.value.startsWith('src/')) {
|
|
context.report({
|
|
node,
|
|
message:
|
|
'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";`',
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
},
|
|
|
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
'enforce-element-suffix-on-element-class-name': {
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: {
|
|
description: 'Enforce Element class name to end with "Element".',
|
|
category: 'Naming',
|
|
recommended: true,
|
|
},
|
|
schema: [],
|
|
},
|
|
create: function (context) {
|
|
return {
|
|
ClassDeclaration(node) {
|
|
// check if the class extends HTMLElement, LitElement, or UmbLitElement
|
|
const isExtendingElement =
|
|
node.superClass && ['HTMLElement', 'LitElement', 'UmbLitElement'].includes(node.superClass.name);
|
|
// check if the class name ends with 'Element'
|
|
const isClassNameValid = node.id.name.endsWith('Element');
|
|
|
|
if (isExtendingElement && !isClassNameValid) {
|
|
context.report({
|
|
node,
|
|
message: "Element class name should end with 'Element'.",
|
|
// There us no fixer on purpose because it's not safe to rename the class. We want to do that trough the refactoring tool.
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
},
|
|
|
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
/*
|
|
'no-external-imports': {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description:
|
|
'Ensures that the application does not rely on imports from external packages. Instead, use the @umbraco-cms/backoffice libs.',
|
|
recommended: true,
|
|
},
|
|
fixable: 'code',
|
|
schema: [],
|
|
},
|
|
create: function (context) {
|
|
return {
|
|
ImportDeclaration: function (node) {
|
|
// Check for imports from "router-slot"
|
|
if (node.source.value.startsWith('router-slot')) {
|
|
context.report({
|
|
node,
|
|
message:
|
|
'Use the `@umbraco-cms/backoffice/router` package instead of importing directly from "router-slot" because we might change that dependency in the future.',
|
|
fix: (fixer) => {
|
|
return fixer.replaceTextRange(node.source.range, `'@umbraco-cms/backoffice/router'`);
|
|
},
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
},
|
|
*/
|
|
|
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
'umb-class-prefix': {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Ensure that all class declarations are prefixed with "Umb"',
|
|
category: 'Best Practices',
|
|
recommended: true,
|
|
},
|
|
schema: [],
|
|
},
|
|
create: function (context) {
|
|
function checkClassName(node) {
|
|
if (node.id && node.id.name && !node.id.name.startsWith('Umb')) {
|
|
context.report({
|
|
node: node.id,
|
|
message: 'Class declaration should be prefixed with "Umb"',
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
ClassDeclaration: checkClassName,
|
|
};
|
|
},
|
|
},
|
|
|
|
/** @type {import('eslint').Rule.RuleModule}*/
|
|
'prefer-static-styles-last': {
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: {
|
|
description:
|
|
'Enforce the "styles" property with the static modifier to be the last property of a class that ends with "Element".',
|
|
category: 'Best Practices',
|
|
recommended: true,
|
|
},
|
|
fixable: 'code',
|
|
schema: [],
|
|
},
|
|
create: function (context) {
|
|
return {
|
|
ClassDeclaration(node) {
|
|
const className = node.id.name;
|
|
if (className.endsWith('Element')) {
|
|
const staticStylesProperty = node.body.body.find((bodyNode) => {
|
|
return bodyNode.type === 'PropertyDefinition' && bodyNode.key.name === 'styles' && bodyNode.static;
|
|
});
|
|
if (staticStylesProperty) {
|
|
const lastProperty = node.body.body[node.body.body.length - 1];
|
|
if (lastProperty.key.name !== staticStylesProperty.key.name) {
|
|
context.report({
|
|
node: staticStylesProperty,
|
|
message: 'The "styles" property should be the last property of a class declaration.',
|
|
data: {
|
|
className: className,
|
|
},
|
|
fix: function (fixer) {
|
|
const sourceCode = context.getSourceCode();
|
|
const staticStylesPropertyText = sourceCode.getText(staticStylesProperty);
|
|
return [
|
|
fixer.replaceTextRange(staticStylesProperty.range, ''),
|
|
fixer.insertTextAfterRange(lastProperty.range, '\n \n ' + staticStylesPropertyText),
|
|
];
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
},
|
|
|
|
/** @type {import('eslint').Rule.RuleModule}*/
|
|
'ensure-relative-import-use-js-extension': {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Ensures relative imports use the ".js" file extension.',
|
|
category: 'Best Practices',
|
|
recommended: true,
|
|
},
|
|
fixable: 'code',
|
|
schema: [],
|
|
},
|
|
create: (context) => {
|
|
function correctImport(value) {
|
|
if (value === '.') {
|
|
return './index.js';
|
|
}
|
|
|
|
if (
|
|
value &&
|
|
value.startsWith('.') &&
|
|
!value.endsWith('.js') &&
|
|
!value.endsWith('.css') &&
|
|
!value.endsWith('.json') &&
|
|
!value.endsWith('.svg') &&
|
|
!value.endsWith('.jpg') &&
|
|
!value.endsWith('.png')
|
|
) {
|
|
return (value.endsWith('/') ? value + 'index' : value) + '.js';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
ImportDeclaration: (node) => {
|
|
const { source } = node;
|
|
const { value } = source;
|
|
|
|
const fixedValue = correctImport(value);
|
|
if (fixedValue) {
|
|
context.report({
|
|
node,
|
|
message: 'Relative imports should use the ".js" file extension.',
|
|
fix: (fixer) => fixer.replaceText(source, `'${fixedValue}'`),
|
|
});
|
|
}
|
|
},
|
|
ImportExpression: (node) => {
|
|
const { source } = node;
|
|
const { value } = source;
|
|
|
|
const fixedSource = correctImport(value);
|
|
if (fixedSource) {
|
|
context.report({
|
|
node: source,
|
|
message: 'Relative imports should use the ".js" file extension.',
|
|
fix: (fixer) => fixer.replaceText(source, `'${fixedSource}'`),
|
|
});
|
|
}
|
|
},
|
|
ExportAllDeclaration: (node) => {
|
|
const { source } = node;
|
|
const { value } = source;
|
|
|
|
const fixedSource = correctImport(value);
|
|
if (fixedSource) {
|
|
context.report({
|
|
node: source,
|
|
message: 'Relative exports should use the ".js" file extension.',
|
|
fix: (fixer) => fixer.replaceText(source, `'${fixedSource}'`),
|
|
});
|
|
}
|
|
},
|
|
ExportNamedDeclaration: (node) => {
|
|
const { source } = node;
|
|
if (!source) return;
|
|
const { value } = source;
|
|
|
|
const fixedSource = correctImport(value);
|
|
if (fixedSource) {
|
|
context.report({
|
|
node: source,
|
|
message: 'Relative exports should use the ".js" file extension.',
|
|
fix: (fixer) => fixer.replaceText(source, `'${fixedSource}'`),
|
|
});
|
|
}
|
|
}
|
|
};
|
|
},
|
|
},
|
|
|
|
/** @type {import('eslint').Rule.RuleModule}*/
|
|
'enforce-umbraco-external-imports': {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Ensures that the application strictly uses node_modules imports from `@umbraco-cms/backoffice/external`. This is needed to run the application in the browser.',
|
|
recommended: true,
|
|
},
|
|
fixable: 'code',
|
|
schema: {
|
|
type: "array",
|
|
minItems: 0,
|
|
maxItems: 1,
|
|
items: [
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
exceptions: { type: "array" }
|
|
},
|
|
additionalProperties: false
|
|
}
|
|
]
|
|
}
|
|
},
|
|
create: (context) => {
|
|
return {
|
|
ImportDeclaration: (node) => {
|
|
const { source } = node;
|
|
const { value } = source;
|
|
|
|
const options = context.options[0] || {};
|
|
const exceptions = options.exceptions || [];
|
|
|
|
// If import starts with any of the following, then it's allowed
|
|
if (exceptions.some(v => value.startsWith(v))) {
|
|
return;
|
|
}
|
|
|
|
context.report({
|
|
node,
|
|
message: 'node_modules imports should be proxied through `@umbraco-cms/backoffice/external`. Please create it if it does not exist.',
|
|
fix: (fixer) => fixer.replaceText(source, `'@umbraco-cms/backoffice/external${value.startsWith('/') ? '' : '/'}${value}'`),
|
|
});
|
|
},
|
|
};
|
|
},
|
|
},
|
|
};
|