UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

430 lines (383 loc) 19.2 kB
// SPDX-License-Identifier: Apache-2.0 import {SoloError} from '../../core/errors/solo-error.js'; import {IllegalArgumentError} from '../../core/errors/illegal-argument-error.js'; import {Numbers} from './numbers.js'; /** * A class representing a semantic version, which can be initialized with either a string or a number. * The class provides methods for comparing semantic versions, as well as validating and formatting them. * It supports both standard semantic versioning (e.g., "1.0.0", "2.1.3-alpha+001") and a simplified numeric versioning (e.g., 1, 2). * The class also includes a static method for validating and formatting semantic version strings with optional 'v' prefix handling. * * @template T - The type of the original value used to create the semantic version, either string or number */ export class SemanticVersion<T extends string | number> { /** * Constant value representing a zero version number. */ public static readonly ZERO: SemanticVersion<string> = new SemanticVersion<string>('0'); /** * The major version number, which is incremented for incompatible API changes. * Initialized to 0 by default. * * @readonly */ public readonly major: number = 0; /** * The minor version number, which is incremented for added functionality in a backwards-compatible manner. * Initialized to 0 by default. * * @readonly */ public readonly minor: number = 0; /** * The patch version number, which is incremented for backwards-compatible bug fixes. * Initialized to 0 by default. * * @readonly */ public readonly patch: number = 0; /** * The pre-release version, which is denoted by a hyphen and can contain alphanumeric identifiers separated by dots. * It indicates that the version is unstable and may not satisfy the intended compatibility requirements as denoted by * its associated normal version. * Initialized to null by default. * * @readonly */ public readonly preRelease: string | null = undefined; /** * The build metadata, which is denoted by a plus sign and can contain alphanumeric identifiers separated by dots. * It is ignored when determining version precedence but can be used to provide additional build information. * Initialized to null by default. * * @readonly */ public readonly buildMetadata: string | null = undefined; /** * The type of the original value used to create the semantic version, either 'string' or 'number'. * This is used to determine how to format the version when converting it back to a string. * * @readonly */ public readonly tType: 'string' | 'number' = 'string'; /** * Creates a new SemanticVersion instance from a string or number. The constructor validates the input and parses it * into its components (major, minor, patch, pre-release, build metadata) based on the type of the original value. * @param originalValue - The original value used to create the semantic version, which can be a string * (e.g., "1.0.0", "2.1.3-alpha+001") or a number (e.g., 1, 2) * @throws IllegalArgumentError if the original value is not a valid semantic version string or a non-negative integer */ public constructor(private readonly originalValue: T | SemanticVersion<T>) { if (!SemanticVersion.isSemanticVersion(this.originalValue)) { throw new IllegalArgumentError(`Invalid semantic version: ${this.originalValue}`, this.originalValue); } if (this.originalValue instanceof SemanticVersion) { this.major = this.originalValue.major; this.minor = this.originalValue.minor; this.patch = this.originalValue.patch; this.preRelease = this.originalValue.preRelease; this.buildMetadata = this.originalValue.buildMetadata; this.tType = this.originalValue.tType; } else if ( Numbers.isNumeric(`${this.originalValue}`) && Number.isSafeInteger(this.originalValue) && (this.originalValue as number) >= 0 ) { this.major = this.originalValue as number; this.tType = 'number'; } else if (SemanticVersion.isSemanticVersion(this.originalValue) && typeof this.originalValue === 'string') { this.tType = 'string'; // @ts-expect-error - This is safe because the constructor will throw if the type is not correct this.originalValue = (originalValue as string).trim(); const versionParts: string[] = this.originalValue.split('.'); this.major = Number.parseInt(versionParts[0].replace(/^v/, ''), 10); if (versionParts.length > 1) { this.minor = Number.parseInt(versionParts[1], 10); if (versionParts.length > 2) { this.patch = Number.parseInt(versionParts[2].split('-')[0].split('+')[0], 10); } } const preReleaseMatch: RegExpMatchArray = this.originalValue.match(/-(.+?)(?:\+|$)/); if (preReleaseMatch) { this.preRelease = preReleaseMatch[1]; } const buildMetadataMatch: RegExpMatchArray = this.originalValue.match(/\+(.+)$/); if (buildMetadataMatch) { this.buildMetadata = buildMetadataMatch[1]; } } } /** * Checks if this semantic version is equal to another semantic version or a valid string/number representation of a semantic version. * @param other - The other semantic version or a valid string/number to compare against * @returns true if they are equal, false otherwise */ public equals(other: SemanticVersion<T> | T): boolean { // other must be a valid semantic version (either an instance of SemanticVersion or a valid string/number) if (!SemanticVersion.isSemanticVersion(other)) { return false; } // if both are the same instance, they are equal if (this === other) { return true; } const otherValue: SemanticVersion<T> = new SemanticVersion<T>(other); return ( this.tType === otherValue.tType && this.major === otherValue.major && this.minor === otherValue.minor && this.patch === otherValue.patch && this.preRelease === otherValue.preRelease && (this.buildMetadata === otherValue.buildMetadata || !this.buildMetadata || !otherValue.buildMetadata) ); } /** * Compares this semantic version to another semantic version or a valid string/number representation of a semantic version. * @returns 0 if they are equal, 1 if this version is greater, -1 if this version is less, or NaN if the other value is not a valid semantic version * @param other - The other semantic version or a valid string/number to compare against * @throws IllegalArgumentError if the other value is not a valid semantic version * @remarks The comparison is based on the precedence rules defined in the Semantic Versioning specification, which * considers major, minor, patch, pre-release, and build metadata components. Pre-release versions are considered * less than their associated normal versions, and build metadata does not affect precedence. */ public compare(other: SemanticVersion<T> | T): number { // throw an error if the other value is not a valid semantic version if (!SemanticVersion.isSemanticVersion(other)) { throw new IllegalArgumentError(`Cannot compare with non-semantic version: ${other}`, other); } if (this.equals(other)) { return 0; } if (this.greaterThan(other)) { return 1; } if (this.lessThan(other)) { return -1; } return Number.NaN; // This should never happen if the above conditions are exhaustive } /** * Determines if this semantic version is greater than another semantic version or a valid string/number representation of a semantic version. * @returns true if this version is greater, false otherwise * @param other - The other semantic version or a valid string/number to compare against * @remarks The comparison is based on the precedence rules defined in the Semantic Versioning specification, which * considers major, minor, patch, pre-release, and build metadata components. Pre-release versions are considered * less than their associated normal versions, and build metadata does not affect precedence. */ public greaterThan(other: SemanticVersion<T> | T): boolean { const otherValue: SemanticVersion<T> = new SemanticVersion<T>(other); if (this.major > otherValue.major) { return true; } if (this.major < otherValue.major) { return false; } if (this.minor > otherValue.minor) { return true; } if (this.minor < otherValue.minor) { return false; } if (this.patch > otherValue.patch) { return true; } if (this.patch < otherValue.patch) { return false; } // Handle pre-release comparison if (this.preRelease && !otherValue.preRelease) { return false; } // Pre-release is less than no pre-release if (!this.preRelease && otherValue.preRelease) { return true; } // No pre-release is greater than pre-release if (this.preRelease && otherValue.preRelease) { const thisPreReleaseParts: string[] = this.preRelease.split('.'); const otherPreReleaseParts: string[] = otherValue.preRelease.split('.'); for (let index: number = 0; index < Math.max(thisPreReleaseParts.length, otherPreReleaseParts.length); index++) { const thisPart: string = thisPreReleaseParts[index]; const otherPart: string = otherPreReleaseParts[index]; if (thisPart === undefined) { return false; } // shorter pre-release is less if (otherPart === undefined) { return true; } // shorter pre-release is less const thisPartIsNumeric: boolean = Numbers.isNumeric(thisPart); const otherPartIsNumeric: boolean = Numbers.isNumeric(otherPart); if (thisPartIsNumeric && otherPartIsNumeric) { const thisNumber: number = Number.parseInt(thisPart, 10); const otherNumber: number = Number.parseInt(otherPart, 10); if (thisNumber > otherNumber) { return true; } if (thisNumber < otherNumber) { return false; } } else if (thisPartIsNumeric) { return false; // Numeric identifiers have lower precedence than non-numeric (semver §11.4.4) } else if (otherPartIsNumeric) { return true; // Numeric identifiers have lower precedence than non-numeric (semver §11.4.4) } else { if (thisPart > otherPart) { return true; } if (thisPart < otherPart) { return false; } } } } // Build metadata does not affect precedence return false; } /** * Determines if this semantic version is less than another semantic version or a valid string/number representation of a semantic version. * @returns true if this version is less, false otherwise * @param other - The other semantic version or a valid string/number to compare against * @remarks The comparison is based on the precedence rules defined in the Semantic Versioning specification, which * considers major, minor, patch, pre-release, and build metadata components. Pre-release versions are considered * less than their associated normal versions, and build metadata does not affect precedence. */ public lessThan(other: SemanticVersion<T> | T): boolean { return !this.equals(other) && !this.greaterThan(other); } /** * Determines if this semantic version is greater than or equal to another semantic version or a valid string/number representation of a semantic version. * @returns true if this version is greater than or equal, false otherwise * @param other - The other semantic version or a valid string/number to compare against * @remarks The comparison is based on the precedence rules defined in the Semantic Versioning specification, which * considers major, minor, patch, pre-release, and build metadata components. Pre-release versions are considered * less than their associated normal versions, and build metadata does not affect precedence. */ public greaterThanOrEqual(other: SemanticVersion<T> | T): boolean { return this.equals(other) || this.greaterThan(other); } /** * Determines if this semantic version is less than or equal to another semantic version or a valid string/number * representation of a semantic version. * @returns true if this version is less than or equal, false otherwise * @param other - The other semantic version or a valid string/number to compare against * @remarks The comparison is based on the precedence rules defined in the Semantic Versioning specification, which * considers major, minor, patch, pre-release, and build metadata components. Pre-release versions are considered * less than their associated normal versions, and build metadata does not affect precedence. */ public lessThanOrEqual(other: SemanticVersion<T> | T): boolean { return this.equals(other) || this.lessThan(other); } /** * Converts this semantic version to a string representation. If the original type was a number, it returns just the * major version as a string. * If the original type was a string, it returns the full semantic version string, including pre-release and build * metadata if present. * @returns The string representation of this semantic version */ public toString(): string { if (this.tType === 'number') { return `${this.major}`; } return `${this.major}.${this.minor}.${this.patch}${this.preRelease ? `-${this.preRelease}` : ''}${this.buildMetadata ? `+${this.buildMetadata}` : ''}`; } /** * Converts this semantic version to a string representation with a 'v' prefix. If the original type was a number, it * returns just the major version with a 'v' prefix. * If the original type was a string, it returns the full semantic version string with a 'v' prefix, including * pre-release and build metadata if present. * @returns The string representation of this semantic version with a 'v' prefix */ public toPrefixedString(): string { return `v${this.toString()}`; } /** * Validates if a value is a valid semantic version, which can be an instance of SemanticVersion, a numeric value, or a * string that matches the semantic version pattern. * The validation allows for an optional 'v' prefix in string representations and ensures that numeric values are * non-negative safe integers. * @returns true if the value is a valid semantic version, false otherwise * @param value - The value to validate, which can be an instance of SemanticVersion, a numeric value, or a string * @private */ private static isSemanticVersion<R extends string | number | SemanticVersion<string | number>>(value: R): boolean { // if it's an instance of SemanticVersion it must be valid if (value instanceof SemanticVersion) { return true; } if (typeof value === 'string') { value = value.trim().replace(/^v/, '') as R; // Remove 'v' prefix if present for validation } // if it's numeric, a safe integer, and non-negative, it's valid if (Numbers.isNumeric(`${value}`) && Number.isSafeInteger(value) && (value as number) >= 0) { return true; } // if it's a string and matches a semantic version regex pattern, // it's valid, // allows for an optional 'v' prefix, // '0', // '0.1', // '0.0.1', // as well as acceptable pre-release and build metadata formats if (typeof value === 'string') { return /^v?(0|[1-9]\d*)(\.(0|[1-9]\d*)(\.(0|[1-9]\d*)(?:-[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*)?(?:\+[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*)?)?)?$/.test( value.trim(), ); } return false; } /** * Validates if a string is a valid semantic version and handles the 'v' prefix * * @param versionString - The version string to validate * @param isNeedPrefix - If true, adds 'v' prefix if missing; if false, removes 'v' prefix if present * @param label - Label to use in error messages (e.g., 'Release tag', 'SemanticVersion') * @returns The processed version string with proper prefix handling * @throws SoloError or IllegalArgumentError if the version string is invalid */ public static getValidSemanticVersion( versionString: string, isNeedPrefix: boolean = false, label: string = 'SemanticVersion', ): string { if (!versionString) { throw new SoloError(`${label} cannot be empty`); } // Validate the version string if (!SemanticVersion.isSemanticVersion<string>(versionString)) { throw new IllegalArgumentError(`Invalid ${label.toLowerCase()}: ${versionString}`, versionString); } const value: SemanticVersion<string> = new SemanticVersion<string>(versionString); return isNeedPrefix ? value.toPrefixedString() : value.toString(); } /** * Returns a new SemanticVersion instance with the minor version incremented by 1 and major versions unchanged. * The patch version is reset to 0, and pre-release and build metadata are cleared. * The returned instance is always of type SemanticVersion<string> to ensure that the version is represented in a * standard semantic version format. * @returns A new SemanticVersion instance with the minor version incremented by 1 * @remarks This method is useful for automatically generating the next minor version based on the current version, * following semantic versioning rules. * For example, if the current version is "1.2.3", calling bumpMinor() will return a new SemanticVersion instance * representing "1.3.0". * If the current version is "1.2.3-alpha+001", calling bumpMinor() will return a new SemanticVersion instance * representing "1.3.0" (pre-release and build metadata are cleared). */ public bumpMinor(): SemanticVersion<string> { return new SemanticVersion<string>(`${this.major}.${this.minor + 1}.${0}`); } /** * Returns a new SemanticVersion instance with the major version incremented by 1 and minor and patch versions reset to 0. * Pre-release and build metadata are cleared. * The returned instance is of the same type as the original (string or number) to maintain consistency in version representation. * @returns A new SemanticVersion instance with the major version incremented by 1 * @remarks This method is useful for automatically generating the next major version based on the current version, * following semantic versioning rules. * For example, if the current version is "1.2.3", calling bumpMajor() will return a new SemanticVersion instance * representing "2.0.0". */ public bumpMajor(): SemanticVersion<T> { return this.tType === 'number' ? // @ts-expect-error - This is safe because the constructor will throw if the type is not correct new SemanticVersion<number>(this.major + 1) : // @ts-expect-error - This is safe because the constructor will throw if the type is not correct new SemanticVersion<string>(`${this.major + 1}.0.0`); } }