diff --git a/.claude/plugins/n8n/skills/create-community-node-lint-rule/SKILL.md b/.claude/plugins/n8n/skills/create-community-node-lint-rule/SKILL.md new file mode 100644 index 00000000000..41c6e2cafb6 --- /dev/null +++ b/.claude/plugins/n8n/skills/create-community-node-lint-rule/SKILL.md @@ -0,0 +1,218 @@ +--- +description: >- + Create new ESLint rules for the @n8n/eslint-plugin-community-nodes package. + Use when adding a lint rule, creating a community node lint, or working on + eslint-plugin-community-nodes. Guides rule implementation, tests, docs, and + plugin registration. +user_invocable: true +--- + +# Create Community Node Lint Rule + +Guide for adding new ESLint rules to `packages/@n8n/eslint-plugin-community-nodes/`. + +All paths below are relative to `packages/@n8n/eslint-plugin-community-nodes/`. + +## Step 1: Understand the Rule + +Before writing code, clarify: +- **What** does the rule detect? (missing property, wrong pattern, bad value) +- **Where** does it apply? (`.node.ts` files, credential classes, both) +- **Severity**: `error` (must fix) or `warn` (should fix)? +- **Fixable?** Can it be auto-fixed safely, or only suggest? +- **Scope**: Both `recommended` configs, or exclude from `recommendedWithoutN8nCloudSupport`? + +## Step 2: Implement the Rule + +Create `src/rules/.ts`: + +```typescript +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import { + isNodeTypeClass, // or isCredentialTypeClass + findClassProperty, + findObjectProperty, + createRule, +} from '../utils/index.js'; + +export const YourRuleNameRule = createRule({ + name: 'rule-name', + meta: { + type: 'problem', // or 'suggestion' + docs: { + description: 'One-line description of what the rule enforces', + }, + messages: { + messageId: 'Human-readable message. Use {{placeholder}} for dynamic data.', + }, + fixable: 'code', // omit if not auto-fixable + hasSuggestions: true, // omit if no suggestions + schema: [], // add options schema if configurable + }, + defaultOptions: [], + create(context) { + return { + ClassDeclaration(node) { + if (!isNodeTypeClass(node)) return; + + const descriptionProperty = findClassProperty(node, 'description'); + if (!descriptionProperty) return; + + const descriptionValue = descriptionProperty.value; + if (descriptionValue?.type !== AST_NODE_TYPES.ObjectExpression) return; + + // Rule logic here — use findObjectProperty(), getLiteralValue(), etc. + + context.report({ + node: targetNode, + messageId: 'messageId', + data: { /* template vars */ }, + fix(fixer) { + return fixer.replaceText(targetNode, 'replacement'); + }, + }); + }, + }; + }, +}); +``` + +**Naming**: Export as `PascalCaseRule` (e.g. `MissingPairedItemRule`). The `name` field is kebab-case. + +**Available AST helpers** — see [reference.md](reference.md) for the full catalog of `ast-utils` and `file-utils` exports. + +## Step 3: Write Tests + +Create `src/rules/.test.ts`: + +```typescript +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import { YourRuleNameRule } from './rule-name.js'; + +const ruleTester = new RuleTester(); + +// Helper to generate test code — keeps test cases readable +function createNodeCode(/* parameterize the varying parts */): string { + return ` +import type { INodeType, INodeTypeDescription } from 'n8n-workflow'; + +export class TestNode implements INodeType { + description: INodeTypeDescription = { + displayName: 'Test Node', + name: 'testNode', + group: ['input'], + version: 1, + description: 'A test node', + defaults: { name: 'Test Node' }, + inputs: [], + outputs: [], + properties: [], + }; +}`; +} + +ruleTester.run('rule-name', YourRuleNameRule, { + valid: [ + { name: 'class that does not implement INodeType', code: '...' }, + { name: 'node with correct pattern', code: createNodeCode(/* correct */) }, + ], + invalid: [ + { + name: 'descriptive case name', + code: createNodeCode(/* incorrect */), + errors: [{ messageId: 'messageId', data: { /* expected template vars */ } }], + output: createNodeCode(/* expected after fix */), // or `output: null` if no fix + }, + ], +}); +``` + +**Test guidelines:** +- Always test that non-INodeType classes are skipped (valid case) +- Test both the error message and the fixed output for fixable rules +- For rules with options, test each option combination +- For rules using filesystem, mock with `vi.mock('../utils/file-utils.js')` +- For suggestion-only rules, use `errors: [{ messageId, suggestions: [...] }]` + +## Step 4: Register the Rule + +### 4a. Add to `src/rules/index.ts` + +```typescript +import { YourRuleNameRule } from './rule-name.js'; + +// Add to the rules object: +export const rules = { + // ... existing rules + 'rule-name': YourRuleNameRule, +} satisfies Record; +``` + +### 4b. Add to `src/plugin.ts` configs + +Add to **both** config objects (unless the rule depends on n8n cloud features): + +```typescript +'@n8n/community-nodes/rule-name': 'error', // or 'warn' +``` + +- Use `error` for rules that catch bugs or required patterns +- Use `warn` for style/convention rules (like `options-sorted-alphabetically`) +- If the rule uses `no-restricted-globals` or `no-restricted-imports` patterns, + only add to `recommended` (not `recommendedWithoutN8nCloudSupport`) + +## Step 5: Write Documentation + +Create `docs/rules/.md`: + +```markdown +# Description of what the rule does (`@n8n/community-nodes/rule-name`) + + + +## Rule Details + +Explain why this rule exists and what problem it prevents. + +## Examples + +### Incorrect + +\`\`\`typescript +// code that triggers the rule +\`\`\` + +### Correct + +\`\`\`typescript +// code that passes the rule +\`\`\` +``` + +The header above `` will be regenerated by `pnpm build:docs`. Write a reasonable first version — it gets overwritten. + +## Step 6: Verify + +Run from `packages/@n8n/eslint-plugin-community-nodes/`: + +```bash +pushd packages/@n8n/eslint-plugin-community-nodes +pnpm test .test.ts # tests pass +pnpm typecheck # types are clean +pnpm build # compiles +pnpm build:docs # regenerates doc headers and README table +pnpm lint:docs # docs match schema +popd +``` + +## Checklist + +- [ ] Rule file: `src/rules/.ts` +- [ ] Test file: `src/rules/.test.ts` +- [ ] Registered in `src/rules/index.ts` +- [ ] Added to configs in `src/plugin.ts` +- [ ] Doc file: `docs/rules/.md` +- [ ] README table updated via `pnpm build:docs` +- [ ] All verification commands pass diff --git a/.claude/plugins/n8n/skills/create-community-node-lint-rule/reference.md b/.claude/plugins/n8n/skills/create-community-node-lint-rule/reference.md new file mode 100644 index 00000000000..92cbab52d91 --- /dev/null +++ b/.claude/plugins/n8n/skills/create-community-node-lint-rule/reference.md @@ -0,0 +1,85 @@ +# AST & File Utilities Reference + +Helpers available from `../utils/index.js`. Use these instead of writing custom AST traversal. + +## ast-utils.ts + +### Class/Interface detection + +| Function | Returns | Use when | +|----------|---------|----------| +| `isNodeTypeClass(node)` | `boolean` | Check if class implements `INodeType` or extends `Node` | +| `isCredentialTypeClass(node)` | `boolean` | Check if class implements `ICredentialType` | + +### Property finding + +| Function | Returns | Use when | +|----------|---------|----------| +| `findClassProperty(node, name)` | `PropertyDefinition \| null` | Find a property on a class (e.g. `description`, `icon`) | +| `findObjectProperty(obj, name)` | `Property \| null` | Find a property in an object literal (Identifier key) | +| `findJsonProperty(obj, name)` | `Property \| null` | Find a property with a Literal key (JSON-style `"key"`) | +| `findArrayLiteralProperty(obj, name)` | `Property \| null` | Find a property whose value is an ArrayExpression | + +### Value extraction + +| Function | Returns | Use when | +|----------|---------|----------| +| `getLiteralValue(node)` | `string \| boolean \| number \| null` | Extract primitive from a Literal node | +| `getStringLiteralValue(node)` | `string \| null` | Extract string specifically | +| `getBooleanLiteralValue(node)` | `boolean \| null` | Extract boolean specifically | +| `getModulePath(node)` | `string \| null` | Get import path from string literal or template literal | + +### Array operations + +| Function | Returns | Use when | +|----------|---------|----------| +| `hasArrayLiteralValue(arr, value)` | `boolean` | Check if array contains a specific string literal | +| `extractCredentialInfoFromArray(element)` | `{ name, testedBy } \| null` | Parse credential object from array element | +| `extractCredentialNameFromArray(element)` | `string \| null` | Get just the credential name from array element | + +### Method matching + +| Function | Returns | Use when | +|----------|---------|----------| +| `isThisHelpersAccess(node)` | `boolean` | Match `this.helpers` member expression | +| `isThisMethodCall(node, method)` | `boolean` | Match `this.methodName(...)` calls | +| `isThisHelpersMethodCall(node, method)` | `boolean` | Match `this.helpers.methodName(...)` calls | + +### Similarity + +| Function | Returns | Use when | +|----------|---------|----------| +| `findSimilarStrings(target, candidates, maxDistance?)` | `string[]` | Suggest similar names (Levenshtein distance) | + +## file-utils.ts + +### Path operations + +| Function | Use when | +|----------|----------| +| `isContainedWithin(child, parent)` | Check path is within a directory | +| `safeJoinPath(base, ...parts)` | Join paths with traversal prevention | + +### Package.json + +| Function | Returns | Use when | +|----------|---------|----------| +| `findPackageJson(startDir)` | `string \| null` | Walk up to find nearest package.json | +| `readPackageJsonN8n(startDir)` | `N8nPackageJson \| null` | Parse n8n config section | +| `readPackageJsonCredentials(startDir)` | `Set` | Get credential names from package.json | +| `readPackageJsonNodes(startDir)` | `string[]` | Get resolved node file paths | + +### File system + +| Function | Use when | +|----------|----------| +| `validateIconPath(filePath, iconValue)` | Check icon file exists and is SVG | +| `extractCredentialNameFromFile(filePath)` | Parse credential class name from file | +| `fileExistsWithCaseSync(filePath)` | Case-sensitive existence check | +| `findSimilarSvgFiles(dir, name)` | Suggest similar SVG filenames | + +### Credential verification + +| Function | Use when | +|----------|----------| +| `areAllCredentialUsagesTestedByNodes(startDir)` | Check all credentials have testedBy |