@n8n/eslint-plugin-community-nodes 0.22.0 → 0.23.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$colon$unchecked.log +1 -1
- package/README.md +5 -0
- package/dist/plugin.d.ts +30 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +10 -0
- package/dist/plugin.js.map +1 -1
- package/dist/rules/index.d.ts +5 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +10 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/no-asterisk-in-option-names.d.ts +2 -0
- package/dist/rules/no-asterisk-in-option-names.d.ts.map +1 -0
- package/dist/rules/no-asterisk-in-option-names.js +90 -0
- package/dist/rules/no-asterisk-in-option-names.js.map +1 -0
- package/dist/rules/no-duplicate-param-options.d.ts +2 -0
- package/dist/rules/no-duplicate-param-options.d.ts.map +1 -0
- package/dist/rules/no-duplicate-param-options.js +97 -0
- package/dist/rules/no-duplicate-param-options.js.map +1 -0
- package/dist/rules/require-mit-license.d.ts +2 -0
- package/dist/rules/require-mit-license.d.ts.map +1 -0
- package/dist/rules/require-mit-license.js +57 -0
- package/dist/rules/require-mit-license.js.map +1 -0
- package/dist/rules/require-param-default.d.ts +15 -0
- package/dist/rules/require-param-default.d.ts.map +1 -0
- package/dist/rules/require-param-default.js +75 -0
- package/dist/rules/require-param-default.js.map +1 -0
- package/dist/rules/trigger-node-conventions.d.ts +2 -0
- package/dist/rules/trigger-node-conventions.d.ts.map +1 -0
- package/dist/rules/trigger-node-conventions.js +70 -0
- package/dist/rules/trigger-node-conventions.js.map +1 -0
- package/dist/typecheck.tsbuildinfo +1 -1
- package/docs/rules/no-asterisk-in-option-names.md +61 -0
- package/docs/rules/no-duplicate-param-options.md +46 -0
- package/docs/rules/require-mit-license.md +38 -0
- package/docs/rules/require-param-default.md +60 -0
- package/docs/rules/trigger-node-conventions.md +49 -0
- package/package.json +6 -5
- package/src/plugin.ts +10 -0
- package/src/rules/index.ts +10 -0
- package/src/rules/no-asterisk-in-option-names.test.ts +185 -0
- package/src/rules/no-asterisk-in-option-names.ts +111 -0
- package/src/rules/no-duplicate-param-options.test.ts +125 -0
- package/src/rules/no-duplicate-param-options.ts +125 -0
- package/src/rules/require-mit-license.test.ts +67 -0
- package/src/rules/require-mit-license.ts +65 -0
- package/src/rules/require-param-default.test.ts +227 -0
- package/src/rules/require-param-default.ts +89 -0
- package/src/rules/trigger-node-conventions.test.ts +110 -0
- package/src/rules/trigger-node-conventions.ts +92 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Disallow asterisk characters in node option name values (`@n8n/community-nodes/no-asterisk-in-option-names`)
|
|
2
|
+
|
|
3
|
+
⚠️ This rule _warns_ in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
|
|
4
|
+
|
|
5
|
+
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
6
|
+
|
|
7
|
+
<!-- end auto-generated rule header -->
|
|
8
|
+
|
|
9
|
+
## Rule Details
|
|
10
|
+
|
|
11
|
+
The `name` of each entry in an `options` array is rendered directly in the n8n
|
|
12
|
+
editor. An asterisk (`*`) in these labels renders ambiguously — it reads like a
|
|
13
|
+
markdown bullet or emphasis marker rather than literal text. This rule flags any
|
|
14
|
+
option `name` containing `*` and suggests replacing it with bracketed notation
|
|
15
|
+
such as `[All]`.
|
|
16
|
+
|
|
17
|
+
The rule only inspects `name` values inside `options` arrays (including nested
|
|
18
|
+
`collection` and `fixedCollection` options); asterisks elsewhere are not
|
|
19
|
+
reported.
|
|
20
|
+
|
|
21
|
+
## Examples
|
|
22
|
+
|
|
23
|
+
### Incorrect
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
export class MyNode implements INodeType {
|
|
27
|
+
description: INodeTypeDescription = {
|
|
28
|
+
displayName: 'My Node',
|
|
29
|
+
name: 'myNode',
|
|
30
|
+
properties: [
|
|
31
|
+
{
|
|
32
|
+
displayName: 'Resource',
|
|
33
|
+
name: 'resource',
|
|
34
|
+
type: 'options',
|
|
35
|
+
options: [{ name: '* All', value: 'all' }],
|
|
36
|
+
default: 'all',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Correct
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
export class MyNode implements INodeType {
|
|
47
|
+
description: INodeTypeDescription = {
|
|
48
|
+
displayName: 'My Node',
|
|
49
|
+
name: 'myNode',
|
|
50
|
+
properties: [
|
|
51
|
+
{
|
|
52
|
+
displayName: 'Resource',
|
|
53
|
+
name: 'resource',
|
|
54
|
+
type: 'options',
|
|
55
|
+
options: [{ name: '[All]', value: 'all' }],
|
|
56
|
+
default: 'all',
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Disallow duplicate option names or values within a single node parameter (`@n8n/community-nodes/no-duplicate-param-options`)
|
|
2
|
+
|
|
3
|
+
⚠️ This rule _warns_ in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
|
|
4
|
+
|
|
5
|
+
<!-- end auto-generated rule header -->
|
|
6
|
+
|
|
7
|
+
## Rule Details
|
|
8
|
+
|
|
9
|
+
Within an `options`- or `multiOptions`-typed node parameter, each option must
|
|
10
|
+
have a unique `name` and a unique `value`. Duplicate names confuse users in the
|
|
11
|
+
dropdown, and duplicate values make selections ambiguous and break value-based
|
|
12
|
+
logic downstream.
|
|
13
|
+
|
|
14
|
+
## Examples
|
|
15
|
+
|
|
16
|
+
### Incorrect
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
{
|
|
20
|
+
displayName: 'Resource',
|
|
21
|
+
name: 'resource',
|
|
22
|
+
type: 'options',
|
|
23
|
+
options: [
|
|
24
|
+
{ name: 'Contact', value: 'contact' },
|
|
25
|
+
{ name: 'Contact', value: 'person' }, // duplicate name
|
|
26
|
+
{ name: 'User', value: 'contact' }, // duplicate value
|
|
27
|
+
],
|
|
28
|
+
default: 'contact',
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Correct
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
{
|
|
36
|
+
displayName: 'Resource',
|
|
37
|
+
name: 'resource',
|
|
38
|
+
type: 'options',
|
|
39
|
+
options: [
|
|
40
|
+
{ name: 'Contact', value: 'contact' },
|
|
41
|
+
{ name: 'Person', value: 'person' },
|
|
42
|
+
{ name: 'User', value: 'user' },
|
|
43
|
+
],
|
|
44
|
+
default: 'contact',
|
|
45
|
+
}
|
|
46
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Require the "license" field in community node package.json to be "MIT" (`@n8n/community-nodes/require-mit-license`)
|
|
2
|
+
|
|
3
|
+
💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
|
|
4
|
+
|
|
5
|
+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
|
|
6
|
+
|
|
7
|
+
<!-- end auto-generated rule header -->
|
|
8
|
+
|
|
9
|
+
## Rule Details
|
|
10
|
+
|
|
11
|
+
Validates that the `package.json` of a community node package declares its `license` as `"MIT"`. Community node packages must be MIT licensed to be distributed through n8n.
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
### ❌ Incorrect
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"name": "n8n-nodes-my-service",
|
|
20
|
+
"version": "1.0.0"
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"name": "n8n-nodes-my-service",
|
|
27
|
+
"license": "Apache-2.0"
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### ✅ Correct
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"name": "n8n-nodes-my-service",
|
|
36
|
+
"license": "MIT"
|
|
37
|
+
}
|
|
38
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Require every node parameter to declare a default value (`@n8n/community-nodes/require-param-default`)
|
|
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
|
+
Every node parameter must declare a `default` property. A parameter is detected
|
|
10
|
+
by its shape: an object inside the node's `description` with `displayName`,
|
|
11
|
+
`name`, and `type` all set to string literals — the shape every
|
|
12
|
+
`INodeProperties` entry has. Entries in an `options` array (which carry
|
|
13
|
+
`name`/`value` but no `displayName`/`type`) are not treated as parameters.
|
|
14
|
+
|
|
15
|
+
Without a `default`, n8n cannot reliably initialise the parameter's value. This
|
|
16
|
+
leads to inconsistent behaviour in the editor (empty or `undefined` fields) and
|
|
17
|
+
at execution time. Even non-input types such as `notice` are expected to declare
|
|
18
|
+
`default: ''`.
|
|
19
|
+
|
|
20
|
+
This rule also checks parameters nested inside `options` of `collection` and
|
|
21
|
+
`fixedCollection` parameters.
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
25
|
+
### Incorrect
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
export class MyNode implements INodeType {
|
|
29
|
+
description: INodeTypeDescription = {
|
|
30
|
+
displayName: 'My Node',
|
|
31
|
+
name: 'myNode',
|
|
32
|
+
properties: [
|
|
33
|
+
{
|
|
34
|
+
displayName: 'Field',
|
|
35
|
+
name: 'field',
|
|
36
|
+
type: 'string',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Correct
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
export class MyNode implements INodeType {
|
|
47
|
+
description: INodeTypeDescription = {
|
|
48
|
+
displayName: 'My Node',
|
|
49
|
+
name: 'myNode',
|
|
50
|
+
properties: [
|
|
51
|
+
{
|
|
52
|
+
displayName: 'Field',
|
|
53
|
+
name: 'field',
|
|
54
|
+
type: 'string',
|
|
55
|
+
default: '',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Trigger nodes (class name ends with `Trigger`) must label themselves consistently as triggers (`@n8n/community-nodes/trigger-node-conventions`)
|
|
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
|
+
When a node class name ends with `Trigger`, the node must consistently present
|
|
10
|
+
itself as a trigger so users and the editor recognize it as one. This rule
|
|
11
|
+
requires all of:
|
|
12
|
+
|
|
13
|
+
- `description.name` ends with `Trigger`
|
|
14
|
+
- `description.displayName` contains `Trigger`
|
|
15
|
+
- `description.inputs` is an empty array (`[]`) — trigger nodes start an
|
|
16
|
+
execution and take no main inputs
|
|
17
|
+
|
|
18
|
+
This consolidates three checks that form a single conceptual requirement: if
|
|
19
|
+
it's a trigger node, label it consistently.
|
|
20
|
+
|
|
21
|
+
## Examples
|
|
22
|
+
|
|
23
|
+
### Incorrect
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
export class MyTrigger implements INodeType {
|
|
27
|
+
description: INodeTypeDescription = {
|
|
28
|
+
displayName: 'My Node',
|
|
29
|
+
name: 'my',
|
|
30
|
+
inputs: ['main'],
|
|
31
|
+
outputs: ['main'],
|
|
32
|
+
// ...
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Correct
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
export class MyTrigger implements INodeType {
|
|
41
|
+
description: INodeTypeDescription = {
|
|
42
|
+
displayName: 'My Trigger',
|
|
43
|
+
name: 'myTrigger',
|
|
44
|
+
inputs: [],
|
|
45
|
+
outputs: ['main'],
|
|
46
|
+
// ...
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
```
|
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.23.0",
|
|
5
5
|
"main": "./dist/plugin.js",
|
|
6
6
|
"types": "./dist/plugin.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -22,10 +22,11 @@
|
|
|
22
22
|
"eslint-plugin-eslint-plugin": "^7.0.0",
|
|
23
23
|
"rimraf": "6.0.1",
|
|
24
24
|
"typescript": "6.0.2",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"@n8n/
|
|
28
|
-
"n8n-
|
|
25
|
+
"vite": "^8.0.2",
|
|
26
|
+
"vitest": "^4.1.9",
|
|
27
|
+
"@n8n/typescript-config": "1.7.0",
|
|
28
|
+
"@n8n/vitest-config": "1.16.0",
|
|
29
|
+
"n8n-workflow": "2.29.0"
|
|
29
30
|
},
|
|
30
31
|
"peerDependencies": {
|
|
31
32
|
"eslint": "9.29.0",
|
package/src/plugin.ts
CHANGED
|
@@ -27,6 +27,8 @@ const configs = {
|
|
|
27
27
|
'@n8n/community-nodes/n8n-object-validation': 'error',
|
|
28
28
|
'@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
|
|
29
29
|
'@n8n/community-nodes/no-emoji-in-options': 'error',
|
|
30
|
+
'@n8n/community-nodes/no-asterisk-in-option-names': 'warn',
|
|
31
|
+
'@n8n/community-nodes/no-duplicate-param-options': 'warn',
|
|
30
32
|
'@n8n/community-nodes/node-usable-as-tool': 'error',
|
|
31
33
|
'@n8n/community-nodes/package-name-convention': 'error',
|
|
32
34
|
'@n8n/community-nodes/credential-test-required': 'error',
|
|
@@ -41,6 +43,7 @@ const configs = {
|
|
|
41
43
|
'@n8n/community-nodes/icon-prefer-themed-variants': 'warn',
|
|
42
44
|
'@n8n/community-nodes/options-sorted-alphabetically': 'warn',
|
|
43
45
|
'@n8n/community-nodes/resource-operation-pattern': 'warn',
|
|
46
|
+
'@n8n/community-nodes/trigger-node-conventions': 'error',
|
|
44
47
|
'@n8n/community-nodes/credential-documentation-url': 'error',
|
|
45
48
|
'@n8n/community-nodes/cred-class-field-icon-missing': 'error',
|
|
46
49
|
'@n8n/community-nodes/cred-class-name-field-conventions': 'error',
|
|
@@ -56,8 +59,10 @@ const configs = {
|
|
|
56
59
|
'@n8n/community-nodes/node-registration-complete': 'warn',
|
|
57
60
|
'@n8n/community-nodes/require-community-node-keyword': 'warn',
|
|
58
61
|
'@n8n/community-nodes/require-continue-on-fail': 'error',
|
|
62
|
+
'@n8n/community-nodes/require-mit-license': 'error',
|
|
59
63
|
'@n8n/community-nodes/require-node-api-error': 'error',
|
|
60
64
|
'@n8n/community-nodes/require-node-description-fields': 'error',
|
|
65
|
+
'@n8n/community-nodes/require-param-default': 'error',
|
|
61
66
|
'@n8n/community-nodes/require-version': 'error',
|
|
62
67
|
'@n8n/community-nodes/valid-author': 'error',
|
|
63
68
|
'@n8n/community-nodes/valid-credential-references': 'error',
|
|
@@ -77,6 +82,8 @@ const configs = {
|
|
|
77
82
|
'@n8n/community-nodes/n8n-object-validation': 'error',
|
|
78
83
|
'@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
|
|
79
84
|
'@n8n/community-nodes/no-emoji-in-options': 'error',
|
|
85
|
+
'@n8n/community-nodes/no-asterisk-in-option-names': 'warn',
|
|
86
|
+
'@n8n/community-nodes/no-duplicate-param-options': 'warn',
|
|
80
87
|
'@n8n/community-nodes/node-usable-as-tool': 'error',
|
|
81
88
|
'@n8n/community-nodes/package-name-convention': 'error',
|
|
82
89
|
'@n8n/community-nodes/credential-test-required': 'error',
|
|
@@ -92,6 +99,7 @@ const configs = {
|
|
|
92
99
|
'@n8n/community-nodes/options-sorted-alphabetically': 'warn',
|
|
93
100
|
'@n8n/community-nodes/credential-documentation-url': 'error',
|
|
94
101
|
'@n8n/community-nodes/resource-operation-pattern': 'warn',
|
|
102
|
+
'@n8n/community-nodes/trigger-node-conventions': 'error',
|
|
95
103
|
'@n8n/community-nodes/cred-class-field-icon-missing': 'error',
|
|
96
104
|
'@n8n/community-nodes/cred-class-name-field-conventions': 'error',
|
|
97
105
|
'@n8n/community-nodes/cred-class-name-suffix': 'error',
|
|
@@ -106,8 +114,10 @@ const configs = {
|
|
|
106
114
|
'@n8n/community-nodes/node-registration-complete': 'warn',
|
|
107
115
|
'@n8n/community-nodes/require-community-node-keyword': 'warn',
|
|
108
116
|
'@n8n/community-nodes/require-continue-on-fail': 'error',
|
|
117
|
+
'@n8n/community-nodes/require-mit-license': 'error',
|
|
109
118
|
'@n8n/community-nodes/require-node-api-error': 'error',
|
|
110
119
|
'@n8n/community-nodes/require-node-description-fields': 'error',
|
|
120
|
+
'@n8n/community-nodes/require-param-default': 'error',
|
|
111
121
|
'@n8n/community-nodes/require-version': 'error',
|
|
112
122
|
'@n8n/community-nodes/valid-author': 'error',
|
|
113
123
|
'@n8n/community-nodes/valid-credential-references': 'error',
|
package/src/rules/index.ts
CHANGED
|
@@ -13,10 +13,12 @@ import { IconPreferThemedVariantsRule } from './icon-prefer-themed-variants.js';
|
|
|
13
13
|
import { IconValidationRule } from './icon-validation.js';
|
|
14
14
|
import { MissingPairedItemRule } from './missing-paired-item.js';
|
|
15
15
|
import { N8nObjectValidationRule } from './n8n-object-validation.js';
|
|
16
|
+
import { NoAsteriskInOptionNamesRule } from './no-asterisk-in-option-names.js';
|
|
16
17
|
import { NoBuilderHintLeakageRule } from './no-builder-hint-leakage.js';
|
|
17
18
|
import { NoCredentialReuseRule } from './no-credential-reuse.js';
|
|
18
19
|
import { NoDangerousFunctionsRule } from './no-dangerous-functions.js';
|
|
19
20
|
import { NoDeprecatedWorkflowFunctionsRule } from './no-deprecated-workflow-functions.js';
|
|
21
|
+
import { NoDuplicateParamOptionsRule } from './no-duplicate-param-options.js';
|
|
20
22
|
import { NoEmojiInOptionsRule } from './no-emoji-in-options.js';
|
|
21
23
|
import { NoForbiddenLifecycleScriptsRule } from './no-forbidden-lifecycle-scripts.js';
|
|
22
24
|
import { NoHttpRequestWithManualAuthRule } from './no-http-request-with-manual-auth.js';
|
|
@@ -36,10 +38,13 @@ import { OptionsSortedAlphabeticallyRule } from './options-sorted-alphabetically
|
|
|
36
38
|
import { PackageNameConventionRule } from './package-name-convention.js';
|
|
37
39
|
import { RequireCommunityNodeKeywordRule } from './require-community-node-keyword.js';
|
|
38
40
|
import { RequireContinueOnFailRule } from './require-continue-on-fail.js';
|
|
41
|
+
import { RequireMitLicenseRule } from './require-mit-license.js';
|
|
39
42
|
import { RequireNodeApiErrorRule } from './require-node-api-error.js';
|
|
40
43
|
import { RequireNodeDescriptionFieldsRule } from './require-node-description-fields.js';
|
|
44
|
+
import { RequireParamDefaultRule } from './require-param-default.js';
|
|
41
45
|
import { RequireVersionRule } from './require-version.js';
|
|
42
46
|
import { ResourceOperationPatternRule } from './resource-operation-pattern.js';
|
|
47
|
+
import { TriggerNodeConventionsRule } from './trigger-node-conventions.js';
|
|
43
48
|
import { ValidAuthorRule } from './valid-author.js';
|
|
44
49
|
import { ValidCredentialReferencesRule } from './valid-credential-references.js';
|
|
45
50
|
import { ValidDescriptionRule } from './valid-description.js';
|
|
@@ -53,6 +58,8 @@ export const rules = {
|
|
|
53
58
|
'credential-password-field': CredentialPasswordFieldRule,
|
|
54
59
|
'no-deprecated-workflow-functions': NoDeprecatedWorkflowFunctionsRule,
|
|
55
60
|
'no-emoji-in-options': NoEmojiInOptionsRule,
|
|
61
|
+
'no-asterisk-in-option-names': NoAsteriskInOptionNamesRule,
|
|
62
|
+
'no-duplicate-param-options': NoDuplicateParamOptionsRule,
|
|
56
63
|
'node-usable-as-tool': NodeUsableAsToolRule,
|
|
57
64
|
'options-sorted-alphabetically': OptionsSortedAlphabeticallyRule,
|
|
58
65
|
'package-name-convention': PackageNameConventionRule,
|
|
@@ -67,6 +74,7 @@ export const rules = {
|
|
|
67
74
|
'icon-validation': IconValidationRule,
|
|
68
75
|
'icon-prefer-themed-variants': IconPreferThemedVariantsRule,
|
|
69
76
|
'resource-operation-pattern': ResourceOperationPatternRule,
|
|
77
|
+
'trigger-node-conventions': TriggerNodeConventionsRule,
|
|
70
78
|
'credential-documentation-url': CredentialDocumentationUrlRule,
|
|
71
79
|
'node-class-description-icon-missing': NodeClassDescriptionIconMissingRule,
|
|
72
80
|
'node-class-description-name-camelcase': NodeClassDescriptionNameCamelCaseRule,
|
|
@@ -84,8 +92,10 @@ export const rules = {
|
|
|
84
92
|
'n8n-object-validation': N8nObjectValidationRule,
|
|
85
93
|
'require-community-node-keyword': RequireCommunityNodeKeywordRule,
|
|
86
94
|
'require-continue-on-fail': RequireContinueOnFailRule,
|
|
95
|
+
'require-mit-license': RequireMitLicenseRule,
|
|
87
96
|
'require-node-api-error': RequireNodeApiErrorRule,
|
|
88
97
|
'require-node-description-fields': RequireNodeDescriptionFieldsRule,
|
|
98
|
+
'require-param-default': RequireParamDefaultRule,
|
|
89
99
|
'require-version': RequireVersionRule,
|
|
90
100
|
'valid-author': ValidAuthorRule,
|
|
91
101
|
'valid-credential-references': ValidCredentialReferencesRule,
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { NoAsteriskInOptionNamesRule } from './no-asterisk-in-option-names.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
function createNodeCode(body: string): string {
|
|
8
|
+
return `
|
|
9
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
10
|
+
|
|
11
|
+
export class TestNode implements INodeType {
|
|
12
|
+
description: INodeTypeDescription = {
|
|
13
|
+
${body}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
ruleTester.run('no-asterisk-in-option-names', NoAsteriskInOptionNamesRule, {
|
|
20
|
+
valid: [
|
|
21
|
+
{
|
|
22
|
+
name: 'class that does not implement INodeType',
|
|
23
|
+
filename: '/tmp/TestNode.node.ts',
|
|
24
|
+
code: `
|
|
25
|
+
export class NotANode {
|
|
26
|
+
description = {
|
|
27
|
+
properties: [
|
|
28
|
+
{ options: [{ name: '* All', value: 'all' }] },
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
`,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'non .node.ts file is ignored',
|
|
36
|
+
filename: '/tmp/helper.ts',
|
|
37
|
+
code: `
|
|
38
|
+
export class TestNode {
|
|
39
|
+
description = {
|
|
40
|
+
properties: [
|
|
41
|
+
{ options: [{ name: '* All', value: 'all' }] },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
`,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'option names without asterisk',
|
|
49
|
+
filename: '/tmp/TestNode.node.ts',
|
|
50
|
+
code: createNodeCode(`
|
|
51
|
+
displayName: 'Test Node',
|
|
52
|
+
name: 'testNode',
|
|
53
|
+
properties: [
|
|
54
|
+
{
|
|
55
|
+
displayName: 'Operation',
|
|
56
|
+
name: 'operation',
|
|
57
|
+
type: 'options',
|
|
58
|
+
options: [
|
|
59
|
+
{ name: 'Create', value: 'create' },
|
|
60
|
+
{ name: '[All]', value: 'all' },
|
|
61
|
+
],
|
|
62
|
+
default: 'create',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
`),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'asterisk in displayName outside options is ignored',
|
|
69
|
+
filename: '/tmp/TestNode.node.ts',
|
|
70
|
+
code: createNodeCode(`
|
|
71
|
+
displayName: 'Test * Node',
|
|
72
|
+
name: 'testNode',
|
|
73
|
+
properties: [
|
|
74
|
+
{ displayName: 'Field *', name: 'field', type: 'string', default: '' },
|
|
75
|
+
],
|
|
76
|
+
`),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
invalid: [
|
|
80
|
+
{
|
|
81
|
+
name: 'asterisk in option name within options array',
|
|
82
|
+
filename: '/tmp/TestNode.node.ts',
|
|
83
|
+
code: createNodeCode(`
|
|
84
|
+
displayName: 'Test Node',
|
|
85
|
+
name: 'testNode',
|
|
86
|
+
properties: [
|
|
87
|
+
{
|
|
88
|
+
displayName: 'Resource',
|
|
89
|
+
name: 'resource',
|
|
90
|
+
type: 'options',
|
|
91
|
+
options: [
|
|
92
|
+
{ name: '* All', value: 'all' },
|
|
93
|
+
{ name: 'User', value: 'user' },
|
|
94
|
+
],
|
|
95
|
+
default: 'all',
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
`),
|
|
99
|
+
errors: [
|
|
100
|
+
{
|
|
101
|
+
messageId: 'asteriskInOptionName',
|
|
102
|
+
data: { name: '* All' },
|
|
103
|
+
suggestions: [
|
|
104
|
+
{
|
|
105
|
+
messageId: 'replaceAsterisk',
|
|
106
|
+
output: createNodeCode(`
|
|
107
|
+
displayName: 'Test Node',
|
|
108
|
+
name: 'testNode',
|
|
109
|
+
properties: [
|
|
110
|
+
{
|
|
111
|
+
displayName: 'Resource',
|
|
112
|
+
name: 'resource',
|
|
113
|
+
type: 'options',
|
|
114
|
+
options: [
|
|
115
|
+
{ name: '[All] All', value: 'all' },
|
|
116
|
+
{ name: 'User', value: 'user' },
|
|
117
|
+
],
|
|
118
|
+
default: 'all',
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
`),
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'asterisk in nested collection options',
|
|
129
|
+
filename: '/tmp/TestNode.node.ts',
|
|
130
|
+
code: createNodeCode(`
|
|
131
|
+
displayName: 'Test Node',
|
|
132
|
+
name: 'testNode',
|
|
133
|
+
properties: [
|
|
134
|
+
{
|
|
135
|
+
displayName: 'Filters',
|
|
136
|
+
name: 'filters',
|
|
137
|
+
type: 'collection',
|
|
138
|
+
default: {},
|
|
139
|
+
options: [
|
|
140
|
+
{
|
|
141
|
+
displayName: 'Status',
|
|
142
|
+
name: 'status',
|
|
143
|
+
type: 'options',
|
|
144
|
+
options: [{ name: '*', value: 'any' }],
|
|
145
|
+
default: 'any',
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
`),
|
|
151
|
+
errors: [
|
|
152
|
+
{
|
|
153
|
+
messageId: 'asteriskInOptionName',
|
|
154
|
+
data: { name: '*' },
|
|
155
|
+
suggestions: [
|
|
156
|
+
{
|
|
157
|
+
messageId: 'replaceAsterisk',
|
|
158
|
+
output: createNodeCode(`
|
|
159
|
+
displayName: 'Test Node',
|
|
160
|
+
name: 'testNode',
|
|
161
|
+
properties: [
|
|
162
|
+
{
|
|
163
|
+
displayName: 'Filters',
|
|
164
|
+
name: 'filters',
|
|
165
|
+
type: 'collection',
|
|
166
|
+
default: {},
|
|
167
|
+
options: [
|
|
168
|
+
{
|
|
169
|
+
displayName: 'Status',
|
|
170
|
+
name: 'status',
|
|
171
|
+
type: 'options',
|
|
172
|
+
options: [{ name: '[All]', value: 'any' }],
|
|
173
|
+
default: 'any',
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
`),
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
isNodeTypeClass,
|
|
6
|
+
findClassProperty,
|
|
7
|
+
findObjectProperty,
|
|
8
|
+
getStringLiteralValue,
|
|
9
|
+
isFileType,
|
|
10
|
+
createRule,
|
|
11
|
+
} from '../utils/index.js';
|
|
12
|
+
|
|
13
|
+
export const NoAsteriskInOptionNamesRule = createRule({
|
|
14
|
+
name: 'no-asterisk-in-option-names',
|
|
15
|
+
meta: {
|
|
16
|
+
type: 'suggestion',
|
|
17
|
+
docs: {
|
|
18
|
+
description: 'Disallow asterisk characters in node option name values',
|
|
19
|
+
},
|
|
20
|
+
hasSuggestions: true,
|
|
21
|
+
messages: {
|
|
22
|
+
asteriskInOptionName:
|
|
23
|
+
'Option name "{{ name }}" contains "*", which renders ambiguously in the n8n UI. Use bracketed notation like "[All]" instead.',
|
|
24
|
+
replaceAsterisk: 'Replace "*" with "[All]".',
|
|
25
|
+
},
|
|
26
|
+
schema: [],
|
|
27
|
+
},
|
|
28
|
+
defaultOptions: [],
|
|
29
|
+
create(context) {
|
|
30
|
+
if (!isFileType(context.filename, '.node.ts')) {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check every object that is a direct element of an `options` array.
|
|
35
|
+
const checkOptionsArray = (optionsArray: TSESTree.ArrayExpression): void => {
|
|
36
|
+
for (const option of optionsArray.elements) {
|
|
37
|
+
if (!option || option.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const valueNode = findObjectProperty(option, 'name')?.value;
|
|
42
|
+
if (!valueNode) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const name = getStringLiteralValue(valueNode);
|
|
47
|
+
if (!name?.includes('*')) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
context.report({
|
|
52
|
+
node: valueNode,
|
|
53
|
+
messageId: 'asteriskInOptionName',
|
|
54
|
+
data: { name },
|
|
55
|
+
suggest: [
|
|
56
|
+
{
|
|
57
|
+
messageId: 'replaceAsterisk',
|
|
58
|
+
fix(fixer) {
|
|
59
|
+
const quote = context.sourceCode.getText(valueNode).at(0) ?? "'";
|
|
60
|
+
return fixer.replaceText(
|
|
61
|
+
valueNode,
|
|
62
|
+
`${quote}${name.replaceAll('*', '[All]')}${quote}`,
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const traverse = (node: TSESTree.Node): void => {
|
|
72
|
+
if (node.type === AST_NODE_TYPES.ObjectExpression) {
|
|
73
|
+
const optionsProperty = findObjectProperty(node, 'options');
|
|
74
|
+
if (optionsProperty?.value.type === AST_NODE_TYPES.ArrayExpression) {
|
|
75
|
+
checkOptionsArray(optionsProperty.value);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const key in node) {
|
|
80
|
+
if (key === 'parent') {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const child = node[key as keyof TSESTree.Node] as unknown;
|
|
84
|
+
if (Array.isArray(child)) {
|
|
85
|
+
for (const item of child) {
|
|
86
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
87
|
+
traverse(item as TSESTree.Node);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} else if (child && typeof child === 'object' && 'type' in child) {
|
|
91
|
+
traverse(child as TSESTree.Node);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
ClassDeclaration(node) {
|
|
98
|
+
if (!isNodeTypeClass(node)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const descriptionProperty = findClassProperty(node, 'description');
|
|
103
|
+
if (!descriptionProperty?.value) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
traverse(descriptionProperty.value);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
});
|