@n8n/eslint-plugin-community-nodes 0.13.0 → 0.15.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.
Files changed (60) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/README.md +32 -24
  3. package/dist/plugin.d.ts +36 -0
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +12 -0
  6. package/dist/plugin.js.map +1 -1
  7. package/dist/rules/index.d.ts +6 -0
  8. package/dist/rules/index.d.ts.map +1 -1
  9. package/dist/rules/index.js +12 -0
  10. package/dist/rules/index.js.map +1 -1
  11. package/dist/rules/no-overrides-field.d.ts +2 -0
  12. package/dist/rules/no-overrides-field.d.ts.map +1 -0
  13. package/dist/rules/no-overrides-field.js +37 -0
  14. package/dist/rules/no-overrides-field.js.map +1 -0
  15. package/dist/rules/no-runtime-dependencies.d.ts +2 -0
  16. package/dist/rules/no-runtime-dependencies.d.ts.map +1 -0
  17. package/dist/rules/no-runtime-dependencies.js +41 -0
  18. package/dist/rules/no-runtime-dependencies.js.map +1 -0
  19. package/dist/rules/require-node-api-error.d.ts +2 -0
  20. package/dist/rules/require-node-api-error.d.ts.map +1 -0
  21. package/dist/rules/require-node-api-error.js +77 -0
  22. package/dist/rules/require-node-api-error.js.map +1 -0
  23. package/dist/rules/valid-credential-references.d.ts +2 -0
  24. package/dist/rules/valid-credential-references.d.ts.map +1 -0
  25. package/dist/rules/valid-credential-references.js +77 -0
  26. package/dist/rules/valid-credential-references.js.map +1 -0
  27. package/dist/rules/valid-peer-dependencies.d.ts +2 -0
  28. package/dist/rules/valid-peer-dependencies.d.ts.map +1 -0
  29. package/dist/rules/valid-peer-dependencies.js +107 -0
  30. package/dist/rules/valid-peer-dependencies.js.map +1 -0
  31. package/dist/rules/webhook-lifecycle-complete.d.ts +2 -0
  32. package/dist/rules/webhook-lifecycle-complete.d.ts.map +1 -0
  33. package/dist/rules/webhook-lifecycle-complete.js +106 -0
  34. package/dist/rules/webhook-lifecycle-complete.js.map +1 -0
  35. package/docs/rules/no-overrides-field.md +50 -0
  36. package/docs/rules/no-runtime-dependencies.md +58 -0
  37. package/docs/rules/node-class-description-icon-missing.md +4 -2
  38. package/docs/rules/require-community-node-keyword.md +3 -3
  39. package/docs/rules/require-node-api-error.md +62 -0
  40. package/docs/rules/require-node-description-fields.md +1 -1
  41. package/docs/rules/valid-credential-references.md +78 -0
  42. package/docs/rules/valid-peer-dependencies.md +72 -0
  43. package/docs/rules/webhook-lifecycle-complete.md +88 -0
  44. package/package.json +5 -5
  45. package/src/plugin.ts +12 -0
  46. package/src/rules/index.ts +12 -0
  47. package/src/rules/no-overrides-field.test.ts +50 -0
  48. package/src/rules/no-overrides-field.ts +43 -0
  49. package/src/rules/no-runtime-dependencies.test.ts +50 -0
  50. package/src/rules/no-runtime-dependencies.ts +50 -0
  51. package/src/rules/require-node-api-error.test.ts +199 -0
  52. package/src/rules/require-node-api-error.ts +90 -0
  53. package/src/rules/valid-credential-references.test.ts +230 -0
  54. package/src/rules/valid-credential-references.ts +105 -0
  55. package/src/rules/valid-peer-dependencies.test.ts +130 -0
  56. package/src/rules/valid-peer-dependencies.ts +116 -0
  57. package/src/rules/webhook-lifecycle-complete.test.ts +217 -0
  58. package/src/rules/webhook-lifecycle-complete.ts +130 -0
  59. package/tsconfig.build.tsbuildinfo +1 -1
  60. 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.13.0",
4
+ "version": "0.15.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
- "n8n-workflow": "2.18.0",
27
- "@n8n/vitest-config": "1.9.0"
26
+ "@n8n/vitest-config": "1.10.0",
27
+ "n8n-workflow": "2.20.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,6 +31,8 @@ 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',
35
+ '@n8n/community-nodes/no-runtime-dependencies': 'error',
34
36
  '@n8n/community-nodes/icon-validation': 'error',
35
37
  '@n8n/community-nodes/options-sorted-alphabetically': 'warn',
36
38
  '@n8n/community-nodes/resource-operation-pattern': 'warn',
@@ -40,7 +42,11 @@ const configs = {
40
42
  '@n8n/community-nodes/missing-paired-item': 'error',
41
43
  '@n8n/community-nodes/require-community-node-keyword': 'warn',
42
44
  '@n8n/community-nodes/require-continue-on-fail': 'error',
45
+ '@n8n/community-nodes/require-node-api-error': 'error',
43
46
  '@n8n/community-nodes/require-node-description-fields': 'error',
47
+ '@n8n/community-nodes/valid-credential-references': 'error',
48
+ '@n8n/community-nodes/valid-peer-dependencies': 'error',
49
+ '@n8n/community-nodes/webhook-lifecycle-complete': 'error',
44
50
  },
45
51
  },
46
52
  recommendedWithoutN8nCloudSupport: {
@@ -58,6 +64,8 @@ const configs = {
58
64
  '@n8n/community-nodes/no-credential-reuse': 'error',
59
65
  '@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error',
60
66
  '@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
67
+ '@n8n/community-nodes/no-overrides-field': 'error',
68
+ '@n8n/community-nodes/no-runtime-dependencies': 'error',
61
69
  '@n8n/community-nodes/icon-validation': 'error',
62
70
  '@n8n/community-nodes/options-sorted-alphabetically': 'warn',
63
71
  '@n8n/community-nodes/credential-documentation-url': 'error',
@@ -67,7 +75,11 @@ const configs = {
67
75
  '@n8n/community-nodes/missing-paired-item': 'error',
68
76
  '@n8n/community-nodes/require-community-node-keyword': 'warn',
69
77
  '@n8n/community-nodes/require-continue-on-fail': 'error',
78
+ '@n8n/community-nodes/require-node-api-error': 'error',
70
79
  '@n8n/community-nodes/require-node-description-fields': 'error',
80
+ '@n8n/community-nodes/valid-credential-references': 'error',
81
+ '@n8n/community-nodes/valid-peer-dependencies': 'error',
82
+ '@n8n/community-nodes/webhook-lifecycle-complete': 'error',
71
83
  },
72
84
  },
73
85
  } satisfies Record<string, Linter.Config>;
@@ -11,8 +11,10 @@ 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';
17
+ import { NoRuntimeDependenciesRule } from './no-runtime-dependencies.js';
16
18
  import { NodeClassDescriptionIconMissingRule } from './node-class-description-icon-missing.js';
17
19
  import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js';
18
20
  import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
@@ -20,8 +22,12 @@ import { OptionsSortedAlphabeticallyRule } from './options-sorted-alphabetically
20
22
  import { PackageNameConventionRule } from './package-name-convention.js';
21
23
  import { RequireCommunityNodeKeywordRule } from './require-community-node-keyword.js';
22
24
  import { RequireContinueOnFailRule } from './require-continue-on-fail.js';
25
+ import { RequireNodeApiErrorRule } from './require-node-api-error.js';
23
26
  import { RequireNodeDescriptionFieldsRule } from './require-node-description-fields.js';
24
27
  import { ResourceOperationPatternRule } from './resource-operation-pattern.js';
28
+ import { ValidCredentialReferencesRule } from './valid-credential-references.js';
29
+ import { ValidPeerDependenciesRule } from './valid-peer-dependencies.js';
30
+ import { WebhookLifecycleCompleteRule } from './webhook-lifecycle-complete.js';
25
31
 
26
32
  export const rules = {
27
33
  'ai-node-package-json': AiNodePackageJsonRule,
@@ -36,6 +42,8 @@ export const rules = {
36
42
  'no-credential-reuse': NoCredentialReuseRule,
37
43
  'no-forbidden-lifecycle-scripts': NoForbiddenLifecycleScriptsRule,
38
44
  'no-http-request-with-manual-auth': NoHttpRequestWithManualAuthRule,
45
+ 'no-overrides-field': NoOverridesFieldRule,
46
+ 'no-runtime-dependencies': NoRuntimeDependenciesRule,
39
47
  'icon-validation': IconValidationRule,
40
48
  'resource-operation-pattern': ResourceOperationPatternRule,
41
49
  'credential-documentation-url': CredentialDocumentationUrlRule,
@@ -45,5 +53,9 @@ export const rules = {
45
53
  'missing-paired-item': MissingPairedItemRule,
46
54
  'require-community-node-keyword': RequireCommunityNodeKeywordRule,
47
55
  'require-continue-on-fail': RequireContinueOnFailRule,
56
+ 'require-node-api-error': RequireNodeApiErrorRule,
48
57
  'require-node-description-fields': RequireNodeDescriptionFieldsRule,
58
+ 'valid-credential-references': ValidCredentialReferencesRule,
59
+ 'valid-peer-dependencies': ValidPeerDependenciesRule,
60
+ 'webhook-lifecycle-complete': WebhookLifecycleCompleteRule,
49
61
  } 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
+ });
@@ -0,0 +1,50 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { NoRuntimeDependenciesRule } from './no-runtime-dependencies.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ ruleTester.run('no-runtime-dependencies', NoRuntimeDependenciesRule, {
8
+ valid: [
9
+ {
10
+ name: 'no dependencies field',
11
+ filename: 'package.json',
12
+ code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
13
+ },
14
+ {
15
+ name: 'empty dependencies object is allowed',
16
+ filename: 'package.json',
17
+ code: '{ "name": "n8n-nodes-example", "dependencies": {} }',
18
+ },
19
+ {
20
+ name: 'non-package.json file is ignored',
21
+ filename: 'some-config.json',
22
+ code: '{ "dependencies": { "axios": "1.0.0" } }',
23
+ },
24
+ {
25
+ name: 'nested "dependencies" key inside another field is allowed',
26
+ filename: 'package.json',
27
+ code: '{ "name": "n8n-nodes-example", "config": { "dependencies": { "axios": "1.0.0" } } }',
28
+ },
29
+ ],
30
+ invalid: [
31
+ {
32
+ name: 'single runtime dependency is forbidden',
33
+ filename: 'package.json',
34
+ code: '{ "name": "n8n-nodes-example", "dependencies": { "axios": "1.0.0" } }',
35
+ errors: [{ messageId: 'runtimeDependenciesForbidden' }],
36
+ },
37
+ {
38
+ name: 'multiple runtime dependencies are forbidden',
39
+ filename: 'package.json',
40
+ code: '{ "name": "n8n-nodes-example", "dependencies": { "axios": "1.0.0", "lodash": "^4.0.0" } }',
41
+ errors: [{ messageId: 'runtimeDependenciesForbidden' }],
42
+ },
43
+ {
44
+ name: 'real-world package with bundled deps is forbidden',
45
+ filename: 'package.json',
46
+ code: '{ "name": "n8n-nodes-sinch", "dependencies": { "axios": "1.7.0", "fast-xml-parser": "4.4.0", "minimatch": "9.0.5" } }',
47
+ errors: [{ messageId: 'runtimeDependenciesForbidden' }],
48
+ },
49
+ ],
50
+ });
@@ -0,0 +1,50 @@
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 NoRuntimeDependenciesRule = createRule({
7
+ name: 'no-runtime-dependencies',
8
+ meta: {
9
+ type: 'problem',
10
+ docs: {
11
+ description: 'Disallow non-empty "dependencies" in community node package.json',
12
+ },
13
+ messages: {
14
+ runtimeDependenciesForbidden:
15
+ 'The "dependencies" field must be empty or absent in community node packages. Runtime dependencies get bundled into the n8n instance and can conflict with other nodes or the n8n runtime itself. Move shared libraries to "peerDependencies" or bundle them into your build artifact.',
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 depsProp = findJsonProperty(node, 'dependencies');
32
+ if (!depsProp) {
33
+ return;
34
+ }
35
+
36
+ if (
37
+ depsProp.value.type !== AST_NODE_TYPES.ObjectExpression ||
38
+ depsProp.value.properties.length === 0
39
+ ) {
40
+ return;
41
+ }
42
+
43
+ context.report({
44
+ node: depsProp,
45
+ messageId: 'runtimeDependenciesForbidden',
46
+ });
47
+ },
48
+ };
49
+ },
50
+ });
@@ -0,0 +1,199 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { RequireNodeApiErrorRule } from './require-node-api-error.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ ruleTester.run('require-node-api-error', RequireNodeApiErrorRule, {
8
+ valid: [
9
+ {
10
+ name: 'throw NodeApiError in catch block',
11
+ code: `
12
+ try {
13
+ await apiRequest();
14
+ } catch (error) {
15
+ throw new NodeApiError(this.getNode(), error as JsonObject);
16
+ }`,
17
+ },
18
+ {
19
+ name: 'throw NodeOperationError in catch block',
20
+ code: `
21
+ try {
22
+ await apiRequest();
23
+ } catch (error) {
24
+ throw new NodeOperationError(this.getNode(), 'Operation failed', { itemIndex: i });
25
+ }`,
26
+ },
27
+ {
28
+ name: 'throw outside catch block (not in scope)',
29
+ code: `
30
+ function validate(input: string) {
31
+ if (!input) {
32
+ throw new Error('Input required');
33
+ }
34
+ }`,
35
+ },
36
+ {
37
+ name: 'throw new Error outside catch block (not in scope)',
38
+ code: `
39
+ throw new Error('Something went wrong');`,
40
+ },
41
+ {
42
+ name: 'continueOnFail pattern with NodeApiError',
43
+ code: `
44
+ try {
45
+ responseData = await apiRequest.call(this, 'POST', '/tasks', body);
46
+ } catch (error) {
47
+ if (this.continueOnFail()) {
48
+ returnData.push({ json: { error: error.message } });
49
+ continue;
50
+ }
51
+ throw new NodeApiError(this.getNode(), error as JsonObject);
52
+ }`,
53
+ },
54
+ {
55
+ name: 'conditional handling then NodeApiError in else',
56
+ code: `
57
+ try {
58
+ await ftp.put(data, path);
59
+ } catch (error) {
60
+ if (error.code === 553) {
61
+ await ftp.mkdir(dirPath, true);
62
+ await ftp.put(data, path);
63
+ } else {
64
+ throw new NodeApiError(this.getNode(), error as JsonObject);
65
+ }
66
+ }`,
67
+ },
68
+ {
69
+ name: 'throw wrapped error stored in variable',
70
+ code: `
71
+ try {
72
+ await apiRequest();
73
+ } catch (error) {
74
+ const wrapped = new NodeApiError(this.getNode(), error as JsonObject);
75
+ throw wrapped;
76
+ }`,
77
+ },
78
+ {
79
+ name: 'shadowed variable with same name as catch param',
80
+ code: `
81
+ try {
82
+ await apiRequest();
83
+ } catch (error) {
84
+ const fn = (error: Error) => {
85
+ throw error;
86
+ };
87
+ }`,
88
+ },
89
+ {
90
+ name: 'no throw in catch block',
91
+ code: `
92
+ try {
93
+ await apiRequest();
94
+ } catch (error) {
95
+ console.error(error);
96
+ }`,
97
+ },
98
+ {
99
+ name: 'bare re-throw in credential file (skipped)',
100
+ filename: '/path/to/MyCredential.credentials.ts',
101
+ code: `
102
+ try {
103
+ await apiRequest();
104
+ } catch (error) {
105
+ throw error;
106
+ }`,
107
+ },
108
+ {
109
+ name: 'bare re-throw in .js file (skipped)',
110
+ filename: '/path/to/helper.js',
111
+ code: `
112
+ try {
113
+ apiRequest();
114
+ } catch (error) {
115
+ throw error;
116
+ }`,
117
+ },
118
+ ],
119
+ invalid: [
120
+ {
121
+ name: 'bare re-throw of caught error',
122
+ code: `
123
+ try {
124
+ await apiRequest();
125
+ } catch (error) {
126
+ throw error;
127
+ }`,
128
+ errors: [{ messageId: 'useNodeApiError' }],
129
+ },
130
+ {
131
+ name: 'throw new Error in catch block',
132
+ code: `
133
+ try {
134
+ await apiRequest();
135
+ } catch (error) {
136
+ throw new Error('Request failed');
137
+ }`,
138
+ errors: [
139
+ {
140
+ messageId: 'useNodeApiErrorInsteadOfGeneric',
141
+ data: { errorClass: 'Error' },
142
+ },
143
+ ],
144
+ },
145
+ {
146
+ name: 'bare re-throw after continueOnFail',
147
+ code: `
148
+ try {
149
+ responseData = await apiRequest.call(this, 'POST', '/tasks', body);
150
+ } catch (error) {
151
+ if (this.continueOnFail()) {
152
+ returnData.push({ json: { error: error.message } });
153
+ continue;
154
+ }
155
+ throw error;
156
+ }`,
157
+ errors: [{ messageId: 'useNodeApiError' }],
158
+ },
159
+ {
160
+ name: 'throw new TypeError in catch block',
161
+ code: `
162
+ try {
163
+ JSON.parse(data);
164
+ } catch (error) {
165
+ throw new TypeError('Invalid JSON');
166
+ }`,
167
+ errors: [
168
+ {
169
+ messageId: 'useNodeApiErrorInsteadOfGeneric',
170
+ data: { errorClass: 'TypeError' },
171
+ },
172
+ ],
173
+ },
174
+ {
175
+ name: 'bare re-throw in nested catch',
176
+ code: `
177
+ try {
178
+ try {
179
+ await apiRequest();
180
+ } catch (innerError) {
181
+ throw innerError;
182
+ }
183
+ } catch (outerError) {
184
+ throw new NodeApiError(this.getNode(), outerError as JsonObject);
185
+ }`,
186
+ errors: [{ messageId: 'useNodeApiError' }],
187
+ },
188
+ {
189
+ name: 'throw named variable in catch',
190
+ code: `
191
+ try {
192
+ await apiRequest();
193
+ } catch (e) {
194
+ throw e;
195
+ }`,
196
+ errors: [{ messageId: 'useNodeApiError' }],
197
+ },
198
+ ],
199
+ });