@n8n/eslint-plugin-community-nodes 0.21.0 → 0.22.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 (32) hide show
  1. package/.turbo/turbo-build$colon$unchecked.log +1 -1
  2. package/README.md +47 -46
  3. package/dist/plugin.d.ts +6 -0
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +2 -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 +1 -0
  11. package/dist/rules/index.d.ts.map +1 -1
  12. package/dist/rules/index.js +2 -0
  13. package/dist/rules/index.js.map +1 -1
  14. package/dist/rules/node-class-description-name-camelcase.d.ts +2 -0
  15. package/dist/rules/node-class-description-name-camelcase.d.ts.map +1 -0
  16. package/dist/rules/node-class-description-name-camelcase.js +92 -0
  17. package/dist/rules/node-class-description-name-camelcase.js.map +1 -0
  18. package/dist/rules/valid-peer-dependencies.d.ts.map +1 -1
  19. package/dist/rules/valid-peer-dependencies.js +5 -3
  20. package/dist/rules/valid-peer-dependencies.js.map +1 -1
  21. package/dist/typecheck.tsbuildinfo +1 -1
  22. package/docs/rules/node-class-description-name-camelcase.md +41 -0
  23. package/docs/rules/valid-peer-dependencies.md +1 -1
  24. package/package.json +2 -2
  25. package/src/plugin.ts +2 -0
  26. package/src/rules/ai-node-package-json.test.ts +5 -0
  27. package/src/rules/ai-node-package-json.ts +4 -3
  28. package/src/rules/index.ts +2 -0
  29. package/src/rules/node-class-description-name-camelcase.test.ts +121 -0
  30. package/src/rules/node-class-description-name-camelcase.ts +114 -0
  31. package/src/rules/valid-peer-dependencies.test.ts +5 -0
  32. package/src/rules/valid-peer-dependencies.ts +5 -3
@@ -0,0 +1,41 @@
1
+ # Node class `description.name` must be camelCase (`@n8n/community-nodes/node-class-description-name-camelcase`)
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
+ The `name` field inside a node class `description` (those implementing `INodeType` or extending `Node` in `*.node.ts` files) is the internal identifier used to register and reference the node. n8n convention requires this identifier to be camelCase: it must start with a lowercase letter and contain only letters and digits, with no spaces, hyphens, underscores, or other separators.
12
+
13
+ The value must match `^[a-z][a-zA-Z0-9]*$`.
14
+
15
+ The rule is automatically fixable: separators are removed, each subsequent word is upper-cased, and the first character is lower-cased (e.g. `My Node` → `myNode`). Names that cannot be repaired into a valid identifier (for example, those starting with a digit) are reported without an autofix.
16
+
17
+ ## Examples
18
+
19
+ ### ❌ Incorrect
20
+
21
+ ```typescript
22
+ export class MyNode implements INodeType {
23
+ description: INodeTypeDescription = {
24
+ displayName: 'My Node',
25
+ name: 'My Node',
26
+ // ...
27
+ };
28
+ }
29
+ ```
30
+
31
+ ### ✅ Correct
32
+
33
+ ```typescript
34
+ export class MyNode implements INodeType {
35
+ description: INodeTypeDescription = {
36
+ displayName: 'My Node',
37
+ name: 'myNode',
38
+ // ...
39
+ };
40
+ }
41
+ ```
@@ -1,4 +1,4 @@
1
- # Require community node package.json peerDependencies to contain only "n8n-workflow": "*" (and optionally "ai-node-sdk") (`@n8n/community-nodes/valid-peer-dependencies`)
1
+ # Require community node package.json peerDependencies to contain only "n8n-workflow": "*" (and optionally "@n8n/ai-node-sdk") (`@n8n/community-nodes/valid-peer-dependencies`)
2
2
 
3
3
  💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
4
 
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.21.0",
4
+ "version": "0.22.0",
5
5
  "main": "./dist/plugin.js",
6
6
  "types": "./dist/plugin.d.ts",
7
7
  "exports": {
@@ -25,7 +25,7 @@
25
25
  "vitest": "^4.1.1",
26
26
  "@n8n/typescript-config": "1.6.0",
27
27
  "@n8n/vitest-config": "1.15.0",
28
- "n8n-workflow": "2.27.0"
28
+ "n8n-workflow": "2.28.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "eslint": "9.29.0",
package/src/plugin.ts CHANGED
@@ -48,6 +48,7 @@ const configs = {
48
48
  '@n8n/community-nodes/cred-class-oauth2-naming': 'error',
49
49
  '@n8n/community-nodes/cred-filename-against-convention': 'error',
50
50
  '@n8n/community-nodes/node-connection-type-literal': 'error',
51
+ '@n8n/community-nodes/node-class-description-name-camelcase': 'error',
51
52
  '@n8n/community-nodes/node-filename-against-convention': 'error',
52
53
  '@n8n/community-nodes/missing-paired-item': 'error',
53
54
  '@n8n/community-nodes/no-builder-hint-leakage': 'error',
@@ -97,6 +98,7 @@ const configs = {
97
98
  '@n8n/community-nodes/cred-class-oauth2-naming': 'error',
98
99
  '@n8n/community-nodes/cred-filename-against-convention': 'error',
99
100
  '@n8n/community-nodes/node-connection-type-literal': 'error',
101
+ '@n8n/community-nodes/node-class-description-name-camelcase': 'error',
100
102
  '@n8n/community-nodes/node-filename-against-convention': 'error',
101
103
  '@n8n/community-nodes/missing-paired-item': 'error',
102
104
  '@n8n/community-nodes/no-builder-hint-leakage': 'error',
@@ -11,6 +11,11 @@ ruleTester.run('ai-node-package-json', AiNodePackageJsonRule, {
11
11
  filename: 'package.json',
12
12
  code: '{ "name": "n8n-nodes-example", "n8n": { "aiNodeSdkVersion": 1 }, "peerDependencies": { "n8n-workflow": "*", "ai-node-sdk": "*" } }',
13
13
  },
14
+ {
15
+ name: 'both n8n.aiNodeSdkVersion and scoped @n8n/ai-node-sdk peer dependency present',
16
+ filename: 'package.json',
17
+ code: '{ "name": "n8n-nodes-example", "n8n": { "aiNodeSdkVersion": 1 }, "peerDependencies": { "n8n-workflow": "*", "@n8n/ai-node-sdk": "*" } }',
18
+ },
14
19
  {
15
20
  name: 'neither n8n.aiNodeSdkVersion nor ai-node-sdk present (non-AI package)',
16
21
  filename: 'package.json',
@@ -13,9 +13,9 @@ export const AiNodePackageJsonRule = createRule({
13
13
  },
14
14
  messages: {
15
15
  missingPeerDep:
16
- 'Package declares "n8n.aiNodeSdkVersion" but is missing "ai-node-sdk" in peerDependencies. Add "ai-node-sdk": "*" to peerDependencies.',
16
+ 'Package declares "n8n.aiNodeSdkVersion" but is missing "@n8n/ai-node-sdk" in peerDependencies. Add "@n8n/ai-node-sdk": "*" to peerDependencies.',
17
17
  missingSdkVersion:
18
- 'Package has "ai-node-sdk" in peerDependencies but is missing "aiNodeSdkVersion" in the "n8n" section of package.json.',
18
+ 'Package has "@n8n/ai-node-sdk" in peerDependencies but is missing "aiNodeSdkVersion" in the "n8n" section of package.json.',
19
19
  invalidSdkVersion: '"n8n.aiNodeSdkVersion" must be a positive integer, got {{ value }}.',
20
20
  wrongLocation:
21
21
  '"aiNodeSdkVersion" must be inside the "n8n" section, not at the root level of package.json.',
@@ -49,7 +49,8 @@ export const AiNodePackageJsonRule = createRule({
49
49
  const hasAiNodeSdkVersion = aiNodeSdkVersionProp !== null;
50
50
  const hasAiNodeSdkPeerDep =
51
51
  peerDependenciesProp?.value.type === AST_NODE_TYPES.ObjectExpression &&
52
- findJsonProperty(peerDependenciesProp.value, 'ai-node-sdk') !== null;
52
+ (findJsonProperty(peerDependenciesProp.value, '@n8n/ai-node-sdk') !== null ||
53
+ findJsonProperty(peerDependenciesProp.value, 'ai-node-sdk') !== null);
53
54
 
54
55
  // Catch aiNodeSdkVersion placed at root level instead of inside n8n
55
56
  if (rootAiNodeSdkVersionProp) {
@@ -26,6 +26,7 @@ import { NoRestrictedImportsRule } from './no-restricted-imports.js';
26
26
  import { NoRuntimeDependenciesRule } from './no-runtime-dependencies.js';
27
27
  import { NoTemplatePlaceholdersRule } from './no-template-placeholders.js';
28
28
  import { NodeClassDescriptionIconMissingRule } from './node-class-description-icon-missing.js';
29
+ import { NodeClassDescriptionNameCamelCaseRule } from './node-class-description-name-camelcase.js';
29
30
  import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js';
30
31
  import { NodeFilenameAgainstConventionRule } from './node-filename-against-convention.js';
31
32
  import { NodeOperationErrorItemIndexRule } from './node-operation-error-itemindex.js';
@@ -68,6 +69,7 @@ export const rules = {
68
69
  'resource-operation-pattern': ResourceOperationPatternRule,
69
70
  'credential-documentation-url': CredentialDocumentationUrlRule,
70
71
  'node-class-description-icon-missing': NodeClassDescriptionIconMissingRule,
72
+ 'node-class-description-name-camelcase': NodeClassDescriptionNameCamelCaseRule,
71
73
  'cred-class-field-icon-missing': CredClassFieldIconMissingRule,
72
74
  'cred-class-name-field-conventions': CredClassNameFieldConventionsRule,
73
75
  'cred-class-name-suffix': CredClassNameSuffixRule,
@@ -0,0 +1,121 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { NodeClassDescriptionNameCamelCaseRule } from './node-class-description-name-camelcase.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ function createNodeCode(name: 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: '${name}',
15
+ group: ['input'],
16
+ version: 1,
17
+ description: 'A test node',
18
+ defaults: { name: 'Test Node' },
19
+ inputs: [],
20
+ outputs: [],
21
+ properties: [],
22
+ };
23
+ }`;
24
+ }
25
+
26
+ function createNodeCodeWithoutName(): string {
27
+ return `
28
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
29
+
30
+ export class TestNode implements INodeType {
31
+ description: INodeTypeDescription = {
32
+ displayName: 'Test Node',
33
+ group: ['input'],
34
+ version: 1,
35
+ description: 'A test node',
36
+ defaults: { name: 'Test Node' },
37
+ inputs: [],
38
+ outputs: [],
39
+ properties: [],
40
+ };
41
+ }`;
42
+ }
43
+
44
+ function createRegularClass(): string {
45
+ return `
46
+ export class SomeHelper {
47
+ name = 'My Helper';
48
+ }`;
49
+ }
50
+
51
+ ruleTester.run('node-class-description-name-camelcase', NodeClassDescriptionNameCamelCaseRule, {
52
+ valid: [
53
+ {
54
+ name: 'single lowercase word',
55
+ filename: '/tmp/Github.node.ts',
56
+ code: createNodeCode('github'),
57
+ },
58
+ {
59
+ name: 'camelCase multi-word name',
60
+ filename: '/tmp/GoogleSheets.node.ts',
61
+ code: createNodeCode('googleSheets'),
62
+ },
63
+ {
64
+ name: 'camelCase name with digits',
65
+ filename: '/tmp/Oauth2.node.ts',
66
+ code: createNodeCode('myNode2'),
67
+ },
68
+ {
69
+ name: 'class not implementing INodeType is ignored',
70
+ filename: '/tmp/Github.node.ts',
71
+ code: createRegularClass(),
72
+ },
73
+ {
74
+ name: 'non-.node.ts file is ignored',
75
+ filename: '/tmp/Github.ts',
76
+ code: createNodeCode('My Node'),
77
+ },
78
+ {
79
+ name: 'missing description.name is ignored',
80
+ filename: '/tmp/Github.node.ts',
81
+ code: createNodeCodeWithoutName(),
82
+ },
83
+ ],
84
+ invalid: [
85
+ {
86
+ name: 'PascalCase first letter is lowercased',
87
+ filename: '/tmp/Github.node.ts',
88
+ code: createNodeCode('MyNode'),
89
+ errors: [{ messageId: 'notCamelCase', data: { value: 'MyNode' } }],
90
+ output: createNodeCode('myNode'),
91
+ },
92
+ {
93
+ name: 'spaces are removed and camelCased',
94
+ filename: '/tmp/Github.node.ts',
95
+ code: createNodeCode('My Node'),
96
+ errors: [{ messageId: 'notCamelCase', data: { value: 'My Node' } }],
97
+ output: createNodeCode('myNode'),
98
+ },
99
+ {
100
+ name: 'hyphen separators are removed and camelCased',
101
+ filename: '/tmp/Github.node.ts',
102
+ code: createNodeCode('my-node'),
103
+ errors: [{ messageId: 'notCamelCase', data: { value: 'my-node' } }],
104
+ output: createNodeCode('myNode'),
105
+ },
106
+ {
107
+ name: 'underscore separators are removed and camelCased',
108
+ filename: '/tmp/Github.node.ts',
109
+ code: createNodeCode('my_cool_node'),
110
+ errors: [{ messageId: 'notCamelCase', data: { value: 'my_cool_node' } }],
111
+ output: createNodeCode('myCoolNode'),
112
+ },
113
+ {
114
+ name: 'names starting with a digit are reported without an autofix',
115
+ filename: '/tmp/Github.node.ts',
116
+ code: createNodeCode('123node'),
117
+ errors: [{ messageId: 'notCamelCase', data: { value: '123node' } }],
118
+ output: null,
119
+ },
120
+ ],
121
+ });
@@ -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
+ });
@@ -11,6 +11,11 @@ ruleTester.run('valid-peer-dependencies', ValidPeerDependenciesRule, {
11
11
  filename: 'package.json',
12
12
  code: '{ "name": "n8n-nodes-example", "peerDependencies": { "n8n-workflow": "*" } }',
13
13
  },
14
+ {
15
+ name: 'n8n-workflow and scoped @n8n/ai-node-sdk',
16
+ filename: 'package.json',
17
+ code: '{ "name": "n8n-nodes-example", "peerDependencies": { "n8n-workflow": "*", "@n8n/ai-node-sdk": "*" } }',
18
+ },
14
19
  {
15
20
  name: 'n8n-workflow and ai-node-sdk',
16
21
  filename: 'package.json',
@@ -5,7 +5,9 @@ import { createRule, findJsonProperty } from '../utils/index.js';
5
5
 
6
6
  const REQUIRED_DEP = 'n8n-workflow';
7
7
  const REQUIRED_VERSION = '*';
8
- const ALLOWED_DEPS = new Set([REQUIRED_DEP, 'ai-node-sdk']);
8
+ // The published SDK is scoped (@n8n/ai-node-sdk); the unscoped name is kept for
9
+ // backwards compatibility, mirroring no-restricted-imports.
10
+ const ALLOWED_DEPS = new Set([REQUIRED_DEP, '@n8n/ai-node-sdk', 'ai-node-sdk']);
9
11
 
10
12
  export const ValidPeerDependenciesRule = createRule({
11
13
  name: 'valid-peer-dependencies',
@@ -13,7 +15,7 @@ export const ValidPeerDependenciesRule = createRule({
13
15
  type: 'problem',
14
16
  docs: {
15
17
  description:
16
- 'Require community node package.json peerDependencies to contain only "n8n-workflow": "*" (and optionally "ai-node-sdk")',
18
+ 'Require community node package.json peerDependencies to contain only "n8n-workflow": "*" (and optionally "@n8n/ai-node-sdk")',
17
19
  },
18
20
  fixable: 'code',
19
21
  messages: {
@@ -22,7 +24,7 @@ export const ValidPeerDependenciesRule = createRule({
22
24
  missingN8nWorkflow: `"peerDependencies" must include "${REQUIRED_DEP}": "${REQUIRED_VERSION}".`,
23
25
  pinnedN8nWorkflow: `"peerDependencies.${REQUIRED_DEP}" must be "${REQUIRED_VERSION}", got {{ value }}.`,
24
26
  forbiddenPeerDependency:
25
- '"{{ name }}" is not allowed in "peerDependencies". Only "n8n-workflow" and "ai-node-sdk" are permitted.',
27
+ '"{{ name }}" is not allowed in "peerDependencies". Only "n8n-workflow" and "@n8n/ai-node-sdk" are permitted.',
26
28
  },
27
29
  schema: [],
28
30
  },