Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support json format output with outdated command #5582

Merged
merged 17 commits into from
Nov 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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