@n8n/eslint-plugin-community-nodes 0.12.0 → 0.14.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 +30 -23
- package/dist/plugin.d.ts +36 -6
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +12 -2
- package/dist/plugin.js.map +1 -1
- package/dist/rules/index.d.ts +6 -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/no-overrides-field.d.ts +2 -0
- package/dist/rules/no-overrides-field.d.ts.map +1 -0
- package/dist/rules/no-overrides-field.js +37 -0
- package/dist/rules/no-overrides-field.js.map +1 -0
- package/dist/rules/node-class-description-icon-missing.d.ts +1 -0
- package/dist/rules/node-class-description-icon-missing.d.ts.map +1 -1
- package/dist/rules/node-class-description-icon-missing.js +6 -1
- package/dist/rules/node-class-description-icon-missing.js.map +1 -1
- package/dist/rules/require-community-node-keyword.d.ts +2 -0
- package/dist/rules/require-community-node-keyword.d.ts.map +1 -0
- package/dist/rules/require-community-node-keyword.js +65 -0
- package/dist/rules/require-community-node-keyword.js.map +1 -0
- package/dist/rules/require-node-api-error.d.ts +2 -0
- package/dist/rules/require-node-api-error.d.ts.map +1 -0
- package/dist/rules/require-node-api-error.js +77 -0
- package/dist/rules/require-node-api-error.js.map +1 -0
- package/dist/rules/require-node-description-fields.d.ts +2 -0
- package/dist/rules/require-node-description-fields.d.ts.map +1 -0
- package/dist/rules/require-node-description-fields.js +58 -0
- package/dist/rules/require-node-description-fields.js.map +1 -0
- package/dist/rules/valid-peer-dependencies.d.ts +2 -0
- package/dist/rules/valid-peer-dependencies.d.ts.map +1 -0
- package/dist/rules/valid-peer-dependencies.js +107 -0
- package/dist/rules/valid-peer-dependencies.js.map +1 -0
- package/dist/rules/webhook-lifecycle-complete.d.ts +2 -0
- package/dist/rules/webhook-lifecycle-complete.d.ts.map +1 -0
- package/dist/rules/webhook-lifecycle-complete.js +98 -0
- package/dist/rules/webhook-lifecycle-complete.js.map +1 -0
- package/docs/rules/no-overrides-field.md +50 -0
- package/docs/rules/node-class-description-icon-missing.md +4 -2
- package/docs/rules/require-community-node-keyword.md +45 -0
- package/docs/rules/require-node-api-error.md +62 -0
- package/docs/rules/require-node-description-fields.md +46 -0
- package/docs/rules/valid-peer-dependencies.md +72 -0
- package/docs/rules/webhook-lifecycle-complete.md +88 -0
- package/package.json +5 -5
- package/src/plugin.ts +12 -2
- package/src/rules/index.ts +12 -0
- package/src/rules/no-overrides-field.test.ts +50 -0
- package/src/rules/no-overrides-field.ts +43 -0
- package/src/rules/node-class-description-icon-missing.ts +7 -1
- package/src/rules/require-community-node-keyword.test.ts +82 -0
- package/src/rules/require-community-node-keyword.ts +78 -0
- package/src/rules/require-node-api-error.test.ts +199 -0
- package/src/rules/require-node-api-error.ts +90 -0
- package/src/rules/require-node-description-fields.test.ts +171 -0
- package/src/rules/require-node-description-fields.ts +77 -0
- package/src/rules/valid-peer-dependencies.test.ts +130 -0
- package/src/rules/valid-peer-dependencies.ts +116 -0
- package/src/rules/webhook-lifecycle-complete.test.ts +212 -0
- package/src/rules/webhook-lifecycle-complete.ts +120 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.json +0 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Require community node package.json peerDependencies to contain only "n8n-workflow": "*" (and optionally "ai-node-sdk") (`@n8n/community-nodes/valid-peer-dependencies`)
|
|
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
|
+
Community node packages must declare their n8n integration via `peerDependencies` so that they resolve against the host n8n installation rather than bundling their own copy. The only permitted entries are:
|
|
12
|
+
|
|
13
|
+
- `n8n-workflow` — required, must be exactly `"*"` (no pinned or ranged versions)
|
|
14
|
+
- `ai-node-sdk` — optional, present only for AI nodes (its shape is validated by [`ai-node-package-json`](ai-node-package-json.md))
|
|
15
|
+
|
|
16
|
+
Any other entry (notably `n8n-core`) is flagged because it causes duplicate or incompatible copies of n8n internals to be loaded at runtime.
|
|
17
|
+
|
|
18
|
+
The rule checks:
|
|
19
|
+
|
|
20
|
+
- `peerDependencies` is present in `package.json`
|
|
21
|
+
- `n8n-workflow` is listed with value `"*"`
|
|
22
|
+
- No other packages (besides `ai-node-sdk`) appear in `peerDependencies`
|
|
23
|
+
|
|
24
|
+
## Examples
|
|
25
|
+
|
|
26
|
+
### ❌ Incorrect
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"name": "n8n-nodes-example"
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"name": "n8n-nodes-example",
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"n8n-workflow": "^1.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"name": "n8n-nodes-example",
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"n8n-workflow": "*",
|
|
48
|
+
"n8n-core": "*"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### ✅ Correct
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"name": "n8n-nodes-example",
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"n8n-workflow": "*"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"name": "n8n-nodes-my-ai-node",
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"n8n-workflow": "*",
|
|
69
|
+
"ai-node-sdk": "*"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Require webhook trigger nodes to implement the complete webhookMethods lifecycle (checkExists, create, delete) (`@n8n/community-nodes/webhook-lifecycle-complete`)
|
|
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
|
+
Webhook-based trigger nodes must implement a complete webhook lifecycle so that
|
|
10
|
+
n8n can register, verify, and clean up webhooks on the third-party service.
|
|
11
|
+
Missing any of the three methods results in leaked webhooks, duplicated
|
|
12
|
+
registrations, or workflows that silently stop firing.
|
|
13
|
+
|
|
14
|
+
This rule applies to node classes that:
|
|
15
|
+
|
|
16
|
+
- declare a non-empty `webhooks` array in their `description`, or
|
|
17
|
+
- define a `webhookMethods` class property.
|
|
18
|
+
|
|
19
|
+
For every webhook group inside `webhookMethods` (typically `default`), the
|
|
20
|
+
methods `checkExists`, `create`, and `delete` must all be implemented.
|
|
21
|
+
|
|
22
|
+
Polling triggers (trigger nodes without a `webhooks` array and without
|
|
23
|
+
`webhookMethods`) are intentionally out of scope.
|
|
24
|
+
|
|
25
|
+
## Examples
|
|
26
|
+
|
|
27
|
+
### ❌ Incorrect
|
|
28
|
+
|
|
29
|
+
Webhook trigger without any lifecycle methods:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
export class MyTrigger implements INodeType {
|
|
33
|
+
description: INodeTypeDescription = {
|
|
34
|
+
displayName: 'My Trigger',
|
|
35
|
+
name: 'myTrigger',
|
|
36
|
+
group: ['trigger'],
|
|
37
|
+
version: 1,
|
|
38
|
+
description: 'Trigger on events',
|
|
39
|
+
defaults: { name: 'My Trigger' },
|
|
40
|
+
inputs: [],
|
|
41
|
+
outputs: ['main'],
|
|
42
|
+
webhooks: [
|
|
43
|
+
{ name: 'default', httpMethod: 'POST', responseMode: 'onReceived', path: 'webhook' },
|
|
44
|
+
],
|
|
45
|
+
properties: [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Webhook trigger with an incomplete `webhookMethods.default`:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
export class MyTrigger implements INodeType {
|
|
54
|
+
description: INodeTypeDescription = { /* ... */ };
|
|
55
|
+
|
|
56
|
+
webhookMethods = {
|
|
57
|
+
default: {
|
|
58
|
+
async create(this: IHookFunctions): Promise<boolean> { /* ... */ return true; },
|
|
59
|
+
// Missing checkExists and delete
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### ✅ Correct
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
export class MyTrigger implements INodeType {
|
|
69
|
+
description: INodeTypeDescription = { /* ... */ };
|
|
70
|
+
|
|
71
|
+
webhookMethods = {
|
|
72
|
+
default: {
|
|
73
|
+
async checkExists(this: IHookFunctions): Promise<boolean> {
|
|
74
|
+
// Return true if the webhook is already registered on the third-party service.
|
|
75
|
+
return true;
|
|
76
|
+
},
|
|
77
|
+
async create(this: IHookFunctions): Promise<boolean> {
|
|
78
|
+
// Register the webhook with the third-party service and persist any IDs.
|
|
79
|
+
return true;
|
|
80
|
+
},
|
|
81
|
+
async delete(this: IHookFunctions): Promise<boolean> {
|
|
82
|
+
// Remove the webhook from the third-party service.
|
|
83
|
+
return true;
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
```
|
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.14.0",
|
|
5
5
|
"main": "./dist/plugin.js",
|
|
6
6
|
"types": "./dist/plugin.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"typescript": "6.0.2",
|
|
24
24
|
"vitest": "^4.1.1",
|
|
25
25
|
"@n8n/typescript-config": "1.4.0",
|
|
26
|
-
"
|
|
27
|
-
"n8n-
|
|
26
|
+
"n8n-workflow": "2.19.0",
|
|
27
|
+
"@n8n/vitest-config": "1.10.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"eslint": ">= 9",
|
|
@@ -54,14 +54,14 @@
|
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "tsc --project tsconfig.build.json",
|
|
57
|
-
"build:docs": "eslint-doc-generator",
|
|
57
|
+
"build:docs": "pnpm build && eslint-doc-generator",
|
|
58
58
|
"clean": "rimraf dist .turbo",
|
|
59
59
|
"dev": "pnpm watch",
|
|
60
60
|
"format": "biome format --write .",
|
|
61
61
|
"format:check": "biome ci .",
|
|
62
62
|
"lint": "eslint src",
|
|
63
63
|
"lint:fix": "eslint src --fix",
|
|
64
|
-
"lint:docs": "eslint-doc-generator --check",
|
|
64
|
+
"lint:docs": "pnpm build && eslint-doc-generator --check",
|
|
65
65
|
"test": "vitest run",
|
|
66
66
|
"test:unit": "vitest run",
|
|
67
67
|
"test:dev": "vitest",
|
package/src/plugin.ts
CHANGED
|
@@ -31,15 +31,20 @@ const configs = {
|
|
|
31
31
|
'@n8n/community-nodes/no-credential-reuse': 'error',
|
|
32
32
|
'@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error',
|
|
33
33
|
'@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
|
|
34
|
+
'@n8n/community-nodes/no-overrides-field': 'error',
|
|
34
35
|
'@n8n/community-nodes/icon-validation': 'error',
|
|
35
36
|
'@n8n/community-nodes/options-sorted-alphabetically': 'warn',
|
|
36
37
|
'@n8n/community-nodes/resource-operation-pattern': 'warn',
|
|
37
38
|
'@n8n/community-nodes/credential-documentation-url': 'error',
|
|
38
|
-
'@n8n/community-nodes/node-class-description-icon-missing': 'error',
|
|
39
39
|
'@n8n/community-nodes/cred-class-field-icon-missing': 'error',
|
|
40
40
|
'@n8n/community-nodes/node-connection-type-literal': 'error',
|
|
41
41
|
'@n8n/community-nodes/missing-paired-item': 'error',
|
|
42
|
+
'@n8n/community-nodes/require-community-node-keyword': 'warn',
|
|
42
43
|
'@n8n/community-nodes/require-continue-on-fail': 'error',
|
|
44
|
+
'@n8n/community-nodes/require-node-api-error': 'error',
|
|
45
|
+
'@n8n/community-nodes/require-node-description-fields': 'error',
|
|
46
|
+
'@n8n/community-nodes/valid-peer-dependencies': 'error',
|
|
47
|
+
'@n8n/community-nodes/webhook-lifecycle-complete': 'error',
|
|
43
48
|
},
|
|
44
49
|
},
|
|
45
50
|
recommendedWithoutN8nCloudSupport: {
|
|
@@ -57,15 +62,20 @@ const configs = {
|
|
|
57
62
|
'@n8n/community-nodes/no-credential-reuse': 'error',
|
|
58
63
|
'@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error',
|
|
59
64
|
'@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
|
|
65
|
+
'@n8n/community-nodes/no-overrides-field': 'error',
|
|
60
66
|
'@n8n/community-nodes/icon-validation': 'error',
|
|
61
67
|
'@n8n/community-nodes/options-sorted-alphabetically': 'warn',
|
|
62
68
|
'@n8n/community-nodes/credential-documentation-url': 'error',
|
|
63
69
|
'@n8n/community-nodes/resource-operation-pattern': 'warn',
|
|
64
|
-
'@n8n/community-nodes/node-class-description-icon-missing': 'error',
|
|
65
70
|
'@n8n/community-nodes/cred-class-field-icon-missing': 'error',
|
|
66
71
|
'@n8n/community-nodes/node-connection-type-literal': 'error',
|
|
67
72
|
'@n8n/community-nodes/missing-paired-item': 'error',
|
|
73
|
+
'@n8n/community-nodes/require-community-node-keyword': 'warn',
|
|
68
74
|
'@n8n/community-nodes/require-continue-on-fail': 'error',
|
|
75
|
+
'@n8n/community-nodes/require-node-api-error': 'error',
|
|
76
|
+
'@n8n/community-nodes/require-node-description-fields': 'error',
|
|
77
|
+
'@n8n/community-nodes/valid-peer-dependencies': 'error',
|
|
78
|
+
'@n8n/community-nodes/webhook-lifecycle-complete': 'error',
|
|
69
79
|
},
|
|
70
80
|
},
|
|
71
81
|
} satisfies Record<string, Linter.Config>;
|
package/src/rules/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { NoCredentialReuseRule } from './no-credential-reuse.js';
|
|
|
11
11
|
import { NoDeprecatedWorkflowFunctionsRule } from './no-deprecated-workflow-functions.js';
|
|
12
12
|
import { NoForbiddenLifecycleScriptsRule } from './no-forbidden-lifecycle-scripts.js';
|
|
13
13
|
import { NoHttpRequestWithManualAuthRule } from './no-http-request-with-manual-auth.js';
|
|
14
|
+
import { NoOverridesFieldRule } from './no-overrides-field.js';
|
|
14
15
|
import { NoRestrictedGlobalsRule } from './no-restricted-globals.js';
|
|
15
16
|
import { NoRestrictedImportsRule } from './no-restricted-imports.js';
|
|
16
17
|
import { NodeClassDescriptionIconMissingRule } from './node-class-description-icon-missing.js';
|
|
@@ -18,8 +19,13 @@ import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js
|
|
|
18
19
|
import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
|
|
19
20
|
import { OptionsSortedAlphabeticallyRule } from './options-sorted-alphabetically.js';
|
|
20
21
|
import { PackageNameConventionRule } from './package-name-convention.js';
|
|
22
|
+
import { RequireCommunityNodeKeywordRule } from './require-community-node-keyword.js';
|
|
21
23
|
import { RequireContinueOnFailRule } from './require-continue-on-fail.js';
|
|
24
|
+
import { RequireNodeApiErrorRule } from './require-node-api-error.js';
|
|
25
|
+
import { RequireNodeDescriptionFieldsRule } from './require-node-description-fields.js';
|
|
22
26
|
import { ResourceOperationPatternRule } from './resource-operation-pattern.js';
|
|
27
|
+
import { ValidPeerDependenciesRule } from './valid-peer-dependencies.js';
|
|
28
|
+
import { WebhookLifecycleCompleteRule } from './webhook-lifecycle-complete.js';
|
|
23
29
|
|
|
24
30
|
export const rules = {
|
|
25
31
|
'ai-node-package-json': AiNodePackageJsonRule,
|
|
@@ -34,6 +40,7 @@ export const rules = {
|
|
|
34
40
|
'no-credential-reuse': NoCredentialReuseRule,
|
|
35
41
|
'no-forbidden-lifecycle-scripts': NoForbiddenLifecycleScriptsRule,
|
|
36
42
|
'no-http-request-with-manual-auth': NoHttpRequestWithManualAuthRule,
|
|
43
|
+
'no-overrides-field': NoOverridesFieldRule,
|
|
37
44
|
'icon-validation': IconValidationRule,
|
|
38
45
|
'resource-operation-pattern': ResourceOperationPatternRule,
|
|
39
46
|
'credential-documentation-url': CredentialDocumentationUrlRule,
|
|
@@ -41,5 +48,10 @@ export const rules = {
|
|
|
41
48
|
'cred-class-field-icon-missing': CredClassFieldIconMissingRule,
|
|
42
49
|
'node-connection-type-literal': NodeConnectionTypeLiteralRule,
|
|
43
50
|
'missing-paired-item': MissingPairedItemRule,
|
|
51
|
+
'require-community-node-keyword': RequireCommunityNodeKeywordRule,
|
|
44
52
|
'require-continue-on-fail': RequireContinueOnFailRule,
|
|
53
|
+
'require-node-api-error': RequireNodeApiErrorRule,
|
|
54
|
+
'require-node-description-fields': RequireNodeDescriptionFieldsRule,
|
|
55
|
+
'valid-peer-dependencies': ValidPeerDependenciesRule,
|
|
56
|
+
'webhook-lifecycle-complete': WebhookLifecycleCompleteRule,
|
|
45
57
|
} satisfies Record<string, AnyRuleModule>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { NoOverridesFieldRule } from './no-overrides-field.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
ruleTester.run('no-overrides-field', NoOverridesFieldRule, {
|
|
8
|
+
valid: [
|
|
9
|
+
{
|
|
10
|
+
name: 'no overrides field',
|
|
11
|
+
filename: 'package.json',
|
|
12
|
+
code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'package.json with dependencies but no overrides',
|
|
16
|
+
filename: 'package.json',
|
|
17
|
+
code: '{ "name": "n8n-nodes-example", "peerDependencies": { "n8n-workflow": "*" } }',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'non-package.json file is ignored',
|
|
21
|
+
filename: 'some-config.json',
|
|
22
|
+
code: '{ "overrides": { "axios": "1.0.0" } }',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'nested "overrides" key inside another field is allowed',
|
|
26
|
+
filename: 'package.json',
|
|
27
|
+
code: '{ "name": "n8n-nodes-example", "config": { "overrides": { "axios": "1.0.0" } } }',
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
invalid: [
|
|
31
|
+
{
|
|
32
|
+
name: 'overrides as object is forbidden',
|
|
33
|
+
filename: 'package.json',
|
|
34
|
+
code: '{ "name": "n8n-nodes-example", "overrides": { "axios": "1.0.0" } }',
|
|
35
|
+
errors: [{ messageId: 'overridesForbidden' }],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'empty overrides object is forbidden',
|
|
39
|
+
filename: 'package.json',
|
|
40
|
+
code: '{ "name": "n8n-nodes-example", "overrides": {} }',
|
|
41
|
+
errors: [{ messageId: 'overridesForbidden' }],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'real-world overrides (CNOC-404 Sinch) is forbidden',
|
|
45
|
+
filename: 'package.json',
|
|
46
|
+
code: '{ "name": "n8n-nodes-sinch", "overrides": { "axios": "1.7.0", "fast-xml-parser": "4.4.0", "minimatch": "9.0.5", "@langchain/core": "0.3.0", "qs": "6.13.0" } }',
|
|
47
|
+
errors: [{ messageId: 'overridesForbidden' }],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
import { createRule, findJsonProperty } from '../utils/index.js';
|
|
5
|
+
|
|
6
|
+
export const NoOverridesFieldRule = createRule({
|
|
7
|
+
name: 'no-overrides-field',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'problem',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Ban the "overrides" field in community node package.json',
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
overridesForbidden:
|
|
15
|
+
'The "overrides" field is not allowed in community node packages. Dependency overrides can introduce incompatible versions of shared libraries into the n8n runtime and cause conflicts with other nodes.',
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
},
|
|
19
|
+
defaultOptions: [],
|
|
20
|
+
create(context) {
|
|
21
|
+
if (!context.filename.endsWith('package.json')) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
ObjectExpression(node: TSESTree.ObjectExpression) {
|
|
27
|
+
if (node.parent?.type !== AST_NODE_TYPES.ExpressionStatement) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const overridesProp = findJsonProperty(node, 'overrides');
|
|
32
|
+
if (!overridesProp) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
context.report({
|
|
37
|
+
node: overridesProp,
|
|
38
|
+
messageId: 'overridesForbidden',
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
});
|
|
@@ -8,12 +8,18 @@ import {
|
|
|
8
8
|
createRule,
|
|
9
9
|
} from '../utils/index.js';
|
|
10
10
|
|
|
11
|
+
/** @deprecated Use `require-node-description-fields` instead. */
|
|
11
12
|
export const NodeClassDescriptionIconMissingRule = createRule({
|
|
12
13
|
name: 'node-class-description-icon-missing',
|
|
13
14
|
meta: {
|
|
14
15
|
type: 'problem',
|
|
16
|
+
deprecated: {
|
|
17
|
+
message: 'Use require-node-description-fields instead.',
|
|
18
|
+
replacedBy: [{ rule: { name: 'require-node-description-fields' } }],
|
|
19
|
+
},
|
|
15
20
|
docs: {
|
|
16
|
-
description:
|
|
21
|
+
description:
|
|
22
|
+
'Node class description must have an `icon` property defined. Deprecated: use `require-node-description-fields` instead.',
|
|
17
23
|
},
|
|
18
24
|
messages: {
|
|
19
25
|
missingIcon: 'Node class description is missing required `icon` property',
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { RequireCommunityNodeKeywordRule } from './require-community-node-keyword.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
ruleTester.run('require-community-node-keyword', RequireCommunityNodeKeywordRule, {
|
|
8
|
+
valid: [
|
|
9
|
+
{
|
|
10
|
+
name: 'keywords array contains required keyword',
|
|
11
|
+
filename: 'package.json',
|
|
12
|
+
code: '{ "name": "n8n-nodes-example", "keywords": ["n8n-community-node-package"] }',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'keywords array contains required keyword among others',
|
|
16
|
+
filename: 'package.json',
|
|
17
|
+
code: '{ "name": "n8n-nodes-example", "keywords": ["n8n", "automation", "n8n-community-node-package", "workflow"] }',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'non-package.json file is ignored',
|
|
21
|
+
filename: 'some-config.json',
|
|
22
|
+
code: '{ "name": "n8n-nodes-example" }',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'nested objects are not checked',
|
|
26
|
+
filename: 'package.json',
|
|
27
|
+
code: '{ "name": "n8n-nodes-example", "keywords": ["n8n-community-node-package"], "config": { "nested": "value" } }',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'objects inside arrays (e.g. contributors) are not flagged',
|
|
31
|
+
filename: 'package.json',
|
|
32
|
+
code: `{
|
|
33
|
+
"name": "n8n-nodes-example",
|
|
34
|
+
"keywords": ["n8n-community-node-package"],
|
|
35
|
+
"contributors": [
|
|
36
|
+
{ "name": "Alice", "email": "alice@example.com" },
|
|
37
|
+
{ "name": "Bob", "email": "bob@example.com" }
|
|
38
|
+
]
|
|
39
|
+
}`,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
invalid: [
|
|
43
|
+
{
|
|
44
|
+
name: 'missing keywords array entirely',
|
|
45
|
+
filename: 'package.json',
|
|
46
|
+
code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
|
|
47
|
+
output:
|
|
48
|
+
'{ "name": "n8n-nodes-example", "version": "1.0.0", "keywords": ["n8n-community-node-package"] }',
|
|
49
|
+
errors: [{ messageId: 'missingKeywordsArray' }],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'empty keywords array',
|
|
53
|
+
filename: 'package.json',
|
|
54
|
+
code: '{ "name": "n8n-nodes-example", "keywords": [] }',
|
|
55
|
+
output: '{ "name": "n8n-nodes-example", "keywords": ["n8n-community-node-package"] }',
|
|
56
|
+
errors: [{ messageId: 'missingKeyword' }],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'keywords array without the required keyword',
|
|
60
|
+
filename: 'package.json',
|
|
61
|
+
code: '{ "name": "n8n-nodes-example", "keywords": ["n8n", "automation"] }',
|
|
62
|
+
output:
|
|
63
|
+
'{ "name": "n8n-nodes-example", "keywords": ["n8n", "automation", "n8n-community-node-package"] }',
|
|
64
|
+
errors: [{ messageId: 'missingKeyword' }],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'keywords array with similar but incorrect keyword',
|
|
68
|
+
filename: 'package.json',
|
|
69
|
+
code: '{ "name": "n8n-nodes-example", "keywords": ["n8n-community-node"] }',
|
|
70
|
+
output:
|
|
71
|
+
'{ "name": "n8n-nodes-example", "keywords": ["n8n-community-node", "n8n-community-node-package"] }',
|
|
72
|
+
errors: [{ messageId: 'missingKeyword' }],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'empty package.json object',
|
|
76
|
+
filename: 'package.json',
|
|
77
|
+
code: '{}',
|
|
78
|
+
output: '{ "keywords": ["n8n-community-node-package"] }',
|
|
79
|
+
errors: [{ messageId: 'missingKeywordsArray' }],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
import { createRule, findJsonProperty } from '../utils/index.js';
|
|
5
|
+
|
|
6
|
+
const REQUIRED_KEYWORD = 'n8n-community-node-package';
|
|
7
|
+
|
|
8
|
+
export const RequireCommunityNodeKeywordRule = createRule({
|
|
9
|
+
name: 'require-community-node-keyword',
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
docs: {
|
|
13
|
+
description:
|
|
14
|
+
'Require the "n8n-community-node-package" keyword in community node package.json',
|
|
15
|
+
},
|
|
16
|
+
fixable: 'code',
|
|
17
|
+
messages: {
|
|
18
|
+
missingKeyword: `The "keywords" array must include "${REQUIRED_KEYWORD}". This keyword is required for n8n to discover community node packages.`,
|
|
19
|
+
missingKeywordsArray: `The package.json must have a "keywords" array containing "${REQUIRED_KEYWORD}".`,
|
|
20
|
+
},
|
|
21
|
+
schema: [],
|
|
22
|
+
},
|
|
23
|
+
defaultOptions: [],
|
|
24
|
+
create(context) {
|
|
25
|
+
if (!context.filename.endsWith('package.json')) {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
ObjectExpression(node: TSESTree.ObjectExpression) {
|
|
31
|
+
if (node.parent?.type !== AST_NODE_TYPES.ExpressionStatement) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const keywordsProp = findJsonProperty(node, 'keywords');
|
|
36
|
+
|
|
37
|
+
if (!keywordsProp) {
|
|
38
|
+
context.report({
|
|
39
|
+
node,
|
|
40
|
+
messageId: 'missingKeywordsArray',
|
|
41
|
+
fix(fixer) {
|
|
42
|
+
const lastProp = node.properties[node.properties.length - 1];
|
|
43
|
+
if (!lastProp) {
|
|
44
|
+
return fixer.replaceText(node, `{ "keywords": ["${REQUIRED_KEYWORD}"] }`);
|
|
45
|
+
}
|
|
46
|
+
return fixer.insertTextAfter(lastProp, `, "keywords": ["${REQUIRED_KEYWORD}"]`);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (keywordsProp.value.type !== AST_NODE_TYPES.ArrayExpression) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const keywordsArray = keywordsProp.value;
|
|
57
|
+
const hasRequiredKeyword = keywordsArray.elements.some(
|
|
58
|
+
(element) =>
|
|
59
|
+
element?.type === AST_NODE_TYPES.Literal && element.value === REQUIRED_KEYWORD,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (!hasRequiredKeyword) {
|
|
63
|
+
context.report({
|
|
64
|
+
node: keywordsProp,
|
|
65
|
+
messageId: 'missingKeyword',
|
|
66
|
+
fix(fixer) {
|
|
67
|
+
const lastElement = keywordsArray.elements[keywordsArray.elements.length - 1];
|
|
68
|
+
if (!lastElement) {
|
|
69
|
+
return fixer.replaceText(keywordsArray, `["${REQUIRED_KEYWORD}"]`);
|
|
70
|
+
}
|
|
71
|
+
return fixer.insertTextAfter(lastElement, `, "${REQUIRED_KEYWORD}"`);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
});
|