@n8n/eslint-plugin-community-nodes 0.20.0 → 0.22.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 +47 -38
- package/dist/plugin.d.ts +54 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +18 -0
- package/dist/plugin.js.map +1 -1
- package/dist/rules/ai-node-package-json.d.ts.map +1 -1
- package/dist/rules/ai-node-package-json.js +4 -3
- package/dist/rules/ai-node-package-json.js.map +1 -1
- package/dist/rules/cred-filename-against-convention.d.ts +2 -0
- package/dist/rules/cred-filename-against-convention.d.ts.map +1 -0
- package/dist/rules/cred-filename-against-convention.js +42 -0
- package/dist/rules/cred-filename-against-convention.js.map +1 -0
- package/dist/rules/icon-prefer-themed-variants.d.ts +2 -0
- package/dist/rules/icon-prefer-themed-variants.d.ts.map +1 -0
- package/dist/rules/icon-prefer-themed-variants.js +53 -0
- package/dist/rules/icon-prefer-themed-variants.js.map +1 -0
- package/dist/rules/icon-validation.d.ts +1 -1
- package/dist/rules/icon-validation.d.ts.map +1 -1
- package/dist/rules/icon-validation.js +4 -25
- package/dist/rules/icon-validation.js.map +1 -1
- package/dist/rules/index.d.ts +10 -1
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +18 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/no-dangerous-functions.d.ts +2 -0
- package/dist/rules/no-dangerous-functions.d.ts.map +1 -0
- package/dist/rules/no-dangerous-functions.js +121 -0
- package/dist/rules/no-dangerous-functions.js.map +1 -0
- package/dist/rules/no-emoji-in-options.d.ts +2 -0
- package/dist/rules/no-emoji-in-options.d.ts.map +1 -0
- package/dist/rules/no-emoji-in-options.js +86 -0
- package/dist/rules/no-emoji-in-options.js.map +1 -0
- package/dist/rules/node-class-description-name-camelcase.d.ts +2 -0
- package/dist/rules/node-class-description-name-camelcase.d.ts.map +1 -0
- package/dist/rules/node-class-description-name-camelcase.js +92 -0
- package/dist/rules/node-class-description-name-camelcase.js.map +1 -0
- package/dist/rules/node-filename-against-convention.d.ts +2 -0
- package/dist/rules/node-filename-against-convention.d.ts.map +1 -0
- package/dist/rules/node-filename-against-convention.js +61 -0
- package/dist/rules/node-filename-against-convention.js.map +1 -0
- package/dist/rules/node-registration-complete.d.ts +2 -0
- package/dist/rules/node-registration-complete.d.ts.map +1 -0
- package/dist/rules/node-registration-complete.js +60 -0
- package/dist/rules/node-registration-complete.js.map +1 -0
- package/dist/rules/require-version.d.ts +2 -0
- package/dist/rules/require-version.d.ts.map +1 -0
- package/dist/rules/require-version.js +50 -0
- package/dist/rules/require-version.js.map +1 -0
- package/dist/rules/valid-author.d.ts +2 -0
- package/dist/rules/valid-author.d.ts.map +1 -0
- package/dist/rules/valid-author.js +89 -0
- package/dist/rules/valid-author.js.map +1 -0
- package/dist/rules/valid-peer-dependencies.d.ts.map +1 -1
- package/dist/rules/valid-peer-dependencies.js +5 -3
- package/dist/rules/valid-peer-dependencies.js.map +1 -1
- package/dist/typecheck.tsbuildinfo +1 -0
- package/dist/utils/file-utils.d.ts +7 -2
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +47 -10
- package/dist/utils/file-utils.js.map +1 -1
- package/docs/rules/cred-filename-against-convention.md +42 -0
- package/docs/rules/icon-prefer-themed-variants.md +71 -0
- package/docs/rules/icon-validation.md +5 -3
- package/docs/rules/no-dangerous-functions.md +41 -0
- package/docs/rules/no-emoji-in-options.md +60 -0
- package/docs/rules/node-class-description-name-camelcase.md +41 -0
- package/docs/rules/node-filename-against-convention.md +50 -0
- package/docs/rules/node-registration-complete.md +46 -0
- package/docs/rules/require-version.md +51 -0
- package/docs/rules/valid-author.md +60 -0
- package/docs/rules/valid-peer-dependencies.md +1 -1
- package/package.json +4 -4
- package/src/plugin.ts +18 -0
- package/src/rules/ai-node-package-json.test.ts +5 -0
- package/src/rules/ai-node-package-json.ts +4 -3
- package/src/rules/cred-filename-against-convention.test.ts +72 -0
- package/src/rules/cred-filename-against-convention.ts +48 -0
- package/src/rules/icon-prefer-themed-variants.test.ts +128 -0
- package/src/rules/icon-prefer-themed-variants.ts +70 -0
- package/src/rules/icon-validation.test.ts +10 -0
- package/src/rules/icon-validation.ts +4 -28
- package/src/rules/index.ts +18 -0
- package/src/rules/no-dangerous-functions.test.ts +83 -0
- package/src/rules/no-dangerous-functions.ts +155 -0
- package/src/rules/no-emoji-in-options.test.ts +157 -0
- package/src/rules/no-emoji-in-options.ts +105 -0
- package/src/rules/node-class-description-name-camelcase.test.ts +121 -0
- package/src/rules/node-class-description-name-camelcase.ts +114 -0
- package/src/rules/node-filename-against-convention.test.ts +115 -0
- package/src/rules/node-filename-against-convention.ts +76 -0
- package/src/rules/node-registration-complete.test.ts +87 -0
- package/src/rules/node-registration-complete.ts +79 -0
- package/src/rules/require-version.test.ts +90 -0
- package/src/rules/require-version.ts +62 -0
- package/src/rules/valid-author.test.ts +108 -0
- package/src/rules/valid-author.ts +100 -0
- package/src/rules/valid-peer-dependencies.test.ts +5 -0
- package/src/rules/valid-peer-dependencies.ts +5 -3
- package/src/utils/file-utils.ts +58 -11
- package/tsconfig.build.tsbuildinfo +0 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Disallow `eval`, the `Function` constructor, and `child_process` process-spawning functions (`exec`, `spawn`, etc.) in community nodes (`@n8n/community-nodes/no-dangerous-functions`)
|
|
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 nodes run inside the n8n runtime, often on shared infrastructure. Functions that execute arbitrary code from strings or spawn operating-system processes are a primary vector for remote code execution and command injection, and have no legitimate use in a community node. This rule bans them outright:
|
|
10
|
+
|
|
11
|
+
- **`eval(...)`** — executes arbitrary code from a string.
|
|
12
|
+
- **`Function(...)` / `new Function(...)`** — the `Function` constructor is an `eval` equivalent that builds a callable from a string body.
|
|
13
|
+
- **`child_process` process spawners** — `exec`, `execSync`, `execFile`, `execFileSync`, `spawn`, `spawnSync`, and `fork`.
|
|
14
|
+
|
|
15
|
+
The `child_process` functions are detected only when they originate from the `child_process` / `node:child_process` module (via `import` or `require`), so unrelated methods such as `RegExp.prototype.exec` are not affected.
|
|
16
|
+
|
|
17
|
+
This complements [`no-restricted-imports`](no-restricted-imports.md) (which blocks the `child_process` module entirely on n8n Cloud) and [`no-restricted-globals`](no-restricted-globals.md), providing a clear, specific error and defense-in-depth that also applies when the import restrictions are relaxed.
|
|
18
|
+
|
|
19
|
+
## Examples
|
|
20
|
+
|
|
21
|
+
### ❌ Incorrect
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { exec } from 'child_process';
|
|
25
|
+
|
|
26
|
+
eval(userProvidedCode);
|
|
27
|
+
|
|
28
|
+
const compiled = new Function('return ' + expression);
|
|
29
|
+
|
|
30
|
+
exec(`rm -rf ${userInput}`);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### ✅ Correct
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// Parse data instead of evaluating it.
|
|
37
|
+
const value = JSON.parse(rawJson);
|
|
38
|
+
|
|
39
|
+
// Use n8n helpers and well-scoped library APIs instead of spawning processes.
|
|
40
|
+
const response = await this.helpers.httpRequest({ url });
|
|
41
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Disallow emoji characters in node option name and displayName values (`@n8n/community-nodes/no-emoji-in-options`)
|
|
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
|
+
User-facing labels in a node — the node's `displayName`, each property's
|
|
10
|
+
`displayName`, and the `name` of entries in `options` arrays — are rendered
|
|
11
|
+
directly in the n8n editor. Emoji in these labels render inconsistently across
|
|
12
|
+
platforms, break alphabetical sorting and search, and clash with the editor's
|
|
13
|
+
visual language. This rule flags any emoji character (pictographs such as 🚀 or
|
|
14
|
+
✅, and regional-indicator flag emoji such as 🇺🇸) found in a `name` or
|
|
15
|
+
`displayName` string within a node's `description`.
|
|
16
|
+
|
|
17
|
+
Accented and non-Latin characters (e.g. `Café`, `日本語`) are allowed — only
|
|
18
|
+
emoji are reported.
|
|
19
|
+
|
|
20
|
+
## Examples
|
|
21
|
+
|
|
22
|
+
### Incorrect
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
export class MyNode implements INodeType {
|
|
26
|
+
description: INodeTypeDescription = {
|
|
27
|
+
displayName: '🚀 My Node',
|
|
28
|
+
name: 'myNode',
|
|
29
|
+
properties: [
|
|
30
|
+
{
|
|
31
|
+
displayName: 'Operation',
|
|
32
|
+
name: 'operation',
|
|
33
|
+
type: 'options',
|
|
34
|
+
options: [{ name: '✅ Create', value: 'create' }],
|
|
35
|
+
default: 'create',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Correct
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
export class MyNode implements INodeType {
|
|
46
|
+
description: INodeTypeDescription = {
|
|
47
|
+
displayName: 'My Node',
|
|
48
|
+
name: 'myNode',
|
|
49
|
+
properties: [
|
|
50
|
+
{
|
|
51
|
+
displayName: 'Operation',
|
|
52
|
+
name: 'operation',
|
|
53
|
+
type: 'options',
|
|
54
|
+
options: [{ name: 'Create', value: 'create' }],
|
|
55
|
+
default: 'create',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Node class `description.name` must be camelCase (`@n8n/community-nodes/node-class-description-name-camelcase`)
|
|
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
|
+
The `name` field inside a node class `description` (those implementing `INodeType` or extending `Node` in `*.node.ts` files) is the internal identifier used to register and reference the node. n8n convention requires this identifier to be camelCase: it must start with a lowercase letter and contain only letters and digits, with no spaces, hyphens, underscores, or other separators.
|
|
12
|
+
|
|
13
|
+
The value must match `^[a-z][a-zA-Z0-9]*$`.
|
|
14
|
+
|
|
15
|
+
The rule is automatically fixable: separators are removed, each subsequent word is upper-cased, and the first character is lower-cased (e.g. `My Node` → `myNode`). Names that cannot be repaired into a valid identifier (for example, those starting with a digit) are reported without an autofix.
|
|
16
|
+
|
|
17
|
+
## Examples
|
|
18
|
+
|
|
19
|
+
### ❌ Incorrect
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
export class MyNode implements INodeType {
|
|
23
|
+
description: INodeTypeDescription = {
|
|
24
|
+
displayName: 'My Node',
|
|
25
|
+
name: 'My Node',
|
|
26
|
+
// ...
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### ✅ Correct
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
export class MyNode implements INodeType {
|
|
35
|
+
description: INodeTypeDescription = {
|
|
36
|
+
displayName: 'My Node',
|
|
37
|
+
name: 'myNode',
|
|
38
|
+
// ...
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Node filename must match the node `description.name` (`@n8n/community-nodes/node-filename-against-convention`)
|
|
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.ts` file must be named after its node's `description.name`, with the
|
|
10
|
+
first character upper-cased. The source of truth is `description.name` — **not**
|
|
11
|
+
the class name. Keeping the filename in sync with the registered node name makes
|
|
12
|
+
nodes predictable to locate and matches the convention n8n tooling expects.
|
|
13
|
+
|
|
14
|
+
A trailing version suffix (`V2`, `V3`, …) is allowed, so versioned implementations
|
|
15
|
+
such as `GithubV2.node.ts` are accepted for a node whose `description.name` is
|
|
16
|
+
`github`.
|
|
17
|
+
|
|
18
|
+
The comparison is case-sensitive: a node named `github` must live in
|
|
19
|
+
`Github.node.ts`, not `github.node.ts` or `GitHub.node.ts`.
|
|
20
|
+
|
|
21
|
+
This rule is not auto-fixable because ESLint cannot rename files. Rename the file
|
|
22
|
+
to the reported name.
|
|
23
|
+
|
|
24
|
+
## Examples
|
|
25
|
+
|
|
26
|
+
### ❌ Incorrect
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// File: Gitlab.node.ts
|
|
30
|
+
export class Github implements INodeType {
|
|
31
|
+
description: INodeTypeDescription = {
|
|
32
|
+
displayName: 'GitHub',
|
|
33
|
+
name: 'github', // expected file to be named "Github.node.ts"
|
|
34
|
+
// ...
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### ✅ Correct
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// File: Github.node.ts (or GithubV2.node.ts)
|
|
43
|
+
export class Github implements INodeType {
|
|
44
|
+
description: INodeTypeDescription = {
|
|
45
|
+
displayName: 'GitHub',
|
|
46
|
+
name: 'github',
|
|
47
|
+
// ...
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Ensure every `.node.ts` file in the `nodes/` directory is registered in the "n8n.nodes" array of package.json (`@n8n/community-nodes/node-registration-complete`)
|
|
2
|
+
|
|
3
|
+
⚠️ This rule _warns_ in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
|
|
4
|
+
|
|
5
|
+
<!-- end auto-generated rule header -->
|
|
6
|
+
|
|
7
|
+
## Rule Details
|
|
8
|
+
|
|
9
|
+
n8n discovers the nodes in a community package from the `n8n.nodes` array in `package.json`. Any `.node.ts` file that exists in the package's `nodes/` directory but is missing from that array will be silently excluded from the published package — the node simply won't show up in n8n.
|
|
10
|
+
|
|
11
|
+
This rule cross-references the `.node.ts` files found in the `nodes/` directory against the entries in `n8n.nodes` and flags every node file that is not registered, so missing registrations are caught at lint time rather than after publishing.
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
Given a package with these files on disk:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
nodes/Foo/Foo.node.ts
|
|
19
|
+
nodes/Bar/Bar.node.ts
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### ❌ Incorrect
|
|
23
|
+
|
|
24
|
+
`Bar.node.ts` exists but is not registered:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"name": "n8n-nodes-my-service",
|
|
29
|
+
"n8n": {
|
|
30
|
+
"nodes": ["dist/nodes/Foo/Foo.node.js"]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### ✅ Correct
|
|
36
|
+
|
|
37
|
+
Every node file is registered:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"name": "n8n-nodes-my-service",
|
|
42
|
+
"n8n": {
|
|
43
|
+
"nodes": ["dist/nodes/Foo/Foo.node.js", "dist/nodes/Bar/Bar.node.js"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Require a valid "version" field in community node package.json (`@n8n/community-nodes/require-version`)
|
|
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 must declare a `version` field in its `package.json`. npm refuses to publish a package without a valid [semantic version](https://semver.org/), and n8n relies on the version to track and update installed community packages.
|
|
10
|
+
|
|
11
|
+
This rule reports a missing `version` key as well as a value that is not a valid semantic version string (for example `"1.0"`, `"v1.0.0"`, a range like `"^1.0.0"`, an empty string, or a non-string value).
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
### ❌ Incorrect
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"name": "n8n-nodes-acme"
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"name": "n8n-nodes-acme",
|
|
26
|
+
"version": "1.0"
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"name": "n8n-nodes-acme",
|
|
33
|
+
"version": "v1.0.0"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### ✅ Correct
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"name": "n8n-nodes-acme",
|
|
42
|
+
"version": "1.0.0"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"name": "n8n-nodes-acme",
|
|
49
|
+
"version": "0.1.0-beta.1"
|
|
50
|
+
}
|
|
51
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Require a non-empty author name and email in package.json (`@n8n/community-nodes/valid-author`)
|
|
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
|
+
A published community node package should identify its author. This rule
|
|
10
|
+
requires the `package.json` to declare an `author` with at minimum a non-empty
|
|
11
|
+
`name` and `email`, so users and n8n can reach the maintainer.
|
|
12
|
+
|
|
13
|
+
Both npm `author` forms are supported:
|
|
14
|
+
|
|
15
|
+
- the object form `{ "name": "...", "email": "..." }`
|
|
16
|
+
- the shorthand string form `"Name <email> (url)"`
|
|
17
|
+
|
|
18
|
+
Whitespace-only values count as empty. The literal `<...>` placeholder pattern
|
|
19
|
+
left over from the node starter template is handled separately by the
|
|
20
|
+
[`no-template-placeholders`](./no-template-placeholders.md) rule.
|
|
21
|
+
|
|
22
|
+
## Examples
|
|
23
|
+
|
|
24
|
+
### ❌ Incorrect
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"name": "n8n-nodes-my-service"
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"name": "n8n-nodes-my-service",
|
|
35
|
+
"author": { "name": "Jane Doe" }
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"name": "n8n-nodes-my-service",
|
|
42
|
+
"author": "Jane Doe"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### ✅ Correct
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"name": "n8n-nodes-my-service",
|
|
51
|
+
"author": { "name": "Jane Doe", "email": "jane@example.com" }
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"name": "n8n-nodes-my-service",
|
|
58
|
+
"author": "Jane Doe <jane@example.com> (https://example.com)"
|
|
59
|
+
}
|
|
60
|
+
```
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Require community node package.json peerDependencies to contain only "n8n-workflow": "*" (and optionally "ai-node-sdk") (`@n8n/community-nodes/valid-peer-dependencies`)
|
|
1
|
+
# Require community node package.json peerDependencies to contain only "n8n-workflow": "*" (and optionally "@n8n/ai-node-sdk") (`@n8n/community-nodes/valid-peer-dependencies`)
|
|
2
2
|
|
|
3
3
|
💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
|
|
4
4
|
|
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.22.0",
|
|
5
5
|
"main": "./dist/plugin.js",
|
|
6
6
|
"types": "./dist/plugin.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"rimraf": "6.0.1",
|
|
24
24
|
"typescript": "6.0.2",
|
|
25
25
|
"vitest": "^4.1.1",
|
|
26
|
-
"@n8n/typescript-config": "1.
|
|
27
|
-
"@n8n/vitest-config": "1.
|
|
28
|
-
"n8n-workflow": "2.
|
|
26
|
+
"@n8n/typescript-config": "1.6.0",
|
|
27
|
+
"@n8n/vitest-config": "1.15.0",
|
|
28
|
+
"n8n-workflow": "2.28.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"eslint": "9.29.0",
|
package/src/plugin.ts
CHANGED
|
@@ -26,16 +26,19 @@ const configs = {
|
|
|
26
26
|
'@n8n/community-nodes/credential-password-field': 'error',
|
|
27
27
|
'@n8n/community-nodes/n8n-object-validation': 'error',
|
|
28
28
|
'@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
|
|
29
|
+
'@n8n/community-nodes/no-emoji-in-options': 'error',
|
|
29
30
|
'@n8n/community-nodes/node-usable-as-tool': 'error',
|
|
30
31
|
'@n8n/community-nodes/package-name-convention': 'error',
|
|
31
32
|
'@n8n/community-nodes/credential-test-required': 'error',
|
|
32
33
|
'@n8n/community-nodes/no-credential-reuse': 'error',
|
|
34
|
+
'@n8n/community-nodes/no-dangerous-functions': 'error',
|
|
33
35
|
'@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error',
|
|
34
36
|
'@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
|
|
35
37
|
'@n8n/community-nodes/no-overrides-field': 'error',
|
|
36
38
|
'@n8n/community-nodes/no-runtime-dependencies': 'error',
|
|
37
39
|
'@n8n/community-nodes/no-template-placeholders': 'error',
|
|
38
40
|
'@n8n/community-nodes/icon-validation': 'error',
|
|
41
|
+
'@n8n/community-nodes/icon-prefer-themed-variants': 'warn',
|
|
39
42
|
'@n8n/community-nodes/options-sorted-alphabetically': 'warn',
|
|
40
43
|
'@n8n/community-nodes/resource-operation-pattern': 'warn',
|
|
41
44
|
'@n8n/community-nodes/credential-documentation-url': 'error',
|
|
@@ -43,14 +46,20 @@ const configs = {
|
|
|
43
46
|
'@n8n/community-nodes/cred-class-name-field-conventions': 'error',
|
|
44
47
|
'@n8n/community-nodes/cred-class-name-suffix': 'error',
|
|
45
48
|
'@n8n/community-nodes/cred-class-oauth2-naming': 'error',
|
|
49
|
+
'@n8n/community-nodes/cred-filename-against-convention': 'error',
|
|
46
50
|
'@n8n/community-nodes/node-connection-type-literal': 'error',
|
|
51
|
+
'@n8n/community-nodes/node-class-description-name-camelcase': 'error',
|
|
52
|
+
'@n8n/community-nodes/node-filename-against-convention': 'error',
|
|
47
53
|
'@n8n/community-nodes/missing-paired-item': 'error',
|
|
48
54
|
'@n8n/community-nodes/no-builder-hint-leakage': 'error',
|
|
49
55
|
'@n8n/community-nodes/node-operation-error-itemindex': 'error',
|
|
56
|
+
'@n8n/community-nodes/node-registration-complete': 'warn',
|
|
50
57
|
'@n8n/community-nodes/require-community-node-keyword': 'warn',
|
|
51
58
|
'@n8n/community-nodes/require-continue-on-fail': 'error',
|
|
52
59
|
'@n8n/community-nodes/require-node-api-error': 'error',
|
|
53
60
|
'@n8n/community-nodes/require-node-description-fields': 'error',
|
|
61
|
+
'@n8n/community-nodes/require-version': 'error',
|
|
62
|
+
'@n8n/community-nodes/valid-author': 'error',
|
|
54
63
|
'@n8n/community-nodes/valid-credential-references': 'error',
|
|
55
64
|
'@n8n/community-nodes/valid-description': 'error',
|
|
56
65
|
'@n8n/community-nodes/valid-peer-dependencies': 'error',
|
|
@@ -67,16 +76,19 @@ const configs = {
|
|
|
67
76
|
'@n8n/community-nodes/credential-password-field': 'error',
|
|
68
77
|
'@n8n/community-nodes/n8n-object-validation': 'error',
|
|
69
78
|
'@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
|
|
79
|
+
'@n8n/community-nodes/no-emoji-in-options': 'error',
|
|
70
80
|
'@n8n/community-nodes/node-usable-as-tool': 'error',
|
|
71
81
|
'@n8n/community-nodes/package-name-convention': 'error',
|
|
72
82
|
'@n8n/community-nodes/credential-test-required': 'error',
|
|
73
83
|
'@n8n/community-nodes/no-credential-reuse': 'error',
|
|
84
|
+
'@n8n/community-nodes/no-dangerous-functions': 'error',
|
|
74
85
|
'@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error',
|
|
75
86
|
'@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
|
|
76
87
|
'@n8n/community-nodes/no-overrides-field': 'error',
|
|
77
88
|
'@n8n/community-nodes/no-runtime-dependencies': 'error',
|
|
78
89
|
'@n8n/community-nodes/no-template-placeholders': 'error',
|
|
79
90
|
'@n8n/community-nodes/icon-validation': 'error',
|
|
91
|
+
'@n8n/community-nodes/icon-prefer-themed-variants': 'warn',
|
|
80
92
|
'@n8n/community-nodes/options-sorted-alphabetically': 'warn',
|
|
81
93
|
'@n8n/community-nodes/credential-documentation-url': 'error',
|
|
82
94
|
'@n8n/community-nodes/resource-operation-pattern': 'warn',
|
|
@@ -84,14 +96,20 @@ const configs = {
|
|
|
84
96
|
'@n8n/community-nodes/cred-class-name-field-conventions': 'error',
|
|
85
97
|
'@n8n/community-nodes/cred-class-name-suffix': 'error',
|
|
86
98
|
'@n8n/community-nodes/cred-class-oauth2-naming': 'error',
|
|
99
|
+
'@n8n/community-nodes/cred-filename-against-convention': 'error',
|
|
87
100
|
'@n8n/community-nodes/node-connection-type-literal': 'error',
|
|
101
|
+
'@n8n/community-nodes/node-class-description-name-camelcase': 'error',
|
|
102
|
+
'@n8n/community-nodes/node-filename-against-convention': 'error',
|
|
88
103
|
'@n8n/community-nodes/missing-paired-item': 'error',
|
|
89
104
|
'@n8n/community-nodes/no-builder-hint-leakage': 'error',
|
|
90
105
|
'@n8n/community-nodes/node-operation-error-itemindex': 'error',
|
|
106
|
+
'@n8n/community-nodes/node-registration-complete': 'warn',
|
|
91
107
|
'@n8n/community-nodes/require-community-node-keyword': 'warn',
|
|
92
108
|
'@n8n/community-nodes/require-continue-on-fail': 'error',
|
|
93
109
|
'@n8n/community-nodes/require-node-api-error': 'error',
|
|
94
110
|
'@n8n/community-nodes/require-node-description-fields': 'error',
|
|
111
|
+
'@n8n/community-nodes/require-version': 'error',
|
|
112
|
+
'@n8n/community-nodes/valid-author': 'error',
|
|
95
113
|
'@n8n/community-nodes/valid-credential-references': 'error',
|
|
96
114
|
'@n8n/community-nodes/valid-description': 'error',
|
|
97
115
|
'@n8n/community-nodes/valid-peer-dependencies': 'error',
|
|
@@ -11,6 +11,11 @@ ruleTester.run('ai-node-package-json', AiNodePackageJsonRule, {
|
|
|
11
11
|
filename: 'package.json',
|
|
12
12
|
code: '{ "name": "n8n-nodes-example", "n8n": { "aiNodeSdkVersion": 1 }, "peerDependencies": { "n8n-workflow": "*", "ai-node-sdk": "*" } }',
|
|
13
13
|
},
|
|
14
|
+
{
|
|
15
|
+
name: 'both n8n.aiNodeSdkVersion and scoped @n8n/ai-node-sdk peer dependency present',
|
|
16
|
+
filename: 'package.json',
|
|
17
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "aiNodeSdkVersion": 1 }, "peerDependencies": { "n8n-workflow": "*", "@n8n/ai-node-sdk": "*" } }',
|
|
18
|
+
},
|
|
14
19
|
{
|
|
15
20
|
name: 'neither n8n.aiNodeSdkVersion nor ai-node-sdk present (non-AI package)',
|
|
16
21
|
filename: 'package.json',
|
|
@@ -13,9 +13,9 @@ export const AiNodePackageJsonRule = createRule({
|
|
|
13
13
|
},
|
|
14
14
|
messages: {
|
|
15
15
|
missingPeerDep:
|
|
16
|
-
'Package declares "n8n.aiNodeSdkVersion" but is missing "ai-node-sdk" in peerDependencies. Add "ai-node-sdk": "*" to peerDependencies.',
|
|
16
|
+
'Package declares "n8n.aiNodeSdkVersion" but is missing "@n8n/ai-node-sdk" in peerDependencies. Add "@n8n/ai-node-sdk": "*" to peerDependencies.',
|
|
17
17
|
missingSdkVersion:
|
|
18
|
-
'Package has "ai-node-sdk" in peerDependencies but is missing "aiNodeSdkVersion" in the "n8n" section of package.json.',
|
|
18
|
+
'Package has "@n8n/ai-node-sdk" in peerDependencies but is missing "aiNodeSdkVersion" in the "n8n" section of package.json.',
|
|
19
19
|
invalidSdkVersion: '"n8n.aiNodeSdkVersion" must be a positive integer, got {{ value }}.',
|
|
20
20
|
wrongLocation:
|
|
21
21
|
'"aiNodeSdkVersion" must be inside the "n8n" section, not at the root level of package.json.',
|
|
@@ -49,7 +49,8 @@ export const AiNodePackageJsonRule = createRule({
|
|
|
49
49
|
const hasAiNodeSdkVersion = aiNodeSdkVersionProp !== null;
|
|
50
50
|
const hasAiNodeSdkPeerDep =
|
|
51
51
|
peerDependenciesProp?.value.type === AST_NODE_TYPES.ObjectExpression &&
|
|
52
|
-
findJsonProperty(peerDependenciesProp.value, 'ai-node-sdk') !== null
|
|
52
|
+
(findJsonProperty(peerDependenciesProp.value, '@n8n/ai-node-sdk') !== null ||
|
|
53
|
+
findJsonProperty(peerDependenciesProp.value, 'ai-node-sdk') !== null);
|
|
53
54
|
|
|
54
55
|
// Catch aiNodeSdkVersion placed at root level instead of inside n8n
|
|
55
56
|
if (rootAiNodeSdkVersionProp) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { CredFilenameAgainstConventionRule } from './cred-filename-against-convention.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
function createCredentialCode(className: string): string {
|
|
8
|
+
return `
|
|
9
|
+
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
10
|
+
|
|
11
|
+
export class ${className} implements ICredentialType {
|
|
12
|
+
name = '${className.charAt(0).toLowerCase() + className.slice(1)}';
|
|
13
|
+
displayName = '${className}';
|
|
14
|
+
properties: INodeProperties[] = [];
|
|
15
|
+
}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createRegularClass(): string {
|
|
19
|
+
return `
|
|
20
|
+
export class SomeHelper {
|
|
21
|
+
name = 'githubApi';
|
|
22
|
+
}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
ruleTester.run('cred-filename-against-convention', CredFilenameAgainstConventionRule, {
|
|
26
|
+
valid: [
|
|
27
|
+
{
|
|
28
|
+
name: 'filename matches credential class name',
|
|
29
|
+
filename: '/tmp/GithubApi.credentials.ts',
|
|
30
|
+
code: createCredentialCode('GithubApi'),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'multi-word class name matches filename',
|
|
34
|
+
filename: '/tmp/GoogleSheetsOAuth2Api.credentials.ts',
|
|
35
|
+
code: createCredentialCode('GoogleSheetsOAuth2Api'),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'class not implementing ICredentialType is ignored',
|
|
39
|
+
filename: '/tmp/Github.credentials.ts',
|
|
40
|
+
code: createRegularClass(),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'non-.credentials.ts file is ignored',
|
|
44
|
+
filename: '/tmp/GithubApi.ts',
|
|
45
|
+
code: createCredentialCode('Mismatch'),
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
invalid: [
|
|
49
|
+
{
|
|
50
|
+
name: 'filename does not match class name',
|
|
51
|
+
filename: '/tmp/GithubApi.credentials.ts',
|
|
52
|
+
code: createCredentialCode('GitlabApi'),
|
|
53
|
+
errors: [
|
|
54
|
+
{
|
|
55
|
+
messageId: 'renameFile',
|
|
56
|
+
data: { className: 'GitlabApi', expected: 'GitlabApi.credentials.ts' },
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'filename has wrong casing',
|
|
62
|
+
filename: '/tmp/githubApi.credentials.ts',
|
|
63
|
+
code: createCredentialCode('GithubApi'),
|
|
64
|
+
errors: [
|
|
65
|
+
{
|
|
66
|
+
messageId: 'renameFile',
|
|
67
|
+
data: { className: 'GithubApi', expected: 'GithubApi.credentials.ts' },
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { isCredentialTypeClass, isFileType, createRule } from '../utils/index.js';
|
|
4
|
+
|
|
5
|
+
export const CredFilenameAgainstConventionRule = createRule({
|
|
6
|
+
name: 'cred-filename-against-convention',
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'problem',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Credential filename must match the credential class name',
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
renameFile:
|
|
14
|
+
'Credential filename must match the class name "{{className}}". Rename file to "{{expected}}".',
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
defaultOptions: [],
|
|
19
|
+
create(context) {
|
|
20
|
+
if (!isFileType(context.filename, '.credentials.ts')) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
ClassDeclaration(node) {
|
|
26
|
+
if (!isCredentialTypeClass(node)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const classNameNode = node.id;
|
|
31
|
+
if (!classNameNode) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const className = classNameNode.name;
|
|
36
|
+
const actualBaseName = path.basename(context.filename, '.credentials.ts');
|
|
37
|
+
|
|
38
|
+
if (actualBaseName !== className) {
|
|
39
|
+
context.report({
|
|
40
|
+
node: classNameNode,
|
|
41
|
+
messageId: 'renameFile',
|
|
42
|
+
data: { className, expected: `${className}.credentials.ts` },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|