feat: Add no-runtime-dependencies ESLint rule (#29366)

This commit is contained in:
Garrit Franke
2026-05-05 08:26:14 +00:00
committed by GitHub
parent 0f7776e972
commit 8aace75535
6 changed files with 163 additions and 0 deletions
@@ -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 | ✅ ☑️ | | 🔧 | | |
@@ -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`.
<!-- end auto-generated rule header -->
## 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": "*"
}
}
```
@@ -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',
@@ -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,
@@ -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' }],
},
],
});
@@ -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',
});
},
};
},
});