@n8n/eslint-plugin-community-nodes 0.9.0 → 0.11.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/.turbo/turbo-build.log +1 -1
- package/README.md +19 -15
- package/dist/plugin.d.ts +24 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +8 -0
- package/dist/plugin.js.map +1 -1
- package/dist/rules/cred-class-field-icon-missing.d.ts +2 -0
- package/dist/rules/cred-class-field-icon-missing.d.ts.map +1 -0
- package/dist/rules/cred-class-field-icon-missing.js +51 -0
- package/dist/rules/cred-class-field-icon-missing.js.map +1 -0
- package/dist/rules/icon-validation.d.ts +1 -1
- package/dist/rules/index.d.ts +5 -1
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +8 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/node-class-description-icon-missing.d.ts +2 -0
- package/dist/rules/node-class-description-icon-missing.d.ts.map +1 -0
- package/dist/rules/node-class-description-icon-missing.js +57 -0
- package/dist/rules/node-class-description-icon-missing.js.map +1 -0
- package/dist/rules/node-connection-type-literal.d.ts +2 -0
- package/dist/rules/node-connection-type-literal.d.ts.map +1 -0
- package/dist/rules/node-connection-type-literal.js +77 -0
- package/dist/rules/node-connection-type-literal.js.map +1 -0
- package/dist/rules/options-sorted-alphabetically.d.ts +2 -0
- package/dist/rules/options-sorted-alphabetically.d.ts.map +1 -0
- package/dist/rules/options-sorted-alphabetically.js +95 -0
- package/dist/rules/options-sorted-alphabetically.js.map +1 -0
- package/docs/rules/cred-class-field-icon-missing.md +47 -0
- package/docs/rules/node-class-description-icon-missing.md +52 -0
- package/docs/rules/node-connection-type-literal.md +46 -0
- package/docs/rules/options-sorted-alphabetically.md +63 -0
- package/package.json +9 -6
- package/src/plugin.ts +8 -0
- package/src/rules/cred-class-field-icon-missing.test.ts +96 -0
- package/src/rules/cred-class-field-icon-missing.ts +59 -0
- package/src/rules/index.ts +8 -0
- package/src/rules/node-class-description-icon-missing.test.ts +113 -0
- package/src/rules/node-class-description-icon-missing.ts +71 -0
- package/src/rules/node-connection-type-literal.test.ts +128 -0
- package/src/rules/node-connection-type-literal.ts +98 -0
- package/src/rules/options-sorted-alphabetically.test.ts +468 -0
- package/src/rules/options-sorted-alphabetically.ts +129 -0
- package/tsconfig.json +1 -1
- package/tsconfig.build.tsbuildinfo +0 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Credential class must have an `icon` property defined (`@n8n/community-nodes/cred-class-field-icon-missing`)
|
|
2
|
+
|
|
3
|
+
💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
|
|
4
|
+
|
|
5
|
+
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
6
|
+
|
|
7
|
+
<!-- end auto-generated rule header -->
|
|
8
|
+
|
|
9
|
+
## Rule Details
|
|
10
|
+
|
|
11
|
+
Validates that credential classes define an `icon` class field. Icons are required for credentials to display correctly in the n8n editor.
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
### ❌ Incorrect
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
export class MyServiceApi implements ICredentialType {
|
|
19
|
+
name = 'myServiceApi';
|
|
20
|
+
displayName = 'My Service API';
|
|
21
|
+
// Missing icon property
|
|
22
|
+
|
|
23
|
+
properties: INodeProperties[] = [];
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### ✅ Correct
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
export class MyServiceApi implements ICredentialType {
|
|
31
|
+
name = 'myServiceApi';
|
|
32
|
+
displayName = 'My Service API';
|
|
33
|
+
icon = 'file:myService.svg' as const;
|
|
34
|
+
|
|
35
|
+
properties: INodeProperties[] = [];
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
export class MyServiceApi implements ICredentialType {
|
|
41
|
+
name = 'myServiceApi';
|
|
42
|
+
displayName = 'My Service API';
|
|
43
|
+
icon = { light: 'file:myService.svg', dark: 'file:myService.dark.svg' } as const;
|
|
44
|
+
|
|
45
|
+
properties: INodeProperties[] = [];
|
|
46
|
+
}
|
|
47
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Node class description must have an `icon` property defined (`@n8n/community-nodes/node-class-description-icon-missing`)
|
|
2
|
+
|
|
3
|
+
💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
|
|
4
|
+
|
|
5
|
+
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
6
|
+
|
|
7
|
+
<!-- end auto-generated rule header -->
|
|
8
|
+
|
|
9
|
+
## Rule Details
|
|
10
|
+
|
|
11
|
+
Validates that node classes define an `icon` property in their `description` object. Icons are required for nodes to display correctly in the n8n editor.
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
### ❌ Incorrect
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
export class MyNode implements INodeType {
|
|
19
|
+
description: INodeTypeDescription = {
|
|
20
|
+
displayName: 'My Node',
|
|
21
|
+
name: 'myNode',
|
|
22
|
+
// Missing icon property
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### ✅ Correct
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
export class MyNode implements INodeType {
|
|
31
|
+
description: INodeTypeDescription = {
|
|
32
|
+
displayName: 'My Node',
|
|
33
|
+
name: 'myNode',
|
|
34
|
+
icon: 'file:myNode.svg',
|
|
35
|
+
// ...
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
export class MyNode implements INodeType {
|
|
42
|
+
description: INodeTypeDescription = {
|
|
43
|
+
displayName: 'My Node',
|
|
44
|
+
name: 'myNode',
|
|
45
|
+
icon: {
|
|
46
|
+
light: 'file:myNode.svg',
|
|
47
|
+
dark: 'file:myNode.dark.svg',
|
|
48
|
+
},
|
|
49
|
+
// ...
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Disallow string literals in node description `inputs`/`outputs` — use `NodeConnectionTypes` enum instead (`@n8n/community-nodes/node-connection-type-literal`)
|
|
2
|
+
|
|
3
|
+
💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
|
|
4
|
+
|
|
5
|
+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
|
|
6
|
+
|
|
7
|
+
<!-- end auto-generated rule header -->
|
|
8
|
+
|
|
9
|
+
## Rule Details
|
|
10
|
+
|
|
11
|
+
Using raw string literals like `'main'` in `inputs` and `outputs` is fragile: the values are not type-checked, and typos or renamed connection types will go undetected. The `NodeConnectionTypes` object from `n8n-workflow` is the single source of truth and should always be used instead.
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
### ❌ Incorrect
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
19
|
+
|
|
20
|
+
export class MyNode implements INodeType {
|
|
21
|
+
description: INodeTypeDescription = {
|
|
22
|
+
displayName: 'My Node',
|
|
23
|
+
name: 'myNode',
|
|
24
|
+
inputs: ['main'],
|
|
25
|
+
outputs: ['main'],
|
|
26
|
+
properties: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### ✅ Correct
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
35
|
+
import { NodeConnectionTypes } from 'n8n-workflow';
|
|
36
|
+
|
|
37
|
+
export class MyNode implements INodeType {
|
|
38
|
+
description: INodeTypeDescription = {
|
|
39
|
+
displayName: 'My Node',
|
|
40
|
+
name: 'myNode',
|
|
41
|
+
inputs: [NodeConnectionTypes.Main],
|
|
42
|
+
outputs: [NodeConnectionTypes.Main],
|
|
43
|
+
properties: [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Enforce alphabetical ordering of options arrays in n8n node properties (`@n8n/community-nodes/options-sorted-alphabetically`)
|
|
2
|
+
|
|
3
|
+
⚠️ This rule _warns_ in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
|
|
4
|
+
|
|
5
|
+
<!-- end auto-generated rule header -->
|
|
6
|
+
|
|
7
|
+
## Rule Details
|
|
8
|
+
|
|
9
|
+
Warns when an `options`-type parameter has its options array not sorted alphabetically by name. Applies to all `type: 'options'` parameters — including `resource`, `operation`, and any other dropdowns.
|
|
10
|
+
|
|
11
|
+
Alphabetical ordering is an [official n8n UI design requirement](https://docs.n8n.io/integrations/creating-nodes/plan/node-ui-design/#lists) and the most frequently flagged issue in community node reviews.
|
|
12
|
+
|
|
13
|
+
Comparison is case-insensitive and locale-aware (handles non-ASCII names such as Spanish or Portuguese labels).
|
|
14
|
+
|
|
15
|
+
## Examples
|
|
16
|
+
|
|
17
|
+
### ❌ Incorrect
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
export class MyNode implements INodeType {
|
|
21
|
+
description: INodeTypeDescription = {
|
|
22
|
+
displayName: 'My Service',
|
|
23
|
+
name: 'myService',
|
|
24
|
+
properties: [
|
|
25
|
+
{
|
|
26
|
+
displayName: 'Resource',
|
|
27
|
+
name: 'resource',
|
|
28
|
+
type: 'options',
|
|
29
|
+
options: [
|
|
30
|
+
{ name: 'User', value: 'user' },
|
|
31
|
+
{ name: 'Contact', value: 'contact' }, // out of order
|
|
32
|
+
{ name: 'Project', value: 'project' },
|
|
33
|
+
],
|
|
34
|
+
default: 'user',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### ✅ Correct
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
export class MyNode implements INodeType {
|
|
45
|
+
description: INodeTypeDescription = {
|
|
46
|
+
displayName: 'My Service',
|
|
47
|
+
name: 'myService',
|
|
48
|
+
properties: [
|
|
49
|
+
{
|
|
50
|
+
displayName: 'Resource',
|
|
51
|
+
name: 'resource',
|
|
52
|
+
type: 'options',
|
|
53
|
+
options: [
|
|
54
|
+
{ name: 'Contact', value: 'contact' },
|
|
55
|
+
{ name: 'Project', value: 'project' },
|
|
56
|
+
{ name: 'User', value: 'user' },
|
|
57
|
+
],
|
|
58
|
+
default: 'contact',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@n8n/eslint-plugin-community-nodes",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.0",
|
|
5
5
|
"main": "./dist/plugin.js",
|
|
6
6
|
"types": "./dist/plugin.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -15,17 +15,20 @@
|
|
|
15
15
|
"fastest-levenshtein": "1.0.16"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
+
"@types/node": "24.10.1",
|
|
18
19
|
"@typescript-eslint/rule-tester": "^8.35.0",
|
|
19
20
|
"eslint-doc-generator": "^2.2.2",
|
|
20
21
|
"eslint-plugin-eslint-plugin": "^7.0.0",
|
|
21
22
|
"rimraf": "6.0.1",
|
|
22
|
-
"typescript": "
|
|
23
|
-
"vitest": "^
|
|
24
|
-
"@n8n/typescript-config": "1.
|
|
25
|
-
"@n8n/vitest-config": "1.
|
|
23
|
+
"typescript": "6.0.2",
|
|
24
|
+
"vitest": "^4.1.1",
|
|
25
|
+
"@n8n/typescript-config": "1.4.0",
|
|
26
|
+
"@n8n/vitest-config": "1.9.0",
|
|
27
|
+
"n8n-workflow": "2.16.0"
|
|
26
28
|
},
|
|
27
29
|
"peerDependencies": {
|
|
28
|
-
"eslint": ">= 9"
|
|
30
|
+
"eslint": ">= 9",
|
|
31
|
+
"n8n-workflow": ">=2"
|
|
29
32
|
},
|
|
30
33
|
"eslint-doc-generator": {
|
|
31
34
|
"configEmoji": [
|
package/src/plugin.ts
CHANGED
|
@@ -31,8 +31,12 @@ const configs = {
|
|
|
31
31
|
'@n8n/community-nodes/no-credential-reuse': 'error',
|
|
32
32
|
'@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
|
|
33
33
|
'@n8n/community-nodes/icon-validation': 'error',
|
|
34
|
+
'@n8n/community-nodes/options-sorted-alphabetically': 'warn',
|
|
34
35
|
'@n8n/community-nodes/resource-operation-pattern': 'warn',
|
|
35
36
|
'@n8n/community-nodes/credential-documentation-url': 'error',
|
|
37
|
+
'@n8n/community-nodes/node-class-description-icon-missing': 'error',
|
|
38
|
+
'@n8n/community-nodes/cred-class-field-icon-missing': 'error',
|
|
39
|
+
'@n8n/community-nodes/node-connection-type-literal': 'error',
|
|
36
40
|
},
|
|
37
41
|
},
|
|
38
42
|
recommendedWithoutN8nCloudSupport: {
|
|
@@ -50,8 +54,12 @@ const configs = {
|
|
|
50
54
|
'@n8n/community-nodes/no-credential-reuse': 'error',
|
|
51
55
|
'@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
|
|
52
56
|
'@n8n/community-nodes/icon-validation': 'error',
|
|
57
|
+
'@n8n/community-nodes/options-sorted-alphabetically': 'warn',
|
|
53
58
|
'@n8n/community-nodes/credential-documentation-url': 'error',
|
|
54
59
|
'@n8n/community-nodes/resource-operation-pattern': 'warn',
|
|
60
|
+
'@n8n/community-nodes/node-class-description-icon-missing': 'error',
|
|
61
|
+
'@n8n/community-nodes/cred-class-field-icon-missing': 'error',
|
|
62
|
+
'@n8n/community-nodes/node-connection-type-literal': 'error',
|
|
55
63
|
},
|
|
56
64
|
},
|
|
57
65
|
} satisfies Record<string, Linter.Config>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { CredClassFieldIconMissingRule } from './cred-class-field-icon-missing.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
const credFilePath = '/tmp/TestCredential.credentials.ts';
|
|
8
|
+
const nonCredFilePath = '/tmp/SomeHelper.ts';
|
|
9
|
+
|
|
10
|
+
function createCredentialCode(withIcon: boolean): string {
|
|
11
|
+
const iconLine = withIcon ? "\n\ticon = 'file:testCredential.svg' as const;" : '';
|
|
12
|
+
return `
|
|
13
|
+
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
14
|
+
|
|
15
|
+
export class TestCredential implements ICredentialType {
|
|
16
|
+
name = 'testApi';
|
|
17
|
+
displayName = 'Test API';
|
|
18
|
+
documentationUrl = 'https://docs.example.com';${iconLine}
|
|
19
|
+
|
|
20
|
+
properties: INodeProperties[] = [];
|
|
21
|
+
}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createCredentialWithLightDarkIcon(): string {
|
|
25
|
+
return `
|
|
26
|
+
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
27
|
+
|
|
28
|
+
export class TestCredential implements ICredentialType {
|
|
29
|
+
name = 'testApi';
|
|
30
|
+
displayName = 'Test API';
|
|
31
|
+
icon = { light: 'file:testCredential.svg', dark: 'file:testCredential.dark.svg' } as const;
|
|
32
|
+
|
|
33
|
+
properties: INodeProperties[] = [];
|
|
34
|
+
}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createRegularClass(): string {
|
|
38
|
+
return `
|
|
39
|
+
export class RegularClass {
|
|
40
|
+
name = 'test';
|
|
41
|
+
}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ruleTester.run('cred-class-field-icon-missing', CredClassFieldIconMissingRule, {
|
|
45
|
+
valid: [
|
|
46
|
+
{
|
|
47
|
+
name: 'credential with icon defined',
|
|
48
|
+
filename: credFilePath,
|
|
49
|
+
code: createCredentialCode(true),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'credential with light/dark icon object',
|
|
53
|
+
filename: credFilePath,
|
|
54
|
+
code: createCredentialWithLightDarkIcon(),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'class not implementing ICredentialType is ignored',
|
|
58
|
+
filename: credFilePath,
|
|
59
|
+
code: createRegularClass(),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'non-.credentials.ts file is ignored',
|
|
63
|
+
filename: nonCredFilePath,
|
|
64
|
+
code: createCredentialCode(false),
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
invalid: [
|
|
68
|
+
{
|
|
69
|
+
name: 'credential missing icon property',
|
|
70
|
+
filename: credFilePath,
|
|
71
|
+
code: createCredentialCode(false),
|
|
72
|
+
errors: [
|
|
73
|
+
{
|
|
74
|
+
messageId: 'missingIcon',
|
|
75
|
+
suggestions: [
|
|
76
|
+
{
|
|
77
|
+
messageId: 'addPlaceholder',
|
|
78
|
+
output: `
|
|
79
|
+
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
80
|
+
|
|
81
|
+
export class TestCredential implements ICredentialType {
|
|
82
|
+
name = 'testApi';
|
|
83
|
+
displayName = 'Test API';
|
|
84
|
+
documentationUrl = 'https://docs.example.com';
|
|
85
|
+
|
|
86
|
+
properties: INodeProperties[] = [];
|
|
87
|
+
|
|
88
|
+
icon = "file:./icon.svg";
|
|
89
|
+
}`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isCredentialTypeClass,
|
|
3
|
+
findClassProperty,
|
|
4
|
+
isFileType,
|
|
5
|
+
createRule,
|
|
6
|
+
} from '../utils/index.js';
|
|
7
|
+
|
|
8
|
+
export const CredClassFieldIconMissingRule = createRule({
|
|
9
|
+
name: 'cred-class-field-icon-missing',
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Credential class must have an `icon` property defined',
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
missingIcon: 'Credential class is missing required `icon` property',
|
|
17
|
+
addPlaceholder: 'Add icon property with placeholder',
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
hasSuggestions: true,
|
|
21
|
+
},
|
|
22
|
+
defaultOptions: [],
|
|
23
|
+
create(context) {
|
|
24
|
+
if (!isFileType(context.filename, '.credentials.ts')) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
ClassDeclaration(node) {
|
|
30
|
+
if (!isCredentialTypeClass(node)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const iconProperty = findClassProperty(node, 'icon');
|
|
35
|
+
if (iconProperty?.value) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
context.report({
|
|
40
|
+
node,
|
|
41
|
+
messageId: 'missingIcon',
|
|
42
|
+
suggest: [
|
|
43
|
+
{
|
|
44
|
+
messageId: 'addPlaceholder',
|
|
45
|
+
fix(fixer) {
|
|
46
|
+
const classBody = node.body.body;
|
|
47
|
+
const lastProperty = classBody[classBody.length - 1];
|
|
48
|
+
if (lastProperty) {
|
|
49
|
+
return fixer.insertTextAfter(lastProperty, '\n\n\ticon = "file:./icon.svg";');
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
});
|
package/src/rules/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AnyRuleModule } from '@typescript-eslint/utils/ts-eslint';
|
|
2
2
|
|
|
3
3
|
import { AiNodePackageJsonRule } from './ai-node-package-json.js';
|
|
4
|
+
import { CredClassFieldIconMissingRule } from './cred-class-field-icon-missing.js';
|
|
4
5
|
import { CredentialDocumentationUrlRule } from './credential-documentation-url.js';
|
|
5
6
|
import { CredentialPasswordFieldRule } from './credential-password-field.js';
|
|
6
7
|
import { CredentialTestRequiredRule } from './credential-test-required.js';
|
|
@@ -10,7 +11,10 @@ import { NoDeprecatedWorkflowFunctionsRule } from './no-deprecated-workflow-func
|
|
|
10
11
|
import { NoHttpRequestWithManualAuthRule } from './no-http-request-with-manual-auth.js';
|
|
11
12
|
import { NoRestrictedGlobalsRule } from './no-restricted-globals.js';
|
|
12
13
|
import { NoRestrictedImportsRule } from './no-restricted-imports.js';
|
|
14
|
+
import { NodeClassDescriptionIconMissingRule } from './node-class-description-icon-missing.js';
|
|
15
|
+
import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js';
|
|
13
16
|
import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
|
|
17
|
+
import { OptionsSortedAlphabeticallyRule } from './options-sorted-alphabetically.js';
|
|
14
18
|
import { PackageNameConventionRule } from './package-name-convention.js';
|
|
15
19
|
import { ResourceOperationPatternRule } from './resource-operation-pattern.js';
|
|
16
20
|
|
|
@@ -21,6 +25,7 @@ export const rules = {
|
|
|
21
25
|
'credential-password-field': CredentialPasswordFieldRule,
|
|
22
26
|
'no-deprecated-workflow-functions': NoDeprecatedWorkflowFunctionsRule,
|
|
23
27
|
'node-usable-as-tool': NodeUsableAsToolRule,
|
|
28
|
+
'options-sorted-alphabetically': OptionsSortedAlphabeticallyRule,
|
|
24
29
|
'package-name-convention': PackageNameConventionRule,
|
|
25
30
|
'credential-test-required': CredentialTestRequiredRule,
|
|
26
31
|
'no-credential-reuse': NoCredentialReuseRule,
|
|
@@ -28,4 +33,7 @@ export const rules = {
|
|
|
28
33
|
'icon-validation': IconValidationRule,
|
|
29
34
|
'resource-operation-pattern': ResourceOperationPatternRule,
|
|
30
35
|
'credential-documentation-url': CredentialDocumentationUrlRule,
|
|
36
|
+
'node-class-description-icon-missing': NodeClassDescriptionIconMissingRule,
|
|
37
|
+
'cred-class-field-icon-missing': CredClassFieldIconMissingRule,
|
|
38
|
+
'node-connection-type-literal': NodeConnectionTypeLiteralRule,
|
|
31
39
|
} satisfies Record<string, AnyRuleModule>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { NodeClassDescriptionIconMissingRule } from './node-class-description-icon-missing.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
const nodeFilePath = '/tmp/TestNode.node.ts';
|
|
8
|
+
const nonNodeFilePath = '/tmp/SomeHelper.ts';
|
|
9
|
+
|
|
10
|
+
function createNodeCode(withIcon: boolean): string {
|
|
11
|
+
const iconLine = withIcon ? "\n\t\ticon: 'file:testNode.svg'," : '';
|
|
12
|
+
return `
|
|
13
|
+
import type { INodeType } from 'n8n-workflow';
|
|
14
|
+
|
|
15
|
+
export class TestNode implements INodeType {
|
|
16
|
+
description = {
|
|
17
|
+
displayName: 'Test Node',
|
|
18
|
+
name: 'testNode',
|
|
19
|
+
group: ['transform'],
|
|
20
|
+
version: 1,
|
|
21
|
+
description: 'Test',${iconLine}
|
|
22
|
+
inputs: ['main'],
|
|
23
|
+
outputs: ['main'],
|
|
24
|
+
properties: [],
|
|
25
|
+
};
|
|
26
|
+
}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createNodeCodeWithLightDarkIcon(): string {
|
|
30
|
+
return `
|
|
31
|
+
import type { INodeType } from 'n8n-workflow';
|
|
32
|
+
|
|
33
|
+
export class TestNode implements INodeType {
|
|
34
|
+
description = {
|
|
35
|
+
displayName: 'Test Node',
|
|
36
|
+
name: 'testNode',
|
|
37
|
+
group: ['transform'],
|
|
38
|
+
version: 1,
|
|
39
|
+
description: 'Test',
|
|
40
|
+
icon: { light: 'file:testNode.svg', dark: 'file:testNode.dark.svg' },
|
|
41
|
+
inputs: ['main'],
|
|
42
|
+
outputs: ['main'],
|
|
43
|
+
properties: [],
|
|
44
|
+
};
|
|
45
|
+
}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createRegularClass(): string {
|
|
49
|
+
return `
|
|
50
|
+
export class RegularClass {
|
|
51
|
+
description = {
|
|
52
|
+
displayName: 'Test',
|
|
53
|
+
};
|
|
54
|
+
}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ruleTester.run('node-class-description-icon-missing', NodeClassDescriptionIconMissingRule, {
|
|
58
|
+
valid: [
|
|
59
|
+
{
|
|
60
|
+
name: 'node with icon defined',
|
|
61
|
+
filename: nodeFilePath,
|
|
62
|
+
code: createNodeCode(true),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'node with light/dark icon object',
|
|
66
|
+
filename: nodeFilePath,
|
|
67
|
+
code: createNodeCodeWithLightDarkIcon(),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'class not implementing INodeType is ignored',
|
|
71
|
+
filename: nodeFilePath,
|
|
72
|
+
code: createRegularClass(),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'non-.node.ts file is ignored',
|
|
76
|
+
filename: nonNodeFilePath,
|
|
77
|
+
code: createNodeCode(false),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
invalid: [
|
|
81
|
+
{
|
|
82
|
+
name: 'node missing icon property',
|
|
83
|
+
filename: nodeFilePath,
|
|
84
|
+
code: createNodeCode(false),
|
|
85
|
+
errors: [
|
|
86
|
+
{
|
|
87
|
+
messageId: 'missingIcon',
|
|
88
|
+
suggestions: [
|
|
89
|
+
{
|
|
90
|
+
messageId: 'addPlaceholder',
|
|
91
|
+
output: `
|
|
92
|
+
import type { INodeType } from 'n8n-workflow';
|
|
93
|
+
|
|
94
|
+
export class TestNode implements INodeType {
|
|
95
|
+
description = {
|
|
96
|
+
displayName: 'Test Node',
|
|
97
|
+
name: 'testNode',
|
|
98
|
+
group: ['transform'],
|
|
99
|
+
version: 1,
|
|
100
|
+
description: 'Test',
|
|
101
|
+
inputs: ['main'],
|
|
102
|
+
outputs: ['main'],
|
|
103
|
+
properties: [],
|
|
104
|
+
icon: "file:./icon.svg",
|
|
105
|
+
};
|
|
106
|
+
}`,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isNodeTypeClass,
|
|
5
|
+
findClassProperty,
|
|
6
|
+
findObjectProperty,
|
|
7
|
+
isFileType,
|
|
8
|
+
createRule,
|
|
9
|
+
} from '../utils/index.js';
|
|
10
|
+
|
|
11
|
+
export const NodeClassDescriptionIconMissingRule = createRule({
|
|
12
|
+
name: 'node-class-description-icon-missing',
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'problem',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Node class description must have an `icon` property defined',
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
missingIcon: 'Node class description is missing required `icon` property',
|
|
20
|
+
addPlaceholder: 'Add icon property with placeholder',
|
|
21
|
+
},
|
|
22
|
+
schema: [],
|
|
23
|
+
hasSuggestions: true,
|
|
24
|
+
},
|
|
25
|
+
defaultOptions: [],
|
|
26
|
+
create(context) {
|
|
27
|
+
if (!isFileType(context.filename, '.node.ts')) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
ClassDeclaration(node) {
|
|
33
|
+
if (!isNodeTypeClass(node)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const descriptionProperty = findClassProperty(node, 'description');
|
|
38
|
+
if (
|
|
39
|
+
!descriptionProperty?.value ||
|
|
40
|
+
descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression
|
|
41
|
+
) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const descriptionValue = descriptionProperty.value;
|
|
46
|
+
const iconProperty = findObjectProperty(descriptionValue, 'icon');
|
|
47
|
+
if (iconProperty) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
context.report({
|
|
52
|
+
node,
|
|
53
|
+
messageId: 'missingIcon',
|
|
54
|
+
suggest: [
|
|
55
|
+
{
|
|
56
|
+
messageId: 'addPlaceholder',
|
|
57
|
+
fix(fixer) {
|
|
58
|
+
const lastProperty =
|
|
59
|
+
descriptionValue.properties[descriptionValue.properties.length - 1];
|
|
60
|
+
if (lastProperty) {
|
|
61
|
+
return fixer.insertTextAfter(lastProperty, ',\n\t\ticon: "file:./icon.svg"');
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
});
|