2022-08-25 12:42:15 +02:00
'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 = {
2023-01-03 15:51:37 +01:00
/** @type {import('eslint').Rule.RuleModule} */
2022-08-25 12:42:15 +02:00
'bad-type-import' : {
meta : {
2023-01-04 10:34:40 +01:00
type : 'problem' ,
2022-08-25 12:42:15 +02:00
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 ) {
2023-03-23 23:06:29 +01:00
if (
node . source . parent . importKind !== 'type' &&
( node . source . value . endsWith ( '/models' ) || node . source . value === 'router-slot/model' )
) {
2022-08-25 12:42:15 +02:00
const sourceCode = context . getSourceCode ( ) ;
const nodeSource = sourceCode . getText ( node ) ;
context . report ( {
node ,
message : 'Use `import type` instead of `import`.' ,
2023-03-23 23:06:29 +01:00
fix : ( fixer ) => fixer . replaceText ( node , nodeSource . replace ( 'import' , 'import type' ) ) ,
2022-08-25 12:42:15 +02:00
} ) ;
}
} ,
} ;
2023-03-23 23:06:29 +01:00
} ,
2023-01-03 15:51:37 +01:00
} ,
/** @type {import('eslint').Rule.RuleModule} */
'no-direct-api-import' : {
meta : {
type : 'suggestion' ,
docs : {
2023-03-23 23:06:29 +01:00
description :
2023-05-25 15:11:37 +02:00
'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.' ,
2023-01-03 15:51:37 +01:00
category : 'Best Practices' ,
2023-03-23 23:06:29 +01:00
recommended : true ,
2023-01-03 15:51:37 +01:00
} ,
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 ) {
2023-03-23 23:06:29 +01:00
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' ) ;
2023-01-03 15:51:37 +01:00
if ( ! hasTryExecuteAndNotify ) {
context . report ( {
node ,
message : 'Wrap this call with `tryExecuteAndNotify()`. Make sure to `await` the result.' ,
2023-03-23 23:06:29 +01:00
fix : ( fixer ) => [
fixer . insertTextBefore ( node , 'tryExecuteAndNotify(this, ' ) ,
fixer . insertTextAfter ( node , ')' ) ,
] ,
2023-01-03 15:51:37 +01:00
} ) ;
}
}
2023-03-23 23:06:29 +01:00
} ,
2023-01-03 15:51:37 +01:00
} ;
} ,
2023-01-23 11:01:51 +01:00
} ,
/** @type {import('eslint').Rule.RuleModule} */
'prefer-import-aliases' : {
meta : {
type : 'suggestion' ,
docs : {
2023-03-23 23:06:29 +01:00
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.' ,
2023-01-23 11:01:51 +01:00
category : 'Best Practices' ,
2023-03-23 23:06:29 +01:00
recommended : true ,
2023-01-23 11:01:51 +01:00
} ,
schema : [ ] ,
} ,
create : function ( context ) {
return {
ImportDeclaration : function ( node ) {
if ( node . source . value . startsWith ( 'src/' ) ) {
context . report ( {
node ,
2023-03-23 23:06:29 +01:00
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";`' ,
} ) ;
}
} ,
} ;
} ,
} ,
2023-04-04 16:47:44 +02:00
2023-03-29 17:56:39 +02:00
/** @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 ,
} ,
2023-04-04 13:19:19 +02:00
schema : [ ] ,
2023-03-29 17:56:39 +02:00
} ,
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' ) ;
2023-03-23 23:06:29 +01:00
2023-03-29 17:56:39 +02:00
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.
} ) ;
}
} ,
} ;
} ,
} ,
2023-04-04 16:47:44 +02:00
2023-03-23 23:06:29 +01:00
/** @type {import('eslint').Rule.RuleModule} */
2023-05-19 08:38:34 +02:00
/ *
2023-04-04 16:47:44 +02:00
'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' ` ) ;
} ,
} ) ;
}
} ,
} ;
} ,
} ,
2023-05-25 15:11:37 +02:00
* /
2023-04-16 20:27:07 +02:00
/** @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 ,
} ;
} ,
} ,
2023-04-21 14:01:09 +02:00
2023-08-03 14:01:04 +02:00
/** @type {import('eslint').Rule.RuleModule}*/
2023-04-21 14:01:09 +02:00
'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 ) ,
] ;
} ,
} ) ;
}
}
}
} ,
} ;
} ,
} ,
2023-05-25 15:11:37 +02:00
2023-08-03 14:01:04 +02:00
/** @type {import('eslint').Rule.RuleModule}*/
2023-05-25 15:11:37 +02:00
'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 ) => {
2023-05-26 00:34:42 +02:00
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 ;
}
2023-05-25 15:11:37 +02:00
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 } ' ` ) ,
} ) ;
}
} ,
2023-05-26 00:34:42 +02:00
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 } ' ` ) ,
} ) ;
2023-05-25 15:11:37 +02:00
}
2023-05-25 15:16:31 +02:00
} ,
2023-08-03 14:01:04 +02:00
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 } ' ` ) ,
} ) ;
}
}
2023-05-25 15:11:37 +02:00
} ;
} ,
} ,
2023-11-02 16:49:32 +01:00
/** @type {import('eslint').Rule.RuleModule}*/
2023-11-06 10:18:16 +01:00
'enforce-umbraco-external-imports' : {
2023-11-02 16:49:32 +01:00
meta : {
type : 'problem' ,
docs : {
2023-11-06 09:47:26 +01:00
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.' ,
2023-11-02 16:49:32 +01:00
recommended : true ,
} ,
fixable : 'code' ,
2023-11-06 09:45:03 +01:00
schema : {
type : "array" ,
minItems : 0 ,
maxItems : 1 ,
items : [
{
type : "object" ,
properties : {
exceptions : { type : "array" }
} ,
additionalProperties : false
}
]
}
2023-11-02 16:49:32 +01:00
} ,
create : ( context ) => {
return {
ImportDeclaration : ( node ) => {
const { source } = node ;
const { value } = source ;
2023-11-06 09:45:03 +01:00
const options = context . options [ 0 ] || { } ;
const exceptions = options . exceptions || [ ] ;
2023-11-02 16:49:32 +01:00
// If import starts with any of the following, then it's allowed
2023-11-06 09:45:03 +01:00
if ( exceptions . some ( v => value . startsWith ( v ) ) ) {
2023-11-02 16:49:32 +01:00
return ;
}
context . report ( {
node ,
2023-11-06 09:47:26 +01:00
message : 'node_modules imports should be proxied through `@umbraco-cms/backoffice/external`. Please create it if it does not exist.' ,
2023-11-02 16:49:32 +01:00
fix : ( fixer ) => fixer . replaceText ( source , ` '@umbraco-cms/backoffice/external ${ value . startsWith ( '/' ) ? '' : '/' } ${ value } ' ` ) ,
} ) ;
} ,
} ;
} ,
} ,
2022-08-25 12:42:15 +02:00
} ;