@dev-build-deploy/commit-it
Version:
(Conventional) Commits library
311 lines (310 loc) • 14.5 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.commitRules = void 0;
/*
* SPDX-FileCopyrightText: 2023 Kevin de Jong <monkaii@hotmail.com>
*
* SPDX-License-Identifier: MIT
* SPDX-License-Identifier: CC-BY-3.0
*/
const diagnose_it_1 = require("@dev-build-deploy/diagnose-it");
const chalk_1 = __importDefault(require("chalk"));
const commit_1 = require("./commit");
/**
* Checks whether the provided string is a noun.
* A noun is defined as a single word which can be capitalized or contain hyphens, therefore
* it will not support multi-word nouns (i.e. New York).
*
* @param str String to check
* @returns True if the string is a noun, false otherwise.
*
* @internal
*/
function isNoun(str) {
return /^[A-Za-z][a-z]*(-[A-Za-z][a-z]*)*$/.test(str);
}
/**
* Validates whether the provided type valid.
* A valid type is defined as a single word, all lowercase, no spaces, and no special characters.
*
* @param str String to check
* @returns True if the string is a valid type, false otherwise.
*
* @internal
*/
function isValidType(str) {
return !str.trim().includes(" ") && !/[^a-z]/i.test(str.trim());
}
function highlightString(str, substring) {
// Ensure that we handle both single and multiple substrings equally
if (!Array.isArray(substring))
substring = [substring];
// Replace all instances of substring with a blue version
let result = str;
substring.forEach(sub => (result = result.replace(sub, `${chalk_1.default.cyan(sub)}`)));
return result;
}
function createDiagnosticsMessage(commit, description, highlight, type, whitespace = false, level = diagnose_it_1.DiagnosticsLevelEnum.Error) {
const element = commit[type];
let hintIndex = element.index;
let hintLength = element.value?.trimEnd().length ?? 1;
if (whitespace) {
let prevElement = undefined;
for (const [_key, value] of Object.entries(commit)) {
if (value.index > (prevElement?.index ?? 0) && value.index < element.index) {
prevElement = value;
}
}
hintIndex = prevElement ? prevElement.index + (prevElement.value?.trimEnd().length ?? 1) : 1;
hintLength = (prevElement?.value?.length ?? 1) - (prevElement?.value?.trimEnd().length ?? 1);
}
return new diagnose_it_1.DiagnosticsMessage({
file: commit.commit.hash,
level,
message: {
text: highlightString(description, highlight),
linenumber: 1,
column: hintIndex,
},
})
.setContext(1, commit.commit.subject.split(/\r?\n/)[0])
.addFixitHint(diagnose_it_1.FixItHint.create({ index: hintIndex, length: hintLength === 0 ? 1 : hintLength }));
}
/**
* Commits MUST be prefixed with a type, which consists of a noun, feat, fix, etc.,
* followed by the OPTIONAL scope, OPTIONAL !, and REQUIRED terminal colon and space.
*/
class CC01 {
id = "CC-01";
description = "Commits MUST be prefixed with a type, which consists of a noun, feat, fix, etc., followed by the OPTIONAL scope, OPTIONAL !, and REQUIRED terminal colon and space.";
validate(commit, _options) {
const errors = [];
// MUST be prefixed with a type
if (!commit.type.value || commit.type.value.trim().length === 0) {
// Validated with EC-02
}
else {
// Ensure that we have a valid type (single word, no spaces, no special characters)
if (!isValidType(commit.type.value)) {
errors.push(createDiagnosticsMessage(commit, this.description, "which consists of a noun", "type"));
}
// Validate for spacing after the type
if (commit.type.value.trim() !== commit.type.value) {
if (commit.scope.value) {
errors.push(createDiagnosticsMessage(commit, this.description, "followed by the OPTIONAL scope", "scope", true));
}
else if (commit.breaking.value) {
errors.push(createDiagnosticsMessage(commit, this.description, ["followed by the", "OPTIONAL !"], "breaking", true));
}
else {
errors.push(createDiagnosticsMessage(commit, this.description, ["followed by the", "REQUIRED terminal colon"], "seperator", true));
}
}
// Validate for spacing after the scope, breaking and seperator
if (commit.scope.value && commit.scope.value.trim() !== commit.scope.value) {
if (commit.breaking.value) {
errors.push(createDiagnosticsMessage(commit, this.description, ["followed by the", "OPTIONAL !"], "breaking", true));
}
else {
errors.push(createDiagnosticsMessage(commit, this.description, ["followed by the", "REQUIRED terminal colon"], "seperator", true));
}
}
if (commit.breaking.value && commit.breaking.value.trim() !== commit.breaking.value) {
errors.push(createDiagnosticsMessage(commit, this.description, ["followed by the", "REQUIRED terminal colon"], "seperator", true));
}
}
// MUST have a terminal colon
if (!commit.seperator.value) {
errors.push(createDiagnosticsMessage(commit, this.description, ["followed by the", "REQUIRED terminal colon"], "seperator"));
}
return errors;
}
}
/**
* A scope MAY be provided after a type. A scope MUST consist of a noun describing
* a section of the codebase surrounded by parenthesis, e.g., fix(parser):
*/
class CC04 {
id = "CC-04";
description = "A scope MAY be provided after a type. A scope MUST consist of a noun describing a section of the codebase surrounded by parenthesis, e.g., fix(parser):";
validate(commit, _options) {
const errors = [];
if (commit.scope.value &&
(commit.scope.value === "()" ||
!isNoun(commit.scope.value.trimEnd().substring(1, commit.scope.value.trimEnd().length - 1)))) {
errors.push(createDiagnosticsMessage(commit, this.description, "A scope MUST consist of a noun", "scope"));
}
return errors;
}
}
/**
* A description MUST immediately follow the colon and space after the type/scope prefix.
* The description is a short summary of the code changes, e.g., fix: array parsing issue
* when multiple spaces were contained in string.
*/
class CC05 {
id = "CC-05";
description = "A description MUST immediately follow the colon and space after the type/scope prefix. The description is a short summary of the code changes, e.g., fix: array parsing issue when multiple spaces were contained in string.";
validate(commit, _options) {
const errors = [];
if (!commit.seperator.value) {
return errors;
}
if (commit.description.value === undefined ||
commit.seperator.value.length - commit.seperator.value.trim().length !== 1) {
errors.push(createDiagnosticsMessage(commit, this.description, "A description MUST immediately follow the colon and space", "description", true));
}
return errors;
}
}
/**
* A longer commit body MAY be provided after the short description, providing
* additional contextual information about the code changes. The body MUST begin one
* blank line after the description.
*/
class CC06 {
id = "CC-06";
description = "A longer commit body MAY be provided after the short description, providing additional contextual information about the code changes. The body MUST begin one blank line after the description.";
validate(commit, _options) {
const errors = [];
if (!commit.commit.subject) {
return errors;
}
const lines = commit.commit.subject.split(/\r?\n/);
if (lines.length > 1) {
return [
diagnose_it_1.DiagnosticsMessage.createError(commit.commit.hash, {
text: highlightString(this.description, "The body MUST begin one blank line after the description"),
linenumber: 2,
column: 1,
})
.setContext(1, lines)
.addFixitHint(diagnose_it_1.FixItHint.createRemoval({ index: 1, length: lines[1].length })),
];
}
return errors;
}
}
/**
* The units of information that make up Conventional Commits MUST NOT be treated as case
* sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase.
*/
class CC15 {
id = "CC-15";
description = "The units of information that make up Conventional Commits MUST NOT be treated as case sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase.";
validate(commit, _options) {
const errors = [];
const footerElements = (0, commit_1.getFooterElementsFromParagraph)(commit.commit.raw);
if (footerElements === undefined)
return errors;
for (const element of footerElements) {
if (["BREAKING CHANGE", "BREAKING-CHANGE"].includes(element.key.toUpperCase())) {
if (element.key !== element.key.toUpperCase()) {
errors.push(diagnose_it_1.DiagnosticsMessage.createError(commit.commit.hash, {
text: highlightString(this.description, "BREAKING CHANGE MUST be uppercase"),
linenumber: element.lineNumber,
column: 1,
})
.setContext(element.lineNumber, commit.commit.raw.split(/\r?\n/)[element.lineNumber - 1])
.addFixitHint(diagnose_it_1.FixItHint.create({ index: 1, length: element.key.length })));
}
}
}
return errors;
}
}
/**
* A scope MAY be provided after a type. A scope MUST consist of one of the configured values (...) surrounded by parenthesis
*/
class EC01 {
id = "EC-01";
description = "A scope MAY be provided after a type. A scope MUST consist of one of the configured values (...) surrounded by parenthesis";
validate(commit, options) {
const uniqueScopeList = Array.from(new Set(options?.scopes ?? []));
if (uniqueScopeList.length === 0) {
return [];
}
if (commit.scope.value === undefined || uniqueScopeList.includes(commit.scope.value.replace(/[()]+/g, ""))) {
return [];
}
this.description = `A scope MAY be provided after a type. A scope MUST consist of one of the configured values (${uniqueScopeList.join(", ")}) surrounded by parenthesis`;
return [
createDiagnosticsMessage(commit, this.description, ["A scope MUST consist of", `(${uniqueScopeList.join(", ")})`], "scope"),
];
}
}
/**
* Commits MUST be prefixed with a type, which consists of one of the configured values (feat, fix, ...)
*/
class EC02 {
id = "EC-02";
description = "Commits MUST be prefixed with a type, which consists of one of the configured values (feat, fix, ...)";
validate(commit, options) {
const uniqueAddedTypes = new Set(options?.types ?? []);
if (uniqueAddedTypes.has("feat"))
uniqueAddedTypes.delete("feat");
if (uniqueAddedTypes.has("fix"))
uniqueAddedTypes.delete("fix");
const expectedTypes = ["feat", "fix", ...Array.from(uniqueAddedTypes)];
this.description = `Commits ${uniqueAddedTypes.size > 0 ? "MUST" : "MAY"} be prefixed with a type, which consists of one of the configured values (${expectedTypes.join(", ")}).`;
if (commit.type.value === undefined ||
!isValidType(commit.type.value) ||
expectedTypes.includes(commit.type.value.toLowerCase().trimEnd())) {
return [];
}
if (commit.type.value.trim().length === 0) {
return [createDiagnosticsMessage(commit, this.description, "prefixed with a type", "type")];
}
if (uniqueAddedTypes.size > 0) {
return [
createDiagnosticsMessage(commit, this.description, ["prefixed with a type, which consists of", `(${expectedTypes.join(", ")})`], "type"),
];
}
else {
return [
createDiagnosticsMessage(commit, this.description, ["prefixed with a type, which consists of", `(${expectedTypes.join(", ")})`], "type", false, diagnose_it_1.DiagnosticsLevelEnum.Warning),
];
}
}
}
/**
* A `BREAKING CHANGE` git-trailer has been found in the body of the commit message and will be ignored as it MUST be included in the footer.
*/
class WA01 {
id = "WA-01";
description = "A `BREAKING CHANGE` git-trailer has been found in the body of the commit message and will be ignored as it MUST be included in the footer.";
validate(commit, _options) {
const errors = [];
if (commit.commit.body === undefined)
return errors;
const elements = (0, commit_1.getFooterElementsFromParagraph)(commit.commit.body);
if (elements === undefined)
return errors;
for (const element of elements) {
if (element.key === "BREAKING CHANGE" || element.key === "BREAKING-CHANGE") {
errors.push(diagnose_it_1.DiagnosticsMessage.createWarning(commit.commit.hash, {
text: highlightString(`A \`${element.key}\` git-trailer has been found in the body of the commit message and will be ignored as it MUST be included in the footer.`, [element.key, "will be ignored as it MUST be included in the footer"]),
linenumber: commit.commit.subject.split(/\r?\n/).length + element.lineNumber,
column: 1,
})
.setContext(commit.commit.subject.split(/\r?\n/).length + 1, commit.commit.body.split(/\r?\n/))
.addFixitHint(diagnose_it_1.FixItHint.create({ index: 1, length: element.key.length })));
}
}
return errors;
}
}
/** @internal */
exports.commitRules = [
new CC01(),
new CC04(),
new CC05(),
new CC06(),
new CC15(),
new EC01(),
new EC02(),
new WA01(),
];
;