@n8n/eslint-plugin-community-nodes 0.15.0 → 0.17.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 +5 -0
- package/dist/plugin.d.ts +36 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +12 -0
- package/dist/plugin.js.map +1 -1
- package/dist/rules/cred-class-name-suffix.d.ts +2 -0
- package/dist/rules/cred-class-name-suffix.d.ts.map +1 -0
- package/dist/rules/cred-class-name-suffix.js +53 -0
- package/dist/rules/cred-class-name-suffix.js.map +1 -0
- package/dist/rules/cred-class-oauth2-naming.d.ts +2 -0
- package/dist/rules/cred-class-oauth2-naming.d.ts.map +1 -0
- package/dist/rules/cred-class-oauth2-naming.js +96 -0
- package/dist/rules/cred-class-oauth2-naming.js.map +1 -0
- package/dist/rules/index.d.ts +8 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +12 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/n8n-object-validation.d.ts +5 -0
- package/dist/rules/n8n-object-validation.d.ts.map +1 -0
- package/dist/rules/n8n-object-validation.js +148 -0
- package/dist/rules/n8n-object-validation.js.map +1 -0
- package/dist/rules/no-builder-hint-leakage.d.ts +7 -0
- package/dist/rules/no-builder-hint-leakage.d.ts.map +1 -0
- package/dist/rules/no-builder-hint-leakage.js +99 -0
- package/dist/rules/no-builder-hint-leakage.js.map +1 -0
- package/dist/rules/no-overrides-field.js +1 -1
- package/dist/rules/no-overrides-field.js.map +1 -1
- package/dist/rules/no-template-placeholders.d.ts +2 -0
- package/dist/rules/no-template-placeholders.d.ts.map +1 -0
- package/dist/rules/no-template-placeholders.js +57 -0
- package/dist/rules/no-template-placeholders.js.map +1 -0
- package/dist/rules/node-operation-error-itemindex.d.ts +12 -0
- package/dist/rules/node-operation-error-itemindex.d.ts.map +1 -0
- package/dist/rules/node-operation-error-itemindex.js +184 -0
- package/dist/rules/node-operation-error-itemindex.js.map +1 -0
- package/dist/utils/ast-utils.d.ts.map +1 -1
- package/dist/utils/ast-utils.js +5 -1
- package/dist/utils/ast-utils.js.map +1 -1
- package/docs/rules/cred-class-name-suffix.md +46 -0
- package/docs/rules/cred-class-oauth2-naming.md +68 -0
- package/docs/rules/n8n-object-validation.md +93 -0
- package/docs/rules/no-overrides-field.md +5 -5
- package/docs/rules/no-template-placeholders.md +51 -0
- package/docs/rules/node-operation-error-itemindex.md +81 -0
- package/package.json +4 -4
- package/src/plugin.ts +12 -0
- package/src/rules/cred-class-name-suffix.test.ts +74 -0
- package/src/rules/cred-class-name-suffix.ts +57 -0
- package/src/rules/cred-class-oauth2-naming.test.ts +197 -0
- package/src/rules/cred-class-oauth2-naming.ts +118 -0
- package/src/rules/index.ts +12 -0
- package/src/rules/n8n-object-validation.test.ts +202 -0
- package/src/rules/n8n-object-validation.ts +200 -0
- package/src/rules/no-builder-hint-leakage.test.ts +84 -0
- package/src/rules/no-builder-hint-leakage.ts +112 -0
- package/src/rules/no-overrides-field.ts +1 -1
- package/src/rules/no-template-placeholders.test.ts +135 -0
- package/src/rules/no-template-placeholders.ts +68 -0
- package/src/rules/node-operation-error-itemindex.test.ts +280 -0
- package/src/rules/node-operation-error-itemindex.ts +223 -0
- package/src/utils/ast-utils.ts +5 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Credential class names must be suffixed with `Api` (`@n8n/community-nodes/cred-class-name-suffix`)
|
|
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
|
+
Credential classes (those implementing `ICredentialType` in `*.credentials.ts` files) must have a class name ending in `Api`. This is the n8n convention so credentials are easily recognisable in code, in import statements, and across the credential registry.
|
|
12
|
+
|
|
13
|
+
For OAuth2 credentials extending the OAuth2 base, see the sibling rule `cred-class-oauth2-naming` which enforces the more specific `OAuth2Api` suffix.
|
|
14
|
+
|
|
15
|
+
## Opt-out
|
|
16
|
+
|
|
17
|
+
For legitimate exceptions (for example credentials representing custom auth headers where the `Api` suffix would be misleading), disable the rule for the specific class with a standard ESLint comment:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// eslint-disable-next-line @n8n/community-nodes/cred-class-name-suffix
|
|
21
|
+
export class CustomAuthHeader implements ICredentialType {
|
|
22
|
+
// ...
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Examples
|
|
27
|
+
|
|
28
|
+
### ❌ Incorrect
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
export class MyService implements ICredentialType {
|
|
32
|
+
name = 'myServiceApi';
|
|
33
|
+
displayName = 'My Service API';
|
|
34
|
+
properties: INodeProperties[] = [];
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### ✅ Correct
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
export class MyServiceApi implements ICredentialType {
|
|
42
|
+
name = 'myServiceApi';
|
|
43
|
+
displayName = 'My Service API';
|
|
44
|
+
properties: INodeProperties[] = [];
|
|
45
|
+
}
|
|
46
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# OAuth2 credentials must include `OAuth2` in the class name, `name`, and `displayName` (`@n8n/community-nodes/cred-class-oauth2-naming`)
|
|
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
|
+
OAuth2 credentials must consistently identify themselves as OAuth2 across all three naming surfaces. This makes them easy to distinguish from other auth flavours (API key, basic auth) in code, in import statements, and in the credentials picker.
|
|
12
|
+
|
|
13
|
+
A credential class is considered an OAuth2 credential if **any** of the following are true:
|
|
14
|
+
|
|
15
|
+
- The class name contains `OAuth2` (e.g. `GoogleOAuth2Api`).
|
|
16
|
+
- The class TypeScript-extends a base whose name contains `OAuth2` (e.g. `class GoogleOAuth2Api extends OAuth2Api`).
|
|
17
|
+
- The `extends` class field references an `OAuth2` credential (e.g. `extends = ['oAuth2Api']`).
|
|
18
|
+
- The `name` or `displayName` field contains `OAuth2`.
|
|
19
|
+
|
|
20
|
+
When a credential is detected as OAuth2, all of the following must contain `OAuth2`:
|
|
21
|
+
|
|
22
|
+
- Class name (must end with `OAuth2Api`).
|
|
23
|
+
- `name` class field.
|
|
24
|
+
- `displayName` class field.
|
|
25
|
+
|
|
26
|
+
The class name is auto-fixable; `name` and `displayName` must be updated manually because their casing convention varies.
|
|
27
|
+
|
|
28
|
+
## Examples
|
|
29
|
+
|
|
30
|
+
### ❌ Incorrect
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// Class name detected as OAuth2 but `name` and `displayName` lack OAuth2.
|
|
34
|
+
export class GoogleOAuth2Api implements ICredentialType {
|
|
35
|
+
name = 'googleApi';
|
|
36
|
+
displayName = 'Google API';
|
|
37
|
+
properties: INodeProperties[] = [];
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// Extends OAuth2 base, but neither class name, `name`, nor `displayName` reflect that.
|
|
43
|
+
export class GoogleApi implements ICredentialType {
|
|
44
|
+
name = 'googleApi';
|
|
45
|
+
displayName = 'Google API';
|
|
46
|
+
extends = ['oAuth2Api'];
|
|
47
|
+
properties: INodeProperties[] = [];
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### ✅ Correct
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
export class GoogleOAuth2Api implements ICredentialType {
|
|
55
|
+
name = 'googleOAuth2Api';
|
|
56
|
+
displayName = 'Google OAuth2 API';
|
|
57
|
+
extends = ['oAuth2Api'];
|
|
58
|
+
properties: INodeProperties[] = [];
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Migrated from
|
|
63
|
+
|
|
64
|
+
This rule consolidates three rules from the legacy `eslint-plugin-n8n-nodes-base` plugin into a single conceptual check:
|
|
65
|
+
|
|
66
|
+
- `cred-class-name-missing-oauth2-suffix`
|
|
67
|
+
- `cred-class-field-name-missing-oauth2`
|
|
68
|
+
- `cred-class-field-display-name-missing-oauth2`
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Validate the structure of the "n8n" object in community node package.json (required keys, types, and dist/ paths) (`@n8n/community-nodes/n8n-object-validation`)
|
|
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 community node package declares its nodes and credentials inside the
|
|
10
|
+
top-level `n8n` object in `package.json`. n8n loads packages by reading this
|
|
11
|
+
object: it looks up `n8n.n8nNodesApiVersion` to pick the correct loader, and it
|
|
12
|
+
loads each path in `n8n.nodes` and `n8n.credentials` as compiled JavaScript
|
|
13
|
+
relative to the package root. If any of those fields are missing, mistyped, or
|
|
14
|
+
point at TypeScript sources instead of compiled output, the package fails to
|
|
15
|
+
register at install time and the failure is opaque to the user.
|
|
16
|
+
|
|
17
|
+
This rule enforces the structural contract:
|
|
18
|
+
|
|
19
|
+
- `package.json` must contain an `n8n` object.
|
|
20
|
+
- `n8n.n8nNodesApiVersion` must be present and a positive integer. It must live
|
|
21
|
+
inside `n8n`, not at the root.
|
|
22
|
+
- `n8n.nodes` must be a non-empty array of strings, each starting with `dist/`.
|
|
23
|
+
- `n8n.credentials`, if present, must be an array of strings, each starting
|
|
24
|
+
with `dist/`.
|
|
25
|
+
|
|
26
|
+
The `dist/` prefix is required because community nodes ship compiled
|
|
27
|
+
JavaScript; n8n cannot load TypeScript sources at runtime. Variants such as
|
|
28
|
+
`./dist/...` or `DIST/...` are rejected to keep the convention consistent with
|
|
29
|
+
the templates published by `@n8n/node-cli` and `@n8n/create-node`.
|
|
30
|
+
|
|
31
|
+
## Examples
|
|
32
|
+
|
|
33
|
+
### Incorrect
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"name": "n8n-nodes-example",
|
|
38
|
+
"version": "1.0.0"
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"name": "n8n-nodes-example",
|
|
45
|
+
"n8nNodesApiVersion": 1,
|
|
46
|
+
"n8n": {
|
|
47
|
+
"nodes": ["dist/nodes/Foo/Foo.node.js"]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"name": "n8n-nodes-example",
|
|
55
|
+
"n8n": {
|
|
56
|
+
"n8nNodesApiVersion": "1",
|
|
57
|
+
"nodes": ["nodes/Foo/Foo.node.js"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"name": "n8n-nodes-example",
|
|
65
|
+
"n8n": {
|
|
66
|
+
"n8nNodesApiVersion": 1,
|
|
67
|
+
"nodes": []
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"name": "n8n-nodes-example",
|
|
75
|
+
"n8n": {
|
|
76
|
+
"n8nNodesApiVersion": 1,
|
|
77
|
+
"nodes": ["./dist/nodes/Foo/Foo.node.js"]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Correct
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"name": "n8n-nodes-example",
|
|
87
|
+
"n8n": {
|
|
88
|
+
"n8nNodesApiVersion": 1,
|
|
89
|
+
"nodes": ["dist/nodes/Foo/Foo.node.js"],
|
|
90
|
+
"credentials": ["dist/credentials/Foo.credentials.js"]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
## Rule Details
|
|
8
8
|
|
|
9
|
-
The `overrides` field in `package.json`
|
|
9
|
+
The `overrides` field in `package.json` forces specific versions of (transitive) dependencies. n8n installs each community package into an isolated `node_modules` tree (peer deps stripped before install, `require()` walks up from each node's compiled file), so an override in one node only affects that node's own resolution — it does **not** bleed into other nodes or into n8n core. The rule bans the field anyway because:
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
11
|
+
- **Almost always unintentional.** In practice, `overrides` blocks in community nodes are copy-pasted boilerplate from unrelated projects, sometimes alongside an empty `dependencies` so the override is a literal no-op.
|
|
12
|
+
- **No useful effect today.** Because of isolation, a maintainer who believes their override coordinates versions across nodes is wrong about what it does. The block is dead weight at best, actively misleading at worst.
|
|
13
|
+
- **Future-proofing.** If the install layout ever moves toward hoisting or partial sharing, today's "harmless" overrides start affecting other nodes' resolution. Banning the field now keeps that change safe to make.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Most community nodes do not need third-party runtime libraries at all. n8n core already provides HTTP requests (`this.helpers.httpRequest`, `this.helpers.httpRequestWithAuthentication`), credential resolution, binary data helpers, and other common building blocks via the execute context — these should be the default. `dependencies` and `peerDependencies` are restricted by [`no-runtime-dependencies`](no-runtime-dependencies.md) and [`valid-peer-dependencies`](valid-peer-dependencies.md) respectively, so neither is a workaround for `overrides`.
|
|
16
16
|
|
|
17
17
|
## Examples
|
|
18
18
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Disallow unresolved template placeholders in package.json (`@n8n/community-nodes/no-template-placeholders`)
|
|
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
|
+
Community node packages are typically scaffolded from a starter template that contains
|
|
10
|
+
placeholder values such as `<PACKAGE_NAME>`, `<USERNAME>`, or `{{ authorName }}`. When
|
|
11
|
+
these placeholders survive into a published `package.json`, the package metadata is
|
|
12
|
+
broken — the name is invalid, the repository link is dead, etc.
|
|
13
|
+
|
|
14
|
+
This rule scans every string value in `package.json` and reports any value containing
|
|
15
|
+
an unresolved placeholder pattern. It catches:
|
|
16
|
+
|
|
17
|
+
- Angle bracket placeholders: `<...>`
|
|
18
|
+
- Mustache placeholders: `{{...}}`
|
|
19
|
+
|
|
20
|
+
The rule applies to **all** string fields, including custom ones — not just the well-known
|
|
21
|
+
fields like `name`, `description`, `homepage`, or `repository.url`.
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
25
|
+
### Incorrect
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"name": "n8n-nodes-<PACKAGE_NAME>",
|
|
30
|
+
"description": "An n8n community node for {{service}}",
|
|
31
|
+
"homepage": "https://github.com/<USERNAME>/n8n-nodes-example#readme",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/<USERNAME>/n8n-nodes-example.git"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Correct
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"name": "n8n-nodes-acme",
|
|
44
|
+
"description": "An n8n community node for the Acme API",
|
|
45
|
+
"homepage": "https://github.com/acme/n8n-nodes-acme#readme",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/acme/n8n-nodes-acme.git"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Require { itemIndex } in NodeOperationError / NodeApiError options inside item loops (`@n8n/community-nodes/node-operation-error-itemindex`)
|
|
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 throwing `NodeOperationError` or `NodeApiError` inside the item-processing loop of an `execute()` method, the options object (third argument) must contain an `itemIndex` property. Without it, n8n cannot associate the error with the specific item that caused it, which breaks per-item error reporting and `continueOnFail` behaviour.
|
|
10
|
+
|
|
11
|
+
The rule only fires inside **item loops** — `for` or `for...of` statements that iterate over the result of `this.getInputData()`. Errors thrown outside such loops (e.g. in webhook handlers, trigger setup, or credential testing helpers) are not flagged.
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
### ❌ Incorrect
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
export class MyNode implements INodeType {
|
|
19
|
+
description: INodeTypeDescription = { /* ... */ };
|
|
20
|
+
|
|
21
|
+
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
22
|
+
const items = this.getInputData();
|
|
23
|
+
const returnData: INodeExecutionData[] = [];
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < items.length; i++) {
|
|
26
|
+
try {
|
|
27
|
+
// ...
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// Missing { itemIndex } — n8n cannot map this error back to item i
|
|
30
|
+
throw new NodeOperationError(this.getNode(), error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return [returnData];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### ✅ Correct
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
export class MyNode implements INodeType {
|
|
43
|
+
description: INodeTypeDescription = { /* ... */ };
|
|
44
|
+
|
|
45
|
+
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
46
|
+
const items = this.getInputData();
|
|
47
|
+
const returnData: INodeExecutionData[] = [];
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < items.length; i++) {
|
|
50
|
+
try {
|
|
51
|
+
// ...
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new NodeOperationError(this.getNode(), error, { itemIndex: i });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return [returnData];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Using `for...of` with a named loop variable:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
66
|
+
const items = this.getInputData();
|
|
67
|
+
const returnData: INodeExecutionData[] = [];
|
|
68
|
+
let itemIndex = 0;
|
|
69
|
+
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
try {
|
|
72
|
+
// ...
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new NodeApiError(this.getNode(), error, { itemIndex });
|
|
75
|
+
}
|
|
76
|
+
itemIndex++;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return [returnData];
|
|
80
|
+
}
|
|
81
|
+
```
|
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.17.0",
|
|
5
5
|
"main": "./dist/plugin.js",
|
|
6
6
|
"types": "./dist/plugin.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"rimraf": "6.0.1",
|
|
23
23
|
"typescript": "6.0.2",
|
|
24
24
|
"vitest": "^4.1.1",
|
|
25
|
+
"@n8n/vitest-config": "1.11.0",
|
|
25
26
|
"@n8n/typescript-config": "1.4.0",
|
|
26
|
-
"
|
|
27
|
-
"n8n-workflow": "2.20.0"
|
|
27
|
+
"n8n-workflow": "2.22.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"eslint": "
|
|
30
|
+
"eslint": "9.29.0",
|
|
31
31
|
"n8n-workflow": ">=2"
|
|
32
32
|
},
|
|
33
33
|
"eslint-doc-generator": {
|
package/src/plugin.ts
CHANGED
|
@@ -24,6 +24,7 @@ const configs = {
|
|
|
24
24
|
'@n8n/community-nodes/no-restricted-globals': 'error',
|
|
25
25
|
'@n8n/community-nodes/no-restricted-imports': 'error',
|
|
26
26
|
'@n8n/community-nodes/credential-password-field': 'error',
|
|
27
|
+
'@n8n/community-nodes/n8n-object-validation': 'error',
|
|
27
28
|
'@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
|
|
28
29
|
'@n8n/community-nodes/node-usable-as-tool': 'error',
|
|
29
30
|
'@n8n/community-nodes/package-name-convention': 'error',
|
|
@@ -33,13 +34,18 @@ const configs = {
|
|
|
33
34
|
'@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
|
|
34
35
|
'@n8n/community-nodes/no-overrides-field': 'error',
|
|
35
36
|
'@n8n/community-nodes/no-runtime-dependencies': 'error',
|
|
37
|
+
'@n8n/community-nodes/no-template-placeholders': 'error',
|
|
36
38
|
'@n8n/community-nodes/icon-validation': 'error',
|
|
37
39
|
'@n8n/community-nodes/options-sorted-alphabetically': 'warn',
|
|
38
40
|
'@n8n/community-nodes/resource-operation-pattern': 'warn',
|
|
39
41
|
'@n8n/community-nodes/credential-documentation-url': 'error',
|
|
40
42
|
'@n8n/community-nodes/cred-class-field-icon-missing': 'error',
|
|
43
|
+
'@n8n/community-nodes/cred-class-name-suffix': 'error',
|
|
44
|
+
'@n8n/community-nodes/cred-class-oauth2-naming': 'error',
|
|
41
45
|
'@n8n/community-nodes/node-connection-type-literal': 'error',
|
|
42
46
|
'@n8n/community-nodes/missing-paired-item': 'error',
|
|
47
|
+
'@n8n/community-nodes/no-builder-hint-leakage': 'error',
|
|
48
|
+
'@n8n/community-nodes/node-operation-error-itemindex': 'error',
|
|
43
49
|
'@n8n/community-nodes/require-community-node-keyword': 'warn',
|
|
44
50
|
'@n8n/community-nodes/require-continue-on-fail': 'error',
|
|
45
51
|
'@n8n/community-nodes/require-node-api-error': 'error',
|
|
@@ -57,6 +63,7 @@ const configs = {
|
|
|
57
63
|
rules: {
|
|
58
64
|
'@n8n/community-nodes/ai-node-package-json': 'error',
|
|
59
65
|
'@n8n/community-nodes/credential-password-field': 'error',
|
|
66
|
+
'@n8n/community-nodes/n8n-object-validation': 'error',
|
|
60
67
|
'@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
|
|
61
68
|
'@n8n/community-nodes/node-usable-as-tool': 'error',
|
|
62
69
|
'@n8n/community-nodes/package-name-convention': 'error',
|
|
@@ -66,13 +73,18 @@ const configs = {
|
|
|
66
73
|
'@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
|
|
67
74
|
'@n8n/community-nodes/no-overrides-field': 'error',
|
|
68
75
|
'@n8n/community-nodes/no-runtime-dependencies': 'error',
|
|
76
|
+
'@n8n/community-nodes/no-template-placeholders': 'error',
|
|
69
77
|
'@n8n/community-nodes/icon-validation': 'error',
|
|
70
78
|
'@n8n/community-nodes/options-sorted-alphabetically': 'warn',
|
|
71
79
|
'@n8n/community-nodes/credential-documentation-url': 'error',
|
|
72
80
|
'@n8n/community-nodes/resource-operation-pattern': 'warn',
|
|
73
81
|
'@n8n/community-nodes/cred-class-field-icon-missing': 'error',
|
|
82
|
+
'@n8n/community-nodes/cred-class-name-suffix': 'error',
|
|
83
|
+
'@n8n/community-nodes/cred-class-oauth2-naming': 'error',
|
|
74
84
|
'@n8n/community-nodes/node-connection-type-literal': 'error',
|
|
75
85
|
'@n8n/community-nodes/missing-paired-item': 'error',
|
|
86
|
+
'@n8n/community-nodes/no-builder-hint-leakage': 'error',
|
|
87
|
+
'@n8n/community-nodes/node-operation-error-itemindex': 'error',
|
|
76
88
|
'@n8n/community-nodes/require-community-node-keyword': 'warn',
|
|
77
89
|
'@n8n/community-nodes/require-continue-on-fail': 'error',
|
|
78
90
|
'@n8n/community-nodes/require-node-api-error': 'error',
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { CredClassNameSuffixRule } from './cred-class-name-suffix.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
const credFilePath = '/tmp/TestCredential.credentials.ts';
|
|
8
|
+
const nonCredFilePath = '/tmp/SomeHelper.ts';
|
|
9
|
+
|
|
10
|
+
function createCredentialCode(className: string): string {
|
|
11
|
+
return `
|
|
12
|
+
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
13
|
+
|
|
14
|
+
export class ${className} implements ICredentialType {
|
|
15
|
+
name = 'testApi';
|
|
16
|
+
displayName = 'Test API';
|
|
17
|
+
properties: INodeProperties[] = [];
|
|
18
|
+
}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createRegularClass(className: string): string {
|
|
22
|
+
return `
|
|
23
|
+
export class ${className} {
|
|
24
|
+
name = 'test';
|
|
25
|
+
}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
ruleTester.run('cred-class-name-suffix', CredClassNameSuffixRule, {
|
|
29
|
+
valid: [
|
|
30
|
+
{
|
|
31
|
+
name: 'credential class with Api suffix',
|
|
32
|
+
filename: credFilePath,
|
|
33
|
+
code: createCredentialCode('TestApi'),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'credential class with OAuth2Api suffix',
|
|
37
|
+
filename: credFilePath,
|
|
38
|
+
code: createCredentialCode('TestOAuth2Api'),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'class not implementing ICredentialType is ignored',
|
|
42
|
+
filename: credFilePath,
|
|
43
|
+
code: createRegularClass('SomeHelper'),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'non-.credentials.ts file is ignored',
|
|
47
|
+
filename: nonCredFilePath,
|
|
48
|
+
code: createCredentialCode('TestCredential'),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
invalid: [
|
|
52
|
+
{
|
|
53
|
+
name: 'credential class missing Api suffix',
|
|
54
|
+
filename: credFilePath,
|
|
55
|
+
code: createCredentialCode('TestCredential'),
|
|
56
|
+
errors: [{ messageId: 'missingSuffix', data: { name: 'TestCredential' } }],
|
|
57
|
+
output: createCredentialCode('TestCredentialApi'),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'credential class name ending in Ap',
|
|
61
|
+
filename: credFilePath,
|
|
62
|
+
code: createCredentialCode('TestAp'),
|
|
63
|
+
errors: [{ messageId: 'missingSuffix', data: { name: 'TestAp' } }],
|
|
64
|
+
output: createCredentialCode('TestApi'),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'credential class name ending in A',
|
|
68
|
+
filename: credFilePath,
|
|
69
|
+
code: createCredentialCode('TestA'),
|
|
70
|
+
errors: [{ messageId: 'missingSuffix', data: { name: 'TestA' } }],
|
|
71
|
+
output: createCredentialCode('TestApi'),
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isCredentialTypeClass, isFileType, createRule } from '../utils/index.js';
|
|
2
|
+
|
|
3
|
+
function addApiSuffix(name: string): string {
|
|
4
|
+
if (name.endsWith('Ap')) return `${name}i`;
|
|
5
|
+
if (name.endsWith('A')) return `${name}pi`;
|
|
6
|
+
return `${name}Api`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const CredClassNameSuffixRule = createRule({
|
|
10
|
+
name: 'cred-class-name-suffix',
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'problem',
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'Credential class names must be suffixed with `Api`',
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
missingSuffix: "Credential class name '{{name}}' must end with 'Api'",
|
|
18
|
+
},
|
|
19
|
+
fixable: 'code',
|
|
20
|
+
schema: [],
|
|
21
|
+
},
|
|
22
|
+
defaultOptions: [],
|
|
23
|
+
create(context) {
|
|
24
|
+
if (!isFileType(context.filename, '.credentials.ts')) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
ClassDeclaration(node) {
|
|
30
|
+
if (!isCredentialTypeClass(node)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const classNameNode = node.id;
|
|
35
|
+
if (!classNameNode) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const className = classNameNode.name;
|
|
40
|
+
if (className.endsWith('Api')) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const fixedName = addApiSuffix(className);
|
|
45
|
+
|
|
46
|
+
context.report({
|
|
47
|
+
node: classNameNode,
|
|
48
|
+
messageId: 'missingSuffix',
|
|
49
|
+
data: { name: className },
|
|
50
|
+
fix(fixer) {
|
|
51
|
+
return fixer.replaceText(classNameNode, fixedName);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
});
|