Skip to content

Commit

Permalink
refactor(compare): refactor getHighestVersion
Browse files Browse the repository at this point in the history
  • Loading branch information
JamieMason committed Feb 19, 2023
1 parent 67deec7 commit 5c41df8
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 101 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { R } from '@mobily/ts-belt';
import 'expect-more-jest';
import { getHighestVersion } from './get-highest-version';

Expand All @@ -10,76 +11,30 @@ const shuffle = (array: string[]): string[] => {
};

describe('getHighestVersion', () => {
it('ignores non-semver versions', () => {
const a: string[] = [];
const b = shuffle([...a, 'http://asdf.com/asdf.tar.gz']);
const c = shuffle([...b, 'file:../foo/bar']);
const d = shuffle([...c, 'latest']);
const e = shuffle([...d, 'git+https://isaacs@github.com/npm/cli.git']);
const f = shuffle([...e, 'expressjs/express']);
const g = shuffle([...f, 'mochajs/mocha#4727d357ea']);
const h = shuffle([...g, 'user/repo#feature/branch']);
const i = shuffle([...h, 'link:../foo/bar']);
const j = shuffle([...i, 'git+ssh://git@github.com:npm/cli.git#v1.0.27']);
const k = shuffle([...j, 'git+ssh://git@github.com:npm/cli#semver:^5.0']);
const l = shuffle([...k, 'git://github.com/npm/cli.git#v1.0.27']);
expect(getHighestVersion(a)).toBeEmptyString();
expect(getHighestVersion(b)).toBeEmptyString();
expect(getHighestVersion(c)).toBeEmptyString();
expect(getHighestVersion(d)).toBeEmptyString();
expect(getHighestVersion(e)).toBeEmptyString();
expect(getHighestVersion(f)).toBeEmptyString();
expect(getHighestVersion(g)).toBeEmptyString();
expect(getHighestVersion(h)).toBeEmptyString();
expect(getHighestVersion(i)).toBeEmptyString();
expect(getHighestVersion(i)).toBeEmptyString();
expect(getHighestVersion(j)).toBeEmptyString();
expect(getHighestVersion(k)).toBeEmptyString();
expect(getHighestVersion(l)).toBeEmptyString();
});

it('returns the newest version from an array of versions', () => {
const a = ['<1.0.0'];
const b = shuffle([...a, '<=1.0.0']);
const c = shuffle([...b, '1.0.0']);
const d = shuffle([...c, '~1.0.0']);
const e = shuffle([...d, '1.x.x']);
const f = shuffle([...e, '^1.0.0']);
const g = shuffle([...f, '>=1.0.0']);
const h = shuffle([...g, '>1.0.0']);
const i = shuffle([...h, '*']);
const j = shuffle([...i, 'http://asdf.com/asdf.tar.gz']);
const k = shuffle([...j, 'file:../foo/bar']);
const l = shuffle([...k, 'latest']);
const m = shuffle([...l, 'git+ssh://git@github.com:npm/cli.git#v1.0.27']);
const n = shuffle([...m, 'git+ssh://git@github.com:npm/cli#semver:^5.0']);
const o = shuffle([...n, 'git+https://isaacs@github.com/npm/cli.git']);
const p = shuffle([...o, 'git://github.com/npm/cli.git#v1.0.27']);
const q = shuffle([...p, 'expressjs/express']);
const r = shuffle([...q, 'mochajs/mocha#4727d357ea']);
const s = shuffle([...r, 'user/repo#feature/branch']);
const t = shuffle([...s, 'link:../foo/bar']);
const c = shuffle([...b, '1']);
const d = shuffle([...c, '1.0.0']);
const e = shuffle([...d, '~1.0.0']);
const f = shuffle([...e, '1.x.x']);
const g = shuffle([...f, '^1.0.0']);
const h = shuffle([...g, '>=1.0.0']);
const i = shuffle([...h, '>1.0.0']);
const j = shuffle([...i, '*']);
// valid semver
expect(getHighestVersion(a)).toEqual('<1.0.0');
expect(getHighestVersion(b)).toEqual('<=1.0.0');
expect(getHighestVersion(c)).toEqual('1.0.0');
expect(getHighestVersion(d)).toEqual('~1.0.0');
expect(getHighestVersion(e)).toEqual('1.x.x');
expect(getHighestVersion(f)).toEqual('^1.0.0');
expect(getHighestVersion(g)).toEqual('>=1.0.0');
expect(getHighestVersion(h)).toEqual('>1.0.0');
expect(getHighestVersion(i)).toEqual('*');
// invalid semver
expect(getHighestVersion(j)).toEqual('*');
expect(getHighestVersion(k)).toEqual('*');
expect(getHighestVersion(l)).toEqual('*');
expect(getHighestVersion(m)).toEqual('*');
expect(getHighestVersion(n)).toEqual('*');
expect(getHighestVersion(o)).toEqual('*');
expect(getHighestVersion(p)).toEqual('*');
expect(getHighestVersion(q)).toEqual('*');
expect(getHighestVersion(r)).toEqual('*');
expect(getHighestVersion(s)).toEqual('*');
expect(getHighestVersion(t)).toEqual('*');
expect(getHighestVersion(a)).toEqual(R.Ok('<1.0.0'));
expect(getHighestVersion(b)).toEqual(R.Ok('<=1.0.0'));
expect(getHighestVersion(c)).toEqual(R.Ok('1'));
expect(getHighestVersion(d)).toEqual(
// "1" and "1.0.0" are equal and first match wins
R.Ok(expect.stringMatching(/^(1|1\.0\.0)$/)),
);
expect(getHighestVersion(e)).toEqual(R.Ok('~1.0.0'));
expect(getHighestVersion(f)).toEqual(R.Ok('1.x.x'));
expect(getHighestVersion(g)).toEqual(R.Ok('^1.0.0'));
expect(getHighestVersion(h)).toEqual(R.Ok('>=1.0.0'));
expect(getHighestVersion(i)).toEqual(R.Ok('>1.0.0'));
expect(getHighestVersion(j)).toEqual(R.Ok('*'));
});
});
Original file line number Diff line number Diff line change
@@ -1,39 +1,48 @@
import coerce from 'semver/functions/coerce';
import eq from 'semver/functions/eq';
import gt from 'semver/functions/gt';
import valid from 'semver/functions/valid';
import { RANGE } from '../../../../constants';
import { isSemver } from '../../../../lib/is-semver';
import { R } from '@mobily/ts-belt';
import { BaseError } from '../../../../lib/error';
import { clean } from './lib/clean';
import { compareSemver } from './lib/compare-semver';
import { getRangeScore } from './lib/get-range-score';

export function getHighestVersion(versions: string[]): string {
return versions.reduce<string>((rawHighest, raw) => {
const version = valid(coerce(raw)) || '';
const highest = valid(coerce(rawHighest)) || '';
if (raw === '*' || rawHighest === '*') return '*';
if (!isSemver(raw) || version === '') return rawHighest;
if (highest === '') return raw;
if (gt(version, highest)) return raw;
if (eq(version, highest) && getRangeScore(raw) > getRangeScore(rawHighest))
return raw;
return rawHighest;
}, '');
interface HighestVersion {
withRange: string;
semver: string;
}

function getRangeScore(version: string): number {
if (version === '') return 0;
if (version === RANGE.ANY) return 8;
const range = getRange(version);
if (range === RANGE.GT) return 7;
if (range === RANGE.GTE) return 6;
if (range === RANGE.MINOR) return 5;
if (version.indexOf('.x') !== -1) return 4;
if (range === RANGE.PATCH) return 3;
if (range === RANGE.EXACT) return 2;
if (range === RANGE.LTE) return 1;
if (range === RANGE.LT) return 0;
return 0;
export function getHighestVersion(
versions: string[],
): R.Result<string, BaseError> {
let highest: HighestVersion | undefined;

for (const withRange of versions) {
switch (compareSemver(withRange, highest?.semver)) {
// highest possible, quit early
case '*': {
return R.Ok(withRange);
}
// impossible to know how the user wants to resolve unsupported versions
case 'invalid': {
return R.Error(new BaseError(`"${withRange}" is not supported`));
}
// we found a new highest version
case 'gt': {
highest = newHighestVersion(withRange);
continue;
}
// versions are the same, but one range might be greedier than another
case 'eq': {
const score = getRangeScore(withRange);
const highestScore = getRangeScore(`${highest?.withRange}`);
if (score > highestScore) highest = newHighestVersion(withRange);
}
}
}

return highest && highest.withRange
? R.Ok(highest.withRange)
: R.Error(new BaseError(`getHighestVersion(): did not return a version`));
}

function getRange(version: string): string {
return version.slice(0, version.search(/[0-9]/));
function newHighestVersion(withRange: string): HighestVersion {
return { withRange, semver: clean(withRange) };
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { R } from '@mobily/ts-belt';
import type { VersionGroup } from '..';
import { BaseError } from '../../../../lib/error';
import { isSemver } from '../../../../lib/is-semver';
Expand Down Expand Up @@ -53,7 +54,7 @@ export class InstanceGroup {
)}`,
);
}
return this.getHighestVersion();
return R.getExn(this.getHighestVersion());
}

getHighestVersion() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import coerce from 'semver/functions/coerce';
import valid from 'semver/functions/valid';

/** Convert eg "1" to "1.0.0" which the semver lib does not understand */
export function clean(v: string): string {
return valid(coerce(v)) || '';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import gt from 'semver/functions/gt';
import { isSemver } from '../../../../../lib/is-semver';
import { clean } from './clean';

/** Is this next version to be inspected higher than the current highest? */
export function compareSemver(
next: string,
highest: string | undefined,
): '*' | 'invalid' | 'gt' | 'lt' | 'eq' {
if (next === '*') return '*';
if (!isSemver(next)) return 'invalid';
if (!highest || gt(clean(next), highest)) return 'gt';
if (gt(clean(next), highest)) return 'lt';
return 'eq';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { RANGE } from '../../../../../constants';

const scoresByRange: Record<string, number | undefined> = {
[RANGE.ANY]: 8,
[RANGE.GT]: 7,
[RANGE.GTE]: 6,
[RANGE.MINOR]: 5,
[RANGE.LOOSE]: 4,
[RANGE.PATCH]: 3,
[RANGE.EXACT]: 2,
[RANGE.LTE]: 1,
[RANGE.LT]: 0,
};

/** Rank a Semver Range according to its greediness */
export function getRangeScore(version: string): number {
const range =
version.indexOf('.x') !== -1
? RANGE.LOOSE
: version.slice(0, version.search(/[0-9]/));
return scoresByRange[range] || 0;
}

0 comments on commit 5c41df8

Please sign in to comment.