mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
396 lines
11 KiB
TypeScript
396 lines
11 KiB
TypeScript
import {
|
|
docsUrl,
|
|
readDepNameCompletions,
|
|
readProjectManifestOnly,
|
|
TABLE_OPTIONS,
|
|
} from '@pnpm/cli-utils'
|
|
import colorizeSemverDiff from '@pnpm/colorize-semver-diff'
|
|
import { type CompletionFunc } from '@pnpm/command'
|
|
import { FILTERING, OPTIONS, UNIVERSAL_OPTIONS } from '@pnpm/common-cli-options-help'
|
|
import { type Config, types as allTypes } from '@pnpm/config'
|
|
import { PnpmError } from '@pnpm/error'
|
|
import {
|
|
outdatedDepsOfProjects,
|
|
type OutdatedPackage,
|
|
} from '@pnpm/outdated'
|
|
import semverDiff from '@pnpm/semver-diff'
|
|
import { type DependenciesField, type PackageManifest, type ProjectRootDir } from '@pnpm/types'
|
|
import { table } from '@zkochan/table'
|
|
import chalk from 'chalk'
|
|
import pick from 'ramda/src/pick'
|
|
import sortWith from 'ramda/src/sortWith'
|
|
import renderHelp from 'render-help'
|
|
import { stripVTControlCharacters as stripAnsi } from 'util'
|
|
import {
|
|
DEFAULT_COMPARATORS,
|
|
NAME_COMPARATOR,
|
|
type OutdatedWithVersionDiff,
|
|
} from './utils'
|
|
import { outdatedRecursive } from './recursive'
|
|
|
|
export function rcOptionsTypes (): Record<string, unknown> {
|
|
return {
|
|
...pick([
|
|
'depth',
|
|
'dev',
|
|
'global-dir',
|
|
'global',
|
|
'long',
|
|
'optional',
|
|
'production',
|
|
], allTypes),
|
|
compatible: Boolean,
|
|
format: ['table', 'list', 'json'],
|
|
'sort-by': 'name',
|
|
}
|
|
}
|
|
|
|
export const cliOptionsTypes = (): Record<string, unknown> => ({
|
|
...rcOptionsTypes(),
|
|
recursive: Boolean,
|
|
})
|
|
|
|
export const shorthands: Record<string, string> = {
|
|
D: '--dev',
|
|
P: '--production',
|
|
table: '--format=table',
|
|
'no-table': '--format=list',
|
|
json: '--format=json',
|
|
}
|
|
|
|
export const commandNames = ['outdated']
|
|
|
|
export function help (): string {
|
|
return renderHelp({
|
|
description: `Check for outdated packages. The check can be limited to a subset of the installed packages by providing arguments (patterns are supported).
|
|
|
|
Examples:
|
|
pnpm outdated
|
|
pnpm outdated --long
|
|
pnpm outdated gulp-* @babel/core`,
|
|
descriptionLists: [
|
|
{
|
|
title: 'Options',
|
|
|
|
list: [
|
|
{
|
|
description: 'Print only versions that satisfy specs in package.json',
|
|
name: '--compatible',
|
|
},
|
|
{
|
|
description: 'By default, details about the outdated packages (such as a link to the repo) are not displayed. \
|
|
To display the details, pass this option.',
|
|
name: '--long',
|
|
},
|
|
{
|
|
description: 'Check for outdated dependencies in every package found in subdirectories \
|
|
or in every workspace package, when executed inside a workspace. \
|
|
For options that may be used with `-r`, see "pnpm help recursive"',
|
|
name: '--recursive',
|
|
shortAlias: '-r',
|
|
},
|
|
{
|
|
description: 'Prints the outdated packages in a list. Good for small consoles',
|
|
name: '--no-table',
|
|
},
|
|
{
|
|
description: 'Check only "dependencies" and "optionalDependencies"',
|
|
name: '--prod',
|
|
shortAlias: '-P',
|
|
},
|
|
{
|
|
description: 'Check only "devDependencies"',
|
|
name: '--dev',
|
|
shortAlias: '-D',
|
|
},
|
|
{
|
|
description: 'Don\'t check "optionalDependencies"',
|
|
name: '--no-optional',
|
|
},
|
|
{
|
|
description: 'Prints the outdated dependencies in the given format. Default is "table". Supported options: "table, list, json"',
|
|
name: '--format <format>',
|
|
},
|
|
{
|
|
description: 'Specify the sorting method. Currently only `name` is supported.',
|
|
name: '--sort-by',
|
|
},
|
|
OPTIONS.globalDir,
|
|
...UNIVERSAL_OPTIONS,
|
|
],
|
|
},
|
|
FILTERING,
|
|
],
|
|
url: docsUrl('outdated'),
|
|
usages: ['pnpm outdated [<pkg> ...]'],
|
|
})
|
|
}
|
|
|
|
export const completion: CompletionFunc = async (cliOpts) => {
|
|
return readDepNameCompletions(cliOpts.dir as string)
|
|
}
|
|
|
|
export type OutdatedCommandOptions = {
|
|
compatible?: boolean
|
|
long?: boolean
|
|
recursive?: boolean
|
|
format?: 'table' | 'list' | 'json'
|
|
sortBy?: 'name'
|
|
} & Pick<Config,
|
|
| 'allProjects'
|
|
| 'ca'
|
|
| 'cacheDir'
|
|
| 'catalogs'
|
|
| 'cert'
|
|
| 'dev'
|
|
| 'dir'
|
|
| 'engineStrict'
|
|
| 'fetchRetries'
|
|
| 'fetchRetryFactor'
|
|
| 'fetchRetryMaxtimeout'
|
|
| 'fetchRetryMintimeout'
|
|
| 'fetchTimeout'
|
|
| 'global'
|
|
| 'httpProxy'
|
|
| 'httpsProxy'
|
|
| 'key'
|
|
| 'localAddress'
|
|
| 'lockfileDir'
|
|
| 'networkConcurrency'
|
|
| 'noProxy'
|
|
| 'offline'
|
|
| 'optional'
|
|
| 'production'
|
|
| 'rawConfig'
|
|
| 'registries'
|
|
| 'selectedProjectsGraph'
|
|
| 'strictSsl'
|
|
| 'tag'
|
|
| 'userAgent'
|
|
> & Partial<Pick<Config, 'userConfig'>>
|
|
|
|
export async function handler (
|
|
opts: OutdatedCommandOptions,
|
|
params: string[] = []
|
|
): Promise<{ output: string, exitCode: number }> {
|
|
const include = {
|
|
dependencies: opts.production !== false,
|
|
devDependencies: opts.dev !== false,
|
|
optionalDependencies: opts.optional !== false,
|
|
}
|
|
if (opts.recursive && (opts.selectedProjectsGraph != null)) {
|
|
const pkgs = Object.values(opts.selectedProjectsGraph).map((wsPkg) => wsPkg.package)
|
|
return outdatedRecursive(pkgs, params, { ...opts, include })
|
|
}
|
|
const manifest = await readProjectManifestOnly(opts.dir, opts)
|
|
const packages = [
|
|
{
|
|
rootDir: opts.dir as ProjectRootDir,
|
|
manifest,
|
|
},
|
|
]
|
|
const [outdatedPackages] = await outdatedDepsOfProjects(packages, params, {
|
|
...opts,
|
|
fullMetadata: opts.long,
|
|
ignoreDependencies: manifest?.pnpm?.updateConfig?.ignoreDependencies,
|
|
include,
|
|
retry: {
|
|
factor: opts.fetchRetryFactor,
|
|
maxTimeout: opts.fetchRetryMaxtimeout,
|
|
minTimeout: opts.fetchRetryMintimeout,
|
|
retries: opts.fetchRetries,
|
|
},
|
|
timeout: opts.fetchTimeout,
|
|
})
|
|
|
|
let output!: string
|
|
switch (opts.format ?? 'table') {
|
|
case 'table': {
|
|
output = renderOutdatedTable(outdatedPackages, opts)
|
|
break
|
|
}
|
|
case 'list': {
|
|
output = renderOutdatedList(outdatedPackages, opts)
|
|
break
|
|
}
|
|
case 'json': {
|
|
output = renderOutdatedJSON(outdatedPackages, opts)
|
|
break
|
|
}
|
|
default: {
|
|
throw new PnpmError('BAD_OUTDATED_FORMAT', `Unsupported format: ${opts.format?.toString() ?? 'undefined'}`)
|
|
}
|
|
}
|
|
return {
|
|
output,
|
|
exitCode: outdatedPackages.length === 0 ? 0 : 1,
|
|
}
|
|
}
|
|
|
|
function renderOutdatedTable (outdatedPackages: readonly OutdatedPackage[], opts: { long?: boolean, sortBy?: 'name' }): string {
|
|
if (outdatedPackages.length === 0) return ''
|
|
const columnNames = [
|
|
'Package',
|
|
'Current',
|
|
'Latest',
|
|
]
|
|
|
|
const columnFns = [
|
|
renderPackageName,
|
|
renderCurrent,
|
|
renderLatest,
|
|
]
|
|
|
|
if (opts.long) {
|
|
columnNames.push('Details')
|
|
columnFns.push(renderDetails)
|
|
}
|
|
|
|
// Avoid the overhead of allocating a new array caused by calling `array.map()`
|
|
for (let i = 0; i < columnNames.length; i++)
|
|
columnNames[i] = chalk.blueBright(columnNames[i])
|
|
|
|
const data = [
|
|
columnNames,
|
|
...sortOutdatedPackages(outdatedPackages, { sortBy: opts.sortBy })
|
|
.map((outdatedPkg) => columnFns.map((fn) => fn(outdatedPkg))),
|
|
]
|
|
let detailsColumnMaxWidth = 40
|
|
if (opts.long) {
|
|
detailsColumnMaxWidth = outdatedPackages.filter(pkg => pkg.latestManifest && !pkg.latestManifest.deprecated).reduce((maxWidth, pkg) => {
|
|
const cellWidth = pkg.latestManifest?.homepage?.length ?? 0
|
|
return Math.max(maxWidth, cellWidth)
|
|
}, 0)
|
|
}
|
|
|
|
return table(data, {
|
|
...TABLE_OPTIONS,
|
|
columns: {
|
|
...TABLE_OPTIONS.columns,
|
|
// Detail column:
|
|
3: {
|
|
width: detailsColumnMaxWidth,
|
|
wrapWord: true,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
function renderOutdatedList (outdatedPackages: readonly OutdatedPackage[], opts: { long?: boolean, sortBy?: 'name' }): string {
|
|
if (outdatedPackages.length === 0) return ''
|
|
return sortOutdatedPackages(outdatedPackages, { sortBy: opts.sortBy })
|
|
.map((outdatedPkg) => {
|
|
let info = `${chalk.bold(renderPackageName(outdatedPkg))}
|
|
${renderCurrent(outdatedPkg)} ${chalk.grey('=>')} ${renderLatest(outdatedPkg)}`
|
|
|
|
if (opts.long) {
|
|
const details = renderDetails(outdatedPkg)
|
|
|
|
if (details) {
|
|
info += `\n${details}`
|
|
}
|
|
}
|
|
|
|
return info
|
|
})
|
|
.join('\n\n') + '\n'
|
|
}
|
|
|
|
export interface OutdatedPackageJSONOutput {
|
|
current?: string
|
|
latest?: string
|
|
wanted: string
|
|
isDeprecated: boolean
|
|
dependencyType: DependenciesField
|
|
latestManifest?: PackageManifest
|
|
}
|
|
|
|
function renderOutdatedJSON (outdatedPackages: readonly OutdatedPackage[], opts: { long?: boolean, sortBy?: 'name' }): string {
|
|
const outdatedPackagesJSON: Record<string, OutdatedPackageJSONOutput> = sortOutdatedPackages(outdatedPackages, { sortBy: opts.sortBy })
|
|
.reduce((acc, outdatedPkg) => {
|
|
acc[outdatedPkg.packageName] = {
|
|
current: outdatedPkg.current,
|
|
latest: outdatedPkg.latestManifest?.version,
|
|
wanted: outdatedPkg.wanted,
|
|
isDeprecated: Boolean(outdatedPkg.latestManifest?.deprecated),
|
|
dependencyType: outdatedPkg.belongsTo,
|
|
}
|
|
if (opts.long) {
|
|
acc[outdatedPkg.packageName].latestManifest = outdatedPkg.latestManifest
|
|
}
|
|
return acc
|
|
}, {} as Record<string, OutdatedPackageJSONOutput>)
|
|
return JSON.stringify(outdatedPackagesJSON, null, 2)
|
|
}
|
|
|
|
function sortOutdatedPackages (outdatedPackages: readonly OutdatedPackage[], opts?: { sortBy?: 'name' }) {
|
|
const sortBy = opts?.sortBy
|
|
const comparators = (sortBy === 'name') ? [NAME_COMPARATOR] : DEFAULT_COMPARATORS
|
|
return sortWith(
|
|
comparators,
|
|
outdatedPackages.map(toOutdatedWithVersionDiff)
|
|
)
|
|
}
|
|
|
|
export function getCellWidth (data: string[][], columnNumber: number, maxWidth: number): number {
|
|
const maxCellWidth = data.reduce((cellWidth, row) => {
|
|
const cellLines = stripAnsi(row[columnNumber]).split('\n')
|
|
const currentCellWidth = cellLines.reduce((lineWidth, line) => {
|
|
return Math.max(lineWidth, line.length)
|
|
}, 0)
|
|
return Math.max(cellWidth, currentCellWidth)
|
|
}, 0)
|
|
return Math.min(maxWidth, maxCellWidth)
|
|
}
|
|
|
|
export function toOutdatedWithVersionDiff<Pkg extends OutdatedPackage> (outdated: Pkg): Pkg & OutdatedWithVersionDiff {
|
|
if (outdated.latestManifest != null) {
|
|
return {
|
|
...outdated,
|
|
...semverDiff(outdated.wanted, outdated.latestManifest.version),
|
|
}
|
|
}
|
|
return {
|
|
...outdated,
|
|
change: 'unknown',
|
|
}
|
|
}
|
|
|
|
export function renderPackageName ({ belongsTo, packageName }: OutdatedPackage): string {
|
|
switch (belongsTo) {
|
|
case 'devDependencies': return `${packageName} ${chalk.dim('(dev)')}`
|
|
case 'optionalDependencies': return `${packageName} ${chalk.dim('(optional)')}`
|
|
default: return packageName
|
|
}
|
|
}
|
|
|
|
export function renderCurrent ({ current, wanted }: OutdatedPackage): string {
|
|
const output = current ?? 'missing'
|
|
if (current === wanted) return output
|
|
return `${output} (wanted ${wanted})`
|
|
}
|
|
|
|
export function renderLatest (outdatedPkg: OutdatedWithVersionDiff): string {
|
|
const { latestManifest, change, diff } = outdatedPkg
|
|
if (latestManifest == null) return ''
|
|
if (change === null || (diff == null)) {
|
|
return latestManifest.deprecated
|
|
? chalk.redBright.bold('Deprecated')
|
|
: latestManifest.version
|
|
}
|
|
|
|
return colorizeSemverDiff({ change, diff })
|
|
}
|
|
|
|
export function renderDetails ({ latestManifest }: OutdatedPackage): string {
|
|
if (latestManifest == null) return ''
|
|
const outputs = []
|
|
if (latestManifest.deprecated) {
|
|
outputs.push(chalk.redBright(latestManifest.deprecated))
|
|
}
|
|
if (latestManifest.homepage) {
|
|
outputs.push(chalk.underline(latestManifest.homepage))
|
|
}
|
|
return outputs.join('\n')
|
|
}
|