Skip to content

Commit

Permalink
feat(outdated): output outdated packages as JSON string with `--forma…
Browse files Browse the repository at this point in the history
…t=json` (#5582)

Co-authored-by: Zoltan Kochan <z@kochan.io>
  • Loading branch information
Shinyaigeek and zkochan committed Nov 6, 2022
1 parent a4c58d4 commit 46852d4
Show file tree
Hide file tree
Showing 9 changed files with 965 additions and 541 deletions.
12 changes: 12 additions & 0 deletions .changeset/empty-boats-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@pnpm/plugin-commands-outdated": minor
"pnpm": minor
---

Support `--format=json` option to output outdated packages in JSON format with `outdated` command [#2705](https://github.com/pnpm/pnpm/issues/2705).

```bash
pnpm outdated --format=json
#or
pnpm outdated --json
```
5 changes: 5 additions & 0 deletions .changeset/unlucky-pigs-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pnpm/plugin-commands-outdated": major
---

To select the output format, the format option should be used. The table boolean option is removed.
1 change: 1 addition & 0 deletions packages/plugin-commands-outdated/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@pnpm/plugin-commands-outdated": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "3.1.0",
"@pnpm/test-fixtures": "workspace:*",
"@types/ramda": "0.28.15",
"@types/wrap-ansi": "^3.0.0",
"@types/zkochan__table": "npm:@types/table@6.0.0"
Expand Down
65 changes: 57 additions & 8 deletions packages/plugin-commands-outdated/src/outdated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import colorizeSemverDiff from '@pnpm/colorize-semver-diff'
import { CompletionFunc } from '@pnpm/command'
import { FILTERING, OPTIONS, UNIVERSAL_OPTIONS } from '@pnpm/common-cli-options-help'
import { Config, types as allTypes } from '@pnpm/config'
import { PnpmError } from '@pnpm/error'
import {
outdatedDepsOfProjects,
OutdatedPackage,
} from '@pnpm/outdated'
import semverDiff from '@pnpm/semver-diff'
import { DependenciesField, PackageManifest } from '@pnpm/types'
import { table } from '@zkochan/table'
import chalk from 'chalk'
import pick from 'ramda/src/pick'
Expand All @@ -38,7 +40,7 @@ export function rcOptionsTypes () {
'production',
], allTypes),
compatible: Boolean,
table: Boolean,
format: ['table', 'list', 'json'],
}
}

Expand All @@ -50,6 +52,9 @@ export const cliOptionsTypes = () => ({
export const shorthands = {
D: '--dev',
P: '--production',
table: '--format=table',
'no-table': '--format=list',
json: '--format=json',
}

export const commandNames = ['outdated']
Expand Down Expand Up @@ -120,7 +125,7 @@ export type OutdatedCommandOptions = {
compatible?: boolean
long?: boolean
recursive?: boolean
table?: boolean
format?: 'table' | 'list' | 'json'
} & Pick<Config,
| 'allProjects'
| 'ca'
Expand Down Expand Up @@ -187,16 +192,32 @@ export async function handler (
timeout: opts.fetchTimeout,
})

if (outdatedPackages.length === 0) return { output: '', exitCode: 0 }

if (opts.table !== false) {
return { output: renderOutdatedTable(outdatedPackages, opts), exitCode: 1 }
} else {
return { output: renderOutdatedList(outdatedPackages, opts), exitCode: 1 }
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 }) {
if (outdatedPackages.length === 0) return ''
const columnNames = [
'Package',
'Current',
Expand Down Expand Up @@ -226,6 +247,7 @@ function renderOutdatedTable (outdatedPackages: readonly OutdatedPackage[], opts
}

function renderOutdatedList (outdatedPackages: readonly OutdatedPackage[], opts: { long?: boolean }) {
if (outdatedPackages.length === 0) return ''
return sortOutdatedPackages(outdatedPackages)
.map((outdatedPkg) => {
let info = `${chalk.bold(renderPackageName(outdatedPkg))}
Expand All @@ -244,6 +266,33 @@ ${renderCurrent(outdatedPkg)} ${chalk.grey('=>')} ${renderLatest(outdatedPkg)}`
.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 }) {
const outdatedPackagesJSON: Record<string, OutdatedPackageJSONOutput> = sortOutdatedPackages(outdatedPackages)
.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
}, {})
return JSON.stringify(outdatedPackagesJSON, null, 2)
}

function sortOutdatedPackages (outdatedPackages: readonly OutdatedPackage[]) {
return sortWith(
DEFAULT_COMPARATORS,
Expand Down
56 changes: 51 additions & 5 deletions packages/plugin-commands-outdated/src/recursive.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TABLE_OPTIONS } from '@pnpm/cli-utils'
import { PnpmError } from '@pnpm/error'
import {
outdatedDepsOfProjects,
OutdatedPackage,
Expand All @@ -15,6 +16,7 @@ import sortWith from 'ramda/src/sortWith'
import {
getCellWidth,
OutdatedCommandOptions,
OutdatedPackageJSONOutput,
renderCurrent,
renderDetails,
renderLatest,
Expand Down Expand Up @@ -74,15 +76,32 @@ export async function outdatedRecursive (
})
}

if (isEmpty(outdatedMap)) return { output: '', exitCode: 0 }

if (opts.table !== false) {
return { output: renderOutdatedTable(outdatedMap, opts), exitCode: 1 }
let output!: string
switch (opts.format ?? 'table') {
case 'table': {
output = renderOutdatedTable(outdatedMap, opts)
break
}
case 'list': {
output = renderOutdatedList(outdatedMap, opts)
break
}
case 'json': {
output = renderOutdatedJSON(outdatedMap, opts)
break
}
default: {
throw new PnpmError('BAD_OUTDATED_FORMAT', `Unsupported format: ${opts.format?.toString() ?? 'undefined'}`)
}
}
return {
output,
exitCode: isEmpty(outdatedMap) ? 0 : 1,
}
return { output: renderOutdatedList(outdatedMap, opts), exitCode: 1 }
}

function renderOutdatedTable (outdatedMap: Record<string, OutdatedInWorkspace>, opts: { long?: boolean }) {
if (isEmpty(outdatedMap)) return ''
const columnNames = [
'Package',
'Current',
Expand Down Expand Up @@ -125,6 +144,7 @@ function renderOutdatedTable (outdatedMap: Record<string, OutdatedInWorkspace>,
}

function renderOutdatedList (outdatedMap: Record<string, OutdatedInWorkspace>, opts: { long?: boolean }) {
if (isEmpty(outdatedMap)) return ''
return sortOutdatedPackages(Object.values(outdatedMap))
.map((outdatedPkg) => {
let info = `${chalk.bold(renderPackageName(outdatedPkg))}
Expand Down Expand Up @@ -153,6 +173,32 @@ ${renderCurrent(outdatedPkg)} ${chalk.grey('=>')} ${renderLatest(outdatedPkg)}`
.join('\n\n') + '\n'
}

export interface OutdatedPackageInWorkspaceJSONOutput extends OutdatedPackageJSONOutput {
dependentPackages: Array<{ name: string, location: string }>
}

function renderOutdatedJSON (
outdatedMap: Record<string, OutdatedInWorkspace>,
opts: { long?: boolean }
): string {
const outdatedPackagesJSON: Record<string, OutdatedPackageInWorkspaceJSONOutput> = sortOutdatedPackages(Object.values(outdatedMap))
.reduce((acc, outdatedPkg) => {
acc[outdatedPkg.packageName] = {
current: outdatedPkg.current,
latest: outdatedPkg.latestManifest?.version,
wanted: outdatedPkg.wanted,
isDeprecated: Boolean(outdatedPkg.latestManifest?.deprecated),
dependencyType: outdatedPkg.belongsTo,
dependentPackages: outdatedPkg.dependentPkgs.map(({ manifest, location }) => ({ name: manifest.name, location })),
}
if (opts.long) {
acc[outdatedPkg.packageName].latestManifest = outdatedPkg.latestManifest
}
return acc
}, {})
return JSON.stringify(outdatedPackagesJSON, null, 2)
}

function dependentPackages ({ dependentPkgs }: OutdatedInWorkspace) {
return dependentPkgs
.map(({ manifest, location }) => manifest.name ?? location)
Expand Down
75 changes: 65 additions & 10 deletions packages/plugin-commands-outdated/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ import { PnpmError } from '@pnpm/error'
import { outdated } from '@pnpm/plugin-commands-outdated'
import { prepare, tempDir } from '@pnpm/prepare'
import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
import { fixtures } from '@pnpm/test-fixtures'
import stripAnsi from 'strip-ansi'

const fixtures = path.join(__dirname, '../../../fixtures')
const hasOutdatedDepsFixture = path.join(fixtures, 'has-outdated-deps')
const has2OutdatedDepsFixture = path.join(fixtures, 'has-2-outdated-deps')
const hasOutdatedDepsFixtureAndExternalLockfile = path.join(fixtures, 'has-outdated-deps-and-external-shrinkwrap', 'pkg')
const hasNotOutdatedDepsFixture = path.join(fixtures, 'has-not-outdated-deps')
const hasMajorOutdatedDepsFixture = path.join(fixtures, 'has-major-outdated-deps')
const hasNoLockfileFixture = path.join(fixtures, 'has-no-lockfile')
const withPnpmUpdateIgnore = path.join(fixtures, 'with-pnpm-update-ignore')
const f = fixtures(__dirname)
const hasOutdatedDepsFixture = f.find('has-outdated-deps')
const has2OutdatedDepsFixture = f.find('has-2-outdated-deps')
const hasOutdatedDepsFixtureAndExternalLockfile = path.join(f.find('has-outdated-deps-and-external-shrinkwrap'), 'pkg')
const hasNotOutdatedDepsFixture = f.find('has-not-outdated-deps')
const hasMajorOutdatedDepsFixture = f.find('has-major-outdated-deps')
const hasNoLockfileFixture = f.find('has-no-lockfile')
const withPnpmUpdateIgnore = f.find('with-pnpm-update-ignore')

const REGISTRY_URL = `http://localhost:${REGISTRY_MOCK_PORT}`

Expand Down Expand Up @@ -148,7 +149,7 @@ test('pnpm outdated: no table', async () => {
const { output, exitCode } = await outdated.handler({
...OUTDATED_OPTIONS,
dir: process.cwd(),
table: false,
format: 'list',
})

expect(exitCode).toBe(1)
Expand All @@ -167,8 +168,8 @@ is-positive (dev)
const { output, exitCode } = await outdated.handler({
...OUTDATED_OPTIONS,
dir: process.cwd(),
format: 'list',
long: true,
table: false,
})

expect(exitCode).toBe(1)
Expand All @@ -190,6 +191,60 @@ https://github.com/kevva/is-positive#readme
}
})

test('pnpm outdated: format json', async () => {
tempDir()

await fs.mkdir(path.resolve('node_modules/.pnpm'), { recursive: true })
await fs.copyFile(path.join(hasOutdatedDepsFixture, 'node_modules/.pnpm/lock.yaml'), path.resolve('node_modules/.pnpm/lock.yaml'))
await fs.copyFile(path.join(hasOutdatedDepsFixture, 'package.json'), path.resolve('package.json'))

{
const { output, exitCode } = await outdated.handler({
...OUTDATED_OPTIONS,
dir: process.cwd(),
format: 'json',
})

expect(exitCode).toBe(1)
expect(stripAnsi(output)).toBe(JSON.stringify({
'@pnpm.e2e/deprecated': {
current: '1.0.0',
latest: '1.0.0',
wanted: '1.0.0',
isDeprecated: true,
dependencyType: 'dependencies',
},
'is-negative': {
current: '1.0.0',
latest: '2.1.0',
wanted: '1.0.0',
isDeprecated: false,
dependencyType: 'dependencies',
},
'is-positive': {
current: '1.0.0',
latest: '3.1.0',
wanted: '1.0.0',
isDeprecated: false,
dependencyType: 'devDependencies',
},
}, null, 2))
}
})

test('pnpm outdated: format json when there are no outdated dependencies', async () => {
prepare()

const { output, exitCode } = await outdated.handler({
...OUTDATED_OPTIONS,
dir: process.cwd(),
format: 'json',
})

expect(exitCode).toBe(0)
expect(stripAnsi(output)).toBe('{}')
})

test('pnpm outdated: only current lockfile is available', async () => {
tempDir()

Expand Down

0 comments on commit 46852d4

Please sign in to comment.