@n8n/eslint-plugin-community-nodes 0.4.0 → 0.6.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 +2 -2
- package/README.md +60 -0
- package/dist/plugin.d.ts +144 -28
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +28 -25
- package/dist/plugin.js.map +1 -1
- package/dist/rules/credential-documentation-url.d.ts +7 -0
- package/dist/rules/credential-documentation-url.d.ts.map +1 -0
- package/dist/rules/credential-documentation-url.js +100 -0
- package/dist/rules/credential-documentation-url.js.map +1 -0
- package/dist/rules/credential-password-field.d.ts +1 -2
- package/dist/rules/credential-password-field.d.ts.map +1 -1
- package/dist/rules/credential-password-field.js +25 -14
- package/dist/rules/credential-password-field.js.map +1 -1
- package/dist/rules/credential-test-required.d.ts +1 -2
- package/dist/rules/credential-test-required.d.ts.map +1 -1
- package/dist/rules/credential-test-required.js +43 -5
- package/dist/rules/credential-test-required.js.map +1 -1
- package/dist/rules/icon-validation.d.ts +1 -2
- package/dist/rules/icon-validation.d.ts.map +1 -1
- package/dist/rules/icon-validation.js +81 -15
- package/dist/rules/icon-validation.js.map +1 -1
- package/dist/rules/index.d.ts +9 -5
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +7 -5
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/no-credential-reuse.d.ts +1 -2
- package/dist/rules/no-credential-reuse.d.ts.map +1 -1
- package/dist/rules/no-credential-reuse.js +33 -4
- package/dist/rules/no-credential-reuse.js.map +1 -1
- package/dist/rules/no-deprecated-workflow-functions.d.ts +1 -2
- package/dist/rules/no-deprecated-workflow-functions.d.ts.map +1 -1
- package/dist/rules/no-deprecated-workflow-functions.js +38 -10
- package/dist/rules/no-deprecated-workflow-functions.js.map +1 -1
- package/dist/rules/no-restricted-globals.d.ts +2 -2
- package/dist/rules/no-restricted-globals.d.ts.map +1 -1
- package/dist/rules/no-restricted-globals.js +5 -3
- package/dist/rules/no-restricted-globals.js.map +1 -1
- package/dist/rules/no-restricted-imports.d.ts +1 -2
- package/dist/rules/no-restricted-imports.d.ts.map +1 -1
- package/dist/rules/no-restricted-imports.js +3 -3
- package/dist/rules/no-restricted-imports.js.map +1 -1
- package/dist/rules/node-usable-as-tool.d.ts +1 -2
- package/dist/rules/node-usable-as-tool.d.ts.map +1 -1
- package/dist/rules/node-usable-as-tool.js +6 -5
- package/dist/rules/node-usable-as-tool.js.map +1 -1
- package/dist/rules/package-name-convention.d.ts +1 -2
- package/dist/rules/package-name-convention.d.ts.map +1 -1
- package/dist/rules/package-name-convention.js +38 -2
- package/dist/rules/package-name-convention.js.map +1 -1
- package/dist/rules/resource-operation-pattern.d.ts +1 -2
- package/dist/rules/resource-operation-pattern.d.ts.map +1 -1
- package/dist/rules/resource-operation-pattern.js +9 -7
- package/dist/rules/resource-operation-pattern.js.map +1 -1
- package/dist/utils/ast-utils.d.ts +2 -1
- package/dist/utils/ast-utils.d.ts.map +1 -1
- package/dist/utils/ast-utils.js +37 -19
- package/dist/utils/ast-utils.js.map +1 -1
- package/dist/utils/file-utils.d.ts +1 -0
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +54 -14
- package/dist/utils/file-utils.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/rule-creator.d.ts +3 -0
- package/dist/utils/rule-creator.d.ts.map +1 -0
- package/dist/utils/rule-creator.js +5 -0
- package/dist/utils/rule-creator.js.map +1 -0
- package/docs/rules/credential-documentation-url.md +94 -0
- package/docs/rules/credential-password-field.md +45 -0
- package/docs/rules/credential-test-required.md +58 -0
- package/docs/rules/icon-validation.md +67 -0
- package/docs/rules/no-credential-reuse.md +82 -0
- package/docs/rules/no-deprecated-workflow-functions.md +61 -0
- package/docs/rules/no-restricted-globals.md +44 -0
- package/docs/rules/no-restricted-imports.md +47 -0
- package/docs/rules/node-usable-as-tool.md +43 -0
- package/docs/rules/package-name-convention.md +52 -0
- package/docs/rules/resource-operation-pattern.md +84 -0
- package/eslint.config.mjs +27 -0
- package/package.json +25 -4
- package/src/plugin.ts +30 -26
- package/src/rules/credential-documentation-url.test.ts +306 -0
- package/src/rules/credential-documentation-url.ts +129 -0
- package/src/rules/credential-password-field.test.ts +1 -0
- package/src/rules/credential-password-field.ts +34 -16
- package/src/rules/credential-test-required.test.ts +84 -57
- package/src/rules/credential-test-required.ts +51 -5
- package/src/rules/icon-validation.test.ts +97 -14
- package/src/rules/icon-validation.ts +95 -14
- package/src/rules/index.ts +8 -5
- package/src/rules/no-credential-reuse.test.ts +306 -58
- package/src/rules/no-credential-reuse.ts +43 -3
- package/src/rules/no-deprecated-workflow-functions.test.ts +70 -0
- package/src/rules/no-deprecated-workflow-functions.ts +44 -10
- package/src/rules/no-restricted-globals.test.ts +1 -0
- package/src/rules/no-restricted-globals.ts +6 -3
- package/src/rules/no-restricted-imports.test.ts +1 -0
- package/src/rules/no-restricted-imports.ts +8 -3
- package/src/rules/node-usable-as-tool.test.ts +1 -0
- package/src/rules/node-usable-as-tool.ts +8 -6
- package/src/rules/package-name-convention.test.ts +82 -5
- package/src/rules/package-name-convention.ts +46 -2
- package/src/rules/resource-operation-pattern.test.ts +1 -0
- package/src/rules/resource-operation-pattern.ts +13 -6
- package/src/utils/ast-utils.ts +47 -19
- package/src/utils/file-utils.ts +69 -14
- package/src/utils/index.ts +1 -0
- package/src/utils/rule-creator.ts +6 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.eslint.json +5 -0
- package/tsconfig.json +1 -2
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
import { createRule } from '../utils/index.js';
|
|
2
5
|
|
|
3
6
|
const DEPRECATED_FUNCTIONS = {
|
|
4
7
|
request: 'httpRequest',
|
|
@@ -21,7 +24,8 @@ function isDeprecatedTypeName(name: string): name is keyof typeof DEPRECATED_TYP
|
|
|
21
24
|
return name in DEPRECATED_TYPES;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
export const NoDeprecatedWorkflowFunctionsRule =
|
|
27
|
+
export const NoDeprecatedWorkflowFunctionsRule = createRule({
|
|
28
|
+
name: 'no-deprecated-workflow-functions',
|
|
25
29
|
meta: {
|
|
26
30
|
type: 'problem',
|
|
27
31
|
docs: {
|
|
@@ -34,8 +38,11 @@ export const NoDeprecatedWorkflowFunctionsRule = ESLintUtils.RuleCreator.without
|
|
|
34
38
|
deprecatedType: "'{{ typeName }}' is deprecated. Use '{{ replacement }}' instead.",
|
|
35
39
|
deprecatedWithoutReplacement:
|
|
36
40
|
"'{{ functionName }}' is deprecated and should be removed or replaced with alternative implementation.",
|
|
41
|
+
suggestReplaceFunction: "Replace '{{ functionName }}' with '{{ replacement }}'",
|
|
42
|
+
suggestReplaceType: "Replace '{{ typeName }}' with '{{ replacement }}'",
|
|
37
43
|
},
|
|
38
44
|
schema: [],
|
|
45
|
+
hasSuggestions: true,
|
|
39
46
|
},
|
|
40
47
|
defaultOptions: [],
|
|
41
48
|
create(context) {
|
|
@@ -45,7 +52,10 @@ export const NoDeprecatedWorkflowFunctionsRule = ESLintUtils.RuleCreator.without
|
|
|
45
52
|
ImportDeclaration(node) {
|
|
46
53
|
if (node.source.value === 'n8n-workflow') {
|
|
47
54
|
node.specifiers.forEach((specifier) => {
|
|
48
|
-
if (
|
|
55
|
+
if (
|
|
56
|
+
specifier.type === AST_NODE_TYPES.ImportSpecifier &&
|
|
57
|
+
specifier.imported.type === AST_NODE_TYPES.Identifier
|
|
58
|
+
) {
|
|
49
59
|
n8nWorkflowTypes.add(specifier.local.name);
|
|
50
60
|
}
|
|
51
61
|
});
|
|
@@ -53,7 +63,10 @@ export const NoDeprecatedWorkflowFunctionsRule = ESLintUtils.RuleCreator.without
|
|
|
53
63
|
},
|
|
54
64
|
|
|
55
65
|
MemberExpression(node) {
|
|
56
|
-
if (
|
|
66
|
+
if (
|
|
67
|
+
node.property.type === AST_NODE_TYPES.Identifier &&
|
|
68
|
+
isDeprecatedFunctionName(node.property.name)
|
|
69
|
+
) {
|
|
57
70
|
if (!isThisHelpersAccess(node)) {
|
|
58
71
|
return;
|
|
59
72
|
}
|
|
@@ -74,6 +87,13 @@ export const NoDeprecatedWorkflowFunctionsRule = ESLintUtils.RuleCreator.without
|
|
|
74
87
|
replacement,
|
|
75
88
|
message: getDeprecationMessage(functionName),
|
|
76
89
|
},
|
|
90
|
+
suggest: [
|
|
91
|
+
{
|
|
92
|
+
messageId: 'suggestReplaceFunction',
|
|
93
|
+
data: { functionName, replacement },
|
|
94
|
+
fix: (fixer) => fixer.replaceText(node.property, replacement),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
77
97
|
});
|
|
78
98
|
} else {
|
|
79
99
|
context.report({
|
|
@@ -89,7 +109,7 @@ export const NoDeprecatedWorkflowFunctionsRule = ESLintUtils.RuleCreator.without
|
|
|
89
109
|
|
|
90
110
|
TSTypeReference(node) {
|
|
91
111
|
if (
|
|
92
|
-
node.typeName.type ===
|
|
112
|
+
node.typeName.type === AST_NODE_TYPES.Identifier &&
|
|
93
113
|
isDeprecatedTypeName(node.typeName.name) &&
|
|
94
114
|
n8nWorkflowTypes.has(node.typeName.name)
|
|
95
115
|
) {
|
|
@@ -103,6 +123,13 @@ export const NoDeprecatedWorkflowFunctionsRule = ESLintUtils.RuleCreator.without
|
|
|
103
123
|
typeName,
|
|
104
124
|
replacement,
|
|
105
125
|
},
|
|
126
|
+
suggest: [
|
|
127
|
+
{
|
|
128
|
+
messageId: 'suggestReplaceType',
|
|
129
|
+
data: { typeName, replacement },
|
|
130
|
+
fix: (fixer) => fixer.replaceText(node.typeName, replacement),
|
|
131
|
+
},
|
|
132
|
+
],
|
|
106
133
|
});
|
|
107
134
|
}
|
|
108
135
|
},
|
|
@@ -111,9 +138,9 @@ export const NoDeprecatedWorkflowFunctionsRule = ESLintUtils.RuleCreator.without
|
|
|
111
138
|
// Check if this import is from n8n-workflow by looking at the parent ImportDeclaration
|
|
112
139
|
const importDeclaration = node.parent;
|
|
113
140
|
if (
|
|
114
|
-
importDeclaration?.type ===
|
|
141
|
+
importDeclaration?.type === AST_NODE_TYPES.ImportDeclaration &&
|
|
115
142
|
importDeclaration.source.value === 'n8n-workflow' &&
|
|
116
|
-
node.imported.type ===
|
|
143
|
+
node.imported.type === AST_NODE_TYPES.Identifier &&
|
|
117
144
|
isDeprecatedTypeName(node.imported.name)
|
|
118
145
|
) {
|
|
119
146
|
const typeName = node.imported.name;
|
|
@@ -126,6 +153,13 @@ export const NoDeprecatedWorkflowFunctionsRule = ESLintUtils.RuleCreator.without
|
|
|
126
153
|
typeName,
|
|
127
154
|
replacement,
|
|
128
155
|
},
|
|
156
|
+
suggest: [
|
|
157
|
+
{
|
|
158
|
+
messageId: 'suggestReplaceType',
|
|
159
|
+
data: { typeName, replacement },
|
|
160
|
+
fix: (fixer) => fixer.replaceText(node.imported, replacement),
|
|
161
|
+
},
|
|
162
|
+
],
|
|
129
163
|
});
|
|
130
164
|
}
|
|
131
165
|
},
|
|
@@ -137,11 +171,11 @@ export const NoDeprecatedWorkflowFunctionsRule = ESLintUtils.RuleCreator.without
|
|
|
137
171
|
* Check if the MemberExpression follows the this.helpers.* pattern
|
|
138
172
|
*/
|
|
139
173
|
function isThisHelpersAccess(node: TSESTree.MemberExpression): boolean {
|
|
140
|
-
if (node.object?.type ===
|
|
174
|
+
if (node.object?.type === AST_NODE_TYPES.MemberExpression) {
|
|
141
175
|
const outerObject = node.object;
|
|
142
176
|
return (
|
|
143
|
-
outerObject.object?.type ===
|
|
144
|
-
outerObject.property?.type ===
|
|
177
|
+
outerObject.object?.type === AST_NODE_TYPES.ThisExpression &&
|
|
178
|
+
outerObject.property?.type === AST_NODE_TYPES.Identifier &&
|
|
145
179
|
outerObject.property.name === 'helpers'
|
|
146
180
|
);
|
|
147
181
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/types';
|
|
2
2
|
import type { TSESLint } from '@typescript-eslint/utils';
|
|
3
3
|
|
|
4
|
+
import { createRule } from '../utils/index.js';
|
|
5
|
+
|
|
4
6
|
const restrictedGlobals = [
|
|
5
7
|
'clearInterval',
|
|
6
8
|
'clearTimeout',
|
|
@@ -15,7 +17,8 @@ const restrictedGlobals = [
|
|
|
15
17
|
'__filename',
|
|
16
18
|
];
|
|
17
19
|
|
|
18
|
-
export const NoRestrictedGlobalsRule =
|
|
20
|
+
export const NoRestrictedGlobalsRule = createRule({
|
|
21
|
+
name: 'no-restricted-globals',
|
|
19
22
|
meta: {
|
|
20
23
|
type: 'problem',
|
|
21
24
|
docs: {
|
|
@@ -33,7 +36,7 @@ export const NoRestrictedGlobalsRule = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
33
36
|
|
|
34
37
|
// Skip property access (like console.process - we want process.exit but not obj.process)
|
|
35
38
|
if (
|
|
36
|
-
parent?.type ===
|
|
39
|
+
parent?.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
|
|
37
40
|
parent.property === ref.identifier &&
|
|
38
41
|
!parent.computed
|
|
39
42
|
) {
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
getModulePath,
|
|
3
|
+
isDirectRequireCall,
|
|
4
|
+
isRequireMemberCall,
|
|
5
|
+
createRule,
|
|
6
|
+
} from '../utils/index.js';
|
|
3
7
|
|
|
4
8
|
const allowedModules = [
|
|
5
9
|
'n8n-workflow',
|
|
@@ -22,7 +26,8 @@ const isModuleAllowed = (modulePath: string): boolean => {
|
|
|
22
26
|
return allowedModules.includes(moduleName);
|
|
23
27
|
};
|
|
24
28
|
|
|
25
|
-
export const NoRestrictedImportsRule =
|
|
29
|
+
export const NoRestrictedImportsRule = createRule({
|
|
30
|
+
name: 'no-restricted-imports',
|
|
26
31
|
meta: {
|
|
27
32
|
type: 'problem',
|
|
28
33
|
docs: {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/types';
|
|
2
|
+
|
|
2
3
|
import {
|
|
3
4
|
isNodeTypeClass,
|
|
4
5
|
findClassProperty,
|
|
5
6
|
findObjectProperty,
|
|
6
|
-
|
|
7
|
+
createRule,
|
|
7
8
|
} from '../utils/index.js';
|
|
8
9
|
|
|
9
|
-
export const NodeUsableAsToolRule =
|
|
10
|
+
export const NodeUsableAsToolRule = createRule({
|
|
11
|
+
name: 'node-usable-as-tool',
|
|
10
12
|
meta: {
|
|
11
13
|
type: 'problem',
|
|
12
14
|
docs: {
|
|
@@ -33,7 +35,7 @@ export const NodeUsableAsToolRule = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const descriptionValue = descriptionProperty.value;
|
|
36
|
-
if (descriptionValue?.type !==
|
|
38
|
+
if (descriptionValue?.type !== TSESTree.AST_NODE_TYPES.ObjectExpression) {
|
|
37
39
|
return;
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -44,10 +46,10 @@ export const NodeUsableAsToolRule = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
44
46
|
node,
|
|
45
47
|
messageId: 'missingUsableAsTool',
|
|
46
48
|
fix(fixer) {
|
|
47
|
-
if (descriptionValue?.type ===
|
|
49
|
+
if (descriptionValue?.type === TSESTree.AST_NODE_TYPES.ObjectExpression) {
|
|
48
50
|
const properties = descriptionValue.properties;
|
|
49
51
|
if (properties.length === 0) {
|
|
50
|
-
const openBrace = descriptionValue.range
|
|
52
|
+
const openBrace = descriptionValue.range[0] + 1;
|
|
51
53
|
return fixer.insertTextAfterRange(
|
|
52
54
|
[openBrace, openBrace],
|
|
53
55
|
'\n\t\tusableAsTool: true,',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
2
3
|
import { PackageNameConventionRule } from './package-name-convention.js';
|
|
3
4
|
|
|
4
5
|
const ruleTester = new RuleTester();
|
|
@@ -80,33 +81,109 @@ ruleTester.run('package-name-convention', PackageNameConventionRule, {
|
|
|
80
81
|
name: 'invalid package name - generic',
|
|
81
82
|
filename: 'package.json',
|
|
82
83
|
code: '{ "name": "my-package", "version": "1.0.0" }',
|
|
83
|
-
errors: [
|
|
84
|
+
errors: [
|
|
85
|
+
{
|
|
86
|
+
messageId: 'invalidPackageName',
|
|
87
|
+
data: { packageName: 'my-package' },
|
|
88
|
+
suggestions: [
|
|
89
|
+
{
|
|
90
|
+
messageId: 'renameTo',
|
|
91
|
+
data: { suggestedName: 'n8n-nodes-my-package' },
|
|
92
|
+
output: '{ "name": "n8n-nodes-my-package", "version": "1.0.0" }',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
84
97
|
},
|
|
85
98
|
{
|
|
86
99
|
name: 'invalid package name - missing nodes',
|
|
87
100
|
filename: 'package.json',
|
|
88
101
|
code: '{ "name": "n8n-example", "version": "1.0.0" }',
|
|
89
|
-
errors: [
|
|
102
|
+
errors: [
|
|
103
|
+
{
|
|
104
|
+
messageId: 'invalidPackageName',
|
|
105
|
+
data: { packageName: 'n8n-example' },
|
|
106
|
+
suggestions: [
|
|
107
|
+
{
|
|
108
|
+
messageId: 'renameTo',
|
|
109
|
+
data: { suggestedName: 'n8n-nodes-example' },
|
|
110
|
+
output: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
90
115
|
},
|
|
91
116
|
{
|
|
92
117
|
name: 'invalid scoped package name',
|
|
93
118
|
filename: 'package.json',
|
|
94
119
|
code: '{ "name": "@company/example-nodes", "version": "1.0.0" }',
|
|
95
120
|
errors: [
|
|
96
|
-
{
|
|
121
|
+
{
|
|
122
|
+
messageId: 'invalidPackageName',
|
|
123
|
+
data: { packageName: '@company/example-nodes' },
|
|
124
|
+
suggestions: [
|
|
125
|
+
{
|
|
126
|
+
messageId: 'renameTo',
|
|
127
|
+
data: { suggestedName: '@company/n8n-nodes-example' },
|
|
128
|
+
output: '{ "name": "@company/n8n-nodes-example", "version": "1.0.0" }',
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
97
132
|
],
|
|
98
133
|
},
|
|
99
134
|
{
|
|
100
135
|
name: 'invalid package name - wrong order',
|
|
101
136
|
filename: 'package.json',
|
|
102
137
|
code: '{ "name": "nodes-n8n-example", "version": "1.0.0" }',
|
|
103
|
-
errors: [
|
|
138
|
+
errors: [
|
|
139
|
+
{
|
|
140
|
+
messageId: 'invalidPackageName',
|
|
141
|
+
data: { packageName: 'nodes-n8n-example' },
|
|
142
|
+
suggestions: [
|
|
143
|
+
{
|
|
144
|
+
messageId: 'renameTo',
|
|
145
|
+
data: { suggestedName: 'n8n-nodes-example' },
|
|
146
|
+
output: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
],
|
|
104
151
|
},
|
|
105
152
|
{
|
|
106
153
|
name: 'empty package name',
|
|
107
154
|
filename: 'package.json',
|
|
108
155
|
code: '{ "name": "", "version": "1.0.0" }',
|
|
109
|
-
errors: [
|
|
156
|
+
errors: [
|
|
157
|
+
{
|
|
158
|
+
messageId: 'invalidPackageName',
|
|
159
|
+
data: { packageName: '' },
|
|
160
|
+
suggestions: [],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'incomplete package name with missing suffix',
|
|
166
|
+
filename: 'package.json',
|
|
167
|
+
code: '{ "name": "n8n-nodes-", "version": "1.0.0" }',
|
|
168
|
+
errors: [
|
|
169
|
+
{
|
|
170
|
+
messageId: 'invalidPackageName',
|
|
171
|
+
data: { packageName: 'n8n-nodes-' },
|
|
172
|
+
suggestions: [],
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'incomplete scoped package name with missing suffix',
|
|
178
|
+
filename: 'package.json',
|
|
179
|
+
code: '{ "name": "@company/n8n-nodes-", "version": "1.0.0" }',
|
|
180
|
+
errors: [
|
|
181
|
+
{
|
|
182
|
+
messageId: 'invalidPackageName',
|
|
183
|
+
data: { packageName: '@company/n8n-nodes-' },
|
|
184
|
+
suggestions: [],
|
|
185
|
+
},
|
|
186
|
+
],
|
|
110
187
|
},
|
|
111
188
|
],
|
|
112
189
|
});
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
+
import type { ReportSuggestionArray } from '@typescript-eslint/utils/ts-eslint';
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
import { createRule } from '../utils/index.js';
|
|
6
|
+
|
|
7
|
+
export const PackageNameConventionRule = createRule({
|
|
8
|
+
name: 'package-name-convention',
|
|
4
9
|
meta: {
|
|
5
10
|
type: 'problem',
|
|
6
11
|
docs: {
|
|
7
12
|
description: 'Enforce correct package naming convention for n8n community nodes',
|
|
8
13
|
},
|
|
9
14
|
messages: {
|
|
15
|
+
renameTo: "Rename to '{{suggestedName}}'",
|
|
10
16
|
invalidPackageName:
|
|
11
17
|
'Package name "{{ packageName }}" must follow the convention "n8n-nodes-[PACKAGE-NAME]" or "@[AUTHOR]/n8n-nodes-[PACKAGE-NAME]"',
|
|
12
18
|
},
|
|
13
19
|
schema: [],
|
|
20
|
+
hasSuggestions: true,
|
|
14
21
|
},
|
|
15
22
|
defaultOptions: [],
|
|
16
23
|
create(context) {
|
|
@@ -43,12 +50,29 @@ export const PackageNameConventionRule = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
43
50
|
const packageNameStr = typeof packageName === 'string' ? packageName : null;
|
|
44
51
|
|
|
45
52
|
if (!packageNameStr || !isValidPackageName(packageNameStr)) {
|
|
53
|
+
const suggestions: ReportSuggestionArray<'invalidPackageName' | 'renameTo'> = [];
|
|
54
|
+
|
|
55
|
+
// Generate package name suggestions if we have a valid string
|
|
56
|
+
if (packageNameStr) {
|
|
57
|
+
const suggestedNames = generatePackageNameSuggestions(packageNameStr);
|
|
58
|
+
for (const suggestedName of suggestedNames) {
|
|
59
|
+
suggestions.push({
|
|
60
|
+
messageId: 'renameTo',
|
|
61
|
+
data: { suggestedName },
|
|
62
|
+
fix(fixer) {
|
|
63
|
+
return fixer.replaceText(nameProperty.value, `"${suggestedName}"`);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
context.report({
|
|
47
70
|
node: nameProperty,
|
|
48
71
|
messageId: 'invalidPackageName',
|
|
49
72
|
data: {
|
|
50
73
|
packageName: packageNameStr ?? 'undefined',
|
|
51
74
|
},
|
|
75
|
+
suggest: suggestions,
|
|
52
76
|
});
|
|
53
77
|
}
|
|
54
78
|
},
|
|
@@ -61,3 +85,23 @@ function isValidPackageName(name: string): boolean {
|
|
|
61
85
|
const scoped = /^@.+\/n8n-nodes-.+$/;
|
|
62
86
|
return unscoped.test(name) || scoped.test(name);
|
|
63
87
|
}
|
|
88
|
+
|
|
89
|
+
function generatePackageNameSuggestions(invalidName: string): string[] {
|
|
90
|
+
const cleanName = (name: string) => {
|
|
91
|
+
return name
|
|
92
|
+
.replace(/^nodes?-?n8n-?/, '')
|
|
93
|
+
.replace(/^n8n-/, '')
|
|
94
|
+
.replace(/^nodes?-?/, '')
|
|
95
|
+
.replace(/^node-/, '')
|
|
96
|
+
.replace(/-nodes$/, '');
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (invalidName.startsWith('@')) {
|
|
100
|
+
const [scope, packagePart] = invalidName.split('/');
|
|
101
|
+
const clean = cleanName(packagePart ?? '');
|
|
102
|
+
return clean ? [`${scope}/n8n-nodes-${clean}`] : [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const clean = cleanName(invalidName);
|
|
106
|
+
return clean ? [`n8n-nodes-${clean}`] : [];
|
|
107
|
+
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
+
|
|
2
4
|
import {
|
|
3
5
|
isNodeTypeClass,
|
|
4
6
|
findClassProperty,
|
|
5
7
|
findObjectProperty,
|
|
6
8
|
getStringLiteralValue,
|
|
7
9
|
isFileType,
|
|
10
|
+
createRule,
|
|
8
11
|
} from '../utils/index.js';
|
|
9
12
|
|
|
10
|
-
export const ResourceOperationPatternRule =
|
|
13
|
+
export const ResourceOperationPatternRule = createRule({
|
|
14
|
+
name: 'resource-operation-pattern',
|
|
11
15
|
meta: {
|
|
12
16
|
type: 'problem',
|
|
13
17
|
docs: {
|
|
@@ -26,12 +30,15 @@ export const ResourceOperationPatternRule = ESLintUtils.RuleCreator.withoutDocs(
|
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
const analyzeNodeDescription = (descriptionValue: TSESTree.Expression | null): void => {
|
|
29
|
-
if (!descriptionValue || descriptionValue.type !==
|
|
33
|
+
if (!descriptionValue || descriptionValue.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
30
34
|
return;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
const propertiesProperty = findObjectProperty(descriptionValue, 'properties');
|
|
34
|
-
if (
|
|
38
|
+
if (
|
|
39
|
+
!propertiesProperty?.value ||
|
|
40
|
+
propertiesProperty.value.type !== AST_NODE_TYPES.ArrayExpression
|
|
41
|
+
) {
|
|
35
42
|
return;
|
|
36
43
|
}
|
|
37
44
|
|
|
@@ -41,7 +48,7 @@ export const ResourceOperationPatternRule = ESLintUtils.RuleCreator.withoutDocs(
|
|
|
41
48
|
let operationNode: TSESTree.Node | null = null;
|
|
42
49
|
|
|
43
50
|
for (const property of propertiesArray.elements) {
|
|
44
|
-
if (!property || property.type !==
|
|
51
|
+
if (!property || property.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
45
52
|
continue;
|
|
46
53
|
}
|
|
47
54
|
|
|
@@ -62,7 +69,7 @@ export const ResourceOperationPatternRule = ESLintUtils.RuleCreator.withoutDocs(
|
|
|
62
69
|
if (name === 'operation' && type === 'options') {
|
|
63
70
|
operationNode = property;
|
|
64
71
|
const optionsProperty = findObjectProperty(property, 'options');
|
|
65
|
-
if (optionsProperty?.value?.type ===
|
|
72
|
+
if (optionsProperty?.value?.type === AST_NODE_TYPES.ArrayExpression) {
|
|
66
73
|
operationCount = optionsProperty.value.elements.length;
|
|
67
74
|
}
|
|
68
75
|
}
|
package/src/utils/ast-utils.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { TSESTree
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
+
import { distance } from 'fastest-levenshtein';
|
|
2
4
|
|
|
3
5
|
function implementsInterface(node: TSESTree.ClassDeclaration, interfaceName: string): boolean {
|
|
4
6
|
return (
|
|
5
7
|
node.implements?.some(
|
|
6
8
|
(impl) =>
|
|
7
|
-
impl.type ===
|
|
8
|
-
impl.expression.type ===
|
|
9
|
+
impl.type === AST_NODE_TYPES.TSClassImplements &&
|
|
10
|
+
impl.expression.type === AST_NODE_TYPES.Identifier &&
|
|
9
11
|
impl.expression.name === interfaceName,
|
|
10
12
|
) ?? false
|
|
11
13
|
);
|
|
@@ -16,7 +18,7 @@ export function isNodeTypeClass(node: TSESTree.ClassDeclaration): boolean {
|
|
|
16
18
|
return true;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
if (node.superClass?.type ===
|
|
21
|
+
if (node.superClass?.type === AST_NODE_TYPES.Identifier && node.superClass.name === 'Node') {
|
|
20
22
|
return true;
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -33,11 +35,11 @@ export function findClassProperty(
|
|
|
33
35
|
): TSESTree.PropertyDefinition | null {
|
|
34
36
|
const property = node.body.body.find(
|
|
35
37
|
(member) =>
|
|
36
|
-
member.type ===
|
|
37
|
-
member.key?.type ===
|
|
38
|
+
member.type === AST_NODE_TYPES.PropertyDefinition &&
|
|
39
|
+
member.key?.type === AST_NODE_TYPES.Identifier &&
|
|
38
40
|
member.key.name === propertyName,
|
|
39
41
|
);
|
|
40
|
-
return property?.type ===
|
|
42
|
+
return property?.type === AST_NODE_TYPES.PropertyDefinition ? property : null;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export function findObjectProperty(
|
|
@@ -46,13 +48,15 @@ export function findObjectProperty(
|
|
|
46
48
|
): TSESTree.Property | null {
|
|
47
49
|
const property = obj.properties.find(
|
|
48
50
|
(prop) =>
|
|
49
|
-
prop.type ===
|
|
51
|
+
prop.type === AST_NODE_TYPES.Property &&
|
|
52
|
+
prop.key.type === AST_NODE_TYPES.Identifier &&
|
|
53
|
+
prop.key.name === propertyName,
|
|
50
54
|
);
|
|
51
|
-
return property?.type ===
|
|
55
|
+
return property?.type === AST_NODE_TYPES.Property ? property : null;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
export function getLiteralValue(node: TSESTree.Node | null): string | boolean | number | null {
|
|
55
|
-
if (node?.type ===
|
|
59
|
+
if (node?.type === AST_NODE_TYPES.Literal) {
|
|
56
60
|
return node.value as string | boolean | number | null;
|
|
57
61
|
}
|
|
58
62
|
return null;
|
|
@@ -70,7 +74,7 @@ export function getModulePath(node: TSESTree.Node | null): string | null {
|
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
if (
|
|
73
|
-
node?.type ===
|
|
77
|
+
node?.type === AST_NODE_TYPES.TemplateLiteral &&
|
|
74
78
|
node.expressions.length === 0 &&
|
|
75
79
|
node.quasis.length === 1
|
|
76
80
|
) {
|
|
@@ -90,7 +94,7 @@ export function findArrayLiteralProperty(
|
|
|
90
94
|
propertyName: string,
|
|
91
95
|
): TSESTree.ArrayExpression | null {
|
|
92
96
|
const property = findObjectProperty(obj, propertyName);
|
|
93
|
-
if (property?.value.type ===
|
|
97
|
+
if (property?.value.type === AST_NODE_TYPES.ArrayExpression) {
|
|
94
98
|
return property.value;
|
|
95
99
|
}
|
|
96
100
|
return null;
|
|
@@ -100,11 +104,11 @@ export function hasArrayLiteralValue(
|
|
|
100
104
|
node: TSESTree.PropertyDefinition,
|
|
101
105
|
searchValue: string,
|
|
102
106
|
): boolean {
|
|
103
|
-
if (node.value?.type !==
|
|
107
|
+
if (node.value?.type !== AST_NODE_TYPES.ArrayExpression) return false;
|
|
104
108
|
|
|
105
109
|
return node.value.elements.some(
|
|
106
110
|
(element) =>
|
|
107
|
-
element?.type ===
|
|
111
|
+
element?.type === AST_NODE_TYPES.Literal &&
|
|
108
112
|
typeof element.value === 'string' &&
|
|
109
113
|
element.value === searchValue,
|
|
110
114
|
);
|
|
@@ -125,14 +129,16 @@ export function isFileType(filename: string, extension: string): boolean {
|
|
|
125
129
|
|
|
126
130
|
export function isDirectRequireCall(node: TSESTree.CallExpression): boolean {
|
|
127
131
|
return (
|
|
128
|
-
node.callee.type ===
|
|
132
|
+
node.callee.type === AST_NODE_TYPES.Identifier &&
|
|
133
|
+
node.callee.name === 'require' &&
|
|
134
|
+
node.arguments.length > 0
|
|
129
135
|
);
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
export function isRequireMemberCall(node: TSESTree.CallExpression): boolean {
|
|
133
139
|
return (
|
|
134
|
-
node.callee.type ===
|
|
135
|
-
node.callee.object.type ===
|
|
140
|
+
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
141
|
+
node.callee.object.type === AST_NODE_TYPES.Identifier &&
|
|
136
142
|
node.callee.object.name === 'require' &&
|
|
137
143
|
node.arguments.length > 0
|
|
138
144
|
);
|
|
@@ -148,7 +154,7 @@ export function extractCredentialInfoFromArray(
|
|
|
148
154
|
return { name: stringValue, node: element };
|
|
149
155
|
}
|
|
150
156
|
|
|
151
|
-
if (element.type ===
|
|
157
|
+
if (element.type === AST_NODE_TYPES.ObjectExpression) {
|
|
152
158
|
const nameProperty = findObjectProperty(element, 'name');
|
|
153
159
|
const testedByProperty = findObjectProperty(element, 'testedBy');
|
|
154
160
|
|
|
@@ -161,7 +167,7 @@ export function extractCredentialInfoFromArray(
|
|
|
161
167
|
if (nameValue) {
|
|
162
168
|
return {
|
|
163
169
|
name: nameValue,
|
|
164
|
-
testedBy: testedByValue
|
|
170
|
+
testedBy: testedByValue ?? undefined,
|
|
165
171
|
node: nameProperty.value,
|
|
166
172
|
};
|
|
167
173
|
}
|
|
@@ -177,3 +183,25 @@ export function extractCredentialNameFromArray(
|
|
|
177
183
|
const info = extractCredentialInfoFromArray(element);
|
|
178
184
|
return info ? { name: info.name, node: info.node } : null;
|
|
179
185
|
}
|
|
186
|
+
|
|
187
|
+
export function findSimilarStrings(
|
|
188
|
+
target: string,
|
|
189
|
+
candidates: Set<string>,
|
|
190
|
+
maxDistance: number = 3,
|
|
191
|
+
maxResults: number = 3,
|
|
192
|
+
): string[] {
|
|
193
|
+
const matches: Array<{ name: string; distance: number }> = [];
|
|
194
|
+
|
|
195
|
+
for (const candidate of candidates) {
|
|
196
|
+
const levenshteinDistance = distance(target.toLowerCase(), candidate.toLowerCase());
|
|
197
|
+
|
|
198
|
+
if (levenshteinDistance <= maxDistance) {
|
|
199
|
+
matches.push({ name: candidate, distance: levenshteinDistance });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return matches
|
|
204
|
+
.sort((a, b) => a.distance - b.distance)
|
|
205
|
+
.slice(0, maxResults)
|
|
206
|
+
.map((match) => match.name);
|
|
207
|
+
}
|