From 8aace75535f53ebf37c2a547849e044948c99cb8 Mon Sep 17 00:00:00 2001 From: Garrit Franke <32395585+garritfra@users.noreply.github.com> Date: Tue, 5 May 2026 10:26:14 +0200 Subject: [PATCH] feat: Add no-runtime-dependencies ESLint rule (#29366) --- .../eslint-plugin-community-nodes/README.md | 1 + .../docs/rules/no-runtime-dependencies.md | 58 +++++++++++++++++++ .../src/plugin.ts | 2 + .../src/rules/index.ts | 2 + .../src/rules/no-runtime-dependencies.test.ts | 50 ++++++++++++++++ .../src/rules/no-runtime-dependencies.ts | 50 ++++++++++++++++ 6 files changed, 163 insertions(+) create mode 100644 packages/@n8n/eslint-plugin-community-nodes/docs/rules/no-runtime-dependencies.md create mode 100644 packages/@n8n/eslint-plugin-community-nodes/src/rules/no-runtime-dependencies.test.ts create mode 100644 packages/@n8n/eslint-plugin-community-nodes/src/rules/no-runtime-dependencies.ts diff --git a/packages/@n8n/eslint-plugin-community-nodes/README.md b/packages/@n8n/eslint-plugin-community-nodes/README.md index 8ec86f34e34..f67b22a17ae 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/README.md +++ b/packages/@n8n/eslint-plugin-community-nodes/README.md @@ -60,6 +60,7 @@ export default [ | [no-overrides-field](docs/rules/no-overrides-field.md) | Ban the "overrides" field in community node package.json | ✅ ☑️ | | | | | | [no-restricted-globals](docs/rules/no-restricted-globals.md) | Disallow usage of restricted global variables in community nodes. | ✅ | | | | | | [no-restricted-imports](docs/rules/no-restricted-imports.md) | Disallow usage of restricted imports in community nodes. | ✅ | | | | | +| [no-runtime-dependencies](docs/rules/no-runtime-dependencies.md) | Disallow non-empty "dependencies" in community node package.json | ✅ ☑️ | | | | | | [node-class-description-icon-missing](docs/rules/node-class-description-icon-missing.md) | Node class description must have an `icon` property defined. Deprecated: use `require-node-description-fields` instead. | | | | 💡 | ❌ | | [node-connection-type-literal](docs/rules/node-connection-type-literal.md) | Disallow string literals in node description `inputs`/`outputs` — use `NodeConnectionTypes` enum instead | ✅ ☑️ | | 🔧 | | | | [node-usable-as-tool](docs/rules/node-usable-as-tool.md) | Ensure node classes have usableAsTool property | ✅ ☑️ | | 🔧 | | | diff --git a/packages/@n8n/eslint-plugin-community-nodes/docs/rules/no-runtime-dependencies.md b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/no-runtime-dependencies.md new file mode 100644 index 00000000000..9d224fdeb39 --- /dev/null +++ b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/no-runtime-dependencies.md @@ -0,0 +1,58 @@ +# Disallow non-empty "dependencies" in community node package.json (`@n8n/community-nodes/no-runtime-dependencies`) + +💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`. + + + +## Rule Details + +The `dependencies` field in `package.json` declares packages that are installed alongside the node at runtime. In the context of n8n community nodes this is dangerous: + +- Community nodes run inside the shared n8n runtime alongside all other installed nodes. Any package listed in `dependencies` gets installed into that shared environment and can shadow or conflict with versions already used by n8n or other nodes. +- Unlike application packages, community nodes should not own their runtime environment. Shared libraries must be declared in `peerDependencies` (so the host runtime supplies them) or bundled at build time into the published artifact. +- A non-empty `dependencies` section is a strong signal that the package was scaffolded from a generic Node.js template without adapting it to the n8n community node model. + +## Examples + +### Incorrect + +```json +{ + "name": "n8n-nodes-example", + "dependencies": { + "axios": "1.0.0" + } +} +``` + +```json +{ + "name": "n8n-nodes-example", + "dependencies": { + "axios": "1.7.0", + "fast-xml-parser": "4.4.0", + "minimatch": "9.0.5" + } +} +``` + +### Correct + +```json +{ + "name": "n8n-nodes-example", + "peerDependencies": { + "n8n-workflow": "*" + } +} +``` + +```json +{ + "name": "n8n-nodes-example", + "dependencies": {}, + "peerDependencies": { + "n8n-workflow": "*" + } +} +``` diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts b/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts index 14b8774c8b2..3db6a5814f8 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts @@ -32,6 +32,7 @@ const configs = { '@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error', '@n8n/community-nodes/no-http-request-with-manual-auth': 'error', '@n8n/community-nodes/no-overrides-field': 'error', + '@n8n/community-nodes/no-runtime-dependencies': 'error', '@n8n/community-nodes/icon-validation': 'error', '@n8n/community-nodes/options-sorted-alphabetically': 'warn', '@n8n/community-nodes/resource-operation-pattern': 'warn', @@ -63,6 +64,7 @@ const configs = { '@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error', '@n8n/community-nodes/no-http-request-with-manual-auth': 'error', '@n8n/community-nodes/no-overrides-field': 'error', + '@n8n/community-nodes/no-runtime-dependencies': 'error', '@n8n/community-nodes/icon-validation': 'error', '@n8n/community-nodes/options-sorted-alphabetically': 'warn', '@n8n/community-nodes/credential-documentation-url': 'error', diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts index 7291988cf87..abadaf68c0d 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts @@ -14,6 +14,7 @@ import { NoHttpRequestWithManualAuthRule } from './no-http-request-with-manual-a import { NoOverridesFieldRule } from './no-overrides-field.js'; import { NoRestrictedGlobalsRule } from './no-restricted-globals.js'; import { NoRestrictedImportsRule } from './no-restricted-imports.js'; +import { NoRuntimeDependenciesRule } from './no-runtime-dependencies.js'; import { NodeClassDescriptionIconMissingRule } from './node-class-description-icon-missing.js'; import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js'; import { NodeUsableAsToolRule } from './node-usable-as-tool.js'; @@ -41,6 +42,7 @@ export const rules = { 'no-forbidden-lifecycle-scripts': NoForbiddenLifecycleScriptsRule, 'no-http-request-with-manual-auth': NoHttpRequestWithManualAuthRule, 'no-overrides-field': NoOverridesFieldRule, + 'no-runtime-dependencies': NoRuntimeDependenciesRule, 'icon-validation': IconValidationRule, 'resource-operation-pattern': ResourceOperationPatternRule, 'credential-documentation-url': CredentialDocumentationUrlRule, diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-runtime-dependencies.test.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-runtime-dependencies.test.ts new file mode 100644 index 00000000000..fb71ff3539b --- /dev/null +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-runtime-dependencies.test.ts @@ -0,0 +1,50 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import { NoRuntimeDependenciesRule } from './no-runtime-dependencies.js'; + +const ruleTester = new RuleTester(); + +ruleTester.run('no-runtime-dependencies', NoRuntimeDependenciesRule, { + valid: [ + { + name: 'no dependencies field', + filename: 'package.json', + code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }', + }, + { + name: 'empty dependencies object is allowed', + filename: 'package.json', + code: '{ "name": "n8n-nodes-example", "dependencies": {} }', + }, + { + name: 'non-package.json file is ignored', + filename: 'some-config.json', + code: '{ "dependencies": { "axios": "1.0.0" } }', + }, + { + name: 'nested "dependencies" key inside another field is allowed', + filename: 'package.json', + code: '{ "name": "n8n-nodes-example", "config": { "dependencies": { "axios": "1.0.0" } } }', + }, + ], + invalid: [ + { + name: 'single runtime dependency is forbidden', + filename: 'package.json', + code: '{ "name": "n8n-nodes-example", "dependencies": { "axios": "1.0.0" } }', + errors: [{ messageId: 'runtimeDependenciesForbidden' }], + }, + { + name: 'multiple runtime dependencies are forbidden', + filename: 'package.json', + code: '{ "name": "n8n-nodes-example", "dependencies": { "axios": "1.0.0", "lodash": "^4.0.0" } }', + errors: [{ messageId: 'runtimeDependenciesForbidden' }], + }, + { + name: 'real-world package with bundled deps is forbidden', + filename: 'package.json', + code: '{ "name": "n8n-nodes-sinch", "dependencies": { "axios": "1.7.0", "fast-xml-parser": "4.4.0", "minimatch": "9.0.5" } }', + errors: [{ messageId: 'runtimeDependenciesForbidden' }], + }, + ], +}); diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-runtime-dependencies.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-runtime-dependencies.ts new file mode 100644 index 00000000000..138df01ee0d --- /dev/null +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-runtime-dependencies.ts @@ -0,0 +1,50 @@ +import type { TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import { createRule, findJsonProperty } from '../utils/index.js'; + +export const NoRuntimeDependenciesRule = createRule({ + name: 'no-runtime-dependencies', + meta: { + type: 'problem', + docs: { + description: 'Disallow non-empty "dependencies" in community node package.json', + }, + messages: { + runtimeDependenciesForbidden: + '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.', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + if (!context.filename.endsWith('package.json')) { + return {}; + } + + return { + ObjectExpression(node: TSESTree.ObjectExpression) { + if (node.parent?.type !== AST_NODE_TYPES.ExpressionStatement) { + return; + } + + const depsProp = findJsonProperty(node, 'dependencies'); + if (!depsProp) { + return; + } + + if ( + depsProp.value.type !== AST_NODE_TYPES.ObjectExpression || + depsProp.value.properties.length === 0 + ) { + return; + } + + context.report({ + node: depsProp, + messageId: 'runtimeDependenciesForbidden', + }); + }, + }; + }, +});