version-range
Version:
Check version ranges like `>=N` and `X || Y || Z` with support for Node.js, Web Browsers, Deno, and TypeScript.
211 lines (201 loc) • 6.15 kB
text/typescript
export type Version = string | number
export type Range = Version | Version[]
const orRegex = /\s*\|\|\s*/g
const andRegex = /\s*&&\s*/g
const rangeRegex = /^\s*([<>=~^]*)\s*([\d.x]+)(-[^\s]+)?\s*/
const xRegex = /[.]x/g
/**
* Check if the version is within the range
* @param subject The version to check against the range
* @param range The range to check the version against
*/
export default function withinVersionRange(
subject: Version,
range: Range
): boolean {
// prepare and verify subject
subject = String(subject)
const [subjectMajor = null, subjectMinor = null, subjectPatch = null] =
subject.split('.')
if (subjectMajor === null)
throw new Error(`subject was invalid: ${JSON.stringify(subject)}`)
const subjectMajorNumber = Number(subjectMajor || 0)
const subjectMinorNumber = Number(subjectMinor || 0)
const subjectPatchNumber = Number(subjectPatch || 0)
// cycle through the conditions
const orRanges = Array.isArray(range)
? range.slice()
: String(range).split(orRegex)
for (const orRange of orRanges) {
let orResult: boolean = false
const andRanges = String(orRange).split(andRegex)
for (const andRange of andRanges) {
let andResult: boolean = false
// process range
const [match, comparator, targetRaw, prerelease] =
String(andRange).match(rangeRegex) || []
const target = (targetRaw || '').replace(xRegex, '')
// // log
// console.log({
// orRange,
// andRange,
// match,
// comparator,
// target,
// prerelease,
// })
// prepare and verify target
const [targetMajor = null, targetMinor = null, targetPatch = null] =
target.split('.')
if (!match || !target || targetMajor == null || prerelease)
throw new Error(
`range condition was invalid: ${JSON.stringify(andRange)}`
)
const targetMajorNumber = Number(targetMajor || 0)
const targetMinorNumber = Number(targetMinor || 0)
const targetPatchNumber = Number(targetPatch || 0)
// is there more range matches? add it to and condition
const remainder = String(andRange).slice(match.length)
if (remainder) andRanges.push(remainder)
// handle comparator
switch (comparator) {
case '^':
if (subjectMajorNumber === targetMajorNumber) {
if (subjectMinorNumber === targetMinorNumber) {
if (subjectPatchNumber >= targetPatchNumber) {
andResult = true
}
} else if (subjectMinorNumber >= targetMinorNumber) {
andResult = true
}
}
break
case '~':
if (subjectMajorNumber === targetMajorNumber) {
if (
subjectMinor !== null &&
subjectMinorNumber === targetMinorNumber
) {
if (subjectPatchNumber >= targetPatchNumber) {
andResult = true
}
}
}
break
case '>=':
if (subjectMajorNumber === targetMajorNumber) {
if (subjectMinorNumber === targetMinorNumber) {
if (subjectPatchNumber >= targetPatchNumber) {
andResult = true
}
} else if (subjectMinorNumber >= targetMinorNumber) {
andResult = true
}
} else if (subjectMajorNumber >= targetMajorNumber) {
andResult = true
}
break
case '>':
if (subjectMajorNumber === targetMajorNumber) {
if (targetMinor === null) {
// x > x = false
// x.y > x = false
} else if (subjectMinorNumber === targetMinorNumber) {
if (targetPatch === null) {
// x.y > x.y = false
// x.y.z > x.y = false
} else if (subjectPatchNumber > targetPatchNumber) {
andResult = true
}
} else if (subjectMinorNumber > targetMinorNumber) {
andResult = true
}
} else if (subjectMajorNumber > targetMajorNumber) {
andResult = true
}
break
case '<':
if (subjectMajorNumber === targetMajorNumber) {
if (subjectMinor === null) {
// x < x = false
// x < x.y = false
} else if (subjectMinorNumber === targetMinorNumber) {
if (subjectPatch === null) {
// x.y < x.y = false
// x.y < x.y.z = false
} else if (subjectPatchNumber < targetPatchNumber) {
andResult = true
}
} else if (subjectMinorNumber < targetMinorNumber) {
andResult = true
}
} else if (subjectMajorNumber < targetMajorNumber) {
andResult = true
}
break
case '<=':
if (subjectMajorNumber === targetMajorNumber) {
if (subjectMinor === null) {
if (targetMinor === null) {
// x <= x = true
andResult = true
}
// x <= x.y = false
} else if (targetMinor === null) {
// x.y <= x = true
andResult = true
} else if (subjectMinorNumber === targetMinorNumber) {
if (subjectPatch === null) {
if (targetPatch === null) {
// x.y <= x.y = true
andResult = true
}
// x.y <= x.y.z = false
} else if (targetPatch === null) {
// x.y.z <= x.y = true
andResult = true
} else if (subjectPatchNumber <= targetPatchNumber) {
// x.y.z <= x.y.z = true
andResult = true
}
} else if (subjectMinorNumber <= targetMinorNumber) {
andResult = true
}
} else if (subjectMajorNumber <= targetMajorNumber) {
andResult = true
}
break
case '=':
case '':
if (subjectMajor === targetMajor) {
if (targetMinor === null) {
andResult = true
} else if (subjectMinor === targetMinor) {
if (targetPatch === null || subjectPatch === targetPatch) {
andResult = true
}
}
}
break
default:
throw new Error(
`range comparator was invalid: ${JSON.stringify(andRange)}`
)
}
// if one of the and conditions failed, don't continue and checks, and note failure to the or condition
if (!andResult) {
orResult = false
break
} else {
// otherwise note success to the or condition
orResult = true
}
}
// if the entire and conditions passed, then we can break out of the or conditions
if (orResult) {
return true
}
}
// nothing passed
return false
}