UNPKG

@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
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}`; } }