UNPKG

@fontoxml/fontoxml-development-tools

Version:

Development tools for Fonto.

554 lines (516 loc) 15.4 kB
import semver from 'semver'; /** * @typedef {'full' | 'build' | 'prerelease' | 'core' | 'patch' | 'minor' | 'major'} CompareScope */ const COMPARE_SCOPES = [ 'full', 'build', 'prerelease', 'core', 'patch', 'minor', 'major', ]; export default class Version { /** * @template {boolean} IsUnparsedLiteral * @template {IsUnparsedLiteral extends false ? false : boolean} IsUnversionedNightly * * @param {string|Version} version * @param {boolean} [allowUnparsedLiteral=false] */ constructor(version, allowUnparsedLiteral = false) { if ( (typeof version !== 'string' || !version.length) && !(version instanceof this.constructor) ) { throw new Error(`Could not parse version "${version}".`); } // Sanitize version. version = version instanceof this.constructor ? version.format() : String(version); /** * @type {string} * * @private */ this._rawVersion = version; /** * When set to true, the major.minor.patch-prerelease+build properties are set to null. * @type {IsUnparsedLiteral} */ this.isUnparsedLiteral = false; /** * When set to true, the major.minor.patch-prerelease+build properties are set to null. * @type {IsUnversionedNightly} */ this.isUnversionedNightly = false; /** @type {IsUnversionedNightly extends true ? true : boolean} */ this.isNightly = false; /** @type {IsUnversionedNightly extends true ? Date | null : null} */ this.nightlyDate = null; /** @type {IsUnversionedNightly extends true ? number : null} */ this.major = null; /** @type {IsUnversionedNightly extends true ? number : null} */ this.minor = null; /** @type {IsUnversionedNightly extends true ? number : null} */ this.patch = null; /** @type {IsUnversionedNightly extends true ? string | null : null} */ this.prerelease = null; /** @type {IsUnversionedNightly extends true ? string | null : null} */ this.build = null; if (version === 'nightly') { this.isNightly = true; this.isUnversionedNightly = true; } else if (version.startsWith('Nightlies for ')) { this.isNightly = true; this.isUnversionedNightly = true; const date = version.substring(14); if ( !date.match( /^(\d+)-(\d+)-(\d+)T(\d+):(\d{2}):(\d{2})\.(\d{3})Z$/ ) ) { throw new Error( `Could not parse nightly version with date "${version}".` ); } this.nightlyDate = new Date(date); } else { const versionParts = semver.parse(version); if (!versionParts) { if (!allowUnparsedLiteral) { throw new Error(`Could not parse version "${version}".`); } this.isUnparsedLiteral = true; } else { this.major = +versionParts.major; this.minor = +versionParts.minor; this.patch = +versionParts.patch; this.prerelease = versionParts.prerelease.length ? versionParts.prerelease.join('.') : null; this.build = versionParts.build.length ? versionParts.build.join('.') : null; // Support a versioned nightly. if ( this.prerelease && this.prerelease.match(/^nightly(\.|$)/) ) { const utcDate = this.prerelease.substring(8); const utcDateParts = utcDate.match( /^(?<year>\d{4,})(?<month>\d{2})(?<day>\d{2})(?<hour>\d{2})(?<minute>\d{2})(?<second>\d{2})$/ ); if (!utcDateParts || !utcDateParts.groups) { throw new Error( `Could not parse nightly version with date "${version}".` ); } this.isNightly = true; this.isUnversionedNightly = false; this.nightlyDate = new Date( Date.UTC( parseInt(utcDateParts.groups.year, 10), parseInt(utcDateParts.groups.month, 10) - 1, parseInt(utcDateParts.groups.day, 10), parseInt(utcDateParts.groups.hour, 10), parseInt(utcDateParts.groups.minute, 10), parseInt(utcDateParts.groups.second, 10), 0 ) ); } } } /** @type {IsUnversionedNightly extends true ? false : IsUnparsedLiteral extends true ? false : boolean} */ this.isStable = !this.isUnparsedLiteral && !this.isNightly && !this.prerelease && !this.build; /** @type {IsUnversionedNightly extends true ? true : IsUnparsedLiteral extends true : true : false} */ this.isUnversioned = !!this.isUnparsedLiteral || !!this.isUnversionedNightly; /** * Is used to rewrite add-ons on upgrade from 6.x.x to 7.x.x. * * @type {boolean} */ this.isPre7_0_0 = !this.isUnversioned && this.major < 7; /** * Is used to switch between the different FDT editor modules switching the build system, * doing an npm install or not, and converting pre 7.7.0 code from AMD to ESM. * * @type {boolean} */ this.isPre7_7_0 = this.nightlyDate ? this.nightlyDate < new Date('2019-05-17T12:00:00.000Z') : semver.satisfies(this._rawVersion, '<=7.6.x', { includePrerelease: true, }); /** * Is used to check if changing add-ons is supported, which was not supported before 7.8.2 * for which the SDK portal was still used. * * @type {boolean} */ this.isPre7_8_2 = this.nightlyDate ? this.nightlyDate < new Date('2019-11-21T12:00:00.000Z') : semver.satisfies(this._rawVersion, '<=7.8.1', { includePrerelease: true, }); /** * Is used to determine if `xq` should be used for selectors in the code generated by the * editor init command. * * @type {boolean} */ this.isPre7_17_0 = this.nightlyDate ? this.nightlyDate < new Date('2021-12-23T12:00:00.000Z') : semver.satisfies(this._rawVersion, '<=7.16.x', { includePrerelease: true, }); /** * Is used to determine if the `enable-experiment/correct-selection-after-enter` * configuration should be used in the code generated by the editor init command. * * @type {boolean} */ this.isPre8_1_0 = this.nightlyDate ? this.nightlyDate < new Date('2022-05-30T12:00:00.000Z') : semver.satisfies(this._rawVersion, '<=8.0.x', { includePrerelease: true, }); /** * Is used to determine if the review backend app should require sx.xml configuration. * * @type {boolean} */ this.isPre8_15_0 = this.nightlyDate ? this.nightlyDate < new Date('2025-11-17T12:00:00.000Z') : semver.satisfies(this._rawVersion, '<=8.14.x', { includePrerelease: true, }); } /** * Format the version as a string. * * @param {'full' | 'build' | 'prerelease' | 'core' | 'patch' | 'minor' | 'major'} format * * @throws When the version `isUnversioned` is true (e.g. due to `isUnversionedNightly`, or * `isUnparsedLiteral`). Be sure to check on `isUnversioned` before using one of those * formats, which includes 'build', 'prerelease', 'patch', 'minor', 'major'. * @return {string} */ format(format = 'full') { const parts = [ `${this.major}`, `.${this.minor}`, `.${this.patch}`, this.prerelease ? `-${this.prerelease}` : undefined, this.build ? `+${this.build}` : undefined, ]; switch (format) { case 'full': if (this.isUnversioned) { return this._rawVersion; } return parts.slice(0, 5).join(''); case 'build': if (this.isUnversionedNightly) { throw new Error( `Cannot use "${format}" formatter on a nightly version.` ); } if (this.isUnparsedLiteral) { throw new Error( `Cannot use "${format}" formatter on an unparsed literal version.` ); } return parts.slice(0, 5).join(''); case 'prerelease': if (this.isUnversionedNightly) { throw new Error( `Cannot use "${format}" formatter on a nightly version.` ); } if (this.isUnparsedLiteral) { throw new Error( `Cannot use "${format}" formatter on an unparsed literal version.` ); } return parts.slice(0, 4).join(''); case 'core': if (this.isUnversionedNightly) { return 'nightly'; } if (this.isUnparsedLiteral) { return this._rawVersion; } return parts.slice(0, 3).join(''); case 'patch': if (this.isUnversionedNightly) { throw new Error( `Cannot use "${format}" formatter on a nightly version.` ); } if (this.isUnparsedLiteral) { throw new Error( `Cannot use "${format}" formatter on an unparsed literal version.` ); } return parts.slice(0, 3).join(''); case 'minor': if (this.isUnversionedNightly) { throw new Error( `Cannot use "${format}" formatter on a nightly version.` ); } if (this.isUnparsedLiteral) { throw new Error( `Cannot use "${format}" formatter on an unparsed literal version.` ); } return parts.slice(0, 2).join(''); case 'major': if (this.isUnversionedNightly) { throw new Error( `Cannot use "${format}" formatter on a nightly version.` ); } if (this.isUnparsedLiteral) { throw new Error( `Cannot use "${format}" formatter on an unparsed literal version.` ); } return parts.slice(0, 1).join(''); default: throw new Error(`Invalid format "${format}" for version.`); } } /** * WARNING: Use .format() instead of .toString(). */ toString() { throw new Error(`Use .format() instead of .toString() for Version.`); } /** * Compare/sort versions, oldest first. * * Sort order: * - '8.1.0' * - '8.2.0-rc.1' * - '8.2.0' * - 'Nightlies for <date>' * - 'nightly' * - 'literal' * * @param {Version} versionA * @param {Version} versionB * @param {CompareScope} [scope='full'] * * @return {-1 | 0 | 1} `0` if `versionA` == `versionB`, `1` if `versionA` is greater, `-1` if `versionB` is greater. */ static compare(versionA, versionB, scope = 'full') { // Unparsed literal versions are sorted after versions and nightlies. if (versionA.isUnparsedLiteral && versionB.isUnparsedLiteral) { return versionA._rawVersion.localeCompare(versionB._rawVersion); } if (versionA.isUnparsedLiteral) { return 1; } if (versionB.isUnparsedLiteral) { return -1; } if ( versionA.isUnversionedNightly && versionB.isUnversionedNightly && versionA.nightlyDate && versionB.nightlyDate ) { return versionA.nightlyDate.getTime() === versionB.nightlyDate.getTime() ? 0 : versionA.nightlyDate > versionB.nightlyDate ? 1 : -1; } if (versionA.isUnversionedNightly && versionA.nightlyDate) { return versionB.isUnversionedNightly ? -1 : 1; } if (versionB.isUnversionedNightly && versionB.nightlyDate) { return versionA.isUnversionedNightly ? 1 : -1; } if (versionA.isUnversionedNightly && versionB.isUnversionedNightly) { return 0; } if (versionA.isUnversionedNightly) { return 1; } if (versionB.isUnversionedNightly) { return -1; } if (!COMPARE_SCOPES.includes(scope)) { throw new Error(`Invalid scope "${scope}" for version compare.`); } const shouldCoerce = scope === 'minor' || scope === 'major'; const scopedVersionA = shouldCoerce ? semver.coerce(versionA.format(scope)) : versionA.format(scope); const scopedVersionB = shouldCoerce ? semver.coerce(versionB.format(scope)) : versionB.format(scope); return semver.compareBuild(scopedVersionA, scopedVersionB); } /** * Reverse compare/sort versions, newest first. * * Sort order: * - 'literal' * - 'nightly' * - 'Nightlies for <date>' * - '8.2.0' * - '8.2.0-rc.1' * - '8.1.0' * * @param {Version} versionA * @param {Version} versionB * @param {CompareScope} [scope='full'] * * @return {-1 | 0 | 1} `0` if `versionA` == `versionB`, `1` if `versionA` is greater, `-1` if `versionB` is greater. */ static compareReverse(versionA, versionB, scope = 'full') { return Version.compare(versionB, versionA, scope); } /** * Check whether the provided version is newer, older, or the same. * * @param {Version} version * @param {CompareScope} [scope='full'] * * @return {-1 | 0 | 1} `0` if `this` == `version`, `1` if `this` is greater, `-1` if `version` is greater. */ compare(version, scope = 'full') { return this.constructor.compare(this, version, scope); } /** * @param {Version} versionA * @param {Version} versionB * @param {CompareScope | 'upgrade_minor'} [scope='full'] * @param {Version[] | null} [upgradeSupportedVersions=null] Only needed when `Scope` is one of `upgrade_*`. * * @return {boolean | 'nightly_a' | 'nightly_b' | 'unparsed_literal_a' | 'unparsed_literal_b'} */ static checkCompatibility( versionA, versionB, scope = 'full', upgradeSupportedVersions = null ) { if ( !versionA || !(versionA instanceof this) || !versionB || !(versionB instanceof this) ) { const versionAString = versionA && versionA instanceof this ? versionA.format() : String(versionA); const versionBString = versionB && versionB instanceof this ? versionB.format() : String(versionB); throw new Error( `Could not check version compatibility between "${versionAString}" and "${versionBString}".` ); } if (versionA.isUnversionedNightly) { return 'nightly_a'; } if (versionB.isUnversionedNightly) { return versionA.isNightly ? 'nightly_a' : 'nightly_b'; } if (versionA.isUnparsedLiteral) { return 'unparsed_literal_a'; } if (versionB.isUnparsedLiteral) { return 'unparsed_literal_b'; } switch (scope) { case 'full': case 'build': case 'prerelease': case 'core': case 'patch': case 'minor': case 'major': { const sameScopedVersion = versionA.format(scope) === versionB.format(scope); if (!sameScopedVersion) { return false; } return versionA.isNightly ? 'nightly_a' : versionB.isNightly ? 'nightly_b' : true; } case 'upgrade_minor': { // - `versionA` is the version to upgrade to. // - `versionB` should either be the same minor or one of the // supported `upgradeSupportedVersions` minors. // Downgrade is not allowed. if (versionA.compare(versionB, 'minor') <= -1) { return false; } // Check against the previous minor versions. if (!upgradeSupportedVersions) { throw new Error( `Could not determine version compatibility for upgrade due to not knowing the previous version for "${versionA.format()}".` ); } const samePreviousVersion = upgradeSupportedVersions.some( (previousVersion) => !previousVersion.isUnversioned && previousVersion.format('minor') === versionB.format('minor') ); if (samePreviousVersion) { return versionA.isNightly ? 'nightly_a' : versionB.isNightly ? 'nightly_b' : true; } return false; } default: throw new Error( `Invalid scope "${scope}" for version compatibility check.` ); } } /** * @param {Version} version * @param {CompareScope | 'upgrade_minor'} [scope='full'] * @param {Version[] | null} [upgradeSupportedVersions=null] Only needed when `Scope` is one of `upgrade_*`. * * @return {boolean | 'nightly_a' | 'nightly_b' | 'unparsed_literal_a' | 'unparsed_literal_b'} */ checkCompatibility( version, scope = 'full', upgradeSupportedVersions = null ) { return this.constructor.checkCompatibility( this, version, scope, upgradeSupportedVersions ); } }