|
import { bumpMajor, bumpMinor, bumpPatch, compareSemVer, parseSemVer } from './semver'; |
|
|
|
type ComparatorOperator = '<' | '<=' | '>' | '>='; |
|
|
|
function compareWithOperator(operator: ComparatorOperator, left: number, right: number): boolean { |
|
if (operator === '<') return left < right; |
|
if (operator === '<=') return left <= right; |
|
if (operator === '>') return left > right; |
|
return left >= right; |
|
} |
|
|
|
function satisfiesComparator(version: string, comparator: string): boolean { |
|
const trimmed = comparator.trim(); |
|
if (!trimmed) return true; |
|
|
|
const match = /^(>=|<=|>|<)\s*(.+)$/.exec(trimmed); |
|
if (!match) { |
|
// If it doesn't look like a comparator, treat it as an exact version. |
|
return rangeSatisfies(trimmed, version); |
|
} |
|
|
|
const operator = match[1] as ComparatorOperator; |
|
const targetRaw = match[2].trim(); |
|
|
|
const parsedVersion = parseSemVer(version); |
|
const parsedTarget = parseSemVer(targetRaw); |
|
if (!parsedVersion || !parsedTarget) return false; |
|
|
|
const cmp = compareSemVer(parsedVersion, parsedTarget); |
|
return compareWithOperator(operator, cmp, 0); |
|
} |
|
|
|
function satisfiesTildeRange(range: string, version: string): boolean { |
|
const base = parseSemVer(range.slice(1)); |
|
const parsedVersion = parseSemVer(version); |
|
if (!base || !parsedVersion) return false; |
|
|
|
const lowerOk = compareSemVer(parsedVersion, base) >= 0; |
|
const upperBound = bumpMinor(base); |
|
const upperOk = compareSemVer(parsedVersion, upperBound) < 0; |
|
|
|
return lowerOk && upperOk; |
|
} |
|
|
|
function satisfiesCaretRange(range: string, version: string): boolean { |
|
const base = parseSemVer(range.slice(1)); |
|
const parsedVersion = parseSemVer(version); |
|
if (!base || !parsedVersion) return false; |
|
|
|
const lowerOk = compareSemVer(parsedVersion, base) >= 0; |
|
|
|
let upperBound; |
|
if (base.major > 0) { |
|
upperBound = bumpMajor(base); |
|
} else if (base.minor > 0) { |
|
upperBound = bumpMinor(base); |
|
} else { |
|
upperBound = bumpPatch(base); |
|
} |
|
|
|
const upperOk = compareSemVer(parsedVersion, upperBound) < 0; |
|
return lowerOk && upperOk; |
|
} |
|
|
|
/** |
|
* Returns true if the given lockfile-resolved version is compatible with the |
|
* version range specified in package.json. |
|
* |
|
* Supported range syntax (pragmatic subset): |
|
* - Exact: `1.2.3` |
|
* - Tilde: `~1.2.3` |
|
* - Caret: `^1.2.3` |
|
* - Comparator sets: `>=22 <23` (space-separated) |
|
* - OR sets: `>=1 <2 || >=3 <4` |
|
*/ |
|
export function rangeSatisfies(range: string, version: string): boolean { |
|
const trimmedRange = range.trim(); |
|
const trimmedVersion = version.trim(); |
|
|
|
if (!trimmedRange) return false; |
|
if (!trimmedVersion) return false; |
|
|
|
// Non-semver specs: fall back to string equality. |
|
if ( |
|
trimmedRange.startsWith('workspace:') || |
|
trimmedRange.startsWith('file:') || |
|
trimmedRange.startsWith('link:') |
|
) { |
|
return trimmedRange === trimmedVersion; |
|
} |
|
|
|
// Support OR sets. |
|
const orParts = trimmedRange.split('||').map((part) => part.trim()); |
|
if (orParts.length > 1) { |
|
return orParts.some((part) => rangeSatisfies(part, trimmedVersion)); |
|
} |
|
|
|
if (trimmedRange.startsWith('~')) return satisfiesTildeRange(trimmedRange, trimmedVersion); |
|
if (trimmedRange.startsWith('^')) return satisfiesCaretRange(trimmedRange, trimmedVersion); |
|
|
|
// Comparator sets. |
|
if (/^(>=|<=|>|<)/.test(trimmedRange)) { |
|
const comparators = trimmedRange.split(/\s+/).filter(Boolean); |
|
return comparators.every((comparator) => satisfiesComparator(trimmedVersion, comparator)); |
|
} |
|
|
|
// Exact version. |
|
const parsedVersion = parseSemVer(trimmedVersion); |
|
const parsedTarget = parseSemVer(trimmedRange); |
|
if (!parsedVersion || !parsedTarget) return false; |
|
return compareSemVer(parsedVersion, parsedTarget) === 0; |
|
} |