@n8n/eslint-plugin-community-nodes 0.3.0 → 0.5.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 +14 -0
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +85 -18
- 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 +108 -18
- package/src/utils/index.ts +1 -0
- package/src/utils/rule-creator.ts +6 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.eslint.json +5 -0
- package/tsconfig.json +1 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
2
3
|
import { CredentialTestRequiredRule } from './credential-test-required.js';
|
|
3
4
|
|
|
4
5
|
const ruleTester = new RuleTester();
|
|
@@ -20,13 +21,13 @@ function createCredentialCode(options: {
|
|
|
20
21
|
} = options;
|
|
21
22
|
|
|
22
23
|
const imports = hasTest
|
|
23
|
-
?
|
|
24
|
-
:
|
|
24
|
+
? "import type { ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';"
|
|
25
|
+
: "import type { ICredentialType, INodeProperties } from 'n8n-workflow';";
|
|
25
26
|
|
|
26
27
|
const extendsStr = extendsArray ? `\n\textends = ${JSON.stringify(extendsArray)};` : '';
|
|
27
28
|
|
|
28
29
|
const testProperty = hasTest
|
|
29
|
-
?
|
|
30
|
+
? "\n\n\ttest: ICredentialTestRequest = {\n\t\trequest: {\n\t\t\tbaseURL: 'https://api.example.com',\n\t\t\turl: '/test',\n\t\t},\n\t};"
|
|
30
31
|
: '';
|
|
31
32
|
|
|
32
33
|
return `
|
|
@@ -46,57 +47,6 @@ export class ${className} {
|
|
|
46
47
|
}`;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
function createNodeCode(options: {
|
|
50
|
-
name?: string;
|
|
51
|
-
displayName?: string;
|
|
52
|
-
credentials?: Array<string | { name: string; testedBy?: string }>;
|
|
53
|
-
extendsClass?: string;
|
|
54
|
-
}): string {
|
|
55
|
-
const { name = 'myNode', displayName = 'My Node', credentials = [], extendsClass } = options;
|
|
56
|
-
|
|
57
|
-
const credentialsArray = credentials
|
|
58
|
-
.map((cred) => {
|
|
59
|
-
if (typeof cred === 'string') {
|
|
60
|
-
return `'${cred}'`;
|
|
61
|
-
}
|
|
62
|
-
const testedByStr = cred.testedBy ? `, testedBy: '${cred.testedBy}'` : '';
|
|
63
|
-
return `{ name: '${cred.name}'${testedByStr} }`;
|
|
64
|
-
})
|
|
65
|
-
.join(', ');
|
|
66
|
-
|
|
67
|
-
const classDeclaration = extendsClass
|
|
68
|
-
? `export class ${name.charAt(0).toUpperCase() + name.slice(1)} extends ${extendsClass}`
|
|
69
|
-
: `export class ${name.charAt(0).toUpperCase() + name.slice(1)} implements INodeType`;
|
|
70
|
-
|
|
71
|
-
const descriptionProperty = extendsClass
|
|
72
|
-
? `description = {` // Extending classes might not need full INodeTypeDescription
|
|
73
|
-
: `description: INodeTypeDescription = {`;
|
|
74
|
-
|
|
75
|
-
return `
|
|
76
|
-
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
77
|
-
|
|
78
|
-
${classDeclaration} {
|
|
79
|
-
${descriptionProperty}
|
|
80
|
-
displayName: '${displayName}',
|
|
81
|
-
name: '${name}',
|
|
82
|
-
group: ['transform'],
|
|
83
|
-
version: 1,
|
|
84
|
-
description: 'A test node',
|
|
85
|
-
defaults: {
|
|
86
|
-
name: '${displayName}',
|
|
87
|
-
},
|
|
88
|
-
inputs: ['main'],
|
|
89
|
-
outputs: ['main'],
|
|
90
|
-
credentials: [${credentialsArray}],
|
|
91
|
-
properties: [],
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
execute() {
|
|
95
|
-
return Promise.resolve([]);
|
|
96
|
-
}
|
|
97
|
-
}`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
50
|
ruleTester.run('credential-test-required', CredentialTestRequiredRule, {
|
|
101
51
|
valid: [
|
|
102
52
|
{
|
|
@@ -129,19 +79,96 @@ ruleTester.run('credential-test-required', CredentialTestRequiredRule, {
|
|
|
129
79
|
name: 'credential class missing test property and no testedBy in package',
|
|
130
80
|
filename: 'MyApi.credentials.ts',
|
|
131
81
|
code: createCredentialCode({}),
|
|
132
|
-
errors: [
|
|
82
|
+
errors: [
|
|
83
|
+
{
|
|
84
|
+
messageId: 'missingCredentialTest',
|
|
85
|
+
data: { className: 'MyApi' },
|
|
86
|
+
suggestions: [
|
|
87
|
+
{
|
|
88
|
+
messageId: 'addTemplate',
|
|
89
|
+
output: `
|
|
90
|
+
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
91
|
+
|
|
92
|
+
export class MyApi implements ICredentialType {
|
|
93
|
+
name = 'myApi';
|
|
94
|
+
displayName = 'My API';
|
|
95
|
+
properties: INodeProperties[] = [];
|
|
96
|
+
|
|
97
|
+
test: ICredentialTestRequest = {
|
|
98
|
+
request: {
|
|
99
|
+
method: 'GET',
|
|
100
|
+
url: '={{$credentials.server}}/test', // Replace with actual endpoint
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}`,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
133
108
|
},
|
|
134
109
|
{
|
|
135
110
|
name: 'credential class with extends but not oAuth2Api and no testedBy in package',
|
|
136
111
|
filename: 'MyApi.credentials.ts',
|
|
137
112
|
code: createCredentialCode({ extends: ['someOtherApi'] }),
|
|
138
|
-
errors: [
|
|
113
|
+
errors: [
|
|
114
|
+
{
|
|
115
|
+
messageId: 'missingCredentialTest',
|
|
116
|
+
data: { className: 'MyApi' },
|
|
117
|
+
suggestions: [
|
|
118
|
+
{
|
|
119
|
+
messageId: 'addTemplate',
|
|
120
|
+
output: `
|
|
121
|
+
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
122
|
+
|
|
123
|
+
export class MyApi implements ICredentialType {
|
|
124
|
+
name = 'myApi';
|
|
125
|
+
extends = ["someOtherApi"];
|
|
126
|
+
displayName = 'My API';
|
|
127
|
+
properties: INodeProperties[] = [];
|
|
128
|
+
|
|
129
|
+
test: ICredentialTestRequest = {
|
|
130
|
+
request: {
|
|
131
|
+
method: 'GET',
|
|
132
|
+
url: '={{$credentials.server}}/test', // Replace with actual endpoint
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}`,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
],
|
|
139
140
|
},
|
|
140
141
|
{
|
|
141
142
|
name: 'credential class with empty extends array and no testedBy in package',
|
|
142
143
|
filename: 'MyApi.credentials.ts',
|
|
143
144
|
code: createCredentialCode({ extends: [] }),
|
|
144
|
-
errors: [
|
|
145
|
+
errors: [
|
|
146
|
+
{
|
|
147
|
+
messageId: 'missingCredentialTest',
|
|
148
|
+
data: { className: 'MyApi' },
|
|
149
|
+
suggestions: [
|
|
150
|
+
{
|
|
151
|
+
messageId: 'addTemplate',
|
|
152
|
+
output: `
|
|
153
|
+
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
154
|
+
|
|
155
|
+
export class MyApi implements ICredentialType {
|
|
156
|
+
name = 'myApi';
|
|
157
|
+
extends = [];
|
|
158
|
+
displayName = 'My API';
|
|
159
|
+
properties: INodeProperties[] = [];
|
|
160
|
+
|
|
161
|
+
test: ICredentialTestRequest = {
|
|
162
|
+
request: {
|
|
163
|
+
method: 'GET',
|
|
164
|
+
url: '={{$credentials.server}}/test', // Replace with actual endpoint
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}`,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
],
|
|
145
172
|
},
|
|
146
173
|
],
|
|
147
174
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ReportSuggestionArray } from '@typescript-eslint/utils/ts-eslint';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
|
|
2
4
|
import {
|
|
3
5
|
isCredentialTypeClass,
|
|
4
6
|
findClassProperty,
|
|
@@ -7,20 +9,23 @@ import {
|
|
|
7
9
|
getStringLiteralValue,
|
|
8
10
|
findPackageJson,
|
|
9
11
|
areAllCredentialUsagesTestedByNodes,
|
|
12
|
+
createRule,
|
|
10
13
|
} from '../utils/index.js';
|
|
11
|
-
import { dirname } from 'node:path';
|
|
12
14
|
|
|
13
|
-
export const CredentialTestRequiredRule =
|
|
15
|
+
export const CredentialTestRequiredRule = createRule({
|
|
16
|
+
name: 'credential-test-required',
|
|
14
17
|
meta: {
|
|
15
18
|
type: 'problem',
|
|
16
19
|
docs: {
|
|
17
20
|
description: 'Ensure credentials have a credential test',
|
|
18
21
|
},
|
|
19
22
|
messages: {
|
|
23
|
+
addTemplate: 'Add basic credential test template',
|
|
20
24
|
missingCredentialTest:
|
|
21
25
|
'Credential class "{{ className }}" must have a test property or be tested by a node via testedBy',
|
|
22
26
|
},
|
|
23
27
|
schema: [],
|
|
28
|
+
hasSuggestions: true,
|
|
24
29
|
},
|
|
25
30
|
defaultOptions: [],
|
|
26
31
|
create(context) {
|
|
@@ -73,27 +78,68 @@ export const CredentialTestRequiredRule = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
73
78
|
|
|
74
79
|
const pkgDir = getPackageDir();
|
|
75
80
|
if (!pkgDir) {
|
|
81
|
+
const suggestions: ReportSuggestionArray<'addTemplate' | 'missingCredentialTest'> = [];
|
|
82
|
+
|
|
83
|
+
const testProperty = createCredentialTestTemplate();
|
|
84
|
+
suggestions.push({
|
|
85
|
+
messageId: 'addTemplate',
|
|
86
|
+
fix(fixer) {
|
|
87
|
+
const classBody = node.body.body;
|
|
88
|
+
const lastProperty = classBody[classBody.length - 1];
|
|
89
|
+
if (lastProperty) {
|
|
90
|
+
return fixer.insertTextAfter(lastProperty, `\n\n${testProperty}`);
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
76
96
|
context.report({
|
|
77
97
|
node,
|
|
78
98
|
messageId: 'missingCredentialTest',
|
|
79
99
|
data: {
|
|
80
|
-
className: node.id?.name
|
|
100
|
+
className: node.id?.name ?? 'Unknown',
|
|
81
101
|
},
|
|
102
|
+
suggest: suggestions,
|
|
82
103
|
});
|
|
83
104
|
return;
|
|
84
105
|
}
|
|
85
106
|
|
|
86
107
|
const allUsagesTestedByNodes = areAllCredentialUsagesTestedByNodes(credentialName, pkgDir);
|
|
87
108
|
if (!allUsagesTestedByNodes) {
|
|
109
|
+
const suggestions: ReportSuggestionArray<'addTemplate' | 'missingCredentialTest'> = [];
|
|
110
|
+
|
|
111
|
+
const testProperty = createCredentialTestTemplate();
|
|
112
|
+
suggestions.push({
|
|
113
|
+
messageId: 'addTemplate',
|
|
114
|
+
fix(fixer) {
|
|
115
|
+
const classBody = node.body.body;
|
|
116
|
+
const lastProperty = classBody[classBody.length - 1];
|
|
117
|
+
if (lastProperty) {
|
|
118
|
+
return fixer.insertTextAfter(lastProperty, `\n\n${testProperty}`);
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
88
124
|
context.report({
|
|
89
125
|
node,
|
|
90
126
|
messageId: 'missingCredentialTest',
|
|
91
127
|
data: {
|
|
92
|
-
className: node.id?.name
|
|
128
|
+
className: node.id?.name ?? 'Unknown',
|
|
93
129
|
},
|
|
130
|
+
suggest: suggestions,
|
|
94
131
|
});
|
|
95
132
|
}
|
|
96
133
|
},
|
|
97
134
|
};
|
|
98
135
|
},
|
|
99
136
|
});
|
|
137
|
+
|
|
138
|
+
function createCredentialTestTemplate(): string {
|
|
139
|
+
return `\ttest: ICredentialTestRequest = {
|
|
140
|
+
\t\trequest: {
|
|
141
|
+
\t\t\tmethod: 'GET',
|
|
142
|
+
\t\t\turl: '={{$credentials.server}}/test', // Replace with actual endpoint
|
|
143
|
+
\t\t},
|
|
144
|
+
\t};`;
|
|
145
|
+
}
|
|
@@ -1,26 +1,51 @@
|
|
|
1
1
|
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
-
import { IconValidationRule } from './icon-validation.js';
|
|
3
|
-
import { vi } from 'vitest';
|
|
4
2
|
import * as fs from 'node:fs';
|
|
3
|
+
import { vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { IconValidationRule } from './icon-validation.js';
|
|
5
6
|
|
|
6
7
|
const ruleTester = new RuleTester();
|
|
7
8
|
|
|
8
9
|
vi.mock('node:fs', () => ({
|
|
9
10
|
existsSync: vi.fn(),
|
|
11
|
+
readdirSync: vi.fn(),
|
|
10
12
|
}));
|
|
11
13
|
|
|
12
14
|
const mockExistsSync = vi.mocked(fs.existsSync);
|
|
15
|
+
const mockReaddirSync = vi.mocked(fs.readdirSync);
|
|
16
|
+
|
|
17
|
+
const mockSvgFiles = [
|
|
18
|
+
'TestNode.svg',
|
|
19
|
+
'ValidIcon.svg',
|
|
20
|
+
'ValidIcon.dark.svg',
|
|
21
|
+
'SameIcon.svg',
|
|
22
|
+
'github.svg',
|
|
23
|
+
];
|
|
13
24
|
|
|
14
25
|
function setupMockFileSystem() {
|
|
15
26
|
mockExistsSync.mockImplementation((path: fs.PathLike) => {
|
|
16
27
|
const pathStr = path.toString();
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
|
|
29
|
+
if (mockSvgFiles.some((file) => pathStr.includes(file)) || pathStr.includes('NotSvg.png')) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (pathStr.endsWith('/tmp/icons') || pathStr.endsWith('/tmp') || pathStr.endsWith('icons')) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return false;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// @ts-expect-error Typescript does not select the correct overload
|
|
41
|
+
mockReaddirSync.mockImplementation((path: fs.PathLike): string[] => {
|
|
42
|
+
const pathStr = path.toString();
|
|
43
|
+
|
|
44
|
+
if (pathStr.includes('icons')) {
|
|
45
|
+
return [...mockSvgFiles, 'NotSvg.png'];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return [];
|
|
24
49
|
});
|
|
25
50
|
}
|
|
26
51
|
|
|
@@ -34,10 +59,10 @@ function createNodeCode(
|
|
|
34
59
|
includeTypeImport: boolean = false,
|
|
35
60
|
): string {
|
|
36
61
|
const typeImport = includeTypeImport
|
|
37
|
-
?
|
|
38
|
-
:
|
|
62
|
+
? "import type { INodeType, INodeTypeDescription } from 'n8n-workflow';"
|
|
63
|
+
: "import type { INodeType } from 'n8n-workflow';";
|
|
39
64
|
|
|
40
|
-
const typeAnnotation = includeTypeImport ?
|
|
65
|
+
const typeAnnotation = includeTypeImport ? ': INodeTypeDescription' : '';
|
|
41
66
|
|
|
42
67
|
let iconProperty = '';
|
|
43
68
|
if (icon) {
|
|
@@ -151,7 +176,18 @@ ruleTester.run('icon-validation', IconValidationRule, {
|
|
|
151
176
|
name: 'node missing icon property in description',
|
|
152
177
|
filename: nodeFilePath,
|
|
153
178
|
code: createNodeCode(undefined, true),
|
|
154
|
-
errors: [
|
|
179
|
+
errors: [
|
|
180
|
+
{
|
|
181
|
+
messageId: 'missingIcon',
|
|
182
|
+
suggestions: [
|
|
183
|
+
{
|
|
184
|
+
messageId: 'addPlaceholder',
|
|
185
|
+
output:
|
|
186
|
+
"\nimport type { INodeType, INodeTypeDescription } from 'n8n-workflow';\n\nexport class TestNode implements INodeType {\n\tdescription: INodeTypeDescription = {\n\t\tdisplayName: 'Test Node',\n\t\tname: 'testNode',\n\t\t\n\t\tgroup: ['input'],\n\t\tversion: 1,\n\t\tdescription: 'A test node',\n\t\tdefaults: {\n\t\t\tname: 'Test Node',\n\t\t},\n\t\tinputs: ['main'],\n\t\toutputs: ['main'],\n\t\tproperties: [],\n\t\ticon: \"file:./icon.svg\",\n\t};\n}",
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
],
|
|
155
191
|
},
|
|
156
192
|
{
|
|
157
193
|
name: 'icon file does not exist in description',
|
|
@@ -175,7 +211,18 @@ ruleTester.run('icon-validation', IconValidationRule, {
|
|
|
175
211
|
name: 'credential missing icon property',
|
|
176
212
|
filename: credentialFilePath,
|
|
177
213
|
code: createCredentialCode(),
|
|
178
|
-
errors: [
|
|
214
|
+
errors: [
|
|
215
|
+
{
|
|
216
|
+
messageId: 'missingIcon',
|
|
217
|
+
suggestions: [
|
|
218
|
+
{
|
|
219
|
+
messageId: 'addPlaceholder',
|
|
220
|
+
output:
|
|
221
|
+
"\nimport type { ICredentialType, INodeProperties } from 'n8n-workflow';\n\nexport class TestCredential implements ICredentialType {\n\tname = 'testApi';\n\tdisplayName = 'Test API';\n\t\n\tproperties: INodeProperties[] = [];\n\n\ticon = \"file:./icon.svg\";\n}",
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
],
|
|
179
226
|
},
|
|
180
227
|
{
|
|
181
228
|
name: 'credential icon file does not exist',
|
|
@@ -192,5 +239,41 @@ ruleTester.run('icon-validation', IconValidationRule, {
|
|
|
192
239
|
}),
|
|
193
240
|
errors: [{ messageId: 'lightDarkSame', data: { iconPath: 'icons/SameIcon.svg' } }],
|
|
194
241
|
},
|
|
242
|
+
{
|
|
243
|
+
name: 'node icon file does not exist but similar file exists - should suggest similar file',
|
|
244
|
+
filename: nodeFilePath,
|
|
245
|
+
code: createNodeCode('file:icons/github2.svg'),
|
|
246
|
+
errors: [
|
|
247
|
+
{
|
|
248
|
+
messageId: 'iconFileNotFound',
|
|
249
|
+
data: { iconPath: 'icons/github2.svg' },
|
|
250
|
+
suggestions: [
|
|
251
|
+
{
|
|
252
|
+
messageId: 'similarIcon',
|
|
253
|
+
data: { suggestedName: 'icons/github.svg' },
|
|
254
|
+
output: `
|
|
255
|
+
import type { INodeType } from 'n8n-workflow';
|
|
256
|
+
|
|
257
|
+
export class TestNode implements INodeType {
|
|
258
|
+
description = {
|
|
259
|
+
displayName: 'Test Node',
|
|
260
|
+
name: 'testNode',
|
|
261
|
+
icon: "file:icons/github.svg",
|
|
262
|
+
group: ['input'],
|
|
263
|
+
version: 1,
|
|
264
|
+
description: 'A test node',
|
|
265
|
+
defaults: {
|
|
266
|
+
name: 'Test Node',
|
|
267
|
+
},
|
|
268
|
+
inputs: ['main'],
|
|
269
|
+
outputs: ['main'],
|
|
270
|
+
properties: [],
|
|
271
|
+
};
|
|
272
|
+
}`,
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
},
|
|
195
278
|
],
|
|
196
279
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import type { ReportSuggestionArray } from '@typescript-eslint/utils/ts-eslint';
|
|
2
3
|
import { dirname } from 'node:path';
|
|
4
|
+
|
|
3
5
|
import {
|
|
4
6
|
isNodeTypeClass,
|
|
5
7
|
isCredentialTypeClass,
|
|
@@ -7,24 +9,34 @@ import {
|
|
|
7
9
|
findObjectProperty,
|
|
8
10
|
getStringLiteralValue,
|
|
9
11
|
validateIconPath,
|
|
12
|
+
findSimilarSvgFiles,
|
|
10
13
|
isFileType,
|
|
14
|
+
createRule,
|
|
11
15
|
} from '../utils/index.js';
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
const messages = {
|
|
18
|
+
iconFileNotFound: 'Icon file "{{ iconPath }}" does not exist',
|
|
19
|
+
iconNotSvg: 'Icon file "{{ iconPath }}" must be an SVG file (end with .svg)',
|
|
20
|
+
lightDarkSame: 'Light and dark icons cannot be the same file. Both point to "{{ iconPath }}"',
|
|
21
|
+
invalidIconPath: 'Icon path "{{ iconPath }}" must use file: protocol and be a string',
|
|
22
|
+
missingIcon: 'Node/Credential class must have an icon property defined',
|
|
23
|
+
addPlaceholder: 'Add icon property with placeholder',
|
|
24
|
+
addFileProtocol: "Add 'file:' protocol to icon path",
|
|
25
|
+
changeExtension: "Change icon extension to '.svg'",
|
|
26
|
+
similarIcon: "Use existing icon '{{ suggestedName }}'",
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
export const IconValidationRule = createRule({
|
|
30
|
+
name: 'icon-validation',
|
|
14
31
|
meta: {
|
|
15
32
|
type: 'problem',
|
|
16
33
|
docs: {
|
|
17
34
|
description:
|
|
18
35
|
'Validate node and credential icon files exist, are SVG format, and light/dark icons are different',
|
|
19
36
|
},
|
|
20
|
-
messages
|
|
21
|
-
iconFileNotFound: 'Icon file "{{ iconPath }}" does not exist',
|
|
22
|
-
iconNotSvg: 'Icon file "{{ iconPath }}" must be an SVG file (end with .svg)',
|
|
23
|
-
lightDarkSame: 'Light and dark icons cannot be the same file. Both point to "{{ iconPath }}"',
|
|
24
|
-
invalidIconPath: 'Icon path "{{ iconPath }}" must use file: protocol and be a string',
|
|
25
|
-
missingIcon: 'Node/Credential class must have an icon property defined',
|
|
26
|
-
},
|
|
37
|
+
messages,
|
|
27
38
|
schema: [],
|
|
39
|
+
hasSuggestions: true,
|
|
28
40
|
},
|
|
29
41
|
defaultOptions: [],
|
|
30
42
|
create(context) {
|
|
@@ -40,7 +52,7 @@ export const IconValidationRule = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
40
52
|
context.report({
|
|
41
53
|
node,
|
|
42
54
|
messageId: 'invalidIconPath',
|
|
43
|
-
data: { iconPath: iconPath
|
|
55
|
+
data: { iconPath: iconPath ?? '' },
|
|
44
56
|
});
|
|
45
57
|
return false;
|
|
46
58
|
}
|
|
@@ -49,30 +61,68 @@ export const IconValidationRule = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
49
61
|
const validation = validateIconPath(iconPath, currentDir);
|
|
50
62
|
|
|
51
63
|
if (!validation.isFile) {
|
|
64
|
+
const suggestions: ReportSuggestionArray<keyof typeof messages> = [];
|
|
65
|
+
if (!iconPath.startsWith('file:')) {
|
|
66
|
+
suggestions.push({
|
|
67
|
+
messageId: 'addFileProtocol',
|
|
68
|
+
fix(fixer) {
|
|
69
|
+
return fixer.replaceText(node, `"file:${iconPath}"`);
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
52
74
|
context.report({
|
|
53
75
|
node,
|
|
54
76
|
messageId: 'invalidIconPath',
|
|
55
77
|
data: { iconPath },
|
|
78
|
+
suggest: suggestions,
|
|
56
79
|
});
|
|
57
80
|
return false;
|
|
58
81
|
}
|
|
59
82
|
|
|
60
83
|
if (!validation.isSvg) {
|
|
61
84
|
const relativePath = iconPath.replace(/^file:/, '');
|
|
85
|
+
const suggestions: ReportSuggestionArray<keyof typeof messages> = [];
|
|
86
|
+
|
|
87
|
+
const pathWithoutExt = relativePath.replace(/\.[^/.]+$/, '');
|
|
88
|
+
const svgPath = `${pathWithoutExt}.svg`;
|
|
89
|
+
suggestions.push({
|
|
90
|
+
messageId: 'changeExtension',
|
|
91
|
+
fix(fixer) {
|
|
92
|
+
return fixer.replaceText(node, `"file:${svgPath}"`);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
62
96
|
context.report({
|
|
63
97
|
node,
|
|
64
98
|
messageId: 'iconNotSvg',
|
|
65
99
|
data: { iconPath: relativePath },
|
|
100
|
+
suggest: suggestions,
|
|
66
101
|
});
|
|
67
102
|
return false;
|
|
68
103
|
}
|
|
69
104
|
|
|
70
105
|
if (!validation.exists) {
|
|
71
106
|
const relativePath = iconPath.replace(/^file:/, '');
|
|
107
|
+
const suggestions: ReportSuggestionArray<keyof typeof messages> = [];
|
|
108
|
+
|
|
109
|
+
// Find similar SVG files in the same directory
|
|
110
|
+
const similarFiles = findSimilarSvgFiles(relativePath, currentDir);
|
|
111
|
+
for (const similarFile of similarFiles) {
|
|
112
|
+
suggestions.push({
|
|
113
|
+
messageId: 'similarIcon',
|
|
114
|
+
data: { suggestedName: similarFile },
|
|
115
|
+
fix(fixer) {
|
|
116
|
+
return fixer.replaceText(node, `"file:${similarFile}"`);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
72
121
|
context.report({
|
|
73
122
|
node,
|
|
74
123
|
messageId: 'iconFileNotFound',
|
|
75
124
|
data: { iconPath: relativePath },
|
|
125
|
+
suggest: suggestions,
|
|
76
126
|
});
|
|
77
127
|
return false;
|
|
78
128
|
}
|
|
@@ -81,10 +131,10 @@ export const IconValidationRule = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
81
131
|
};
|
|
82
132
|
|
|
83
133
|
const validateIconValue = (iconValue: TSESTree.Node) => {
|
|
84
|
-
if (iconValue.type ===
|
|
134
|
+
if (iconValue.type === TSESTree.AST_NODE_TYPES.Literal) {
|
|
85
135
|
const iconPath = getStringLiteralValue(iconValue);
|
|
86
136
|
validateIcon(iconPath, iconValue);
|
|
87
|
-
} else if (iconValue.type ===
|
|
137
|
+
} else if (iconValue.type === TSESTree.AST_NODE_TYPES.ObjectExpression) {
|
|
88
138
|
const lightProperty = findObjectProperty(iconValue, 'light');
|
|
89
139
|
const darkProperty = findObjectProperty(iconValue, 'dark');
|
|
90
140
|
|
|
@@ -121,7 +171,7 @@ export const IconValidationRule = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
121
171
|
const descriptionProperty = findClassProperty(node, 'description');
|
|
122
172
|
if (
|
|
123
173
|
!descriptionProperty?.value ||
|
|
124
|
-
descriptionProperty.value.type !==
|
|
174
|
+
descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression
|
|
125
175
|
) {
|
|
126
176
|
context.report({
|
|
127
177
|
node,
|
|
@@ -130,11 +180,27 @@ export const IconValidationRule = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
130
180
|
return;
|
|
131
181
|
}
|
|
132
182
|
|
|
133
|
-
const
|
|
183
|
+
const descriptionValue = descriptionProperty.value;
|
|
184
|
+
const iconProperty = findObjectProperty(descriptionValue, 'icon');
|
|
134
185
|
if (!iconProperty) {
|
|
186
|
+
const suggestions: ReportSuggestionArray<keyof typeof messages> = [];
|
|
187
|
+
|
|
188
|
+
suggestions.push({
|
|
189
|
+
messageId: 'addPlaceholder',
|
|
190
|
+
fix(fixer) {
|
|
191
|
+
const lastProperty =
|
|
192
|
+
descriptionValue.properties[descriptionValue.properties.length - 1];
|
|
193
|
+
if (lastProperty) {
|
|
194
|
+
return fixer.insertTextAfter(lastProperty, ',\n\t\ticon: "file:./icon.svg"');
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
135
200
|
context.report({
|
|
136
201
|
node,
|
|
137
202
|
messageId: 'missingIcon',
|
|
203
|
+
suggest: suggestions,
|
|
138
204
|
});
|
|
139
205
|
return;
|
|
140
206
|
}
|
|
@@ -143,9 +209,24 @@ export const IconValidationRule = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
143
209
|
} else if (isCredentialClass) {
|
|
144
210
|
const iconProperty = findClassProperty(node, 'icon');
|
|
145
211
|
if (!iconProperty?.value) {
|
|
212
|
+
const suggestions: ReportSuggestionArray<keyof typeof messages> = [];
|
|
213
|
+
|
|
214
|
+
suggestions.push({
|
|
215
|
+
messageId: 'addPlaceholder',
|
|
216
|
+
fix(fixer) {
|
|
217
|
+
const classBody = node.body.body;
|
|
218
|
+
const lastProperty = classBody[classBody.length - 1];
|
|
219
|
+
if (lastProperty) {
|
|
220
|
+
return fixer.insertTextAfter(lastProperty, '\n\n\ticon = "file:./icon.svg";');
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
146
226
|
context.report({
|
|
147
227
|
node,
|
|
148
228
|
messageId: 'missingIcon',
|
|
229
|
+
suggest: suggestions,
|
|
149
230
|
});
|
|
150
231
|
return;
|
|
151
232
|
}
|
package/src/rules/index.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { AnyRuleModule } from '@typescript-eslint/utils/ts-eslint';
|
|
2
|
-
|
|
3
|
-
import {
|
|
2
|
+
|
|
3
|
+
import { CredentialDocumentationUrlRule } from './credential-documentation-url.js';
|
|
4
4
|
import { CredentialPasswordFieldRule } from './credential-password-field.js';
|
|
5
|
+
import { CredentialTestRequiredRule } from './credential-test-required.js';
|
|
6
|
+
import { IconValidationRule } from './icon-validation.js';
|
|
7
|
+
import { NoCredentialReuseRule } from './no-credential-reuse.js';
|
|
5
8
|
import { NoDeprecatedWorkflowFunctionsRule } from './no-deprecated-workflow-functions.js';
|
|
9
|
+
import { NoRestrictedGlobalsRule } from './no-restricted-globals.js';
|
|
10
|
+
import { NoRestrictedImportsRule } from './no-restricted-imports.js';
|
|
6
11
|
import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
|
|
7
12
|
import { PackageNameConventionRule } from './package-name-convention.js';
|
|
8
|
-
import { CredentialTestRequiredRule } from './credential-test-required.js';
|
|
9
|
-
import { NoCredentialReuseRule } from './no-credential-reuse.js';
|
|
10
|
-
import { IconValidationRule } from './icon-validation.js';
|
|
11
13
|
import { ResourceOperationPatternRule } from './resource-operation-pattern.js';
|
|
12
14
|
|
|
13
15
|
export const rules = {
|
|
@@ -21,4 +23,5 @@ export const rules = {
|
|
|
21
23
|
'no-credential-reuse': NoCredentialReuseRule,
|
|
22
24
|
'icon-validation': IconValidationRule,
|
|
23
25
|
'resource-operation-pattern': ResourceOperationPatternRule,
|
|
26
|
+
'credential-documentation-url': CredentialDocumentationUrlRule,
|
|
24
27
|
} satisfies Record<string, AnyRuleModule>;
|