// import project dependencies
import semver from 'semver';
// import local dependencies
import {EnvironmentList} from '../index';
import {Environments} from '../constants';
import Environment from '../environment/Environment';
import {crypto} from './contextuals'
/**
* Get a {@link VersionList} for the specified module and environment.
* @param module - The module string (electron, recorder)
* @param environment - The environment value string or {@link Environment} instance
* @returns {Promise<VersionList>}
*/
export const getVersionListByEnvironment = async ({module, environment}) => {
// get the environment by string value if necessary
if (!(environment instanceof Environment)) {
environment = (await EnvironmentList.load()).getEnvironment(environment);
}
// load and return the version list
return await environment.getVersionList(module);
}
/**
*
* Compare the current version to the {@link VersionList} and return an update object
* if there are available updates.
*
* This method will handle environment fallbacks in case of non-existent environment, or
* an environment that doesn't have builds for the specified module. In either case, it will
* fallback to the best suitable static environment. See {@link EnvironmentList#getEnvironmentOrFallback}.
*
* @param module
* @param environment
* @param currentVersion
* @param dependencies
* @param opts
* @returns {Promise<{versionList: {VersionList}, attributes: {isMinor: boolean, isMajor: boolean, isDependency: boolean, isPatch: boolean}, updateAvailable: boolean, latest: {VersionEntry}, fallbackEnvironment: boolean|{Environment}}>}
*/
export const checkForUpdates = async ({module, environment, currentVersion, dependencies = {}}, opts = {
allowDowngrade: true,
log: () => {},
}) => {
// get the environment by string value if necessary
const originalEnvironment = environment;
if (!(environment instanceof Environment)) {
environment = (await EnvironmentList.load()).getEnvironmentOrFallback(environment, {requireModule: module});
}
const versionList = await environment.getVersionList(module);
let legacyEntry = false;
// check for missing fields that indicate the current version is a legacy version
if (!currentVersion.semver || !currentVersion.origin) {
legacyEntry = true;
currentVersion = {
version: currentVersion.version || currentVersion,
semver: semver.coerce(currentVersion.version || currentVersion).version
}
}
let latest = null;
let updateAvailable = false;
const latestEntry = latest = versionList.latest();
const latestIsNewer = semver.gt(latestEntry.semver, currentVersion.semver);
if (legacyEntry || latestEntry.isLegacy()) {
// for legacy entries, only check if semver is greater
updateAvailable = latestIsNewer;
} else {
// for modern entries, we perform a more complex check
const currentVersionEntry = versionList.find((entry) => semver.eq(entry.semver, currentVersion.semver));
if (!currentVersionEntry) {
// no matching semver in list, compare against latest
updateAvailable = latestIsNewer;
} else if (latest.isMutationOf(currentVersionEntry)) {
// if the latest version is a mutation of the current version and the
// current is not explicitly equal in it's attributes, then an update is available
updateAvailable = !latest.isEqual(currentVersionEntry);
} else {
// there is a current version entry, and latest isn't a mutation of it
// an update is available if the latest has a greater semver
updateAvailable = latestIsNewer;
}
}
// check dependencies
let isDependency = false;
if (dependencies && latest.dependencies && Object.keys(dependencies).length > 0 && Object.keys(latest.dependencies).length > 0) {
for (let key of Object.keys(dependencies)) {
if (!latest.dependencies[key]) {
if (typeof opts?.log === 'function') {
opts.log(`Missing dependency on latest update object, comparing to current ${key} property (${dependencies[key]})`)
}
continue
}
const hasDependencyUpdate = opts?.allowDowngrade
? !semver.eq(latest.dependencies[key], dependencies[key])
: semver.gt(latest.dependencies[key], dependencies[key])
if (typeof opts?.log === 'function') {
opts.log(`Downgrade ${opts?.allowDowngrade ? 'allowed' : 'disallowed'}, ${hasDependencyUpdate ? 'found update' : 'no update found'} comparing current dependency ${dependencies[key]} vs latest update ${latest.dependencies[key]}`)
}
if (hasDependencyUpdate) {
updateAvailable = true;
isDependency = true;
break;
}
}
}
const isMajor = updateAvailable &&
semver.coerce(latest.version).major > semver.coerce(currentVersion.version).major;
const isMinor = updateAvailable &&
!isMajor &&
semver.coerce(latest.version).major === semver.coerce(currentVersion.version).major &&
semver.coerce(latest.version).minor > semver.coerce(currentVersion.version).minor;
const isPatch = updateAvailable &&
!isMajor &&
!isMinor &&
semver.coerce(latest.version).major === semver.coerce(currentVersion.version).major &&
semver.coerce(latest.version).minor === semver.coerce(currentVersion.version).minor &&
semver.coerce(latest.version).patch > semver.coerce(currentVersion.version).patch;
// updates are available if the latest version isn't equal
// @todo make sure to check all versions for a mutation of the current version
return {
updateAvailable,
latest,
attributes: {
isMajor,
isMinor,
isPatch,
isDependency
},
versionList,
fallbackEnvironment: originalEnvironment !== environment.getValue() ? environment : false
};
};
export const ucwords = (str) => {
return (str + '').replace(/^([a-z])|\s+([a-z])/g, ($1) => {
return $1.toUpperCase();
});
}
/**
* Return an sha256 hash for a given branch name.
* @param branchName
* @returns {{shortHash: string, hash: string}}
*/
export const getBranchHash = (branchName) => {
const hash = crypto
.createHash('sha256')
.update(branchName.split('/').join('-'))
.digest('hex');
return {
hash,
shortHash: hash.substring(0, 8)
}
}
/**
* Parse a branch name to an environment name value.
* @param {string} branchName
* @returns {string}
*/
export const branchNameToEnvironmentValue = (branchName) => {
const value = branchName.split('/').pop();
if (Object.values(Environments).includes(value)) {
throw new Error('Cannot resolve branch name to environment value due to static environment collision: ' + value);
}
return branchName.split('/').pop();
};
/**
*
* Return true if it's a feature or beta branch.
* @param branchName
* @param branchName
* @returns {{eligible: boolean, prefix: (string)}}
*/
export const eligibleForDynamicEnvironment = (branchName) => {
const isBeta = branchName.startsWith('beta/');
const prefix = isBeta
? 'beta'
: 'sandbox';
return {
eligible: isBeta ||
![Environments.PRODUCTION, Environments.EARLY_ACCESS, Environments.SANDBOX].includes(branchName),
prefix
};
}
/**
* Create a new dynamic {@link Environment} based on the module and branch name that
* triggered the creation of the environment.
* @param {string} branchName
* @param {string} module
* @returns {Environment}
*/
export const createDynamicEnvironment = ({branchName, module}) => {
const eligibilityCheck = eligibleForDynamicEnvironment(branchName);
if (!eligibilityCheck.eligible) {
throw new Error('Branch not eligible for dynamic environment creation: ' + branchName);
}
const envValue = branchNameToEnvironmentValue(branchName);
const props = {
value: envValue,
modules: [module],
target: `${eligibilityCheck.prefix.length > 0 ? (eligibilityCheck.prefix + '/') : ''}${envValue}/`,
experimental: true,
origin: getBranchHash(branchName).shortHash,
public: false,
static: false,
}
return new Environment(props);
}