CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-commitlint--types

Shared TypeScript type definitions for the commitlint ecosystem.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

plugins.mddocs/

Plugin System

The plugin system provides types for creating and integrating custom rules and functionality through the commitlint plugin architecture. This enables extending commitlint with organization-specific rules and validation logic.

Plugin Structure

Plugin Interface

interface Plugin {
  rules: {
    [ruleName: string]: Rule | AsyncRule | SyncRule;
  };
}

A plugin is an object containing a collection of custom rules:

  • rules: Record mapping rule names to rule implementation functions
  • Rule names should be unique within the plugin
  • Rules can be synchronous, asynchronous, or support both execution modes

Plugin Records

type PluginRecords = Record<string, Plugin>;

Registry of loaded plugins indexed by plugin name. This is the resolved form after plugin loading and registration.

Rule Implementation Types

Base Rule Function

type BaseRule<Value = never, Type extends RuleType = "either"> = (
  parsed: Commit,
  when?: RuleConfigCondition,
  value?: Value,
) => Type extends "either"
  ? RuleOutcome | Promise<RuleOutcome>
  : Type extends "async"
    ? Promise<RuleOutcome>
    : Type extends "sync"
      ? RuleOutcome
      : never;

Generic rule function signature:

  • parsed: Parsed commit object from conventional-commits-parser
  • when: Rule condition ("always" or "never")
  • value: Optional configuration value specific to the rule
  • Returns RuleOutcome for sync rules, Promise<RuleOutcome> for async rules

Specialized Rule Types

type Rule<Value = never> = BaseRule<Value, "either">;
type AsyncRule<Value = never> = BaseRule<Value, "async">;
type SyncRule<Value = never> = BaseRule<Value, "sync">;

Specialized rule function types:

  • Rule: Can be either synchronous or asynchronous
  • AsyncRule: Must return a Promise (for async operations)
  • SyncRule: Must return synchronously (for simple validations)

Rule Outcomes

type RuleOutcome = Readonly<[boolean, string?]>;

Rule execution result:

  • boolean: true if rule passes, false if rule fails
  • string (optional): Error message when rule fails

Rule Types

type RuleType = "async" | "sync" | "either";

Execution type constraints for rules to ensure proper async handling.

Usage Examples

Basic Plugin Creation

import { Plugin, Rule, RuleOutcome } from "@commitlint/types";

// Simple synchronous rule
const requireJiraTicket: Rule = (parsed, when) => {
  const hasJiraTicket = /[A-Z]+-\d+/.test(parsed.subject || "");
  const shouldHave = when === "always";
  
  if (shouldHave && !hasJiraTicket) {
    return [false, "Subject must contain JIRA ticket (e.g., ABC-123)"];
  }
  
  if (!shouldHave && hasJiraTicket) {
    return [false, "Subject must not contain JIRA ticket"];
  }
  
  return [true];
};

// Plugin definition
const jiraPlugin: Plugin = {
  rules: {
    "jira-ticket-in-subject": requireJiraTicket
  }
};

Async Plugin with External Validation

import { Plugin, AsyncRule } from "@commitlint/types";

// Async rule that validates against external service
const validateTicketExists: AsyncRule<{ apiUrl: string }> = async (parsed, when, value) => {
  if (when !== "always" || !value?.apiUrl) {
    return [true];
  }
  
  const ticketMatch = parsed.subject?.match(/([A-Z]+-\d+)/);
  if (!ticketMatch) {
    return [false, "No ticket found in subject"];
  }
  
  const ticketId = ticketMatch[1];
  
  try {
    const response = await fetch(`${value.apiUrl}/tickets/${ticketId}`);
    if (!response.ok) {
      return [false, `Ticket ${ticketId} does not exist or is not accessible`];
    }
    return [true];
  } catch (error) {
    return [false, `Failed to validate ticket ${ticketId}: ${error.message}`];
  }
};

const ticketValidationPlugin: Plugin = {
  rules: {
    "ticket-exists": validateTicketExists
  }
};

Complex Plugin with Multiple Rules

import { Plugin, Rule, AsyncRule, SyncRule } from "@commitlint/types";

// Synchronous rules
const noWipCommits: SyncRule = (parsed, when) => {
  const isWip = /^wip|^WIP/.test(parsed.subject || "");
  if (when === "never" && isWip) {
    return [false, "WIP commits are not allowed"];
  }
  return [true];
};

const requireCoAuthor: Rule<{ required: boolean }> = (parsed, when, value) => {
  const hasCoAuthor = /Co-authored-by:/i.test(parsed.body || "");
  const shouldRequire = when === "always" && value?.required;
  
  if (shouldRequire && !hasCoAuthor) {
    return [false, "Commit must include Co-authored-by trailer"];
  }
  
  return [true];
};

// Async rule for validation
const validateBranchName: AsyncRule<{ allowedPatterns: string[] }> = async (parsed, when, value) => {
  if (when !== "always" || !value?.allowedPatterns) {
    return [true];
  }
  
  // Simulate getting current branch (in real implementation, use git)
  const currentBranch = process.env.GIT_BRANCH || "main";
  
  const isAllowed = value.allowedPatterns.some(pattern => {
    const regex = new RegExp(pattern);
    return regex.test(currentBranch);
  });
  
  if (!isAllowed) {
    return [false, `Branch "${currentBranch}" does not match allowed patterns`];
  }
  
  return [true];
};

const organizationPlugin: Plugin = {
  rules: {
    "no-wip-commits": noWipCommits,
    "require-co-author": requireCoAuthor,
    "validate-branch-name": validateBranchName
  }
};

Plugin Configuration

import { Plugin, UserConfig } from "@commitlint/types";

// Plugin as object
const customPlugin: Plugin = {
  rules: {
    "custom-rule": (parsed, when, value) => [true]
  }
};

// Configuration using plugins
const config: UserConfig = {
  // Load plugins by name (must be installed)
  plugins: [
    "@my-org/commitlint-plugin",
    "commitlint-plugin-custom"
  ],
  
  // Use custom plugin object
  plugins: [customPlugin],
  
  // Mixed plugins
  plugins: [
    "@my-org/commitlint-plugin", // npm package
    customPlugin,                // inline plugin
    "local-plugin"              // local module
  ],
  
  // Configure plugin rules
  rules: {
    // Standard rules
    "type-enum": [2, "always", ["feat", "fix"]],
    
    // Plugin rules (prefixed with plugin scope)
    "jira-ticket-in-subject": [2, "always"],
    "ticket-exists": [1, "always", { apiUrl: "https://api.company.com" }],
    "require-co-author": [1, "always", { required: true }],
    "validate-branch-name": [2, "always", { 
      allowedPatterns: ["^feature/", "^bugfix/", "^hotfix/"] 
    }]
  }
};

Plugin Development Best Practices

import { Plugin, Rule, RuleOutcome } from "@commitlint/types";

// Template for creating robust plugins
function createPlugin(options: { 
  prefix?: string;
  defaultSeverity?: number;
}): Plugin {
  const { prefix = "", defaultSeverity = 2 } = options;
  
  // Helper to create consistent rule names
  const ruleName = (name: string) => prefix ? `${prefix}-${name}` : name;
  
  // Helper to validate rule configuration
  const validateConfig = <T>(value: T, validator: (v: T) => boolean): RuleOutcome => {
    if (!validator(value)) {
      return [false, "Invalid rule configuration"];
    }
    return [true];
  };
  
  // Example rule with validation
  const exampleRule: Rule<{ pattern: string; message?: string }> = (parsed, when, value) => {
    // Validate configuration
    if (value && typeof value.pattern !== "string") {
      return [false, "Rule requires 'pattern' configuration"];
    }
    
    const pattern = value?.pattern || ".*";
    const message = value?.message || `Subject must match pattern: ${pattern}`;
    
    const regex = new RegExp(pattern);
    const matches = regex.test(parsed.subject || "");
    
    if (when === "always" && !matches) {
      return [false, message];
    }
    
    if (when === "never" && matches) {
      return [false, `Subject must not match pattern: ${pattern}`];
    }
    
    return [true];
  };
  
  return {
    rules: {
      [ruleName("example")]: exampleRule
    }
  };
}

// Usage
const myPlugin = createPlugin({ 
  prefix: "myorg",
  defaultSeverity: 1 
});

Plugin Testing

import { Plugin, Rule } from "@commitlint/types";

// Test helper for plugin rules
function testRule(
  rule: Rule,
  commit: { subject?: string; body?: string },
  when: "always" | "never" = "always",
  value?: unknown
) {
  const parsed = {
    subject: commit.subject || "",
    body: commit.body || "",
    header: commit.subject || "",
    type: null,
    scope: null,
    footer: null,
    notes: [],
    references: [],
    mentions: [],
    revert: null
  };
  
  return rule(parsed, when, value);
}

// Example tests
const plugin: Plugin = {
  rules: {
    "require-ticket": (parsed, when) => {
      const hasTicket = /TICKET-\d+/.test(parsed.subject || "");
      if (when === "always" && !hasTicket) {
        return [false, "Subject must contain ticket number"];
      }
      return [true];
    }
  }
};

// Test cases
console.log(testRule(
  plugin.rules["require-ticket"],
  { subject: "fix: something" }
)); // [false, "Subject must contain ticket number"]

console.log(testRule(
  plugin.rules["require-ticket"],
  { subject: "fix: TICKET-123 something" }
)); // [true]

docs

case-validation.md

configuration.md

formatting.md

index.md

linting.md

parsing.md

plugins.md

prompts.md

rules-config.md

tile.json