diff --git a/lib/config/definitions.js b/lib/config/definitions.js index 096acc68dcfe191b9b430f857792ab8666c322a6..f443328778bbf27d438b55553c227585f0306491 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -450,6 +450,14 @@ const options = [ stage: 'package', type: 'boolean', }, + { + name: 'upgradeInRange', + description: + 'Upgrade ranges to latest version even if latest version satisfies existing range', + stage: 'package', + type: 'boolean', + default: false, + }, { name: 'versionStrategy', description: diff --git a/lib/workers/package-file/index.js b/lib/workers/package-file/index.js index c4da377906324760eca54dab7dcdf5a869589d72..7399a6bb6b921530bd8dcdd14958d06d4009d642 100644 --- a/lib/workers/package-file/index.js +++ b/lib/workers/package-file/index.js @@ -111,6 +111,7 @@ async function renovatePackageJson(input) { // Pin dependencies if we're pretty sure it's not a browser library if ( depTypeConfig.pinVersions === null && + !depTypeConfig.upgradeInRange && (depType === 'devDependencies' || (depType === 'dependencies' && !mightBeABrowserLibrary(config.content))) ) { diff --git a/lib/workers/package/versions.js b/lib/workers/package/versions.js index 83b5d32b4821bcdfb1df25935f04ea077fd0be5b..e1ad1f1e110569f6fd3ef519aa80c8272ad82031 100644 --- a/lib/workers/package/versions.js +++ b/lib/workers/package/versions.js @@ -18,12 +18,7 @@ function determineUpgrades(npmDep, config) { const result = { type: 'warning', }; - const { - currentVersion, - lockedVersion, - pinVersions, - allowedVersions, - } = config; + const { lockedVersion, pinVersions, allowedVersions } = config; const { versions } = npmDep; if (!versions || Object.keys(versions).length === 0) { result.message = `No versions returned from registry for this package`; @@ -32,6 +27,28 @@ function determineUpgrades(npmDep, config) { } const versionList = Object.keys(versions); const allUpgrades = {}; + let { currentVersion } = config; + let rangeOperator; + if (config.upgradeInRange && semver.validRange(currentVersion)) { + logger.debug({ currentVersion }, 'upgradeInRange is true'); + const parsedRange = semverUtils.parseRange(currentVersion); + if (parsedRange && parsedRange.length === 1) { + const [range] = parsedRange; + if (range.major && range.minor && range.patch) { + if (range.operator === '^' || range.operator === '~') { + logger.debug('Applying upgradeInRange'); + currentVersion = `${range.major}.${range.minor}.${range.patch}`; + rangeOperator = range.operator; + } else { + logger.debug({ currentVersion }, 'Unsupported range type'); + } + } else { + logger.debug({ currentVersion }, 'Range is not fully specified'); + } + } else { + logger.debug({ currentVersion }, 'Skipping complex range'); + } + } let changeLogFromVersion = currentVersion; // Check for a current range and pin it if (isRange(currentVersion)) { @@ -61,7 +78,7 @@ function determineUpgrades(npmDep, config) { newVersionMajor: semver.major(newVersion), }; changeLogFromVersion = newVersion; - } else if (versionList.indexOf(currentVersion) === -1) { + } else if (versionList.indexOf(currentVersion) === -1 && !rangeOperator) { logger.debug({ dependency }, 'Cannot find currentVersion'); try { const rollbackVersion = semver.maxSatisfying( @@ -163,6 +180,7 @@ function determineUpgrades(npmDep, config) { changeLogFromVersion, changeLogToVersion, }; + logger.debug({ allUpgrades }); if (type === 'major') { allUpgrades[upgradeKey].isMajor = true; } else if (type === 'minor') { @@ -182,7 +200,14 @@ function determineUpgrades(npmDep, config) { // Return now if array is empty, or we can keep pinned version upgrades if (upgrades.length === 0 || config.pinVersions || !isRange(currentVersion)) { - return upgrades; + logger.debug({ upgrades }); + return rangeOperator + ? upgrades.map(upgrade => ({ + ...upgrade, + newVersion: `${rangeOperator}${upgrade.newVersion}`, + isRange: true, + })) + : upgrades; } logger.debug({ dependency }, 'User wanrs ranges - filtering out pins'); diff --git a/test/workers/package/__snapshots__/versions.spec.js.snap b/test/workers/package/__snapshots__/versions.spec.js.snap index 1bd86b1f0a160f33a71853d0e50106d06084187a..97fba9d5d280d665ccdc4bb2bab98799e02bdd20 100644 --- a/test/workers/package/__snapshots__/versions.spec.js.snap +++ b/test/workers/package/__snapshots__/versions.spec.js.snap @@ -71,6 +71,27 @@ Array [ ] `; +exports[`workers/package/versions .determineUpgrades(npmDep, config) rejects complex range in-range updates 1`] = `Array []`; + +exports[`workers/package/versions .determineUpgrades(npmDep, config) rejects in-range unsupported operator 1`] = `Array []`; + +exports[`workers/package/versions .determineUpgrades(npmDep, config) rejects non-fully specified in-range updates 1`] = `Array []`; + +exports[`workers/package/versions .determineUpgrades(npmDep, config) rejects non-range in-range updates 1`] = ` +Array [ + Object { + "changeLogFromVersion": "1.0.0", + "changeLogToVersion": "1.4.1", + "isMinor": true, + "newVersion": "1.4.1", + "newVersionMajor": 1, + "newVersionMinor": 4, + "type": "minor", + "unpublishable": false, + }, +] +`; + exports[`workers/package/versions .determineUpgrades(npmDep, config) rejects reverse ordered less than greater than 1`] = ` Array [ Object { @@ -478,6 +499,22 @@ Array [ ] `; +exports[`workers/package/versions .determineUpgrades(npmDep, config) supports in-range updates 1`] = ` +Array [ + Object { + "changeLogFromVersion": "1.0.0", + "changeLogToVersion": "1.4.1", + "isMinor": true, + "isRange": true, + "newVersion": "~1.4.1", + "newVersionMajor": 1, + "newVersionMinor": 4, + "type": "minor", + "unpublishable": false, + }, +] +`; + exports[`workers/package/versions .determineUpgrades(npmDep, config) supports minor and major upgrades for ranged versions 1`] = ` Array [ Object { diff --git a/test/workers/package/versions.spec.js b/test/workers/package/versions.spec.js index c883196065ec6397e2c1e6b8450979da9eec694d..e5ed164a7c7c7a2142f3d6d9a31a6bcb16f3d0b8 100644 --- a/test/workers/package/versions.spec.js +++ b/test/workers/package/versions.spec.js @@ -367,6 +367,35 @@ describe('workers/package/versions', () => { const res = versions.determineUpgrades(nextJson, config); expect(res).toHaveLength(0); }); + it('supports in-range updates', () => { + config.upgradeInRange = true; + config.currentVersion = '~1.0.0'; + expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); + }); + it('rejects in-range unsupported operator', () => { + config.upgradeInRange = true; + config.pinVersions = false; + config.currentVersion = '>=1.0.0'; + expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); + }); + it('rejects non-fully specified in-range updates', () => { + config.upgradeInRange = true; + config.pinVersions = false; + config.currentVersion = '1.x'; + expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); + }); + it('rejects complex range in-range updates', () => { + config.upgradeInRange = true; + config.pinVersions = false; + config.currentVersion = '^0.9.0 || ^1.0.0'; + expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); + }); + it('rejects non-range in-range updates', () => { + config.upgradeInRange = true; + config.pinVersions = false; + config.currentVersion = '1.0.0'; + expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); + }); }); describe('.isRange(input)', () => { it('rejects simple semver', () => { diff --git a/website/docs/_posts/2017-10-05-configuration-options.md b/website/docs/_posts/2017-10-05-configuration-options.md index 74ba01f3cb47280f92cba01d801f674e936b75e2..966d164b1e9491505081bb46c596b3f5218e189a 100644 --- a/website/docs/_posts/2017-10-05-configuration-options.md +++ b/website/docs/_posts/2017-10-05-configuration-options.md @@ -1082,6 +1082,21 @@ When schedules are in use, it generally means "no updates". However there are ca This is default true, meaning that Renovate will perform certain "desirable" updates to _existing_ PRs even when outside of schedule. If you wish to disable all updates outside of scheduled hours then set this field to false. +## upgradeInRange + +Upgrade ranges to latest version even if latest version satisfies existing range. + +| name | value | +| ------- | ------- | +| type | boolean | +| default | false | + +By default, Renovate assumes that if you are using ranges then it's because you want them to be wide/open. As such, Renovate won't deliberately "narrow" the range by increasing the semver value inside. + +For example, if your `package.json` specifies a value for `left-pad` of `^1.0.0` and the latest version on npmjs is `1.2.0`, then Renovate won't change anything. If instead you'd prefer to be updated to `^1.2.0` in cases like this, then set `upgradeInRange` to `true` in your Renovate config. + +This feature supports simple caret (`^`) and tilde (`~`) ranges only, like `^1.0.0` and `~1.0.0`. It is not compatible with `pinVersions=true`. + ## versionStrategy Strategy for how to modify/update existing versions/semver. Possible values: auto, replace, or widen