Shared TypeScript type definitions for the commitlint ecosystem.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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.
interface Plugin {
rules: {
[ruleName: string]: Rule | AsyncRule | SyncRule;
};
}A plugin is an object containing a collection of custom rules:
type PluginRecords = Record<string, Plugin>;Registry of loaded plugins indexed by plugin name. This is the resolved form after plugin loading and registration.
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:
RuleOutcome for sync rules, Promise<RuleOutcome> for async rulestype Rule<Value = never> = BaseRule<Value, "either">;
type AsyncRule<Value = never> = BaseRule<Value, "async">;
type SyncRule<Value = never> = BaseRule<Value, "sync">;Specialized rule function types:
type RuleOutcome = Readonly<[boolean, string?]>;Rule execution result:
true if rule passes, false if rule failstype RuleType = "async" | "sync" | "either";Execution type constraints for rules to ensure proper async handling.
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
}
};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
}
};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
}
};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/"]
}]
}
};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
});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]