// import dependencies
import semver from 'semver';
import sort from 'semver-sort';
import {getBranchHash} from '../utils';
import {download, unzip} from '../utils/files'
import Rollout from './Rollout'
/**
* A VersionEntry holds all the metadata about a given build. The VersionEntry instance can be used to
* interact with and update build metadata, compare the entry to another one, and even download & extract
* the VersionEntry.
*/
class VersionEntry {
constructor(props = {}) {
this.hash = props.hash
this.autoUpdate = props.autoUpdate !== undefined ? props.autoUpdate : true
this.promptUpdate = props.promptUpdate !== undefined ? props.promptUpdate : true
this.forceUpdate = props.forceUpdate !== undefined ? props.forceUpdate : false
this.patches = props.patches
this.origin = props.origin
this.semver = props.semver
this.downloadUrl = props.downloadUrl
this.checksums = props.checksums
this.releaseTimestamp = props.releaseTimestamp
// dependencies are used to determine if the module version is dependent on
// another specific module version
this.dependencies = props.dependencies;
this.platform = props.platform || process.platform;
// used for electron autoupdate module, for distributingnew electron binaries
this.installerUrl = props.installerUrl;
this.nupkgUrl = props.nupkgUrl;
this.nupkgReleasesFileUrl = props.nupkgReleasesFileUrl;
// rollout configuration
if (props.rollout !== undefined) {
this.rollout = new Rollout(props.rollout)
}
this.version = props.version // legacy
this.legacy = !(!!props.semver && !!props.hash) // legacy
if (!props.version && !props.semver) {
throw new Error('New entry is missing property: version or semver')
}
if (!this.legacy) {
if (!props.hash) {
throw new Error('New entry is missing property: hash')
} else if (!props.branchName && !props.origin) {
throw new Error('New entry is missing property: branchName or origin')
}
// set values for non-legacy versions
this.timestamp = props.timestamp || new Date().getTime()
this.setBranch(props.branchName || props.origin, !props.branchName)
}
this.setVersion(props.semver || props.version)
}
/**
* Set the version entry to the specified semver.
* @param {String} version
*/
setVersion (version) {
version = semver.coerce(version).version
this.semver = version
this.version = `v${version}` // legacy
if (this.promptUpdate === undefined) {
this.promptUpdate = semver.patch(version) === 0 // set true by default when not already set
}
return this
}
/**
* Set the origin branch for the {@link VersionEntry}.
* @param {String} branchName
* @param {Boolean} isOrigin
* @returns {VersionEntry}
*/
setBranch (branchName, isOrigin = false) {
this.origin = isOrigin
? branchName
: getBranchHash(branchName).shortHash;
return this
}
/**
* Set the commit hash for the {@link VersionEntry}.
* @param {String} shortHash
* @returns {VersionEntry}
*/
setCommitHash (shortHash) {
this.hash = shortHash
return this
}
/**
* Set the patches for the {@link VersionEntry}.
* @param {VersionEntry[]} patches
* @returns {VersionEntry}
*/
setPatches (patches) {
this.patches = patches
return this
}
/**
* Set the dependency on another module, specifying the origin branch and the semver to target.
* For example, electron can be dependent on a specific recorder version or vice-versa.
* @param module
* @param semver
*/
setDependency (module, semver) {
if (!this.dependencies) {
this.dependencies = {};
}
this.dependencies[module] = semver
return this
}
/**
* Set the timestamp of the release (not the build itself)
* @param timestamp
* @returns {VersionEntry}
*/
setReleaseTimestamp (timestamp = Date.now()) {
this.releaseTimestamp = timestamp
return this
}
/**
* Set the {@link Rollout} configuration for this {@link VersionEntry}
* @param rollout
*/
setRollout ({direction, seed, percentage, totalBuckets}) {
this.rollout = new Rollout({
direction,
percentage,
seed,
totalBuckets
})
return this
}
/**
* Return true if this is a legacy entry (pre-update refactor, no semver or origin props etc).
* @returns {*|boolean}
*/
isLegacy () {
return this.legacy;
}
/**
* Add a patch to the {@link VersionEntry}.
* @param patch
* @returns {VersionEntry}
*/
addPatch (patch) {
// @todo validate the patch in some better way
if (!patch.hash || !patch.semver || !patch.origin) {
throw new Error('Cannot add invalid patch: ' + JSON.stringify(patch))
}
if (!this.patches) this.patches = []
this.patches.push(patch)
// map the versions to an array, then sort ascending for patches
const sortedVersionStrings = sort.asc(this.patches.map(v => (v.semver || semver.coerce(v.version).version)));
// map back to an array of version entries
this.patches = sortedVersionStrings.map(v => {
return this.patches.find(rv => {
return semver.coerce(rv.semver || rv.version).version === v;
})
})
return this
}
/**
* Set the force update state for the {@link VersionEntry}.
* If true, the users app will restart at the first chance it gets (outside of game) and install the update. Default is false.
* @param {Boolean} forceUpdate
* @returns {VersionEntry}
*/
setForceUpdate (forceUpdate) {
this.forceUpdate = this.forced = forceUpdate
this.forced = forceUpdate // legacy
return this
}
/**
* Set the prompt update state for the {@link VersionEntry}.
* If true, the user will be shown UI prompts to update the app. Default is true.
* @param {Boolean} promptUpdate
* @returns {VersionEntry}
*/
setPromptUpdate (promptUpdate) {
this.promptUpdate = promptUpdate
return this
}
/**
* Set whether or not this {@link VersionEntry} should automatically install updates
* on application launch.
* @param autoUpdate
*/
setAutoUpdate (autoUpdate) {
this.autoUpdate = autoUpdate;
return this
}
/**
* Set the url for the installer MedalSetup.exe for this version.
* @param installer
* @returns {VersionEntry}
*/
setInstallerUrl (installer) {
this.installerUrl = installer;
return this;
}
/**
* Set the url for the NuGet package for this version.
* @param nupkgUrl
* @returns {VersionEntry}
*/
setNupkgUrl (nupkgUrl) {
this.nupkgUrl = nupkgUrl;
return this;
}
/**
* Set the url for the NuGet RELEASES file for this version.
* @param nupkgReleasesFileUrl
* @returns {VersionEntry}
*/
setNupkgReleasesFileUrl (nupkgReleasesFileUrl) {
this.nupkgReleasesFileUrl = nupkgReleasesFileUrl;
return this;
}
/**
* Update the checksums of the {@link VersionEntry}.
* @param checksums
* @returns {VersionEntry}
*/
setChecksums (checksums) {
this.checksums = checksums;
return this;
}
/**
* Return true if this {@link VersionEntry} is a mutation of the specified {@link VersionEntry}, or vice versa.
* @param {VersionEntry} compareTo
* @param opts
* @returns {boolean}
*/
isMutationOf (compareTo, opts = {matchOrigin: true, matchMajorMinor: true, matchSemver: true}) {
if (!this.platform !== compareTo.platform) {
return false
}
// mutation = version match, origin branch match, or one is a patch of the other
if (opts?.matchSemver !== false && semver.eq(this.semver, compareTo.semver)) {
return true
}
if (opts?.matchMajorMinor !== false && semver.major(this.semver) === semver.major(compareTo.semver) && semver.minor(this.semver) === semver.minor(compareTo.semver)) {
return true
}
if (opts?.matchOrigin !== false && this.origin && compareTo.origin && this.origin === compareTo.origin) {
return true
}
return false
}
/**
* Return true if this entry is a mutation of the comparison {@link VersionEntry}, or vice versa.
* @param {VersionEntry} compareTo
* @returns {boolean}
*/
isEqual (compareTo) {
// mutation = version match, origin branch match,
// @todo or one is a patch of the other
return semver.eq(this.semver, compareTo.semver) &&
this.hash === compareTo.hash &&
this.origin === compareTo.origin &&
this.platform === compareTo.platform &&
this.timestamp === compareTo.timestamp
}
/**
* Download the update package for this {@link VersionEntry} to the specified destination.
* @param downloadPath
* @param onProgress
*/
async download (downloadPath, onProgress = () => {}) {
return download(this.downloadUrl, downloadPath, onProgress);
}
/**
* Extract the update package for this {@link VersionEntry} to the specified destination.
*/
async extract (src, dest) {
return unzip(src, dest);
}
/**
* Convert to a parsed JSON object.
* @returns {Object}
*/
toJson () {
const json = JSON.parse(JSON.stringify(this))
if (json.legacy !== undefined) {
delete json.legacy
}
return json
}
}
export default VersionEntry;