@n8n/eslint-plugin-community-nodes 0.34.0 → 0.35.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/README.md CHANGED
@@ -88,6 +88,7 @@ export default [
88
88
  | [valid-author](docs/rules/valid-author.md) | Require a non-empty author name and email in package.json | ✅ ☑️ | | | | |
89
89
  | [valid-credential-references](docs/rules/valid-credential-references.md) | Ensure credentials referenced in node descriptions exist as credential classes in the package | ✅ ☑️ | | | 💡 | |
90
90
  | [valid-description](docs/rules/valid-description.md) | Require a non-empty "description" field in community node package.json | ✅ ☑️ | | | | |
91
+ | [valid-node-categories](docs/rules/valid-node-categories.md) | Require supported community node categories in .node.json codex files | ✅ ☑️ | | | | |
91
92
  | [valid-peer-dependencies](docs/rules/valid-peer-dependencies.md) | Require community node package.json peerDependencies to contain only "n8n-workflow": "*" (and optionally "@n8n/ai-node-sdk") | ✅ ☑️ | | 🔧 | | |
92
93
  | [webhook-lifecycle-complete](docs/rules/webhook-lifecycle-complete.md) | Require webhook trigger nodes to implement the complete webhookMethods lifecycle (checkExists, create, delete) | ✅ ☑️ | | | | |
93
94
 
package/dist/plugin.d.ts CHANGED
@@ -55,6 +55,7 @@ declare const configs: {
55
55
  '@n8n/community-nodes/valid-author': "error";
56
56
  '@n8n/community-nodes/valid-credential-references': "error";
57
57
  '@n8n/community-nodes/valid-description': "error";
58
+ '@n8n/community-nodes/valid-node-categories': "error";
58
59
  '@n8n/community-nodes/valid-peer-dependencies': "error";
59
60
  '@n8n/community-nodes/webhook-lifecycle-complete': "error";
60
61
  };
@@ -111,6 +112,7 @@ declare const configs: {
111
112
  '@n8n/community-nodes/valid-author': "error";
112
113
  '@n8n/community-nodes/valid-credential-references': "error";
113
114
  '@n8n/community-nodes/valid-description': "error";
115
+ '@n8n/community-nodes/valid-node-categories': "error";
114
116
  '@n8n/community-nodes/valid-peer-dependencies': "error";
115
117
  '@n8n/community-nodes/webhook-lifecycle-complete': "error";
116
118
  };
@@ -178,6 +180,7 @@ declare const pluginWithConfigs: {
178
180
  '@n8n/community-nodes/valid-author': "error";
179
181
  '@n8n/community-nodes/valid-credential-references': "error";
180
182
  '@n8n/community-nodes/valid-description': "error";
183
+ '@n8n/community-nodes/valid-node-categories': "error";
181
184
  '@n8n/community-nodes/valid-peer-dependencies': "error";
182
185
  '@n8n/community-nodes/webhook-lifecycle-complete': "error";
183
186
  };
@@ -234,6 +237,7 @@ declare const pluginWithConfigs: {
234
237
  '@n8n/community-nodes/valid-author': "error";
235
238
  '@n8n/community-nodes/valid-credential-references': "error";
236
239
  '@n8n/community-nodes/valid-description': "error";
240
+ '@n8n/community-nodes/valid-node-categories': "error";
237
241
  '@n8n/community-nodes/valid-peer-dependencies': "error";
238
242
  '@n8n/community-nodes/webhook-lifecycle-complete': "error";
239
243
  };
@@ -302,6 +306,7 @@ declare const n8nCommunityNodesPlugin: {
302
306
  '@n8n/community-nodes/valid-author': "error";
303
307
  '@n8n/community-nodes/valid-credential-references': "error";
304
308
  '@n8n/community-nodes/valid-description': "error";
309
+ '@n8n/community-nodes/valid-node-categories': "error";
305
310
  '@n8n/community-nodes/valid-peer-dependencies': "error";
306
311
  '@n8n/community-nodes/webhook-lifecycle-complete': "error";
307
312
  };
@@ -358,6 +363,7 @@ declare const n8nCommunityNodesPlugin: {
358
363
  '@n8n/community-nodes/valid-author': "error";
359
364
  '@n8n/community-nodes/valid-credential-references': "error";
360
365
  '@n8n/community-nodes/valid-description': "error";
366
+ '@n8n/community-nodes/valid-node-categories': "error";
361
367
  '@n8n/community-nodes/valid-peer-dependencies': "error";
362
368
  '@n8n/community-nodes/webhook-lifecycle-complete': "error";
363
369
  };
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAG7C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAYzC,QAAA,MAAM,OAAO;;;;YAIV,sBAAsB;;oBAZvB,IAAI;oBACJ,OAAO;oBACP,SAAS;;uBAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YA0DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CgB,CAAC;AAE1C,QAAA,MAAM,iBAAiB;;QA/GrB,IAAI;QACJ,OAAO;QACP,SAAS;;WAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;gBAOpC,sBAAsB;;wBAZvB,IAAI;wBACJ,OAAO;wBACP,SAAS;;2BAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA0DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgD+C,CAAC;AAEzE,QAAA,MAAM,uBAAuB;;QAjH3B,IAAI;QACJ,OAAO;QACP,SAAS;;WAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;gBAOpC,sBAAsB;;wBAZvB,IAAI;wBACJ,OAAO;wBACP,SAAS;;2BAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA0DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkDwB,CAAC;eACnC,iBAAiB;AAChC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAG7C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAYzC,QAAA,MAAM,OAAO;;;;YAIV,sBAAsB;;oBAZvB,IAAI;oBACJ,OAAO;oBACP,SAAS;;uBAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YA2DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+CgB,CAAC;AAE1C,QAAA,MAAM,iBAAiB;;QAjHrB,IAAI;QACJ,OAAO;QACP,SAAS;;WAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;gBAOpC,sBAAsB;;wBAZvB,IAAI;wBACJ,OAAO;wBACP,SAAS;;2BAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA2DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiD+C,CAAC;AAEzE,QAAA,MAAM,uBAAuB;;QAnH3B,IAAI;QACJ,OAAO;QACP,SAAS;;WAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;gBAOpC,sBAAsB;;wBAZvB,IAAI;wBACJ,OAAO;wBACP,SAAS;;2BAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA2DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDwB,CAAC;eACnC,iBAAiB;AAChC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC"}
package/dist/plugin.js CHANGED
@@ -57,6 +57,7 @@ const configs = {
57
57
  '@n8n/community-nodes/valid-author': 'error',
58
58
  '@n8n/community-nodes/valid-credential-references': 'error',
59
59
  '@n8n/community-nodes/valid-description': 'error',
60
+ '@n8n/community-nodes/valid-node-categories': 'error',
60
61
  '@n8n/community-nodes/valid-peer-dependencies': 'error',
61
62
  '@n8n/community-nodes/webhook-lifecycle-complete': 'error',
62
63
  },
@@ -106,6 +107,7 @@ const configs = {
106
107
  '@n8n/community-nodes/valid-author': 'error',
107
108
  '@n8n/community-nodes/valid-credential-references': 'error',
108
109
  '@n8n/community-nodes/valid-description': 'error',
110
+ '@n8n/community-nodes/valid-node-categories': 'error',
109
111
  '@n8n/community-nodes/valid-peer-dependencies': 'error',
110
112
  '@n8n/community-nodes/webhook-lifecycle-complete': 'error',
111
113
  },
@@ -130,6 +130,9 @@ export declare const rules: {
130
130
  'valid-description': import("@typescript-eslint/utils/ts-eslint").RuleModule<"emptyDescription" | "missingDescription", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
131
131
  name: string;
132
132
  };
133
+ 'valid-node-categories': import("@typescript-eslint/utils/ts-eslint").RuleModule<"invalidCategories" | "invalidCategory" | "invalidCategoryType" | "marketingCategory", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
134
+ name: string;
135
+ };
133
136
  'valid-peer-dependencies': import("@typescript-eslint/utils/ts-eslint").RuleModule<"forbiddenPeerDependency" | "invalidPeerDependenciesType" | "missingN8nWorkflow" | "missingPeerDependencies" | "pinnedN8nWorkflow", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
134
137
  name: string;
135
138
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AA+CA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6CuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAgDA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CuB,CAAC"}
@@ -40,6 +40,7 @@ import { TriggerNodeConventionsRule } from './trigger-node-conventions.js';
40
40
  import { ValidAuthorRule } from './valid-author.js';
41
41
  import { ValidCredentialReferencesRule } from './valid-credential-references.js';
42
42
  import { ValidDescriptionRule } from './valid-description.js';
43
+ import { ValidNodeCategoriesRule } from './valid-node-categories.js';
43
44
  import { ValidPeerDependenciesRule } from './valid-peer-dependencies.js';
44
45
  import { WebhookLifecycleCompleteRule } from './webhook-lifecycle-complete.js';
45
46
  export const rules = {
@@ -85,6 +86,7 @@ export const rules = {
85
86
  'valid-author': ValidAuthorRule,
86
87
  'valid-credential-references': ValidCredentialReferencesRule,
87
88
  'valid-description': ValidDescriptionRule,
89
+ 'valid-node-categories': ValidNodeCategoriesRule,
88
90
  'valid-peer-dependencies': ValidPeerDependenciesRule,
89
91
  'webhook-lifecycle-complete': WebhookLifecycleCompleteRule,
90
92
  };
@@ -0,0 +1,4 @@
1
+ export declare const ValidNodeCategoriesRule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"invalidCategories" | "invalidCategory" | "invalidCategoryType" | "marketingCategory", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
2
+ name: string;
3
+ };
4
+ //# sourceMappingURL=valid-node-categories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"valid-node-categories.d.ts","sourceRoot":"","sources":["../../src/rules/valid-node-categories.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,uBAAuB;;CAqDlC,CAAC"}
@@ -0,0 +1,68 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import { createRule, findJsonProperty, getTopLevelObjectInJson } from '../utils/index.js';
3
+ const validCategories = new Set([
4
+ 'Data & Storage',
5
+ 'Finance & Accounting',
6
+ 'Marketing & Content',
7
+ 'Productivity',
8
+ 'Miscellaneous',
9
+ 'Sales',
10
+ 'Development',
11
+ 'Analytics',
12
+ 'Communication',
13
+ 'Utility',
14
+ ]);
15
+ export const ValidNodeCategoriesRule = createRule({
16
+ name: 'valid-node-categories',
17
+ meta: {
18
+ type: 'problem',
19
+ docs: {
20
+ description: 'Require supported community node categories in .node.json codex files',
21
+ },
22
+ messages: {
23
+ invalidCategories: '"categories" must be a non-empty array of community node categories.',
24
+ invalidCategoryType: 'Each category must be a string from the community node category list.',
25
+ invalidCategory: '"{{ category }}" is not a supported community node category. See https://docs.n8n.io/connect/create-nodes/build-your-node/reference/codex-files/#node-categories for the allowed values.',
26
+ marketingCategory: '"Marketing" is not a supported category. Use "Marketing & Content".',
27
+ },
28
+ schema: [],
29
+ },
30
+ defaultOptions: [],
31
+ create(context) {
32
+ if (!context.filename.endsWith('.node.json'))
33
+ return {};
34
+ return {
35
+ ObjectExpression(node) {
36
+ const root = getTopLevelObjectInJson(node);
37
+ if (!root)
38
+ return;
39
+ const categories = findJsonProperty(root, 'categories');
40
+ if (!categories)
41
+ return;
42
+ if (categories.value.type !== AST_NODE_TYPES.ArrayExpression ||
43
+ categories.value.elements.length === 0) {
44
+ context.report({ node: categories.value, messageId: 'invalidCategories' });
45
+ return;
46
+ }
47
+ for (const category of categories.value.elements) {
48
+ if (!category)
49
+ continue;
50
+ if (category.type !== AST_NODE_TYPES.Literal || typeof category.value !== 'string') {
51
+ context.report({ node: category, messageId: 'invalidCategoryType' });
52
+ }
53
+ else if (category.value === 'Marketing') {
54
+ context.report({ node: category, messageId: 'marketingCategory' });
55
+ }
56
+ else if (!validCategories.has(category.value)) {
57
+ context.report({
58
+ node: category,
59
+ messageId: 'invalidCategory',
60
+ data: { category: category.value },
61
+ });
62
+ }
63
+ }
64
+ },
65
+ };
66
+ },
67
+ });
68
+ //# sourceMappingURL=valid-node-categories.js.map
@@ -0,0 +1,27 @@
1
+ # Require supported community node categories in .node.json codex files (`@n8n/community-nodes/valid-node-categories`)
2
+
3
+ 💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
+
5
+ <!-- end auto-generated rule header -->
6
+
7
+ ## Rule Details
8
+
9
+ This rule checks `categories` in community node `.node.json` codex files. Each category must match a value in the [node category list](https://docs.n8n.io/connect/create-nodes/build-your-node/reference/codex-files/#node-categories). The rule also rejects empty arrays and values that are not strings. The `categories` field is optional.
10
+
11
+ ## Examples
12
+
13
+ ### ❌ Incorrect
14
+
15
+ ```json
16
+ {
17
+ "categories": ["Marketing", "Bananas"]
18
+ }
19
+ ```
20
+
21
+ ### ✅ Correct
22
+
23
+ ```json
24
+ {
25
+ "categories": ["Marketing & Content", "Development"]
26
+ }
27
+ ```
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.34.0",
4
+ "version": "0.35.0",
5
5
  "main": "./dist/plugin.js",
6
6
  "types": "./dist/plugin.d.ts",
7
7
  "exports": {
@@ -17,9 +17,9 @@
17
17
  },
18
18
  "devDependencies": {
19
19
  "@n8n/typescript-config": "1.12.0",
20
- "@n8n/vitest-config": "1.22.0",
20
+ "@n8n/vitest-config": "1.23.0",
21
21
  "@types/node": "24.10.1",
22
- "n8n-workflow": "2.41.0",
22
+ "n8n-workflow": "2.42.0",
23
23
  "@typescript-eslint/rule-tester": "8.70.0",
24
24
  "eslint-doc-generator": "^2.2.2",
25
25
  "eslint-plugin-eslint-plugin": "^7.0.0",
@@ -27,7 +27,7 @@
27
27
  "@typescript/native": "npm:typescript@7.0.2",
28
28
  "typescript": "npm:@typescript/typescript6@6.0.2",
29
29
  "vite": "^8.0.2",
30
- "vitest": "^4.1.9"
30
+ "vitest": "5.0.1"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "eslint": "9.29.0",
package/src/plugin.ts CHANGED
@@ -61,6 +61,7 @@ const configs = {
61
61
  '@n8n/community-nodes/valid-author': 'error',
62
62
  '@n8n/community-nodes/valid-credential-references': 'error',
63
63
  '@n8n/community-nodes/valid-description': 'error',
64
+ '@n8n/community-nodes/valid-node-categories': 'error',
64
65
  '@n8n/community-nodes/valid-peer-dependencies': 'error',
65
66
  '@n8n/community-nodes/webhook-lifecycle-complete': 'error',
66
67
  },
@@ -110,6 +111,7 @@ const configs = {
110
111
  '@n8n/community-nodes/valid-author': 'error',
111
112
  '@n8n/community-nodes/valid-credential-references': 'error',
112
113
  '@n8n/community-nodes/valid-description': 'error',
114
+ '@n8n/community-nodes/valid-node-categories': 'error',
113
115
  '@n8n/community-nodes/valid-peer-dependencies': 'error',
114
116
  '@n8n/community-nodes/webhook-lifecycle-complete': 'error',
115
117
  },
@@ -42,6 +42,7 @@ import { TriggerNodeConventionsRule } from './trigger-node-conventions.js';
42
42
  import { ValidAuthorRule } from './valid-author.js';
43
43
  import { ValidCredentialReferencesRule } from './valid-credential-references.js';
44
44
  import { ValidDescriptionRule } from './valid-description.js';
45
+ import { ValidNodeCategoriesRule } from './valid-node-categories.js';
45
46
  import { ValidPeerDependenciesRule } from './valid-peer-dependencies.js';
46
47
  import { WebhookLifecycleCompleteRule } from './webhook-lifecycle-complete.js';
47
48
 
@@ -88,6 +89,7 @@ export const rules = {
88
89
  'valid-author': ValidAuthorRule,
89
90
  'valid-credential-references': ValidCredentialReferencesRule,
90
91
  'valid-description': ValidDescriptionRule,
92
+ 'valid-node-categories': ValidNodeCategoriesRule,
91
93
  'valid-peer-dependencies': ValidPeerDependenciesRule,
92
94
  'webhook-lifecycle-complete': WebhookLifecycleCompleteRule,
93
95
  } satisfies Record<string, AnyRuleModule>;
@@ -0,0 +1,79 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { ValidNodeCategoriesRule } from './valid-node-categories.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+ const filename = 'nodes/Example/Example.node.json';
7
+
8
+ function codex(categories: unknown): string {
9
+ return JSON.stringify({ node: 'n8n-nodes-example', categories });
10
+ }
11
+
12
+ ruleTester.run('valid-node-categories', ValidNodeCategoriesRule, {
13
+ valid: [
14
+ {
15
+ name: 'all supported community categories',
16
+ filename,
17
+ code: codex([
18
+ 'Data & Storage',
19
+ 'Finance & Accounting',
20
+ 'Marketing & Content',
21
+ 'Productivity',
22
+ 'Miscellaneous',
23
+ 'Sales',
24
+ 'Development',
25
+ 'Analytics',
26
+ 'Communication',
27
+ 'Utility',
28
+ ]),
29
+ },
30
+ { name: 'codex without categories', filename, code: '{ "node": "n8n-nodes-example" }' },
31
+ {
32
+ name: 'unrelated JSON file',
33
+ filename: 'package.json',
34
+ code: codex(['Bananas']),
35
+ },
36
+ {
37
+ name: 'non-node class',
38
+ filename: 'nodes/Example/Example.node.ts',
39
+ code: 'class Example { categories = ["Bananas"]; }',
40
+ },
41
+ {
42
+ name: 'nested categories do not count as codex categories',
43
+ filename,
44
+ code: '{ "resources": { "categories": ["Bananas"] } }',
45
+ },
46
+ ],
47
+ invalid: [
48
+ {
49
+ name: 'unknown category',
50
+ filename,
51
+ code: codex(['Development', 'Bananas']),
52
+ errors: [{ messageId: 'invalidCategory', data: { category: 'Bananas' } }],
53
+ },
54
+ {
55
+ name: 'old marketing category',
56
+ filename,
57
+ code: codex(['Marketing']),
58
+ errors: [{ messageId: 'marketingCategory' }],
59
+ },
60
+ {
61
+ name: 'invalid category type',
62
+ filename,
63
+ code: codex(['Sales', 12]),
64
+ errors: [{ messageId: 'invalidCategoryType' }],
65
+ },
66
+ {
67
+ name: 'categories must be an array',
68
+ filename,
69
+ code: codex('Sales'),
70
+ errors: [{ messageId: 'invalidCategories' }],
71
+ },
72
+ {
73
+ name: 'empty categories array',
74
+ filename,
75
+ code: codex([]),
76
+ errors: [{ messageId: 'invalidCategories' }],
77
+ },
78
+ ],
79
+ });
@@ -0,0 +1,72 @@
1
+ import type { TSESTree } from '@typescript-eslint/utils';
2
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
3
+
4
+ import { createRule, findJsonProperty, getTopLevelObjectInJson } from '../utils/index.js';
5
+
6
+ const validCategories = new Set([
7
+ 'Data & Storage',
8
+ 'Finance & Accounting',
9
+ 'Marketing & Content',
10
+ 'Productivity',
11
+ 'Miscellaneous',
12
+ 'Sales',
13
+ 'Development',
14
+ 'Analytics',
15
+ 'Communication',
16
+ 'Utility',
17
+ ]);
18
+
19
+ export const ValidNodeCategoriesRule = createRule({
20
+ name: 'valid-node-categories',
21
+ meta: {
22
+ type: 'problem',
23
+ docs: {
24
+ description: 'Require supported community node categories in .node.json codex files',
25
+ },
26
+ messages: {
27
+ invalidCategories: '"categories" must be a non-empty array of community node categories.',
28
+ invalidCategoryType: 'Each category must be a string from the community node category list.',
29
+ invalidCategory:
30
+ '"{{ category }}" is not a supported community node category. See https://docs.n8n.io/connect/create-nodes/build-your-node/reference/codex-files/#node-categories for the allowed values.',
31
+ marketingCategory: '"Marketing" is not a supported category. Use "Marketing & Content".',
32
+ },
33
+ schema: [],
34
+ },
35
+ defaultOptions: [],
36
+ create(context) {
37
+ if (!context.filename.endsWith('.node.json')) return {};
38
+
39
+ return {
40
+ ObjectExpression(node: TSESTree.ObjectExpression) {
41
+ const root = getTopLevelObjectInJson(node);
42
+ if (!root) return;
43
+
44
+ const categories = findJsonProperty(root, 'categories');
45
+ if (!categories) return;
46
+
47
+ if (
48
+ categories.value.type !== AST_NODE_TYPES.ArrayExpression ||
49
+ categories.value.elements.length === 0
50
+ ) {
51
+ context.report({ node: categories.value, messageId: 'invalidCategories' });
52
+ return;
53
+ }
54
+
55
+ for (const category of categories.value.elements) {
56
+ if (!category) continue;
57
+ if (category.type !== AST_NODE_TYPES.Literal || typeof category.value !== 'string') {
58
+ context.report({ node: category, messageId: 'invalidCategoryType' });
59
+ } else if (category.value === 'Marketing') {
60
+ context.report({ node: category, messageId: 'marketingCategory' });
61
+ } else if (!validCategories.has(category.value)) {
62
+ context.report({
63
+ node: category,
64
+ messageId: 'invalidCategory',
65
+ data: { category: category.value },
66
+ });
67
+ }
68
+ }
69
+ },
70
+ };
71
+ },
72
+ });