forked from renovatebot/renovate
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(versioning/semver): add semver-coerced versioning (renovatebot#1…
…1995) Co-authored-by: Rhys Arkins <rhys@arkins.net>
- Loading branch information
1 parent
0e7c50b
commit 7cc72c7
Showing
4 changed files
with
348 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
import semverCoerced from '.'; | ||
|
||
describe('versioning/semver-coerced/index', () => { | ||
describe('.equals(a, b)', () => { | ||
it('should return true for strictly equal versions', () => { | ||
expect(semverCoerced.equals('1.0.0', '1.0.0')).toBeTrue(); | ||
}); | ||
|
||
it('should return true for non-strictly equal versions', () => { | ||
expect(semverCoerced.equals('v1.0', '1.0.0')).toBeTrue(); | ||
expect(semverCoerced.equals('v1.0', 'v1.x')).toBeTrue(); | ||
}); | ||
|
||
it('should return false for non-equal versions', () => { | ||
expect(semverCoerced.equals('2.0.1', '2.3.0')).toBeFalse(); | ||
}); | ||
}); | ||
|
||
describe('.getMajor(input)', () => { | ||
it('should return major version number for strict semver', () => { | ||
expect(semverCoerced.getMajor('1.0.2')).toEqual(1); | ||
}); | ||
|
||
it('should return major version number for non-strict semver', () => { | ||
expect(semverCoerced.getMajor('v3.1')).toEqual(3); | ||
}); | ||
}); | ||
|
||
describe('.getMinor(input)', () => { | ||
it('should return minor version number for strict semver', () => { | ||
expect(semverCoerced.getMinor('1.0.2')).toEqual(0); | ||
}); | ||
|
||
it('should return minor version number for non-strict semver', () => { | ||
expect(semverCoerced.getMinor('v3.1')).toEqual(1); | ||
}); | ||
}); | ||
|
||
describe('.getPatch(input)', () => { | ||
it('should return patch version number for strict semver', () => { | ||
expect(semverCoerced.getPatch('1.0.2')).toEqual(2); | ||
}); | ||
|
||
it('should return patch version number for non-strict semver', () => { | ||
expect(semverCoerced.getPatch('v3.1.2-foo')).toEqual(2); | ||
}); | ||
}); | ||
|
||
describe('.isCompatible(input)', () => { | ||
it('should return true for strict semver', () => { | ||
expect(semverCoerced.isCompatible('1.0.2')).toBeTruthy(); | ||
}); | ||
|
||
it('should return true for non-strict semver', () => { | ||
expect(semverCoerced.isCompatible('v3.1.2-foo')).toBeTruthy(); | ||
}); | ||
|
||
it('should return false for non-semver', () => { | ||
expect(semverCoerced.isCompatible('foo')).toBeFalsy(); | ||
}); | ||
}); | ||
|
||
describe('.isGreaterThan(a, b)', () => { | ||
it('should return true for a greater version in strict semver', () => { | ||
expect(semverCoerced.isGreaterThan('1.0.2', '1.0.0')).toBeTruthy(); | ||
}); | ||
|
||
it('should return false for lower version in strict semver', () => { | ||
expect(semverCoerced.isGreaterThan('3.1.2', '4.1.0')).toBeFalsy(); | ||
}); | ||
}); | ||
|
||
describe('.isLessThanRange(version, range)', () => { | ||
it('should return true for a lower version in strict semver', () => { | ||
expect(semverCoerced.isLessThanRange('1.0.2', '~2.0')).toBeTruthy(); | ||
}); | ||
|
||
it('should return false for in-range version in strict semver', () => { | ||
expect(semverCoerced.isLessThanRange('3.0.2', '~3.0')).toBeFalsy(); | ||
}); | ||
}); | ||
|
||
describe('.isSingleVersion()', () => { | ||
it('returns true if naked version', () => { | ||
expect(semverCoerced.isSingleVersion('1.2.3')).toBeTruthy(); | ||
expect(semverCoerced.isSingleVersion('1.2.3-alpha.1')).toBeTruthy(); | ||
}); | ||
|
||
it('returns false if equals', () => { | ||
expect(semverCoerced.isSingleVersion('=1.2.3')).toBeFalsy(); | ||
expect(semverCoerced.isSingleVersion('= 1.2.3')).toBeFalsy(); | ||
}); | ||
|
||
it('returns false when not version', () => { | ||
expect(semverCoerced.isSingleVersion('~1.0')).toBeFalsy(); | ||
}); | ||
}); | ||
|
||
describe('.isStable(input)', () => { | ||
it('should return true for a stable version', () => { | ||
expect(semverCoerced.isStable('1.0.0')).toBeTruthy(); | ||
}); | ||
|
||
it('should return false for an prerelease version', () => { | ||
expect(semverCoerced.isStable('v1.0-alpha')).toBeFalsy(); | ||
}); | ||
}); | ||
|
||
describe('.isValid(input)', () => { | ||
it('should return null for non-digit version strings', () => { | ||
expect(semverCoerced.isValid('version two')).toBeFalsy(); | ||
}); | ||
|
||
it('should return null for irregular version strings', () => { | ||
expect(semverCoerced.isValid('17.04.0')).toBeFalsy(); | ||
}); | ||
|
||
it('should support strict semver', () => { | ||
expect(semverCoerced.isValid('1.2.3')).toBeTruthy(); | ||
}); | ||
|
||
it('should treat semver with dash as a valid version', () => { | ||
expect(semverCoerced.isValid('1.2.3-foo')).toBeTruthy(); | ||
}); | ||
|
||
it('should treat semver without dash as a valid version', () => { | ||
expect(semverCoerced.isValid('1.2.3foo')).toBeTruthy(); | ||
}); | ||
|
||
it('should treat ranges as valid versions', () => { | ||
expect(semverCoerced.isValid('~1.2.3')).toBeTruthy(); | ||
expect(semverCoerced.isValid('^1.2.3')).toBeTruthy(); | ||
expect(semverCoerced.isValid('>1.2.3')).toBeTruthy(); | ||
}); | ||
|
||
it('should reject github repositories', () => { | ||
expect(semverCoerced.isValid('renovatebot/renovate')).toBeFalsy(); | ||
expect(semverCoerced.isValid('renovatebot/renovate#master')).toBeFalsy(); | ||
expect( | ||
semverCoerced.isValid('https://github.com/renovatebot/renovate.git') | ||
).toBeFalsy(); | ||
}); | ||
}); | ||
|
||
describe('.isVersion(input)', () => { | ||
it('should return null for non-digit versions', () => { | ||
expect(semverCoerced.isValid('version one')).toBeFalsy(); | ||
}); | ||
|
||
it('should support strict semver versions', () => { | ||
expect(semverCoerced.isValid('1.2.3')).toBeTruthy(); | ||
}); | ||
|
||
it('should support non-strict versions', () => { | ||
expect(semverCoerced.isValid('v1.2')).toBeTruthy(); | ||
}); | ||
}); | ||
|
||
describe('.matches(version, range)', () => { | ||
it('should return true when version is in range', () => { | ||
expect(semverCoerced.matches('1.0.0', '1.0.0 || 1.0.1')).toBeTruthy(); | ||
}); | ||
|
||
it('should return true with non-strict version in range', () => { | ||
expect(semverCoerced.matches('v1.0', '1.0.0 || 1.0.1')).toBeTruthy(); | ||
}); | ||
|
||
it('should return false when version is not in range', () => { | ||
expect(semverCoerced.matches('1.2.3', '1.4.1 || 1.4.2')).toBeFalsy(); | ||
}); | ||
}); | ||
|
||
describe('.getSatisfyingVersion(versions, range)', () => { | ||
it('should return max satisfying version in range', () => { | ||
expect( | ||
semverCoerced.getSatisfyingVersion(['1.0.0', '1.0.4'], '^1.0') | ||
).toEqual('1.0.4'); | ||
}); | ||
|
||
it('should support coercion', () => { | ||
expect( | ||
semverCoerced.getSatisfyingVersion(['v1.0', '1.0.4-foo'], '^1.0') | ||
).toEqual('1.0.4'); | ||
}); | ||
}); | ||
|
||
describe('.minSatisfyingVersion(versions, range)', () => { | ||
it('should return min satisfying version in range', () => { | ||
expect( | ||
semverCoerced.minSatisfyingVersion(['1.0.0', '1.0.4'], '^1.0') | ||
).toEqual('1.0.0'); | ||
}); | ||
|
||
it('should support coercion', () => { | ||
expect( | ||
semverCoerced.minSatisfyingVersion(['v1.0', '1.0.4-foo'], '^1.0') | ||
).toEqual('1.0.0'); | ||
}); | ||
}); | ||
|
||
describe('getNewValue()', () => { | ||
it('uses newVersion', () => { | ||
expect( | ||
semverCoerced.getNewValue({ | ||
currentValue: '=1.0.0', | ||
rangeStrategy: 'bump', | ||
currentVersion: '1.0.0', | ||
newVersion: '1.1.0', | ||
}) | ||
).toEqual('1.1.0'); | ||
}); | ||
}); | ||
|
||
describe('.sortVersions(a, b)', () => { | ||
it('should return zero for equal versions', () => { | ||
expect(semverCoerced.sortVersions('1.0.0', '1.0.0')).toEqual(0); | ||
}); | ||
|
||
it('should return -1 for a < b', () => { | ||
expect(semverCoerced.sortVersions('1.0.0', '1.0.1')).toEqual(-1); | ||
}); | ||
|
||
it('should return 1 for a > b', () => { | ||
expect(semverCoerced.sortVersions('1.0.1', '1.0.0')).toEqual(1); | ||
}); | ||
|
||
it('should return zero for equal non-strict versions', () => { | ||
expect(semverCoerced.sortVersions('v1.0', '1.x')).toEqual(0); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import semver, { SemVer } from 'semver'; | ||
import stable from 'semver-stable'; | ||
import { regEx } from '../../util/regex'; | ||
import type { NewValueConfig, VersioningApi } from '../types'; | ||
|
||
export const id = 'semver-coerced'; | ||
export const displayName = 'Coerced Semantic Versioning'; | ||
export const urls = ['https://semver.org/']; | ||
export const supportsRanges = false; | ||
|
||
const { is: isStable } = stable; | ||
|
||
function sortVersions(a: string, b: string): number { | ||
return semver.compare(semver.coerce(a), semver.coerce(b)); | ||
} | ||
|
||
function getMajor(a: string | SemVer): number | null { | ||
return semver.major(semver.coerce(a)); | ||
} | ||
|
||
function getMinor(a: string | SemVer): number | null { | ||
return semver.minor(semver.coerce(a)); | ||
} | ||
|
||
function getPatch(a: string | SemVer): number | null { | ||
return semver.patch(a); | ||
} | ||
|
||
function matches(version: string, range: string): boolean { | ||
return semver.satisfies(semver.coerce(version), range); | ||
} | ||
|
||
function equals(a: string, b: string): boolean { | ||
return semver.eq(semver.coerce(a), semver.coerce(b)); | ||
} | ||
|
||
function isValid(version: string): string | boolean | null { | ||
return semver.valid(semver.coerce(version)); | ||
} | ||
|
||
function getSatisfyingVersion( | ||
versions: string[], | ||
range: string | ||
): string | null { | ||
const coercedVersions = versions.map((version) => { | ||
const coercedVersion = semver.coerce(version); | ||
return coercedVersion ? coercedVersion.version : null; | ||
}); | ||
return semver.maxSatisfying(coercedVersions, range); | ||
} | ||
|
||
function minSatisfyingVersion( | ||
versions: string[], | ||
range: string | ||
): string | null { | ||
const coercedVersions = versions.map((version) => { | ||
const coercedVersion = semver.coerce(version); | ||
return coercedVersion ? coercedVersion.version : null; | ||
}); | ||
return semver.minSatisfying(coercedVersions, range); | ||
} | ||
|
||
function isLessThanRange(version: string, range: string): boolean { | ||
return semver.ltr(semver.coerce(version), range); | ||
} | ||
|
||
function isGreaterThan(version: string, other: string): boolean { | ||
return semver.gt(semver.coerce(version), semver.coerce(other)); | ||
} | ||
|
||
const startsWithNumberRegex = regEx(`^\\d`); | ||
|
||
function isSingleVersion(version: string): string | boolean | null { | ||
// Since coercion accepts ranges as well as versions, we have to manually | ||
// check that the version string starts with either 'v' or a digit. | ||
if (!version.startsWith('v') && !startsWithNumberRegex.exec(version)) { | ||
return null; | ||
} | ||
|
||
return semver.valid(semver.coerce(version)); | ||
} | ||
|
||
// If this is left as an alias, inputs like "17.04.0" throw errors | ||
export const isVersion = (input: string): string | boolean => isValid(input); | ||
|
||
export { isVersion as isValid, getSatisfyingVersion }; | ||
|
||
function getNewValue({ newVersion }: NewValueConfig): string { | ||
return newVersion; | ||
} | ||
|
||
export const api: VersioningApi = { | ||
equals, | ||
getMajor, | ||
getMinor, | ||
getPatch, | ||
isCompatible: isVersion, | ||
isGreaterThan, | ||
isLessThanRange, | ||
isSingleVersion, | ||
isStable, | ||
isValid, | ||
isVersion, | ||
matches, | ||
getSatisfyingVersion, | ||
minSatisfyingVersion, | ||
getNewValue, | ||
sortVersions, | ||
}; | ||
export default api; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
Renovate's Coerced Semantic Versioning is a forgiving variant of [Semantic Versioning 2.0](https://semver.org) with coercion enabled for versions. | ||
|
||
This versioning provides a very forgiving translation of inputs in non-strict-SemVer format into strict SemVer. For example, "v1" is coerced into "1.0.0", "2.1" => "2.1.0", "~3.1" => "3.1.0", "1.1-foo" => "1.1.0". Look at the Coercion section of [this page](https://www.npmjs.com/package/semver) for more info on input coercion. | ||
|
||
Since this versioning is very forgiving, it doesn't actually provide the coercion for version ranges. The range functions only accept strict SemVer as input and equivalent to those provided by the Renovate's semver versioning. |