From 2f77ec6892f702d32f72d6a4dced6749bfca21c6 Mon Sep 17 00:00:00 2001 From: Regev Brody Date: Sat, 8 Jun 2024 19:48:49 +0300 Subject: [PATCH] feat: when packages are filter due to engine not matching, we need to print it out, just like we do in peer checks #1422 --- src/lib/getEnginesNodeFromRegistry.ts | 35 +++++++++++++ src/lib/getIgnoredUpgradesDueToEnginesNode.ts | 49 +++++++++++++++++++ ....ts => getIgnoredUpgradesDueToPeerDeps.ts} | 8 +-- src/lib/getPeerDependenciesFromRegistry.ts | 30 +++++------- src/lib/logging.ts | 23 ++++++++- src/lib/runLocal.ts | 28 +++++++++-- src/package-managers/npm.ts | 13 +++++ src/package-managers/pnpm.ts | 2 +- src/package-managers/yarn.ts | 2 + src/types/IgnoredUpgrade.ts | 9 ---- src/types/IgnoredUpgradeDueToEnginesNode.ts | 8 +++ src/types/IgnoredUpgradeDueToPeerDeps.ts | 9 ++++ src/types/Options.ts | 4 +- src/types/PackageFile.ts | 3 +- src/types/PackageManager.ts | 1 + src/types/RunOptions.json | 6 ++- test/getEnginesNodeFromRegistry.test.ts | 38 ++++++++++++++ ...getIgnoredUpgradesDueToEnginesNode.test.ts | 32 ++++++++++++ ...> getIgnoredUpgradesDueToPeerDeps.test.ts} | 6 +-- test/package-managers/npm/index.test.ts | 5 ++ 20 files changed, 267 insertions(+), 44 deletions(-) create mode 100644 src/lib/getEnginesNodeFromRegistry.ts create mode 100644 src/lib/getIgnoredUpgradesDueToEnginesNode.ts rename src/lib/{getIgnoredUpgrades.ts => getIgnoredUpgradesDueToPeerDeps.ts} (86%) delete mode 100644 src/types/IgnoredUpgrade.ts create mode 100644 src/types/IgnoredUpgradeDueToEnginesNode.ts create mode 100644 src/types/IgnoredUpgradeDueToPeerDeps.ts create mode 100644 test/getEnginesNodeFromRegistry.test.ts create mode 100644 test/getIgnoredUpgradesDueToEnginesNode.test.ts rename test/{getIgnoredUpgrades.test.ts => getIgnoredUpgradesDueToPeerDeps.test.ts} (74%) diff --git a/src/lib/getEnginesNodeFromRegistry.ts b/src/lib/getEnginesNodeFromRegistry.ts new file mode 100644 index 000000000..e506aed84 --- /dev/null +++ b/src/lib/getEnginesNodeFromRegistry.ts @@ -0,0 +1,35 @@ +import ProgressBar from 'progress' +import { Index } from '../types/IndexType' +import { Options } from '../types/Options' +import { Version } from '../types/Version' +import getPackageManager from './getPackageManager' + +/** + * Get the engines.node versions from the NPM repository based on the version target. + * + * @param packageMap An object whose keys are package name and values are version + * @param [options={}] Options. + * @returns Promised {packageName: engines.node} collection + */ +async function getEnginesNodeFromRegistry(packageMap: Index, options: Options) { + const packageManager = getPackageManager(options, options.packageManager) + if (!packageManager.getEngines) return {} + + const numItems = Object.keys(packageMap).length + let bar: ProgressBar + if (!options.json && options.loglevel !== 'silent' && options.loglevel !== 'verbose' && numItems > 0) { + bar = new ProgressBar('[:bar] :current/:total :percent', { total: numItems, width: 20 }) + bar.render() + } + + return Object.entries(packageMap).reduce(async (accumPromise, [pkg, version]) => { + const enginesNode = (await packageManager.getEngines!(pkg, version)).node + if (bar) { + bar.tick() + } + const accum = await accumPromise + return { ...accum, [pkg]: enginesNode } + }, Promise.resolve>({})) +} + +export default getEnginesNodeFromRegistry diff --git a/src/lib/getIgnoredUpgradesDueToEnginesNode.ts b/src/lib/getIgnoredUpgradesDueToEnginesNode.ts new file mode 100644 index 000000000..e030245d7 --- /dev/null +++ b/src/lib/getIgnoredUpgradesDueToEnginesNode.ts @@ -0,0 +1,49 @@ +import { minVersion, satisfies } from 'semver' +import { IgnoredUpgradeDueToEnginesNode } from '../types/IgnoredUpgradeDueToEnginesNode' +import { Index } from '../types/IndexType' +import { Maybe } from '../types/Maybe' +import { Options } from '../types/Options' +import { Version } from '../types/Version' +import { VersionSpec } from '../types/VersionSpec' +import getEnginesNodeFromRegistry from './getEnginesNodeFromRegistry' +import upgradePackageDefinitions from './upgradePackageDefinitions' + +/** Checks if package.json min node version satisfies given package engine.node spec */ +const satisfiesNodeEngine = (enginesNode: Maybe, optionsEnginesNodeMinVersion: string) => + !enginesNode || satisfies(optionsEnginesNodeMinVersion, enginesNode) + +/** Get all upgrades that are ignored due to incompatible engines.node. */ +export async function getIgnoredUpgradesDueToEnginesNode( + current: Index, + upgraded: Index, + options: Options = {}, +) { + if (!options.nodeEngineVersion) return {} + const optionsEnginesNodeMinVersion = minVersion(options.nodeEngineVersion)?.version + if (!optionsEnginesNodeMinVersion) return {} + const [upgradedLatestVersions] = await upgradePackageDefinitions(current, { + ...options, + enginesNode: false, + nodeEngineVersion: undefined, + loglevel: 'silent', + }) + const enginesNodes = await getEnginesNodeFromRegistry(upgradedLatestVersions, options) + return Object.entries(upgradedLatestVersions) + .filter( + ([pkgName, newVersion]) => + upgraded[pkgName] !== newVersion && !satisfiesNodeEngine(enginesNodes[pkgName], optionsEnginesNodeMinVersion), + ) + .reduce( + (accum, [pkgName, newVersion]) => ({ + ...accum, + [pkgName]: { + from: current[pkgName], + to: newVersion, + enginesNode: enginesNodes[pkgName]!, + }, + }), + {} as Index, + ) +} + +export default getIgnoredUpgradesDueToEnginesNode diff --git a/src/lib/getIgnoredUpgrades.ts b/src/lib/getIgnoredUpgradesDueToPeerDeps.ts similarity index 86% rename from src/lib/getIgnoredUpgrades.ts rename to src/lib/getIgnoredUpgradesDueToPeerDeps.ts index f46cd6a2b..571ffbe4d 100644 --- a/src/lib/getIgnoredUpgrades.ts +++ b/src/lib/getIgnoredUpgradesDueToPeerDeps.ts @@ -1,5 +1,5 @@ import { satisfies } from 'semver' -import { IgnoredUpgrade } from '../types/IgnoredUpgrade' +import { IgnoredUpgradeDueToPeerDeps } from '../types/IgnoredUpgradeDueToPeerDeps' import { Index } from '../types/IndexType' import { Options } from '../types/Options' import { Version } from '../types/Version' @@ -7,7 +7,7 @@ import { VersionSpec } from '../types/VersionSpec' import upgradePackageDefinitions from './upgradePackageDefinitions' /** Get all upgrades that are ignored due to incompatible peer dependencies. */ -export async function getIgnoredUpgrades( +export async function getIgnoredUpgradesDueToPeerDeps( current: Index, upgraded: Index, upgradedPeerDependencies: Index>, @@ -41,8 +41,8 @@ export async function getIgnoredUpgrades( ), }, }), - {} as Index, + {} as Index, ) } -export default getIgnoredUpgrades +export default getIgnoredUpgradesDueToPeerDeps diff --git a/src/lib/getPeerDependenciesFromRegistry.ts b/src/lib/getPeerDependenciesFromRegistry.ts index 23bdd0712..9ff589acf 100644 --- a/src/lib/getPeerDependenciesFromRegistry.ts +++ b/src/lib/getPeerDependenciesFromRegistry.ts @@ -59,23 +59,19 @@ async function getPeerDependenciesFromRegistry(packageMap: Index, optio bar.render() } - const peerDependencies: Index> = Object.entries(packageMap).reduce( - async (accumPromise, [pkg, version]) => { - const dep = await packageManager.getPeerDependencies!(pkg, version) - if (bar) { - bar.tick() - } - const accum = await accumPromise - const newAcc: Index> = { ...accum, [pkg]: dep } - const circularData = isCircularPeer(newAcc, pkg) - if (circularData.isCircular) { - delete newAcc[pkg][circularData.offendingPackage] - } - return newAcc - }, - {}, - ) - return peerDependencies + return Object.entries(packageMap).reduce(async (accumPromise, [pkg, version]) => { + const dep = await packageManager.getPeerDependencies!(pkg, version) + if (bar) { + bar.tick() + } + const accum = await accumPromise + const newAcc: Index> = { ...accum, [pkg]: dep } + const circularData = isCircularPeer(newAcc, pkg) + if (circularData.isCircular) { + delete newAcc[pkg][circularData.offendingPackage] + } + return newAcc + }, Promise.resolve>>({})) } export default getPeerDependenciesFromRegistry diff --git a/src/lib/logging.ts b/src/lib/logging.ts index 1eec5301c..091c3427d 100755 --- a/src/lib/logging.ts +++ b/src/lib/logging.ts @@ -2,7 +2,8 @@ * Loggin functions. */ import Table from 'cli-table3' -import { IgnoredUpgrade } from '../types/IgnoredUpgrade' +import { IgnoredUpgradeDueToEnginesNode } from '../types/IgnoredUpgradeDueToEnginesNode' +import { IgnoredUpgradeDueToPeerDeps } from '../types/IgnoredUpgradeDueToPeerDeps' import { Index } from '../types/IndexType' import { Options } from '../types/Options' import { VersionResult } from '../types/VersionResult' @@ -368,7 +369,7 @@ export async function printUpgrades( } /** Print updates that were ignored due to incompatible peer dependencies. */ -export function printIgnoredUpdates(options: Options, ignoredUpdates: Index) { +export function printIgnoredUpdatesDueToPeerDeps(options: Options, ignoredUpdates: Index) { print(options, `\nIgnored incompatible updates (peer dependencies):\n`) const table = renderDependencyTable( Object.entries(ignoredUpdates).map(([pkgName, { from, to, reason }]) => { @@ -382,3 +383,21 @@ export function printIgnoredUpdates(options: Options, ignoredUpdates: Index, +) { + print(options, `\nIgnored incompatible updates (engines node):\n`) + const table = renderDependencyTable( + Object.entries(ignoredUpdates).map(([pkgName, { from, to, enginesNode }]) => [ + pkgName, + from, + '→', + colorizeDiff(from, to), + `reason: requires node ${enginesNode}`, + ]), + ) + print(options, table) +} diff --git a/src/lib/runLocal.ts b/src/lib/runLocal.ts index 2e3864d90..b994e1116 100644 --- a/src/lib/runLocal.ts +++ b/src/lib/runLocal.ts @@ -10,11 +10,20 @@ import { Version } from '../types/Version' import { VersionSpec } from '../types/VersionSpec' import chalk from './chalk' import getCurrentDependencies from './getCurrentDependencies' -import getIgnoredUpgrades from './getIgnoredUpgrades' +import { getIgnoredUpgradesDueToEnginesNode } from './getIgnoredUpgradesDueToEnginesNode' +import getIgnoredUpgradesDueToPeerDeps from './getIgnoredUpgradesDueToPeerDeps' import getPackageManager from './getPackageManager' import getPeerDependenciesFromRegistry from './getPeerDependenciesFromRegistry' import keyValueBy from './keyValueBy' -import { print, printIgnoredUpdates, printJson, printSorted, printUpgrades, toDependencyTable } from './logging' +import { + print, + printIgnoredUpdatesDueToEnginesNode, + printIgnoredUpdatesDueToPeerDeps, + printJson, + printSorted, + printUpgrades, + toDependencyTable, +} from './logging' import { pick } from './pick' import programError from './programError' import resolveDepSections from './resolveDepSections' @@ -246,9 +255,20 @@ async function runLocal( }, ) if (options.peer) { - const ignoredUpdates = await getIgnoredUpgrades(current, upgraded, upgradedPeerDependencies!, options) + const ignoredUpdates = await getIgnoredUpgradesDueToPeerDeps( + current, + upgraded, + upgradedPeerDependencies!, + options, + ) + if (Object.keys(ignoredUpdates).length > 0) { + printIgnoredUpdatesDueToPeerDeps(options, ignoredUpdates) + } + } + if (options.enginesNode) { + const ignoredUpdates = await getIgnoredUpgradesDueToEnginesNode(current, upgraded, options) if (Object.keys(ignoredUpdates).length > 0) { - printIgnoredUpdates(options, ignoredUpdates) + printIgnoredUpdatesDueToEnginesNode(options, ignoredUpdates) } } } diff --git a/src/package-managers/npm.ts b/src/package-managers/npm.ts index e7ccf0f6a..42eeb714b 100644 --- a/src/package-managers/npm.ts +++ b/src/package-managers/npm.ts @@ -665,6 +665,19 @@ export const getPeerDependencies = async (packageName: string, version: Version) return result ? parseJson(result, { command: [...args, '--json'].join(' ') }) : {} } +/** + * Fetches the engines list from the registry for a specific package version. + * + * @param packageName + * @param version + * @returns Promised engines collection + */ +export const getEngines = async (packageName: string, version: Version): Promise> => { + const args = ['view', `${packageName}@${version}`, 'engines'] + const result = await spawnNpm(args, {}, { rejectOnError: false }) + return result ? parseJson<{ node?: string }>(result, { command: [...args, '--json'].join(' ') }) : {} +} + /** * Fetches the list of all installed packages. * diff --git a/src/package-managers/pnpm.ts b/src/package-managers/pnpm.ts index 16464e95d..1d33a945a 100644 --- a/src/package-managers/pnpm.ts +++ b/src/package-managers/pnpm.ts @@ -107,4 +107,4 @@ export default async function spawnPnpm( return stdout } -export { defaultPrefix, getPeerDependencies, packageAuthorChanged } from './npm' +export { defaultPrefix, getPeerDependencies, getEngines, packageAuthorChanged } from './npm' diff --git a/src/package-managers/yarn.ts b/src/package-managers/yarn.ts index 96c84031b..b947421af 100644 --- a/src/package-managers/yarn.ts +++ b/src/package-managers/yarn.ts @@ -294,3 +294,5 @@ export const patch = withNpmConfigFromYarn(npm.patch) export const semver = withNpmConfigFromYarn(npm.semver) export default spawnYarn + +export { getEngines } from './npm' diff --git a/src/types/IgnoredUpgrade.ts b/src/types/IgnoredUpgrade.ts deleted file mode 100644 index aa741c844..000000000 --- a/src/types/IgnoredUpgrade.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Index } from './IndexType' -import { Version } from './Version' - -/** An object that represents an upgrade that was ignored, along with the reason. */ -export interface IgnoredUpgrade { - from: Version - to: Version - reason: Index -} diff --git a/src/types/IgnoredUpgradeDueToEnginesNode.ts b/src/types/IgnoredUpgradeDueToEnginesNode.ts new file mode 100644 index 000000000..c87098160 --- /dev/null +++ b/src/types/IgnoredUpgradeDueToEnginesNode.ts @@ -0,0 +1,8 @@ +import { Version } from './Version' + +/** An object that represents an upgrade that was ignored due to mismatch of engines.node */ +export interface IgnoredUpgradeDueToEnginesNode { + from: Version + to: Version + enginesNode: Version +} diff --git a/src/types/IgnoredUpgradeDueToPeerDeps.ts b/src/types/IgnoredUpgradeDueToPeerDeps.ts new file mode 100644 index 000000000..3211ec159 --- /dev/null +++ b/src/types/IgnoredUpgradeDueToPeerDeps.ts @@ -0,0 +1,9 @@ +import { Index } from './IndexType' +import { Version } from './Version' + +/** An object that represents an upgrade that was ignored due to peer dependencies, along with the reason. */ +export interface IgnoredUpgradeDueToPeerDeps { + from: Version + to: Version + reason: Index +} diff --git a/src/types/Options.ts b/src/types/Options.ts index 60f7f09d7..54d21dfc2 100644 --- a/src/types/Options.ts +++ b/src/types/Options.ts @@ -1,7 +1,7 @@ import { Cacher } from './Cacher' import { Index } from './IndexType' import { RunOptions } from './RunOptions' -import { VersionSpec } from './VersionSpec' +import { Version } from './Version' /** Internal, normalized options for all ncu behavior. Includes RunOptions that are specified in the CLI or passed to the ncu module, as well as meta information including CLI arguments, package information, and ncurc config. */ export type Options = RunOptions & { @@ -10,7 +10,7 @@ export type Options = RunOptions & { cli?: boolean distTag?: string json?: boolean - nodeEngineVersion?: VersionSpec + nodeEngineVersion?: Version packageData?: string peerDependencies?: Index rcConfigPath?: string diff --git a/src/types/PackageFile.ts b/src/types/PackageFile.ts index 99223c1e7..5b0e44450 100644 --- a/src/types/PackageFile.ts +++ b/src/types/PackageFile.ts @@ -1,5 +1,6 @@ import { Index } from './IndexType' import { PackageFileRepository } from './PackageFileRepository' +import { Version } from './Version' import { VersionSpec } from './VersionSpec' type NestedVersionSpecs = { @@ -12,7 +13,7 @@ export interface PackageFile { devDependencies?: Index // deno only imports?: Index - engines?: Index + engines?: Index name?: string // https://nodejs.org/api/packages.html#packagemanager packageManager?: string diff --git a/src/types/PackageManager.ts b/src/types/PackageManager.ts index 53538f6d9..e646a4389 100644 --- a/src/types/PackageManager.ts +++ b/src/types/PackageManager.ts @@ -21,4 +21,5 @@ export interface PackageManager { options?: Options, ) => Promise getPeerDependencies?: (packageName: string, version: Version) => Promise> + getEngines?: (packageName: string, version: Version) => Promise> } diff --git a/src/types/RunOptions.json b/src/types/RunOptions.json index 900237ba0..46e2b2ae7 100644 --- a/src/types/RunOptions.json +++ b/src/types/RunOptions.json @@ -5,6 +5,10 @@ "description": "A very generic object.", "type": "object" }, + "Index": { + "description": "A very generic object.", + "type": "object" + }, "NestedVersionSpecs": { "additionalProperties": { "anyOf": [ @@ -30,7 +34,7 @@ "description": "A very generic object." }, "engines": { - "$ref": "#/definitions/Index", + "$ref": "#/definitions/Index", "description": "A very generic object." }, "imports": { diff --git a/test/getEnginesNodeFromRegistry.test.ts b/test/getEnginesNodeFromRegistry.test.ts new file mode 100644 index 000000000..7700aba26 --- /dev/null +++ b/test/getEnginesNodeFromRegistry.test.ts @@ -0,0 +1,38 @@ +import { chalkInit } from '../src/lib/chalk' +import getEnginesNodeFromRegistry from '../src/lib/getEnginesNodeFromRegistry' +import chaiSetup from './helpers/chaiSetup' + +chaiSetup() + +describe('getEnginesNodeFromRegistry', function () { + it('single package', async () => { + await chalkInit() + const data = await getEnginesNodeFromRegistry({ del: '2.0.0' }, {}) + data.should.deep.equal({ + del: '>=0.10.0', + }) + }) + + it('single package empty', async () => { + await chalkInit() + const data = await getEnginesNodeFromRegistry({ 'ncu-test-return-version': '1.0' }, {}) + data.should.deep.equal({ 'ncu-test-return-version': undefined }) + }) + + it('multiple packages', async () => { + await chalkInit() + const data = await getEnginesNodeFromRegistry( + { + 'ncu-test-return-version': '1.0.0', + 'ncu-test-peer': '1.0.0', + del: '2.0.0', + }, + {}, + ) + data.should.deep.equal({ + 'ncu-test-return-version': undefined, + 'ncu-test-peer': undefined, + del: '>=0.10.0', + }) + }) +}) diff --git a/test/getIgnoredUpgradesDueToEnginesNode.test.ts b/test/getIgnoredUpgradesDueToEnginesNode.test.ts new file mode 100644 index 000000000..d597da45b --- /dev/null +++ b/test/getIgnoredUpgradesDueToEnginesNode.test.ts @@ -0,0 +1,32 @@ +import getIgnoredUpgradesDueToEnginesNode from '../src/lib/getIgnoredUpgradesDueToEnginesNode' +import chaiSetup from './helpers/chaiSetup' + +chaiSetup() + +describe('getIgnoredUpgradesDueToEnginesNode', function () { + it('ncu-test-peer-update', async () => { + const data = await getIgnoredUpgradesDueToEnginesNode( + { + 'ncu-test-return-version': '1.0.0', + 'ncu-test-peer': '1.0.0', + del: '2.2.2', + }, + { + 'ncu-test-return-version': '2.0.0', + 'ncu-test-peer': '1.1.0', + del: '2.2.2', + }, + { + enginesNode: true, + nodeEngineVersion: `^0.10.0`, + }, + ) + data.should.deep.equal({ + del: { + enginesNode: '>=14.16', + from: '2.2.2', + to: '7.1.0', + }, + }) + }) +}) diff --git a/test/getIgnoredUpgrades.test.ts b/test/getIgnoredUpgradesDueToPeerDeps.test.ts similarity index 74% rename from test/getIgnoredUpgrades.test.ts rename to test/getIgnoredUpgradesDueToPeerDeps.test.ts index 7ed9d37be..ee05e3e28 100644 --- a/test/getIgnoredUpgrades.test.ts +++ b/test/getIgnoredUpgradesDueToPeerDeps.test.ts @@ -1,11 +1,11 @@ -import getIgnoredUpgrades from '../src/lib/getIgnoredUpgrades' +import getIgnoredUpgradesDueToPeerDeps from '../src/lib/getIgnoredUpgradesDueToPeerDeps' import chaiSetup from './helpers/chaiSetup' chaiSetup() -describe('getIgnoredUpgrades', function () { +describe('getIgnoredUpgradesDueToPeerDeps', function () { it('ncu-test-peer-update', async () => { - const data = await getIgnoredUpgrades( + const data = await getIgnoredUpgradesDueToPeerDeps( { 'ncu-test-return-version': '1.0.0', 'ncu-test-peer': '1.0.0', diff --git a/test/package-managers/npm/index.test.ts b/test/package-managers/npm/index.test.ts index 40b9e0584..bfb04e083 100644 --- a/test/package-managers/npm/index.test.ts +++ b/test/package-managers/npm/index.test.ts @@ -31,4 +31,9 @@ describe('npm', function () { 'ncu-test-return-version': '1.x', }) }) + + it('getEngines', async () => { + await npm.getEngines('del', '2.0.0').should.eventually.deep.equal({ node: '>=0.10.0' }) + await npm.getEngines('ncu-test-return-version', '1.0').should.eventually.deep.equal({}) + }) })