@better-builds/lets-version
Version:
A package that reads your conventional commits and git history and recommends (or applies) a SemVer version bump for you
414 lines (413 loc) • 12.6 kB
JavaScript
import os from 'node:os';
import path from 'node:path';
/**
* Represents a raw git commit with no conventional commits connection
* (basically straight from "git log")
*/
export class GitCommit {
author;
date;
email;
message;
sha;
/**
* @param author - Author of the commit
* @param date - Date when the commit happened
* @param email - Author's email
* @param message - Raw commit message
* @param sha - Unique hash for the commit
*/
constructor(author, date, email, message, sha) {
this.author = author;
this.date = date;
this.email = email;
this.message = message;
this.sha = sha;
}
}
/**
* Represents a raw git commit with no conventional commits connection
* (basically straight from "git log"), but with package info
* attached to the commit. this might be uber verbose, but at least
* we have all the data available
*/
export class GitCommitWithPackageInfo extends GitCommit {
author;
date;
email;
message;
sha;
packageInfo;
/**
* @param author - Author of the commit
* @param date - Date when the commit happened
* @param email - Author's email
* @param message - Raw commit message
* @param sha - Unique hash for the commit
* @param packageInfo - Parsed packageInfo
*/
constructor(author, date, email, message, sha, packageInfo) {
super(author, date, email, message, sha);
this.author = author;
this.date = date;
this.email = email;
this.message = message;
this.sha = sha;
this.packageInfo = packageInfo;
}
}
/**
* Represents notes detected on a conventional commit
*/
export class GitConventionalNote {
title;
text;
constructor(title, text) {
this.title = title;
this.text = text;
}
}
/**
* Represents a parsed commit with conventional commits enriched data
*/
export class GitConventional {
sha;
author;
email;
body;
breaking;
footer;
header;
mentions;
merge;
notes;
references;
revert;
scope;
subject;
type;
constructor({ body, breaking, footer, header, mentions, merge, notes, references, revert, scope, sha, subject, type, author, email, }) {
this.sha = sha;
this.author = author;
this.email = email;
this.body = body;
this.breaking = breaking;
this.footer = footer;
this.header = header;
this.mentions = mentions;
this.merge = merge;
this.notes = notes;
this.references = references;
this.revert = revert;
this.scope = scope;
this.subject = subject;
this.type = type;
}
}
/**
* Represents a commit that has had its message parsed and converted
* into a valid Conventional Commits structure
*/
export class GitCommitWithConventional extends GitCommit {
author;
date;
email;
message;
sha;
conventional;
constructor(author, date, email, message, sha, conventional) {
super(author, date, email, message, sha);
this.author = author;
this.date = date;
this.email = email;
this.message = message;
this.sha = sha;
this.conventional = conventional;
}
}
/**
* Represents a commit that has had its message parsed and converted
* into a valid Conventional Commits structure. Additionally, package
* info will be attached to the commit
*/
/**
* Represents a Git commit with conventional commit and package information
*/
export class GitCommitWithConventionalAndPackageInfo extends GitCommitWithConventional {
packageInfo;
constructor(author, date, email, message, sha, conventional, packageInfo) {
super(author, date, email, message, sha, conventional);
this.packageInfo = packageInfo;
}
}
export class PackageInfo {
isPrivate;
name;
packageJSONPath;
packagePath;
pkg;
root;
version;
filesChanged;
constructor({ filesChanged, isPrivate, name, packageJSONPath, packagePath, pkg, root, version }) {
this.isPrivate = isPrivate;
this.name = name;
this.packageJSONPath = packageJSONPath;
this.packagePath = packagePath;
this.pkg = pkg;
this.root = root;
this.version = version;
this.filesChanged = filesChanged;
}
}
/**
* Represents an instance of a local repository dependency
* and any other local deps it relies on
*/
export class LocalDependencyGraphNode extends PackageInfo {
depType;
deps;
constructor({ depType, deps, ...info }) {
super(info);
this.depType = depType;
this.deps = deps;
}
}
/**
* Represents information about a package and its latest detected git tag
* that corresponds to a version or publish event
*/
export class PublishTagInfo {
packageName;
tag;
sha;
constructor(packageName, tag, sha) {
this.packageName = packageName;
this.tag = tag;
this.sha = sha;
}
}
/**
* Represents the which type of preset release a user can do.
*/
export var ReleaseAsPresets;
(function (ReleaseAsPresets) {
ReleaseAsPresets["ALPHA"] = "alpha";
ReleaseAsPresets["AUTO"] = "auto";
ReleaseAsPresets["BETA"] = "beta";
ReleaseAsPresets["MAJOR"] = "major";
ReleaseAsPresets["MINOR"] = "minor";
ReleaseAsPresets["PATCH"] = "patch";
})(ReleaseAsPresets || (ReleaseAsPresets = {}));
/**
* Represents the type of commit, as detected
* by the conventional parser
*
*/
export var ConventionalCommitType;
(function (ConventionalCommitType) {
ConventionalCommitType["BUILD"] = "build";
ConventionalCommitType["CHORE"] = "chore";
ConventionalCommitType["CI"] = "ci";
ConventionalCommitType["DOCS"] = "docs";
ConventionalCommitType["FEAT"] = "feat";
ConventionalCommitType["FIX"] = "fix";
ConventionalCommitType["PERF"] = "perf";
ConventionalCommitType["REFACTOR"] = "refactor";
ConventionalCommitType["REVERT"] = "revert";
ConventionalCommitType["STYLE"] = "style";
ConventionalCommitType["TEST"] = "test";
})(ConventionalCommitType || (ConventionalCommitType = {}));
/**
* Represents a semver bump type operation
*/
export var BumpType;
(function (BumpType) {
BumpType[BumpType["PATCH"] = 0] = "PATCH";
BumpType[BumpType["MINOR"] = 1] = "MINOR";
BumpType[BumpType["MAJOR"] = 2] = "MAJOR";
BumpType[BumpType["FIRST"] = 3] = "FIRST";
BumpType[BumpType["PRERELEASE"] = 4] = "PRERELEASE";
BumpType[BumpType["EXACT"] = 5] = "EXACT";
})(BumpType || (BumpType = {}));
/**
* Represents the type of entry that will be
* added to a changelog
*
* @enum {string}
*/
export var ChangelogEntryType;
(function (ChangelogEntryType) {
ChangelogEntryType["BREAKING"] = "BREAKING";
ChangelogEntryType["DOCS"] = "DOCS";
ChangelogEntryType["FEATURES"] = "FEATURES";
ChangelogEntryType["FIXES"] = "FIXES";
ChangelogEntryType["MISC"] = "MISC";
})(ChangelogEntryType || (ChangelogEntryType = {}));
/**
* Will return the renderer that will be used for the
* changelog entry type
*/
export const getChangelogEntryTypeRenderer = (type) => {
switch (type) {
case ChangelogEntryType.BREAKING:
return '🚨 Breaking Changes 🚨';
case ChangelogEntryType.DOCS:
return '📖 Docs 📖';
case ChangelogEntryType.FEATURES:
return '✨ Features ✨';
case ChangelogEntryType.FIXES:
return '🛠️ Fixes 🛠️';
default:
return '🔀 Miscellaneous 🔀';
}
};
/**
* Represents the string version of a bump type for user display
*/
export const BumpTypeToString = Object.entries(BumpType).reduce((prev, [key, val]) => ({
...prev,
[val]: key,
}), {});
/**
* Represents information about a version bump recommendation
* for a specific parsed package
*/
export class BumpRecommendation {
packageInfo;
from;
to;
type;
parentBumps;
constructor(packageInfo, from, to, type, parentBump) {
this.packageInfo = packageInfo;
this.from = from;
this.to = to;
this.type = type;
this.parentBumps = new Set(parentBump ? [parentBump] : []);
}
/**
* Determines if the bump is valid.
* An invalid bump is one where the "from" an "to" are marked as the same.
* This means there was an issue when attempting to recommend a bump in the "lets-version" logic
*/
get isValid() {
return this.from !== this.to;
}
/**
* Computes a human-friendly bump type name
* from the BumpType enum for this Bump recommendation
*/
get bumpTypeName() {
for (const [key, val] of Object.entries(BumpType)) {
if (this.type === val)
return key.toLowerCase();
}
throw new Error(`Invalid bump type of "${this.type}" detected`);
}
}
/**
* Represents a single type of changelog update
* that should be included as part of a larger "ChangelogUpdate."
* This individual entry should be written to a CHANGELOG.md file
* as part of the larger update
*/
export class ChangelogUpdateEntry {
type;
lines;
formatter;
/**
* Default formatter, will format each individual line of the changelog
*/
static defaultFormatter(line) {
const formatted = `${line.header || line.subject || ''} ${line.sha ? `(${line.sha})` : ''}`;
return `- ${formatted.trim()}`;
}
constructor(type, lines, formatter = ChangelogUpdateEntry.defaultFormatter) {
this.type = type;
this.lines = lines;
this.formatter = formatter;
}
/**
* Returns a string representation of this specific changelog entry
* that can be included in a parent ChangelogUpdate (which will then be
* prepended to a CHANGELOG.md file)
*/
toString() {
return `### ${getChangelogEntryTypeRenderer(this.type)}${os.EOL}${os.EOL}${this.lines
.map(l => this.formatter(l))
.filter(Boolean)
.join(os.EOL)}`;
}
}
/**
* Represents a changelog update that can be
* prepends to a new or existing CHANGELOG.md file for a specific
* package
*/
export class ChangelogUpdate {
formattedDate;
bumpRecommendation;
entries;
constructor(formattedDate, bumpRecommendation, entries) {
this.formattedDate = formattedDate;
this.bumpRecommendation = bumpRecommendation;
this.entries = entries;
}
/**
* Returns an absolute path to the CHANGELOG.md
* file that should correspond to this update
*/
get changelogPath() {
return path.join(this.bumpRecommendation.packageInfo.packagePath, 'CHANGELOG.md');
}
/**
* Converts this ChangelogUpdate instance into a string that
* can be safely prepended to a CHANGELOG.md file
*
* @returns {string}
*/
toString() {
const header = `## ${this.bumpRecommendation.to} (${this.formattedDate})`;
const entries = Object.values(this.entries)
.map(e => e.toString())
.join(`${os.EOL}${os.EOL}${os.EOL}${os.EOL}`);
return `${header}${os.EOL}${os.EOL}${entries}${os.EOL}`;
}
}
/**
* Represents a "rollup" CHANGELOG.md that will contain
* all of the updates that happened for a given version bump operation.
* Typically, this is placed at the root of a multi-package monorepo
*/
export class ChangelogAggregateUpdate {
cwd;
formattedDate;
changelogUpdates;
/**
* @param {string} cwd
* @param {string} formattedDate
* @param {ChangelogUpdate[]} changelogUpdates
*/
constructor(cwd, formattedDate, changelogUpdates) {
this.cwd = cwd;
this.formattedDate = formattedDate;
this.changelogUpdates = changelogUpdates;
}
get changelogPath() {
return path.join(this.cwd, 'CHANGELOG.md');
}
/**
* Converts this ChangelogAggregateUpdate instance into a string that
* can be safely prepended to a CHANGELOG.md file in the root of your project
*/
toString() {
const header = `# __All updates from ${this.formattedDate} are below:__`;
const perChangeUpdates = this.changelogUpdates.reduce((prev, c) => {
let update = `# ${c.bumpRecommendation.packageInfo.name} Updates${os.EOL}${os.EOL}`;
update += `${c.toString()}${os.EOL}${os.EOL}`;
return `${update}${prev}`;
}, '');
return `${header}${os.EOL}${os.EOL}${perChangeUpdates}---${os.EOL}`;
}
}