or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

case-validation.mdconfiguration.mdformatting.mdindex.mdlinting.mdparsing.mdplugins.mdprompts.mdrules-config.md
tile.json

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]