@n8n/eslint-plugin-community-nodes 0.19.0 → 0.21.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 (84) hide show
  1. package/.turbo/turbo-build$colon$unchecked.log +4 -0
  2. package/README.md +9 -1
  3. package/dist/plugin.d.ts +48 -0
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +16 -0
  6. package/dist/plugin.js.map +1 -1
  7. package/dist/rules/cred-filename-against-convention.d.ts +2 -0
  8. package/dist/rules/cred-filename-against-convention.d.ts.map +1 -0
  9. package/dist/rules/cred-filename-against-convention.js +42 -0
  10. package/dist/rules/cred-filename-against-convention.js.map +1 -0
  11. package/dist/rules/icon-prefer-themed-variants.d.ts +2 -0
  12. package/dist/rules/icon-prefer-themed-variants.d.ts.map +1 -0
  13. package/dist/rules/icon-prefer-themed-variants.js +53 -0
  14. package/dist/rules/icon-prefer-themed-variants.js.map +1 -0
  15. package/dist/rules/icon-validation.d.ts +1 -1
  16. package/dist/rules/icon-validation.d.ts.map +1 -1
  17. package/dist/rules/icon-validation.js +4 -25
  18. package/dist/rules/icon-validation.js.map +1 -1
  19. package/dist/rules/index.d.ts +9 -1
  20. package/dist/rules/index.d.ts.map +1 -1
  21. package/dist/rules/index.js +16 -0
  22. package/dist/rules/index.js.map +1 -1
  23. package/dist/rules/no-dangerous-functions.d.ts +2 -0
  24. package/dist/rules/no-dangerous-functions.d.ts.map +1 -0
  25. package/dist/rules/no-dangerous-functions.js +121 -0
  26. package/dist/rules/no-dangerous-functions.js.map +1 -0
  27. package/dist/rules/no-emoji-in-options.d.ts +2 -0
  28. package/dist/rules/no-emoji-in-options.d.ts.map +1 -0
  29. package/dist/rules/no-emoji-in-options.js +86 -0
  30. package/dist/rules/no-emoji-in-options.js.map +1 -0
  31. package/dist/rules/node-filename-against-convention.d.ts +2 -0
  32. package/dist/rules/node-filename-against-convention.d.ts.map +1 -0
  33. package/dist/rules/node-filename-against-convention.js +61 -0
  34. package/dist/rules/node-filename-against-convention.js.map +1 -0
  35. package/dist/rules/node-registration-complete.d.ts +2 -0
  36. package/dist/rules/node-registration-complete.d.ts.map +1 -0
  37. package/dist/rules/node-registration-complete.js +60 -0
  38. package/dist/rules/node-registration-complete.js.map +1 -0
  39. package/dist/rules/require-version.d.ts +2 -0
  40. package/dist/rules/require-version.d.ts.map +1 -0
  41. package/dist/rules/require-version.js +50 -0
  42. package/dist/rules/require-version.js.map +1 -0
  43. package/dist/rules/valid-author.d.ts +2 -0
  44. package/dist/rules/valid-author.d.ts.map +1 -0
  45. package/dist/rules/valid-author.js +89 -0
  46. package/dist/rules/valid-author.js.map +1 -0
  47. package/dist/typecheck.tsbuildinfo +1 -0
  48. package/dist/utils/file-utils.d.ts +7 -2
  49. package/dist/utils/file-utils.d.ts.map +1 -1
  50. package/dist/utils/file-utils.js +47 -10
  51. package/dist/utils/file-utils.js.map +1 -1
  52. package/docs/rules/cred-filename-against-convention.md +42 -0
  53. package/docs/rules/icon-prefer-themed-variants.md +71 -0
  54. package/docs/rules/icon-validation.md +5 -3
  55. package/docs/rules/no-dangerous-functions.md +41 -0
  56. package/docs/rules/no-emoji-in-options.md +60 -0
  57. package/docs/rules/node-filename-against-convention.md +50 -0
  58. package/docs/rules/node-registration-complete.md +46 -0
  59. package/docs/rules/require-version.md +51 -0
  60. package/docs/rules/valid-author.md +60 -0
  61. package/package.json +5 -4
  62. package/src/plugin.ts +16 -0
  63. package/src/rules/cred-filename-against-convention.test.ts +72 -0
  64. package/src/rules/cred-filename-against-convention.ts +48 -0
  65. package/src/rules/icon-prefer-themed-variants.test.ts +128 -0
  66. package/src/rules/icon-prefer-themed-variants.ts +70 -0
  67. package/src/rules/icon-validation.test.ts +10 -0
  68. package/src/rules/icon-validation.ts +4 -28
  69. package/src/rules/index.ts +16 -0
  70. package/src/rules/no-dangerous-functions.test.ts +83 -0
  71. package/src/rules/no-dangerous-functions.ts +155 -0
  72. package/src/rules/no-emoji-in-options.test.ts +157 -0
  73. package/src/rules/no-emoji-in-options.ts +105 -0
  74. package/src/rules/node-filename-against-convention.test.ts +115 -0
  75. package/src/rules/node-filename-against-convention.ts +76 -0
  76. package/src/rules/node-registration-complete.test.ts +87 -0
  77. package/src/rules/node-registration-complete.ts +79 -0
  78. package/src/rules/require-version.test.ts +90 -0
  79. package/src/rules/require-version.ts +62 -0
  80. package/src/rules/valid-author.test.ts +108 -0
  81. package/src/rules/valid-author.ts +100 -0
  82. package/src/utils/file-utils.ts +58 -11
  83. package/.turbo/turbo-build.log +0 -4
  84. package/tsconfig.build.tsbuildinfo +0 -1
@@ -0,0 +1,79 @@
1
+ import type { TSESTree } from '@typescript-eslint/utils';
2
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
3
+ import * as path from 'node:path';
4
+
5
+ import {
6
+ createRule,
7
+ findJsonProperty,
8
+ findNodeSourceFilesOnDisk,
9
+ readPackageJsonNodes,
10
+ } from '../utils/index.js';
11
+
12
+ export const NodeRegistrationCompleteRule = createRule({
13
+ name: 'node-registration-complete',
14
+ meta: {
15
+ type: 'problem',
16
+ docs: {
17
+ description:
18
+ 'Ensure every `.node.ts` file in the `nodes/` directory is registered in the "n8n.nodes" array of package.json',
19
+ },
20
+ messages: {
21
+ nodeNotRegistered:
22
+ 'The node file "{{ nodeFile }}" is not registered in the "n8n.nodes" array of package.json. Add it so n8n can discover the node.',
23
+ },
24
+ schema: [],
25
+ },
26
+ defaultOptions: [],
27
+ create(context) {
28
+ if (!context.filename.endsWith('package.json')) {
29
+ return {};
30
+ }
31
+
32
+ return {
33
+ ObjectExpression(node: TSESTree.ObjectExpression) {
34
+ // Only inspect the root object of the package.json file.
35
+ if (node.parent?.type !== AST_NODE_TYPES.ExpressionStatement) {
36
+ return;
37
+ }
38
+
39
+ const nodeFilesOnDisk = findNodeSourceFilesOnDisk(context.filename);
40
+ if (nodeFilesOnDisk.length === 0) {
41
+ return;
42
+ }
43
+
44
+ const registered = new Set(
45
+ readPackageJsonNodes(context.filename).map((filePath) => path.resolve(filePath)),
46
+ );
47
+
48
+ const packageDir = path.dirname(context.filename);
49
+ const reportTarget = resolveReportTarget(node);
50
+
51
+ for (const nodeFile of nodeFilesOnDisk) {
52
+ if (registered.has(path.resolve(nodeFile))) {
53
+ continue;
54
+ }
55
+
56
+ context.report({
57
+ node: reportTarget,
58
+ messageId: 'nodeNotRegistered',
59
+ data: { nodeFile: path.relative(packageDir, nodeFile) },
60
+ });
61
+ }
62
+ },
63
+ };
64
+ },
65
+ });
66
+
67
+ /**
68
+ * Reports against the most specific available node: the `n8n.nodes` array, the
69
+ * `n8n` object, or the package.json root object as a fallback.
70
+ */
71
+ function resolveReportTarget(root: TSESTree.ObjectExpression): TSESTree.Node {
72
+ const n8nProperty = findJsonProperty(root, 'n8n');
73
+ if (n8nProperty?.value.type !== AST_NODE_TYPES.ObjectExpression) {
74
+ return n8nProperty ?? root;
75
+ }
76
+
77
+ const nodesProperty = findJsonProperty(n8nProperty.value, 'nodes');
78
+ return nodesProperty ?? n8nProperty;
79
+ }
@@ -0,0 +1,90 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { RequireVersionRule } from './require-version.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ ruleTester.run('require-version', RequireVersionRule, {
8
+ valid: [
9
+ {
10
+ name: 'version is a valid semver string',
11
+ filename: 'package.json',
12
+ code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
13
+ },
14
+ {
15
+ name: 'version with pre-release and build metadata',
16
+ filename: 'package.json',
17
+ code: '{ "name": "n8n-nodes-example", "version": "1.2.3-beta.1+build.5" }',
18
+ },
19
+ {
20
+ name: 'zero version is valid',
21
+ filename: 'package.json',
22
+ code: '{ "name": "n8n-nodes-example", "version": "0.1.0" }',
23
+ },
24
+ {
25
+ name: 'non-package.json file is ignored',
26
+ filename: 'some-config.json',
27
+ code: '{ "name": "n8n-nodes-example" }',
28
+ },
29
+ {
30
+ name: 'nested objects with missing version are not checked',
31
+ filename: 'package.json',
32
+ code: '{ "name": "n8n-nodes-example", "version": "1.0.0", "config": { "nested": "value" } }',
33
+ },
34
+ {
35
+ name: 'objects inside arrays (e.g. contributors) are not flagged',
36
+ filename: 'package.json',
37
+ code: `{
38
+ "name": "n8n-nodes-example",
39
+ "version": "1.0.0",
40
+ "contributors": [
41
+ { "name": "Alice", "email": "alice@example.com" }
42
+ ]
43
+ }`,
44
+ },
45
+ ],
46
+ invalid: [
47
+ {
48
+ name: 'version field is missing entirely',
49
+ filename: 'package.json',
50
+ code: '{ "name": "n8n-nodes-example", "description": "Example" }',
51
+ errors: [{ messageId: 'missingVersion' }],
52
+ },
53
+ {
54
+ name: 'empty package.json object',
55
+ filename: 'package.json',
56
+ code: '{}',
57
+ errors: [{ messageId: 'missingVersion' }],
58
+ },
59
+ {
60
+ name: 'version is an empty string',
61
+ filename: 'package.json',
62
+ code: '{ "name": "n8n-nodes-example", "version": "" }',
63
+ errors: [{ messageId: 'invalidVersion' }],
64
+ },
65
+ {
66
+ name: 'version is not a valid semver',
67
+ filename: 'package.json',
68
+ code: '{ "name": "n8n-nodes-example", "version": "1.0" }',
69
+ errors: [{ messageId: 'invalidVersion' }],
70
+ },
71
+ {
72
+ name: 'version has a leading "v"',
73
+ filename: 'package.json',
74
+ code: '{ "name": "n8n-nodes-example", "version": "v1.0.0" }',
75
+ errors: [{ messageId: 'invalidVersion' }],
76
+ },
77
+ {
78
+ name: 'version is a range, not an exact version',
79
+ filename: 'package.json',
80
+ code: '{ "name": "n8n-nodes-example", "version": "^1.0.0" }',
81
+ errors: [{ messageId: 'invalidVersion' }],
82
+ },
83
+ {
84
+ name: 'version is a number, not a string',
85
+ filename: 'package.json',
86
+ code: '{ "name": "n8n-nodes-example", "version": 1 }',
87
+ errors: [{ messageId: 'invalidVersion' }],
88
+ },
89
+ ],
90
+ });
@@ -0,0 +1,62 @@
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
+ // Official SemVer 2.0.0 regex (https://semver.org/), anchored.
7
+ const SEMVER_REGEX =
8
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
9
+
10
+ export const RequireVersionRule = createRule({
11
+ name: 'require-version',
12
+ meta: {
13
+ type: 'problem',
14
+ docs: {
15
+ description: 'Require a valid "version" field in community node package.json',
16
+ },
17
+ messages: {
18
+ missingVersion:
19
+ 'The package.json must have a "version" field. npm requires a valid semantic version (e.g. "1.0.0") to publish the package.',
20
+ invalidVersion:
21
+ 'The "version" field must be a valid semantic version string (e.g. "1.0.0"), got {{ value }}.',
22
+ },
23
+ schema: [],
24
+ },
25
+ defaultOptions: [],
26
+ create(context) {
27
+ if (!context.filename.endsWith('package.json')) {
28
+ return {};
29
+ }
30
+
31
+ return {
32
+ ObjectExpression(node: TSESTree.ObjectExpression) {
33
+ if (node.parent?.type !== AST_NODE_TYPES.ExpressionStatement) {
34
+ return;
35
+ }
36
+
37
+ const versionProp = findJsonProperty(node, 'version');
38
+
39
+ if (!versionProp) {
40
+ context.report({
41
+ node,
42
+ messageId: 'missingVersion',
43
+ });
44
+ return;
45
+ }
46
+
47
+ const valueNode = versionProp.value;
48
+ const value = valueNode.type === AST_NODE_TYPES.Literal ? valueNode.value : null;
49
+
50
+ if (typeof value !== 'string' || !SEMVER_REGEX.test(value)) {
51
+ const rawValue =
52
+ valueNode.type === AST_NODE_TYPES.Literal ? String(valueNode.raw) : 'non-literal';
53
+ context.report({
54
+ node: versionProp,
55
+ messageId: 'invalidVersion',
56
+ data: { value: rawValue },
57
+ });
58
+ }
59
+ },
60
+ };
61
+ },
62
+ });
@@ -0,0 +1,108 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { ValidAuthorRule } from './valid-author.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ ruleTester.run('valid-author', ValidAuthorRule, {
8
+ valid: [
9
+ {
10
+ name: 'author object with non-empty name and email',
11
+ filename: 'package.json',
12
+ code: '{ "name": "n8n-nodes-example", "author": { "name": "Jane Doe", "email": "jane@example.com" } }',
13
+ },
14
+ {
15
+ name: 'author object with extra fields',
16
+ filename: 'package.json',
17
+ code: '{ "author": { "name": "Jane Doe", "email": "jane@example.com", "url": "https://example.com" } }',
18
+ },
19
+ {
20
+ name: 'author shorthand string with name and email',
21
+ filename: 'package.json',
22
+ code: '{ "author": "Jane Doe <jane@example.com>" }',
23
+ },
24
+ {
25
+ name: 'author shorthand string with name, email and url',
26
+ filename: 'package.json',
27
+ code: '{ "author": "Jane Doe <jane@example.com> (https://example.com)" }',
28
+ },
29
+ {
30
+ name: 'non-package.json file is ignored',
31
+ filename: 'some-config.json',
32
+ code: '{ "name": "n8n-nodes-example" }',
33
+ },
34
+ {
35
+ name: 'nested objects are not checked',
36
+ filename: 'package.json',
37
+ code: '{ "author": { "name": "Jane Doe", "email": "jane@example.com" }, "config": { "nested": "value" } }',
38
+ },
39
+ ],
40
+ invalid: [
41
+ {
42
+ name: 'missing author field entirely',
43
+ filename: 'package.json',
44
+ code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
45
+ errors: [{ messageId: 'authorMissing' }],
46
+ },
47
+ {
48
+ name: 'empty package.json object',
49
+ filename: 'package.json',
50
+ code: '{}',
51
+ errors: [{ messageId: 'authorMissing' }],
52
+ },
53
+ {
54
+ name: 'author object missing email',
55
+ filename: 'package.json',
56
+ code: '{ "author": { "name": "Jane Doe" } }',
57
+ errors: [{ messageId: 'authorEmailMissing' }],
58
+ },
59
+ {
60
+ name: 'author object missing name',
61
+ filename: 'package.json',
62
+ code: '{ "author": { "email": "jane@example.com" } }',
63
+ errors: [{ messageId: 'authorNameMissing' }],
64
+ },
65
+ {
66
+ name: 'author object with empty name and email strings',
67
+ filename: 'package.json',
68
+ code: '{ "author": { "name": "", "email": " " } }',
69
+ errors: [{ messageId: 'authorNameMissing' }, { messageId: 'authorEmailMissing' }],
70
+ },
71
+ {
72
+ name: 'author object with non-string name',
73
+ filename: 'package.json',
74
+ code: '{ "author": { "name": 123, "email": "jane@example.com" } }',
75
+ errors: [{ messageId: 'authorNameMissing' }],
76
+ },
77
+ {
78
+ name: 'author shorthand string with name only',
79
+ filename: 'package.json',
80
+ code: '{ "author": "Jane Doe" }',
81
+ errors: [{ messageId: 'authorEmailMissing' }],
82
+ },
83
+ {
84
+ name: 'author shorthand string with email only',
85
+ filename: 'package.json',
86
+ code: '{ "author": "<jane@example.com>" }',
87
+ errors: [{ messageId: 'authorNameMissing' }],
88
+ },
89
+ {
90
+ name: 'author shorthand string with empty email angle brackets',
91
+ filename: 'package.json',
92
+ code: '{ "author": "Jane Doe <>" }',
93
+ errors: [{ messageId: 'authorEmailMissing' }],
94
+ },
95
+ {
96
+ name: 'empty author string',
97
+ filename: 'package.json',
98
+ code: '{ "author": "" }',
99
+ errors: [{ messageId: 'authorNameMissing' }, { messageId: 'authorEmailMissing' }],
100
+ },
101
+ {
102
+ name: 'author as empty array',
103
+ filename: 'package.json',
104
+ code: '{ "author": [] }',
105
+ errors: [{ messageId: 'authorMissing' }],
106
+ },
107
+ ],
108
+ });
@@ -0,0 +1,100 @@
1
+ import type { TSESTree } from '@typescript-eslint/utils';
2
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
3
+
4
+ import { createRule, findJsonProperty, getTopLevelObjectInJson } from '../utils/index.js';
5
+
6
+ /**
7
+ * Parses an npm "person" shorthand string of the form
8
+ * `Name <email> (url)` into its name and email parts. Any part may be absent.
9
+ */
10
+ function parsePersonString(value: string): { name: string; email: string } {
11
+ const emailMatch = /<([^>]*)>/.exec(value);
12
+ const email = (emailMatch?.[1] ?? '').trim();
13
+
14
+ const ltIndex = value.indexOf('<');
15
+ const parenIndex = value.indexOf('(');
16
+ let nameEnd = value.length;
17
+ if (ltIndex !== -1) nameEnd = Math.min(nameEnd, ltIndex);
18
+ if (parenIndex !== -1) nameEnd = Math.min(nameEnd, parenIndex);
19
+ const name = value.slice(0, nameEnd).trim();
20
+
21
+ return { name, email };
22
+ }
23
+
24
+ function isNonEmptyStringLiteral(node: TSESTree.Property | null): boolean {
25
+ if (!node || node.value.type !== AST_NODE_TYPES.Literal) {
26
+ return false;
27
+ }
28
+ const { value } = node.value;
29
+ return typeof value === 'string' && value.trim().length > 0;
30
+ }
31
+
32
+ export const ValidAuthorRule = createRule({
33
+ name: 'valid-author',
34
+ meta: {
35
+ type: 'problem',
36
+ docs: {
37
+ description: 'Require a non-empty author name and email in package.json',
38
+ },
39
+ messages: {
40
+ authorMissing: 'package.json must have an "author" field with a non-empty name and email.',
41
+ authorNameMissing: 'The "author" field must include a non-empty name.',
42
+ authorEmailMissing: 'The "author" field must include a non-empty email.',
43
+ },
44
+ schema: [],
45
+ },
46
+ defaultOptions: [],
47
+ create(context) {
48
+ if (!context.filename.endsWith('package.json')) {
49
+ return {};
50
+ }
51
+
52
+ return {
53
+ ObjectExpression(node: TSESTree.ObjectExpression) {
54
+ const root = getTopLevelObjectInJson(node);
55
+ if (!root) {
56
+ return;
57
+ }
58
+
59
+ const authorProp = findJsonProperty(root, 'author');
60
+ if (!authorProp) {
61
+ context.report({ node: root, messageId: 'authorMissing' });
62
+ return;
63
+ }
64
+
65
+ const authorValue = authorProp.value;
66
+
67
+ // Shorthand string form: "Name <email> (url)"
68
+ if (authorValue.type === AST_NODE_TYPES.Literal) {
69
+ if (typeof authorValue.value !== 'string') {
70
+ context.report({ node: authorProp, messageId: 'authorMissing' });
71
+ return;
72
+ }
73
+
74
+ const { name, email } = parsePersonString(authorValue.value);
75
+ if (name.length === 0) {
76
+ context.report({ node: authorProp, messageId: 'authorNameMissing' });
77
+ }
78
+ if (email.length === 0) {
79
+ context.report({ node: authorProp, messageId: 'authorEmailMissing' });
80
+ }
81
+ return;
82
+ }
83
+
84
+ // Object form: { "name": "...", "email": "..." }
85
+ if (authorValue.type === AST_NODE_TYPES.ObjectExpression) {
86
+ if (!isNonEmptyStringLiteral(findJsonProperty(authorValue, 'name'))) {
87
+ context.report({ node: authorProp, messageId: 'authorNameMissing' });
88
+ }
89
+ if (!isNonEmptyStringLiteral(findJsonProperty(authorValue, 'email'))) {
90
+ context.report({ node: authorProp, messageId: 'authorEmailMissing' });
91
+ }
92
+ return;
93
+ }
94
+
95
+ // Any other shape (e.g. array, null) cannot carry a name and email.
96
+ context.report({ node: authorProp, messageId: 'authorMissing' });
97
+ },
98
+ };
99
+ },
100
+ });
@@ -158,20 +158,17 @@ export function validateIconPath(
158
158
  ): {
159
159
  isValid: boolean;
160
160
  isFile: boolean;
161
- isSvg: boolean;
162
161
  exists: boolean;
163
162
  } {
164
163
  const isFile = iconPath.startsWith('file:');
165
164
  const relativePath = iconPath.replace(/^file:/, '');
166
- const isSvg = relativePath.endsWith('.svg');
167
165
  // Should not use safeJoinPath here because iconPath can be outside of the node class folder
168
166
  const fullPath = path.join(baseDir, relativePath);
169
167
  const exists = fileExistsWithCaseSync(fullPath);
170
168
 
171
169
  return {
172
- isValid: isFile && isSvg && exists,
170
+ isValid: isFile && exists,
173
171
  isFile,
174
- isSvg,
175
172
  exists,
176
173
  };
177
174
  }
@@ -182,6 +179,44 @@ export function readPackageJsonNodes(packageJsonPath: string): string[] {
182
179
  return resolveN8nFilePaths(packageJsonPath, nodePaths);
183
180
  }
184
181
 
182
+ function findFilesRecursively(dir: string, matches: (fileName: string) => boolean): string[] {
183
+ const results: string[] = [];
184
+
185
+ let entries;
186
+ try {
187
+ entries = readdirSync(dir, { withFileTypes: true });
188
+ } catch {
189
+ return results;
190
+ }
191
+
192
+ for (const entry of entries) {
193
+ const fullPath = path.join(dir, entry.name);
194
+ if (entry.isDirectory()) {
195
+ results.push(...findFilesRecursively(fullPath, matches));
196
+ } else if (entry.isFile() && matches(entry.name)) {
197
+ results.push(fullPath);
198
+ }
199
+ }
200
+
201
+ return results;
202
+ }
203
+
204
+ /**
205
+ * Finds all `*.node.ts` source files in the package's `nodes/` directory,
206
+ * returning their absolute paths. Returns an empty array if there is no
207
+ * `nodes/` directory.
208
+ */
209
+ export function findNodeSourceFilesOnDisk(packageJsonPath: string): string[] {
210
+ const packageDir = dirname(packageJsonPath);
211
+ const nodesDir = safeJoinPath(packageDir, 'nodes');
212
+
213
+ if (!existsSync(nodesDir)) {
214
+ return [];
215
+ }
216
+
217
+ return findFilesRecursively(nodesDir, (fileName) => fileName.endsWith('.node.ts'));
218
+ }
219
+
185
220
  export function areAllCredentialUsagesTestedByNodes(
186
221
  credentialName: string,
187
222
  packageDir: string,
@@ -268,7 +303,9 @@ function fileExistsWithCaseSync(filePath: string): boolean {
268
303
  }
269
304
  }
270
305
 
271
- export function findSimilarSvgFiles(targetPath: string, baseDir: string): string[] {
306
+ const ICON_EXTENSIONS = ['.svg', '.png'];
307
+
308
+ export function findSimilarIconFiles(targetPath: string, baseDir: string): string[] {
272
309
  try {
273
310
  const targetFileName = path.basename(targetPath, path.extname(targetPath));
274
311
  const targetDir = path.dirname(targetPath);
@@ -279,15 +316,25 @@ export function findSimilarSvgFiles(targetPath: string, baseDir: string): string
279
316
  return [];
280
317
  }
281
318
 
282
- const files = readdirSync(searchDir);
283
- const svgFileNames = files
284
- .filter((file) => file.endsWith('.svg'))
285
- .map((file) => path.basename(file, '.svg'));
319
+ const files = readdirSync(searchDir).filter((file) =>
320
+ ICON_EXTENSIONS.includes(path.extname(file).toLowerCase()),
321
+ );
322
+
323
+ // Map icon base names to their actual filenames so suggestions keep their extension.
324
+ const baseNameToFiles = new Map<string, string[]>();
325
+ for (const file of files) {
326
+ const baseName = path.basename(file, path.extname(file));
327
+ const existing = baseNameToFiles.get(baseName) ?? [];
328
+ existing.push(file);
329
+ baseNameToFiles.set(baseName, existing);
330
+ }
286
331
 
287
- const candidateNames = new Set(svgFileNames);
332
+ const candidateNames = new Set(baseNameToFiles.keys());
288
333
  const similarNames = findSimilarStrings(targetFileName, candidateNames);
289
334
 
290
- return similarNames.map((name) => path.join(targetDir, `${name}.svg`));
335
+ return similarNames.flatMap((name) =>
336
+ (baseNameToFiles.get(name) ?? []).map((file) => path.join(targetDir, file)),
337
+ );
291
338
  } catch {
292
339
  return [];
293
340
  }
@@ -1,4 +0,0 @@
1
-
2
- > @n8n/eslint-plugin-community-nodes@0.19.0 build /home/runner/work/n8n/n8n/packages/@n8n/eslint-plugin-community-nodes
3
- > tsc --project tsconfig.build.json
4
-