From 0b9dedccada7aff7d5fdce47894c9aeebab7392e Mon Sep 17 00:00:00 2001 From: IMalyugin Date: Thu, 16 Jun 2022 16:30:49 +0300 Subject: [PATCH] Add target=@[dist-tag] option support (#1134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Иван Малюгин Co-authored-by: Raine Revere --- README.md | 10 ++- src/cli-options.ts | 17 ++-- src/lib/isUpgradeable.ts | 8 +- src/lib/queryVersions.ts | 119 +++++++++++-------------- src/package-managers/npm.ts | 34 ++++++- src/package-managers/yarn.ts | 85 ++++++++++-------- src/types/Options.ts | 1 + src/types/RunOptions.ts | 4 +- src/types/Target.ts | 5 +- src/version-util.ts | 27 ++++++ test/index.test.ts | 167 +++++++++++++++++++++++++++++++---- test/version-util.test.ts | 22 +++++ 12 files changed, 357 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index 7dff03cc..469ff54d 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,8 @@ ncu "/^(?!react-).*$/" # windows - `0.1.0` → `0.2.1` - With `--target patch`, only update patch: - `0.1.0` → `0.1.2` +- With `--target @next`, update to the version published on the `next` tag: + - `0.1.0` -> `0.1.1-next.1` ## Options @@ -206,10 +208,10 @@ ncu "/^(?!react-).*$/" # windows package info. (default: 3) -s, --silent Don't output anything (--loglevel silent). --stdin Read package.json from stdin. --t, --target Target version or function that returns version - to upgrade to: latest, newest, greatest, minor, - patch. Run "ncu --help --target" for details. - (default: "latest") +-t, --target Determines the version to upgrade to: latest, + newest, greatest, minor, patch, @[tag], or + [function]. Run "ncu --help --target" for + details. (default: "latest") --timeout Global timeout in milliseconds. (default: no global timeout and 30 seconds per npm-registry-fetch) diff --git a/src/cli-options.ts b/src/cli-options.ts index 7b194884..fd91fe6e 100755 --- a/src/cli-options.ts +++ b/src/cli-options.ts @@ -32,15 +32,10 @@ const getHelpTargetTable = (): string => { table.push([ 'greatest', - `Upgrade to the highest version number, regardless of release date or tag. + `Upgrade to the highest version number published, regardless of release date or tag. Includes prereleases.`, ]) - table.push([ - 'latest', - `Upgrade to whatever the package's "latest" git tag points to. It's usually the -non-prerelease version with the highest version number, but is ultimately decided -by each project's maintainers. Default.`, - ]) + table.push(['latest', `Upgrade to whatever the package's "latest" git tag points to. Excludes pre is specified.`]) table.push(['minor', 'Upgrade to the highest minor version without bumping the major version.']) table.push([ 'newest', @@ -48,8 +43,9 @@ by each project's maintainers. Default.`, other version numbers that are higher. Includes prereleases.`, ]) table.push(['patch', `Upgrade to the highest patch version without bumping the minor or major versions.`]) + table.push(['@[tag]', `Upgrade to the version published to a specific tag, e.g. 'next' or 'beta'.`]) - return `Set the target version that is upgraded to. (default: "latest") + return `Determines the version to upgrade to. (default: "latest") ${table.toString()} @@ -354,9 +350,10 @@ As a comparison: without using the --peer option, ncu will suggest the latest ve short: 't', arg: 'value', description: - 'Target version or function that returns version to upgrade to: latest, newest, greatest, minor, patch. Run "ncu --help --target" for details. (default: "latest")', + 'Determines the version to upgrade to: latest, newest, greatest, minor, patch, @[tag], or [function]. Run "ncu --help --target" for details. (default: "latest")', help: getHelpTargetTable(), - type: `'latest' | 'newest' | 'greatest' | 'minor' | 'patch' | TargetFunction`, + // eslint-disable-next-line no-template-curly-in-string + type: `'latest' | 'newest' | 'greatest' | 'minor' | 'patch' | ${'`@${string}`'} | TargetFunction`, }, { long: 'timeout', diff --git a/src/lib/isUpgradeable.ts b/src/lib/isUpgradeable.ts index 812d2fc1..2ce9d7b9 100644 --- a/src/lib/isUpgradeable.ts +++ b/src/lib/isUpgradeable.ts @@ -1,6 +1,6 @@ import { Version } from '../types/Version' import { VersionSpec } from '../types/VersionSpec' -import { fixPseudoVersion, stringify, isWildCard } from '../version-util' +import { fixPseudoVersion, stringify, isWildCard, isComparable } from '../version-util' import * as semver from 'semver' import semverutils from 'semver-utils' @@ -40,8 +40,10 @@ function isUpgradeable(current: VersionSpec, latest: Version) { return ( isValidCurrent && isValidLatest && - !semver.satisfies(latestNormalized, range.operator === '<' ? current : version) && - !semver.ltr(latestNormalized, version) + // allow an upgrade if two prerelease versions can't be compared by semver + (!isComparable(latestNormalized, version) || + (!semver.satisfies(latestNormalized, range.operator === '<' ? current : version) && + !semver.ltr(latestNormalized, version))) ) } diff --git a/src/lib/queryVersions.ts b/src/lib/queryVersions.ts index 21ecfd93..1b0986bf 100644 --- a/src/lib/queryVersions.ts +++ b/src/lib/queryVersions.ts @@ -24,7 +24,7 @@ import { VersionSpec } from '../types/VersionSpec' async function queryVersions(packageMap: Index, options: Options = {}): Promise> { const target = options.target || 'latest' const packageList = Object.keys(packageMap) - const packageManager = getPackageManager(options.packageManager) + const globalPackageManager = getPackageManager(options.packageManager) let bar: ProgressBar if (!options.json && options.loglevel !== 'silent' && options.loglevel !== 'verbose' && packageList.length > 0) { @@ -42,79 +42,68 @@ async function queryVersions(packageMap: Index, options: Options = async function getPackageVersionProtected(dep: VersionSpec): Promise { const npmAlias = parseNpmAlias(packageMap[dep]) const [name, version] = npmAlias || [dep, packageMap[dep]] - const targetResult = typeof target === 'string' ? target : target(name, parseRange(version)) + let targetResult = typeof target === 'string' ? target : target(name, parseRange(version)) + let distTag = 'latest' + + if (targetResult[0] === '@') { + distTag = targetResult.slice(1) + targetResult = 'distTag' + } let versionNew: Version | null = null + const isGithubDependency = isGithubUrl(packageMap[dep]) - // use gitTags package manager for git urls - if (isGithubUrl(packageMap[dep])) { - // override packageManager and getPackageVersion just for this dependency - const packageManager = packageManagers.gitTags - const getPackageVersion = packageManager[targetResult as keyof typeof packageManager] as GetVersion - - if (!getPackageVersion) { - const packageManagerSupportedVersionTargets = supportedVersionTargets.filter(t => t in packageManager) - return Promise.reject( - new Error( - `Unsupported target "${targetResult}" for github urls. Supported version targets are: ${packageManagerSupportedVersionTargets.join( - ', ', - )}`, - ), - ) - } + // use gitTags package manager for git urls (for this dependency only) + const packageManager = isGithubDependency ? packageManagers.gitTags : globalPackageManager + const packageManagerName = isGithubDependency ? 'github urls' : options.packageManager || 'npm' + + const getPackageVersion = packageManager[targetResult as keyof typeof packageManager] as GetVersion + + if (!getPackageVersion) { + const packageManagerSupportedVersionTargets = supportedVersionTargets.filter(t => t in packageManager) + return Promise.reject( + new Error( + `Unsupported target "${targetResult}" for ${packageManagerName}. Supported version targets are: ` + + packageManagerSupportedVersionTargets.join(', ') + + (!isGithubDependency ? ' and custom distribution tags, following "@" (example: @next)' : ''), + ), + ) + } + + try { versionNew = await getPackageVersion(name, version, { ...options, // upgrade prereleases to newer prereleases by default - pre: options.pre != null ? options.pre : isPre(version), + distTag, + pre: options.pre != null ? options.pre : distTag !== 'latest' || isPre(version), + retry: options.retry ?? 2, }) - } else { - // set the getPackageVersion function from options.target - // TODO: Remove "as GetVersion" and fix types - const getPackageVersion = packageManager[targetResult as keyof typeof packageManager] as GetVersion - if (!getPackageVersion) { - const packageManagerSupportedVersionTargets = supportedVersionTargets.filter(t => t in packageManager) - return Promise.reject( - new Error( - `Unsupported target "${targetResult}" for ${ - options.packageManager || 'npm' - }. Supported version targets are: ${packageManagerSupportedVersionTargets.join(', ')}`, - ), - ) - } - try { - versionNew = await getPackageVersion(name, version, { - ...options, - // upgrade prereleases to newer prereleases by default - pre: options.pre != null ? options.pre : isPre(version), - retry: options.retry ?? 2, - }) - versionNew = npmAlias && versionNew ? createNpmAlias(name, versionNew) : versionNew - } catch (err: any) { - const errorMessage = err ? (err.message || err).toString() : '' - if (errorMessage.match(/E404|ENOTFOUND|404 Not Found/i)) { - return { - error: `${errorMessage.replace( - / - Not found$/i, - '', - )}. Either your internet connection is down or unstable and all ${ - options.retry - } retry attempts failed, or the registry is not accessible, or the package does not exist.`, - } - } else { - // print a hint about the --timeout option for network timeout errors - if (!process.env.NCU_TESTS && /(Response|network) timeout/i.test(errorMessage)) { - console.error( - '\n\n' + - chalk.red( - 'FetchError: Request Timeout. npm-registry-fetch defaults to 30000 (30 seconds). Try setting the --timeout option (in milliseconds) to override this.', - ) + - '\n', - ) - } - - throw err + versionNew = !isGithubDependency && npmAlias && versionNew ? createNpmAlias(name, versionNew) : versionNew + } catch (err: any) { + const errorMessage = err ? (err.message || err).toString() : '' + if (errorMessage.match(/E404|ENOTFOUND|404 Not Found/i)) { + return { + error: `${errorMessage.replace( + / - Not found$/i, + '', + )}. Either your internet connection is down or unstable and all ${ + options.retry + } retry attempts failed, or the registry is not accessible, or the package does not exist.`, } + } else { + // print a hint about the --timeout option for network timeout errors + if (!process.env.NCU_TESTS && /(Response|network) timeout/i.test(errorMessage)) { + console.error( + '\n\n' + + chalk.red( + 'FetchError: Request Timeout. npm-registry-fetch defaults to 30000 (30 seconds). Try setting the --timeout option (in milliseconds) to override this.', + ) + + '\n', + ) + } + + throw err } } diff --git a/src/package-managers/npm.ts b/src/package-managers/npm.ts index 875251a4..d2c8b6e5 100644 --- a/src/package-managers/npm.ts +++ b/src/package-managers/npm.ts @@ -311,6 +311,8 @@ export async function defaultPrefix(options: Options): Promise { } /** + * Fetches the version of a package published to options.distTag. + * * @param packageName * @param currentVersion * @param options * @returns */ -export const latest: GetVersion = async (packageName, currentVersion, options = {}) => { - const latest = (await viewOne(packageName, 'dist-tags.latest', currentVersion, { +export const distTag: GetVersion = async (packageName, currentVersion, options: Options = {}) => { + const revision = (await viewOne(packageName, `dist-tags.${options.distTag}`, currentVersion, { registry: options.registry, timeout: options.timeout, retry: options.retry, @@ -381,7 +387,10 @@ export const latest: GetVersion = async (packageName, currentVersion, options = // if latest exists and latest is not a prerelease version, return it // if latest exists and latest is a prerelease version and --pre is specified, return it // if latest exists and latest not satisfies min version of engines.node - if (latest && filterPredicate(options)(latest)) return latest.version + if (revision && filterPredicate(options)(revision)) return revision.version + + // If we use a custom dist-tag, we do not want to get other 'pre' versions, just the ones from this dist-tag + if (options.distTag && options.distTag !== 'latest') return null // if latest is a prerelease version and --pre is not specified // or latest is deprecated @@ -391,6 +400,19 @@ export const latest: GetVersion = async (packageName, currentVersion, options = } /** + * Fetches the version published to the latest tag. + * + * @param packageName + * @param currentVersion + * @param options + * @returns + */ +export const latest: GetVersion = async (packageName: string, currentVersion: Version, options: Options = {}) => + distTag(packageName, currentVersion, { ...options, distTag: 'latest' }) + +/** + * Fetches the most recently published version, regardless of version number. + * * @param packageName * @param currentVersion * @param options @@ -417,6 +439,8 @@ export const newest: GetVersion = async (packageName, currentVersion, options = } /** + * Fetches the highest version with the same major version as currentVersion. + * * @param packageName * @param currentVersion * @param options @@ -432,6 +456,8 @@ export const minor: GetVersion = async (packageName, currentVersion, options = { } /** + * Fetches the highest version with the same minor and major version as currentVersion. + * * @param packageName * @param currentVersion * @param options diff --git a/src/package-managers/yarn.ts b/src/package-managers/yarn.ts index a8262993..987891be 100644 --- a/src/package-managers/yarn.ts +++ b/src/package-managers/yarn.ts @@ -202,6 +202,8 @@ export async function defaultPrefix(options: Options) { } /** + * Fetches the list of all installed packages. + * * @param [options] * @param [options.cwd] * @param [options.global] @@ -221,15 +223,36 @@ export const list = async (options: Options = {}, spawnOptions?: SpawnOptions) = } /** + * Fetches the highest version number, regardless of tag or publish time. + * * @param packageName * @param currentVersion * @param options * @returns */ -export const latest: GetVersion = async (packageName: string, currentVersion: Version, options: Options = {}) => { - const latest = (await viewOne( +export const greatest: GetVersion = async (packageName, currentVersion, options = {}) => { + const versions = (await viewOne(packageName, 'versions', currentVersion, options, npmConfigFromYarn())) as Packument[] + + return ( + _.last( + // eslint-disable-next-line fp/no-mutating-methods + _.filter(versions, filterPredicate(options)) + .map(o => o.version) + .sort(versionUtil.compareVersions), + ) || null + ) +} + +/** + * @param packageName + * @param currentVersion + * @param options + * @returns + */ +export const distTag: GetVersion = async (packageName, currentVersion, options: Options = {}) => { + const revision = (await viewOne( packageName, - 'dist-tags.latest', + `dist-tags.${options.distTag}`, currentVersion, { registry: options.registry, @@ -243,29 +266,32 @@ export const latest: GetVersion = async (packageName: string, currentVersion: Ve // if latest exists and latest is not a prerelease version, return it // if latest exists and latest is a prerelease version and --pre is specified, return it // if latest exists and latest not satisfies min version of engines.node - if (latest && filterPredicate(options)(latest)) return latest.version + if (revision && filterPredicate(options)(revision)) return revision.version + + // If we use a custom dist-tag, we do not want to get other 'pre' versions, just the ones from this dist-tag + if (options.distTag && options.distTag !== 'latest') return null // if latest is a prerelease version and --pre is not specified // or latest is deprecated // find the next valid version // known type based on dist-tags.latest - const versions = (await viewOne( - packageName, - 'versions', - currentVersion, - { - registry: options.registry, - timeout: options.timeout, - retry: options.retry, - }, - npmConfigFromYarn(), - )) as Packument[] - const validVersions = _.filter(versions, filterPredicate(options)) - - return _.last(validVersions.map(o => o.version)) || null + return greatest(packageName, currentVersion, options) } /** + * Fetches the version published to the latest tag. + * + * @param packageName + * @param currentVersion + * @param options + * @returns + */ +export const latest: GetVersion = async (packageName: string, currentVersion: Version, options: Options = {}) => + distTag(packageName, currentVersion, { ...options, distTag: 'latest' }) + +/** + * Fetches the most recently published version, regardless of version number. + * * @param packageName * @param currentVersion * @param options @@ -299,25 +325,8 @@ export const newest: GetVersion = async (packageName: string, currentVersion, op } /** - * @param packageName - * @param currentVersion - * @param options - * @returns - */ -export const greatest: GetVersion = async (packageName, currentVersion, options = {}) => { - const versions = (await viewOne(packageName, 'versions', currentVersion, options, npmConfigFromYarn())) as Packument[] - - return ( - _.last( - // eslint-disable-next-line fp/no-mutating-methods - _.filter(versions, filterPredicate(options)) - .map(o => o.version) - .sort(versionUtil.compareVersions), - ) || null - ) -} - -/** + * Fetches the highest version with the same major version as currentVersion. + * * @param packageName * @param currentVersion * @param options @@ -333,6 +342,8 @@ export const minor: GetVersion = async (packageName, currentVersion, options = { } /** + * Fetches the highest version with the same minor and major version as currentVersion. + * * @param packageName * @param currentVersion * @param options diff --git a/src/types/Options.ts b/src/types/Options.ts index 99de83cb..a38d4d84 100644 --- a/src/types/Options.ts +++ b/src/types/Options.ts @@ -6,6 +6,7 @@ import { VersionSpec } from './VersionSpec' export type Options = RunOptions & { args?: any[] cli?: boolean + distTag?: string json?: boolean nodeEngineVersion?: VersionSpec packageData?: string diff --git a/src/types/RunOptions.ts b/src/types/RunOptions.ts index 6961051f..56275b7d 100644 --- a/src/types/RunOptions.ts +++ b/src/types/RunOptions.ts @@ -117,8 +117,8 @@ export interface RunOptions { /** Read package.json from stdin. */ stdin?: string - /** Target version or function that returns version to upgrade to: latest, newest, greatest, minor, patch. Run "ncu --help --target" for details. (default: "latest") */ - target?: 'latest' | 'newest' | 'greatest' | 'minor' | 'patch' | TargetFunction + /** Determines the version to upgrade to: latest, newest, greatest, minor, patch, @[tag], or [function]. Run "ncu --help --target" for details. (default: "latest") */ + target?: 'latest' | 'newest' | 'greatest' | 'minor' | 'patch' | `@${string}` | TargetFunction /** Global timeout in milliseconds. (default: no global timeout and 30 seconds per npm-registry-fetch) */ timeout?: number diff --git a/src/types/Target.ts b/src/types/Target.ts index 76990f89..e6a2a734 100644 --- a/src/types/Target.ts +++ b/src/types/Target.ts @@ -3,5 +3,8 @@ import { TargetFunction } from './TargetFunction' /** Valid strings for the --target option. Indicates the desired version to upgrade to. */ type TargetString = 'latest' | 'newest' | 'greatest' | 'minor' | 'patch' +/** Upgrading to specific distribution tags can be done by passing @-starting value to --target option. */ +export type TargetDistTag = `@${string}` + /** The type of the --target option. Specifies the range from which to select the version to upgrade to. */ -export type Target = TargetString | TargetFunction +export type Target = TargetString | TargetDistTag | TargetFunction diff --git a/src/version-util.ts b/src/version-util.ts index c1c2659b..ba4cb078 100755 --- a/src/version-util.ts +++ b/src/version-util.ts @@ -189,6 +189,33 @@ export function colorizeDiff(from: string, to: string) { return leadingWildcard + partsToColor.slice(0, i).join('.') + middot + chalk[color](partsToColor.slice(i).join('.')) } +/** + * Extract prerelease tag, omitting build number + * Example: 1.0.0-next.alpha.2 -> next.alpha + * + * @param version + */ +const getPre = (version: string) => { + const pre = semver.prerelease(version) + return pre && pre.slice(0, -1).join('.') +} + +/** + * Check if it is allowed to compare two versions based on their prerelease tag + * + * SemVer both states that different prerelease versions can`t be compared + * and at the same time compares them as part of the version via strcmp + * + * @param a + * @param b + * @returns True if two versions can be compared by the means of SemVer + */ +export function isComparable(a: string, b: string) { + const preA = getPre(a) + const preB = getPre(b) + return typeof preA !== 'string' || typeof preB !== 'string' || preA === preB +} + /** Comparator used to sort semver versions */ export function compareVersions(a: string, b: string) { const isValid = semver.valid(a) && semver.valid(b) diff --git a/test/index.test.ts b/test/index.test.ts index 4bcf59f8..406733a2 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -6,6 +6,8 @@ import chaiString from 'chai-string' import * as ncu from '../src/' import { FilterFunction } from '../src/types/FilterFunction' import { TargetFunction } from '../src/types/TargetFunction' +import { Index } from '../src/types/IndexType' +import { Version } from '../src/types/Version' chai.should() chai.use(chaiAsPromised) @@ -331,15 +333,21 @@ describe('run', function () { }) it('update minor versions with --target minor', async () => { - const pkgData = await ncu.run({ target: 'minor', packageData: '{ "dependencies": { "chalk": "2.3.0" } }' }) + const pkgData = (await ncu.run({ + target: 'minor', + packageData: '{ "dependencies": { "chalk": "2.3.0" } }', + })) as Index pkgData!.should.have.property('chalk') - ;(pkgData as any).chalk.should.equal('2.4.2') + pkgData.chalk.should.equal('2.4.2') }) it('update patch versions with --target minor', async () => { - const pkgData = await ncu.run({ target: 'minor', packageData: '{ "dependencies": { "chalk": "2.4.0" } }' }) + const pkgData = (await ncu.run({ + target: 'minor', + packageData: '{ "dependencies": { "chalk": "2.4.0" } }', + })) as Index pkgData!.should.have.property('chalk') - ;(pkgData as any).chalk.should.equal('2.4.2') + pkgData.chalk.should.equal('2.4.2') }) it('do not update major versions with --target patch', async () => { @@ -353,9 +361,12 @@ describe('run', function () { }) it('update patch versions with --target patch', async () => { - const pkgData = await ncu.run({ target: 'patch', packageData: '{ "dependencies": { "chalk": "2.4.1" } }' }) + const pkgData = (await ncu.run({ + target: 'patch', + packageData: '{ "dependencies": { "chalk": "2.4.1" } }', + })) as Index pkgData!.should.have.property('chalk') - ;(pkgData as any).chalk.should.equal('2.4.2') + pkgData.chalk.should.equal('2.4.2') }) it('skip non-semver versions with --target patch', async () => { @@ -367,7 +378,7 @@ describe('run', function () { // eslint-disable-next-line jsdoc/require-jsdoc const target: TargetFunction = (name, [{ operator }]) => operator === '^' ? 'minor' : operator === '~' ? 'patch' : 'latest' - const pkgData = await ncu.run({ + const pkgData = (await ncu.run({ target, packageData: JSON.stringify({ dependencies: { @@ -377,15 +388,15 @@ describe('run', function () { mocha: '^8.3.2', }, }), - }) + })) as Index pkgData!.should.have.property('eslint-plugin-jsdoc') - ;(pkgData as any)['eslint-plugin-jsdoc'].should.equal('~36.1.1') + pkgData['eslint-plugin-jsdoc'].should.equal('~36.1.1') pkgData!.should.have.property('jsonlines') - ;(pkgData as any).jsonlines.should.equal('0.1.1') + pkgData.jsonlines.should.equal('0.1.1') pkgData!.should.have.property('juggernaut') - ;(pkgData as any).juggernaut.should.equal('2.1.1') + pkgData.juggernaut.should.equal('2.1.1') pkgData!.should.have.property('mocha') - ;(pkgData as any).mocha.should.equal('^8.4.0') + pkgData.mocha.should.equal('^8.4.0') }) it('custom target and filter function to mimic semver', async () => { @@ -395,7 +406,7 @@ describe('run', function () { // eslint-disable-next-line jsdoc/require-jsdoc const filter: FilterFunction = (_, [{ major, operator }]) => !(major === '0' || major === undefined || operator === undefined) - const pkgData = await ncu.run({ + const pkgData = (await ncu.run({ filter, target, packageData: JSON.stringify({ @@ -406,16 +417,140 @@ describe('run', function () { mocha: '^8.3.2', }, }), - }) + })) as Index pkgData!.should.have.property('eslint-plugin-jsdoc') - ;(pkgData as any)['eslint-plugin-jsdoc'].should.equal('~36.1.1') + pkgData['eslint-plugin-jsdoc'].should.equal('~36.1.1') pkgData!.should.not.have.property('jsonlines') pkgData!.should.not.have.property('juggernaut') pkgData!.should.have.property('mocha') - ;(pkgData as any).mocha.should.equal('^8.4.0') + pkgData.mocha.should.equal('^8.4.0') }) }) // end 'target' + describe('distTag as target', () => { + it('upgrade nonprerelease version to specific tag', async () => { + const upgraded = (await ncu.run({ + target: '@next', + packageData: JSON.stringify({ + dependencies: { + 'ncu-test-tag': '0.1.0', + }, + }), + })) as Index + + upgraded['ncu-test-tag'].should.equal('1.0.0-1') + }) + + it('upgrade prerelease version without preid to nonprerelease', async () => { + const upgraded = (await ncu.run({ + target: 'latest', + packageData: JSON.stringify({ + dependencies: { + 'ncu-test-tag': '1.0.0-1', + }, + }), + })) as Index + + upgraded['ncu-test-tag'].should.equal('1.1.0') + }) + + it('upgrade prerelease version with preid to higher version on a specific tag', async () => { + const upgraded = (await ncu.run({ + target: '@beta', + packageData: JSON.stringify({ + dependencies: { + 'ncu-test-tag': '1.0.0-task-42.0', + }, + }), + })) as Index + + upgraded['ncu-test-tag'].should.equal('1.0.1-beta.0') + }) + + // can't detect which prerelease is higher, so just allow switching + it('upgrade from prerelease without preid to prerelease with preid at a specific tag if major.minor.patch is the same', async () => { + const upgraded = (await ncu.run({ + target: '@task-42', + packageData: JSON.stringify({ + dependencies: { + 'ncu-test-tag': '1.0.0-beta.0', + }, + }), + })) as Index + + upgraded['ncu-test-tag'].should.equal('1.0.0-task-42.0') + }) + + // need to test reverse order too, because by base semver logic preid are sorted alphabetically + it('upgrade from prerelease with preid to prerelease without preid at a specific tag if major.minor.patch is the same', async () => { + const upgraded = (await ncu.run({ + target: '@next', + packageData: JSON.stringify({ + dependencies: { + 'ncu-test-tag': '1.0.0-task-42.0', + }, + }), + })) as Index + + upgraded['ncu-test-tag'].should.equal('1.0.0-1') + }) + + // comparing semver between different dist-tags is incorrect, both versions could be released from the same latest + // so instead of looking at numbers, we should focus on intention of the user upgrading to specific dist-tag + it('downgrade to tag with a non-matching preid and lower patch', async () => { + const upgraded = (await ncu.run({ + target: '@task-42', + packageData: JSON.stringify({ + dependencies: { + 'ncu-test-tag': '1.0.1-beta.0', + }, + }), + })) as Index + + upgraded['ncu-test-tag'].should.equal('1.0.0-task-42.0') + }) + + // same as previous, doesn't matter if it's patch, minor or major, comparing different dist-tags is incorrect + it('downgrade to tag with a non-matching preid and lower minor', async () => { + const upgraded = (await ncu.run({ + target: '@next', + packageData: JSON.stringify({ + dependencies: { + 'ncu-test-tag': '1.2.0-dev.0', + }, + }), + })) as Index + + upgraded['ncu-test-tag'].should.equal('1.0.0-1') + }) + + it('do not downgrade nonprerelease version to lower version with with specific tag', async () => { + const upgraded = await ncu.run({ + target: '@next', + packageData: JSON.stringify({ + dependencies: { + 'ncu-test-tag': '1.1.0', + }, + }), + }) + + upgraded!.should.not.have.property('ncu-test-tag') + }) + + it('do not downgrade to latest with lower version', async () => { + const upgraded = await ncu.run({ + target: 'latest', + packageData: JSON.stringify({ + dependencies: { + 'ncu-test-tag': '1.1.1-beta.0', + }, + }), + }) + + upgraded!.should.not.have.property('ncu-test-tag') + }) + }) // end 'distTAg as target' + describe('filterVersion', () => { it('filter by package version with string', async () => { const pkg = { diff --git a/test/version-util.test.ts b/test/version-util.test.ts index 95b0e9eb..9aff33d6 100644 --- a/test/version-util.test.ts +++ b/test/version-util.test.ts @@ -255,6 +255,28 @@ describe('version-util', () => { }) }) + describe('isComparable', () => { + it('a version without a preid is comparable to any version', () => { + versionUtil.isComparable('2.0.1', '0.0.1').should.equal(true) + versionUtil.isComparable('1.2.3-1', '1.2.3').should.equal(true) + versionUtil.isComparable('1.3.3', '1.2.3-2').should.equal(true) + versionUtil.isComparable('2.0.1-1', '0.0.1-2').should.equal(true) + versionUtil.isComparable('1.2.3-alpha.1', '1.2.3').should.equal(true) + versionUtil.isComparable('1.3.3', '1.2.3-alpha.2').should.equal(true) + versionUtil.isComparable('1.2.3-.dev.1', '1.2.3').should.equal(true) + versionUtil.isComparable('1.2.3', '1.2.3-next.dev.1').should.equal(true) + versionUtil.isComparable('1.2.3-next.dev.5', '1.2.3-next.dev.1').should.equal(true) + }) + + it('versions with non-matching preids are not comparable', () => { + versionUtil.isComparable('1.2.3-1', '1.2.3-dev.1').should.equal(false) + versionUtil.isComparable('1.3.3-next.1', '1.2.3-dev.2').should.equal(false) + versionUtil.isComparable('2.0.1-next.1', '0.0.1-task-42.2').should.equal(false) + versionUtil.isComparable('1.2.3-next.0', '1.2.3-next.dev.0').should.equal(false) + versionUtil.isComparable('1.2.3-alpha.0', '1.2.3-next.dev.0').should.equal(false) + }) + }) + describe('precisionAdd', () => { it('handle precision increase/decrease of base precisions', () => { versionUtil.precisionAdd('major', 0).should.equal('major')