@n8n/eslint-plugin-community-nodes 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/README.md +2 -0
- package/dist/plugin.d.ts +12 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +4 -0
- package/dist/plugin.js.map +1 -1
- package/dist/rules/index.d.ts +3 -1
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +4 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/no-runtime-dependencies.d.ts +2 -0
- package/dist/rules/no-runtime-dependencies.d.ts.map +1 -0
- package/dist/rules/no-runtime-dependencies.js +41 -0
- package/dist/rules/no-runtime-dependencies.js.map +1 -0
- package/dist/rules/valid-credential-references.d.ts +2 -0
- package/dist/rules/valid-credential-references.d.ts.map +1 -0
- package/dist/rules/valid-credential-references.js +77 -0
- package/dist/rules/valid-credential-references.js.map +1 -0
- package/dist/rules/webhook-lifecycle-complete.d.ts +1 -1
- package/dist/rules/webhook-lifecycle-complete.d.ts.map +1 -1
- package/dist/rules/webhook-lifecycle-complete.js +8 -0
- package/dist/rules/webhook-lifecycle-complete.js.map +1 -1
- package/docs/rules/no-runtime-dependencies.md +58 -0
- package/docs/rules/valid-credential-references.md +78 -0
- package/package.json +3 -3
- package/src/plugin.ts +4 -0
- package/src/rules/index.ts +4 -0
- package/src/rules/no-runtime-dependencies.test.ts +50 -0
- package/src/rules/no-runtime-dependencies.ts +50 -0
- package/src/rules/valid-credential-references.test.ts +230 -0
- package/src/rules/valid-credential-references.ts +105 -0
- package/src/rules/webhook-lifecycle-complete.test.ts +5 -0
- package/src/rules/webhook-lifecycle-complete.ts +10 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/plugin.ts
CHANGED
|
@@ -32,6 +32,7 @@ const configs = {
|
|
|
32
32
|
'@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error',
|
|
33
33
|
'@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
|
|
34
34
|
'@n8n/community-nodes/no-overrides-field': 'error',
|
|
35
|
+
'@n8n/community-nodes/no-runtime-dependencies': 'error',
|
|
35
36
|
'@n8n/community-nodes/icon-validation': 'error',
|
|
36
37
|
'@n8n/community-nodes/options-sorted-alphabetically': 'warn',
|
|
37
38
|
'@n8n/community-nodes/resource-operation-pattern': 'warn',
|
|
@@ -43,6 +44,7 @@ const configs = {
|
|
|
43
44
|
'@n8n/community-nodes/require-continue-on-fail': 'error',
|
|
44
45
|
'@n8n/community-nodes/require-node-api-error': 'error',
|
|
45
46
|
'@n8n/community-nodes/require-node-description-fields': 'error',
|
|
47
|
+
'@n8n/community-nodes/valid-credential-references': 'error',
|
|
46
48
|
'@n8n/community-nodes/valid-peer-dependencies': 'error',
|
|
47
49
|
'@n8n/community-nodes/webhook-lifecycle-complete': 'error',
|
|
48
50
|
},
|
|
@@ -63,6 +65,7 @@ const configs = {
|
|
|
63
65
|
'@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error',
|
|
64
66
|
'@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
|
|
65
67
|
'@n8n/community-nodes/no-overrides-field': 'error',
|
|
68
|
+
'@n8n/community-nodes/no-runtime-dependencies': 'error',
|
|
66
69
|
'@n8n/community-nodes/icon-validation': 'error',
|
|
67
70
|
'@n8n/community-nodes/options-sorted-alphabetically': 'warn',
|
|
68
71
|
'@n8n/community-nodes/credential-documentation-url': 'error',
|
|
@@ -74,6 +77,7 @@ const configs = {
|
|
|
74
77
|
'@n8n/community-nodes/require-continue-on-fail': 'error',
|
|
75
78
|
'@n8n/community-nodes/require-node-api-error': 'error',
|
|
76
79
|
'@n8n/community-nodes/require-node-description-fields': 'error',
|
|
80
|
+
'@n8n/community-nodes/valid-credential-references': 'error',
|
|
77
81
|
'@n8n/community-nodes/valid-peer-dependencies': 'error',
|
|
78
82
|
'@n8n/community-nodes/webhook-lifecycle-complete': 'error',
|
|
79
83
|
},
|
package/src/rules/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { NoHttpRequestWithManualAuthRule } from './no-http-request-with-manual-a
|
|
|
14
14
|
import { NoOverridesFieldRule } from './no-overrides-field.js';
|
|
15
15
|
import { NoRestrictedGlobalsRule } from './no-restricted-globals.js';
|
|
16
16
|
import { NoRestrictedImportsRule } from './no-restricted-imports.js';
|
|
17
|
+
import { NoRuntimeDependenciesRule } from './no-runtime-dependencies.js';
|
|
17
18
|
import { NodeClassDescriptionIconMissingRule } from './node-class-description-icon-missing.js';
|
|
18
19
|
import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js';
|
|
19
20
|
import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
|
|
@@ -24,6 +25,7 @@ import { RequireContinueOnFailRule } from './require-continue-on-fail.js';
|
|
|
24
25
|
import { RequireNodeApiErrorRule } from './require-node-api-error.js';
|
|
25
26
|
import { RequireNodeDescriptionFieldsRule } from './require-node-description-fields.js';
|
|
26
27
|
import { ResourceOperationPatternRule } from './resource-operation-pattern.js';
|
|
28
|
+
import { ValidCredentialReferencesRule } from './valid-credential-references.js';
|
|
27
29
|
import { ValidPeerDependenciesRule } from './valid-peer-dependencies.js';
|
|
28
30
|
import { WebhookLifecycleCompleteRule } from './webhook-lifecycle-complete.js';
|
|
29
31
|
|
|
@@ -41,6 +43,7 @@ export const rules = {
|
|
|
41
43
|
'no-forbidden-lifecycle-scripts': NoForbiddenLifecycleScriptsRule,
|
|
42
44
|
'no-http-request-with-manual-auth': NoHttpRequestWithManualAuthRule,
|
|
43
45
|
'no-overrides-field': NoOverridesFieldRule,
|
|
46
|
+
'no-runtime-dependencies': NoRuntimeDependenciesRule,
|
|
44
47
|
'icon-validation': IconValidationRule,
|
|
45
48
|
'resource-operation-pattern': ResourceOperationPatternRule,
|
|
46
49
|
'credential-documentation-url': CredentialDocumentationUrlRule,
|
|
@@ -52,6 +55,7 @@ export const rules = {
|
|
|
52
55
|
'require-continue-on-fail': RequireContinueOnFailRule,
|
|
53
56
|
'require-node-api-error': RequireNodeApiErrorRule,
|
|
54
57
|
'require-node-description-fields': RequireNodeDescriptionFieldsRule,
|
|
58
|
+
'valid-credential-references': ValidCredentialReferencesRule,
|
|
55
59
|
'valid-peer-dependencies': ValidPeerDependenciesRule,
|
|
56
60
|
'webhook-lifecycle-complete': WebhookLifecycleCompleteRule,
|
|
57
61
|
} satisfies Record<string, AnyRuleModule>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { NoRuntimeDependenciesRule } from './no-runtime-dependencies.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
ruleTester.run('no-runtime-dependencies', NoRuntimeDependenciesRule, {
|
|
8
|
+
valid: [
|
|
9
|
+
{
|
|
10
|
+
name: 'no dependencies field',
|
|
11
|
+
filename: 'package.json',
|
|
12
|
+
code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'empty dependencies object is allowed',
|
|
16
|
+
filename: 'package.json',
|
|
17
|
+
code: '{ "name": "n8n-nodes-example", "dependencies": {} }',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'non-package.json file is ignored',
|
|
21
|
+
filename: 'some-config.json',
|
|
22
|
+
code: '{ "dependencies": { "axios": "1.0.0" } }',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'nested "dependencies" key inside another field is allowed',
|
|
26
|
+
filename: 'package.json',
|
|
27
|
+
code: '{ "name": "n8n-nodes-example", "config": { "dependencies": { "axios": "1.0.0" } } }',
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
invalid: [
|
|
31
|
+
{
|
|
32
|
+
name: 'single runtime dependency is forbidden',
|
|
33
|
+
filename: 'package.json',
|
|
34
|
+
code: '{ "name": "n8n-nodes-example", "dependencies": { "axios": "1.0.0" } }',
|
|
35
|
+
errors: [{ messageId: 'runtimeDependenciesForbidden' }],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'multiple runtime dependencies are forbidden',
|
|
39
|
+
filename: 'package.json',
|
|
40
|
+
code: '{ "name": "n8n-nodes-example", "dependencies": { "axios": "1.0.0", "lodash": "^4.0.0" } }',
|
|
41
|
+
errors: [{ messageId: 'runtimeDependenciesForbidden' }],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'real-world package with bundled deps is forbidden',
|
|
45
|
+
filename: 'package.json',
|
|
46
|
+
code: '{ "name": "n8n-nodes-sinch", "dependencies": { "axios": "1.7.0", "fast-xml-parser": "4.4.0", "minimatch": "9.0.5" } }',
|
|
47
|
+
errors: [{ messageId: 'runtimeDependenciesForbidden' }],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
export const NoRuntimeDependenciesRule = createRule({
|
|
7
|
+
name: 'no-runtime-dependencies',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'problem',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Disallow non-empty "dependencies" in community node package.json',
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
runtimeDependenciesForbidden:
|
|
15
|
+
'The "dependencies" field must be empty or absent in community node packages. Runtime dependencies get bundled into the n8n instance and can conflict with other nodes or the n8n runtime itself. Move shared libraries to "peerDependencies" or bundle them into your build artifact.',
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
},
|
|
19
|
+
defaultOptions: [],
|
|
20
|
+
create(context) {
|
|
21
|
+
if (!context.filename.endsWith('package.json')) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
ObjectExpression(node: TSESTree.ObjectExpression) {
|
|
27
|
+
if (node.parent?.type !== AST_NODE_TYPES.ExpressionStatement) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const depsProp = findJsonProperty(node, 'dependencies');
|
|
32
|
+
if (!depsProp) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
depsProp.value.type !== AST_NODE_TYPES.ObjectExpression ||
|
|
38
|
+
depsProp.value.properties.length === 0
|
|
39
|
+
) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
context.report({
|
|
44
|
+
node: depsProp,
|
|
45
|
+
messageId: 'runtimeDependenciesForbidden',
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
import { afterEach, beforeEach, describe, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { ValidCredentialReferencesRule } from './valid-credential-references.js';
|
|
5
|
+
import * as fileUtils from '../utils/file-utils.js';
|
|
6
|
+
|
|
7
|
+
vi.mock('../utils/file-utils.js', async () => {
|
|
8
|
+
const actual = await vi.importActual('../utils/file-utils.js');
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
readPackageJsonCredentials: vi.fn(),
|
|
12
|
+
findPackageJson: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const mockReadPackageJsonCredentials = vi.mocked(fileUtils.readPackageJsonCredentials);
|
|
17
|
+
const mockFindPackageJson = vi.mocked(fileUtils.findPackageJson);
|
|
18
|
+
|
|
19
|
+
const ruleTester = new RuleTester();
|
|
20
|
+
|
|
21
|
+
const nodeFilePath = '/tmp/TestNode.node.ts';
|
|
22
|
+
|
|
23
|
+
function createNodeCode(
|
|
24
|
+
credentials: Array<string | { name: string; required?: boolean }> = [],
|
|
25
|
+
): string {
|
|
26
|
+
const credentialsArray =
|
|
27
|
+
credentials.length > 0
|
|
28
|
+
? credentials
|
|
29
|
+
.map((cred) => {
|
|
30
|
+
if (typeof cred === 'string') {
|
|
31
|
+
return `'${cred}'`;
|
|
32
|
+
} else {
|
|
33
|
+
const required =
|
|
34
|
+
cred.required !== undefined ? `,\n\t\t\t\trequired: ${cred.required}` : '';
|
|
35
|
+
return `{\n\t\t\t\tname: '${cred.name}'${required},\n\t\t\t}`;
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
.join(',\n\t\t\t')
|
|
39
|
+
: '';
|
|
40
|
+
|
|
41
|
+
const credentialsProperty =
|
|
42
|
+
credentials.length > 0 ? `credentials: [\n\t\t\t${credentialsArray}\n\t\t],` : '';
|
|
43
|
+
|
|
44
|
+
return `
|
|
45
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
46
|
+
|
|
47
|
+
export class TestNode implements INodeType {
|
|
48
|
+
description: INodeTypeDescription = {
|
|
49
|
+
displayName: 'Test Node',
|
|
50
|
+
name: 'testNode',
|
|
51
|
+
group: ['output'],
|
|
52
|
+
version: 1,
|
|
53
|
+
inputs: ['main'],
|
|
54
|
+
outputs: ['main'],
|
|
55
|
+
${credentialsProperty}
|
|
56
|
+
properties: [],
|
|
57
|
+
};
|
|
58
|
+
}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Same as createNodeCode but uses double quotes for the credential name — matches fixer output */
|
|
62
|
+
function createExpectedNodeCode(
|
|
63
|
+
credentials: Array<string | { name: string; required?: boolean }> = [],
|
|
64
|
+
): string {
|
|
65
|
+
const credentialsArray =
|
|
66
|
+
credentials.length > 0
|
|
67
|
+
? credentials
|
|
68
|
+
.map((cred) => {
|
|
69
|
+
if (typeof cred === 'string') {
|
|
70
|
+
return `"${cred}"`;
|
|
71
|
+
} else {
|
|
72
|
+
const required =
|
|
73
|
+
cred.required !== undefined ? `,\n\t\t\t\trequired: ${cred.required}` : '';
|
|
74
|
+
return `{\n\t\t\t\tname: "${cred.name}"${required},\n\t\t\t}`;
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
.join(',\n\t\t\t')
|
|
78
|
+
: '';
|
|
79
|
+
|
|
80
|
+
const credentialsProperty =
|
|
81
|
+
credentials.length > 0 ? `credentials: [\n\t\t\t${credentialsArray}\n\t\t],` : '';
|
|
82
|
+
|
|
83
|
+
return `
|
|
84
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
85
|
+
|
|
86
|
+
export class TestNode implements INodeType {
|
|
87
|
+
description: INodeTypeDescription = {
|
|
88
|
+
displayName: 'Test Node',
|
|
89
|
+
name: 'testNode',
|
|
90
|
+
group: ['output'],
|
|
91
|
+
version: 1,
|
|
92
|
+
inputs: ['main'],
|
|
93
|
+
outputs: ['main'],
|
|
94
|
+
${credentialsProperty}
|
|
95
|
+
properties: [],
|
|
96
|
+
};
|
|
97
|
+
}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createNonNodeClass(): string {
|
|
101
|
+
return `
|
|
102
|
+
export class RegularClass {
|
|
103
|
+
credentials = [
|
|
104
|
+
{ name: 'ExternalApi', required: true }
|
|
105
|
+
];
|
|
106
|
+
}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createNonINodeTypeClass(): string {
|
|
110
|
+
return `
|
|
111
|
+
export class NotANode {
|
|
112
|
+
description = {
|
|
113
|
+
displayName: 'Not A Node',
|
|
114
|
+
credentials: [
|
|
115
|
+
{ name: 'ExternalApi', required: true }
|
|
116
|
+
]
|
|
117
|
+
};
|
|
118
|
+
}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
mockFindPackageJson.mockReturnValue('/tmp/package.json');
|
|
122
|
+
mockReadPackageJsonCredentials.mockReturnValue(new Set(['myApiCredential', 'oauthApi']));
|
|
123
|
+
|
|
124
|
+
ruleTester.run('valid-credential-references', ValidCredentialReferencesRule, {
|
|
125
|
+
valid: [
|
|
126
|
+
{
|
|
127
|
+
name: 'node referencing a credential that exists (object form)',
|
|
128
|
+
filename: nodeFilePath,
|
|
129
|
+
code: createNodeCode([{ name: 'myApiCredential', required: true }]),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'node referencing a credential that exists (string form)',
|
|
133
|
+
filename: nodeFilePath,
|
|
134
|
+
code: createNodeCode(['myApiCredential']),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'node referencing multiple credentials that all exist',
|
|
138
|
+
filename: nodeFilePath,
|
|
139
|
+
code: createNodeCode(['myApiCredential', { name: 'oauthApi', required: false }]),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'node without credentials array',
|
|
143
|
+
filename: nodeFilePath,
|
|
144
|
+
code: createNodeCode(),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'non-node file ignored',
|
|
148
|
+
filename: '/tmp/regular-file.ts',
|
|
149
|
+
code: createNonNodeClass(),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'non-INodeType class ignored',
|
|
153
|
+
filename: nodeFilePath,
|
|
154
|
+
code: createNonINodeTypeClass(),
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
invalid: [
|
|
158
|
+
{
|
|
159
|
+
name: 'credential name does not exist in package (object form)',
|
|
160
|
+
filename: nodeFilePath,
|
|
161
|
+
code: createNodeCode([{ name: 'brokenReference', required: true }]),
|
|
162
|
+
errors: [
|
|
163
|
+
{
|
|
164
|
+
messageId: 'credentialNotFound',
|
|
165
|
+
data: { credentialName: 'brokenReference' },
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'credential name does not exist in package (string form)',
|
|
171
|
+
filename: nodeFilePath,
|
|
172
|
+
code: createNodeCode(['unknownCredential']),
|
|
173
|
+
errors: [
|
|
174
|
+
{
|
|
175
|
+
messageId: 'credentialNotFound',
|
|
176
|
+
data: { credentialName: 'unknownCredential' },
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'credential name is a typo close to an existing credential — suggestion provided',
|
|
182
|
+
filename: nodeFilePath,
|
|
183
|
+
code: createNodeCode([{ name: 'myApiCredentail', required: true }]),
|
|
184
|
+
errors: [
|
|
185
|
+
{
|
|
186
|
+
messageId: 'credentialNotFound',
|
|
187
|
+
data: { credentialName: 'myApiCredentail' },
|
|
188
|
+
suggestions: [
|
|
189
|
+
{
|
|
190
|
+
messageId: 'didYouMean',
|
|
191
|
+
data: { suggestedName: 'myApiCredential' },
|
|
192
|
+
output: createExpectedNodeCode([{ name: 'myApiCredential', required: true }]),
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'mix of valid and invalid credentials — only invalid reported',
|
|
200
|
+
filename: nodeFilePath,
|
|
201
|
+
code: createNodeCode(['myApiCredential', { name: 'brokenRef', required: true }]),
|
|
202
|
+
errors: [
|
|
203
|
+
{
|
|
204
|
+
messageId: 'credentialNotFound',
|
|
205
|
+
data: { credentialName: 'brokenRef' },
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('valid-credential-references — no package.json found', () => {
|
|
213
|
+
beforeEach(() => {
|
|
214
|
+
mockFindPackageJson.mockReturnValue(null);
|
|
215
|
+
});
|
|
216
|
+
afterEach(() => {
|
|
217
|
+
mockFindPackageJson.mockReturnValue('/tmp/package.json');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
ruleTester.run('valid-credential-references (no package.json)', ValidCredentialReferencesRule, {
|
|
221
|
+
valid: [
|
|
222
|
+
{
|
|
223
|
+
name: 'check is skipped when package.json cannot be found',
|
|
224
|
+
filename: nodeFilePath,
|
|
225
|
+
code: createNodeCode([{ name: 'anyCredential', required: true }]),
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
invalid: [],
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/types';
|
|
2
|
+
import type { ReportSuggestionArray } from '@typescript-eslint/utils/ts-eslint';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
isNodeTypeClass,
|
|
6
|
+
findClassProperty,
|
|
7
|
+
findArrayLiteralProperty,
|
|
8
|
+
extractCredentialNameFromArray,
|
|
9
|
+
findPackageJson,
|
|
10
|
+
readPackageJsonCredentials,
|
|
11
|
+
isFileType,
|
|
12
|
+
findSimilarStrings,
|
|
13
|
+
createRule,
|
|
14
|
+
} from '../utils/index.js';
|
|
15
|
+
|
|
16
|
+
export const ValidCredentialReferencesRule = createRule({
|
|
17
|
+
name: 'valid-credential-references',
|
|
18
|
+
meta: {
|
|
19
|
+
type: 'problem',
|
|
20
|
+
docs: {
|
|
21
|
+
description:
|
|
22
|
+
'Ensure credentials referenced in node descriptions exist as credential classes in the package',
|
|
23
|
+
},
|
|
24
|
+
messages: {
|
|
25
|
+
credentialNotFound:
|
|
26
|
+
'Credential "{{ credentialName }}" does not exist in this package. Check for typos or ensure the credential class is declared and listed in package.json.',
|
|
27
|
+
didYouMean: "Did you mean '{{ suggestedName }}'?",
|
|
28
|
+
},
|
|
29
|
+
schema: [],
|
|
30
|
+
hasSuggestions: true,
|
|
31
|
+
},
|
|
32
|
+
defaultOptions: [],
|
|
33
|
+
create(context) {
|
|
34
|
+
if (!isFileType(context.filename, '.node.ts')) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let packageCredentials: Set<string> | null = null;
|
|
39
|
+
|
|
40
|
+
const loadPackageCredentials = (): Set<string> => {
|
|
41
|
+
if (packageCredentials !== null) {
|
|
42
|
+
return packageCredentials;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const packageJsonPath = findPackageJson(context.filename);
|
|
46
|
+
if (!packageJsonPath) {
|
|
47
|
+
packageCredentials = new Set();
|
|
48
|
+
return packageCredentials;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
packageCredentials = readPackageJsonCredentials(packageJsonPath);
|
|
52
|
+
return packageCredentials;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
ClassDeclaration(node) {
|
|
57
|
+
if (!isNodeTypeClass(node)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const descriptionProperty = findClassProperty(node, 'description');
|
|
62
|
+
if (
|
|
63
|
+
!descriptionProperty?.value ||
|
|
64
|
+
descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression
|
|
65
|
+
) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const credentialsArray = findArrayLiteralProperty(descriptionProperty.value, 'credentials');
|
|
70
|
+
if (!credentialsArray) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const knownCredentials = loadPackageCredentials();
|
|
75
|
+
if (knownCredentials.size === 0) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
credentialsArray.elements.forEach((element) => {
|
|
80
|
+
const credentialInfo = extractCredentialNameFromArray(element);
|
|
81
|
+
if (!credentialInfo || knownCredentials.has(credentialInfo.name)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const similar = findSimilarStrings(credentialInfo.name, knownCredentials);
|
|
86
|
+
const suggestions: ReportSuggestionArray<'credentialNotFound' | 'didYouMean'> =
|
|
87
|
+
similar.map((suggestedName) => ({
|
|
88
|
+
messageId: 'didYouMean' as const,
|
|
89
|
+
data: { suggestedName },
|
|
90
|
+
fix(fixer) {
|
|
91
|
+
return fixer.replaceText(credentialInfo.node, `"${suggestedName}"`);
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
context.report({
|
|
96
|
+
node: credentialInfo.node,
|
|
97
|
+
messageId: 'credentialNotFound',
|
|
98
|
+
data: { credentialName: credentialInfo.name },
|
|
99
|
+
suggest: suggestions,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
});
|
|
@@ -107,6 +107,11 @@ export class RegularClass {
|
|
|
107
107
|
code: createTriggerNode({ webhookMethods: null }),
|
|
108
108
|
errors: [{ messageId: 'missingWebhookMethods' }],
|
|
109
109
|
},
|
|
110
|
+
{
|
|
111
|
+
name: 'trigger node with empty webhookMethods object (no lifecycle groups)',
|
|
112
|
+
code: createTriggerNode({ webhookMethods: '{}' }),
|
|
113
|
+
errors: [{ messageId: 'emptyWebhookMethods' }],
|
|
114
|
+
},
|
|
110
115
|
{
|
|
111
116
|
name: 'trigger node with empty webhookMethods group (all three missing)',
|
|
112
117
|
code: createTriggerNode({
|
|
@@ -56,6 +56,8 @@ export const WebhookLifecycleCompleteRule = createRule({
|
|
|
56
56
|
messages: {
|
|
57
57
|
missingWebhookMethods:
|
|
58
58
|
'Webhook trigger node is missing the `webhookMethods` property. Implement `checkExists`, `create`, and `delete` to register, verify, and clean up the webhook on the third-party service.',
|
|
59
|
+
emptyWebhookMethods:
|
|
60
|
+
'Webhook trigger node has an empty `webhookMethods` object. Define at least one lifecycle group with `checkExists`, `create`, and `delete` methods.',
|
|
59
61
|
missingLifecycleMethod:
|
|
60
62
|
'Webhook trigger lifecycle is incomplete. `webhookMethods.{{group}}` is missing: {{missing}}. All of `checkExists`, `create`, and `delete` must be implemented.',
|
|
61
63
|
},
|
|
@@ -91,6 +93,14 @@ export const WebhookLifecycleCompleteRule = createRule({
|
|
|
91
93
|
return;
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
if (webhookMethodsProperty.value.properties.length === 0) {
|
|
97
|
+
context.report({
|
|
98
|
+
node: webhookMethodsProperty.key,
|
|
99
|
+
messageId: 'emptyWebhookMethods',
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
for (const groupProperty of webhookMethodsProperty.value.properties) {
|
|
95
105
|
if (groupProperty.type !== AST_NODE_TYPES.Property) continue;
|
|
96
106
|
if (groupProperty.value.type !== AST_NODE_TYPES.ObjectExpression) continue;
|