import { isString } from '@sindresorhus/is'; import semver from 'semver'; import semverUtils from 'semver-utils'; import { logger } from '../../../logger/index.ts'; import { regEx } from '../../../util/regex.ts'; import { isSemVerXRange, normalizeLegacyXRanges } from '../semver/common.ts'; import type { NewValueConfig } from '../types.ts'; const { inc: increment, valid: isVersion, major, minor, patch, prerelease, satisfies, } = semver; function replaceCaretValue(oldValue: string, newValue: string): string { const toVersionMajor = major(newValue); const toVersionMinor = minor(newValue); const toVersionPatch = patch(newValue); const currentMajor = major(oldValue); const currentMinor = minor(oldValue); const currentPatch = patch(oldValue); const oldTuple = [currentMajor, currentMinor, currentPatch]; const newTuple = [toVersionMajor, toVersionMinor, toVersionPatch]; const resultTuple = []; let leadingZero = true; let needReplace = false; for (let idx = 0; idx < 3; idx += 1) { const oldVal = oldTuple[idx]; const newVal = newTuple[idx]; let leadingDigit = false; if ((oldVal !== 0 || newVal !== 0) && leadingZero) { leadingZero = false; leadingDigit = true; } if (leadingDigit && newVal > oldVal) { needReplace = true; } if (!needReplace && newVal < oldVal) { return newValue; } resultTuple.push(leadingDigit ? newVal : 0); } return needReplace ? resultTuple.join('.') : oldValue; } function stripV(value: string): string { return value.replace(regEx(/^v/), ''); } function satisfiesRange(version: string, range: string): boolean { return satisfies(version, normalizeLegacyXRanges(range)); } // TODO: #22198 export function getNewValue({ currentValue, rangeStrategy, currentVersion, newVersion, }: NewValueConfig): string | null { if (rangeStrategy !== 'update-lockfile' && isSemVerXRange(currentValue)) { return null; } if (isVersion(currentValue)) { return newVersion; } if (rangeStrategy === 'update-lockfile') { if (satisfiesRange(newVersion, currentValue)) { return currentValue; } return getNewValue({ currentValue, rangeStrategy: 'replace', currentVersion, newVersion, }); } const parsedRange = semverUtils.parseRange(currentValue); const element = parsedRange.at(-1)!; if (rangeStrategy === 'widen') { if (satisfiesRange(newVersion, currentValue)) { return currentValue; } const newValue = getNewValue({ currentValue, rangeStrategy: 'replace', currentVersion, newVersion, }); if (element.operator?.startsWith('<')) { // TODO fix this const splitCurrent = currentValue.split(element.operator); splitCurrent.pop(); // TODO: types (#22198) return `${splitCurrent.join(element.operator)}${newValue!}`; } if (parsedRange.length > 1) { const previousElement = parsedRange.at(-2)!; if (previousElement.operator === '-') { const splitCurrent = currentValue.split('-'); splitCurrent.pop(); // TODO: types (#22198) return `${splitCurrent.join('-')}- ${newValue!}`; } if (element.operator?.startsWith('>')) { logger.warn(`Complex ranges ending in greater than are not supported`); return null; } } // TODO: types (#22198) return `${currentValue} || ${newValue!}`; } const toVersionMajor = major(newVersion); const toVersionMinor = minor(newVersion); const toVersionPatch = patch(newVersion); const toNewVersion = prerelease(newVersion); const suffix = toNewVersion ? `-${toNewVersion[0]}` : ''; // Simple range if (rangeStrategy === 'bump') { if (parsedRange.length === 1) { if (!element.operator) { return stripV(newVersion); } if (element.operator === '^') { return `^${stripV(newVersion)}`; } if (element.operator === '~') { return `~${stripV(newVersion)}`; } if (element.operator === '=') { return `=${stripV(newVersion)}`; } if (element.operator === '>=') { return currentValue.includes('>= ') ? `>= ${stripV(newVersion)}` : `>=${stripV(newVersion)}`; } if (element.operator.startsWith('<')) { return currentValue; } } else { return semverUtils .parseRange(currentValue) .map((x) => x.semver) .filter(isString) .map((subRange) => { const bumpedSubRange = getNewValue({ currentValue: subRange, rangeStrategy: 'bump', currentVersion, newVersion, }); if (bumpedSubRange && satisfiesRange(newVersion, bumpedSubRange)) { return bumpedSubRange; } return getNewValue({ currentValue: subRange, rangeStrategy: 'replace', currentVersion, newVersion, }); }) .filter((x) => x !== null && x !== '') .join(' '); } logger.debug( `Unsupported range type for rangeStrategy=bump: ${currentValue}`, ); return null; } if (element.operator === '~>') { return `~> ${toVersionMajor}.${toVersionMinor}.0`; } if (element.operator === '^') { if (suffix.length || !currentVersion) { return `^${toVersionMajor}.${toVersionMinor}.${toVersionPatch}${suffix}`; } return `^${replaceCaretValue(currentVersion, newVersion)}`; } if (element.operator === '=') { return `=${stripV(newVersion)}`; } if (element.operator === '~') { if (suffix.length) { return `~${toVersionMajor}.${toVersionMinor}.${toVersionPatch}${suffix}`; } return `~${toVersionMajor}.${toVersionMinor}.0`; } if (element.operator === '<=') { let res; if (!!element.patch || suffix.length) { res = `<=${stripV(newVersion)}`; } else if (element.minor) { res = `<=${toVersionMajor}.${toVersionMinor}`; } else { res = `<=${toVersionMajor}`; } if (currentValue.includes('<= ')) { res = res.replace('<=', '<= '); } return res; } if (element.operator === '<') { let res; if (currentValue.endsWith('.0.0')) { const newMajor = toVersionMajor + 1; res = `<${newMajor}.0.0`; } else if (currentValue.endsWith('.0')) { const newMinor = toVersionMinor + 1; res = `<${toVersionMajor}.${newMinor}${element.patch ? '.0' : ''}`; } else if (element.patch) { // TODO: types (#22198) res = `<${increment(newVersion, 'patch')!}`; } else if (element.minor) { res = `<${toVersionMajor}.${toVersionMinor + 1}`; } else { res = `<${toVersionMajor + 1}`; } if (currentValue.includes('< ')) { res = res.replace(regEx(/