Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Print reason when node engine does not match #1424

Merged
merged 7 commits into from
Jul 7, 2024
35 changes: 35 additions & 0 deletions src/lib/getEnginesNodeFromRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import ProgressBar from 'progress'
import { Index } from '../types/IndexType'
import { Options } from '../types/Options'
import { VersionSpec } from '../types/VersionSpec'
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<VersionSpec>, 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, options)).node
if (bar) {
bar.tick()
}
const accum = await accumPromise
return { ...accum, [pkg]: enginesNode }
}, Promise.resolve<Index<VersionSpec | undefined>>({}))
}

export default getEnginesNodeFromRegistry
49 changes: 49 additions & 0 deletions src/lib/getIgnoredUpgradesDueToEnginesNode.ts
Original file line number Diff line number Diff line change
@@ -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<VersionSpec>, optionsEnginesNodeMinVersion: Version) =>
!enginesNode || satisfies(optionsEnginesNodeMinVersion, enginesNode)

/** Get all upgrades that are ignored due to incompatible engines.node. */
export async function getIgnoredUpgradesDueToEnginesNode(
current: Index<VersionSpec>,
upgraded: Index<VersionSpec>,
options: Options = {},
) {
if (!options.nodeEngineVersion) return {}
const optionsEnginesNodeMinVersion = minVersion(options.nodeEngineVersion)?.version
if (!optionsEnginesNodeMinVersion) return {}
const [upgradedLatestVersions] = await upgradePackageDefinitions(current, {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unfortunate that we have to call upgradePackageDefinitions again, since it was already called to produce upgraded. Optimizing this probably involves larger changes to the upgrade pipeline though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I first tried to see how I can overcome it, but it requires major changes. If we ever do go ahead and make those changes, we can also not run the upgrades again for the ignored upgrades due to peer dependencies...

...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<IgnoredUpgradeDueToEnginesNode>,
)
}

export default getIgnoredUpgradesDueToEnginesNode
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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'
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<VersionSpec>,
upgraded: Index<VersionSpec>,
upgradedPeerDependencies: Index<Index<Version>>,
Expand Down Expand Up @@ -41,8 +41,8 @@ export async function getIgnoredUpgrades(
),
},
}),
{} as Index<IgnoredUpgrade>,
{} as Index<IgnoredUpgradeDueToPeerDeps>,
)
}

export default getIgnoredUpgrades
export default getIgnoredUpgradesDueToPeerDeps
30 changes: 13 additions & 17 deletions src/lib/getPeerDependenciesFromRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,23 +59,19 @@ async function getPeerDependenciesFromRegistry(packageMap: Index<Version>, optio
bar.render()
}

const peerDependencies: Index<Index<string>> = 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<Index<string>> = { ...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<Index<string>> = { ...accum, [pkg]: dep }
const circularData = isCircularPeer(newAcc, pkg)
if (circularData.isCircular) {
delete newAcc[pkg][circularData.offendingPackage]
}
return newAcc
}, Promise.resolve<Index<Index<string>>>({}))
}

export default getPeerDependenciesFromRegistry
23 changes: 21 additions & 2 deletions src/lib/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<IgnoredUpgrade>) {
export function printIgnoredUpdatesDueToPeerDeps(options: Options, ignoredUpdates: Index<IgnoredUpgradeDueToPeerDeps>) {
print(options, `\nIgnored incompatible updates (peer dependencies):\n`)
const table = renderDependencyTable(
Object.entries(ignoredUpdates).map(([pkgName, { from, to, reason }]) => {
Expand All @@ -382,3 +383,21 @@ export function printIgnoredUpdates(options: Options, ignoredUpdates: Index<Igno
)
print(options, table)
}

/** Print updates that were ignored due to incompatible engines.node. */
export function printIgnoredUpdatesDueToEnginesNode(
options: Options,
ignoredUpdates: Index<IgnoredUpgradeDueToEnginesNode>,
) {
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)
}
28 changes: 24 additions & 4 deletions src/lib/runLocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
}
}
}
Expand Down
33 changes: 32 additions & 1 deletion src/package-managers/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const fetchPartialPackument = async (
fields: (keyof Packument)[],
tag: string | null,
opts: npmRegistryFetch.FetchOptions = {},
version?: Version,
): Promise<Partial<Packument>> => {
const corgiDoc = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'
const fullDoc = 'application/json'
Expand All @@ -58,7 +59,10 @@ const fetchPartialPackument = async (
accept: opts.fullMetadata ? fullDoc : corgiDoc,
...opts.headers,
}
const url = path.join(registry, name)
let url = path.join(registry, name)
if (version) {
url = path.join(url, version)
}
const fetchOptions = {
...opts,
headers,
Expand Down Expand Up @@ -665,6 +669,33 @@ 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 (
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function doesn't do enough to justify it being factored out. Instead, call fetchPartialPackument directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

call it from where? fetchPartialPackument is internal to npm implementation, and getEngines is part of the package managers interface that is being used in the business logic. Where do you suggest I inline it and how?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I did not realize it was not exported. Let's leave this as-is then. Thanks.

packageName: string,
version: Version,
options: Options = {},
npmConfigLocal?: NpmConfig,
): Promise<Index<VersionSpec | undefined>> => {
const result = await fetchPartialPackument(
packageName,
[`engines`],
null,
{
...npmConfigLocal,
...npmConfig,
...(options.registry ? { registry: options.registry, silent: true } : null),
},
version,
)
return result.engines || {}
}

/**
* Fetches the list of all installed packages.
*
Expand Down
2 changes: 1 addition & 1 deletion src/package-managers/pnpm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,4 @@ export default async function spawnPnpm(
return stdout
}

export { defaultPrefix, getPeerDependencies, packageAuthorChanged } from './npm'
export { defaultPrefix, getPeerDependencies, getEngines, packageAuthorChanged } from './npm'
2 changes: 1 addition & 1 deletion src/package-managers/yarn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,4 +295,4 @@ export const semver = withNpmConfigFromYarn(npm.semver)

export default spawnYarn

export { getPeerDependencies, packageAuthorChanged } from './npm'
export { getEngines, getPeerDependencies, packageAuthorChanged } from './npm'
9 changes: 0 additions & 9 deletions src/types/IgnoredUpgrade.ts

This file was deleted.

9 changes: 9 additions & 0 deletions src/types/IgnoredUpgradeDueToEnginesNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Version } from './Version'
import { VersionSpec } from './VersionSpec'

/** An object that represents an upgrade that was ignored due to mismatch of engines.node */
export interface IgnoredUpgradeDueToEnginesNode {
from: Version
to: Version
enginesNode: VersionSpec
}
9 changes: 9 additions & 0 deletions src/types/IgnoredUpgradeDueToPeerDeps.ts
Original file line number Diff line number Diff line change
@@ -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<string>
}
7 changes: 7 additions & 0 deletions src/types/PackageManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { GetVersion } from './GetVersion'
import { Index } from './IndexType'
import { NpmConfig } from './NpmConfig'
import { Options } from './Options'
import { Version } from './Version'
import { VersionSpec } from './VersionSpec'
Expand All @@ -21,4 +22,10 @@ export interface PackageManager {
options?: Options,
) => Promise<boolean>
getPeerDependencies?: (packageName: string, version: Version) => Promise<Index<Version>>
getEngines?: (
packageName: string,
version: Version,
options: Options,
npmConfigLocal?: NpmConfig,
) => Promise<Index<VersionSpec | undefined>>
}
38 changes: 38 additions & 0 deletions test/getEnginesNodeFromRegistry.test.ts
Original file line number Diff line number Diff line change
@@ -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',
})
})
})
Loading
Loading