Source: utils/index.js

// 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);
}