feat: Add require-version lint rule for community nodes (no-changelog) (#32302)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Garrit Franke
2026-06-16 07:01:46 +00:00
committed by GitHub
co-authored by Cursor
parent e6bc5012ba
commit 105858976c
6 changed files with 208 additions and 0 deletions
@@ -80,6 +80,7 @@ export default [
| [require-continue-on-fail](docs/rules/require-continue-on-fail.md) | Require continueOnFail() handling in execute() methods of node classes | ✅ ☑️ | | | | |
| [require-node-api-error](docs/rules/require-node-api-error.md) | Require NodeApiError or NodeOperationError for error wrapping in catch blocks. Raw errors lose HTTP context in the n8n UI. | ✅ ☑️ | | | | |
| [require-node-description-fields](docs/rules/require-node-description-fields.md) | Node class description must define all required fields: icon, subtitle | ✅ ☑️ | | | | |
| [require-version](docs/rules/require-version.md) | Require a valid "version" field in community node package.json | ✅ ☑️ | | | | |
| [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 | ✅ ☑️ | | | | |
@@ -0,0 +1,51 @@
# Require a valid "version" field in community node package.json (`@n8n/community-nodes/require-version`)
💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
<!-- end auto-generated rule header -->
## Rule Details
Every community node package must declare a `version` field in its `package.json`. npm refuses to publish a package without a valid [semantic version](https://semver.org/), and n8n relies on the version to track and update installed community packages.
This rule reports a missing `version` key as well as a value that is not a valid semantic version string (for example `"1.0"`, `"v1.0.0"`, a range like `"^1.0.0"`, an empty string, or a non-string value).
## Examples
### ❌ Incorrect
```json
{
"name": "n8n-nodes-acme"
}
```
```json
{
"name": "n8n-nodes-acme",
"version": "1.0"
}
```
```json
{
"name": "n8n-nodes-acme",
"version": "v1.0.0"
}
```
### ✅ Correct
```json
{
"name": "n8n-nodes-acme",
"version": "1.0.0"
}
```
```json
{
"name": "n8n-nodes-acme",
"version": "0.1.0-beta.1"
}
```
@@ -54,6 +54,7 @@ const configs = {
'@n8n/community-nodes/require-continue-on-fail': 'error',
'@n8n/community-nodes/require-node-api-error': 'error',
'@n8n/community-nodes/require-node-description-fields': 'error',
'@n8n/community-nodes/require-version': 'error',
'@n8n/community-nodes/valid-credential-references': 'error',
'@n8n/community-nodes/valid-description': 'error',
'@n8n/community-nodes/valid-peer-dependencies': 'error',
@@ -98,6 +99,7 @@ const configs = {
'@n8n/community-nodes/require-continue-on-fail': 'error',
'@n8n/community-nodes/require-node-api-error': 'error',
'@n8n/community-nodes/require-node-description-fields': 'error',
'@n8n/community-nodes/require-version': 'error',
'@n8n/community-nodes/valid-credential-references': 'error',
'@n8n/community-nodes/valid-description': 'error',
'@n8n/community-nodes/valid-peer-dependencies': 'error',
@@ -34,6 +34,7 @@ import { RequireCommunityNodeKeywordRule } from './require-community-node-keywor
import { RequireContinueOnFailRule } from './require-continue-on-fail.js';
import { RequireNodeApiErrorRule } from './require-node-api-error.js';
import { RequireNodeDescriptionFieldsRule } from './require-node-description-fields.js';
import { RequireVersionRule } from './require-version.js';
import { ResourceOperationPatternRule } from './resource-operation-pattern.js';
import { ValidCredentialReferencesRule } from './valid-credential-references.js';
import { ValidDescriptionRule } from './valid-description.js';
@@ -76,6 +77,7 @@ export const rules = {
'require-continue-on-fail': RequireContinueOnFailRule,
'require-node-api-error': RequireNodeApiErrorRule,
'require-node-description-fields': RequireNodeDescriptionFieldsRule,
'require-version': RequireVersionRule,
'valid-credential-references': ValidCredentialReferencesRule,
'valid-description': ValidDescriptionRule,
'valid-peer-dependencies': ValidPeerDependenciesRule,
@@ -0,0 +1,90 @@
import { RuleTester } from '@typescript-eslint/rule-tester';
import { RequireVersionRule } from './require-version.js';
const ruleTester = new RuleTester();
ruleTester.run('require-version', RequireVersionRule, {
valid: [
{
name: 'version is a valid semver string',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
},
{
name: 'version with pre-release and build metadata',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "version": "1.2.3-beta.1+build.5" }',
},
{
name: 'zero version is valid',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "version": "0.1.0" }',
},
{
name: 'non-package.json file is ignored',
filename: 'some-config.json',
code: '{ "name": "n8n-nodes-example" }',
},
{
name: 'nested objects with missing version are not checked',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "version": "1.0.0", "config": { "nested": "value" } }',
},
{
name: 'objects inside arrays (e.g. contributors) are not flagged',
filename: 'package.json',
code: `{
"name": "n8n-nodes-example",
"version": "1.0.0",
"contributors": [
{ "name": "Alice", "email": "alice@example.com" }
]
}`,
},
],
invalid: [
{
name: 'version field is missing entirely',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "description": "Example" }',
errors: [{ messageId: 'missingVersion' }],
},
{
name: 'empty package.json object',
filename: 'package.json',
code: '{}',
errors: [{ messageId: 'missingVersion' }],
},
{
name: 'version is an empty string',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "version": "" }',
errors: [{ messageId: 'invalidVersion' }],
},
{
name: 'version is not a valid semver',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "version": "1.0" }',
errors: [{ messageId: 'invalidVersion' }],
},
{
name: 'version has a leading "v"',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "version": "v1.0.0" }',
errors: [{ messageId: 'invalidVersion' }],
},
{
name: 'version is a range, not an exact version',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "version": "^1.0.0" }',
errors: [{ messageId: 'invalidVersion' }],
},
{
name: 'version is a number, not a string',
filename: 'package.json',
code: '{ "name": "n8n-nodes-example", "version": 1 }',
errors: [{ messageId: 'invalidVersion' }],
},
],
});
@@ -0,0 +1,62 @@
import type { TSESTree } from '@typescript-eslint/utils';
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
import { createRule, findJsonProperty } from '../utils/index.js';
// Official SemVer 2.0.0 regex (https://semver.org/), anchored.
const SEMVER_REGEX =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
export const RequireVersionRule = createRule({
name: 'require-version',
meta: {
type: 'problem',
docs: {
description: 'Require a valid "version" field in community node package.json',
},
messages: {
missingVersion:
'The package.json must have a "version" field. npm requires a valid semantic version (e.g. "1.0.0") to publish the package.',
invalidVersion:
'The "version" field must be a valid semantic version string (e.g. "1.0.0"), got {{ value }}.',
},
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 versionProp = findJsonProperty(node, 'version');
if (!versionProp) {
context.report({
node,
messageId: 'missingVersion',
});
return;
}
const valueNode = versionProp.value;
const value = valueNode.type === AST_NODE_TYPES.Literal ? valueNode.value : null;
if (typeof value !== 'string' || !SEMVER_REGEX.test(value)) {
const rawValue =
valueNode.type === AST_NODE_TYPES.Literal ? String(valueNode.raw) : 'non-literal';
context.report({
node: versionProp,
messageId: 'invalidVersion',
data: { value: rawValue },
});
}
},
};
},
});