feat(core): Add valid-description ESLint rule for community nodes (no-changelog) (#30318)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garrit Franke
2026-05-26 07:34:02 +00:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 267658e076
commit 1623f2428c
6 changed files with 155 additions and 0 deletions
@@ -77,6 +77,7 @@ export default [
| [require-node-description-fields](docs/rules/require-node-description-fields.md) | Node class description must define all required fields: icon, subtitle | ✅ ☑️ | | | | |
| [resource-operation-pattern](docs/rules/resource-operation-pattern.md) | Enforce proper resource/operation pattern for better UX in n8n nodes | | ✅ ☑️ | | | |
| [valid-credential-references](docs/rules/valid-credential-references.md) | Ensure credentials referenced in node descriptions exist as credential classes in the package | ✅ ☑️ | | | 💡 | |
| [valid-description](docs/rules/valid-description.md) | Require a non-empty "description" field in community node package.json | ✅ ☑️ | | | | |
| [valid-peer-dependencies](docs/rules/valid-peer-dependencies.md) | Require community node package.json peerDependencies to contain only "n8n-workflow": "*" (and optionally "ai-node-sdk") | ✅ ☑️ | | 🔧 | | |
| [webhook-lifecycle-complete](docs/rules/webhook-lifecycle-complete.md) | Require webhook trigger nodes to implement the complete webhookMethods lifecycle (checkExists, create, delete) | ✅ ☑️ | | | | |
@@ -0,0 +1,37 @@
# Require a non-empty "description" field in community node package.json (`@n8n/community-nodes/valid-description`)
💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
<!-- end auto-generated rule header -->
## Rule Details
The `description` field in `package.json` is what users see when discovering your community node package on npm and inside n8n. It must be present and non-empty so that users can understand what the package does before installing it.
The `n8n-nodes-starter` template ships with `"description": ""`, which is a common oversight. This rule catches both a missing `description` key and the unchanged placeholder.
## Examples
### ❌ Incorrect
```json
{
"name": "n8n-nodes-acme"
}
```
```json
{
"name": "n8n-nodes-acme",
"description": ""
}
```
### ✅ Correct
```json
{
"name": "n8n-nodes-acme",
"description": "n8n community nodes for the Acme Corp API"
}
```
@@ -51,6 +51,7 @@ const configs = {
'@n8n/community-nodes/require-node-api-error': 'error',
'@n8n/community-nodes/require-node-description-fields': 'error',
'@n8n/community-nodes/valid-credential-references': 'error',
'@n8n/community-nodes/valid-description': 'error',
'@n8n/community-nodes/valid-peer-dependencies': 'error',
'@n8n/community-nodes/webhook-lifecycle-complete': 'error',
},
@@ -90,6 +91,7 @@ const configs = {
'@n8n/community-nodes/require-node-api-error': 'error',
'@n8n/community-nodes/require-node-description-fields': 'error',
'@n8n/community-nodes/valid-credential-references': 'error',
'@n8n/community-nodes/valid-description': 'error',
'@n8n/community-nodes/valid-peer-dependencies': 'error',
'@n8n/community-nodes/webhook-lifecycle-complete': 'error',
},
@@ -32,6 +32,7 @@ import { RequireNodeApiErrorRule } from './require-node-api-error.js';
import { RequireNodeDescriptionFieldsRule } from './require-node-description-fields.js';
import { ResourceOperationPatternRule } from './resource-operation-pattern.js';
import { ValidCredentialReferencesRule } from './valid-credential-references.js';
import { ValidDescriptionRule } from './valid-description.js';
import { ValidPeerDependenciesRule } from './valid-peer-dependencies.js';
import { WebhookLifecycleCompleteRule } from './webhook-lifecycle-complete.js';
@@ -68,6 +69,7 @@ export const rules = {
'require-node-api-error': RequireNodeApiErrorRule,
'require-node-description-fields': RequireNodeDescriptionFieldsRule,
'valid-credential-references': ValidCredentialReferencesRule,
'valid-description': ValidDescriptionRule,
'valid-peer-dependencies': ValidPeerDependenciesRule,
'webhook-lifecycle-complete': WebhookLifecycleCompleteRule,
} satisfies Record<string, AnyRuleModule>;
@@ -0,0 +1,56 @@
import { RuleTester } from '@typescript-eslint/rule-tester';
import { ValidDescriptionRule } from './valid-description.js';
const ruleTester = new RuleTester();
ruleTester.run('valid-description', ValidDescriptionRule, {
valid: [
{
name: 'description is a non-empty string',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "description": "Nodes for talking to Acme Corp\'s API" }',
},
{
name: 'non-package.json file is ignored',
filename: 'some-config.json',
code: '{ "name": "n8n-nodes-example" }',
},
{
name: 'nested objects with empty description are not checked',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "description": "Real description", "config": { "description": "" } }',
},
{
name: 'objects inside arrays (e.g. contributors) are not flagged',
filename: 'package.json',
code: `{
"name": "n8n-nodes-example",
"description": "Real description",
"contributors": [
{ "name": "Alice", "description": "" }
]
}`,
},
],
invalid: [
{
name: 'description field is missing entirely',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
errors: [{ messageId: 'missingDescription' }],
},
{
name: 'description is an empty string (starter template default)',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "description": "" }',
errors: [{ messageId: 'emptyDescription' }],
},
{
name: 'empty package.json object',
filename: 'package.json',
code: '{}',
errors: [{ messageId: 'missingDescription' }],
},
],
});
@@ -0,0 +1,57 @@
import type { TSESTree } from '@typescript-eslint/utils';
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
import { createRule, findJsonProperty } from '../utils/index.js';
export const ValidDescriptionRule = createRule({
name: 'valid-description',
meta: {
type: 'problem',
docs: {
description: 'Require a non-empty "description" field in community node package.json',
},
messages: {
missingDescription:
'The package.json must have a "description" field describing what the community node package does.',
emptyDescription:
'The "description" field must be a non-empty string. Replace the placeholder with a real description of your community node package.',
},
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 descriptionProp = findJsonProperty(node, 'description');
if (!descriptionProp) {
context.report({
node,
messageId: 'missingDescription',
});
return;
}
if (descriptionProp.value.type !== AST_NODE_TYPES.Literal) {
return;
}
const value = descriptionProp.value.value;
if (typeof value !== 'string' || value === '') {
context.report({
node: descriptionProp,
messageId: 'emptyDescription',
});
}
},
};
},
});