@n8n/eslint-plugin-community-nodes 0.12.0 → 0.13.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 (34) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/README.md +2 -1
  3. package/dist/plugin.d.ts +12 -6
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +4 -2
  6. package/dist/plugin.js.map +1 -1
  7. package/dist/rules/index.d.ts +2 -0
  8. package/dist/rules/index.d.ts.map +1 -1
  9. package/dist/rules/index.js +4 -0
  10. package/dist/rules/index.js.map +1 -1
  11. package/dist/rules/node-class-description-icon-missing.d.ts +1 -0
  12. package/dist/rules/node-class-description-icon-missing.d.ts.map +1 -1
  13. package/dist/rules/node-class-description-icon-missing.js +6 -1
  14. package/dist/rules/node-class-description-icon-missing.js.map +1 -1
  15. package/dist/rules/require-community-node-keyword.d.ts +2 -0
  16. package/dist/rules/require-community-node-keyword.d.ts.map +1 -0
  17. package/dist/rules/require-community-node-keyword.js +65 -0
  18. package/dist/rules/require-community-node-keyword.js.map +1 -0
  19. package/dist/rules/require-node-description-fields.d.ts +2 -0
  20. package/dist/rules/require-node-description-fields.d.ts.map +1 -0
  21. package/dist/rules/require-node-description-fields.js +58 -0
  22. package/dist/rules/require-node-description-fields.js.map +1 -0
  23. package/docs/rules/node-class-description-icon-missing.md +1 -1
  24. package/docs/rules/require-community-node-keyword.md +45 -0
  25. package/docs/rules/require-node-description-fields.md +46 -0
  26. package/package.json +3 -3
  27. package/src/plugin.ts +4 -2
  28. package/src/rules/index.ts +4 -0
  29. package/src/rules/node-class-description-icon-missing.ts +7 -1
  30. package/src/rules/require-community-node-keyword.test.ts +82 -0
  31. package/src/rules/require-community-node-keyword.ts +78 -0
  32. package/src/rules/require-node-description-fields.test.ts +171 -0
  33. package/src/rules/require-node-description-fields.ts +77 -0
  34. package/tsconfig.build.tsbuildinfo +1 -1
@@ -0,0 +1,82 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { RequireCommunityNodeKeywordRule } from './require-community-node-keyword.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ ruleTester.run('require-community-node-keyword', RequireCommunityNodeKeywordRule, {
8
+ valid: [
9
+ {
10
+ name: 'keywords array contains required keyword',
11
+ filename: 'package.json',
12
+ code: '{ "name": "n8n-nodes-example", "keywords": ["n8n-community-node-package"] }',
13
+ },
14
+ {
15
+ name: 'keywords array contains required keyword among others',
16
+ filename: 'package.json',
17
+ code: '{ "name": "n8n-nodes-example", "keywords": ["n8n", "automation", "n8n-community-node-package", "workflow"] }',
18
+ },
19
+ {
20
+ name: 'non-package.json file is ignored',
21
+ filename: 'some-config.json',
22
+ code: '{ "name": "n8n-nodes-example" }',
23
+ },
24
+ {
25
+ name: 'nested objects are not checked',
26
+ filename: 'package.json',
27
+ code: '{ "name": "n8n-nodes-example", "keywords": ["n8n-community-node-package"], "config": { "nested": "value" } }',
28
+ },
29
+ {
30
+ name: 'objects inside arrays (e.g. contributors) are not flagged',
31
+ filename: 'package.json',
32
+ code: `{
33
+ "name": "n8n-nodes-example",
34
+ "keywords": ["n8n-community-node-package"],
35
+ "contributors": [
36
+ { "name": "Alice", "email": "alice@example.com" },
37
+ { "name": "Bob", "email": "bob@example.com" }
38
+ ]
39
+ }`,
40
+ },
41
+ ],
42
+ invalid: [
43
+ {
44
+ name: 'missing keywords array entirely',
45
+ filename: 'package.json',
46
+ code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
47
+ output:
48
+ '{ "name": "n8n-nodes-example", "version": "1.0.0", "keywords": ["n8n-community-node-package"] }',
49
+ errors: [{ messageId: 'missingKeywordsArray' }],
50
+ },
51
+ {
52
+ name: 'empty keywords array',
53
+ filename: 'package.json',
54
+ code: '{ "name": "n8n-nodes-example", "keywords": [] }',
55
+ output: '{ "name": "n8n-nodes-example", "keywords": ["n8n-community-node-package"] }',
56
+ errors: [{ messageId: 'missingKeyword' }],
57
+ },
58
+ {
59
+ name: 'keywords array without the required keyword',
60
+ filename: 'package.json',
61
+ code: '{ "name": "n8n-nodes-example", "keywords": ["n8n", "automation"] }',
62
+ output:
63
+ '{ "name": "n8n-nodes-example", "keywords": ["n8n", "automation", "n8n-community-node-package"] }',
64
+ errors: [{ messageId: 'missingKeyword' }],
65
+ },
66
+ {
67
+ name: 'keywords array with similar but incorrect keyword',
68
+ filename: 'package.json',
69
+ code: '{ "name": "n8n-nodes-example", "keywords": ["n8n-community-node"] }',
70
+ output:
71
+ '{ "name": "n8n-nodes-example", "keywords": ["n8n-community-node", "n8n-community-node-package"] }',
72
+ errors: [{ messageId: 'missingKeyword' }],
73
+ },
74
+ {
75
+ name: 'empty package.json object',
76
+ filename: 'package.json',
77
+ code: '{}',
78
+ output: '{ "keywords": ["n8n-community-node-package"] }',
79
+ errors: [{ messageId: 'missingKeywordsArray' }],
80
+ },
81
+ ],
82
+ });
@@ -0,0 +1,78 @@
1
+ import type { TSESTree } from '@typescript-eslint/utils';
2
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
3
+
4
+ import { createRule, findJsonProperty } from '../utils/index.js';
5
+
6
+ const REQUIRED_KEYWORD = 'n8n-community-node-package';
7
+
8
+ export const RequireCommunityNodeKeywordRule = createRule({
9
+ name: 'require-community-node-keyword',
10
+ meta: {
11
+ type: 'problem',
12
+ docs: {
13
+ description:
14
+ 'Require the "n8n-community-node-package" keyword in community node package.json',
15
+ },
16
+ fixable: 'code',
17
+ messages: {
18
+ missingKeyword: `The "keywords" array must include "${REQUIRED_KEYWORD}". This keyword is required for n8n to discover community node packages.`,
19
+ missingKeywordsArray: `The package.json must have a "keywords" array containing "${REQUIRED_KEYWORD}".`,
20
+ },
21
+ schema: [],
22
+ },
23
+ defaultOptions: [],
24
+ create(context) {
25
+ if (!context.filename.endsWith('package.json')) {
26
+ return {};
27
+ }
28
+
29
+ return {
30
+ ObjectExpression(node: TSESTree.ObjectExpression) {
31
+ if (node.parent?.type !== AST_NODE_TYPES.ExpressionStatement) {
32
+ return;
33
+ }
34
+
35
+ const keywordsProp = findJsonProperty(node, 'keywords');
36
+
37
+ if (!keywordsProp) {
38
+ context.report({
39
+ node,
40
+ messageId: 'missingKeywordsArray',
41
+ fix(fixer) {
42
+ const lastProp = node.properties[node.properties.length - 1];
43
+ if (!lastProp) {
44
+ return fixer.replaceText(node, `{ "keywords": ["${REQUIRED_KEYWORD}"] }`);
45
+ }
46
+ return fixer.insertTextAfter(lastProp, `, "keywords": ["${REQUIRED_KEYWORD}"]`);
47
+ },
48
+ });
49
+ return;
50
+ }
51
+
52
+ if (keywordsProp.value.type !== AST_NODE_TYPES.ArrayExpression) {
53
+ return;
54
+ }
55
+
56
+ const keywordsArray = keywordsProp.value;
57
+ const hasRequiredKeyword = keywordsArray.elements.some(
58
+ (element) =>
59
+ element?.type === AST_NODE_TYPES.Literal && element.value === REQUIRED_KEYWORD,
60
+ );
61
+
62
+ if (!hasRequiredKeyword) {
63
+ context.report({
64
+ node: keywordsProp,
65
+ messageId: 'missingKeyword',
66
+ fix(fixer) {
67
+ const lastElement = keywordsArray.elements[keywordsArray.elements.length - 1];
68
+ if (!lastElement) {
69
+ return fixer.replaceText(keywordsArray, `["${REQUIRED_KEYWORD}"]`);
70
+ }
71
+ return fixer.insertTextAfter(lastElement, `, "${REQUIRED_KEYWORD}"`);
72
+ },
73
+ });
74
+ }
75
+ },
76
+ };
77
+ },
78
+ });
@@ -0,0 +1,171 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { RequireNodeDescriptionFieldsRule } from './require-node-description-fields.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ const nodeFilePath = '/tmp/TestNode.node.ts';
8
+ const nonNodeFilePath = '/tmp/SomeHelper.ts';
9
+
10
+ function createFullNode(): string {
11
+ return `
12
+ import type { INodeType } from 'n8n-workflow';
13
+
14
+ export class TestNode implements INodeType {
15
+ description = {
16
+ displayName: 'Test Node',
17
+ name: 'testNode',
18
+ icon: 'file:testNode.svg',
19
+ group: ['transform'],
20
+ version: 1,
21
+ description: 'Test node description',
22
+ subtitle: '={{$parameter["operation"]}}',
23
+ inputs: ['main'],
24
+ outputs: ['main'],
25
+ properties: [],
26
+ };
27
+ }`;
28
+ }
29
+
30
+ function createNodeMissingSubtitle(): string {
31
+ return `
32
+ import type { INodeType } from 'n8n-workflow';
33
+
34
+ export class TestNode implements INodeType {
35
+ description = {
36
+ displayName: 'Test Node',
37
+ name: 'testNode',
38
+ icon: 'file:testNode.svg',
39
+ group: ['transform'],
40
+ version: 1,
41
+ description: 'Test node description',
42
+ inputs: ['main'],
43
+ outputs: ['main'],
44
+ properties: [],
45
+ };
46
+ }`;
47
+ }
48
+
49
+ function createNodeMissingIconAndSubtitle(): string {
50
+ return `
51
+ import type { INodeType } from 'n8n-workflow';
52
+
53
+ export class TestNode implements INodeType {
54
+ description = {
55
+ displayName: 'Test Node',
56
+ name: 'testNode',
57
+ group: ['transform'],
58
+ version: 1,
59
+ description: 'Test node description',
60
+ inputs: ['main'],
61
+ outputs: ['main'],
62
+ properties: [],
63
+ };
64
+ }`;
65
+ }
66
+
67
+ function createNodeExtendsBase(): string {
68
+ return `
69
+ export class TestNode extends Node {
70
+ description = {
71
+ displayName: 'Test Node',
72
+ name: 'testNode',
73
+ icon: 'file:testNode.svg',
74
+ group: ['transform'],
75
+ version: 1,
76
+ description: 'Test node description',
77
+ subtitle: '={{$parameter["operation"]}}',
78
+ inputs: ['main'],
79
+ outputs: ['main'],
80
+ properties: [],
81
+ };
82
+ }`;
83
+ }
84
+
85
+ function createNodeExtendsBaseMissingSubtitle(): string {
86
+ return `
87
+ export class TestNode extends Node {
88
+ description = {
89
+ displayName: 'Test Node',
90
+ name: 'testNode',
91
+ icon: 'file:testNode.svg',
92
+ group: ['transform'],
93
+ version: 1,
94
+ description: 'Test node description',
95
+ inputs: ['main'],
96
+ outputs: ['main'],
97
+ properties: [],
98
+ };
99
+ }`;
100
+ }
101
+
102
+ function createNodeWithOnlyIconAndSubtitle(): string {
103
+ return `
104
+ import type { INodeType } from 'n8n-workflow';
105
+
106
+ export class TestNode implements INodeType {
107
+ description = {
108
+ icon: 'file:testNode.svg',
109
+ subtitle: '={{$parameter["operation"]}}',
110
+ };
111
+ }`;
112
+ }
113
+
114
+ function createRegularClass(): string {
115
+ return `
116
+ export class RegularClass {
117
+ description = {
118
+ displayName: 'Test',
119
+ };
120
+ }`;
121
+ }
122
+
123
+ ruleTester.run('require-node-description-fields', RequireNodeDescriptionFieldsRule, {
124
+ valid: [
125
+ {
126
+ name: 'node with all required fields',
127
+ filename: nodeFilePath,
128
+ code: createFullNode(),
129
+ },
130
+ {
131
+ name: 'class extending Node with all required fields',
132
+ filename: nodeFilePath,
133
+ code: createNodeExtendsBase(),
134
+ },
135
+ {
136
+ name: 'node with only icon and subtitle passes (other fields enforced by tsc)',
137
+ filename: nodeFilePath,
138
+ code: createNodeWithOnlyIconAndSubtitle(),
139
+ },
140
+ {
141
+ name: 'class not implementing INodeType is ignored',
142
+ filename: nodeFilePath,
143
+ code: createRegularClass(),
144
+ },
145
+ {
146
+ name: 'non-.node.ts file is ignored',
147
+ filename: nonNodeFilePath,
148
+ code: createNodeMissingSubtitle(),
149
+ },
150
+ ],
151
+ invalid: [
152
+ {
153
+ name: 'node missing subtitle',
154
+ filename: nodeFilePath,
155
+ code: createNodeMissingSubtitle(),
156
+ errors: [{ messageId: 'missingField', data: { field: 'subtitle' } }],
157
+ },
158
+ {
159
+ name: 'node missing icon and subtitle',
160
+ filename: nodeFilePath,
161
+ code: createNodeMissingIconAndSubtitle(),
162
+ errors: [{ messageId: 'missingFields', data: { fields: '`icon`, `subtitle`' } }],
163
+ },
164
+ {
165
+ name: 'class extending Node missing subtitle',
166
+ filename: nodeFilePath,
167
+ code: createNodeExtendsBaseMissingSubtitle(),
168
+ errors: [{ messageId: 'missingField', data: { field: 'subtitle' } }],
169
+ },
170
+ ],
171
+ });
@@ -0,0 +1,77 @@
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
+ // Fields that are optional in the TypeScript interface but required by
12
+ // community node review standards. TypeScript-required fields (displayName,
13
+ // name, group, version, description, etc.) are already caught by tsc.
14
+ const REQUIRED_FIELDS = ['icon', 'subtitle'] as const;
15
+
16
+ type RequiredField = (typeof REQUIRED_FIELDS)[number];
17
+
18
+ export const RequireNodeDescriptionFieldsRule = createRule({
19
+ name: 'require-node-description-fields',
20
+ meta: {
21
+ type: 'problem',
22
+ docs: {
23
+ description: `Node class description must define all required fields: ${REQUIRED_FIELDS.join(', ')}`,
24
+ },
25
+ messages: {
26
+ missingField: 'Node class description is missing required `{{field}}` property',
27
+ missingFields: 'Node class description is missing required properties: {{fields}}',
28
+ },
29
+ schema: [],
30
+ },
31
+ defaultOptions: [],
32
+ create(context) {
33
+ if (!isFileType(context.filename, '.node.ts')) {
34
+ return {};
35
+ }
36
+
37
+ return {
38
+ ClassDeclaration(node) {
39
+ if (!isNodeTypeClass(node)) {
40
+ return;
41
+ }
42
+
43
+ const descriptionProperty = findClassProperty(node, 'description');
44
+ if (
45
+ !descriptionProperty?.value ||
46
+ descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression
47
+ ) {
48
+ return;
49
+ }
50
+
51
+ const descriptionValue = descriptionProperty.value;
52
+
53
+ const missingFields: RequiredField[] = REQUIRED_FIELDS.filter(
54
+ (field) => !findObjectProperty(descriptionValue, field),
55
+ );
56
+
57
+ if (missingFields.length === 0) {
58
+ return;
59
+ }
60
+
61
+ if (missingFields.length === 1) {
62
+ context.report({
63
+ node: descriptionProperty,
64
+ messageId: 'missingField',
65
+ data: { field: missingFields[0] },
66
+ });
67
+ } else {
68
+ context.report({
69
+ node: descriptionProperty,
70
+ messageId: 'missingFields',
71
+ data: { fields: missingFields.map((f) => `\`${f}\``).join(', ') },
72
+ });
73
+ }
74
+ },
75
+ };
76
+ },
77
+ });