@n8n/eslint-plugin-community-nodes 0.10.0 → 0.12.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/.turbo/turbo-build.log +1 -1
- package/README.md +22 -17
- package/dist/plugin.d.ts +30 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +10 -0
- package/dist/plugin.js.map +1 -1
- package/dist/rules/index.d.ts +5 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +10 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/missing-paired-item.d.ts +16 -0
- package/dist/rules/missing-paired-item.d.ts.map +1 -0
- package/dist/rules/missing-paired-item.js +121 -0
- package/dist/rules/missing-paired-item.js.map +1 -0
- package/dist/rules/no-forbidden-lifecycle-scripts.d.ts +2 -0
- package/dist/rules/no-forbidden-lifecycle-scripts.d.ts.map +1 -0
- package/dist/rules/no-forbidden-lifecycle-scripts.js +59 -0
- package/dist/rules/no-forbidden-lifecycle-scripts.js.map +1 -0
- package/dist/rules/node-connection-type-literal.d.ts +2 -0
- package/dist/rules/node-connection-type-literal.d.ts.map +1 -0
- package/dist/rules/node-connection-type-literal.js +77 -0
- package/dist/rules/node-connection-type-literal.js.map +1 -0
- package/dist/rules/options-sorted-alphabetically.d.ts +2 -0
- package/dist/rules/options-sorted-alphabetically.d.ts.map +1 -0
- package/dist/rules/options-sorted-alphabetically.js +95 -0
- package/dist/rules/options-sorted-alphabetically.js.map +1 -0
- package/dist/rules/require-continue-on-fail.d.ts +2 -0
- package/dist/rules/require-continue-on-fail.d.ts.map +1 -0
- package/dist/rules/require-continue-on-fail.js +76 -0
- package/dist/rules/require-continue-on-fail.js.map +1 -0
- package/docs/rules/missing-paired-item.md +70 -0
- package/docs/rules/no-forbidden-lifecycle-scripts.md +46 -0
- package/docs/rules/node-connection-type-literal.md +46 -0
- package/docs/rules/options-sorted-alphabetically.md +63 -0
- package/docs/rules/require-continue-on-fail.md +56 -0
- package/package.json +9 -6
- package/src/plugin.ts +10 -0
- package/src/rules/index.ts +10 -0
- package/src/rules/missing-paired-item.test.ts +229 -0
- package/src/rules/missing-paired-item.ts +149 -0
- package/src/rules/no-forbidden-lifecycle-scripts.test.ts +103 -0
- package/src/rules/no-forbidden-lifecycle-scripts.ts +69 -0
- package/src/rules/node-connection-type-literal.test.ts +128 -0
- package/src/rules/node-connection-type-literal.ts +98 -0
- package/src/rules/options-sorted-alphabetically.test.ts +468 -0
- package/src/rules/options-sorted-alphabetically.ts +129 -0
- package/src/rules/require-continue-on-fail.test.ts +129 -0
- package/src/rules/require-continue-on-fail.ts +88 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { isNodeTypeClass, findClassProperty, findObjectProperty, createRule, } from '../utils/index.js';
|
|
4
|
+
// n8n-workflow's ESM dist uses bare module specifiers that Node's native ESM
|
|
5
|
+
// loader cannot resolve. Loading via CJS (createRequire) sidesteps this.
|
|
6
|
+
const { NodeConnectionTypes } = createRequire(import.meta.url)('n8n-workflow');
|
|
7
|
+
// Reverse map: string value (e.g. 'main') → enum key name (e.g. 'Main').
|
|
8
|
+
// Derived directly from NodeConnectionTypes so it stays in sync automatically.
|
|
9
|
+
const VALUE_TO_ENUM_KEY = Object.fromEntries(Object.entries(NodeConnectionTypes).map(([key, value]) => [value, key]));
|
|
10
|
+
export const NodeConnectionTypeLiteralRule = createRule({
|
|
11
|
+
name: 'node-connection-type-literal',
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'problem',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Disallow string literals in node description `inputs`/`outputs` — use `NodeConnectionTypes` enum instead',
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
stringLiteralInInputs: 'Use NodeConnectionTypes.{{enumKey}} from "n8n-workflow" instead of the string literal "{{value}}" in "inputs".',
|
|
19
|
+
stringLiteralInOutputs: 'Use NodeConnectionTypes.{{enumKey}} from "n8n-workflow" instead of the string literal "{{value}}" in "outputs".',
|
|
20
|
+
unknownStringLiteralInInputs: 'Use the NodeConnectionTypes enum from "n8n-workflow" instead of the string literal "{{value}}" in "inputs".',
|
|
21
|
+
unknownStringLiteralInOutputs: 'Use the NodeConnectionTypes enum from "n8n-workflow" instead of the string literal "{{value}}" in "outputs".',
|
|
22
|
+
},
|
|
23
|
+
fixable: 'code',
|
|
24
|
+
schema: [],
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: [],
|
|
27
|
+
create(context) {
|
|
28
|
+
function checkArrayElements(elements, property) {
|
|
29
|
+
for (const element of elements) {
|
|
30
|
+
if (element?.type !== AST_NODE_TYPES.Literal)
|
|
31
|
+
continue;
|
|
32
|
+
if (typeof element.value !== 'string')
|
|
33
|
+
continue;
|
|
34
|
+
const value = element.value;
|
|
35
|
+
const enumKey = VALUE_TO_ENUM_KEY[value];
|
|
36
|
+
if (enumKey) {
|
|
37
|
+
context.report({
|
|
38
|
+
node: element,
|
|
39
|
+
messageId: property === 'inputs' ? 'stringLiteralInInputs' : 'stringLiteralInOutputs',
|
|
40
|
+
data: { value, enumKey },
|
|
41
|
+
fix(fixer) {
|
|
42
|
+
return fixer.replaceText(element, `NodeConnectionTypes.${enumKey}`);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
context.report({
|
|
48
|
+
node: element,
|
|
49
|
+
messageId: property === 'inputs'
|
|
50
|
+
? 'unknownStringLiteralInInputs'
|
|
51
|
+
: 'unknownStringLiteralInOutputs',
|
|
52
|
+
data: { value },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
ClassDeclaration(node) {
|
|
59
|
+
if (!isNodeTypeClass(node))
|
|
60
|
+
return;
|
|
61
|
+
const descriptionProperty = findClassProperty(node, 'description');
|
|
62
|
+
if (!descriptionProperty)
|
|
63
|
+
return;
|
|
64
|
+
const descriptionValue = descriptionProperty.value;
|
|
65
|
+
if (descriptionValue?.type !== AST_NODE_TYPES.ObjectExpression)
|
|
66
|
+
return;
|
|
67
|
+
for (const prop of ['inputs', 'outputs']) {
|
|
68
|
+
const property = findObjectProperty(descriptionValue, prop);
|
|
69
|
+
if (property?.value.type !== AST_NODE_TYPES.ArrayExpression)
|
|
70
|
+
continue;
|
|
71
|
+
checkArrayElements(property.value.elements, prop);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
//# sourceMappingURL=node-connection-type-literal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-connection-type-literal.js","sourceRoot":"","sources":["../../src/rules/node-connection-type-literal.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options-sorted-alphabetically.d.ts","sourceRoot":"","sources":["../../src/rules/options-sorted-alphabetically.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,+BAA+B,qJAoH1C,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
import { isNodeTypeClass, findClassProperty, findObjectProperty, getStringLiteralValue, isFileType, createRule, } from '../utils/index.js';
|
|
3
|
+
export const OptionsSortedAlphabeticallyRule = createRule({
|
|
4
|
+
name: 'options-sorted-alphabetically',
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'suggestion',
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'Enforce alphabetical ordering of options arrays in n8n node properties',
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
optionsNotSorted: 'Options in "{{ displayName }}" are not sorted alphabetically. Expected order: {{ expectedOrder }}.',
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
},
|
|
15
|
+
defaultOptions: [],
|
|
16
|
+
create(context) {
|
|
17
|
+
if (!isFileType(context.filename, '.node.ts')) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
const analyzeNodeDescription = (descriptionValue) => {
|
|
21
|
+
if (!descriptionValue || descriptionValue.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const propertiesProperty = findObjectProperty(descriptionValue, 'properties');
|
|
25
|
+
if (!propertiesProperty?.value ||
|
|
26
|
+
propertiesProperty.value.type !== AST_NODE_TYPES.ArrayExpression) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
for (const property of propertiesProperty.value.elements) {
|
|
30
|
+
if (!property || property.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const typeProperty = findObjectProperty(property, 'type');
|
|
34
|
+
const type = typeProperty ? getStringLiteralValue(typeProperty.value) : null;
|
|
35
|
+
if (type !== 'options') {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const optionsProperty = findObjectProperty(property, 'options');
|
|
39
|
+
if (!optionsProperty?.value ||
|
|
40
|
+
optionsProperty.value.type !== AST_NODE_TYPES.ArrayExpression) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const optionsArray = optionsProperty.value;
|
|
44
|
+
// Extract all option names; skip if any name is non-literal (dynamic options)
|
|
45
|
+
const names = [];
|
|
46
|
+
let hasDynamicName = false;
|
|
47
|
+
for (const option of optionsArray.elements) {
|
|
48
|
+
if (!option || option.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
49
|
+
hasDynamicName = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
const nameProperty = findObjectProperty(option, 'name');
|
|
53
|
+
const name = nameProperty ? getStringLiteralValue(nameProperty.value) : null;
|
|
54
|
+
if (name === null) {
|
|
55
|
+
hasDynamicName = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
names.push(name);
|
|
59
|
+
}
|
|
60
|
+
if (hasDynamicName || names.length <= 1) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const sorted = [...names].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
|
|
64
|
+
const isSorted = names.every((name, i) => name === sorted[i]);
|
|
65
|
+
if (isSorted) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const displayNameProperty = findObjectProperty(property, 'displayName');
|
|
69
|
+
const displayName = (displayNameProperty ? getStringLiteralValue(displayNameProperty.value) : null) ??
|
|
70
|
+
'unknown';
|
|
71
|
+
context.report({
|
|
72
|
+
node: optionsArray,
|
|
73
|
+
messageId: 'optionsNotSorted',
|
|
74
|
+
data: {
|
|
75
|
+
displayName,
|
|
76
|
+
expectedOrder: sorted.join(', '),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
ClassDeclaration(node) {
|
|
83
|
+
if (!isNodeTypeClass(node)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const descriptionProperty = findClassProperty(node, 'description');
|
|
87
|
+
if (!descriptionProperty) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
analyzeNodeDescription(descriptionProperty.value);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
//# sourceMappingURL=options-sorted-alphabetically.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options-sorted-alphabetically.js","sourceRoot":"","sources":["../../src/rules/options-sorted-alphabetically.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,EACN,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,qBAAqB,EACrB,UAAU,EACV,UAAU,GACV,MAAM,mBAAmB,CAAC;AAE3B,MAAM,CAAC,MAAM,+BAA+B,GAAG,UAAU,CAAC;IACzD,IAAI,EAAE,+BAA+B;IACrC,IAAI,EAAE;QACL,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;YACL,WAAW,EAAE,wEAAwE;SACrF;QACD,QAAQ,EAAE;YACT,gBAAgB,EACf,oGAAoG;SACrG;QACD,MAAM,EAAE,EAAE;KACV;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACb,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,CAAC;YAC/C,OAAO,EAAE,CAAC;QACX,CAAC;QAED,MAAM,sBAAsB,GAAG,CAAC,gBAA4C,EAAQ,EAAE;YACrF,IAAI,CAAC,gBAAgB,IAAI,gBAAgB,CAAC,IAAI,KAAK,cAAc,CAAC,gBAAgB,EAAE,CAAC;gBACpF,OAAO;YACR,CAAC;YAED,MAAM,kBAAkB,GAAG,kBAAkB,CAAC,gBAAgB,EAAE,YAAY,CAAC,CAAC;YAC9E,IACC,CAAC,kBAAkB,EAAE,KAAK;gBAC1B,kBAAkB,CAAC,KAAK,CAAC,IAAI,KAAK,cAAc,CAAC,eAAe,EAC/D,CAAC;gBACF,OAAO;YACR,CAAC;YAED,KAAK,MAAM,QAAQ,IAAI,kBAAkB,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;gBAC1D,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,IAAI,KAAK,cAAc,CAAC,gBAAgB,EAAE,CAAC;oBACpE,SAAS;gBACV,CAAC;gBAED,MAAM,YAAY,GAAG,kBAAkB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;gBAC1D,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,qBAAqB,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBAE7E,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;oBACxB,SAAS;gBACV,CAAC;gBAED,MAAM,eAAe,GAAG,kBAAkB,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;gBAChE,IACC,CAAC,eAAe,EAAE,KAAK;oBACvB,eAAe,CAAC,KAAK,CAAC,IAAI,KAAK,cAAc,CAAC,eAAe,EAC5D,CAAC;oBACF,SAAS;gBACV,CAAC;gBAED,MAAM,YAAY,GAAG,eAAe,CAAC,KAAK,CAAC;gBAE3C,8EAA8E;gBAC9E,MAAM,KAAK,GAAa,EAAE,CAAC;gBAC3B,IAAI,cAAc,GAAG,KAAK,CAAC;gBAE3B,KAAK,MAAM,MAAM,IAAI,YAAY,CAAC,QAAQ,EAAE,CAAC;oBAC5C,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,cAAc,CAAC,gBAAgB,EAAE,CAAC;wBAChE,cAAc,GAAG,IAAI,CAAC;wBACtB,MAAM;oBACP,CAAC;oBACD,MAAM,YAAY,GAAG,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;oBACxD,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,qBAAqB,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;oBAC7E,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;wBACnB,cAAc,GAAG,IAAI,CAAC;wBACtB,MAAM;oBACP,CAAC;oBACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAClB,CAAC;gBAED,IAAI,cAAc,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;oBACzC,SAAS;gBACV,CAAC;gBAED,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACvC,CAAC,CAAC,aAAa,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,CACtD,CAAC;gBAEF,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC9D,IAAI,QAAQ,EAAE,CAAC;oBACd,SAAS;gBACV,CAAC;gBAED,MAAM,mBAAmB,GAAG,kBAAkB,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;gBACxE,MAAM,WAAW,GAChB,CAAC,mBAAmB,CAAC,CAAC,CAAC,qBAAqB,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;oBAC/E,SAAS,CAAC;gBAEX,OAAO,CAAC,MAAM,CAAC;oBACd,IAAI,EAAE,YAAY;oBAClB,SAAS,EAAE,kBAAkB;oBAC7B,IAAI,EAAE;wBACL,WAAW;wBACX,aAAa,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;qBAChC;iBACD,CAAC,CAAC;YACJ,CAAC;QACF,CAAC,CAAC;QAEF,OAAO;YACN,gBAAgB,CAAC,IAAI;gBACpB,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC5B,OAAO;gBACR,CAAC;gBAED,MAAM,mBAAmB,GAAG,iBAAiB,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;gBACnE,IAAI,CAAC,mBAAmB,EAAE,CAAC;oBAC1B,OAAO;gBACR,CAAC;gBAED,sBAAsB,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC;YACnD,CAAC;SACD,CAAC;IACH,CAAC;CACD,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-continue-on-fail.d.ts","sourceRoot":"","sources":["../../src/rules/require-continue-on-fail.ts"],"names":[],"mappings":"AA8CA,eAAO,MAAM,yBAAyB,0JAyCpC,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
import { isNodeTypeClass } from '../utils/index.js';
|
|
3
|
+
import { createRule } from '../utils/rule-creator.js';
|
|
4
|
+
/** Keys that are not child AST nodes (back-references, metadata). */
|
|
5
|
+
const NON_CHILD_KEYS = new Set(['parent', 'loc', 'range', 'start', 'end', 'tokens', 'comments']);
|
|
6
|
+
/**
|
|
7
|
+
* Recursively checks whether any descendant of the given AST node is a
|
|
8
|
+
* `this.continueOnFail()` call expression.
|
|
9
|
+
*/
|
|
10
|
+
function containsContinueOnFailCall(node) {
|
|
11
|
+
if (node.type === AST_NODE_TYPES.CallExpression &&
|
|
12
|
+
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
13
|
+
node.callee.object.type === AST_NODE_TYPES.ThisExpression &&
|
|
14
|
+
node.callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
15
|
+
node.callee.property.name === 'continueOnFail') {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
for (const key of Object.keys(node)) {
|
|
19
|
+
if (NON_CHILD_KEYS.has(key))
|
|
20
|
+
continue;
|
|
21
|
+
const value = node[key];
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
for (const child of value) {
|
|
24
|
+
if (child && typeof child === 'object' && 'type' in child) {
|
|
25
|
+
if (containsContinueOnFailCall(child)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (value && typeof value === 'object' && 'type' in value) {
|
|
32
|
+
if (containsContinueOnFailCall(value)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
export const RequireContinueOnFailRule = createRule({
|
|
40
|
+
name: 'require-continue-on-fail',
|
|
41
|
+
meta: {
|
|
42
|
+
type: 'problem',
|
|
43
|
+
docs: {
|
|
44
|
+
description: 'Require continueOnFail() handling in execute() methods of node classes',
|
|
45
|
+
},
|
|
46
|
+
messages: {
|
|
47
|
+
missingContinueOnFail: 'execute() method must handle this.continueOnFail() for proper error handling. ' +
|
|
48
|
+
'Wrap item processing in a try/catch and check this.continueOnFail() in the catch block.',
|
|
49
|
+
},
|
|
50
|
+
schema: [],
|
|
51
|
+
},
|
|
52
|
+
defaultOptions: [],
|
|
53
|
+
create(context) {
|
|
54
|
+
return {
|
|
55
|
+
ClassDeclaration(node) {
|
|
56
|
+
if (!isNodeTypeClass(node)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
for (const member of node.body.body) {
|
|
60
|
+
if (member.type !== AST_NODE_TYPES.MethodDefinition ||
|
|
61
|
+
member.key.type !== AST_NODE_TYPES.Identifier ||
|
|
62
|
+
member.key.name !== 'execute') {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (member.value.body && !containsContinueOnFailCall(member.value.body)) {
|
|
66
|
+
context.report({
|
|
67
|
+
node: member.key,
|
|
68
|
+
messageId: 'missingContinueOnFail',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
//# sourceMappingURL=require-continue-on-fail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-continue-on-fail.js","sourceRoot":"","sources":["../../src/rules/require-continue-on-fail.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Require pairedItem on INodeExecutionData objects in execute() methods to preserve item linking (`@n8n/community-nodes/missing-paired-item`)
|
|
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
|
+
Every `INodeExecutionData` object returned from `execute()` should include a `pairedItem` property. Without it, downstream nodes cannot trace data lineage and expressions like `$('NodeName').item` will silently fail.
|
|
10
|
+
|
|
11
|
+
The rule detects object literals with a `json` property but no `pairedItem` inside `execute()` methods of `INodeType` classes.
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
### Incorrect
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
class MyNode implements INodeType {
|
|
19
|
+
async execute() {
|
|
20
|
+
const items = this.getInputData();
|
|
21
|
+
// Missing pairedItem
|
|
22
|
+
return [items.map((item) => ({ json: item.json }))];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
class MyNode implements INodeType {
|
|
29
|
+
async execute() {
|
|
30
|
+
const returnData: INodeExecutionData[] = [];
|
|
31
|
+
// Missing pairedItem
|
|
32
|
+
returnData.push({ json: { result: true } });
|
|
33
|
+
return [returnData];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Correct
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
class MyNode implements INodeType {
|
|
42
|
+
async execute() {
|
|
43
|
+
const items = this.getInputData();
|
|
44
|
+
return [items.map((item, index) => ({ json: item.json, pairedItem: { item: index } }))];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
class MyNode implements INodeType {
|
|
51
|
+
async execute() {
|
|
52
|
+
const returnData: INodeExecutionData[] = [];
|
|
53
|
+
returnData.push({ json: { result: true }, pairedItem: { item: 0 } });
|
|
54
|
+
return [returnData];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## When to Disable
|
|
60
|
+
|
|
61
|
+
If your node intentionally does not support item linking (e.g. it aggregates all input items into a single output), you can suppress this rule:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// eslint-disable-next-line @n8n/community-nodes/missing-paired-item
|
|
65
|
+
returnData.push({ json: aggregatedResult });
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Further Reading
|
|
69
|
+
|
|
70
|
+
- [n8n Paired Items Documentation](https://docs.n8n.io/integrations/creating-nodes/build/reference/paired-items/)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Ban lifecycle scripts (prepare, preinstall, postinstall, etc.) in community node packages (`@n8n/community-nodes/no-forbidden-lifecycle-scripts`)
|
|
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
|
+
npm lifecycle scripts (`prepare`, `preinstall`, `install`, `postinstall`, `prepublish`, `preprepare`, `postprepare`) run automatically — without user confirmation — during `npm install`. In the context of n8n community nodes, this means arbitrary code executes on the n8n instance the moment a community node is installed.
|
|
10
|
+
|
|
11
|
+
n8n community nodes are distributed as pre-built npm packages. Unlike regular npm libraries, there is no legitimate reason for a community node to hook into install-time lifecycle events — the package should already contain compiled code ready to use. A `prepare` or `postinstall` script in a community node is either a misconfiguration (the author forgot to remove a build step meant for development) or a supply-chain attack vector.
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
### Incorrect
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"name": "n8n-nodes-example",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"prepare": "npm run build"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"name": "n8n-nodes-example",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"postinstall": "node setup.js"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Correct
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"name": "n8n-nodes-example",
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc",
|
|
43
|
+
"test": "jest"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Disallow string literals in node description `inputs`/`outputs` — use `NodeConnectionTypes` enum instead (`@n8n/community-nodes/node-connection-type-literal`)
|
|
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
|
+
Using raw string literals like `'main'` in `inputs` and `outputs` is fragile: the values are not type-checked, and typos or renamed connection types will go undetected. The `NodeConnectionTypes` object from `n8n-workflow` is the single source of truth and should always be used instead.
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
### ❌ Incorrect
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
19
|
+
|
|
20
|
+
export class MyNode implements INodeType {
|
|
21
|
+
description: INodeTypeDescription = {
|
|
22
|
+
displayName: 'My Node',
|
|
23
|
+
name: 'myNode',
|
|
24
|
+
inputs: ['main'],
|
|
25
|
+
outputs: ['main'],
|
|
26
|
+
properties: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### ✅ Correct
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
35
|
+
import { NodeConnectionTypes } from 'n8n-workflow';
|
|
36
|
+
|
|
37
|
+
export class MyNode implements INodeType {
|
|
38
|
+
description: INodeTypeDescription = {
|
|
39
|
+
displayName: 'My Node',
|
|
40
|
+
name: 'myNode',
|
|
41
|
+
inputs: [NodeConnectionTypes.Main],
|
|
42
|
+
outputs: [NodeConnectionTypes.Main],
|
|
43
|
+
properties: [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Enforce alphabetical ordering of options arrays in n8n node properties (`@n8n/community-nodes/options-sorted-alphabetically`)
|
|
2
|
+
|
|
3
|
+
⚠️ This rule _warns_ in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
|
|
4
|
+
|
|
5
|
+
<!-- end auto-generated rule header -->
|
|
6
|
+
|
|
7
|
+
## Rule Details
|
|
8
|
+
|
|
9
|
+
Warns when an `options`-type parameter has its options array not sorted alphabetically by name. Applies to all `type: 'options'` parameters — including `resource`, `operation`, and any other dropdowns.
|
|
10
|
+
|
|
11
|
+
Alphabetical ordering is an [official n8n UI design requirement](https://docs.n8n.io/integrations/creating-nodes/plan/node-ui-design/#lists) and the most frequently flagged issue in community node reviews.
|
|
12
|
+
|
|
13
|
+
Comparison is case-insensitive and locale-aware (handles non-ASCII names such as Spanish or Portuguese labels).
|
|
14
|
+
|
|
15
|
+
## Examples
|
|
16
|
+
|
|
17
|
+
### ❌ Incorrect
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
export class MyNode implements INodeType {
|
|
21
|
+
description: INodeTypeDescription = {
|
|
22
|
+
displayName: 'My Service',
|
|
23
|
+
name: 'myService',
|
|
24
|
+
properties: [
|
|
25
|
+
{
|
|
26
|
+
displayName: 'Resource',
|
|
27
|
+
name: 'resource',
|
|
28
|
+
type: 'options',
|
|
29
|
+
options: [
|
|
30
|
+
{ name: 'User', value: 'user' },
|
|
31
|
+
{ name: 'Contact', value: 'contact' }, // out of order
|
|
32
|
+
{ name: 'Project', value: 'project' },
|
|
33
|
+
],
|
|
34
|
+
default: 'user',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### ✅ Correct
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
export class MyNode implements INodeType {
|
|
45
|
+
description: INodeTypeDescription = {
|
|
46
|
+
displayName: 'My Service',
|
|
47
|
+
name: 'myService',
|
|
48
|
+
properties: [
|
|
49
|
+
{
|
|
50
|
+
displayName: 'Resource',
|
|
51
|
+
name: 'resource',
|
|
52
|
+
type: 'options',
|
|
53
|
+
options: [
|
|
54
|
+
{ name: 'Contact', value: 'contact' },
|
|
55
|
+
{ name: 'Project', value: 'project' },
|
|
56
|
+
{ name: 'User', value: 'user' },
|
|
57
|
+
],
|
|
58
|
+
default: 'contact',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Require continueOnFail() handling in execute() methods of node classes (`@n8n/community-nodes/require-continue-on-fail`)
|
|
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
|
+
Ensures that `execute()` methods in node classes include a `this.continueOnFail()` check. Without this, a single item error will abort the entire workflow instead of allowing execution to continue past the failing item.
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
### ❌ Incorrect
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
export class MyNode implements INodeType {
|
|
17
|
+
description: INodeTypeDescription = { /* ... */ };
|
|
18
|
+
|
|
19
|
+
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
20
|
+
const items = this.getInputData();
|
|
21
|
+
const returnData: INodeExecutionData[] = [];
|
|
22
|
+
for (let i = 0; i < items.length; i++) {
|
|
23
|
+
// No error handling — one bad item kills the whole workflow
|
|
24
|
+
const result = await someApiCall(items[i]);
|
|
25
|
+
returnData.push({ json: result });
|
|
26
|
+
}
|
|
27
|
+
return [returnData];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### ✅ Correct
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
export class MyNode implements INodeType {
|
|
36
|
+
description: INodeTypeDescription = { /* ... */ };
|
|
37
|
+
|
|
38
|
+
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
39
|
+
const items = this.getInputData();
|
|
40
|
+
const returnData: INodeExecutionData[] = [];
|
|
41
|
+
for (let i = 0; i < items.length; i++) {
|
|
42
|
+
try {
|
|
43
|
+
const result = await someApiCall(items[i]);
|
|
44
|
+
returnData.push({ json: result });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (this.continueOnFail()) {
|
|
47
|
+
returnData.push({ json: { error: error.message }, pairedItem: { item: i } });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [returnData];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
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.
|
|
4
|
+
"version": "0.12.0",
|
|
5
5
|
"main": "./dist/plugin.js",
|
|
6
6
|
"types": "./dist/plugin.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -15,17 +15,20 @@
|
|
|
15
15
|
"fastest-levenshtein": "1.0.16"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
+
"@types/node": "24.10.1",
|
|
18
19
|
"@typescript-eslint/rule-tester": "^8.35.0",
|
|
19
20
|
"eslint-doc-generator": "^2.2.2",
|
|
20
21
|
"eslint-plugin-eslint-plugin": "^7.0.0",
|
|
21
22
|
"rimraf": "6.0.1",
|
|
22
|
-
"typescript": "
|
|
23
|
-
"vitest": "^
|
|
24
|
-
"@n8n/typescript-config": "1.
|
|
25
|
-
"@n8n/vitest-config": "1.
|
|
23
|
+
"typescript": "6.0.2",
|
|
24
|
+
"vitest": "^4.1.1",
|
|
25
|
+
"@n8n/typescript-config": "1.4.0",
|
|
26
|
+
"@n8n/vitest-config": "1.9.0",
|
|
27
|
+
"n8n-workflow": "2.17.0"
|
|
26
28
|
},
|
|
27
29
|
"peerDependencies": {
|
|
28
|
-
"eslint": ">= 9"
|
|
30
|
+
"eslint": ">= 9",
|
|
31
|
+
"n8n-workflow": ">=2"
|
|
29
32
|
},
|
|
30
33
|
"eslint-doc-generator": {
|
|
31
34
|
"configEmoji": [
|
package/src/plugin.ts
CHANGED
|
@@ -29,12 +29,17 @@ const configs = {
|
|
|
29
29
|
'@n8n/community-nodes/package-name-convention': 'error',
|
|
30
30
|
'@n8n/community-nodes/credential-test-required': 'error',
|
|
31
31
|
'@n8n/community-nodes/no-credential-reuse': 'error',
|
|
32
|
+
'@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error',
|
|
32
33
|
'@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
|
|
33
34
|
'@n8n/community-nodes/icon-validation': 'error',
|
|
35
|
+
'@n8n/community-nodes/options-sorted-alphabetically': 'warn',
|
|
34
36
|
'@n8n/community-nodes/resource-operation-pattern': 'warn',
|
|
35
37
|
'@n8n/community-nodes/credential-documentation-url': 'error',
|
|
36
38
|
'@n8n/community-nodes/node-class-description-icon-missing': 'error',
|
|
37
39
|
'@n8n/community-nodes/cred-class-field-icon-missing': 'error',
|
|
40
|
+
'@n8n/community-nodes/node-connection-type-literal': 'error',
|
|
41
|
+
'@n8n/community-nodes/missing-paired-item': 'error',
|
|
42
|
+
'@n8n/community-nodes/require-continue-on-fail': 'error',
|
|
38
43
|
},
|
|
39
44
|
},
|
|
40
45
|
recommendedWithoutN8nCloudSupport: {
|
|
@@ -50,12 +55,17 @@ const configs = {
|
|
|
50
55
|
'@n8n/community-nodes/package-name-convention': 'error',
|
|
51
56
|
'@n8n/community-nodes/credential-test-required': 'error',
|
|
52
57
|
'@n8n/community-nodes/no-credential-reuse': 'error',
|
|
58
|
+
'@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error',
|
|
53
59
|
'@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
|
|
54
60
|
'@n8n/community-nodes/icon-validation': 'error',
|
|
61
|
+
'@n8n/community-nodes/options-sorted-alphabetically': 'warn',
|
|
55
62
|
'@n8n/community-nodes/credential-documentation-url': 'error',
|
|
56
63
|
'@n8n/community-nodes/resource-operation-pattern': 'warn',
|
|
57
64
|
'@n8n/community-nodes/node-class-description-icon-missing': 'error',
|
|
58
65
|
'@n8n/community-nodes/cred-class-field-icon-missing': 'error',
|
|
66
|
+
'@n8n/community-nodes/node-connection-type-literal': 'error',
|
|
67
|
+
'@n8n/community-nodes/missing-paired-item': 'error',
|
|
68
|
+
'@n8n/community-nodes/require-continue-on-fail': 'error',
|
|
59
69
|
},
|
|
60
70
|
},
|
|
61
71
|
} satisfies Record<string, Linter.Config>;
|