diff --git a/packages/pnpm/package.json b/packages/pnpm/package.json index 3eae977505..813ce48fe2 100644 --- a/packages/pnpm/package.json +++ b/packages/pnpm/package.json @@ -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", diff --git a/packages/pnpm/src/cmd/outdated.ts b/packages/pnpm/src/cmd/outdated.ts index 04964ed3a7..bc406840e5 100644 --- a/packages/pnpm/src/cmd/outdated.ts +++ b/packages/pnpm/src/cmd/outdated.ts @@ -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 (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 ( diff --git a/packages/pnpm/src/cmd/recursive/outdated.ts b/packages/pnpm/src/cmd/recursive/outdated.ts index 82bc9c5f48..22069c22f5 100644 --- a/packages/pnpm/src/cmd/recursive/outdated.ts +++ b/packages/pnpm/src/cmd/recursive/outdated.ts @@ -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 = { dependencies: 1, @@ -12,6 +20,15 @@ const DEP_PRIORITY: Record = { 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, - latest?: string, - packageName: string, - wanted: string, - }> + const outdatedByNameAndType = {} as Record 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() diff --git a/packages/pnpm/src/main.ts b/packages/pnpm/src/main.ts index 75b7e5351e..a405f34d17 100644 --- a/packages/pnpm/src/main.ts +++ b/packages/pnpm/src/main.ts @@ -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) { diff --git a/packages/pnpm/test/outdated.ts b/packages/pnpm/test/outdated.ts index 0d5a335bd8..4c6b1f12ce 100644 --- a/packages/pnpm/test/outdated.ts +++ b/packages/pnpm/test/outdated.ts @@ -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') }) diff --git a/packages/pnpm/test/recursive/outdated.ts b/packages/pnpm/test/recursive/outdated.ts index d183fe1771..db49da567a 100644 --- a/packages/pnpm/test/recursive/outdated.ts +++ b/packages/pnpm/test/recursive/outdated.ts @@ -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') } }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2339b611ad..19935f8e5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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