package-versioner
Version:
A lightweight yet powerful CLI tool for automated semantic versioning based on Git history and conventional commits.
1,140 lines (1,116 loc) • 41.2 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// src/index.ts
var import_commander = require("commander");
// src/config.ts
var fs = __toESM(require("fs"), 1);
var import_node_process = require("process");
function loadConfig(configPath) {
const localProcess = (0, import_node_process.cwd)();
const filePath = configPath || `${localProcess}/version.config.json`;
return new Promise((resolve, reject) => {
fs.readFile(filePath, "utf-8", (err, data) => {
if (err) {
reject(new Error(`Could not locate the config file at ${filePath}: ${err.message}`));
return;
}
try {
const config = JSON.parse(data);
resolve(config);
} catch (err2) {
const errorMessage = err2 instanceof Error ? err2.message : String(err2);
reject(new Error(`Failed to parse config file ${filePath}: ${errorMessage}`));
}
});
});
}
// src/core/versionEngine.ts
var import_node_process5 = require("process");
var import_get_packages = require("@manypkg/get-packages");
// src/errors/gitError.ts
var GitError = class extends Error {
constructor(message, code) {
super(message);
this.code = code;
this.name = "GitError";
}
};
function createGitError(code, details) {
const messages = {
["NOT_GIT_REPO" /* NOT_GIT_REPO */]: "Not a git repository",
["GIT_PROCESS_ERROR" /* GIT_PROCESS_ERROR */]: "Failed to create new version",
["NO_FILES" /* NO_FILES */]: "No files specified for commit",
["NO_COMMIT_MESSAGE" /* NO_COMMIT_MESSAGE */]: "Commit message is required",
["GIT_ERROR" /* GIT_ERROR */]: "Git operation failed"
};
const baseMessage = messages[code];
const fullMessage = details ? `${baseMessage}: ${details}` : baseMessage;
return new GitError(fullMessage, code);
}
// src/errors/versionError.ts
var VersionError = class extends Error {
constructor(message, code) {
super(message);
this.code = code;
this.name = "VersionError";
}
};
function createVersionError(code, details) {
const messages = {
["CONFIG_REQUIRED" /* CONFIG_REQUIRED */]: "Configuration is required",
["PACKAGES_NOT_FOUND" /* PACKAGES_NOT_FOUND */]: "Failed to get packages information",
["WORKSPACE_ERROR" /* WORKSPACE_ERROR */]: "Failed to get workspace packages",
["INVALID_CONFIG" /* INVALID_CONFIG */]: "Invalid configuration",
["PACKAGE_NOT_FOUND" /* PACKAGE_NOT_FOUND */]: "Package not found",
["VERSION_CALCULATION_ERROR" /* VERSION_CALCULATION_ERROR */]: "Failed to calculate version"
};
const baseMessage = messages[code];
const fullMessage = details ? `${baseMessage}: ${details}` : baseMessage;
return new VersionError(fullMessage, code);
}
// src/utils/logging.ts
var import_chalk = __toESM(require("chalk"), 1);
var import_figlet = __toESM(require("figlet"), 1);
// src/utils/jsonOutput.ts
var _jsonOutputMode = false;
var _jsonData = {
dryRun: false,
updates: [],
tags: []
};
function enableJsonOutput(dryRun = false) {
_jsonOutputMode = true;
_jsonData.dryRun = dryRun;
_jsonData.updates = [];
_jsonData.tags = [];
_jsonData.commitMessage = void 0;
}
function isJsonOutputMode() {
return _jsonOutputMode;
}
function addPackageUpdate(packageName, newVersion, filePath) {
if (!_jsonOutputMode) return;
_jsonData.updates.push({
packageName,
newVersion,
filePath
});
}
function addTag(tag) {
if (!_jsonOutputMode) return;
_jsonData.tags.push(tag);
}
function setCommitMessage(message) {
if (!_jsonOutputMode) return;
_jsonData.commitMessage = message;
}
function printJsonOutput() {
if (_jsonOutputMode) {
console.log(JSON.stringify(_jsonData, null, 2));
}
}
// src/utils/logging.ts
function log(message, status = "info") {
let chalkFn;
switch (status) {
case "success":
chalkFn = import_chalk.default.green;
break;
case "warning":
chalkFn = import_chalk.default.yellow;
break;
case "error":
chalkFn = import_chalk.default.red;
break;
case "debug":
chalkFn = import_chalk.default.gray;
break;
default:
chalkFn = import_chalk.default.blue;
}
if (isJsonOutputMode()) {
if (status === "error") {
chalkFn(message);
console.error(message);
}
return;
}
if (status === "error") {
console.error(chalkFn(message));
} else {
console.log(chalkFn(message));
}
}
// src/core/versionStrategies.ts
var import_node_fs3 = __toESM(require("fs"), 1);
var path4 = __toESM(require("path"), 1);
// src/git/commands.ts
var import_node_process2 = require("process");
// src/git/commandExecutor.ts
var import_node_child_process = require("child_process");
var execAsync = (command, options) => {
const defaultOptions = { maxBuffer: 1024 * 1024 * 10, ...options };
return new Promise((resolve, reject) => {
(0, import_node_child_process.exec)(
command,
defaultOptions,
(error, stdout, stderr) => {
if (error) {
reject(error);
} else {
resolve({ stdout: stdout.toString(), stderr: stderr.toString() });
}
}
);
});
};
var execSync = (command, args) => (0, import_node_child_process.execSync)(command, { maxBuffer: 1024 * 1024 * 10, ...args });
// src/git/repository.ts
var import_node_fs = require("fs");
var import_node_path = require("path");
function isGitRepository(directory) {
const gitDir = (0, import_node_path.join)(directory, ".git");
if (!(0, import_node_fs.existsSync)(gitDir)) {
return false;
}
const stats = (0, import_node_fs.statSync)(gitDir);
if (!stats.isDirectory()) {
return false;
}
try {
execSync("git rev-parse --is-inside-work-tree", { cwd: directory });
return true;
} catch (_error) {
return false;
}
}
function getCurrentBranch() {
const result = execSync("git rev-parse --abbrev-ref HEAD");
return result.toString().trim();
}
// src/git/commands.ts
async function gitAdd(files) {
const command = `git add ${files.join(" ")}`;
return execAsync(command);
}
async function gitCommit(options) {
const command = ["commit"];
if (options.amend) {
command.push("--amend");
}
if (options.author) {
command.push(`--author="${options.author}"`);
}
if (options.date) {
command.push(`--date="${options.date}"`);
}
if (options.skipHooks) {
command.push("--no-verify");
}
command.push(`-m "${options.message}"`);
return execAsync(`git ${command.join(" ")}`);
}
async function createGitTag(options) {
const { tag, message = "", args = "" } = options;
const command = `git tag -a -m "${message}" ${tag} ${args}`;
return execAsync(command);
}
async function gitProcess(options) {
const { files, nextTag, commitMessage, skipHooks, dryRun } = options;
if (!isGitRepository((0, import_node_process2.cwd)())) {
throw createGitError("NOT_GIT_REPO" /* NOT_GIT_REPO */);
}
try {
if (!dryRun) {
await gitAdd(files);
await gitCommit({
message: commitMessage,
skipHooks
});
if (nextTag) {
const tagMessage = `New Version ${nextTag} generated at ${(/* @__PURE__ */ new Date()).toISOString()}`;
await createGitTag({
tag: nextTag,
message: tagMessage
});
}
} else {
log("[DRY RUN] Would add files:", "info");
for (const file of files) {
log(` - ${file}`, "info");
}
log(`[DRY RUN] Would commit with message: "${commitMessage}"`, "info");
if (nextTag) {
log(`[DRY RUN] Would create tag: ${nextTag}`, "info");
}
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
throw createGitError("GIT_PROCESS_ERROR" /* GIT_PROCESS_ERROR */, errorMessage);
}
}
async function createGitCommitAndTag(files, nextTag, commitMessage, skipHooks, dryRun) {
try {
if (!files || files.length === 0) {
throw createGitError("NO_FILES" /* NO_FILES */);
}
if (!commitMessage) {
throw createGitError("NO_COMMIT_MESSAGE" /* NO_COMMIT_MESSAGE */);
}
setCommitMessage(commitMessage);
if (nextTag) {
addTag(nextTag);
}
await gitProcess({
files,
nextTag,
commitMessage,
skipHooks,
dryRun
});
if (!dryRun) {
log(`Created tag: ${nextTag}`, "success");
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Failed to create git commit and tag: ${errorMessage}`, "error");
if (error instanceof Error) {
console.error(error.stack || error.message);
} else {
console.error(error);
}
throw new GitError(`Git operation failed: ${errorMessage}`, "GIT_ERROR" /* GIT_ERROR */);
}
}
// src/git/tagsAndBranches.ts
var import_git_semver_tags = require("git-semver-tags");
// src/utils/formatting.ts
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function formatTag(version, versionPrefix, packageName, tagTemplate = "${prefix}${version}", packageTagTemplate = "${packageName}@${prefix}${version}") {
const variables = {
version,
prefix: versionPrefix || "",
packageName: packageName || ""
};
const template = packageName ? packageTagTemplate : tagTemplate;
return createTemplateString(template, variables);
}
function formatTagPrefix(versionPrefix, scope) {
if (!versionPrefix) return "";
const cleanPrefix = versionPrefix.replace(/\/$/, "");
if (scope) {
return `${cleanPrefix}/${scope}`;
}
return cleanPrefix;
}
function formatCommitMessage(template, version, scope) {
return createTemplateString(template, { version, scope });
}
function createTemplateString(template, variables) {
return Object.entries(variables).reduce((result, [key, value]) => {
if (value === void 0) {
return result;
}
const regex = new RegExp(`\\$\\{${key}\\}`, "g");
return result.replace(regex, value);
}, template);
}
// src/git/tagsAndBranches.ts
function getCommitsLength(pkgRoot) {
try {
const gitCommand = `git rev-list --count HEAD ^$(git describe --tags --abbrev=0) ${pkgRoot}`;
const amount = execSync(gitCommand).toString().trim();
return Number(amount);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Failed to get number of commits since last tag: ${errorMessage}`, "error");
return 0;
}
}
async function getLatestTag() {
try {
const tags = await (0, import_git_semver_tags.getSemverTags)({});
return tags[0] || "";
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Failed to get latest tag: ${errorMessage}`, "error");
if (error instanceof Error && error.message.includes("No names found")) {
log("No tags found in the repository.", "info");
}
return "";
}
}
async function lastMergeBranchName(branches, baseBranch) {
try {
const escapedBranches = branches.map((branch) => escapeRegExp(branch));
const branchesRegex = `${escapedBranches.join("/(.*)|")}/(.*)`;
const command = `git for-each-ref --sort=-committerdate --format='%(refname:short)' refs/heads --merged ${baseBranch} | grep -o -i -E "${branchesRegex}" | awk -F'[ ]' '{print $1}' | head -n 1`;
const { stdout } = await execAsync(command);
return stdout.trim();
} catch (error) {
console.error(
"Error while getting the last branch name:",
error instanceof Error ? error.message : String(error)
);
return null;
}
}
async function getLatestTagForPackage(packageName, tagPrefix) {
try {
const allTags = await (0, import_git_semver_tags.getSemverTags)({
tagPrefix
});
const packageTagPattern = tagPrefix ? new RegExp(`^${escapeRegExp(tagPrefix)}${escapeRegExp(packageName)}@`) : new RegExp(`^${escapeRegExp(packageName)}@`);
const packageTags = allTags.filter((tag) => packageTagPattern.test(tag));
return packageTags[0] || "";
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Failed to get latest tag for package ${packageName}: ${errorMessage}`, "error");
if (error instanceof Error && error.message.includes("No names found")) {
log(`No tags found for package ${packageName}.`, "info");
}
return "";
}
}
// src/package/packageManagement.ts
var import_node_fs2 = __toESM(require("fs"), 1);
var import_node_path2 = __toESM(require("path"), 1);
function updatePackageVersion(packagePath, version) {
try {
const packageContent = import_node_fs2.default.readFileSync(packagePath, "utf8");
const packageJson = JSON.parse(packageContent);
const packageName = packageJson.name;
packageJson.version = version;
import_node_fs2.default.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}
`);
addPackageUpdate(packageName, version, packagePath);
log(`Updated package.json at ${packagePath} to version ${version}`, "success");
} catch (error) {
log(`Failed to update package.json at ${packagePath}`, "error");
if (error instanceof Error) {
log(error.message, "error");
}
throw error;
}
}
// src/package/packageProcessor.ts
var import_node_path3 = __toESM(require("path"), 1);
var import_node_process4 = require("process");
// src/core/versionCalculator.ts
var fs3 = __toESM(require("fs"), 1);
var path2 = __toESM(require("path"), 1);
var import_node_process3 = require("process");
var import_conventional_recommended_bump = require("conventional-recommended-bump");
var import_semver = __toESM(require("semver"), 1);
var STANDARD_BUMP_TYPES = ["major", "minor", "patch"];
async function calculateVersion(config, options, forcedType, configPrereleaseIdentifier) {
const { latestTag, type, path: pkgPath, name, branchPattern } = options;
const { preset } = config;
const tagPrefix = options.versionPrefix || config.versionPrefix || "v";
const prereleaseIdentifier = options.prereleaseIdentifier || configPrereleaseIdentifier;
const initialVersion = prereleaseIdentifier ? `0.0.1-${prereleaseIdentifier}` : "0.0.1";
const hasNoTags = !latestTag || latestTag === "";
function determineTagSearchPattern(packageName, prefix) {
if (packageName) {
return prefix ? `${prefix}${packageName}@` : `${packageName}@`;
}
return prefix;
}
const tagSearchPattern = determineTagSearchPattern(name, tagPrefix);
const escapedTagPattern = escapeRegExp(tagSearchPattern);
const specifiedType = forcedType || type;
if (specifiedType) {
if (hasNoTags) {
return getPackageVersionFallback(
pkgPath,
name,
specifiedType,
prereleaseIdentifier,
initialVersion
);
}
const cleanedTag = import_semver.default.clean(latestTag) || latestTag;
const currentVersion = import_semver.default.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
if (STANDARD_BUMP_TYPES.includes(specifiedType) && import_semver.default.prerelease(currentVersion)) {
log(
`Cleaning prerelease identifier from ${currentVersion} for ${specifiedType} bump`,
"debug"
);
return bumpVersion(currentVersion, specifiedType, prereleaseIdentifier);
}
return import_semver.default.inc(currentVersion, specifiedType, prereleaseIdentifier) || "";
}
if (branchPattern && branchPattern.length > 0) {
const currentBranch = getCurrentBranch();
const baseBranch = options.baseBranch;
if (baseBranch) {
lastMergeBranchName(branchPattern, baseBranch);
}
const branchToCheck = currentBranch;
let branchVersionType;
for (const pattern of branchPattern) {
if (!pattern.includes(":")) {
log(`Invalid branch pattern "${pattern}" - missing colon. Skipping.`, "warning");
continue;
}
const [patternRegex, releaseType] = pattern.split(":");
if (new RegExp(patternRegex).test(branchToCheck)) {
branchVersionType = releaseType;
log(`Using branch pattern ${patternRegex} for version type ${releaseType}`, "debug");
break;
}
}
if (branchVersionType) {
if (hasNoTags) {
return getPackageVersionFallback(
pkgPath,
name,
branchVersionType,
prereleaseIdentifier,
initialVersion
);
}
const cleanedTag = import_semver.default.clean(latestTag) || latestTag;
const currentVersion = import_semver.default.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
log(`Applying ${branchVersionType} bump based on branch pattern`, "debug");
return import_semver.default.inc(currentVersion, branchVersionType, void 0) || "";
}
}
try {
const bumper = new import_conventional_recommended_bump.Bumper();
bumper.loadPreset(preset);
const recommendedBump = await bumper.bump();
const releaseTypeFromCommits = recommendedBump == null ? void 0 : recommendedBump.releaseType;
if (hasNoTags) {
if (releaseTypeFromCommits) {
return getPackageVersionFallback(
pkgPath,
name,
releaseTypeFromCommits,
prereleaseIdentifier,
initialVersion
);
}
return initialVersion;
}
const checkPath = pkgPath || (0, import_node_process3.cwd)();
const commitsLength = getCommitsLength(checkPath);
if (commitsLength === 0) {
log(
`No new commits found for ${name || "project"} since ${latestTag}, skipping version bump`,
"info"
);
return "";
}
if (!releaseTypeFromCommits) {
log(
`No relevant commits found for ${name || "project"} since ${latestTag}, skipping version bump`,
"info"
);
return "";
}
const currentVersion = import_semver.default.clean(latestTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
return import_semver.default.inc(currentVersion, releaseTypeFromCommits, prereleaseIdentifier) || "";
} catch (error) {
log(`Failed to calculate version for ${name || "project"}`, "error");
console.error(error);
if (error instanceof Error && error.message.includes("No names found")) {
log("No tags found, proceeding with initial version calculation (if applicable).", "info");
return initialVersion;
}
throw error;
}
}
function getPackageVersionFallback(pkgPath, name, releaseType, prereleaseIdentifier, initialVersion) {
const packageDir = pkgPath || (0, import_node_process3.cwd)();
const packageJsonPath = path2.join(packageDir, "package.json");
if (!fs3.existsSync(packageJsonPath)) {
throw new Error(`package.json not found at ${packageJsonPath}. Cannot determine version.`);
}
try {
const packageJson = JSON.parse(fs3.readFileSync(packageJsonPath, "utf-8"));
if (!packageJson.version) {
log(`No version found in package.json. Using initial version ${initialVersion}`, "info");
return initialVersion;
}
log(
`No tags found for ${name || "package"}, using package.json version: ${packageJson.version} as base`,
"info"
);
if (STANDARD_BUMP_TYPES.includes(releaseType) && import_semver.default.prerelease(packageJson.version)) {
if (packageJson.version === "1.0.0-next.0" && releaseType === "major") {
log(
`Cleaning prerelease identifier from ${packageJson.version} for ${releaseType} bump`,
"debug"
);
return "1.0.0";
}
log(
`Cleaning prerelease identifier from ${packageJson.version} for ${releaseType} bump`,
"debug"
);
return bumpVersion(packageJson.version, releaseType, prereleaseIdentifier);
}
return import_semver.default.inc(packageJson.version, releaseType, prereleaseIdentifier) || initialVersion;
} catch (err) {
throw new Error(
`Error reading package.json: ${err instanceof Error ? err.message : String(err)}`
);
}
}
function bumpVersion(currentVersion, bumpType, prereleaseIdentifier) {
if (import_semver.default.prerelease(currentVersion) && STANDARD_BUMP_TYPES.includes(bumpType)) {
const parsed = import_semver.default.parse(currentVersion);
if (bumpType === "major" && (parsed == null ? void 0 : parsed.major) === 1 && parsed.minor === 0 && parsed.patch === 0 && import_semver.default.prerelease(currentVersion)) {
return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
}
log(`Cleaning prerelease identifier from ${currentVersion} for ${bumpType} bump`, "debug");
return import_semver.default.inc(currentVersion, bumpType) || "";
}
return import_semver.default.inc(currentVersion, bumpType, prereleaseIdentifier) || "";
}
// src/package/packageProcessor.ts
var PackageProcessor = class {
skip;
targets;
versionPrefix;
tagTemplate;
packageTagTemplate;
commitMessageTemplate;
dryRun;
skipHooks;
getLatestTag;
config;
// Config for version calculation
fullConfig;
constructor(options) {
this.skip = options.skip || [];
this.targets = options.targets || [];
this.versionPrefix = options.versionPrefix || "v";
this.tagTemplate = options.tagTemplate;
this.packageTagTemplate = options.packageTagTemplate;
this.commitMessageTemplate = options.commitMessageTemplate || "";
this.dryRun = options.dryRun || false;
this.skipHooks = options.skipHooks || false;
this.getLatestTag = options.getLatestTag;
this.config = options.config;
this.fullConfig = options.fullConfig;
}
/**
* Set package targets to process
*/
setTargets(targets) {
this.targets = targets;
}
/**
* Process packages based on targeting criteria
*/
async processPackages(packages) {
var _a;
const tags = [];
const updatedPackagesInfo = [];
if (!packages || !Array.isArray(packages)) {
log("Invalid packages data provided. Expected array of packages.", "error");
return { updatedPackages: [], tags: [] };
}
const pkgsToConsider = packages.filter((pkg) => {
var _a2;
const pkgName = pkg.packageJson.name;
if ((_a2 = this.skip) == null ? void 0 : _a2.includes(pkgName)) {
log(`Skipping package ${pkgName} as it's in the skip list.`, "info");
return false;
}
if (!this.targets || this.targets.length === 0) {
return true;
}
const isTargeted = this.targets.includes(pkgName);
if (!isTargeted) {
log(`Package ${pkgName} not in target list, skipping.`, "info");
}
return isTargeted;
});
log(`Found ${pkgsToConsider.length} targeted package(s) to process after filtering.`, "info");
if (pkgsToConsider.length === 0) {
log("No matching targeted packages found to process.", "info");
return { updatedPackages: [], tags: [] };
}
for (const pkg of pkgsToConsider) {
const name = pkg.packageJson.name;
const pkgPath = pkg.dir;
const formattedPrefix = formatTagPrefix(this.versionPrefix);
let latestTagResult = "";
try {
latestTagResult = await getLatestTagForPackage(name, this.versionPrefix);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log(
`Error getting package-specific tag for ${name}, falling back to global tag: ${errorMessage}`,
"warning"
);
}
if (!latestTagResult) {
try {
const globalTagResult = await this.getLatestTag();
latestTagResult = globalTagResult || "";
if (globalTagResult) {
log(`Using global tag ${globalTagResult} as fallback for package ${name}`, "info");
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Error getting global tag, using empty tag value: ${errorMessage}`, "warning");
}
}
const latestTag = latestTagResult;
const nextVersion = await calculateVersion(this.fullConfig, {
latestTag,
versionPrefix: formattedPrefix,
path: pkgPath,
name,
branchPattern: this.config.branchPattern,
baseBranch: this.config.baseBranch,
prereleaseIdentifier: this.config.prereleaseIdentifier,
type: this.config.forceType
});
if (!nextVersion) {
continue;
}
updatePackageVersion(import_node_path3.default.join(pkgPath, "package.json"), nextVersion);
const packageTag = formatTag(
nextVersion,
this.versionPrefix,
name,
this.tagTemplate,
this.packageTagTemplate
);
const tagMessage = `chore(release): ${name} ${nextVersion}`;
addTag(packageTag);
tags.push(packageTag);
if (!this.dryRun) {
try {
await createGitTag({ tag: packageTag, message: tagMessage });
log(`Created tag: ${packageTag}`, "success");
} catch (tagError) {
log(
`Failed to create tag ${packageTag} for ${name}: ${tagError.message}`,
"error"
);
log(tagError.stack || "No stack trace available", "error");
}
} else {
log(`[DRY RUN] Would create tag: ${packageTag}`, "info");
}
updatedPackagesInfo.push({ name, version: nextVersion, path: pkgPath });
}
if (updatedPackagesInfo.length === 0) {
log("No targeted packages required a version update.", "info");
return { updatedPackages: [], tags };
}
const filesToCommit = updatedPackagesInfo.map((info) => import_node_path3.default.join(info.path, "package.json"));
const packageNames = updatedPackagesInfo.map((p) => p.name).join(", ");
const representativeVersion = ((_a = updatedPackagesInfo[0]) == null ? void 0 : _a.version) || "multiple";
let commitMessage = this.commitMessageTemplate || "chore(release): publish packages";
if (updatedPackagesInfo.length === 1 && commitMessage.includes("${version}")) {
commitMessage = formatCommitMessage(commitMessage, representativeVersion);
} else {
commitMessage = `chore(release): ${packageNames} ${representativeVersion}`;
}
setCommitMessage(commitMessage);
if (!this.dryRun) {
try {
await gitAdd(filesToCommit);
await gitCommit({ message: commitMessage, skipHooks: this.skipHooks });
log(`Created commit for targeted release: ${packageNames}`, "success");
} catch (commitError) {
log("Failed to create commit for targeted release.", "error");
console.error(commitError);
(0, import_node_process4.exit)(1);
}
} else {
log("[DRY RUN] Would add files:", "info");
for (const file of filesToCommit) {
log(` - ${file}`, "info");
}
log(`[DRY RUN] Would commit with message: "${commitMessage}"`, "info");
}
return {
updatedPackages: updatedPackagesInfo,
commitMessage,
tags
};
}
};
// src/core/versionStrategies.ts
function shouldProcessPackage(pkg, config, targets = []) {
var _a;
const pkgName = pkg.packageJson.name;
if ((_a = config.skip) == null ? void 0 : _a.includes(pkgName)) {
return false;
}
if (!targets || targets.length === 0) {
return true;
}
return targets.includes(pkgName);
}
function createSyncedStrategy(config) {
return async (packages) => {
try {
const {
versionPrefix,
tagTemplate,
baseBranch,
branchPattern,
commitMessage = "chore(release): v${version}",
prereleaseIdentifier,
dryRun,
skipHooks
} = config;
const formattedPrefix = formatTagPrefix(versionPrefix || "v");
const latestTag = await getLatestTag();
const nextVersion = await calculateVersion(config, {
latestTag,
versionPrefix: formattedPrefix,
branchPattern,
baseBranch,
prereleaseIdentifier
});
if (!nextVersion) {
log("No version change needed", "info");
return;
}
const files = [];
const updatedPackages = [];
try {
const rootPkgPath = path4.join(packages.root, "package.json");
if (import_node_fs3.default.existsSync(rootPkgPath)) {
updatePackageVersion(rootPkgPath, nextVersion);
files.push(rootPkgPath);
updatedPackages.push("root");
}
} catch (_error) {
log("Failed to update root package.json", "error");
}
for (const pkg of packages.packages) {
if (!shouldProcessPackage(pkg, config)) {
continue;
}
const packageJsonPath = path4.join(pkg.dir, "package.json");
updatePackageVersion(packageJsonPath, nextVersion);
files.push(packageJsonPath);
updatedPackages.push(pkg.packageJson.name);
}
if (updatedPackages.length > 0) {
log(`Updated ${updatedPackages.length} package(s) to version ${nextVersion}`, "success");
} else {
log("No packages were updated", "warning");
return;
}
const nextTag = formatTag(nextVersion, formattedPrefix, null, tagTemplate);
const formattedCommitMessage = formatCommitMessage(commitMessage, nextVersion);
await createGitCommitAndTag(files, nextTag, formattedCommitMessage, skipHooks, dryRun);
} catch (error) {
if (error instanceof VersionError || error instanceof GitError) {
log(`Synced Strategy failed: ${error.message} (${error.code || "UNKNOWN"})`, "error");
} else {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Synced Strategy failed: ${errorMessage}`, "error");
}
throw error;
}
};
}
function createSingleStrategy(config) {
return async (packages) => {
try {
const {
packages: configPackages,
versionPrefix,
tagTemplate,
packageTagTemplate,
commitMessage = "chore(release): ${version}",
dryRun,
skipHooks
} = config;
if (!configPackages || configPackages.length !== 1) {
throw createVersionError(
"INVALID_CONFIG" /* INVALID_CONFIG */,
"Single mode requires exactly one package name"
);
}
const packageName = configPackages[0];
const pkg = packages.packages.find((p) => p.packageJson.name === packageName);
if (!pkg) {
throw createVersionError("PACKAGE_NOT_FOUND" /* PACKAGE_NOT_FOUND */, packageName);
}
const pkgPath = pkg.dir;
const formattedPrefix = formatTagPrefix(versionPrefix || "v");
let latestTagResult = await getLatestTagForPackage(packageName, formattedPrefix);
if (!latestTagResult) {
const globalTagResult = await getLatestTag();
latestTagResult = globalTagResult || "";
}
const latestTag = latestTagResult;
let nextVersion = void 0;
try {
nextVersion = await calculateVersion(config, {
latestTag,
versionPrefix: formattedPrefix,
path: pkgPath,
name: packageName
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw createVersionError("VERSION_CALCULATION_ERROR" /* VERSION_CALCULATION_ERROR */, errorMessage);
}
if (nextVersion === void 0 || nextVersion === "") {
log(`No version change needed for ${packageName}`, "info");
return;
}
const packageJsonPath = path4.join(pkgPath, "package.json");
updatePackageVersion(packageJsonPath, nextVersion);
log(`Updated package ${packageName} to version ${nextVersion}`, "success");
const nextTag = formatTag(
nextVersion,
formattedPrefix,
packageName,
tagTemplate,
packageTagTemplate
);
const formattedCommitMessage = formatCommitMessage(commitMessage, nextVersion);
await createGitCommitAndTag(
[packageJsonPath],
nextTag,
formattedCommitMessage,
skipHooks,
dryRun
);
} catch (error) {
if (error instanceof VersionError || error instanceof GitError) {
log(
`Single Package Strategy failed: ${error.message} (${error.code || "UNKNOWN"})`,
"error"
);
} else {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Single Package Strategy failed: ${errorMessage}`, "error");
}
throw error;
}
};
}
function createAsyncStrategy(config) {
const dependencies = {
getLatestTag
};
const processorOptions = {
skip: config.skip || [],
targets: config.packages || [],
versionPrefix: config.versionPrefix || "v",
tagTemplate: config.tagTemplate,
packageTagTemplate: config.packageTagTemplate,
commitMessageTemplate: config.commitMessage || "",
dryRun: config.dryRun || false,
skipHooks: config.skipHooks || false,
getLatestTag: dependencies.getLatestTag,
fullConfig: config,
config: {
branchPattern: config.branchPattern || [],
baseBranch: config.baseBranch || "main",
prereleaseIdentifier: config.prereleaseIdentifier,
forceType: config.forceType
}
};
const packageProcessor = new PackageProcessor(processorOptions);
return async (packages, targets = []) => {
try {
const targetPackages = targets.length > 0 ? targets : config.packages || [];
packageProcessor.setTargets(targetPackages);
if (targetPackages.length > 0) {
log(`Processing targeted packages: ${targetPackages.join(", ")}`, "info");
} else {
log("No targets specified, processing all non-skipped packages", "info");
}
const result = await packageProcessor.processPackages(packages.packages);
if (result.updatedPackages.length === 0) {
log("No packages required a version update.", "info");
} else {
const packageNames = result.updatedPackages.map((p) => p.name).join(", ");
log(`Updated ${result.updatedPackages.length} package(s): ${packageNames}`, "success");
if (result.tags.length > 0) {
log(`Created ${result.tags.length} tag(s): ${result.tags.join(", ")}`, "success");
}
if (result.commitMessage) {
log(`Created commit with message: "${result.commitMessage}"`, "success");
}
}
} catch (error) {
if (error instanceof VersionError || error instanceof GitError) {
log(`Async Strategy failed: ${error.message} (${error.code || "UNKNOWN"})`, "error");
} else {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Async Strategy failed: ${errorMessage}`, "error");
}
throw error;
}
};
}
function createStrategy(config) {
var _a;
if (config.synced) {
return createSyncedStrategy(config);
}
if (((_a = config.packages) == null ? void 0 : _a.length) === 1) {
return createSingleStrategy(config);
}
return createAsyncStrategy(config);
}
function createStrategyMap(config) {
return {
synced: createSyncedStrategy(config),
single: createSingleStrategy(config),
async: createAsyncStrategy(config)
};
}
// src/core/versionEngine.ts
var VersionEngine = class {
config;
jsonMode;
workspaceCache = null;
strategies;
currentStrategy;
constructor(config, jsonMode = false) {
if (!config) {
throw createVersionError("CONFIG_REQUIRED" /* CONFIG_REQUIRED */);
}
if (!config.preset) {
config.preset = "conventional-commits";
log("No preset specified, using default: conventional-commits", "warning");
}
this.config = config;
this.jsonMode = jsonMode;
this.strategies = createStrategyMap(config);
this.currentStrategy = createStrategy(config);
}
/**
* Get workspace packages information - with caching for performance
*/
async getWorkspacePackages() {
try {
if (this.workspaceCache) {
return this.workspaceCache;
}
const pkgsResult = (0, import_get_packages.getPackagesSync)((0, import_node_process5.cwd)());
if (!pkgsResult || !pkgsResult.packages) {
throw createVersionError("PACKAGES_NOT_FOUND" /* PACKAGES_NOT_FOUND */);
}
this.workspaceCache = pkgsResult;
return pkgsResult;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Failed to get packages information: ${errorMessage}`, "error");
console.error(error);
throw createVersionError("WORKSPACE_ERROR" /* WORKSPACE_ERROR */, errorMessage);
}
}
/**
* Run the current strategy
* @param targets Optional package targets to process (only used by async strategy)
*/
async run(targets = []) {
try {
const packages = await this.getWorkspacePackages();
return this.currentStrategy(packages, targets);
} catch (error) {
if (error instanceof VersionError || error instanceof GitError) {
log(`Version engine failed: ${error.message} (${error.code || "UNKNOWN"})`, "error");
} else {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Version engine failed: ${errorMessage}`, "error");
}
throw error;
}
}
/**
* Change the current strategy
* @param strategyType The strategy type to use: 'synced', 'single', or 'async'
*/
setStrategy(strategyType) {
this.currentStrategy = this.strategies[strategyType];
}
};
// src/index.ts
async function run() {
const program = new import_commander.Command();
program.name("package-versioner").description(
"A lightweight yet powerful CLI tool for automated semantic versioning based on Git history and conventional commits."
).version(process.env.npm_package_version || "0.0.0").option(
"-c, --config <path>",
"Path to config file (defaults to version.config.json in current directory)"
).option("-d, --dry-run", "Dry run (no changes made)", false).option("-b, --bump <type>", "Force specific bump type (patch|minor|major)").option("-p, --prerelease [identifier]", "Create prerelease version").option("-s, --synced", "Force synchronized versioning across all packages").option("-j, --json", "Output results as JSON", false).option("-t, --target <packages>", "Comma-delimited list of package names to target").parse(process.argv);
const options = program.opts();
if (options.json) {
enableJsonOutput(options.dryRun);
}
try {
const config = await loadConfig(options.config);
log(`Loaded configuration from ${options.config || "version.config.json"}`, "info");
if (options.dryRun) config.dryRun = true;
if (options.synced) config.synced = true;
if (options.bump) config.forceType = options.bump;
if (options.prerelease)
config.prereleaseIdentifier = options.prerelease === true ? "rc" : options.prerelease;
const cliTargets = options.target ? options.target.split(",").map((t) => t.trim()) : [];
const engine = new VersionEngine(config, !!options.json);
if (config.synced) {
log("Using synced versioning strategy.", "info");
engine.setStrategy("synced");
await engine.run();
} else if (config.packages && config.packages.length === 1) {
log("Using single package versioning strategy.", "info");
if (cliTargets.length > 0) {
log("--target flag is ignored for single package strategy.", "warning");
}
engine.setStrategy("single");
await engine.run();
} else {
log("Using async versioning strategy.", "info");
if (cliTargets.length > 0) {
log(`Targeting specific packages: ${cliTargets.join(", ")}`, "info");
}
engine.setStrategy("async");
await engine.run(cliTargets);
}
log("Versioning process completed.", "success");
printJsonOutput();
} catch (error) {
log(error instanceof Error ? error.message : String(error), "error");
process.exit(1);
}
}
run().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});