@n8n/eslint-plugin-community-nodes 0.24.0 → 0.25.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 +39 -52
- package/dist/plugin.d.ts +0 -78
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +0 -26
- package/dist/plugin.js.map +1 -1
- package/dist/rules/index.d.ts +0 -13
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +0 -26
- package/dist/rules/index.js.map +1 -1
- package/dist/typecheck.tsbuildinfo +1 -1
- package/docs/rules/no-forbidden-lifecycle-scripts.md +1 -1
- package/package.json +4 -4
- package/src/plugin.ts +0 -26
- package/src/rules/index.ts +0 -26
- package/dist/rules/cred-class-name-field-conventions.d.ts +0 -2
- package/dist/rules/cred-class-name-field-conventions.d.ts.map +0 -1
- package/dist/rules/cred-class-name-field-conventions.js +0 -89
- package/dist/rules/cred-class-name-field-conventions.js.map +0 -1
- package/dist/rules/cred-class-name-suffix.d.ts +0 -2
- package/dist/rules/cred-class-name-suffix.d.ts.map +0 -1
- package/dist/rules/cred-class-name-suffix.js +0 -53
- package/dist/rules/cred-class-name-suffix.js.map +0 -1
- package/dist/rules/cred-filename-against-convention.d.ts +0 -2
- package/dist/rules/cred-filename-against-convention.d.ts.map +0 -1
- package/dist/rules/cred-filename-against-convention.js +0 -42
- package/dist/rules/cred-filename-against-convention.js.map +0 -1
- package/dist/rules/no-asterisk-in-option-names.d.ts +0 -2
- package/dist/rules/no-asterisk-in-option-names.d.ts.map +0 -1
- package/dist/rules/no-asterisk-in-option-names.js +0 -90
- package/dist/rules/no-asterisk-in-option-names.js.map +0 -1
- package/dist/rules/no-duplicate-param-options.d.ts +0 -2
- package/dist/rules/no-duplicate-param-options.d.ts.map +0 -1
- package/dist/rules/no-duplicate-param-options.js +0 -97
- package/dist/rules/no-duplicate-param-options.js.map +0 -1
- package/dist/rules/node-class-description-name-camelcase.d.ts +0 -2
- package/dist/rules/node-class-description-name-camelcase.d.ts.map +0 -1
- package/dist/rules/node-class-description-name-camelcase.js +0 -92
- package/dist/rules/node-class-description-name-camelcase.js.map +0 -1
- package/dist/rules/node-filename-against-convention.d.ts +0 -2
- package/dist/rules/node-filename-against-convention.d.ts.map +0 -1
- package/dist/rules/node-filename-against-convention.js +0 -61
- package/dist/rules/node-filename-against-convention.js.map +0 -1
- package/dist/rules/node-operation-error-itemindex.d.ts +0 -12
- package/dist/rules/node-operation-error-itemindex.d.ts.map +0 -1
- package/dist/rules/node-operation-error-itemindex.js +0 -184
- package/dist/rules/node-operation-error-itemindex.js.map +0 -1
- package/dist/rules/options-sorted-alphabetically.d.ts +0 -2
- package/dist/rules/options-sorted-alphabetically.d.ts.map +0 -1
- package/dist/rules/options-sorted-alphabetically.js +0 -95
- package/dist/rules/options-sorted-alphabetically.js.map +0 -1
- package/dist/rules/require-community-node-keyword.d.ts +0 -2
- package/dist/rules/require-community-node-keyword.d.ts.map +0 -1
- package/dist/rules/require-community-node-keyword.js +0 -65
- package/dist/rules/require-community-node-keyword.js.map +0 -1
- package/dist/rules/require-continue-on-fail.d.ts +0 -2
- package/dist/rules/require-continue-on-fail.d.ts.map +0 -1
- package/dist/rules/require-continue-on-fail.js +0 -76
- package/dist/rules/require-continue-on-fail.js.map +0 -1
- package/dist/rules/require-mit-license.d.ts +0 -2
- package/dist/rules/require-mit-license.d.ts.map +0 -1
- package/dist/rules/require-mit-license.js +0 -57
- package/dist/rules/require-mit-license.js.map +0 -1
- package/dist/rules/require-param-default.d.ts +0 -15
- package/dist/rules/require-param-default.d.ts.map +0 -1
- package/dist/rules/require-param-default.js +0 -75
- package/dist/rules/require-param-default.js.map +0 -1
- package/docs/rules/cred-class-name-field-conventions.md +0 -38
- package/docs/rules/cred-class-name-suffix.md +0 -46
- package/docs/rules/cred-filename-against-convention.md +0 -42
- package/docs/rules/no-asterisk-in-option-names.md +0 -61
- package/docs/rules/no-duplicate-param-options.md +0 -46
- package/docs/rules/node-class-description-name-camelcase.md +0 -41
- package/docs/rules/node-filename-against-convention.md +0 -50
- package/docs/rules/node-operation-error-itemindex.md +0 -81
- package/docs/rules/options-sorted-alphabetically.md +0 -63
- package/docs/rules/require-community-node-keyword.md +0 -45
- package/docs/rules/require-continue-on-fail.md +0 -56
- package/docs/rules/require-mit-license.md +0 -38
- package/docs/rules/require-param-default.md +0 -60
- package/src/rules/cred-class-name-field-conventions.test.ts +0 -121
- package/src/rules/cred-class-name-field-conventions.ts +0 -102
- package/src/rules/cred-class-name-suffix.test.ts +0 -74
- package/src/rules/cred-class-name-suffix.ts +0 -57
- package/src/rules/cred-filename-against-convention.test.ts +0 -72
- package/src/rules/cred-filename-against-convention.ts +0 -48
- package/src/rules/no-asterisk-in-option-names.test.ts +0 -185
- package/src/rules/no-asterisk-in-option-names.ts +0 -111
- package/src/rules/no-duplicate-param-options.test.ts +0 -125
- package/src/rules/no-duplicate-param-options.ts +0 -125
- package/src/rules/node-class-description-name-camelcase.test.ts +0 -121
- package/src/rules/node-class-description-name-camelcase.ts +0 -114
- package/src/rules/node-filename-against-convention.test.ts +0 -115
- package/src/rules/node-filename-against-convention.ts +0 -76
- package/src/rules/node-operation-error-itemindex.test.ts +0 -280
- package/src/rules/node-operation-error-itemindex.ts +0 -223
- package/src/rules/options-sorted-alphabetically.test.ts +0 -468
- package/src/rules/options-sorted-alphabetically.ts +0 -129
- package/src/rules/require-community-node-keyword.test.ts +0 -82
- package/src/rules/require-community-node-keyword.ts +0 -78
- package/src/rules/require-continue-on-fail.test.ts +0 -129
- package/src/rules/require-continue-on-fail.ts +0 -88
- package/src/rules/require-mit-license.test.ts +0 -67
- package/src/rules/require-mit-license.ts +0 -65
- package/src/rules/require-param-default.test.ts +0 -227
- package/src/rules/require-param-default.ts +0 -89
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
isNodeTypeClass,
|
|
6
|
-
findClassProperty,
|
|
7
|
-
findObjectProperty,
|
|
8
|
-
getStringLiteralValue,
|
|
9
|
-
isFileType,
|
|
10
|
-
createRule,
|
|
11
|
-
} from '../utils/index.js';
|
|
12
|
-
|
|
13
|
-
export const OptionsSortedAlphabeticallyRule = createRule({
|
|
14
|
-
name: 'options-sorted-alphabetically',
|
|
15
|
-
meta: {
|
|
16
|
-
type: 'suggestion',
|
|
17
|
-
docs: {
|
|
18
|
-
description: 'Enforce alphabetical ordering of options arrays in n8n node properties',
|
|
19
|
-
},
|
|
20
|
-
messages: {
|
|
21
|
-
optionsNotSorted:
|
|
22
|
-
'Options in "{{ displayName }}" are not sorted alphabetically. Expected order: {{ expectedOrder }}.',
|
|
23
|
-
},
|
|
24
|
-
schema: [],
|
|
25
|
-
},
|
|
26
|
-
defaultOptions: [],
|
|
27
|
-
create(context) {
|
|
28
|
-
if (!isFileType(context.filename, '.node.ts')) {
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const analyzeNodeDescription = (descriptionValue: TSESTree.Expression | null): void => {
|
|
33
|
-
if (!descriptionValue || descriptionValue.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const propertiesProperty = findObjectProperty(descriptionValue, 'properties');
|
|
38
|
-
if (
|
|
39
|
-
!propertiesProperty?.value ||
|
|
40
|
-
propertiesProperty.value.type !== AST_NODE_TYPES.ArrayExpression
|
|
41
|
-
) {
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
for (const property of propertiesProperty.value.elements) {
|
|
46
|
-
if (!property || property.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const typeProperty = findObjectProperty(property, 'type');
|
|
51
|
-
const type = typeProperty ? getStringLiteralValue(typeProperty.value) : null;
|
|
52
|
-
|
|
53
|
-
if (type !== 'options') {
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const optionsProperty = findObjectProperty(property, 'options');
|
|
58
|
-
if (
|
|
59
|
-
!optionsProperty?.value ||
|
|
60
|
-
optionsProperty.value.type !== AST_NODE_TYPES.ArrayExpression
|
|
61
|
-
) {
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const optionsArray = optionsProperty.value;
|
|
66
|
-
|
|
67
|
-
// Extract all option names; skip if any name is non-literal (dynamic options)
|
|
68
|
-
const names: string[] = [];
|
|
69
|
-
let hasDynamicName = false;
|
|
70
|
-
|
|
71
|
-
for (const option of optionsArray.elements) {
|
|
72
|
-
if (!option || option.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
73
|
-
hasDynamicName = true;
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
const nameProperty = findObjectProperty(option, 'name');
|
|
77
|
-
const name = nameProperty ? getStringLiteralValue(nameProperty.value) : null;
|
|
78
|
-
if (name === null) {
|
|
79
|
-
hasDynamicName = true;
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
names.push(name);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (hasDynamicName || names.length <= 1) {
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const sorted = [...names].sort((a, b) =>
|
|
90
|
-
a.localeCompare(b, undefined, { sensitivity: 'base' }),
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
const isSorted = names.every((name, i) => name === sorted[i]);
|
|
94
|
-
if (isSorted) {
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const displayNameProperty = findObjectProperty(property, 'displayName');
|
|
99
|
-
const displayName =
|
|
100
|
-
(displayNameProperty ? getStringLiteralValue(displayNameProperty.value) : null) ??
|
|
101
|
-
'unknown';
|
|
102
|
-
|
|
103
|
-
context.report({
|
|
104
|
-
node: optionsArray,
|
|
105
|
-
messageId: 'optionsNotSorted',
|
|
106
|
-
data: {
|
|
107
|
-
displayName,
|
|
108
|
-
expectedOrder: sorted.join(', '),
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
ClassDeclaration(node) {
|
|
116
|
-
if (!isNodeTypeClass(node)) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const descriptionProperty = findClassProperty(node, 'description');
|
|
121
|
-
if (!descriptionProperty) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
analyzeNodeDescription(descriptionProperty.value);
|
|
126
|
-
},
|
|
127
|
-
};
|
|
128
|
-
},
|
|
129
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
-
|
|
3
|
-
import { RequireCommunityNodeKeywordRule } from './require-community-node-keyword.js';
|
|
4
|
-
|
|
5
|
-
const ruleTester = new RuleTester();
|
|
6
|
-
|
|
7
|
-
ruleTester.run('require-community-node-keyword', RequireCommunityNodeKeywordRule, {
|
|
8
|
-
valid: [
|
|
9
|
-
{
|
|
10
|
-
name: 'keywords array contains required keyword',
|
|
11
|
-
filename: 'package.json',
|
|
12
|
-
code: '{ "name": "n8n-nodes-example", "keywords": ["n8n-community-node-package"] }',
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
name: 'keywords array contains required keyword among others',
|
|
16
|
-
filename: 'package.json',
|
|
17
|
-
code: '{ "name": "n8n-nodes-example", "keywords": ["n8n", "automation", "n8n-community-node-package", "workflow"] }',
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
name: 'non-package.json file is ignored',
|
|
21
|
-
filename: 'some-config.json',
|
|
22
|
-
code: '{ "name": "n8n-nodes-example" }',
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
name: 'nested objects are not checked',
|
|
26
|
-
filename: 'package.json',
|
|
27
|
-
code: '{ "name": "n8n-nodes-example", "keywords": ["n8n-community-node-package"], "config": { "nested": "value" } }',
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
name: 'objects inside arrays (e.g. contributors) are not flagged',
|
|
31
|
-
filename: 'package.json',
|
|
32
|
-
code: `{
|
|
33
|
-
"name": "n8n-nodes-example",
|
|
34
|
-
"keywords": ["n8n-community-node-package"],
|
|
35
|
-
"contributors": [
|
|
36
|
-
{ "name": "Alice", "email": "alice@example.com" },
|
|
37
|
-
{ "name": "Bob", "email": "bob@example.com" }
|
|
38
|
-
]
|
|
39
|
-
}`,
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
invalid: [
|
|
43
|
-
{
|
|
44
|
-
name: 'missing keywords array entirely',
|
|
45
|
-
filename: 'package.json',
|
|
46
|
-
code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
|
|
47
|
-
output:
|
|
48
|
-
'{ "name": "n8n-nodes-example", "version": "1.0.0", "keywords": ["n8n-community-node-package"] }',
|
|
49
|
-
errors: [{ messageId: 'missingKeywordsArray' }],
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
name: 'empty keywords array',
|
|
53
|
-
filename: 'package.json',
|
|
54
|
-
code: '{ "name": "n8n-nodes-example", "keywords": [] }',
|
|
55
|
-
output: '{ "name": "n8n-nodes-example", "keywords": ["n8n-community-node-package"] }',
|
|
56
|
-
errors: [{ messageId: 'missingKeyword' }],
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: 'keywords array without the required keyword',
|
|
60
|
-
filename: 'package.json',
|
|
61
|
-
code: '{ "name": "n8n-nodes-example", "keywords": ["n8n", "automation"] }',
|
|
62
|
-
output:
|
|
63
|
-
'{ "name": "n8n-nodes-example", "keywords": ["n8n", "automation", "n8n-community-node-package"] }',
|
|
64
|
-
errors: [{ messageId: 'missingKeyword' }],
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
name: 'keywords array with similar but incorrect keyword',
|
|
68
|
-
filename: 'package.json',
|
|
69
|
-
code: '{ "name": "n8n-nodes-example", "keywords": ["n8n-community-node"] }',
|
|
70
|
-
output:
|
|
71
|
-
'{ "name": "n8n-nodes-example", "keywords": ["n8n-community-node", "n8n-community-node-package"] }',
|
|
72
|
-
errors: [{ messageId: 'missingKeyword' }],
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
name: 'empty package.json object',
|
|
76
|
-
filename: 'package.json',
|
|
77
|
-
code: '{}',
|
|
78
|
-
output: '{ "keywords": ["n8n-community-node-package"] }',
|
|
79
|
-
errors: [{ messageId: 'missingKeywordsArray' }],
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
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
|
-
const REQUIRED_KEYWORD = 'n8n-community-node-package';
|
|
7
|
-
|
|
8
|
-
export const RequireCommunityNodeKeywordRule = createRule({
|
|
9
|
-
name: 'require-community-node-keyword',
|
|
10
|
-
meta: {
|
|
11
|
-
type: 'problem',
|
|
12
|
-
docs: {
|
|
13
|
-
description:
|
|
14
|
-
'Require the "n8n-community-node-package" keyword in community node package.json',
|
|
15
|
-
},
|
|
16
|
-
fixable: 'code',
|
|
17
|
-
messages: {
|
|
18
|
-
missingKeyword: `The "keywords" array must include "${REQUIRED_KEYWORD}". This keyword is required for n8n to discover community node packages.`,
|
|
19
|
-
missingKeywordsArray: `The package.json must have a "keywords" array containing "${REQUIRED_KEYWORD}".`,
|
|
20
|
-
},
|
|
21
|
-
schema: [],
|
|
22
|
-
},
|
|
23
|
-
defaultOptions: [],
|
|
24
|
-
create(context) {
|
|
25
|
-
if (!context.filename.endsWith('package.json')) {
|
|
26
|
-
return {};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
ObjectExpression(node: TSESTree.ObjectExpression) {
|
|
31
|
-
if (node.parent?.type !== AST_NODE_TYPES.ExpressionStatement) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const keywordsProp = findJsonProperty(node, 'keywords');
|
|
36
|
-
|
|
37
|
-
if (!keywordsProp) {
|
|
38
|
-
context.report({
|
|
39
|
-
node,
|
|
40
|
-
messageId: 'missingKeywordsArray',
|
|
41
|
-
fix(fixer) {
|
|
42
|
-
const lastProp = node.properties[node.properties.length - 1];
|
|
43
|
-
if (!lastProp) {
|
|
44
|
-
return fixer.replaceText(node, `{ "keywords": ["${REQUIRED_KEYWORD}"] }`);
|
|
45
|
-
}
|
|
46
|
-
return fixer.insertTextAfter(lastProp, `, "keywords": ["${REQUIRED_KEYWORD}"]`);
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (keywordsProp.value.type !== AST_NODE_TYPES.ArrayExpression) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const keywordsArray = keywordsProp.value;
|
|
57
|
-
const hasRequiredKeyword = keywordsArray.elements.some(
|
|
58
|
-
(element) =>
|
|
59
|
-
element?.type === AST_NODE_TYPES.Literal && element.value === REQUIRED_KEYWORD,
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
if (!hasRequiredKeyword) {
|
|
63
|
-
context.report({
|
|
64
|
-
node: keywordsProp,
|
|
65
|
-
messageId: 'missingKeyword',
|
|
66
|
-
fix(fixer) {
|
|
67
|
-
const lastElement = keywordsArray.elements[keywordsArray.elements.length - 1];
|
|
68
|
-
if (!lastElement) {
|
|
69
|
-
return fixer.replaceText(keywordsArray, `["${REQUIRED_KEYWORD}"]`);
|
|
70
|
-
}
|
|
71
|
-
return fixer.insertTextAfter(lastElement, `, "${REQUIRED_KEYWORD}"`);
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
},
|
|
78
|
-
});
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
-
|
|
3
|
-
import { RequireContinueOnFailRule } from './require-continue-on-fail.js';
|
|
4
|
-
|
|
5
|
-
const ruleTester = new RuleTester();
|
|
6
|
-
|
|
7
|
-
function createNodeWithExecute(executeBody: string): string {
|
|
8
|
-
return `
|
|
9
|
-
import type { INodeType, INodeTypeDescription, IExecuteFunctions, INodeExecutionData } 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: ['main'],
|
|
20
|
-
outputs: ['main'],
|
|
21
|
-
properties: [],
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
25
|
-
${executeBody}
|
|
26
|
-
}
|
|
27
|
-
}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
ruleTester.run('require-continue-on-fail', RequireContinueOnFailRule, {
|
|
31
|
-
valid: [
|
|
32
|
-
{
|
|
33
|
-
name: 'node with continueOnFail in catch block',
|
|
34
|
-
code: createNodeWithExecute(`
|
|
35
|
-
const items = this.getInputData();
|
|
36
|
-
const returnData: INodeExecutionData[] = [];
|
|
37
|
-
for (let i = 0; i < items.length; i++) {
|
|
38
|
-
try {
|
|
39
|
-
returnData.push({ json: { result: true } });
|
|
40
|
-
} catch (error) {
|
|
41
|
-
if (this.continueOnFail()) {
|
|
42
|
-
returnData.push({ json: { error: error.message } });
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
throw error;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return [returnData];
|
|
49
|
-
`),
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
name: 'non-node class with execute method (should be ignored)',
|
|
53
|
-
code: `
|
|
54
|
-
export class RegularClass {
|
|
55
|
-
async execute() {
|
|
56
|
-
return [[]];
|
|
57
|
-
}
|
|
58
|
-
}`,
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
name: 'node class without execute method (should be ignored)',
|
|
62
|
-
code: `
|
|
63
|
-
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
64
|
-
|
|
65
|
-
export class TestNode implements INodeType {
|
|
66
|
-
description: INodeTypeDescription = {
|
|
67
|
-
displayName: 'Test Node',
|
|
68
|
-
name: 'testNode',
|
|
69
|
-
group: ['input'],
|
|
70
|
-
version: 1,
|
|
71
|
-
description: 'A test node',
|
|
72
|
-
defaults: { name: 'Test Node' },
|
|
73
|
-
inputs: ['main'],
|
|
74
|
-
outputs: ['main'],
|
|
75
|
-
properties: [],
|
|
76
|
-
};
|
|
77
|
-
}`,
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
name: 'node extending Node base class with continueOnFail',
|
|
81
|
-
code: `
|
|
82
|
-
import { Node } from 'n8n-workflow';
|
|
83
|
-
|
|
84
|
-
export class TestNode extends Node {
|
|
85
|
-
async execute() {
|
|
86
|
-
try {
|
|
87
|
-
// do work
|
|
88
|
-
} catch (error) {
|
|
89
|
-
if (this.continueOnFail()) {
|
|
90
|
-
return [[]];
|
|
91
|
-
}
|
|
92
|
-
throw error;
|
|
93
|
-
}
|
|
94
|
-
return [[]];
|
|
95
|
-
}
|
|
96
|
-
}`,
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
invalid: [
|
|
100
|
-
{
|
|
101
|
-
name: 'node with execute but no continueOnFail',
|
|
102
|
-
code: createNodeWithExecute(`
|
|
103
|
-
const items = this.getInputData();
|
|
104
|
-
const returnData: INodeExecutionData[] = [];
|
|
105
|
-
for (let i = 0; i < items.length; i++) {
|
|
106
|
-
returnData.push({ json: { result: true } });
|
|
107
|
-
}
|
|
108
|
-
return [returnData];
|
|
109
|
-
`),
|
|
110
|
-
errors: [{ messageId: 'missingContinueOnFail' }],
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
name: 'node with try/catch but no continueOnFail check',
|
|
114
|
-
code: createNodeWithExecute(`
|
|
115
|
-
const items = this.getInputData();
|
|
116
|
-
const returnData: INodeExecutionData[] = [];
|
|
117
|
-
for (let i = 0; i < items.length; i++) {
|
|
118
|
-
try {
|
|
119
|
-
returnData.push({ json: { result: true } });
|
|
120
|
-
} catch (error) {
|
|
121
|
-
throw error;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return [returnData];
|
|
125
|
-
`),
|
|
126
|
-
errors: [{ messageId: 'missingContinueOnFail' }],
|
|
127
|
-
},
|
|
128
|
-
],
|
|
129
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
|
|
3
|
-
import { isNodeTypeClass } from '../utils/index.js';
|
|
4
|
-
import { createRule } from '../utils/rule-creator.js';
|
|
5
|
-
|
|
6
|
-
/** Keys that are not child AST nodes (back-references, metadata). */
|
|
7
|
-
const NON_CHILD_KEYS = new Set(['parent', 'loc', 'range', 'start', 'end', 'tokens', 'comments']);
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Recursively checks whether any descendant of the given AST node is a
|
|
11
|
-
* `this.continueOnFail()` call expression.
|
|
12
|
-
*/
|
|
13
|
-
function containsContinueOnFailCall(node: TSESTree.Node): boolean {
|
|
14
|
-
if (
|
|
15
|
-
node.type === AST_NODE_TYPES.CallExpression &&
|
|
16
|
-
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
17
|
-
node.callee.object.type === AST_NODE_TYPES.ThisExpression &&
|
|
18
|
-
node.callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
19
|
-
node.callee.property.name === 'continueOnFail'
|
|
20
|
-
) {
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
for (const key of Object.keys(node)) {
|
|
25
|
-
if (NON_CHILD_KEYS.has(key)) continue;
|
|
26
|
-
|
|
27
|
-
const value = (node as unknown as Record<string, unknown>)[key];
|
|
28
|
-
|
|
29
|
-
if (Array.isArray(value)) {
|
|
30
|
-
for (const child of value) {
|
|
31
|
-
if (child && typeof child === 'object' && 'type' in child) {
|
|
32
|
-
if (containsContinueOnFailCall(child as TSESTree.Node)) {
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
} else if (value && typeof value === 'object' && 'type' in value) {
|
|
38
|
-
if (containsContinueOnFailCall(value as TSESTree.Node)) {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const RequireContinueOnFailRule = createRule({
|
|
48
|
-
name: 'require-continue-on-fail',
|
|
49
|
-
meta: {
|
|
50
|
-
type: 'problem',
|
|
51
|
-
docs: {
|
|
52
|
-
description: 'Require continueOnFail() handling in execute() methods of node classes',
|
|
53
|
-
},
|
|
54
|
-
messages: {
|
|
55
|
-
missingContinueOnFail:
|
|
56
|
-
'execute() method must handle this.continueOnFail() for proper error handling. ' +
|
|
57
|
-
'Wrap item processing in a try/catch and check this.continueOnFail() in the catch block.',
|
|
58
|
-
},
|
|
59
|
-
schema: [],
|
|
60
|
-
},
|
|
61
|
-
defaultOptions: [],
|
|
62
|
-
create(context) {
|
|
63
|
-
return {
|
|
64
|
-
ClassDeclaration(node) {
|
|
65
|
-
if (!isNodeTypeClass(node)) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (const member of node.body.body) {
|
|
70
|
-
if (
|
|
71
|
-
member.type !== AST_NODE_TYPES.MethodDefinition ||
|
|
72
|
-
member.key.type !== AST_NODE_TYPES.Identifier ||
|
|
73
|
-
member.key.name !== 'execute'
|
|
74
|
-
) {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (member.value.body && !containsContinueOnFailCall(member.value.body)) {
|
|
79
|
-
context.report({
|
|
80
|
-
node: member.key,
|
|
81
|
-
messageId: 'missingContinueOnFail',
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
},
|
|
88
|
-
});
|
|
@@ -1,67 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
});
|