feat(cli): UI improvemnts to "pnpm outdated"

#1978
This commit is contained in:
Zoltan Kochan
2019-08-26 14:49:46 +03:00
committed by GitHub
parent bbfc5a5611
commit 8563ecf853
7 changed files with 182 additions and 67 deletions

View File

@@ -37,6 +37,7 @@
"@pnpm/package-is-installable": "workspace:2.2.1",
"@pnpm/package-store": "workspace:4.0.16",
"@pnpm/read-importer-manifest": "workspace:1.0.8",
"@pnpm/semver-diff": "1.0.0",
"@pnpm/server": "workspace:3.0.6",
"@pnpm/store-controller-types": "workspace:3.0.3",
"@pnpm/store-path": "2.1.0",

View File

@@ -7,14 +7,18 @@ import {
import outdated, {
forPackages as outdatedForPackages, OutdatedPackage,
} from '@pnpm/outdated'
import semverDiff, { SEMVER_CHANGE } from '@pnpm/semver-diff'
import storePath from '@pnpm/store-path'
import { PackageJson, Registries } from '@pnpm/types'
import chalk from 'chalk'
import R = require('ramda')
import stripColor = require('strip-color')
import table = require('text-table')
import createLatestVersionGetter from '../createLatestVersionGetter'
import { readImporterManifestOnly } from '../readImporterManifest'
export type OutdatedWithVersionDiff = OutdatedPackage & { change: SEMVER_CHANGE | null, diff?: [string[], string[]] }
export default async function (
args: string[],
opts: {
@@ -58,27 +62,93 @@ export default async function (
const columnNames = [
'Package',
'Current',
'Wanted',
'Latest',
...(opts.global ? [] : ['Belongs To']),
].map((txt) => chalk.underline(txt))
let columnFns: Array<(outdatedPkg: OutdatedPackage) => string> = [
({ packageName }) => chalk.yellow(packageName),
({ current }) => current || 'missing',
({ wanted }) => chalk.green(wanted),
({ latest }) => latest && chalk.magenta(latest) || '',
let columnFns: Array<(outdatedPkg: OutdatedWithVersionDiff) => string> = [
renderPackageName,
renderCurrent,
renderLatest,
]
if (!opts.global) {
columnFns.push(({ belongsTo }) => belongsTo)
return table([
columnNames,
...R.sortWith(
[
sortBySemverChange,
(o1, o2) => o1.packageName.localeCompare(o2.packageName),
],
outdatedPackages.map(toOutdatedWithVersionDiff)
)
.map((outdatedPkg) => columnFns.map((fn) => fn(outdatedPkg))),
], {
stringLength: (s: string) => stripColor(s).length,
})
}
export function toOutdatedWithVersionDiff<T> (outdated: T & OutdatedPackage): T & OutdatedWithVersionDiff {
if (outdated.latest) {
return {
...outdated,
...semverDiff(outdated.wanted, outdated.latest),
}
}
return {
...outdated,
change: 'unknown',
}
}
export function renderPackageName ({ belongsTo, packageName }: OutdatedWithVersionDiff) {
switch (belongsTo) {
case 'devDependencies': return `${packageName} ${chalk.dim('(dev)')}`
case 'optionalDependencies': return `${packageName} ${chalk.dim('(optional)')}`
default: return packageName
}
}
export function renderCurrent ({ current, wanted }: OutdatedWithVersionDiff) {
let output = current || 'missing'
if (current === wanted) return output
return `${output} (wanted ${wanted})`
}
const DIFF_COLORS = {
feature: chalk.yellowBright.bold,
fix: chalk.greenBright.bold,
}
export function renderLatest ({ latest, change, diff }: OutdatedWithVersionDiff) {
if (!latest) return ''
if (change === null || !diff) return latest
const highlight = DIFF_COLORS[change] || chalk.redBright.bold
const same = joinVersionTuples(diff[0], 0)
const other = highlight(joinVersionTuples(diff[1], diff[0].length))
if (!same) return other
return diff[0].length === 3 ? `${same}-${other}` : `${same}.${other}`
}
function joinVersionTuples (versionTuples: string[], startIndex: number) {
const neededForSemver = 3 - startIndex
if (versionTuples.length <= neededForSemver) return versionTuples.join('.')
return `${
versionTuples.slice(0, neededForSemver).join('.')
}-${
versionTuples.slice(neededForSemver).join('.')
}`
}
export function sortBySemverChange (outdated1: OutdatedWithVersionDiff, outdated2: OutdatedWithVersionDiff) {
return pkgPriority(outdated1) - pkgPriority(outdated2)
}
function pkgPriority (pkg: OutdatedWithVersionDiff) {
switch (pkg.change) {
case null: return 0
case 'fix': return 1
case 'feature': return 2
case 'breaking': return 3
default: return 4
}
console.log(
table([
columnNames,
...outdatedPackages.map((outdatedPkg) => columnFns.map((fn) => fn(outdatedPkg))),
], {
stringLength: (s: string) => stripColor(s).length,
}),
)
}
export async function outdatedDependenciesOfWorkspacePackages (

View File

@@ -1,10 +1,18 @@
import { getLockfileImporterId } from '@pnpm/lockfile-file'
import { OutdatedPackage } from '@pnpm/outdated'
import { DependenciesField, PackageJson, Registries } from '@pnpm/types'
import chalk from 'chalk'
import R = require('ramda')
import stripColor = require('strip-color')
import table = require('text-table')
import { outdatedDependenciesOfWorkspacePackages } from '../outdated'
import {
outdatedDependenciesOfWorkspacePackages,
renderCurrent,
renderLatest,
renderPackageName,
sortBySemverChange,
toOutdatedWithVersionDiff,
} from '../outdated'
const DEP_PRIORITY: Record<DependenciesField, number> = {
dependencies: 1,
@@ -12,6 +20,15 @@ const DEP_PRIORITY: Record<DependenciesField, number> = {
optionalDependencies: 0,
}
type OutdatedInWorkspace = OutdatedPackage & {
belongsTo: DependenciesField,
current?: string,
dependentPkgs: Array<{ location: string, manifest: PackageJson }>,
latest?: string,
packageName: string,
wanted: string,
}
export default async (
pkgs: Array<{path: string, manifest: PackageJson}>,
args: string[],
@@ -42,14 +59,7 @@ export default async (
userAgent: string,
},
) => {
const outdatedByNameAndType = {} as Record<string, {
belongsTo: DependenciesField,
current?: string,
dependentPkgs: Array<{ location: string, manifest: PackageJson }>,
latest?: string,
packageName: string,
wanted: string,
}>
const outdatedByNameAndType = {} as Record<string, OutdatedInWorkspace>
if (opts.lockfileDirectory) {
const outdatedPackagesByProject = await outdatedDependenciesOfWorkspacePackages(pkgs, args, opts)
for (let { prefix, outdatedPackages, manifest } of outdatedPackagesByProject) {
@@ -76,23 +86,24 @@ export default async (
}))
}
const columnNames = ['Package', 'Current', 'Wanted', 'Latest', 'Belongs To', 'Dependents'].map((txt) => chalk.underline(txt))
const columnNames = ['Package', 'Current', 'Latest', 'Dependents'].map((txt) => chalk.underline(txt))
console.log(
table([
columnNames,
...R.sortWith(
[
sortBySemverChange,
(o1, o2) => o1.packageName.localeCompare(o2.packageName),
(o1, o2) => DEP_PRIORITY[o1.belongsTo] - DEP_PRIORITY[o2.belongsTo],
],
(Object.values(outdatedByNameAndType)),
(
Object.values(outdatedByNameAndType).map(toOutdatedWithVersionDiff)
),
)
.map((outdatedPkg) => [
chalk.yellow(outdatedPkg.packageName),
outdatedPkg.current || 'missing',
chalk.green(outdatedPkg.wanted),
chalk.magenta(outdatedPkg.latest || ''),
outdatedPkg.belongsTo,
renderPackageName(outdatedPkg),
renderCurrent(outdatedPkg),
renderLatest(outdatedPkg),
outdatedPkg.dependentPkgs
.map(({ manifest, location }) => manifest.name || location)
.sort()

View File

@@ -280,9 +280,17 @@ export default async function run (argv: string[]) {
const result = pnpmCmds[cmd](cliArgs, opts, cliConf.argv.remain[0])
if (result instanceof Promise) {
result
.then(resolve)
.then((output) => {
if (typeof output === 'string') {
console.log(output)
}
resolve()
})
.catch(reject)
} else {
if (typeof result === 'string') {
console.log(result)
}
resolve()
}
} catch (err) {

View File

@@ -1,5 +1,6 @@
import { WANTED_LOCKFILE } from '@pnpm/constants'
import prepare, { tempDir } from '@pnpm/prepare'
import chalk from 'chalk'
import { stripIndents } from 'common-tags'
import makeDir = require('make-dir')
import fs = require('mz/fs')
@@ -7,6 +8,7 @@ import normalizeNewline = require('normalize-newline')
import path = require('path')
import tape = require('tape')
import promisifyTape from 'tape-promise'
import outdated from '../src/cmd/outdated'
import { execPnpm, execPnpmSync } from './utils'
const hasOutdatedDepsFixture = path.join(__dirname, 'packages', 'has-outdated-deps')
@@ -18,15 +20,30 @@ const testOnly = promisifyTape(tape.only)
test('pnpm outdated', async (t: tape.Test) => {
process.chdir(hasOutdatedDepsFixture)
const result = execPnpmSync('outdated')
t.equal(result.status, 0)
t.equal(normalizeNewline(result.stdout.toString()), stripIndents`
Package Current Wanted Latest Belongs To
is-negative 1.0.0 1.1.0 2.1.0 dependencies
is-positive 1.0.0 3.1.0 3.1.0 dependencies
` + '\n')
t.equal(
await outdated([], {
alwaysAuth: false,
fetchRetries: 2,
fetchRetryFactor: 1,
fetchRetryMaxtimeout: 60000,
fetchRetryMintimeout: 10000,
global: false,
independentLeaves: false,
networkConcurrency: 16,
offline: false,
prefix: process.cwd(),
rawNpmConfig: { registry: 'https://localhost:4873' },
registries: { default: 'https://localhost:4873' },
strictSsl: false,
tag: 'latest',
userAgent: '',
}, 'outdated'),
stripIndents`
${chalk.underline('Package')} ${chalk.underline('Current')} ${chalk.underline('Latest')}
is-positive 1.0.0 (wanted 3.1.0) 3.1.0
is-negative 1.0.0 (wanted 1.1.0) ${chalk.redBright.bold('2.1.0')}
`,
)
})
test('pnpm outdated: only current lockfile is available', async (t: tape.Test) => {
@@ -41,9 +58,9 @@ test('pnpm outdated: only current lockfile is available', async (t: tape.Test) =
t.equal(result.status, 0)
t.equal(normalizeNewline(result.stdout.toString()), stripIndents`
Package Current Wanted Latest Belongs To
is-negative 1.0.0 1.0.0 2.1.0 dependencies
is-positive 1.0.0 1.0.0 3.1.0 dependencies
Package Current Latest
is-negative 1.0.0 2.1.0
is-positive 1.0.0 3.1.0
` + '\n')
})
@@ -58,9 +75,9 @@ test('pnpm outdated: only wanted lockfile is available', async (t: tape.Test) =>
t.equal(result.status, 0)
t.equal(normalizeNewline(result.stdout.toString()), stripIndents`
Package Current Wanted Latest Belongs To
is-negative missing 1.1.0 2.1.0 dependencies
is-positive missing 3.1.0 3.1.0 dependencies
Package Current Latest
is-positive missing (wanted 3.1.0) 3.1.0
is-negative missing (wanted 1.1.0) 2.1.0
` + '\n')
})
@@ -82,9 +99,9 @@ test('pnpm outdated with external lockfile', async (t: tape.Test) => {
t.equal(result.status, 0)
t.equal(normalizeNewline(result.stdout.toString()), stripIndents`
Package Current Wanted Latest Belongs To
is-negative 1.0.0 1.1.0 2.1.0 dependencies
is-positive 1.0.0 3.1.0 3.1.0 dependencies
Package Current Latest
is-positive 1.0.0 (wanted 3.1.0) 3.1.0
is-negative 1.0.0 (wanted 1.1.0) 2.1.0
` + '\n')
})
@@ -102,9 +119,9 @@ test('pnpm outdated on global packages', async (t: tape.Test) => {
t.equal(result.status, 0)
t.equal(normalizeNewline(result.stdout.toString()), stripIndents`
Package Current Wanted Latest
is-negative 1.0.0 1.0.0 2.1.0
is-positive 1.0.0 1.0.0 3.1.0
Package Current Latest
is-negative 1.0.0 2.1.0
is-positive 1.0.0 3.1.0
` + '\n')
})

View File

@@ -51,10 +51,10 @@ test('pnpm recursive outdated', async (t: tape.Test) => {
t.equal(result.status, 0)
t.equal(normalizeNewline(result.stdout.toString()), stripIndents`
Package Current Wanted Latest Belongs To Dependents
is-negative 1.0.0 1.0.0 2.1.0 dependencies project-2
is-negative 1.0.0 1.0.0 2.1.0 devDependencies project-3
is-positive 1.0.0 1.0.0 3.1.0 dependencies project-1, project-3
Package Current Latest Dependents
is-negative 1.0.0 2.1.0 project-2
is-negative (dev) 1.0.0 2.1.0 project-3
is-positive 1.0.0 3.1.0 project-1, project-3
` + '\n')
}
@@ -64,8 +64,8 @@ test('pnpm recursive outdated', async (t: tape.Test) => {
t.equal(result.status, 0)
t.equal(normalizeNewline(result.stdout.toString()), stripIndents`
Package Current Wanted Latest Belongs To Dependents
is-positive 1.0.0 1.0.0 3.1.0 dependencies project-1, project-3
Package Current Latest Dependents
is-positive 1.0.0 3.1.0 project-1, project-3
` + '\n')
}
})
@@ -111,10 +111,10 @@ test('pnpm recursive outdated in workspace with shared lockfile', async (t: tape
t.equal(result.status, 0)
t.equal(normalizeNewline(result.stdout.toString()), stripIndents`
Package Current Wanted Latest Belongs To Dependents
is-negative 1.0.0 1.0.0 2.1.0 dependencies project-2
is-negative 1.0.0 1.0.0 2.1.0 devDependencies project-3
is-positive 1.0.0 1.0.0 3.1.0 dependencies project-1, project-3
Package Current Latest Dependents
is-negative 1.0.0 2.1.0 project-2
is-negative (dev) 1.0.0 2.1.0 project-3
is-positive 1.0.0 3.1.0 project-1, project-3
` + '\n')
}
@@ -124,8 +124,8 @@ test('pnpm recursive outdated in workspace with shared lockfile', async (t: tape
t.equal(result.status, 0)
t.equal(normalizeNewline(result.stdout.toString()), stripIndents`
Package Current Wanted Latest Belongs To Dependents
is-positive 1.0.0 1.0.0 3.1.0 dependencies project-1, project-3
Package Current Latest Dependents
is-positive 1.0.0 3.1.0 project-1, project-3
` + '\n')
}
})

8
pnpm-lock.yaml generated
View File

@@ -1036,6 +1036,7 @@ importers:
'@pnpm/package-is-installable': 'link:../package-is-installable'
'@pnpm/package-store': 'link:../package-store'
'@pnpm/read-importer-manifest': 'link:../read-importer-manifest'
'@pnpm/semver-diff': 1.0.0
'@pnpm/server': 'link:../server'
'@pnpm/store-controller-types': 'link:../store-controller-types'
'@pnpm/store-path': 2.1.0
@@ -1148,6 +1149,7 @@ importers:
'@pnpm/read-importer-manifest': 'workspace:1.0.8'
'@pnpm/read-package-json': 'link:../read-package-json'
'@pnpm/registry-mock': 1.4.0
'@pnpm/semver-diff': 1.0.0
'@pnpm/server': 'workspace:3.0.6'
'@pnpm/store-controller-types': 'workspace:3.0.3'
'@pnpm/store-path': 2.1.0
@@ -2225,6 +2227,12 @@ packages:
hasBin: true
resolution:
integrity: sha512-2GzsC4937QJcdWr9uWcBIaHF7CIeIeCVJhwfab31B73O9+Pc7baikKjMMUWDPgnMJG7kTRlRSadVYN6KDjiLaw==
/@pnpm/semver-diff/1.0.0:
dev: false
engines:
node: '>=8.15'
resolution:
integrity: sha512-xxjp9zkTS8cfRNEhfoTNIG9tGfiXn42anXDc6pLLepJlBMsDH4RVIF/ygOO2EKz7/9E2PF1zCQUTwjFpjFwL2g==
/@pnpm/store-path/2.1.0:
dependencies:
can-link: 1.0.2