mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-21 12:51:16 +08:00
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:
co-authored by
Claude Opus 4.7
parent
267658e076
commit
1623f2428c
@@ -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',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user