mirror of
https://github.com/pnpm/pnpm.git
synced 2026-01-27 16:28:59 -05:00
PR #2416 Co-authored-by: Renovate Bot <bot@renovateapp.com> Co-authored-by: Zoltan Kochan <z@kochan.io>
158 lines
5.1 KiB
TypeScript
158 lines
5.1 KiB
TypeScript
import { WANTED_LOCKFILE } from '@pnpm/constants'
|
|
import PnpmError from '@pnpm/error'
|
|
import {
|
|
getLockfileImporterId,
|
|
Lockfile,
|
|
} from '@pnpm/lockfile-file'
|
|
import { nameVerFromPkgSnapshot } from '@pnpm/lockfile-utils'
|
|
import {
|
|
DependenciesField,
|
|
DEPENDENCIES_FIELDS,
|
|
IncludedDependencies,
|
|
PackageManifest,
|
|
ProjectManifest,
|
|
} from '@pnpm/types'
|
|
import { getAllDependenciesFromPackage } from '@pnpm/utils'
|
|
import * as dp from 'dependency-path'
|
|
import semver = require('semver')
|
|
|
|
export * from './createManifestGetter'
|
|
|
|
export type GetLatestManifestFunction = (packageName: string, rangeOrTag: string) => Promise<PackageManifest | null>
|
|
|
|
export interface OutdatedPackage {
|
|
alias: string,
|
|
belongsTo: DependenciesField,
|
|
current?: string, // not defined means the package is not installed
|
|
latestManifest?: PackageManifest,
|
|
packageName: string,
|
|
wanted: string,
|
|
}
|
|
|
|
export default async function outdated (
|
|
opts: {
|
|
compatible?: boolean,
|
|
currentLockfile: Lockfile | null,
|
|
getLatestManifest: GetLatestManifestFunction,
|
|
include?: IncludedDependencies,
|
|
lockfileDir: string,
|
|
manifest: ProjectManifest,
|
|
match?: (dependencyName: string) => boolean,
|
|
prefix: string,
|
|
wantedLockfile: Lockfile | null,
|
|
},
|
|
): Promise<OutdatedPackage[]> {
|
|
if (packageHasNoDeps(opts.manifest)) return []
|
|
if (!opts.wantedLockfile) {
|
|
throw new PnpmError('OUTDATED_NO_LOCKFILE', 'No lockfile in this directory. Run `pnpm install` to generate one.')
|
|
}
|
|
const allDeps = getAllDependenciesFromPackage(opts.manifest)
|
|
const importerId = getLockfileImporterId(opts.lockfileDir, opts.prefix)
|
|
const currentLockfile = opts.currentLockfile || { importers: { [importerId]: {} } }
|
|
|
|
const outdated: OutdatedPackage[] = []
|
|
|
|
await Promise.all(
|
|
DEPENDENCIES_FIELDS.map(async (depType) => {
|
|
if (
|
|
opts.include?.[depType] === false ||
|
|
!opts.wantedLockfile!.importers[importerId][depType]
|
|
) return
|
|
|
|
let pkgs = Object.keys(opts.wantedLockfile!.importers[importerId][depType]!)
|
|
|
|
if (opts.match) {
|
|
pkgs = pkgs.filter((pkgName) => opts.match!(pkgName))
|
|
}
|
|
|
|
await Promise.all(
|
|
pkgs.map(async (alias) => {
|
|
const ref = opts.wantedLockfile!.importers[importerId][depType]![alias]
|
|
|
|
// ignoring linked packages. (For backward compatibility)
|
|
if (ref.startsWith('file:')) {
|
|
return
|
|
}
|
|
|
|
const relativeDepPath = dp.refToRelative(ref, alias)
|
|
|
|
// ignoring linked packages
|
|
if (relativeDepPath === null) return
|
|
|
|
const pkgSnapshot = opts.wantedLockfile!.packages?.[relativeDepPath]
|
|
|
|
if (!pkgSnapshot) {
|
|
throw new Error(`Invalid ${WANTED_LOCKFILE} file. ${relativeDepPath} not found in packages field`)
|
|
}
|
|
|
|
const currentRef = currentLockfile.importers[importerId] &&
|
|
currentLockfile.importers[importerId][depType] &&
|
|
currentLockfile.importers[importerId][depType]![alias]
|
|
const currentRelative = currentRef && dp.refToRelative(currentRef, alias)
|
|
const current = currentRelative && dp.parse(currentRelative).version || currentRef
|
|
const wanted = dp.parse(relativeDepPath).version || ref
|
|
const { name: packageName } = nameVerFromPkgSnapshot(relativeDepPath, pkgSnapshot)
|
|
|
|
// It might be not the best solution to check for pkgSnapshot.name
|
|
// TODO: add some other field to distinct packages not from the registry
|
|
if (pkgSnapshot.resolution && (pkgSnapshot.resolution['type'] || pkgSnapshot.name)) { // tslint:disable-line:no-string-literal
|
|
if (current !== wanted) {
|
|
outdated.push({
|
|
alias,
|
|
belongsTo: depType,
|
|
current,
|
|
latestManifest: undefined,
|
|
packageName,
|
|
wanted,
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
const name = dp.parse(relativeDepPath).name ?? packageName
|
|
const latestManifest = await opts.getLatestManifest(
|
|
name,
|
|
opts.compatible ? (allDeps[name] ?? 'latest') : 'latest',
|
|
)
|
|
|
|
if (!latestManifest) return
|
|
|
|
if (!current) {
|
|
outdated.push({
|
|
alias,
|
|
belongsTo: depType,
|
|
latestManifest,
|
|
packageName,
|
|
wanted,
|
|
})
|
|
return
|
|
}
|
|
|
|
if (current !== wanted || semver.lt(current, latestManifest.version) || latestManifest.deprecated) {
|
|
outdated.push({
|
|
alias,
|
|
belongsTo: depType,
|
|
current,
|
|
latestManifest,
|
|
packageName,
|
|
wanted,
|
|
})
|
|
}
|
|
}),
|
|
)
|
|
}),
|
|
)
|
|
|
|
return outdated.sort((pkg1, pkg2) => pkg1.packageName.localeCompare(pkg2.packageName))
|
|
}
|
|
|
|
function packageHasNoDeps (manifest: ProjectManifest) {
|
|
return (!manifest.dependencies || isEmpty(manifest.dependencies))
|
|
&& (!manifest.devDependencies || isEmpty(manifest.devDependencies))
|
|
&& (!manifest.optionalDependencies || isEmpty(manifest.optionalDependencies))
|
|
}
|
|
|
|
function isEmpty (obj: object) {
|
|
return Object.keys(obj).length === 0
|
|
}
|