@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.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/README.md +19 -15
  3. package/dist/plugin.d.ts +24 -0
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +8 -0
  6. package/dist/plugin.js.map +1 -1
  7. package/dist/rules/cred-class-field-icon-missing.d.ts +2 -0
  8. package/dist/rules/cred-class-field-icon-missing.d.ts.map +1 -0
  9. package/dist/rules/cred-class-field-icon-missing.js +51 -0
  10. package/dist/rules/cred-class-field-icon-missing.js.map +1 -0
  11. package/dist/rules/icon-validation.d.ts +1 -1
  12. package/dist/rules/index.d.ts +5 -1
  13. package/dist/rules/index.d.ts.map +1 -1
  14. package/dist/rules/index.js +8 -0
  15. package/dist/rules/index.js.map +1 -1
  16. package/dist/rules/node-class-description-icon-missing.d.ts +2 -0
  17. package/dist/rules/node-class-description-icon-missing.d.ts.map +1 -0
  18. package/dist/rules/node-class-description-icon-missing.js +57 -0
  19. package/dist/rules/node-class-description-icon-missing.js.map +1 -0
  20. package/dist/rules/node-connection-type-literal.d.ts +2 -0
  21. package/dist/rules/node-connection-type-literal.d.ts.map +1 -0
  22. package/dist/rules/node-connection-type-literal.js +77 -0
  23. package/dist/rules/node-connection-type-literal.js.map +1 -0
  24. package/dist/rules/options-sorted-alphabetically.d.ts +2 -0
  25. package/dist/rules/options-sorted-alphabetically.d.ts.map +1 -0
  26. package/dist/rules/options-sorted-alphabetically.js +95 -0
  27. package/dist/rules/options-sorted-alphabetically.js.map +1 -0
  28. package/docs/rules/cred-class-field-icon-missing.md +47 -0
  29. package/docs/rules/node-class-description-icon-missing.md +52 -0
  30. package/docs/rules/node-connection-type-literal.md +46 -0
  31. package/docs/rules/options-sorted-alphabetically.md +63 -0
  32. package/package.json +9 -6
  33. package/src/plugin.ts +8 -0
  34. package/src/rules/cred-class-field-icon-missing.test.ts +96 -0
  35. package/src/rules/cred-class-field-icon-missing.ts +59 -0
  36. package/src/rules/index.ts +8 -0
  37. package/src/rules/node-class-description-icon-missing.test.ts +113 -0
  38. package/src/rules/node-class-description-icon-missing.ts +71 -0
  39. package/src/rules/node-connection-type-literal.test.ts +128 -0
  40. package/src/rules/node-connection-type-literal.ts +98 -0
  41. package/src/rules/options-sorted-alphabetically.test.ts +468 -0
  42. package/src/rules/options-sorted-alphabetically.ts +129 -0
  43. package/tsconfig.json +1 -1
  44. 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.9.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": "5.9.2",
23
- "vitest": "^3.1.3",
24
- "@n8n/typescript-config": "1.3.0",
25
- "@n8n/vitest-config": "1.7.0"
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
+ });
@@ -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
+ });