@n8n/eslint-plugin-community-nodes 0.14.0 → 0.16.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 (83) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/README.md +7 -0
  3. package/dist/plugin.d.ts +48 -0
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +16 -0
  6. package/dist/plugin.js.map +1 -1
  7. package/dist/rules/cred-class-name-suffix.d.ts +2 -0
  8. package/dist/rules/cred-class-name-suffix.d.ts.map +1 -0
  9. package/dist/rules/cred-class-name-suffix.js +53 -0
  10. package/dist/rules/cred-class-name-suffix.js.map +1 -0
  11. package/dist/rules/cred-class-oauth2-naming.d.ts +2 -0
  12. package/dist/rules/cred-class-oauth2-naming.d.ts.map +1 -0
  13. package/dist/rules/cred-class-oauth2-naming.js +96 -0
  14. package/dist/rules/cred-class-oauth2-naming.js.map +1 -0
  15. package/dist/rules/index.d.ts +11 -1
  16. package/dist/rules/index.d.ts.map +1 -1
  17. package/dist/rules/index.js +16 -0
  18. package/dist/rules/index.js.map +1 -1
  19. package/dist/rules/n8n-object-validation.d.ts +5 -0
  20. package/dist/rules/n8n-object-validation.d.ts.map +1 -0
  21. package/dist/rules/n8n-object-validation.js +148 -0
  22. package/dist/rules/n8n-object-validation.js.map +1 -0
  23. package/dist/rules/no-builder-hint-leakage.d.ts +7 -0
  24. package/dist/rules/no-builder-hint-leakage.d.ts.map +1 -0
  25. package/dist/rules/no-builder-hint-leakage.js +99 -0
  26. package/dist/rules/no-builder-hint-leakage.js.map +1 -0
  27. package/dist/rules/no-overrides-field.js +1 -1
  28. package/dist/rules/no-overrides-field.js.map +1 -1
  29. package/dist/rules/no-runtime-dependencies.d.ts +2 -0
  30. package/dist/rules/no-runtime-dependencies.d.ts.map +1 -0
  31. package/dist/rules/no-runtime-dependencies.js +41 -0
  32. package/dist/rules/no-runtime-dependencies.js.map +1 -0
  33. package/dist/rules/no-template-placeholders.d.ts +2 -0
  34. package/dist/rules/no-template-placeholders.d.ts.map +1 -0
  35. package/dist/rules/no-template-placeholders.js +57 -0
  36. package/dist/rules/no-template-placeholders.js.map +1 -0
  37. package/dist/rules/node-operation-error-itemindex.d.ts +12 -0
  38. package/dist/rules/node-operation-error-itemindex.d.ts.map +1 -0
  39. package/dist/rules/node-operation-error-itemindex.js +184 -0
  40. package/dist/rules/node-operation-error-itemindex.js.map +1 -0
  41. package/dist/rules/valid-credential-references.d.ts +2 -0
  42. package/dist/rules/valid-credential-references.d.ts.map +1 -0
  43. package/dist/rules/valid-credential-references.js +77 -0
  44. package/dist/rules/valid-credential-references.js.map +1 -0
  45. package/dist/rules/webhook-lifecycle-complete.d.ts +1 -1
  46. package/dist/rules/webhook-lifecycle-complete.d.ts.map +1 -1
  47. package/dist/rules/webhook-lifecycle-complete.js +8 -0
  48. package/dist/rules/webhook-lifecycle-complete.js.map +1 -1
  49. package/dist/utils/ast-utils.d.ts.map +1 -1
  50. package/dist/utils/ast-utils.js +5 -1
  51. package/dist/utils/ast-utils.js.map +1 -1
  52. package/docs/rules/cred-class-name-suffix.md +46 -0
  53. package/docs/rules/cred-class-oauth2-naming.md +68 -0
  54. package/docs/rules/n8n-object-validation.md +93 -0
  55. package/docs/rules/no-overrides-field.md +5 -5
  56. package/docs/rules/no-runtime-dependencies.md +58 -0
  57. package/docs/rules/no-template-placeholders.md +51 -0
  58. package/docs/rules/node-operation-error-itemindex.md +81 -0
  59. package/docs/rules/valid-credential-references.md +78 -0
  60. package/package.json +3 -3
  61. package/src/plugin.ts +16 -0
  62. package/src/rules/cred-class-name-suffix.test.ts +74 -0
  63. package/src/rules/cred-class-name-suffix.ts +57 -0
  64. package/src/rules/cred-class-oauth2-naming.test.ts +197 -0
  65. package/src/rules/cred-class-oauth2-naming.ts +118 -0
  66. package/src/rules/index.ts +16 -0
  67. package/src/rules/n8n-object-validation.test.ts +202 -0
  68. package/src/rules/n8n-object-validation.ts +200 -0
  69. package/src/rules/no-builder-hint-leakage.test.ts +84 -0
  70. package/src/rules/no-builder-hint-leakage.ts +112 -0
  71. package/src/rules/no-overrides-field.ts +1 -1
  72. package/src/rules/no-runtime-dependencies.test.ts +50 -0
  73. package/src/rules/no-runtime-dependencies.ts +50 -0
  74. package/src/rules/no-template-placeholders.test.ts +135 -0
  75. package/src/rules/no-template-placeholders.ts +68 -0
  76. package/src/rules/node-operation-error-itemindex.test.ts +280 -0
  77. package/src/rules/node-operation-error-itemindex.ts +223 -0
  78. package/src/rules/valid-credential-references.test.ts +230 -0
  79. package/src/rules/valid-credential-references.ts +105 -0
  80. package/src/rules/webhook-lifecycle-complete.test.ts +5 -0
  81. package/src/rules/webhook-lifecycle-complete.ts +10 -0
  82. package/src/utils/ast-utils.ts +5 -1
  83. package/tsconfig.build.tsbuildinfo +1 -1
@@ -0,0 +1,112 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import type { TSESTree } from '@typescript-eslint/utils';
3
+
4
+ import { createRule } from '../utils/index.js';
5
+
6
+ const WIRE_EXPRESSION_RE = /={{/;
7
+ const CONNECTION_NAME_GROUP =
8
+ 'agent|chain|document|embedding|languageModel|memory|outputParser|retriever|reranker|textSplitter|tool|vectorStore';
9
+ const CONNECTION_NAME_RE = new RegExp(`\\bai_(${CONNECTION_NAME_GROUP})\\b`, 'g');
10
+ // Strings that are *exactly* a connection name are structured data values,
11
+ // not prose, so they're allowed (e.g. `connectionType: 'ai_languageModel'`).
12
+ const EXACT_CONNECTION_NAME_RE = new RegExp(`^ai_(${CONNECTION_NAME_GROUP})$`);
13
+
14
+ type Options = [{ scope?: 'builderHint' | 'all' }];
15
+ type MessageIds = 'wireExpression' | 'connectionTypeLiteral';
16
+
17
+ function isPropertyKey(node: TSESTree.Node): boolean {
18
+ const parent = node.parent;
19
+ return parent?.type === AST_NODE_TYPES.Property && parent.key === node;
20
+ }
21
+
22
+ function isInsideBuilderHintValue(node: TSESTree.Node): boolean {
23
+ let current: TSESTree.Node | undefined = node;
24
+ let parent = current.parent;
25
+ while (parent) {
26
+ if (
27
+ parent.type === AST_NODE_TYPES.Property &&
28
+ parent.value === current &&
29
+ ((parent.key.type === AST_NODE_TYPES.Identifier && parent.key.name === 'builderHint') ||
30
+ (parent.key.type === AST_NODE_TYPES.Literal && parent.key.value === 'builderHint'))
31
+ ) {
32
+ return true;
33
+ }
34
+ current = parent;
35
+ parent = current.parent;
36
+ }
37
+ return false;
38
+ }
39
+
40
+ export const NoBuilderHintLeakageRule = createRule<Options, MessageIds>({
41
+ name: 'no-builder-hint-leakage',
42
+ meta: {
43
+ type: 'problem',
44
+ docs: {
45
+ description:
46
+ 'Disallow wire-format expression syntax (={{...}}) and NodeConnectionType string literals in builderHint texts and AI-builder prompts. Use expr() and SDK-canonical references instead.',
47
+ },
48
+ messages: {
49
+ wireExpression:
50
+ 'Wire-format expression syntax leaks into {{ ctx }}. Use the expr() SDK helper instead.',
51
+ connectionTypeLiteral:
52
+ 'NodeConnectionType literal "{{ value }}" leaks wire format into {{ ctx }}. Refer to the SDK helper instead (e.g. languageModel(), tool(), memory()).',
53
+ },
54
+ schema: [
55
+ {
56
+ type: 'object',
57
+ description:
58
+ 'Configures where the rule scans for forbidden patterns. Default `builderHint` only checks string values inside builderHint property values; `all` checks every string in the file (used for AI-builder prompts).',
59
+ properties: {
60
+ scope: {
61
+ type: 'string',
62
+ enum: ['builderHint', 'all'],
63
+ description:
64
+ '`builderHint` (default): only flag strings inside builderHint property values. `all`: flag every string literal/template in the file.',
65
+ },
66
+ },
67
+ additionalProperties: false,
68
+ },
69
+ ],
70
+ },
71
+ defaultOptions: [{}],
72
+ create(context, [options]) {
73
+ const scope = options.scope ?? 'builderHint';
74
+ const ctx = scope === 'all' ? 'AI-builder prompt' : 'builderHint';
75
+
76
+ function reportFor(node: TSESTree.Node, str: string): void {
77
+ if (WIRE_EXPRESSION_RE.test(str)) {
78
+ context.report({ node, messageId: 'wireExpression', data: { ctx } });
79
+ }
80
+ if (EXACT_CONNECTION_NAME_RE.test(str)) return;
81
+ CONNECTION_NAME_RE.lastIndex = 0;
82
+ let match: RegExpExecArray | null;
83
+ while ((match = CONNECTION_NAME_RE.exec(str)) !== null) {
84
+ context.report({
85
+ node,
86
+ messageId: 'connectionTypeLiteral',
87
+ data: { value: match[0], ctx },
88
+ });
89
+ }
90
+ }
91
+
92
+ function shouldCheck(node: TSESTree.Node): boolean {
93
+ if (isPropertyKey(node)) return false;
94
+ if (scope === 'all') return true;
95
+ return isInsideBuilderHintValue(node);
96
+ }
97
+
98
+ return {
99
+ Literal(node) {
100
+ if (typeof node.value !== 'string') return;
101
+ if (!shouldCheck(node)) return;
102
+ reportFor(node, node.value);
103
+ },
104
+ TemplateLiteral(node) {
105
+ if (!shouldCheck(node)) return;
106
+ for (const quasi of node.quasis) {
107
+ reportFor(quasi, quasi.value.cooked ?? quasi.value.raw);
108
+ }
109
+ },
110
+ };
111
+ },
112
+ });
@@ -12,7 +12,7 @@ export const NoOverridesFieldRule = createRule({
12
12
  },
13
13
  messages: {
14
14
  overridesForbidden:
15
- 'The "overrides" field is not allowed in community node packages. Dependency overrides can introduce incompatible versions of shared libraries into the n8n runtime and cause conflicts with other nodes.',
15
+ 'The "overrides" field is not allowed in community node packages. Each community package installs into an isolated dependency tree, so overrides do not affect other nodes or n8n core in practice they are copy-pasted boilerplate with no useful effect. Use the helpers on the execute context (this.helpers.httpRequest, etc.) instead; most community nodes do not need third-party runtime libraries.',
16
16
  },
17
17
  schema: [],
18
18
  },
@@ -0,0 +1,50 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { NoRuntimeDependenciesRule } from './no-runtime-dependencies.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ ruleTester.run('no-runtime-dependencies', NoRuntimeDependenciesRule, {
8
+ valid: [
9
+ {
10
+ name: 'no dependencies field',
11
+ filename: 'package.json',
12
+ code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
13
+ },
14
+ {
15
+ name: 'empty dependencies object is allowed',
16
+ filename: 'package.json',
17
+ code: '{ "name": "n8n-nodes-example", "dependencies": {} }',
18
+ },
19
+ {
20
+ name: 'non-package.json file is ignored',
21
+ filename: 'some-config.json',
22
+ code: '{ "dependencies": { "axios": "1.0.0" } }',
23
+ },
24
+ {
25
+ name: 'nested "dependencies" key inside another field is allowed',
26
+ filename: 'package.json',
27
+ code: '{ "name": "n8n-nodes-example", "config": { "dependencies": { "axios": "1.0.0" } } }',
28
+ },
29
+ ],
30
+ invalid: [
31
+ {
32
+ name: 'single runtime dependency is forbidden',
33
+ filename: 'package.json',
34
+ code: '{ "name": "n8n-nodes-example", "dependencies": { "axios": "1.0.0" } }',
35
+ errors: [{ messageId: 'runtimeDependenciesForbidden' }],
36
+ },
37
+ {
38
+ name: 'multiple runtime dependencies are forbidden',
39
+ filename: 'package.json',
40
+ code: '{ "name": "n8n-nodes-example", "dependencies": { "axios": "1.0.0", "lodash": "^4.0.0" } }',
41
+ errors: [{ messageId: 'runtimeDependenciesForbidden' }],
42
+ },
43
+ {
44
+ name: 'real-world package with bundled deps is forbidden',
45
+ filename: 'package.json',
46
+ code: '{ "name": "n8n-nodes-sinch", "dependencies": { "axios": "1.7.0", "fast-xml-parser": "4.4.0", "minimatch": "9.0.5" } }',
47
+ errors: [{ messageId: 'runtimeDependenciesForbidden' }],
48
+ },
49
+ ],
50
+ });
@@ -0,0 +1,50 @@
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
+ export const NoRuntimeDependenciesRule = createRule({
7
+ name: 'no-runtime-dependencies',
8
+ meta: {
9
+ type: 'problem',
10
+ docs: {
11
+ description: 'Disallow non-empty "dependencies" in community node package.json',
12
+ },
13
+ messages: {
14
+ runtimeDependenciesForbidden:
15
+ 'The "dependencies" field must be empty or absent in community node packages. Runtime dependencies get bundled into the n8n instance and can conflict with other nodes or the n8n runtime itself. Move shared libraries to "peerDependencies" or bundle them into your build artifact.',
16
+ },
17
+ schema: [],
18
+ },
19
+ defaultOptions: [],
20
+ create(context) {
21
+ if (!context.filename.endsWith('package.json')) {
22
+ return {};
23
+ }
24
+
25
+ return {
26
+ ObjectExpression(node: TSESTree.ObjectExpression) {
27
+ if (node.parent?.type !== AST_NODE_TYPES.ExpressionStatement) {
28
+ return;
29
+ }
30
+
31
+ const depsProp = findJsonProperty(node, 'dependencies');
32
+ if (!depsProp) {
33
+ return;
34
+ }
35
+
36
+ if (
37
+ depsProp.value.type !== AST_NODE_TYPES.ObjectExpression ||
38
+ depsProp.value.properties.length === 0
39
+ ) {
40
+ return;
41
+ }
42
+
43
+ context.report({
44
+ node: depsProp,
45
+ messageId: 'runtimeDependenciesForbidden',
46
+ });
47
+ },
48
+ };
49
+ },
50
+ });
@@ -0,0 +1,135 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { NoTemplatePlaceholdersRule } from './no-template-placeholders.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ ruleTester.run('no-template-placeholders', NoTemplatePlaceholdersRule, {
8
+ valid: [
9
+ {
10
+ name: 'package.json with no placeholders',
11
+ filename: 'package.json',
12
+ code: `{
13
+ "name": "n8n-nodes-example",
14
+ "version": "1.0.0",
15
+ "description": "An example community node",
16
+ "homepage": "https://example.com",
17
+ "repository": { "type": "git", "url": "git+https://github.com/acme/n8n-nodes-example.git" }
18
+ }`,
19
+ },
20
+ {
21
+ name: 'angle brackets that do not look like placeholders are ignored',
22
+ filename: 'package.json',
23
+ code: '{ "description": "Compares a < b values" }',
24
+ },
25
+ {
26
+ name: 'single curly braces are ignored',
27
+ filename: 'package.json',
28
+ code: '{ "description": "Use { key: value } syntax" }',
29
+ },
30
+ {
31
+ name: 'non-package.json file is ignored even if it has placeholders',
32
+ filename: 'tsconfig.json',
33
+ code: '{ "name": "<PACKAGE_NAME>" }',
34
+ },
35
+ {
36
+ name: 'numeric and boolean values are not flagged',
37
+ filename: 'package.json',
38
+ code: '{ "name": "n8n-nodes-example", "private": false, "engines": { "node": ">=18" } }',
39
+ },
40
+ ],
41
+ invalid: [
42
+ {
43
+ name: 'angle bracket placeholder in name',
44
+ filename: 'package.json',
45
+ code: '{ "name": "n8n-nodes-<PACKAGE_NAME>" }',
46
+ errors: [
47
+ {
48
+ messageId: 'unresolvedPlaceholder',
49
+ data: { pattern: '<PACKAGE_NAME>' },
50
+ },
51
+ ],
52
+ },
53
+ {
54
+ name: 'angle bracket placeholder in description',
55
+ filename: 'package.json',
56
+ code: '{ "description": "An n8n community node for <SERVICE>" }',
57
+ errors: [
58
+ {
59
+ messageId: 'unresolvedPlaceholder',
60
+ data: { pattern: '<SERVICE>' },
61
+ },
62
+ ],
63
+ },
64
+ {
65
+ name: 'angle bracket placeholder in repository url',
66
+ filename: 'package.json',
67
+ code: '{ "repository": { "type": "git", "url": "git+https://github.com/<USERNAME>/n8n-nodes-example.git" } }',
68
+ errors: [
69
+ {
70
+ messageId: 'unresolvedPlaceholder',
71
+ data: { pattern: '<USERNAME>' },
72
+ },
73
+ ],
74
+ },
75
+ {
76
+ name: 'angle bracket placeholder in homepage',
77
+ filename: 'package.json',
78
+ code: '{ "homepage": "https://github.com/<USERNAME>/n8n-nodes-example#readme" }',
79
+ errors: [
80
+ {
81
+ messageId: 'unresolvedPlaceholder',
82
+ data: { pattern: '<USERNAME>' },
83
+ },
84
+ ],
85
+ },
86
+ {
87
+ name: 'mustache placeholder in author',
88
+ filename: 'package.json',
89
+ code: '{ "author": "{{ authorName }}" }',
90
+ errors: [
91
+ {
92
+ messageId: 'unresolvedPlaceholder',
93
+ data: { pattern: '{{ authorName }}' },
94
+ },
95
+ ],
96
+ },
97
+ {
98
+ name: 'mustache placeholder inside larger string',
99
+ filename: 'package.json',
100
+ code: '{ "description": "Node by {{author}} for service" }',
101
+ errors: [
102
+ {
103
+ messageId: 'unresolvedPlaceholder',
104
+ data: { pattern: '{{author}}' },
105
+ },
106
+ ],
107
+ },
108
+ {
109
+ name: 'placeholder in custom field',
110
+ filename: 'package.json',
111
+ code: '{ "n8n": { "n8nNodesApiVersion": 1, "credentials": ["<CREDENTIAL>"] } }',
112
+ errors: [
113
+ {
114
+ messageId: 'unresolvedPlaceholder',
115
+ data: { pattern: '<CREDENTIAL>' },
116
+ },
117
+ ],
118
+ },
119
+ {
120
+ name: 'multiple placeholders in different fields are all reported',
121
+ filename: 'package.json',
122
+ code: '{ "name": "<NAME>", "description": "{{description}}" }',
123
+ errors: [
124
+ {
125
+ messageId: 'unresolvedPlaceholder',
126
+ data: { pattern: '<NAME>' },
127
+ },
128
+ {
129
+ messageId: 'unresolvedPlaceholder',
130
+ data: { pattern: '{{description}}' },
131
+ },
132
+ ],
133
+ },
134
+ ],
135
+ });
@@ -0,0 +1,68 @@
1
+ import type { TSESTree } from '@typescript-eslint/utils';
2
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
3
+
4
+ import { createRule } from '../utils/index.js';
5
+
6
+ const ANGLE_PLACEHOLDER = /<[^<>\n]+?>/;
7
+ const MUSTACHE_PLACEHOLDER = /\{\{[^{}\n]+?\}\}/;
8
+
9
+ function findPlaceholder(value: string): { pattern: string; type: 'angle' | 'mustache' } | null {
10
+ const angleMatch = ANGLE_PLACEHOLDER.exec(value);
11
+ if (angleMatch) {
12
+ return { pattern: angleMatch[0], type: 'angle' };
13
+ }
14
+ const mustacheMatch = MUSTACHE_PLACEHOLDER.exec(value);
15
+ if (mustacheMatch) {
16
+ return { pattern: mustacheMatch[0], type: 'mustache' };
17
+ }
18
+ return null;
19
+ }
20
+
21
+ export const NoTemplatePlaceholdersRule = createRule({
22
+ name: 'no-template-placeholders',
23
+ meta: {
24
+ type: 'problem',
25
+ docs: {
26
+ description: 'Disallow unresolved template placeholders in package.json',
27
+ },
28
+ messages: {
29
+ unresolvedPlaceholder:
30
+ 'String value contains an unresolved template placeholder "{{ pattern }}". Replace it with a real value before publishing.',
31
+ },
32
+ schema: [],
33
+ },
34
+ defaultOptions: [],
35
+ create(context) {
36
+ if (!context.filename.endsWith('package.json')) {
37
+ return {};
38
+ }
39
+
40
+ return {
41
+ Literal(node: TSESTree.Literal) {
42
+ if (typeof node.value !== 'string') {
43
+ return;
44
+ }
45
+
46
+ // Skip property keys — only flag values.
47
+ if (
48
+ node.parent?.type === AST_NODE_TYPES.Property &&
49
+ node.parent.key === node &&
50
+ !node.parent.computed
51
+ ) {
52
+ return;
53
+ }
54
+
55
+ const placeholder = findPlaceholder(node.value);
56
+ if (!placeholder) {
57
+ return;
58
+ }
59
+
60
+ context.report({
61
+ node,
62
+ messageId: 'unresolvedPlaceholder',
63
+ data: { pattern: placeholder.pattern },
64
+ });
65
+ },
66
+ };
67
+ },
68
+ });