@n8n/eslint-plugin-community-nodes 0.33.0 → 0.35.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/README.md +1 -0
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +2 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +2 -0
- package/dist/rules/no-silent-error-swallowing.d.ts.map +1 -1
- package/dist/rules/no-silent-error-swallowing.js +2 -12
- package/dist/rules/valid-node-categories.d.ts +4 -0
- package/dist/rules/valid-node-categories.d.ts.map +1 -0
- package/dist/rules/valid-node-categories.js +68 -0
- package/dist/rules/webhook-lifecycle-complete.d.ts.map +1 -1
- package/dist/rules/webhook-lifecycle-complete.js +3 -11
- package/dist/utils/ast-utils.d.ts +6 -0
- package/dist/utils/ast-utils.d.ts.map +1 -1
- package/dist/utils/ast-utils.js +22 -5
- package/docs/rules/valid-node-categories.md +27 -0
- package/package.json +4 -4
- package/src/plugin.ts +2 -0
- package/src/rules/index.ts +2 -0
- package/src/rules/no-silent-error-swallowing.test.ts +26 -0
- package/src/rules/no-silent-error-swallowing.ts +2 -9
- package/src/rules/valid-node-categories.test.ts +79 -0
- package/src/rules/valid-node-categories.ts +72 -0
- package/src/rules/webhook-lifecycle-complete.test.ts +42 -0
- package/src/rules/webhook-lifecycle-complete.ts +3 -12
- package/src/utils/ast-utils.ts +29 -6
package/README.md
CHANGED
|
@@ -88,6 +88,7 @@ export default [
|
|
|
88
88
|
| [valid-author](docs/rules/valid-author.md) | Require a non-empty author name and email in package.json | ✅ ☑️ | | | | |
|
|
89
89
|
| [valid-credential-references](docs/rules/valid-credential-references.md) | Ensure credentials referenced in node descriptions exist as credential classes in the package | ✅ ☑️ | | | 💡 | |
|
|
90
90
|
| [valid-description](docs/rules/valid-description.md) | Require a non-empty "description" field in community node package.json | ✅ ☑️ | | | | |
|
|
91
|
+
| [valid-node-categories](docs/rules/valid-node-categories.md) | Require supported community node categories in .node.json codex files | ✅ ☑️ | | | | |
|
|
91
92
|
| [valid-peer-dependencies](docs/rules/valid-peer-dependencies.md) | Require community node package.json peerDependencies to contain only "n8n-workflow": "*" (and optionally "@n8n/ai-node-sdk") | ✅ ☑️ | | 🔧 | | |
|
|
92
93
|
| [webhook-lifecycle-complete](docs/rules/webhook-lifecycle-complete.md) | Require webhook trigger nodes to implement the complete webhookMethods lifecycle (checkExists, create, delete) | ✅ ☑️ | | | | |
|
|
93
94
|
|
package/dist/plugin.d.ts
CHANGED
|
@@ -55,6 +55,7 @@ declare const configs: {
|
|
|
55
55
|
'@n8n/community-nodes/valid-author': "error";
|
|
56
56
|
'@n8n/community-nodes/valid-credential-references': "error";
|
|
57
57
|
'@n8n/community-nodes/valid-description': "error";
|
|
58
|
+
'@n8n/community-nodes/valid-node-categories': "error";
|
|
58
59
|
'@n8n/community-nodes/valid-peer-dependencies': "error";
|
|
59
60
|
'@n8n/community-nodes/webhook-lifecycle-complete': "error";
|
|
60
61
|
};
|
|
@@ -111,6 +112,7 @@ declare const configs: {
|
|
|
111
112
|
'@n8n/community-nodes/valid-author': "error";
|
|
112
113
|
'@n8n/community-nodes/valid-credential-references': "error";
|
|
113
114
|
'@n8n/community-nodes/valid-description': "error";
|
|
115
|
+
'@n8n/community-nodes/valid-node-categories': "error";
|
|
114
116
|
'@n8n/community-nodes/valid-peer-dependencies': "error";
|
|
115
117
|
'@n8n/community-nodes/webhook-lifecycle-complete': "error";
|
|
116
118
|
};
|
|
@@ -178,6 +180,7 @@ declare const pluginWithConfigs: {
|
|
|
178
180
|
'@n8n/community-nodes/valid-author': "error";
|
|
179
181
|
'@n8n/community-nodes/valid-credential-references': "error";
|
|
180
182
|
'@n8n/community-nodes/valid-description': "error";
|
|
183
|
+
'@n8n/community-nodes/valid-node-categories': "error";
|
|
181
184
|
'@n8n/community-nodes/valid-peer-dependencies': "error";
|
|
182
185
|
'@n8n/community-nodes/webhook-lifecycle-complete': "error";
|
|
183
186
|
};
|
|
@@ -234,6 +237,7 @@ declare const pluginWithConfigs: {
|
|
|
234
237
|
'@n8n/community-nodes/valid-author': "error";
|
|
235
238
|
'@n8n/community-nodes/valid-credential-references': "error";
|
|
236
239
|
'@n8n/community-nodes/valid-description': "error";
|
|
240
|
+
'@n8n/community-nodes/valid-node-categories': "error";
|
|
237
241
|
'@n8n/community-nodes/valid-peer-dependencies': "error";
|
|
238
242
|
'@n8n/community-nodes/webhook-lifecycle-complete': "error";
|
|
239
243
|
};
|
|
@@ -302,6 +306,7 @@ declare const n8nCommunityNodesPlugin: {
|
|
|
302
306
|
'@n8n/community-nodes/valid-author': "error";
|
|
303
307
|
'@n8n/community-nodes/valid-credential-references': "error";
|
|
304
308
|
'@n8n/community-nodes/valid-description': "error";
|
|
309
|
+
'@n8n/community-nodes/valid-node-categories': "error";
|
|
305
310
|
'@n8n/community-nodes/valid-peer-dependencies': "error";
|
|
306
311
|
'@n8n/community-nodes/webhook-lifecycle-complete': "error";
|
|
307
312
|
};
|
|
@@ -358,6 +363,7 @@ declare const n8nCommunityNodesPlugin: {
|
|
|
358
363
|
'@n8n/community-nodes/valid-author': "error";
|
|
359
364
|
'@n8n/community-nodes/valid-credential-references': "error";
|
|
360
365
|
'@n8n/community-nodes/valid-description': "error";
|
|
366
|
+
'@n8n/community-nodes/valid-node-categories': "error";
|
|
361
367
|
'@n8n/community-nodes/valid-peer-dependencies': "error";
|
|
362
368
|
'@n8n/community-nodes/webhook-lifecycle-complete': "error";
|
|
363
369
|
};
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAG7C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAYzC,QAAA,MAAM,OAAO;;;;YAIV,sBAAsB;;oBAZvB,IAAI;oBACJ,OAAO;oBACP,SAAS;;uBAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAG7C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAYzC,QAAA,MAAM,OAAO;;;;YAIV,sBAAsB;;oBAZvB,IAAI;oBACJ,OAAO;oBACP,SAAS;;uBAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YA2DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+CgB,CAAC;AAE1C,QAAA,MAAM,iBAAiB;;QAjHrB,IAAI;QACJ,OAAO;QACP,SAAS;;WAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;gBAOpC,sBAAsB;;wBAZvB,IAAI;wBACJ,OAAO;wBACP,SAAS;;2BAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA2DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiD+C,CAAC;AAEzE,QAAA,MAAM,uBAAuB;;QAnH3B,IAAI;QACJ,OAAO;QACP,SAAS;;WAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;gBAOpC,sBAAsB;;wBAZvB,IAAI;wBACJ,OAAO;wBACP,SAAS;;2BAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA2DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDwB,CAAC;eACnC,iBAAiB;AAChC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC"}
|
package/dist/plugin.js
CHANGED
|
@@ -57,6 +57,7 @@ const configs = {
|
|
|
57
57
|
'@n8n/community-nodes/valid-author': 'error',
|
|
58
58
|
'@n8n/community-nodes/valid-credential-references': 'error',
|
|
59
59
|
'@n8n/community-nodes/valid-description': 'error',
|
|
60
|
+
'@n8n/community-nodes/valid-node-categories': 'error',
|
|
60
61
|
'@n8n/community-nodes/valid-peer-dependencies': 'error',
|
|
61
62
|
'@n8n/community-nodes/webhook-lifecycle-complete': 'error',
|
|
62
63
|
},
|
|
@@ -106,6 +107,7 @@ const configs = {
|
|
|
106
107
|
'@n8n/community-nodes/valid-author': 'error',
|
|
107
108
|
'@n8n/community-nodes/valid-credential-references': 'error',
|
|
108
109
|
'@n8n/community-nodes/valid-description': 'error',
|
|
110
|
+
'@n8n/community-nodes/valid-node-categories': 'error',
|
|
109
111
|
'@n8n/community-nodes/valid-peer-dependencies': 'error',
|
|
110
112
|
'@n8n/community-nodes/webhook-lifecycle-complete': 'error',
|
|
111
113
|
},
|
package/dist/rules/index.d.ts
CHANGED
|
@@ -130,6 +130,9 @@ export declare const rules: {
|
|
|
130
130
|
'valid-description': import("@typescript-eslint/utils/ts-eslint").RuleModule<"emptyDescription" | "missingDescription", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
131
131
|
name: string;
|
|
132
132
|
};
|
|
133
|
+
'valid-node-categories': import("@typescript-eslint/utils/ts-eslint").RuleModule<"invalidCategories" | "invalidCategory" | "invalidCategoryType" | "marketingCategory", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
134
|
+
name: string;
|
|
135
|
+
};
|
|
133
136
|
'valid-peer-dependencies': import("@typescript-eslint/utils/ts-eslint").RuleModule<"forbiddenPeerDependency" | "invalidPeerDependenciesType" | "missingN8nWorkflow" | "missingPeerDependencies" | "pinnedN8nWorkflow", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
134
137
|
name: string;
|
|
135
138
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAgDA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CuB,CAAC"}
|
package/dist/rules/index.js
CHANGED
|
@@ -40,6 +40,7 @@ import { TriggerNodeConventionsRule } from './trigger-node-conventions.js';
|
|
|
40
40
|
import { ValidAuthorRule } from './valid-author.js';
|
|
41
41
|
import { ValidCredentialReferencesRule } from './valid-credential-references.js';
|
|
42
42
|
import { ValidDescriptionRule } from './valid-description.js';
|
|
43
|
+
import { ValidNodeCategoriesRule } from './valid-node-categories.js';
|
|
43
44
|
import { ValidPeerDependenciesRule } from './valid-peer-dependencies.js';
|
|
44
45
|
import { WebhookLifecycleCompleteRule } from './webhook-lifecycle-complete.js';
|
|
45
46
|
export const rules = {
|
|
@@ -85,6 +86,7 @@ export const rules = {
|
|
|
85
86
|
'valid-author': ValidAuthorRule,
|
|
86
87
|
'valid-credential-references': ValidCredentialReferencesRule,
|
|
87
88
|
'valid-description': ValidDescriptionRule,
|
|
89
|
+
'valid-node-categories': ValidNodeCategoriesRule,
|
|
88
90
|
'valid-peer-dependencies': ValidPeerDependenciesRule,
|
|
89
91
|
'webhook-lifecycle-complete': WebhookLifecycleCompleteRule,
|
|
90
92
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-silent-error-swallowing.d.ts","sourceRoot":"","sources":["../../src/rules/no-silent-error-swallowing.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"no-silent-error-swallowing.d.ts","sourceRoot":"","sources":["../../src/rules/no-silent-error-swallowing.ts"],"names":[],"mappings":"AAsCA,eAAO,MAAM,2BAA2B;;CAiFtC,CAAC"}
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
-
import { createRule, findClassProperty, isNodeTypeClass, WEBHOOK_LIFECYCLE_METHODS, } from '../utils/index.js';
|
|
2
|
+
import { createRule, findClassProperty, getPropertyKeyName, isNodeTypeClass, WEBHOOK_LIFECYCLE_METHODS, } from '../utils/index.js';
|
|
3
3
|
const LIFECYCLE_METHODS = WEBHOOK_LIFECYCLE_METHODS;
|
|
4
|
-
/** Returns the static name of a non-computed object property key, or null. */
|
|
5
|
-
function getPropertyName(property) {
|
|
6
|
-
if (property.computed)
|
|
7
|
-
return null;
|
|
8
|
-
if (property.key.type === AST_NODE_TYPES.Identifier)
|
|
9
|
-
return property.key.name;
|
|
10
|
-
if (property.key.type === AST_NODE_TYPES.Literal)
|
|
11
|
-
return String(property.key.value);
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
4
|
function isFunctionNode(node) {
|
|
15
5
|
return (node.type === AST_NODE_TYPES.FunctionDeclaration ||
|
|
16
6
|
node.type === AST_NODE_TYPES.FunctionExpression ||
|
|
@@ -70,7 +60,7 @@ export const NoSilentErrorSwallowingRule = createRule({
|
|
|
70
60
|
for (const methodProperty of groupProperty.value.properties) {
|
|
71
61
|
if (methodProperty.type !== AST_NODE_TYPES.Property)
|
|
72
62
|
continue;
|
|
73
|
-
const methodName =
|
|
63
|
+
const methodName = getPropertyKeyName(methodProperty);
|
|
74
64
|
if (methodName === null || !LIFECYCLE_METHODS.includes(methodName))
|
|
75
65
|
continue;
|
|
76
66
|
const fn = methodProperty.value;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const ValidNodeCategoriesRule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"invalidCategories" | "invalidCategory" | "invalidCategoryType" | "marketingCategory", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
2
|
+
name: string;
|
|
3
|
+
};
|
|
4
|
+
//# sourceMappingURL=valid-node-categories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"valid-node-categories.d.ts","sourceRoot":"","sources":["../../src/rules/valid-node-categories.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,uBAAuB;;CAqDlC,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
import { createRule, findJsonProperty, getTopLevelObjectInJson } from '../utils/index.js';
|
|
3
|
+
const validCategories = new Set([
|
|
4
|
+
'Data & Storage',
|
|
5
|
+
'Finance & Accounting',
|
|
6
|
+
'Marketing & Content',
|
|
7
|
+
'Productivity',
|
|
8
|
+
'Miscellaneous',
|
|
9
|
+
'Sales',
|
|
10
|
+
'Development',
|
|
11
|
+
'Analytics',
|
|
12
|
+
'Communication',
|
|
13
|
+
'Utility',
|
|
14
|
+
]);
|
|
15
|
+
export const ValidNodeCategoriesRule = createRule({
|
|
16
|
+
name: 'valid-node-categories',
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'problem',
|
|
19
|
+
docs: {
|
|
20
|
+
description: 'Require supported community node categories in .node.json codex files',
|
|
21
|
+
},
|
|
22
|
+
messages: {
|
|
23
|
+
invalidCategories: '"categories" must be a non-empty array of community node categories.',
|
|
24
|
+
invalidCategoryType: 'Each category must be a string from the community node category list.',
|
|
25
|
+
invalidCategory: '"{{ category }}" is not a supported community node category. See https://docs.n8n.io/connect/create-nodes/build-your-node/reference/codex-files/#node-categories for the allowed values.',
|
|
26
|
+
marketingCategory: '"Marketing" is not a supported category. Use "Marketing & Content".',
|
|
27
|
+
},
|
|
28
|
+
schema: [],
|
|
29
|
+
},
|
|
30
|
+
defaultOptions: [],
|
|
31
|
+
create(context) {
|
|
32
|
+
if (!context.filename.endsWith('.node.json'))
|
|
33
|
+
return {};
|
|
34
|
+
return {
|
|
35
|
+
ObjectExpression(node) {
|
|
36
|
+
const root = getTopLevelObjectInJson(node);
|
|
37
|
+
if (!root)
|
|
38
|
+
return;
|
|
39
|
+
const categories = findJsonProperty(root, 'categories');
|
|
40
|
+
if (!categories)
|
|
41
|
+
return;
|
|
42
|
+
if (categories.value.type !== AST_NODE_TYPES.ArrayExpression ||
|
|
43
|
+
categories.value.elements.length === 0) {
|
|
44
|
+
context.report({ node: categories.value, messageId: 'invalidCategories' });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
for (const category of categories.value.elements) {
|
|
48
|
+
if (!category)
|
|
49
|
+
continue;
|
|
50
|
+
if (category.type !== AST_NODE_TYPES.Literal || typeof category.value !== 'string') {
|
|
51
|
+
context.report({ node: category, messageId: 'invalidCategoryType' });
|
|
52
|
+
}
|
|
53
|
+
else if (category.value === 'Marketing') {
|
|
54
|
+
context.report({ node: category, messageId: 'marketingCategory' });
|
|
55
|
+
}
|
|
56
|
+
else if (!validCategories.has(category.value)) {
|
|
57
|
+
context.report({
|
|
58
|
+
node: category,
|
|
59
|
+
messageId: 'invalidCategory',
|
|
60
|
+
data: { category: category.value },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
//# sourceMappingURL=valid-node-categories.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook-lifecycle-complete.d.ts","sourceRoot":"","sources":["../../src/rules/webhook-lifecycle-complete.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"webhook-lifecycle-complete.d.ts","sourceRoot":"","sources":["../../src/rules/webhook-lifecycle-complete.ts"],"names":[],"mappings":"AA0CA,eAAO,MAAM,4BAA4B;;CA6EvC,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
-
import { createRule, findClassProperty, findObjectProperty, isNodeTypeClass, WEBHOOK_LIFECYCLE_METHODS, } from '../utils/index.js';
|
|
2
|
+
import { createRule, findClassProperty, findObjectProperty, getPropertyKeyName, isNodeTypeClass, WEBHOOK_LIFECYCLE_METHODS, } from '../utils/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Returns true if the description declares webhook endpoints, indicating the
|
|
5
5
|
* node is a webhook-based trigger that needs a complete lifecycle.
|
|
@@ -17,11 +17,7 @@ function hasWebhooksDeclared(descriptionValue) {
|
|
|
17
17
|
function isMethodProperty(property, name) {
|
|
18
18
|
if (property.type !== AST_NODE_TYPES.Property)
|
|
19
19
|
return false;
|
|
20
|
-
if (property
|
|
21
|
-
return false;
|
|
22
|
-
const keyMatches = (property.key.type === AST_NODE_TYPES.Identifier && property.key.name === name) ||
|
|
23
|
-
(property.key.type === AST_NODE_TYPES.Literal && property.key.value === name);
|
|
24
|
-
if (!keyMatches)
|
|
20
|
+
if (getPropertyKeyName(property) !== name)
|
|
25
21
|
return false;
|
|
26
22
|
return (property.value.type === AST_NODE_TYPES.FunctionExpression ||
|
|
27
23
|
property.value.type === AST_NODE_TYPES.ArrowFunctionExpression);
|
|
@@ -81,11 +77,7 @@ export const WebhookLifecycleCompleteRule = createRule({
|
|
|
81
77
|
continue;
|
|
82
78
|
if (groupProperty.value.type !== AST_NODE_TYPES.ObjectExpression)
|
|
83
79
|
continue;
|
|
84
|
-
const groupName = groupProperty
|
|
85
|
-
? groupProperty.key.name
|
|
86
|
-
: groupProperty.key.type === AST_NODE_TYPES.Literal
|
|
87
|
-
? String(groupProperty.key.value)
|
|
88
|
-
: 'default';
|
|
80
|
+
const groupName = getPropertyKeyName(groupProperty) ?? 'default';
|
|
89
81
|
const missing = findMissingMethods(groupProperty.value);
|
|
90
82
|
if (missing.length === 0)
|
|
91
83
|
continue;
|
|
@@ -9,6 +9,12 @@ export declare function isTriggerNodeClass(node: TSESTree.ClassDeclaration): boo
|
|
|
9
9
|
* `group` values.
|
|
10
10
|
*/
|
|
11
11
|
export declare function isTriggerNode(node: TSESTree.ClassDeclaration, descriptionValue: TSESTree.ObjectExpression): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Returns the name a property key stands for, or null when it is only known at
|
|
14
|
+
* runtime. `delete`, `'delete'` and `['delete']` all name the same property, so
|
|
15
|
+
* how the key is written must not decide whether a rule sees it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getPropertyKeyName(property: TSESTree.Property | TSESTree.PropertyDefinition | TSESTree.MethodDefinition): string | null;
|
|
12
18
|
export declare function findClassProperty(node: TSESTree.ClassDeclaration, propertyName: string): TSESTree.PropertyDefinition | null;
|
|
13
19
|
/**
|
|
14
20
|
* Returns the `description` object literal of a node class. Versioned nodes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast-utils.d.ts","sourceRoot":"","sources":["../../src/utils/ast-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAezD,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,GAAG,OAAO,CAUxE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,GAAG,OAAO,CAE9E;AAED,sGAAsG;AACtG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,GAAG,OAAO,CAE3E;AAWD;;;;GAIG;AACH,wBAAgB,aAAa,CAC5B,IAAI,EAAE,QAAQ,CAAC,gBAAgB,EAC/B,gBAAgB,EAAE,QAAQ,CAAC,gBAAgB,GACzC,OAAO,CAET;AAED,wBAAgB,iBAAiB,CAChC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,EAC/B,YAAY,EAAE,MAAM,GAClB,QAAQ,CAAC,kBAAkB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"ast-utils.d.ts","sourceRoot":"","sources":["../../src/utils/ast-utils.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/ast-utils.js
CHANGED
|
@@ -33,10 +33,29 @@ function hasTriggerGroup(descriptionValue) {
|
|
|
33
33
|
export function isTriggerNode(node, descriptionValue) {
|
|
34
34
|
return hasTriggerGroup(descriptionValue) || isTriggerNodeClass(node);
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns the name a property key stands for, or null when it is only known at
|
|
38
|
+
* runtime. `delete`, `'delete'` and `['delete']` all name the same property, so
|
|
39
|
+
* how the key is written must not decide whether a rule sees it.
|
|
40
|
+
*/
|
|
41
|
+
export function getPropertyKeyName(property) {
|
|
42
|
+
const { key } = property;
|
|
43
|
+
if (key.type === AST_NODE_TYPES.Literal) {
|
|
44
|
+
return key.value === null ? null : String(key.value);
|
|
45
|
+
}
|
|
46
|
+
// A computed key still names one property when it is a template without
|
|
47
|
+
// substitutions; written plainly, an identifier is the name itself.
|
|
48
|
+
if (key.type === AST_NODE_TYPES.TemplateLiteral) {
|
|
49
|
+
return key.expressions.length === 0 ? (key.quasis[0]?.value.cooked ?? null) : null;
|
|
50
|
+
}
|
|
51
|
+
if (key.type === AST_NODE_TYPES.Identifier) {
|
|
52
|
+
return property.computed ? null : key.name;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
36
56
|
export function findClassProperty(node, propertyName) {
|
|
37
57
|
const property = node.body.body.find((member) => member.type === AST_NODE_TYPES.PropertyDefinition &&
|
|
38
|
-
member
|
|
39
|
-
member.key.name === propertyName);
|
|
58
|
+
getPropertyKeyName(member) === propertyName);
|
|
40
59
|
return property?.type === AST_NODE_TYPES.PropertyDefinition ? property : null;
|
|
41
60
|
}
|
|
42
61
|
/**
|
|
@@ -90,9 +109,7 @@ export function findNodeDescriptionObject(node) {
|
|
|
90
109
|
return findConstructorAssignedDescription(node);
|
|
91
110
|
}
|
|
92
111
|
export function findObjectProperty(obj, propertyName) {
|
|
93
|
-
const property = obj.properties.find((prop) => prop.type === AST_NODE_TYPES.Property &&
|
|
94
|
-
prop.key.type === AST_NODE_TYPES.Identifier &&
|
|
95
|
-
prop.key.name === propertyName);
|
|
112
|
+
const property = obj.properties.find((prop) => prop.type === AST_NODE_TYPES.Property && getPropertyKeyName(prop) === propertyName);
|
|
96
113
|
return property?.type === AST_NODE_TYPES.Property ? property : null;
|
|
97
114
|
}
|
|
98
115
|
export function getLiteralValue(node) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Require supported community node categories in .node.json codex files (`@n8n/community-nodes/valid-node-categories`)
|
|
2
|
+
|
|
3
|
+
💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
|
|
4
|
+
|
|
5
|
+
<!-- end auto-generated rule header -->
|
|
6
|
+
|
|
7
|
+
## Rule Details
|
|
8
|
+
|
|
9
|
+
This rule checks `categories` in community node `.node.json` codex files. Each category must match a value in the [node category list](https://docs.n8n.io/connect/create-nodes/build-your-node/reference/codex-files/#node-categories). The rule also rejects empty arrays and values that are not strings. The `categories` field is optional.
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
### ❌ Incorrect
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"categories": ["Marketing", "Bananas"]
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### ✅ Correct
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"categories": ["Marketing & Content", "Development"]
|
|
26
|
+
}
|
|
27
|
+
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@n8n/eslint-plugin-community-nodes",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.35.0",
|
|
5
5
|
"main": "./dist/plugin.js",
|
|
6
6
|
"types": "./dist/plugin.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@n8n/typescript-config": "1.12.0",
|
|
20
|
-
"@n8n/vitest-config": "1.
|
|
20
|
+
"@n8n/vitest-config": "1.23.0",
|
|
21
21
|
"@types/node": "24.10.1",
|
|
22
|
-
"n8n-workflow": "2.
|
|
22
|
+
"n8n-workflow": "2.42.0",
|
|
23
23
|
"@typescript-eslint/rule-tester": "8.70.0",
|
|
24
24
|
"eslint-doc-generator": "^2.2.2",
|
|
25
25
|
"eslint-plugin-eslint-plugin": "^7.0.0",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
28
28
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
29
29
|
"vite": "^8.0.2",
|
|
30
|
-
"vitest": "
|
|
30
|
+
"vitest": "5.0.1"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"eslint": "9.29.0",
|
package/src/plugin.ts
CHANGED
|
@@ -61,6 +61,7 @@ const configs = {
|
|
|
61
61
|
'@n8n/community-nodes/valid-author': 'error',
|
|
62
62
|
'@n8n/community-nodes/valid-credential-references': 'error',
|
|
63
63
|
'@n8n/community-nodes/valid-description': 'error',
|
|
64
|
+
'@n8n/community-nodes/valid-node-categories': 'error',
|
|
64
65
|
'@n8n/community-nodes/valid-peer-dependencies': 'error',
|
|
65
66
|
'@n8n/community-nodes/webhook-lifecycle-complete': 'error',
|
|
66
67
|
},
|
|
@@ -110,6 +111,7 @@ const configs = {
|
|
|
110
111
|
'@n8n/community-nodes/valid-author': 'error',
|
|
111
112
|
'@n8n/community-nodes/valid-credential-references': 'error',
|
|
112
113
|
'@n8n/community-nodes/valid-description': 'error',
|
|
114
|
+
'@n8n/community-nodes/valid-node-categories': 'error',
|
|
113
115
|
'@n8n/community-nodes/valid-peer-dependencies': 'error',
|
|
114
116
|
'@n8n/community-nodes/webhook-lifecycle-complete': 'error',
|
|
115
117
|
},
|
package/src/rules/index.ts
CHANGED
|
@@ -42,6 +42,7 @@ import { TriggerNodeConventionsRule } from './trigger-node-conventions.js';
|
|
|
42
42
|
import { ValidAuthorRule } from './valid-author.js';
|
|
43
43
|
import { ValidCredentialReferencesRule } from './valid-credential-references.js';
|
|
44
44
|
import { ValidDescriptionRule } from './valid-description.js';
|
|
45
|
+
import { ValidNodeCategoriesRule } from './valid-node-categories.js';
|
|
45
46
|
import { ValidPeerDependenciesRule } from './valid-peer-dependencies.js';
|
|
46
47
|
import { WebhookLifecycleCompleteRule } from './webhook-lifecycle-complete.js';
|
|
47
48
|
|
|
@@ -88,6 +89,7 @@ export const rules = {
|
|
|
88
89
|
'valid-author': ValidAuthorRule,
|
|
89
90
|
'valid-credential-references': ValidCredentialReferencesRule,
|
|
90
91
|
'valid-description': ValidDescriptionRule,
|
|
92
|
+
'valid-node-categories': ValidNodeCategoriesRule,
|
|
91
93
|
'valid-peer-dependencies': ValidPeerDependenciesRule,
|
|
92
94
|
'webhook-lifecycle-complete': WebhookLifecycleCompleteRule,
|
|
93
95
|
} satisfies Record<string, AnyRuleModule>;
|
|
@@ -174,6 +174,32 @@ export class RegularClass {
|
|
|
174
174
|
}`),
|
|
175
175
|
errors: [{ messageId: 'silentReturn', data: { method: 'delete' } }],
|
|
176
176
|
},
|
|
177
|
+
{
|
|
178
|
+
name: 'lifecycle method written with a computed literal key',
|
|
179
|
+
code: createTriggerNode(`{
|
|
180
|
+
default: {
|
|
181
|
+
['delete']: async function (this: IHookFunctions): Promise<boolean> {
|
|
182
|
+
try {
|
|
183
|
+
return await this.helpers.httpRequest({ url: 'https://example.com' });
|
|
184
|
+
} catch (error) {}
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
}`),
|
|
188
|
+
errors: [{ messageId: 'emptyCatch', data: { method: 'delete' } }],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'lifecycle group written with a quoted key',
|
|
192
|
+
code: createTriggerNode(`{
|
|
193
|
+
'default': {
|
|
194
|
+
async delete(this: IHookFunctions): Promise<boolean> {
|
|
195
|
+
try {
|
|
196
|
+
return await this.helpers.httpRequest({ url: 'https://example.com' });
|
|
197
|
+
} catch (error) {}
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
}`),
|
|
201
|
+
errors: [{ messageId: 'emptyCatch', data: { method: 'delete' } }],
|
|
202
|
+
},
|
|
177
203
|
{
|
|
178
204
|
name: 'catch block with a bare return',
|
|
179
205
|
code: createTriggerNode(`{
|
|
@@ -3,20 +3,13 @@ import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils';
|
|
|
3
3
|
import {
|
|
4
4
|
createRule,
|
|
5
5
|
findClassProperty,
|
|
6
|
+
getPropertyKeyName,
|
|
6
7
|
isNodeTypeClass,
|
|
7
8
|
WEBHOOK_LIFECYCLE_METHODS,
|
|
8
9
|
} from '../utils/index.js';
|
|
9
10
|
|
|
10
11
|
const LIFECYCLE_METHODS: readonly string[] = WEBHOOK_LIFECYCLE_METHODS;
|
|
11
12
|
|
|
12
|
-
/** Returns the static name of a non-computed object property key, or null. */
|
|
13
|
-
function getPropertyName(property: TSESTree.Property): string | null {
|
|
14
|
-
if (property.computed) return null;
|
|
15
|
-
if (property.key.type === AST_NODE_TYPES.Identifier) return property.key.name;
|
|
16
|
-
if (property.key.type === AST_NODE_TYPES.Literal) return String(property.key.value);
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
13
|
function isFunctionNode(node: TSESTree.Node): boolean {
|
|
21
14
|
return (
|
|
22
15
|
node.type === AST_NODE_TYPES.FunctionDeclaration ||
|
|
@@ -81,7 +74,7 @@ export const NoSilentErrorSwallowingRule = createRule({
|
|
|
81
74
|
for (const methodProperty of groupProperty.value.properties) {
|
|
82
75
|
if (methodProperty.type !== AST_NODE_TYPES.Property) continue;
|
|
83
76
|
|
|
84
|
-
const methodName =
|
|
77
|
+
const methodName = getPropertyKeyName(methodProperty);
|
|
85
78
|
if (methodName === null || !LIFECYCLE_METHODS.includes(methodName)) continue;
|
|
86
79
|
|
|
87
80
|
const fn = methodProperty.value;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { ValidNodeCategoriesRule } from './valid-node-categories.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
const filename = 'nodes/Example/Example.node.json';
|
|
7
|
+
|
|
8
|
+
function codex(categories: unknown): string {
|
|
9
|
+
return JSON.stringify({ node: 'n8n-nodes-example', categories });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
ruleTester.run('valid-node-categories', ValidNodeCategoriesRule, {
|
|
13
|
+
valid: [
|
|
14
|
+
{
|
|
15
|
+
name: 'all supported community categories',
|
|
16
|
+
filename,
|
|
17
|
+
code: codex([
|
|
18
|
+
'Data & Storage',
|
|
19
|
+
'Finance & Accounting',
|
|
20
|
+
'Marketing & Content',
|
|
21
|
+
'Productivity',
|
|
22
|
+
'Miscellaneous',
|
|
23
|
+
'Sales',
|
|
24
|
+
'Development',
|
|
25
|
+
'Analytics',
|
|
26
|
+
'Communication',
|
|
27
|
+
'Utility',
|
|
28
|
+
]),
|
|
29
|
+
},
|
|
30
|
+
{ name: 'codex without categories', filename, code: '{ "node": "n8n-nodes-example" }' },
|
|
31
|
+
{
|
|
32
|
+
name: 'unrelated JSON file',
|
|
33
|
+
filename: 'package.json',
|
|
34
|
+
code: codex(['Bananas']),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'non-node class',
|
|
38
|
+
filename: 'nodes/Example/Example.node.ts',
|
|
39
|
+
code: 'class Example { categories = ["Bananas"]; }',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'nested categories do not count as codex categories',
|
|
43
|
+
filename,
|
|
44
|
+
code: '{ "resources": { "categories": ["Bananas"] } }',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
invalid: [
|
|
48
|
+
{
|
|
49
|
+
name: 'unknown category',
|
|
50
|
+
filename,
|
|
51
|
+
code: codex(['Development', 'Bananas']),
|
|
52
|
+
errors: [{ messageId: 'invalidCategory', data: { category: 'Bananas' } }],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'old marketing category',
|
|
56
|
+
filename,
|
|
57
|
+
code: codex(['Marketing']),
|
|
58
|
+
errors: [{ messageId: 'marketingCategory' }],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'invalid category type',
|
|
62
|
+
filename,
|
|
63
|
+
code: codex(['Sales', 12]),
|
|
64
|
+
errors: [{ messageId: 'invalidCategoryType' }],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'categories must be an array',
|
|
68
|
+
filename,
|
|
69
|
+
code: codex('Sales'),
|
|
70
|
+
errors: [{ messageId: 'invalidCategories' }],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'empty categories array',
|
|
74
|
+
filename,
|
|
75
|
+
code: codex([]),
|
|
76
|
+
errors: [{ messageId: 'invalidCategories' }],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
const validCategories = new Set([
|
|
7
|
+
'Data & Storage',
|
|
8
|
+
'Finance & Accounting',
|
|
9
|
+
'Marketing & Content',
|
|
10
|
+
'Productivity',
|
|
11
|
+
'Miscellaneous',
|
|
12
|
+
'Sales',
|
|
13
|
+
'Development',
|
|
14
|
+
'Analytics',
|
|
15
|
+
'Communication',
|
|
16
|
+
'Utility',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export const ValidNodeCategoriesRule = createRule({
|
|
20
|
+
name: 'valid-node-categories',
|
|
21
|
+
meta: {
|
|
22
|
+
type: 'problem',
|
|
23
|
+
docs: {
|
|
24
|
+
description: 'Require supported community node categories in .node.json codex files',
|
|
25
|
+
},
|
|
26
|
+
messages: {
|
|
27
|
+
invalidCategories: '"categories" must be a non-empty array of community node categories.',
|
|
28
|
+
invalidCategoryType: 'Each category must be a string from the community node category list.',
|
|
29
|
+
invalidCategory:
|
|
30
|
+
'"{{ category }}" is not a supported community node category. See https://docs.n8n.io/connect/create-nodes/build-your-node/reference/codex-files/#node-categories for the allowed values.',
|
|
31
|
+
marketingCategory: '"Marketing" is not a supported category. Use "Marketing & Content".',
|
|
32
|
+
},
|
|
33
|
+
schema: [],
|
|
34
|
+
},
|
|
35
|
+
defaultOptions: [],
|
|
36
|
+
create(context) {
|
|
37
|
+
if (!context.filename.endsWith('.node.json')) return {};
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
ObjectExpression(node: TSESTree.ObjectExpression) {
|
|
41
|
+
const root = getTopLevelObjectInJson(node);
|
|
42
|
+
if (!root) return;
|
|
43
|
+
|
|
44
|
+
const categories = findJsonProperty(root, 'categories');
|
|
45
|
+
if (!categories) return;
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
categories.value.type !== AST_NODE_TYPES.ArrayExpression ||
|
|
49
|
+
categories.value.elements.length === 0
|
|
50
|
+
) {
|
|
51
|
+
context.report({ node: categories.value, messageId: 'invalidCategories' });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const category of categories.value.elements) {
|
|
56
|
+
if (!category) continue;
|
|
57
|
+
if (category.type !== AST_NODE_TYPES.Literal || typeof category.value !== 'string') {
|
|
58
|
+
context.report({ node: category, messageId: 'invalidCategoryType' });
|
|
59
|
+
} else if (category.value === 'Marketing') {
|
|
60
|
+
context.report({ node: category, messageId: 'marketingCategory' });
|
|
61
|
+
} else if (!validCategories.has(category.value)) {
|
|
62
|
+
context.report({
|
|
63
|
+
node: category,
|
|
64
|
+
messageId: 'invalidCategory',
|
|
65
|
+
data: { category: category.value },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
});
|
|
@@ -88,6 +88,30 @@ export class RegularClass {
|
|
|
88
88
|
}`,
|
|
89
89
|
}),
|
|
90
90
|
},
|
|
91
|
+
{
|
|
92
|
+
name: 'lifecycle methods written with computed literal keys',
|
|
93
|
+
code: createTriggerNode({
|
|
94
|
+
webhookMethods: `{
|
|
95
|
+
default: {
|
|
96
|
+
['checkExists']: async () => true,
|
|
97
|
+
['create']: async () => true,
|
|
98
|
+
[\`delete\`]: async () => true,
|
|
99
|
+
},
|
|
100
|
+
}`,
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'lifecycle group and methods written with quoted keys',
|
|
105
|
+
code: createTriggerNode({
|
|
106
|
+
webhookMethods: `{
|
|
107
|
+
'default': {
|
|
108
|
+
'checkExists': async () => true,
|
|
109
|
+
'create': async () => true,
|
|
110
|
+
'delete': async () => true,
|
|
111
|
+
},
|
|
112
|
+
}`,
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
91
115
|
{
|
|
92
116
|
name: 'webhook lifecycle defined via arrow functions',
|
|
93
117
|
code: createTriggerNode({
|
|
@@ -189,6 +213,24 @@ export class RegularClass {
|
|
|
189
213
|
}),
|
|
190
214
|
errors: [{ messageId: 'missingWebhookMethods' }],
|
|
191
215
|
},
|
|
216
|
+
{
|
|
217
|
+
name: 'lifecycle method behind a key only known at runtime',
|
|
218
|
+
code: createTriggerNode({
|
|
219
|
+
webhookMethods: `{
|
|
220
|
+
default: {
|
|
221
|
+
async checkExists() { return true; },
|
|
222
|
+
async create() { return true; },
|
|
223
|
+
[methodName]: async () => true,
|
|
224
|
+
},
|
|
225
|
+
}`,
|
|
226
|
+
}),
|
|
227
|
+
errors: [
|
|
228
|
+
{
|
|
229
|
+
messageId: 'missingLifecycleMethod',
|
|
230
|
+
data: { group: 'default', missing: '`delete`' },
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
},
|
|
192
234
|
{
|
|
193
235
|
name: 'multiple webhook groups each missing methods',
|
|
194
236
|
code: createTriggerNode({
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createRule,
|
|
5
5
|
findClassProperty,
|
|
6
6
|
findObjectProperty,
|
|
7
|
+
getPropertyKeyName,
|
|
7
8
|
isNodeTypeClass,
|
|
8
9
|
WEBHOOK_LIFECYCLE_METHODS,
|
|
9
10
|
type WebhookLifecycleMethod,
|
|
@@ -25,12 +26,7 @@ function hasWebhooksDeclared(descriptionValue: TSESTree.ObjectExpression): boole
|
|
|
25
26
|
/** Returns true when the property defines a (possibly async) method named `name`. */
|
|
26
27
|
function isMethodProperty(property: TSESTree.ObjectLiteralElement, name: string): boolean {
|
|
27
28
|
if (property.type !== AST_NODE_TYPES.Property) return false;
|
|
28
|
-
if (property
|
|
29
|
-
|
|
30
|
-
const keyMatches =
|
|
31
|
-
(property.key.type === AST_NODE_TYPES.Identifier && property.key.name === name) ||
|
|
32
|
-
(property.key.type === AST_NODE_TYPES.Literal && property.key.value === name);
|
|
33
|
-
if (!keyMatches) return false;
|
|
29
|
+
if (getPropertyKeyName(property) !== name) return false;
|
|
34
30
|
|
|
35
31
|
return (
|
|
36
32
|
property.value.type === AST_NODE_TYPES.FunctionExpression ||
|
|
@@ -104,12 +100,7 @@ export const WebhookLifecycleCompleteRule = createRule({
|
|
|
104
100
|
if (groupProperty.type !== AST_NODE_TYPES.Property) continue;
|
|
105
101
|
if (groupProperty.value.type !== AST_NODE_TYPES.ObjectExpression) continue;
|
|
106
102
|
|
|
107
|
-
const groupName =
|
|
108
|
-
groupProperty.key.type === AST_NODE_TYPES.Identifier
|
|
109
|
-
? groupProperty.key.name
|
|
110
|
-
: groupProperty.key.type === AST_NODE_TYPES.Literal
|
|
111
|
-
? String(groupProperty.key.value)
|
|
112
|
-
: 'default';
|
|
103
|
+
const groupName = getPropertyKeyName(groupProperty) ?? 'default';
|
|
113
104
|
|
|
114
105
|
const missing = findMissingMethods(groupProperty.value);
|
|
115
106
|
if (missing.length === 0) continue;
|
package/src/utils/ast-utils.ts
CHANGED
|
@@ -55,6 +55,33 @@ export function isTriggerNode(
|
|
|
55
55
|
return hasTriggerGroup(descriptionValue) || isTriggerNodeClass(node);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Returns the name a property key stands for, or null when it is only known at
|
|
60
|
+
* runtime. `delete`, `'delete'` and `['delete']` all name the same property, so
|
|
61
|
+
* how the key is written must not decide whether a rule sees it.
|
|
62
|
+
*/
|
|
63
|
+
export function getPropertyKeyName(
|
|
64
|
+
property: TSESTree.Property | TSESTree.PropertyDefinition | TSESTree.MethodDefinition,
|
|
65
|
+
): string | null {
|
|
66
|
+
const { key } = property;
|
|
67
|
+
|
|
68
|
+
if (key.type === AST_NODE_TYPES.Literal) {
|
|
69
|
+
return key.value === null ? null : String(key.value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A computed key still names one property when it is a template without
|
|
73
|
+
// substitutions; written plainly, an identifier is the name itself.
|
|
74
|
+
if (key.type === AST_NODE_TYPES.TemplateLiteral) {
|
|
75
|
+
return key.expressions.length === 0 ? (key.quasis[0]?.value.cooked ?? null) : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (key.type === AST_NODE_TYPES.Identifier) {
|
|
79
|
+
return property.computed ? null : key.name;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
58
85
|
export function findClassProperty(
|
|
59
86
|
node: TSESTree.ClassDeclaration,
|
|
60
87
|
propertyName: string,
|
|
@@ -62,8 +89,7 @@ export function findClassProperty(
|
|
|
62
89
|
const property = node.body.body.find(
|
|
63
90
|
(member) =>
|
|
64
91
|
member.type === AST_NODE_TYPES.PropertyDefinition &&
|
|
65
|
-
member
|
|
66
|
-
member.key.name === propertyName,
|
|
92
|
+
getPropertyKeyName(member) === propertyName,
|
|
67
93
|
);
|
|
68
94
|
return property?.type === AST_NODE_TYPES.PropertyDefinition ? property : null;
|
|
69
95
|
}
|
|
@@ -140,10 +166,7 @@ export function findObjectProperty(
|
|
|
140
166
|
propertyName: string,
|
|
141
167
|
): TSESTree.Property | null {
|
|
142
168
|
const property = obj.properties.find(
|
|
143
|
-
(prop) =>
|
|
144
|
-
prop.type === AST_NODE_TYPES.Property &&
|
|
145
|
-
prop.key.type === AST_NODE_TYPES.Identifier &&
|
|
146
|
-
prop.key.name === propertyName,
|
|
169
|
+
(prop) => prop.type === AST_NODE_TYPES.Property && getPropertyKeyName(prop) === propertyName,
|
|
147
170
|
);
|
|
148
171
|
return property?.type === AST_NODE_TYPES.Property ? property : null;
|
|
149
172
|
}
|