UNPKG

package-versioner

Version:

A lightweight yet powerful CLI tool for automated semantic versioning based on Git history and conventional commits.

1,116 lines (1,092 loc) 38.9 kB
#!/usr/bin/env node // src/index.ts import { Command } from "commander"; // src/config.ts import * as fs from "node:fs"; import { cwd } from "node:process"; function loadConfig(configPath) { const localProcess = 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 import { cwd as cwd4 } from "node:process"; import { getPackagesSync } from "@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 import chalk from "chalk"; import figlet from "figlet"; // 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 = chalk.green; break; case "warning": chalkFn = chalk.yellow; break; case "error": chalkFn = chalk.red; break; case "debug": chalkFn = chalk.gray; break; default: chalkFn = chalk.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 import fs4 from "node:fs"; import * as path3 from "node:path"; // src/git/commands.ts import { cwd as cwd2 } from "node:process"; // src/git/commandExecutor.ts import { exec, execSync as nativeExecSync } from "node:child_process"; var execAsync = (command, options) => { const defaultOptions = { maxBuffer: 1024 * 1024 * 10, ...options }; return new Promise((resolve, reject) => { exec( command, defaultOptions, (error, stdout, stderr) => { if (error) { reject(error); } else { resolve({ stdout: stdout.toString(), stderr: stderr.toString() }); } } ); }); }; var execSync = (command, args) => nativeExecSync(command, { maxBuffer: 1024 * 1024 * 10, ...args }); // src/git/repository.ts import { existsSync, statSync } from "node:fs"; import { join } from "node:path"; function isGitRepository(directory) { const gitDir = join(directory, ".git"); if (!existsSync(gitDir)) { return false; } const stats = 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(cwd2())) { 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 import { getSemverTags } from "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 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 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 import fs2 from "node:fs"; function updatePackageVersion(packagePath, version) { try { const packageContent = fs2.readFileSync(packagePath, "utf8"); const packageJson = JSON.parse(packageContent); const packageName = packageJson.name; packageJson.version = version; fs2.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 import path2 from "node:path"; import { exit } from "node:process"; // src/core/versionCalculator.ts import * as fs3 from "node:fs"; import * as path from "node:path"; import { cwd as cwd3 } from "node:process"; import { Bumper } from "conventional-recommended-bump"; import semver from "semver"; 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 = semver.clean(latestTag) || latestTag; const currentVersion = semver.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0"; if (STANDARD_BUMP_TYPES.includes(specifiedType) && semver.prerelease(currentVersion)) { log( `Cleaning prerelease identifier from ${currentVersion} for ${specifiedType} bump`, "debug" ); return bumpVersion(currentVersion, specifiedType, prereleaseIdentifier); } return semver.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 = semver.clean(latestTag) || latestTag; const currentVersion = semver.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0"; log(`Applying ${branchVersionType} bump based on branch pattern`, "debug"); return semver.inc(currentVersion, branchVersionType, void 0) || ""; } } try { const bumper = new 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 || cwd3(); 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 = semver.clean(latestTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0"; return semver.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 || cwd3(); const packageJsonPath = path.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) && semver.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 semver.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 (semver.prerelease(currentVersion) && STANDARD_BUMP_TYPES.includes(bumpType)) { const parsed = semver.parse(currentVersion); if (bumpType === "major" && (parsed == null ? void 0 : parsed.major) === 1 && parsed.minor === 0 && parsed.patch === 0 && semver.prerelease(currentVersion)) { return `${parsed.major}.${parsed.minor}.${parsed.patch}`; } log(`Cleaning prerelease identifier from ${currentVersion} for ${bumpType} bump`, "debug"); return semver.inc(currentVersion, bumpType) || ""; } return semver.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(path2.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) => path2.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); 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 = path3.join(packages.root, "package.json"); if (fs4.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 = path3.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 = path3.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 = getPackagesSync(cwd4()); 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 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); });