All files / core/src/kernel semver.ts

100% Statements 97/97
100% Branches 31/31
100% Functions 2/2
100% Lines 97/97

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 981x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 165x 165x 165x 165x 176x 176x 176x 191x 191x 191x 191x 191x 191x 191x 191x 191x 191x 191x 191x 191x 191x 191x 191x 165x 191x 191x 140x 140x 26x 26x 26x 26x 191x 191x 51x 51x 51x 191x 176x 176x 226x 226x 226x 226x 226x 226x 226x 226x 226x 226x 226x 226x 226x 220x 220x 226x 226x 226x 226x 226x 226x 226x 226x 226x 176x 176x 176x 176x 176x 165x 165x 76x  
/**
 * Best-effort **semver** matcher. The supplied `version` will be tested against
 * the supplied `range`.
 *
 * @param version - The to-be tested semantic `version` string.
 * @param range - The `range` to test the `version` against.
 * @returns Whether `version` satisfies `range`.
 *
 * @example
 * Test `'1.2.3'` against `'>2 <1 || ~1.2.*'`:
 * ```ts
 * import { semver } from '@sgrud/core';
 *
 * semver('1.2.3', '>2 <1 || ~1.2.*'); // true
 * ```
 *
 * @example
 * Test `'1.2.3'` against `'~1.1'`:
 * ```ts
 * import { semver } from '@sgrud/core';
 *
 * semver('1.2.3', '~1.1'); // false
 * ```
 */
export function semver(version: string, range: string): boolean {
  const input = version.replace(/\+.*$/, '').split(/[-.]/);
  const paths = range.split(/\s*\|\|\s*/);
 
  for (const path of paths) {
    const parts = path.split(/\s+/);
    let tests = [] as [string, string[]][];
    let valid = true;
 
    for (let part of parts) {
      let index;
      let mode = '=';
 
      part = part.replace(/^[<>=~^]*/, (match) => {
        if (match) mode = match;
        return '';
      }).replace(/^V|\.[X*]/gi, '');
 
      if (part === 'latest' || /^[X~*^]*$/i.test(part)) {
        tests = [['>=', ['0', '0', '0', '0']]];
        break;
      }
 
      const split = part.replace(/\+.*$/, '').split(/[-.]/);
 
      if (mode === '^') {
        index = Math.min(split.lastIndexOf('0') + 1, split.length - 1, 2);
      } else if (mode === '~' || mode === '~>') {
        index = Math.min(split.length - 1, 1);
      } else {
        tests.push([mode, split]);
        continue;
      }
 
      const empty = new Array(split.length - index).fill(0);
      const match = split.slice(0, index + 1).concat(...empty);
      match[index] = (parseInt(match[index]) + 1).toString();
      tests.push(['>=', split], ['<', match]);
    }
 
    for (const [mode, taken] of tests) {
      const latest = input.some((i) => /[^\d]+/.test(i));
      const length = Math.min(input.length, taken.length);
      const source = input.slice(0, length).join('.');
      const target = taken.slice(0, length).join('.');
      const weight = source.localeCompare(target, undefined, {
        numeric: true,
        sensitivity: 'base'
      });
 
      valid &&= (!latest || length === input.length);
 
      switch (mode) {
        case '<': valid &&= weight < 0; break;
        case '<=': valid &&= weight <= 0; break;
        case '>': valid &&= weight > 0; break;
        case '>=': valid &&= weight >= 0; break;
        case '=': valid &&= weight === 0; break;
        default: valid = false; break;
      }
 
      if (!valid) {
        break;
      }
    }
 
    if (valid) {
      return true;
    }
  }
 
  return false;
}