@n8n/eslint-plugin-community-nodes 0.8.0 → 0.10.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 +17 -13
- 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/cred-class-field-icon-missing.d.ts +2 -0
- package/dist/rules/cred-class-field-icon-missing.d.ts.map +1 -0
- package/dist/rules/cred-class-field-icon-missing.js +51 -0
- package/dist/rules/cred-class-field-icon-missing.js.map +1 -0
- package/dist/rules/icon-validation.d.ts +1 -1
- package/dist/rules/index.d.ts +4 -1
- 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/no-deprecated-workflow-functions.d.ts.map +1 -1
- package/dist/rules/no-deprecated-workflow-functions.js +1 -13
- package/dist/rules/no-deprecated-workflow-functions.js.map +1 -1
- package/dist/rules/no-http-request-with-manual-auth.d.ts +19 -0
- package/dist/rules/no-http-request-with-manual-auth.d.ts.map +1 -0
- package/dist/rules/no-http-request-with-manual-auth.js +65 -0
- package/dist/rules/no-http-request-with-manual-auth.js.map +1 -0
- package/dist/rules/node-class-description-icon-missing.d.ts +2 -0
- package/dist/rules/node-class-description-icon-missing.d.ts.map +1 -0
- package/dist/rules/node-class-description-icon-missing.js +57 -0
- package/dist/rules/node-class-description-icon-missing.js.map +1 -0
- package/dist/utils/ast-utils.d.ts +6 -0
- package/dist/utils/ast-utils.d.ts.map +1 -1
- package/dist/utils/ast-utils.js +21 -0
- package/dist/utils/ast-utils.js.map +1 -1
- package/docs/rules/ai-node-package-json.md +62 -0
- package/docs/rules/cred-class-field-icon-missing.md +47 -0
- package/docs/rules/credential-documentation-url.md +5 -3
- package/docs/rules/no-http-request-with-manual-auth.md +57 -0
- package/docs/rules/node-class-description-icon-missing.md +52 -0
- package/package.json +1 -1
- package/src/plugin.ts +6 -0
- package/src/rules/cred-class-field-icon-missing.test.ts +96 -0
- package/src/rules/cred-class-field-icon-missing.ts +59 -0
- package/src/rules/index.ts +6 -0
- package/src/rules/no-deprecated-workflow-functions.ts +1 -17
- package/src/rules/no-http-request-with-manual-auth.test.ts +104 -0
- package/src/rules/no-http-request-with-manual-auth.ts +78 -0
- package/src/rules/node-class-description-icon-missing.test.ts +113 -0
- package/src/rules/node-class-description-icon-missing.ts +71 -0
- package/src/utils/ast-utils.ts +30 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flags `this.helpers.httpRequest()` in functions that also call `this.getCredentials()`.
|
|
3
|
+
* Those functions should use `this.helpers.httpRequestWithAuthentication()` instead.
|
|
4
|
+
*
|
|
5
|
+
* Uses a function-scope stack: if both `getCredentials` and `httpRequest` appear in
|
|
6
|
+
* the same function body, every `httpRequest` call is reported. Nested functions are
|
|
7
|
+
* checked independently.
|
|
8
|
+
*
|
|
9
|
+
* Alternatives considered:
|
|
10
|
+
* - Checking for credential variables in `httpRequest` arguments — misses the common
|
|
11
|
+
* pattern where options are built in a separate variable first.
|
|
12
|
+
* - Matching auth header names (`Authorization`, etc.) — brittle and requires deep
|
|
13
|
+
* AST traversal with no guarantee of coverage.
|
|
14
|
+
*
|
|
15
|
+
* Known false positive: a function that fetches credentials for a non-HTTP purpose
|
|
16
|
+
* and also makes an unauthenticated request. Use eslint-disable to suppress.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
20
|
+
|
|
21
|
+
import { createRule, isThisHelpersMethodCall, isThisMethodCall } from '../utils/index.js';
|
|
22
|
+
|
|
23
|
+
type FunctionScope = {
|
|
24
|
+
getCredentialsCall: TSESTree.CallExpression | null;
|
|
25
|
+
httpRequestCalls: TSESTree.CallExpression[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const NoHttpRequestWithManualAuthRule = createRule({
|
|
29
|
+
name: 'no-http-request-with-manual-auth',
|
|
30
|
+
meta: {
|
|
31
|
+
type: 'suggestion',
|
|
32
|
+
docs: {
|
|
33
|
+
description:
|
|
34
|
+
'Disallow this.helpers.httpRequest() in functions that call this.getCredentials(). Use this.helpers.httpRequestWithAuthentication() instead.',
|
|
35
|
+
},
|
|
36
|
+
messages: {
|
|
37
|
+
useHttpRequestWithAuthentication:
|
|
38
|
+
"Avoid calling 'this.helpers.httpRequest()' in a function that retrieves credentials via 'this.getCredentials()'. Use 'this.helpers.httpRequestWithAuthentication()' instead — it handles authentication internally and benefits from future n8n improvements like token refresh and audit logging.",
|
|
39
|
+
},
|
|
40
|
+
schema: [],
|
|
41
|
+
hasSuggestions: false,
|
|
42
|
+
},
|
|
43
|
+
defaultOptions: [],
|
|
44
|
+
create(context) {
|
|
45
|
+
const scopeStack: FunctionScope[] = [];
|
|
46
|
+
|
|
47
|
+
const pushScope = () => scopeStack.push({ getCredentialsCall: null, httpRequestCalls: [] });
|
|
48
|
+
|
|
49
|
+
const popScope = () => {
|
|
50
|
+
const scope = scopeStack.pop();
|
|
51
|
+
if (scope?.getCredentialsCall && scope.httpRequestCalls.length > 0) {
|
|
52
|
+
for (const call of scope.httpRequestCalls) {
|
|
53
|
+
context.report({ node: call, messageId: 'useHttpRequestWithAuthentication' });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
FunctionDeclaration: pushScope,
|
|
60
|
+
FunctionExpression: pushScope,
|
|
61
|
+
ArrowFunctionExpression: pushScope,
|
|
62
|
+
'FunctionDeclaration:exit': popScope,
|
|
63
|
+
'FunctionExpression:exit': popScope,
|
|
64
|
+
'ArrowFunctionExpression:exit': popScope,
|
|
65
|
+
|
|
66
|
+
CallExpression(node: TSESTree.CallExpression) {
|
|
67
|
+
const scope = scopeStack[scopeStack.length - 1];
|
|
68
|
+
if (!scope) return;
|
|
69
|
+
if (isThisMethodCall(node, 'getCredentials')) {
|
|
70
|
+
scope.getCredentialsCall = node;
|
|
71
|
+
}
|
|
72
|
+
if (isThisHelpersMethodCall(node, 'httpRequest')) {
|
|
73
|
+
scope.httpRequestCalls.push(node);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { NodeClassDescriptionIconMissingRule } from './node-class-description-icon-missing.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
const nodeFilePath = '/tmp/TestNode.node.ts';
|
|
8
|
+
const nonNodeFilePath = '/tmp/SomeHelper.ts';
|
|
9
|
+
|
|
10
|
+
function createNodeCode(withIcon: boolean): string {
|
|
11
|
+
const iconLine = withIcon ? "\n\t\ticon: 'file:testNode.svg'," : '';
|
|
12
|
+
return `
|
|
13
|
+
import type { INodeType } from 'n8n-workflow';
|
|
14
|
+
|
|
15
|
+
export class TestNode implements INodeType {
|
|
16
|
+
description = {
|
|
17
|
+
displayName: 'Test Node',
|
|
18
|
+
name: 'testNode',
|
|
19
|
+
group: ['transform'],
|
|
20
|
+
version: 1,
|
|
21
|
+
description: 'Test',${iconLine}
|
|
22
|
+
inputs: ['main'],
|
|
23
|
+
outputs: ['main'],
|
|
24
|
+
properties: [],
|
|
25
|
+
};
|
|
26
|
+
}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createNodeCodeWithLightDarkIcon(): string {
|
|
30
|
+
return `
|
|
31
|
+
import type { INodeType } from 'n8n-workflow';
|
|
32
|
+
|
|
33
|
+
export class TestNode implements INodeType {
|
|
34
|
+
description = {
|
|
35
|
+
displayName: 'Test Node',
|
|
36
|
+
name: 'testNode',
|
|
37
|
+
group: ['transform'],
|
|
38
|
+
version: 1,
|
|
39
|
+
description: 'Test',
|
|
40
|
+
icon: { light: 'file:testNode.svg', dark: 'file:testNode.dark.svg' },
|
|
41
|
+
inputs: ['main'],
|
|
42
|
+
outputs: ['main'],
|
|
43
|
+
properties: [],
|
|
44
|
+
};
|
|
45
|
+
}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createRegularClass(): string {
|
|
49
|
+
return `
|
|
50
|
+
export class RegularClass {
|
|
51
|
+
description = {
|
|
52
|
+
displayName: 'Test',
|
|
53
|
+
};
|
|
54
|
+
}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ruleTester.run('node-class-description-icon-missing', NodeClassDescriptionIconMissingRule, {
|
|
58
|
+
valid: [
|
|
59
|
+
{
|
|
60
|
+
name: 'node with icon defined',
|
|
61
|
+
filename: nodeFilePath,
|
|
62
|
+
code: createNodeCode(true),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'node with light/dark icon object',
|
|
66
|
+
filename: nodeFilePath,
|
|
67
|
+
code: createNodeCodeWithLightDarkIcon(),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'class not implementing INodeType is ignored',
|
|
71
|
+
filename: nodeFilePath,
|
|
72
|
+
code: createRegularClass(),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'non-.node.ts file is ignored',
|
|
76
|
+
filename: nonNodeFilePath,
|
|
77
|
+
code: createNodeCode(false),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
invalid: [
|
|
81
|
+
{
|
|
82
|
+
name: 'node missing icon property',
|
|
83
|
+
filename: nodeFilePath,
|
|
84
|
+
code: createNodeCode(false),
|
|
85
|
+
errors: [
|
|
86
|
+
{
|
|
87
|
+
messageId: 'missingIcon',
|
|
88
|
+
suggestions: [
|
|
89
|
+
{
|
|
90
|
+
messageId: 'addPlaceholder',
|
|
91
|
+
output: `
|
|
92
|
+
import type { INodeType } from 'n8n-workflow';
|
|
93
|
+
|
|
94
|
+
export class TestNode implements INodeType {
|
|
95
|
+
description = {
|
|
96
|
+
displayName: 'Test Node',
|
|
97
|
+
name: 'testNode',
|
|
98
|
+
group: ['transform'],
|
|
99
|
+
version: 1,
|
|
100
|
+
description: 'Test',
|
|
101
|
+
inputs: ['main'],
|
|
102
|
+
outputs: ['main'],
|
|
103
|
+
properties: [],
|
|
104
|
+
icon: "file:./icon.svg",
|
|
105
|
+
};
|
|
106
|
+
}`,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isNodeTypeClass,
|
|
5
|
+
findClassProperty,
|
|
6
|
+
findObjectProperty,
|
|
7
|
+
isFileType,
|
|
8
|
+
createRule,
|
|
9
|
+
} from '../utils/index.js';
|
|
10
|
+
|
|
11
|
+
export const NodeClassDescriptionIconMissingRule = createRule({
|
|
12
|
+
name: 'node-class-description-icon-missing',
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'problem',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Node class description must have an `icon` property defined',
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
missingIcon: 'Node class description is missing required `icon` property',
|
|
20
|
+
addPlaceholder: 'Add icon property with placeholder',
|
|
21
|
+
},
|
|
22
|
+
schema: [],
|
|
23
|
+
hasSuggestions: true,
|
|
24
|
+
},
|
|
25
|
+
defaultOptions: [],
|
|
26
|
+
create(context) {
|
|
27
|
+
if (!isFileType(context.filename, '.node.ts')) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
ClassDeclaration(node) {
|
|
33
|
+
if (!isNodeTypeClass(node)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const descriptionProperty = findClassProperty(node, 'description');
|
|
38
|
+
if (
|
|
39
|
+
!descriptionProperty?.value ||
|
|
40
|
+
descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression
|
|
41
|
+
) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const descriptionValue = descriptionProperty.value;
|
|
46
|
+
const iconProperty = findObjectProperty(descriptionValue, 'icon');
|
|
47
|
+
if (iconProperty) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
context.report({
|
|
52
|
+
node,
|
|
53
|
+
messageId: 'missingIcon',
|
|
54
|
+
suggest: [
|
|
55
|
+
{
|
|
56
|
+
messageId: 'addPlaceholder',
|
|
57
|
+
fix(fixer) {
|
|
58
|
+
const lastProperty =
|
|
59
|
+
descriptionValue.properties[descriptionValue.properties.length - 1];
|
|
60
|
+
if (lastProperty) {
|
|
61
|
+
return fixer.insertTextAfter(lastProperty, ',\n\t\ticon: "file:./icon.svg"');
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
});
|
package/src/utils/ast-utils.ts
CHANGED
|
@@ -197,6 +197,36 @@ export function extractCredentialNameFromArray(
|
|
|
197
197
|
return info ? { name: info.name, node: info.node } : null;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
/** Matches the `this.helpers` MemberExpression (the object part of `this.helpers.foo`). */
|
|
201
|
+
export function isThisHelpersAccess(node: TSESTree.MemberExpression): boolean {
|
|
202
|
+
return (
|
|
203
|
+
node.object?.type === AST_NODE_TYPES.MemberExpression &&
|
|
204
|
+
node.object.object?.type === AST_NODE_TYPES.ThisExpression &&
|
|
205
|
+
node.object.property?.type === AST_NODE_TYPES.Identifier &&
|
|
206
|
+
node.object.property.name === 'helpers'
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Matches a call expression of the form `this.methodName(...)`. */
|
|
211
|
+
export function isThisMethodCall(node: TSESTree.CallExpression, method: string): boolean {
|
|
212
|
+
return (
|
|
213
|
+
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
214
|
+
node.callee.object.type === AST_NODE_TYPES.ThisExpression &&
|
|
215
|
+
node.callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
216
|
+
node.callee.property.name === method
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Matches a call expression of the form `this.helpers.methodName(...)`. */
|
|
221
|
+
export function isThisHelpersMethodCall(node: TSESTree.CallExpression, method: string): boolean {
|
|
222
|
+
return (
|
|
223
|
+
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
224
|
+
node.callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
225
|
+
node.callee.property.name === method &&
|
|
226
|
+
isThisHelpersAccess(node.callee)
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
200
230
|
export function findSimilarStrings(
|
|
201
231
|
target: string,
|
|
202
232
|
candidates: Set<string>,
|