@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.
Files changed (116) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/README.md +60 -0
  3. package/dist/plugin.d.ts +144 -28
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +28 -25
  6. package/dist/plugin.js.map +1 -1
  7. package/dist/rules/credential-documentation-url.d.ts +7 -0
  8. package/dist/rules/credential-documentation-url.d.ts.map +1 -0
  9. package/dist/rules/credential-documentation-url.js +100 -0
  10. package/dist/rules/credential-documentation-url.js.map +1 -0
  11. package/dist/rules/credential-password-field.d.ts +1 -2
  12. package/dist/rules/credential-password-field.d.ts.map +1 -1
  13. package/dist/rules/credential-password-field.js +25 -14
  14. package/dist/rules/credential-password-field.js.map +1 -1
  15. package/dist/rules/credential-test-required.d.ts +1 -2
  16. package/dist/rules/credential-test-required.d.ts.map +1 -1
  17. package/dist/rules/credential-test-required.js +43 -5
  18. package/dist/rules/credential-test-required.js.map +1 -1
  19. package/dist/rules/icon-validation.d.ts +1 -2
  20. package/dist/rules/icon-validation.d.ts.map +1 -1
  21. package/dist/rules/icon-validation.js +81 -15
  22. package/dist/rules/icon-validation.js.map +1 -1
  23. package/dist/rules/index.d.ts +9 -5
  24. package/dist/rules/index.d.ts.map +1 -1
  25. package/dist/rules/index.js +7 -5
  26. package/dist/rules/index.js.map +1 -1
  27. package/dist/rules/no-credential-reuse.d.ts +1 -2
  28. package/dist/rules/no-credential-reuse.d.ts.map +1 -1
  29. package/dist/rules/no-credential-reuse.js +33 -4
  30. package/dist/rules/no-credential-reuse.js.map +1 -1
  31. package/dist/rules/no-deprecated-workflow-functions.d.ts +1 -2
  32. package/dist/rules/no-deprecated-workflow-functions.d.ts.map +1 -1
  33. package/dist/rules/no-deprecated-workflow-functions.js +38 -10
  34. package/dist/rules/no-deprecated-workflow-functions.js.map +1 -1
  35. package/dist/rules/no-restricted-globals.d.ts +2 -2
  36. package/dist/rules/no-restricted-globals.d.ts.map +1 -1
  37. package/dist/rules/no-restricted-globals.js +5 -3
  38. package/dist/rules/no-restricted-globals.js.map +1 -1
  39. package/dist/rules/no-restricted-imports.d.ts +1 -2
  40. package/dist/rules/no-restricted-imports.d.ts.map +1 -1
  41. package/dist/rules/no-restricted-imports.js +3 -3
  42. package/dist/rules/no-restricted-imports.js.map +1 -1
  43. package/dist/rules/node-usable-as-tool.d.ts +1 -2
  44. package/dist/rules/node-usable-as-tool.d.ts.map +1 -1
  45. package/dist/rules/node-usable-as-tool.js +6 -5
  46. package/dist/rules/node-usable-as-tool.js.map +1 -1
  47. package/dist/rules/package-name-convention.d.ts +1 -2
  48. package/dist/rules/package-name-convention.d.ts.map +1 -1
  49. package/dist/rules/package-name-convention.js +38 -2
  50. package/dist/rules/package-name-convention.js.map +1 -1
  51. package/dist/rules/resource-operation-pattern.d.ts +1 -2
  52. package/dist/rules/resource-operation-pattern.d.ts.map +1 -1
  53. package/dist/rules/resource-operation-pattern.js +9 -7
  54. package/dist/rules/resource-operation-pattern.js.map +1 -1
  55. package/dist/utils/ast-utils.d.ts +2 -1
  56. package/dist/utils/ast-utils.d.ts.map +1 -1
  57. package/dist/utils/ast-utils.js +37 -19
  58. package/dist/utils/ast-utils.js.map +1 -1
  59. package/dist/utils/file-utils.d.ts +1 -0
  60. package/dist/utils/file-utils.d.ts.map +1 -1
  61. package/dist/utils/file-utils.js +54 -14
  62. package/dist/utils/file-utils.js.map +1 -1
  63. package/dist/utils/index.d.ts +1 -0
  64. package/dist/utils/index.d.ts.map +1 -1
  65. package/dist/utils/index.js +1 -0
  66. package/dist/utils/index.js.map +1 -1
  67. package/dist/utils/rule-creator.d.ts +3 -0
  68. package/dist/utils/rule-creator.d.ts.map +1 -0
  69. package/dist/utils/rule-creator.js +5 -0
  70. package/dist/utils/rule-creator.js.map +1 -0
  71. package/docs/rules/credential-documentation-url.md +94 -0
  72. package/docs/rules/credential-password-field.md +45 -0
  73. package/docs/rules/credential-test-required.md +58 -0
  74. package/docs/rules/icon-validation.md +67 -0
  75. package/docs/rules/no-credential-reuse.md +82 -0
  76. package/docs/rules/no-deprecated-workflow-functions.md +61 -0
  77. package/docs/rules/no-restricted-globals.md +44 -0
  78. package/docs/rules/no-restricted-imports.md +47 -0
  79. package/docs/rules/node-usable-as-tool.md +43 -0
  80. package/docs/rules/package-name-convention.md +52 -0
  81. package/docs/rules/resource-operation-pattern.md +84 -0
  82. package/eslint.config.mjs +27 -0
  83. package/package.json +25 -4
  84. package/src/plugin.ts +30 -26
  85. package/src/rules/credential-documentation-url.test.ts +306 -0
  86. package/src/rules/credential-documentation-url.ts +129 -0
  87. package/src/rules/credential-password-field.test.ts +1 -0
  88. package/src/rules/credential-password-field.ts +34 -16
  89. package/src/rules/credential-test-required.test.ts +84 -57
  90. package/src/rules/credential-test-required.ts +51 -5
  91. package/src/rules/icon-validation.test.ts +97 -14
  92. package/src/rules/icon-validation.ts +95 -14
  93. package/src/rules/index.ts +8 -5
  94. package/src/rules/no-credential-reuse.test.ts +306 -58
  95. package/src/rules/no-credential-reuse.ts +43 -3
  96. package/src/rules/no-deprecated-workflow-functions.test.ts +70 -0
  97. package/src/rules/no-deprecated-workflow-functions.ts +44 -10
  98. package/src/rules/no-restricted-globals.test.ts +1 -0
  99. package/src/rules/no-restricted-globals.ts +6 -3
  100. package/src/rules/no-restricted-imports.test.ts +1 -0
  101. package/src/rules/no-restricted-imports.ts +8 -3
  102. package/src/rules/node-usable-as-tool.test.ts +1 -0
  103. package/src/rules/node-usable-as-tool.ts +8 -6
  104. package/src/rules/package-name-convention.test.ts +82 -5
  105. package/src/rules/package-name-convention.ts +46 -2
  106. package/src/rules/resource-operation-pattern.test.ts +1 -0
  107. package/src/rules/resource-operation-pattern.ts +13 -6
  108. package/src/utils/ast-utils.ts +47 -19
  109. package/src/utils/file-utils.ts +69 -14
  110. package/src/utils/index.ts +1 -0
  111. package/src/utils/rule-creator.ts +6 -0
  112. package/tsconfig.build.json +4 -0
  113. package/tsconfig.build.tsbuildinfo +1 -0
  114. package/tsconfig.eslint.json +5 -0
  115. package/tsconfig.json +1 -2
  116. package/tsconfig.tsbuildinfo +0 -1
@@ -1,4 +1,7 @@
1
- import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
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 = ESLintUtils.RuleCreator.withoutDocs({
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 (specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier') {
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 (node.property.type === 'Identifier' && isDeprecatedFunctionName(node.property.name)) {
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 === 'Identifier' &&
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 === 'ImportDeclaration' &&
141
+ importDeclaration?.type === AST_NODE_TYPES.ImportDeclaration &&
115
142
  importDeclaration.source.value === 'n8n-workflow' &&
116
- node.imported.type === 'Identifier' &&
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 === 'MemberExpression') {
174
+ if (node.object?.type === AST_NODE_TYPES.MemberExpression) {
141
175
  const outerObject = node.object;
142
176
  return (
143
- outerObject.object?.type === 'ThisExpression' &&
144
- outerObject.property?.type === 'Identifier' &&
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,4 +1,5 @@
1
1
  import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
2
3
  import { NoRestrictedGlobalsRule } from './no-restricted-globals.js';
3
4
 
4
5
  const ruleTester = new RuleTester();
@@ -1,6 +1,8 @@
1
- import { ESLintUtils } from '@typescript-eslint/utils';
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 = ESLintUtils.RuleCreator.withoutDocs({
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 === 'MemberExpression' &&
39
+ parent?.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
37
40
  parent.property === ref.identifier &&
38
41
  !parent.computed
39
42
  ) {
@@ -1,4 +1,5 @@
1
1
  import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
2
3
  import { NoRestrictedImportsRule } from './no-restricted-imports.js';
3
4
 
4
5
  const ruleTester = new RuleTester();
@@ -1,5 +1,9 @@
1
- import { ESLintUtils } from '@typescript-eslint/utils';
2
- import { getModulePath, isDirectRequireCall, isRequireMemberCall } from '../utils/index.js';
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 = ESLintUtils.RuleCreator.withoutDocs({
29
+ export const NoRestrictedImportsRule = createRule({
30
+ name: 'no-restricted-imports',
26
31
  meta: {
27
32
  type: 'problem',
28
33
  docs: {
@@ -1,4 +1,5 @@
1
1
  import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
2
3
  import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
3
4
 
4
5
  const ruleTester = new RuleTester();
@@ -1,12 +1,14 @@
1
- import { ESLintUtils } from '@typescript-eslint/utils';
1
+ import { TSESTree } from '@typescript-eslint/types';
2
+
2
3
  import {
3
4
  isNodeTypeClass,
4
5
  findClassProperty,
5
6
  findObjectProperty,
6
- getBooleanLiteralValue,
7
+ createRule,
7
8
  } from '../utils/index.js';
8
9
 
9
- export const NodeUsableAsToolRule = ESLintUtils.RuleCreator.withoutDocs({
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 !== 'ObjectExpression') {
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 === 'ObjectExpression') {
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![0] + 1;
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: [{ messageId: 'invalidPackageName', data: { packageName: 'my-package' } }],
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: [{ messageId: 'invalidPackageName', data: { packageName: 'n8n-example' } }],
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
- { messageId: 'invalidPackageName', data: { packageName: '@company/example-nodes' } },
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: [{ messageId: 'invalidPackageName', data: { packageName: 'nodes-n8n-example' } }],
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: [{ messageId: 'invalidPackageName', data: { packageName: '' } }],
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 { ESLintUtils, TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils';
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
- export const PackageNameConventionRule = ESLintUtils.RuleCreator.withoutDocs({
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,4 +1,5 @@
1
1
  import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
2
3
  import { ResourceOperationPatternRule } from './resource-operation-pattern.js';
3
4
 
4
5
  const ruleTester = new RuleTester();
@@ -1,13 +1,17 @@
1
- import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
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 = ESLintUtils.RuleCreator.withoutDocs({
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 !== 'ObjectExpression') {
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 (!propertiesProperty?.value || propertiesProperty.value.type !== 'ArrayExpression') {
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 !== 'ObjectExpression') {
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 === 'ArrayExpression') {
72
+ if (optionsProperty?.value?.type === AST_NODE_TYPES.ArrayExpression) {
66
73
  operationCount = optionsProperty.value.elements.length;
67
74
  }
68
75
  }
@@ -1,11 +1,13 @@
1
- import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils';
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 === 'TSClassImplements' &&
8
- impl.expression.type === 'Identifier' &&
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 === 'Identifier' && node.superClass.name === 'Node') {
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 === 'PropertyDefinition' &&
37
- member.key?.type === 'Identifier' &&
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 === 'PropertyDefinition' ? property : null;
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 === 'Property' && prop.key.type === 'Identifier' && prop.key.name === propertyName,
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 === 'Property' ? property : null;
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 === 'Literal') {
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 === 'TemplateLiteral' &&
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 === 'ArrayExpression') {
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 !== 'ArrayExpression') return false;
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 === 'Literal' &&
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 === 'Identifier' && node.callee.name === 'require' && node.arguments.length > 0
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 === 'MemberExpression' &&
135
- node.callee.object.type === 'Identifier' &&
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 === 'ObjectExpression') {
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 || undefined,
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
+ }