From 6fef1d1650ec936321dc2819d409d604c7415fab Mon Sep 17 00:00:00 2001 From: Gabriel-Ladzaretti <97394622+Gabriel-Ladzaretti@users.noreply.github.com> Date: Fri, 14 Oct 2022 12:26:20 +0300 Subject: [PATCH] feat(manager/npm): add support for x-range "all" - `"*"` range (#18251) --- .../extract/__snapshots__/index.spec.ts.snap | 1 - lib/modules/manager/npm/extract/index.spec.ts | 2 +- lib/modules/manager/npm/extract/index.ts | 3 - lib/modules/versioning/npm/index.spec.ts | 28 ++++++- lib/modules/versioning/npm/range.ts | 4 + lib/modules/versioning/semver/common.spec.ts | 16 ++++ lib/modules/versioning/semver/common.ts | 10 +++ .../repository/process/lookup/index.spec.ts | 82 +++++++++++++++++++ 8 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 lib/modules/versioning/semver/common.spec.ts create mode 100644 lib/modules/versioning/semver/common.ts diff --git a/lib/modules/manager/npm/extract/__snapshots__/index.spec.ts.snap b/lib/modules/manager/npm/extract/__snapshots__/index.spec.ts.snap index f9808f6b9d..165138b1c3 100644 --- a/lib/modules/manager/npm/extract/__snapshots__/index.spec.ts.snap +++ b/lib/modules/manager/npm/extract/__snapshots__/index.spec.ts.snap @@ -59,7 +59,6 @@ exports[`modules/manager/npm/extract/index .extractPackageFile() extracts engine "depName": "foo", "depType": "devDependencies", "prettyDepType": "devDependency", - "skipReason": "any-version", }, { "currentValue": "file:../foo/bar", diff --git a/lib/modules/manager/npm/extract/index.spec.ts b/lib/modules/manager/npm/extract/index.spec.ts index 98ff5aac49..bf5095ee46 100644 --- a/lib/modules/manager/npm/extract/index.spec.ts +++ b/lib/modules/manager/npm/extract/index.spec.ts @@ -353,7 +353,7 @@ describe('modules/manager/npm/extract/index', () => { deps: [ { depName: 'angular', currentValue: '1.6.0' }, { depName: '@angular/cli', currentValue: '1.6.0' }, - { depName: 'foo', currentValue: '*', skipReason: 'any-version' }, + { depName: 'foo', currentValue: '*' }, { depName: 'bar', currentValue: 'file:../foo/bar', diff --git a/lib/modules/manager/npm/extract/index.ts b/lib/modules/manager/npm/extract/index.ts index ac2947c6ed..d395a4940d 100644 --- a/lib/modules/manager/npm/extract/index.ts +++ b/lib/modules/manager/npm/extract/index.ts @@ -271,9 +271,6 @@ export async function extractPackageFile( } if (isValid(dep.currentValue)) { dep.datasource = NpmDatasource.id; - if (dep.currentValue === '*') { - dep.skipReason = 'any-version'; - } if (dep.currentValue === '') { dep.skipReason = 'empty'; } diff --git a/lib/modules/versioning/npm/index.spec.ts b/lib/modules/versioning/npm/index.spec.ts index c56323ab65..7b70d5eee3 100644 --- a/lib/modules/versioning/npm/index.spec.ts +++ b/lib/modules/versioning/npm/index.spec.ts @@ -5,19 +5,44 @@ describe('modules/versioning/npm/index', () => { version | isValid ${'17.04.0'} | ${false} ${'1.2.3'} | ${true} + ${'*'} | ${true} + ${'x'} | ${true} + ${'X'} | ${true} + ${'1'} | ${true} ${'1.2.3-foo'} | ${true} ${'1.2.3foo'} | ${false} ${'~1.2.3'} | ${true} + ${'1.2'} | ${true} + ${'1.2.x'} | ${true} + ${'1.2.X'} | ${true} + ${'1.2.*'} | ${true} + ${'~1.2.3'} | ${true} ${'^1.2.3'} | ${true} ${'>1.2.3'} | ${true} ${'renovatebot/renovate'} | ${false} ${'renovatebot/renovate#main'} | ${false} ${'https://github.com/renovatebot/renovate.git'} | ${false} `('isValid("$version") === $isValid', ({ version, isValid }) => { - const res = !!semver.isValid(version); + const res = semver.isValid(version); expect(res).toBe(isValid); }); + test.each` + versions | range | maxSatisfying + ${['2.3.3.', '2.3.4', '2.4.5', '2.5.1', '3.0.0']} | ${'*'} | ${'3.0.0'} + ${['2.3.3.', '2.3.4', '2.4.5', '2.5.1', '3.0.0']} | ${'x'} | ${'3.0.0'} + ${['2.3.3.', '2.3.4', '2.4.5', '2.5.1', '3.0.0']} | ${'X'} | ${'3.0.0'} + ${['2.3.3.', '2.3.4', '2.4.5', '2.5.1', '3.0.0']} | ${'2'} | ${'2.5.1'} + ${['2.3.3.', '2.3.4', '2.4.5', '2.5.1', '3.0.0']} | ${'2.*'} | ${'2.5.1'} + ${['2.3.3.', '2.3.4', '2.4.5', '2.5.1', '3.0.0']} | ${'2.3'} | ${'2.3.4'} + ${['2.3.3.', '2.3.4', '2.4.5', '2.5.1', '3.0.0']} | ${'2.3.*'} | ${'2.3.4'} + `( + 'getSatisfyingVersion("$versions","$range") === $maxSatisfying', + ({ versions, range, maxSatisfying }) => { + expect(semver.getSatisfyingVersion(versions, range)).toBe(maxSatisfying); + } + ); + test.each` version | isSingle ${'1.2.3'} | ${true} @@ -59,6 +84,7 @@ describe('modules/versioning/npm/index', () => { ${'>= 0.0.1 < 0.0.4'} | ${'bump'} | ${'0.0.4'} | ${'0.0.5'} | ${'>= 0.0.5 < 0.0.6'} ${'>= 0.0.1 < 1'} | ${'bump'} | ${'1.0.0'} | ${'1.0.1'} | ${'>= 1.0.1 < 2'} ${'>= 0.0.1 < 1'} | ${'bump'} | ${'1.0.0'} | ${'1.0.1'} | ${'>= 1.0.1 < 2'} + ${'*'} | ${'bump'} | ${'1.0.0'} | ${'1.0.1'} | ${null} ${'<=1.2.3'} | ${'widen'} | ${'1.0.0'} | ${'1.2.3'} | ${'<=1.2.3'} ${'<=1.2.3'} | ${'widen'} | ${'1.0.0'} | ${'1.2.4'} | ${'<=1.2.4'} ${'>=1.2.3'} | ${'widen'} | ${'1.0.0'} | ${'1.2.3'} | ${'>=1.2.3'} diff --git a/lib/modules/versioning/npm/range.ts b/lib/modules/versioning/npm/range.ts index 9e2458c8e4..8ddfad58d3 100644 --- a/lib/modules/versioning/npm/range.ts +++ b/lib/modules/versioning/npm/range.ts @@ -3,6 +3,7 @@ import semver from 'semver'; import semverUtils from 'semver-utils'; import { logger } from '../../../logger'; import { regEx } from '../../../util/regex'; +import { isSemVerXRange } from '../semver/common'; import type { NewValueConfig } from '../types'; const { @@ -63,6 +64,9 @@ export function getNewValue({ currentVersion, newVersion, }: NewValueConfig): string | null { + if (rangeStrategy === 'bump' && isSemVerXRange(currentValue)) { + return null; + } if (rangeStrategy === 'pin' || isVersion(currentValue)) { return newVersion; } diff --git a/lib/modules/versioning/semver/common.spec.ts b/lib/modules/versioning/semver/common.spec.ts new file mode 100644 index 0000000000..372ede4bdd --- /dev/null +++ b/lib/modules/versioning/semver/common.spec.ts @@ -0,0 +1,16 @@ +import { isSemVerXRange } from './common'; + +describe('modules/versioning/semver/common', () => { + test.each` + range | expected + ${'*'} | ${true} + ${'x'} | ${true} + ${'X'} | ${true} + ${''} | ${true} + ${'1'} | ${false} + ${'1.2'} | ${false} + ${'1.2.3'} | ${false} + `('isSemVerXRange("range") === $expected', ({ range, expected }) => { + expect(isSemVerXRange(range)).toBe(expected); + }); +}); diff --git a/lib/modules/versioning/semver/common.ts b/lib/modules/versioning/semver/common.ts new file mode 100644 index 0000000000..58f1230a01 --- /dev/null +++ b/lib/modules/versioning/semver/common.ts @@ -0,0 +1,10 @@ +const SEMVER_X_RANGE = ['*', 'x', 'X', ''] as const; +type SemVerXRangeArray = typeof SEMVER_X_RANGE; +export type SemVerXRange = SemVerXRangeArray[number]; + +/** + * https://docs.npmjs.com/cli/v6/using-npm/semver#x-ranges-12x-1x-12- + */ +export function isSemVerXRange(range: string): range is SemVerXRange { + return SEMVER_X_RANGE.includes(range as SemVerXRange); +} diff --git a/lib/workers/repository/process/lookup/index.spec.ts b/lib/workers/repository/process/lookup/index.spec.ts index f89a35406d..5a0dd27cab 100644 --- a/lib/workers/repository/process/lookup/index.spec.ts +++ b/lib/workers/repository/process/lookup/index.spec.ts @@ -334,6 +334,88 @@ describe('workers/repository/process/lookup/index', () => { ]); }); + it.each` + strategy | updates + ${'update-lockfile'} | ${[{ isLockfileUpdate: true, newValue: '*', newVersion: '0.9.7', updateType: 'minor' }, { isLockfileUpdate: true, newValue: '*', newVersion: '1.4.1', updateType: 'major' }]} + ${'pin'} | ${[{ newValue: '0.4.0', updateType: 'pin' }, { newValue: '0.9.7', updateType: 'minor' }, { newValue: '1.4.1', updateType: 'major' }]} + `( + 'supports for x-range-all for replaceStrategy = $strategy (with lockfile)', + async ({ strategy, updates }) => { + config.currentValue = '*'; + config.rangeStrategy = strategy; + config.lockedVersion = '0.4.0'; + config.depName = 'q'; + config.datasource = NpmDatasource.id; + httpMock + .scope('https://registry.npmjs.org') + .get('/q') + .reply(200, qJson); + expect(await lookup.lookupUpdates(config)).toMatchObject({ updates }); + } + ); + + it.each` + strategy + ${'widen'} + ${'bump'} + ${'replace'} + `( + 'doesnt offer updates for x-range-all (with lockfile) when replaceStrategy = $strategy', + async ({ strategy }) => { + config.currentValue = 'x'; + config.rangeStrategy = strategy; + config.lockedVersion = '0.4.0'; + config.depName = 'q'; + config.datasource = NpmDatasource.id; + httpMock + .scope('https://registry.npmjs.org') + .get('/q') + .reply(200, qJson); + expect((await lookup.lookupUpdates(config)).updates).toEqual([]); + } + ); + + it('supports pinning for x-range-all (no lockfile)', async () => { + config.currentValue = '*'; + config.rangeStrategy = 'pin'; + config.depName = 'q'; + config.datasource = NpmDatasource.id; + httpMock.scope('https://registry.npmjs.org').get('/q').reply(200, qJson); + expect(await lookup.lookupUpdates(config)).toMatchObject({ + updates: [{ newValue: '1.4.1', updateType: 'pin' }], + }); + }); + + it('covers pinning an unsupported x-range-all value', async () => { + config.currentValue = ''; + config.rangeStrategy = 'pin'; + config.depName = 'q'; + config.datasource = NpmDatasource.id; + httpMock.scope('https://registry.npmjs.org').get('/q').reply(200, qJson); + expect((await lookup.lookupUpdates(config)).updates).toEqual([]); + }); + + it.each` + strategy + ${'widen'} + ${'bump'} + ${'update-lockfile'} + ${'replace'} + `( + 'doesnt offer updates for x-range-all (no lockfile) when replaceStrategy = $strategy', + async ({ strategy }) => { + config.currentValue = 'X'; + config.rangeStrategy = strategy; + config.depName = 'q'; + config.datasource = NpmDatasource.id; + httpMock + .scope('https://registry.npmjs.org') + .get('/q') + .reply(200, qJson); + expect((await lookup.lookupUpdates(config)).updates).toEqual([]); + } + ); + it('ignores pinning for ranges when other upgrade exists', async () => { config.currentValue = '~0.9.0'; config.rangeStrategy = 'pin';