diff --git a/packages/@n8n/eslint-plugin-community-nodes/README.md b/packages/@n8n/eslint-plugin-community-nodes/README.md index e1ba068e4d6..2ce5ae04c74 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/README.md +++ b/packages/@n8n/eslint-plugin-community-nodes/README.md @@ -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) | ✅ ☑️ | | | | | diff --git a/packages/@n8n/eslint-plugin-community-nodes/docs/rules/valid-description.md b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/valid-description.md new file mode 100644 index 00000000000..9d38bdbe6e3 --- /dev/null +++ b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/valid-description.md @@ -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`. + + + +## 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" +} +``` diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts b/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts index f98e7e2264c..c4359cf31ef 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts @@ -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', }, 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 bd265abbcdc..935b137173a 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts @@ -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; diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/valid-description.test.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/valid-description.test.ts new file mode 100644 index 00000000000..563470a36d0 --- /dev/null +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/valid-description.test.ts @@ -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' }], + }, + ], +}); diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/valid-description.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/valid-description.ts new file mode 100644 index 00000000000..63c8900b41a --- /dev/null +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/valid-description.ts @@ -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', + }); + } + }, + }; + }, +});