tomchentw/avn-homebrew-with-versions

View on GitHub
src/homebrew-with-versions.js

Summary

Maintainability
A
0 mins
Test Coverage
import semverRegExpFactory from 'semver-regex';

import {
  satisfies,
  gt,
  major,
  minor,
  valid,
  parse,
} from 'semver';

import Promise from 'any-promise';

import {
  exec,
} from 'child-process-promise';

const VERSION_REGEX = /(\w+)(?:-|@)(.+)/;

/**
 * Extract a name from a version (to support iojs)
 *
 * @function
 * @param {string} version
 * @return {string}
 */
function versionName(version) {
  const matchResult = version.match(VERSION_REGEX);
  const name = matchResult ? matchResult[1] : null;
  if (name && name === `iojs`) {
    return `io`;
  } else {
    return `node`;
  }
}

/**
 * Extract just the version number from a version.
 *
 * @function
 * @param {string} version
 * @return {string}
 */
function versionNumber(version) {
  const matchResult = version.match(VERSION_REGEX);
  return matchResult ? matchResult[2] : version;
}

const HOMEBREW_VERSIONS_NODE = /^homebrew\/versions\/node(\d+(-lts)?)$/;
const HOMEBREW_NODE = /^node$/;

/**
 * Sanitize raw output of 'brew search node' and return names list.
 *
 * @function
 * @param {string} raw
 * @return {Array[string]}
 */
function _parseNames(raw) {
  return raw.split(`\n`).filter(line =>
    HOMEBREW_VERSIONS_NODE.test(line) ||
    HOMEBREW_NODE.test(line)
  );
}

const SEMVER_REGEXP = semverRegExpFactory();

/**
 * Sanitize raw output of 'brew info node' and return a semver version.
 *
 * @function
 * @param {string} raw
 * @return {string}
 */
function _parseVersion(raw) {
  const firstLine = raw.split(`\n`)[0];
  if (SEMVER_REGEXP.test(firstLine)) {
    return firstLine.match(SEMVER_REGEXP)[0];
  } else {
    throw new Error(`Parse semver error: unknown version from (${firstLine})`);
  }
}

/**
 * Find current version.
 *
 * @function
 * @return {Promise<String>
 */
function _findCurrentVersion() {
  return Promise.resolve()
    .then(() => exec(`node -v`))
    .then(({ stdout }) => parse(stdout.toString().trim()).version)
    .catch(error => false);
}

/**
 * Search available versions list.
 *
 * @function
 * @return {Promise<Array[String]>
 */
function _SearchAvailableVersions() {
  return Promise.resolve()
    .then(() => exec(`brew tap homebrew/versions && brew search node`))
    .then(({ stdout, stderr }) => _parseNames(stdout.toString()));
}

/**
 * List all versions.
 *
 * @function
 * @return {Promise<Array[{String, String}]>
 */
function _listVersions() {
  return Promise.all([
    _findCurrentVersion(),
    _SearchAvailableVersions(),
  ])
    .then(([currentVersion, availableVersions]) => {
      function transformToNameVersionTuple(name) {
        return exec(`brew info ${name}`)
          .then(({ stdout, stderr }) => _parseVersion(stdout.toString()))
          .then(version => ({
            name,
            version,
            isCurrent: currentVersion && major(version) === major(currentVersion),
          }));
      }

      return Promise.all(availableVersions.map(transformToNameVersionTuple));
    });
}

function _isVersionSatisfies(version, mNumber) {
  if (satisfies(version, mNumber)) {
    return true;
  }
  if (valid(mNumber) && major(version) === major(mNumber)) {
    return (
      major(version) !== 0 ||
      minor(version) === minor(mNumber)
    );
  }
  return false;
}

/**
 * Find a highest matched version from installed versions.
 *
 * @param {Array[{String, String}]} installed versions
 * @param {string} matching
 * @return {{String, String}}
 */
function _findVersion(versions, matching) {
  const mName = versionName(matching);
  const mNumber = versionNumber(matching);

  return versions.reduce((acc, v) => {
    const current = v.isCurrent ? v : acc.current;
    let { next } = acc;

    if (v.name.match(mName) && _isVersionSatisfies(v.version, mNumber)) {
      if (!next) {
        next = v;
      } else if (gt(v.version, next.version)) {
        next = v;
      }
    }
    return {
      current,
      next,
    };
  }, {
    current: null,
    next: null,
  });
}

/**
 * Get installed version matching a given version.
 *
 * @param {string} matching
 * @return {Promise}
 */
function _installedVersion(matching) {
  return Promise.resolve()
    .then(() => _listVersions())
    .then(versions => _findVersion(versions, matching));
}

/**
 * Match a specific version.
 *
 * @public interface used by avn
 * @param {string} matching
 * @return {Promise}
 */
export function match(matching) {
  return Promise.resolve()
    .then(() => _installedVersion(matching))
    .then(({ current, next }) => {
      if (!next) {
        throw new Error(`no version matching ${matching}`);
      }
      let command = (
`
brew install --without-npm ${next.name} > /dev/null
 && 
brew link ${next.name} > /dev/null
`.replace(/\n/g, ``)
      );
      if (current && current !== next) {
        command = `brew unlink ${current.name} > /dev/null && ${command}`;
      }

      return {
        version: next.version,
        command,
      };
    });
}