@n8n/eslint-plugin-community-nodes 0.11.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 +3 -0
- package/dist/plugin.d.ts +18 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +6 -0
- package/dist/plugin.js.map +1 -1
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +6 -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/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/require-continue-on-fail.md +56 -0
- package/package.json +2 -2
- package/src/plugin.ts +6 -0
- package/src/rules/index.ts +6 -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/require-continue-on-fail.test.ts +129 -0
- package/src/rules/require-continue-on-fail.ts +88 -0
- package/tsconfig.build.tsbuildinfo +1 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { NoForbiddenLifecycleScriptsRule } from './no-forbidden-lifecycle-scripts.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
ruleTester.run('no-forbidden-lifecycle-scripts', NoForbiddenLifecycleScriptsRule, {
|
|
8
|
+
valid: [
|
|
9
|
+
{
|
|
10
|
+
name: 'no scripts field',
|
|
11
|
+
filename: 'package.json',
|
|
12
|
+
code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'only safe scripts',
|
|
16
|
+
filename: 'package.json',
|
|
17
|
+
code: '{ "name": "n8n-nodes-example", "scripts": { "build": "tsc", "test": "jest", "dev": "nodemon" } }',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'non-package.json file is ignored',
|
|
21
|
+
filename: 'some-config.json',
|
|
22
|
+
code: '{ "scripts": { "prepare": "npm run build" } }',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'empty scripts object',
|
|
26
|
+
filename: 'package.json',
|
|
27
|
+
code: '{ "name": "n8n-nodes-example", "scripts": {} }',
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
invalid: [
|
|
31
|
+
{
|
|
32
|
+
name: 'prepare script is forbidden',
|
|
33
|
+
filename: 'package.json',
|
|
34
|
+
code: '{ "name": "n8n-nodes-example", "scripts": { "prepare": "npm run build" } }',
|
|
35
|
+
errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'prepare' } }],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'preinstall script is forbidden',
|
|
39
|
+
filename: 'package.json',
|
|
40
|
+
code: '{ "name": "n8n-nodes-example", "scripts": { "preinstall": "node setup.js" } }',
|
|
41
|
+
errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'preinstall' } }],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'install script is forbidden',
|
|
45
|
+
filename: 'package.json',
|
|
46
|
+
code: '{ "name": "n8n-nodes-example", "scripts": { "install": "node install.js" } }',
|
|
47
|
+
errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'install' } }],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'postinstall script is forbidden',
|
|
51
|
+
filename: 'package.json',
|
|
52
|
+
code: '{ "name": "n8n-nodes-example", "scripts": { "postinstall": "node setup.js" } }',
|
|
53
|
+
errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'postinstall' } }],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'prepublish script is forbidden',
|
|
57
|
+
filename: 'package.json',
|
|
58
|
+
code: '{ "name": "n8n-nodes-example", "scripts": { "prepublish": "npm run build" } }',
|
|
59
|
+
errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'prepublish' } }],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'preprepare script is forbidden',
|
|
63
|
+
filename: 'package.json',
|
|
64
|
+
code: '{ "name": "n8n-nodes-example", "scripts": { "preprepare": "echo prep" } }',
|
|
65
|
+
errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'preprepare' } }],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'postprepare script is forbidden',
|
|
69
|
+
filename: 'package.json',
|
|
70
|
+
code: '{ "name": "n8n-nodes-example", "scripts": { "postprepare": "echo done" } }',
|
|
71
|
+
errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'postprepare' } }],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'multiple forbidden scripts report separate errors',
|
|
75
|
+
filename: 'package.json',
|
|
76
|
+
code: '{ "name": "n8n-nodes-example", "scripts": { "prepare": "npm run build", "postinstall": "node setup.js" } }',
|
|
77
|
+
errors: [
|
|
78
|
+
{ messageId: 'forbiddenScript', data: { scriptName: 'prepare' } },
|
|
79
|
+
{ messageId: 'forbiddenScript', data: { scriptName: 'postinstall' } },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'mix of allowed and forbidden scripts — only forbidden reported',
|
|
84
|
+
filename: 'package.json',
|
|
85
|
+
code: '{ "name": "n8n-nodes-example", "scripts": { "build": "tsc", "prepare": "npm run build", "test": "jest" } }',
|
|
86
|
+
errors: [{ messageId: 'forbiddenScript', data: { scriptName: 'prepare' } }],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'all seven forbidden scripts present',
|
|
90
|
+
filename: 'package.json',
|
|
91
|
+
code: '{ "name": "n8n-nodes-example", "scripts": { "prepare": "a", "preinstall": "b", "install": "c", "postinstall": "d", "prepublish": "e", "preprepare": "f", "postprepare": "g" } }',
|
|
92
|
+
errors: [
|
|
93
|
+
{ messageId: 'forbiddenScript', data: { scriptName: 'prepare' } },
|
|
94
|
+
{ messageId: 'forbiddenScript', data: { scriptName: 'preinstall' } },
|
|
95
|
+
{ messageId: 'forbiddenScript', data: { scriptName: 'install' } },
|
|
96
|
+
{ messageId: 'forbiddenScript', data: { scriptName: 'postinstall' } },
|
|
97
|
+
{ messageId: 'forbiddenScript', data: { scriptName: 'prepublish' } },
|
|
98
|
+
{ messageId: 'forbiddenScript', data: { scriptName: 'preprepare' } },
|
|
99
|
+
{ messageId: 'forbiddenScript', data: { scriptName: 'postprepare' } },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
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 FORBIDDEN_SCRIPTS = [
|
|
7
|
+
'prepare',
|
|
8
|
+
'preinstall',
|
|
9
|
+
'install',
|
|
10
|
+
'postinstall',
|
|
11
|
+
'prepublish',
|
|
12
|
+
'preprepare',
|
|
13
|
+
'postprepare',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export const NoForbiddenLifecycleScriptsRule = createRule({
|
|
17
|
+
name: 'no-forbidden-lifecycle-scripts',
|
|
18
|
+
meta: {
|
|
19
|
+
type: 'problem',
|
|
20
|
+
docs: {
|
|
21
|
+
description:
|
|
22
|
+
'Ban lifecycle scripts (prepare, preinstall, postinstall, etc.) in community node packages',
|
|
23
|
+
},
|
|
24
|
+
messages: {
|
|
25
|
+
forbiddenScript:
|
|
26
|
+
'Lifecycle script "{{ scriptName }}" is not allowed in community node packages. These scripts execute arbitrary code during installation.',
|
|
27
|
+
},
|
|
28
|
+
schema: [],
|
|
29
|
+
},
|
|
30
|
+
defaultOptions: [],
|
|
31
|
+
create(context) {
|
|
32
|
+
if (!context.filename.endsWith('package.json')) {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
ObjectExpression(node: TSESTree.ObjectExpression) {
|
|
38
|
+
// Only process the root object, not nested ones
|
|
39
|
+
if (node.parent?.type === AST_NODE_TYPES.Property) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const scriptsProp = findJsonProperty(node, 'scripts');
|
|
44
|
+
if (!scriptsProp || scriptsProp.value.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const property of scriptsProp.value.properties) {
|
|
49
|
+
if (property.type !== AST_NODE_TYPES.Property) continue;
|
|
50
|
+
|
|
51
|
+
const key =
|
|
52
|
+
property.key.type === AST_NODE_TYPES.Identifier
|
|
53
|
+
? property.key.name
|
|
54
|
+
: property.key.type === AST_NODE_TYPES.Literal
|
|
55
|
+
? String(property.key.value)
|
|
56
|
+
: null;
|
|
57
|
+
|
|
58
|
+
if (key !== null && FORBIDDEN_SCRIPTS.includes(key)) {
|
|
59
|
+
context.report({
|
|
60
|
+
node: property,
|
|
61
|
+
messageId: 'forbiddenScript',
|
|
62
|
+
data: { scriptName: key },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
});
|