@n8n/eslint-plugin-community-nodes 0.21.0 → 0.23.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 (67) hide show
  1. package/.turbo/turbo-build$colon$unchecked.log +1 -1
  2. package/README.md +52 -46
  3. package/dist/plugin.d.ts +36 -0
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +12 -0
  6. package/dist/plugin.js.map +1 -1
  7. package/dist/rules/ai-node-package-json.d.ts.map +1 -1
  8. package/dist/rules/ai-node-package-json.js +4 -3
  9. package/dist/rules/ai-node-package-json.js.map +1 -1
  10. package/dist/rules/index.d.ts +6 -0
  11. package/dist/rules/index.d.ts.map +1 -1
  12. package/dist/rules/index.js +12 -0
  13. package/dist/rules/index.js.map +1 -1
  14. package/dist/rules/no-asterisk-in-option-names.d.ts +2 -0
  15. package/dist/rules/no-asterisk-in-option-names.d.ts.map +1 -0
  16. package/dist/rules/no-asterisk-in-option-names.js +90 -0
  17. package/dist/rules/no-asterisk-in-option-names.js.map +1 -0
  18. package/dist/rules/no-duplicate-param-options.d.ts +2 -0
  19. package/dist/rules/no-duplicate-param-options.d.ts.map +1 -0
  20. package/dist/rules/no-duplicate-param-options.js +97 -0
  21. package/dist/rules/no-duplicate-param-options.js.map +1 -0
  22. package/dist/rules/node-class-description-name-camelcase.d.ts +2 -0
  23. package/dist/rules/node-class-description-name-camelcase.d.ts.map +1 -0
  24. package/dist/rules/node-class-description-name-camelcase.js +92 -0
  25. package/dist/rules/node-class-description-name-camelcase.js.map +1 -0
  26. package/dist/rules/require-mit-license.d.ts +2 -0
  27. package/dist/rules/require-mit-license.d.ts.map +1 -0
  28. package/dist/rules/require-mit-license.js +57 -0
  29. package/dist/rules/require-mit-license.js.map +1 -0
  30. package/dist/rules/require-param-default.d.ts +15 -0
  31. package/dist/rules/require-param-default.d.ts.map +1 -0
  32. package/dist/rules/require-param-default.js +75 -0
  33. package/dist/rules/require-param-default.js.map +1 -0
  34. package/dist/rules/trigger-node-conventions.d.ts +2 -0
  35. package/dist/rules/trigger-node-conventions.d.ts.map +1 -0
  36. package/dist/rules/trigger-node-conventions.js +70 -0
  37. package/dist/rules/trigger-node-conventions.js.map +1 -0
  38. package/dist/rules/valid-peer-dependencies.d.ts.map +1 -1
  39. package/dist/rules/valid-peer-dependencies.js +5 -3
  40. package/dist/rules/valid-peer-dependencies.js.map +1 -1
  41. package/dist/typecheck.tsbuildinfo +1 -1
  42. package/docs/rules/no-asterisk-in-option-names.md +61 -0
  43. package/docs/rules/no-duplicate-param-options.md +46 -0
  44. package/docs/rules/node-class-description-name-camelcase.md +41 -0
  45. package/docs/rules/require-mit-license.md +38 -0
  46. package/docs/rules/require-param-default.md +60 -0
  47. package/docs/rules/trigger-node-conventions.md +49 -0
  48. package/docs/rules/valid-peer-dependencies.md +1 -1
  49. package/package.json +6 -5
  50. package/src/plugin.ts +12 -0
  51. package/src/rules/ai-node-package-json.test.ts +5 -0
  52. package/src/rules/ai-node-package-json.ts +4 -3
  53. package/src/rules/index.ts +12 -0
  54. package/src/rules/no-asterisk-in-option-names.test.ts +185 -0
  55. package/src/rules/no-asterisk-in-option-names.ts +111 -0
  56. package/src/rules/no-duplicate-param-options.test.ts +125 -0
  57. package/src/rules/no-duplicate-param-options.ts +125 -0
  58. package/src/rules/node-class-description-name-camelcase.test.ts +121 -0
  59. package/src/rules/node-class-description-name-camelcase.ts +114 -0
  60. package/src/rules/require-mit-license.test.ts +67 -0
  61. package/src/rules/require-mit-license.ts +65 -0
  62. package/src/rules/require-param-default.test.ts +227 -0
  63. package/src/rules/require-param-default.ts +89 -0
  64. package/src/rules/trigger-node-conventions.test.ts +110 -0
  65. package/src/rules/trigger-node-conventions.ts +92 -0
  66. package/src/rules/valid-peer-dependencies.test.ts +5 -0
  67. package/src/rules/valid-peer-dependencies.ts +5 -3
@@ -0,0 +1,114 @@
1
+ import { TSESTree } from '@typescript-eslint/utils';
2
+
3
+ import {
4
+ isNodeTypeClass,
5
+ findClassProperty,
6
+ findObjectProperty,
7
+ getStringLiteralValue,
8
+ isFileType,
9
+ createRule,
10
+ } from '../utils/index.js';
11
+
12
+ // `description.name` must be camelCase: a lowercase letter followed by
13
+ // letters/digits, with no separators.
14
+ const CAMEL_CASE_PATTERN = /^[a-z][a-zA-Z0-9]*$/;
15
+
16
+ /**
17
+ * Converts an arbitrary node name to camelCase by splitting on any
18
+ * non-alphanumeric separators, upper-casing the first character of each
19
+ * subsequent segment, and lower-casing the very first character.
20
+ * Examples: `My Node` -> `myNode`, `my-node` -> `myNode`, `GitHub` -> `gitHub`.
21
+ */
22
+ function toCamelCase(value: string): string {
23
+ const segments = value.split(/[^a-zA-Z0-9]+/).filter(Boolean);
24
+ if (segments.length === 0) {
25
+ return value;
26
+ }
27
+
28
+ const joined = segments
29
+ .map((segment, index) =>
30
+ index === 0 ? segment : segment.charAt(0).toUpperCase() + segment.slice(1),
31
+ )
32
+ .join('');
33
+
34
+ return joined.charAt(0).toLowerCase() + joined.slice(1);
35
+ }
36
+
37
+ // Serialize a value as a string literal using the original quote character,
38
+ // escaping any characters that would otherwise break the literal so the
39
+ // autofix never emits invalid code.
40
+ function toStringLiteral(value: string, quote: string): string {
41
+ const escaped = value
42
+ .replace(/\\/g, '\\\\')
43
+ .replace(/\n/g, '\\n')
44
+ .replace(/\r/g, '\\r')
45
+ .split(quote)
46
+ .join(`\\${quote}`);
47
+ return `${quote}${escaped}${quote}`;
48
+ }
49
+
50
+ export const NodeClassDescriptionNameCamelCaseRule = createRule({
51
+ name: 'node-class-description-name-camelcase',
52
+ meta: {
53
+ type: 'problem',
54
+ docs: {
55
+ description: 'Node class `description.name` must be camelCase',
56
+ },
57
+ messages: {
58
+ notCamelCase: "Node `description.name` '{{value}}' must be camelCase",
59
+ },
60
+ fixable: 'code',
61
+ schema: [],
62
+ },
63
+ defaultOptions: [],
64
+ create(context) {
65
+ if (!isFileType(context.filename, '.node.ts')) {
66
+ return {};
67
+ }
68
+
69
+ return {
70
+ ClassDeclaration(node) {
71
+ if (!isNodeTypeClass(node)) {
72
+ return;
73
+ }
74
+
75
+ const descriptionProperty = findClassProperty(node, 'description');
76
+ if (descriptionProperty?.value?.type !== TSESTree.AST_NODE_TYPES.ObjectExpression) {
77
+ return;
78
+ }
79
+
80
+ const nameProperty = findObjectProperty(descriptionProperty.value, 'name');
81
+ if (!nameProperty) {
82
+ return;
83
+ }
84
+
85
+ const nameValue = getStringLiteralValue(nameProperty.value);
86
+ if (nameValue === null) {
87
+ return;
88
+ }
89
+
90
+ if (CAMEL_CASE_PATTERN.test(nameValue)) {
91
+ return;
92
+ }
93
+
94
+ const valueNode = nameProperty.value;
95
+ const fixedValue = toCamelCase(nameValue);
96
+ // Only offer an autofix when it actually yields a valid camelCase
97
+ // value (e.g. names starting with a digit cannot be repaired).
98
+ const canFix = fixedValue !== nameValue && CAMEL_CASE_PATTERN.test(fixedValue);
99
+
100
+ context.report({
101
+ node: valueNode,
102
+ messageId: 'notCamelCase',
103
+ data: { value: nameValue },
104
+ fix: canFix
105
+ ? (fixer) => {
106
+ const quote = context.sourceCode.getText(valueNode).charAt(0);
107
+ return fixer.replaceText(valueNode, toStringLiteral(fixedValue, quote));
108
+ }
109
+ : undefined,
110
+ });
111
+ },
112
+ };
113
+ },
114
+ });
@@ -0,0 +1,67 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { RequireMitLicenseRule } from './require-mit-license.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ ruleTester.run('require-mit-license', RequireMitLicenseRule, {
8
+ valid: [
9
+ {
10
+ name: 'license is MIT',
11
+ filename: 'package.json',
12
+ code: '{ "name": "n8n-nodes-example", "license": "MIT" }',
13
+ },
14
+ {
15
+ name: 'non-package.json file is ignored',
16
+ filename: 'some-config.json',
17
+ code: '{ "name": "n8n-nodes-example", "license": "Apache-2.0" }',
18
+ },
19
+ {
20
+ name: 'nested objects are not checked',
21
+ filename: 'package.json',
22
+ code: '{ "name": "n8n-nodes-example", "license": "MIT", "config": { "license": "Apache-2.0" } }',
23
+ },
24
+ {
25
+ name: 'objects inside arrays are not flagged',
26
+ filename: 'package.json',
27
+ code: `{
28
+ "name": "n8n-nodes-example",
29
+ "license": "MIT",
30
+ "contributors": [
31
+ { "name": "Alice" },
32
+ { "name": "Bob" }
33
+ ]
34
+ }`,
35
+ },
36
+ ],
37
+ invalid: [
38
+ {
39
+ name: 'missing license field',
40
+ filename: 'package.json',
41
+ code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
42
+ output: '{ "name": "n8n-nodes-example", "version": "1.0.0", "license": "MIT" }',
43
+ errors: [{ messageId: 'missingLicense' }],
44
+ },
45
+ {
46
+ name: 'empty package.json object',
47
+ filename: 'package.json',
48
+ code: '{}',
49
+ output: '{ "license": "MIT" }',
50
+ errors: [{ messageId: 'missingLicense' }],
51
+ },
52
+ {
53
+ name: 'wrong license value',
54
+ filename: 'package.json',
55
+ code: '{ "name": "n8n-nodes-example", "license": "Apache-2.0" }',
56
+ output: '{ "name": "n8n-nodes-example", "license": "MIT" }',
57
+ errors: [{ messageId: 'wrongLicense' }],
58
+ },
59
+ {
60
+ name: 'case-mismatched license value',
61
+ filename: 'package.json',
62
+ code: '{ "name": "n8n-nodes-example", "license": "mit" }',
63
+ output: '{ "name": "n8n-nodes-example", "license": "MIT" }',
64
+ errors: [{ messageId: 'wrongLicense' }],
65
+ },
66
+ ],
67
+ });
@@ -0,0 +1,65 @@
1
+ import type { TSESTree } from '@typescript-eslint/utils';
2
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
3
+
4
+ import { createRule, findJsonProperty, getStringLiteralValue } from '../utils/index.js';
5
+
6
+ const REQUIRED_LICENSE = 'MIT';
7
+
8
+ export const RequireMitLicenseRule = createRule({
9
+ name: 'require-mit-license',
10
+ meta: {
11
+ type: 'problem',
12
+ docs: {
13
+ description: 'Require the "license" field in community node package.json to be "MIT"',
14
+ },
15
+ fixable: 'code',
16
+ messages: {
17
+ missingLicense: `The package.json must have a "license" field set to "${REQUIRED_LICENSE}".`,
18
+ wrongLicense: `The "license" field must be "${REQUIRED_LICENSE}".`,
19
+ },
20
+ schema: [],
21
+ },
22
+ defaultOptions: [],
23
+ create(context) {
24
+ if (!context.filename.endsWith('package.json')) {
25
+ return {};
26
+ }
27
+
28
+ return {
29
+ ObjectExpression(node: TSESTree.ObjectExpression) {
30
+ if (node.parent?.type !== AST_NODE_TYPES.ExpressionStatement) {
31
+ return;
32
+ }
33
+
34
+ const licenseProp = findJsonProperty(node, 'license');
35
+
36
+ if (!licenseProp) {
37
+ context.report({
38
+ node,
39
+ messageId: 'missingLicense',
40
+ fix(fixer) {
41
+ const lastProp = node.properties[node.properties.length - 1];
42
+ if (!lastProp) {
43
+ return fixer.replaceText(node, `{ "license": "${REQUIRED_LICENSE}" }`);
44
+ }
45
+ return fixer.insertTextAfter(lastProp, `, "license": "${REQUIRED_LICENSE}"`);
46
+ },
47
+ });
48
+ return;
49
+ }
50
+
51
+ if (getStringLiteralValue(licenseProp.value) === REQUIRED_LICENSE) {
52
+ return;
53
+ }
54
+
55
+ context.report({
56
+ node: licenseProp.value,
57
+ messageId: 'wrongLicense',
58
+ fix(fixer) {
59
+ return fixer.replaceText(licenseProp.value, `"${REQUIRED_LICENSE}"`);
60
+ },
61
+ });
62
+ },
63
+ };
64
+ },
65
+ });
@@ -0,0 +1,227 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { RequireParamDefaultRule } from './require-param-default.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ function createNodeCode(properties: string): string {
8
+ return `
9
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
10
+
11
+ export class TestNode implements INodeType {
12
+ description: INodeTypeDescription = {
13
+ displayName: 'Test Node',
14
+ name: 'testNode',
15
+ group: ['input'],
16
+ version: 1,
17
+ description: 'A test node',
18
+ defaults: { name: 'Test Node' },
19
+ inputs: [],
20
+ outputs: [],
21
+ properties: [
22
+ ${properties}
23
+ ],
24
+ };
25
+ }
26
+ `;
27
+ }
28
+
29
+ ruleTester.run('require-param-default', RequireParamDefaultRule, {
30
+ valid: [
31
+ {
32
+ name: 'class that does not implement INodeType',
33
+ filename: '/tmp/TestNode.node.ts',
34
+ code: `
35
+ export class NotANode {
36
+ description = {
37
+ properties: [{ displayName: 'Field', name: 'field', type: 'string' }],
38
+ };
39
+ }
40
+ `,
41
+ },
42
+ {
43
+ name: 'param-shaped object outside description (e.g. in a method) is ignored',
44
+ filename: '/tmp/TestNode.node.ts',
45
+ code: `
46
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
47
+
48
+ export class TestNode implements INodeType {
49
+ description: INodeTypeDescription = {
50
+ displayName: 'Test Node',
51
+ name: 'testNode',
52
+ properties: [],
53
+ };
54
+
55
+ methods = {
56
+ loadOptions: {
57
+ async getThings() {
58
+ const config = { displayName: 'X', name: 'x', type: 'string' };
59
+ return [config];
60
+ },
61
+ },
62
+ };
63
+ }
64
+ `,
65
+ },
66
+ {
67
+ name: 'non .node.ts file is ignored',
68
+ filename: '/tmp/helper.ts',
69
+ code: `
70
+ export class TestNode {
71
+ description = {
72
+ properties: [{ displayName: 'Field', name: 'field', type: 'string' }],
73
+ };
74
+ }
75
+ `,
76
+ },
77
+ {
78
+ name: 'string parameter with default',
79
+ filename: '/tmp/TestNode.node.ts',
80
+ code: createNodeCode(`
81
+ {
82
+ displayName: 'Field',
83
+ name: 'field',
84
+ type: 'string',
85
+ default: '',
86
+ },
87
+ `),
88
+ },
89
+ {
90
+ name: 'options parameter with default',
91
+ filename: '/tmp/TestNode.node.ts',
92
+ code: createNodeCode(`
93
+ {
94
+ displayName: 'Operation',
95
+ name: 'operation',
96
+ type: 'options',
97
+ options: [{ name: 'Create', value: 'create' }],
98
+ default: 'create',
99
+ },
100
+ `),
101
+ },
102
+ {
103
+ name: 'notice parameter with empty default',
104
+ filename: '/tmp/TestNode.node.ts',
105
+ code: createNodeCode(`
106
+ {
107
+ displayName: 'Some notice',
108
+ name: 'notice',
109
+ type: 'notice',
110
+ default: '',
111
+ },
112
+ `),
113
+ },
114
+ {
115
+ name: 'options-array entries (name/value, no displayName) are not parameters',
116
+ filename: '/tmp/TestNode.node.ts',
117
+ code: createNodeCode(`
118
+ {
119
+ displayName: 'Operation',
120
+ name: 'operation',
121
+ type: 'options',
122
+ options: [{ name: 'Create', value: 'create', type: 'whatever' }],
123
+ default: 'create',
124
+ },
125
+ `),
126
+ },
127
+ {
128
+ name: 'object with name and type but no displayName is not a parameter',
129
+ filename: '/tmp/TestNode.node.ts',
130
+ code: createNodeCode(`
131
+ {
132
+ displayName: 'Routing',
133
+ name: 'routing',
134
+ type: 'string',
135
+ default: '',
136
+ routing: { request: { method: 'GET', name: 'x', type: 'y' } },
137
+ },
138
+ `),
139
+ },
140
+ {
141
+ name: 'nested collection options all have defaults',
142
+ filename: '/tmp/TestNode.node.ts',
143
+ code: createNodeCode(`
144
+ {
145
+ displayName: 'Additional Fields',
146
+ name: 'additionalFields',
147
+ type: 'collection',
148
+ default: {},
149
+ options: [
150
+ {
151
+ displayName: 'Limit',
152
+ name: 'limit',
153
+ type: 'number',
154
+ default: 50,
155
+ },
156
+ ],
157
+ },
158
+ `),
159
+ },
160
+ ],
161
+ invalid: [
162
+ {
163
+ name: 'string parameter missing default',
164
+ filename: '/tmp/TestNode.node.ts',
165
+ code: createNodeCode(`
166
+ {
167
+ displayName: 'Field',
168
+ name: 'field',
169
+ type: 'string',
170
+ },
171
+ `),
172
+ errors: [{ messageId: 'missingDefault', data: { name: 'field' } }],
173
+ },
174
+ {
175
+ name: 'notice parameter missing default',
176
+ filename: '/tmp/TestNode.node.ts',
177
+ code: createNodeCode(`
178
+ {
179
+ displayName: 'Some notice',
180
+ name: 'notice',
181
+ type: 'notice',
182
+ },
183
+ `),
184
+ errors: [{ messageId: 'missingDefault', data: { name: 'notice' } }],
185
+ },
186
+ {
187
+ name: 'nested collection option missing default',
188
+ filename: '/tmp/TestNode.node.ts',
189
+ code: createNodeCode(`
190
+ {
191
+ displayName: 'Additional Fields',
192
+ name: 'additionalFields',
193
+ type: 'collection',
194
+ default: {},
195
+ options: [
196
+ {
197
+ displayName: 'Limit',
198
+ name: 'limit',
199
+ type: 'number',
200
+ },
201
+ ],
202
+ },
203
+ `),
204
+ errors: [{ messageId: 'missingDefault', data: { name: 'limit' } }],
205
+ },
206
+ {
207
+ name: 'multiple parameters missing defaults',
208
+ filename: '/tmp/TestNode.node.ts',
209
+ code: createNodeCode(`
210
+ {
211
+ displayName: 'First',
212
+ name: 'first',
213
+ type: 'string',
214
+ },
215
+ {
216
+ displayName: 'Second',
217
+ name: 'second',
218
+ type: 'boolean',
219
+ },
220
+ `),
221
+ errors: [
222
+ { messageId: 'missingDefault', data: { name: 'first' } },
223
+ { messageId: 'missingDefault', data: { name: 'second' } },
224
+ ],
225
+ },
226
+ ],
227
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Requires every node parameter to declare a `default` property.
3
+ *
4
+ * A parameter is detected structurally: an object literal inside a node's
5
+ * `description` with `displayName`, `name`, and `type` all set to string
6
+ * literals — the shape every `INodeProperties` entry has. This avoids
7
+ * hardcoding the `NodePropertyTypes` union (which would silently go stale as
8
+ * new types are added) and naturally excludes `options`-array entries, which
9
+ * carry `name`/`value` but no `displayName`/`type`.
10
+ *
11
+ * Without a `default`, n8n cannot reliably initialise the parameter's value,
12
+ * which leads to inconsistent runtime behaviour in the editor and on execution.
13
+ */
14
+
15
+ import { simpleTraverse } from '@typescript-eslint/typescript-estree';
16
+ import type { TSESTree } from '@typescript-eslint/utils';
17
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
18
+
19
+ import {
20
+ createRule,
21
+ findClassProperty,
22
+ findObjectProperty,
23
+ getStringLiteralValue,
24
+ isFileType,
25
+ isNodeTypeClass,
26
+ } from '../utils/index.js';
27
+
28
+ /** Property keys an `INodeProperties` parameter always carries as string literals. */
29
+ const REQUIRED_PARAM_KEYS = ['displayName', 'name', 'type'] as const;
30
+
31
+ /** Returns true when the object literal has the shape of a node parameter. */
32
+ function isNodeParameter(node: TSESTree.ObjectExpression): boolean {
33
+ return REQUIRED_PARAM_KEYS.every((key) => {
34
+ const property = findObjectProperty(node, key);
35
+ return property !== null && getStringLiteralValue(property.value) !== null;
36
+ });
37
+ }
38
+
39
+ export const RequireParamDefaultRule = createRule({
40
+ name: 'require-param-default',
41
+ meta: {
42
+ type: 'problem',
43
+ docs: {
44
+ description: 'Require every node parameter to declare a default value.',
45
+ },
46
+ messages: {
47
+ missingDefault:
48
+ "Node parameter '{{ name }}' is missing a `default` value. Add a `default` property so the parameter initialises consistently.",
49
+ },
50
+ schema: [],
51
+ },
52
+ defaultOptions: [],
53
+ create(context) {
54
+ if (!isFileType(context.filename, '.node.ts')) {
55
+ return {};
56
+ }
57
+
58
+ const reportIfMissingDefault = (node: TSESTree.ObjectExpression): void => {
59
+ if (!isNodeParameter(node) || findObjectProperty(node, 'default') !== null) {
60
+ return;
61
+ }
62
+ const nameProperty = findObjectProperty(node, 'name');
63
+ context.report({
64
+ node,
65
+ messageId: 'missingDefault',
66
+ data: { name: getStringLiteralValue(nameProperty?.value ?? null) ?? '' },
67
+ });
68
+ };
69
+
70
+ return {
71
+ // Scope to an INodeType class's `description`, then walk only that
72
+ // subtree, so helper classes and object literals elsewhere in the file
73
+ // (e.g. in method bodies) that share the parameter shape are ignored.
74
+ ClassDeclaration(node) {
75
+ if (!isNodeTypeClass(node)) return;
76
+ const description = findClassProperty(node, 'description');
77
+ if (description?.value?.type !== AST_NODE_TYPES.ObjectExpression) return;
78
+
79
+ simpleTraverse(description.value, {
80
+ enter(child) {
81
+ if (child.type === AST_NODE_TYPES.ObjectExpression) {
82
+ reportIfMissingDefault(child);
83
+ }
84
+ },
85
+ });
86
+ },
87
+ };
88
+ },
89
+ });
@@ -0,0 +1,110 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { TriggerNodeConventionsRule } from './trigger-node-conventions.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ function createNodeCode(options: {
8
+ className: string;
9
+ name: string;
10
+ displayName: string;
11
+ inputs: string;
12
+ }): string {
13
+ const { className, name, displayName, inputs } = options;
14
+ return `
15
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
16
+
17
+ export class ${className} implements INodeType {
18
+ description: INodeTypeDescription = {
19
+ displayName: '${displayName}',
20
+ name: '${name}',
21
+ group: ['trigger'],
22
+ version: 1,
23
+ description: 'A test node',
24
+ defaults: { name: '${displayName}' },
25
+ inputs: ${inputs},
26
+ outputs: ['main'],
27
+ properties: [],
28
+ };
29
+ }`;
30
+ }
31
+
32
+ ruleTester.run('trigger-node-conventions', TriggerNodeConventionsRule, {
33
+ valid: [
34
+ {
35
+ name: 'class that does not implement INodeType',
36
+ filename: 'MyTrigger.node.ts',
37
+ code: 'export class MyTrigger {}',
38
+ },
39
+ {
40
+ name: 'non-trigger node class is ignored',
41
+ filename: 'My.node.ts',
42
+ code: createNodeCode({
43
+ className: 'My',
44
+ name: 'my',
45
+ displayName: 'My Node',
46
+ inputs: "['main']",
47
+ }),
48
+ },
49
+ {
50
+ name: 'compliant trigger node',
51
+ filename: 'MyTrigger.node.ts',
52
+ code: createNodeCode({
53
+ className: 'MyTrigger',
54
+ name: 'myTrigger',
55
+ displayName: 'My Trigger',
56
+ inputs: '[]',
57
+ }),
58
+ },
59
+ ],
60
+ invalid: [
61
+ {
62
+ name: 'name missing Trigger suffix',
63
+ filename: 'MyTrigger.node.ts',
64
+ code: createNodeCode({
65
+ className: 'MyTrigger',
66
+ name: 'my',
67
+ displayName: 'My Trigger',
68
+ inputs: '[]',
69
+ }),
70
+ errors: [{ messageId: 'nameMissingSuffix', data: { value: 'my' } }],
71
+ },
72
+ {
73
+ name: 'displayName missing Trigger',
74
+ filename: 'MyTrigger.node.ts',
75
+ code: createNodeCode({
76
+ className: 'MyTrigger',
77
+ name: 'myTrigger',
78
+ displayName: 'My Node',
79
+ inputs: '[]',
80
+ }),
81
+ errors: [{ messageId: 'displayNameMissingTrigger', data: { value: 'My Node' } }],
82
+ },
83
+ {
84
+ name: 'inputs is not empty',
85
+ filename: 'MyTrigger.node.ts',
86
+ code: createNodeCode({
87
+ className: 'MyTrigger',
88
+ name: 'myTrigger',
89
+ displayName: 'My Trigger',
90
+ inputs: "['main']",
91
+ }),
92
+ errors: [{ messageId: 'inputsNotEmpty' }],
93
+ },
94
+ {
95
+ name: 'all three violations at once',
96
+ filename: 'MyTrigger.node.ts',
97
+ code: createNodeCode({
98
+ className: 'MyTrigger',
99
+ name: 'my',
100
+ displayName: 'My Node',
101
+ inputs: "['main']",
102
+ }),
103
+ errors: [
104
+ { messageId: 'displayNameMissingTrigger' },
105
+ { messageId: 'nameMissingSuffix' },
106
+ { messageId: 'inputsNotEmpty' },
107
+ ],
108
+ },
109
+ ],
110
+ });