mirror of
https://github.com/pnpm/pnpm.git
synced 2026-02-02 19:22:52 -05:00
feat(outdated): output outdated packages as JSON string with --format=json (#5582)
Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
12
.changeset/empty-boats-jog.md
Normal file
12
.changeset/empty-boats-jog.md
Normal file
@@ -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
.changeset/unlucky-pigs-bow.md
Normal file
5
.changeset/unlucky-pigs-bow.md
Normal file
@@ -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.
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
@@ -38,7 +40,7 @@ export function rcOptionsTypes () {
|
||||
'production',
|
||||
], allTypes),
|
||||
compatible: Boolean,
|
||||
table: Boolean,
|
||||
format: ['table', 'list', 'json'],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
@@ -120,7 +125,7 @@ export type OutdatedCommandOptions = {
|
||||
compatible?: boolean
|
||||
long?: boolean
|
||||
recursive?: boolean
|
||||
table?: boolean
|
||||
format?: 'table' | 'list' | 'json'
|
||||
} & Pick<Config,
|
||||
| 'allProjects'
|
||||
| 'ca'
|
||||
@@ -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',
|
||||
@@ -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))}
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TABLE_OPTIONS } from '@pnpm/cli-utils'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import {
|
||||
outdatedDepsOfProjects,
|
||||
OutdatedPackage,
|
||||
@@ -15,6 +16,7 @@ import sortWith from 'ramda/src/sortWith'
|
||||
import {
|
||||
getCellWidth,
|
||||
OutdatedCommandOptions,
|
||||
OutdatedPackageJSONOutput,
|
||||
renderCurrent,
|
||||
renderDetails,
|
||||
renderLatest,
|
||||
@@ -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',
|
||||
@@ -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))}
|
||||
@@ -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)
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import path from 'path'
|
||||
import { readProjects } from '@pnpm/filter-workspace-packages'
|
||||
import { install } from '@pnpm/plugin-commands-installation'
|
||||
import { outdated } from '@pnpm/plugin-commands-outdated'
|
||||
@@ -123,9 +124,9 @@ test('pnpm recursive outdated', async () => {
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
dir: process.cwd(),
|
||||
format: 'list',
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
table: false,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(1)
|
||||
@@ -153,10 +154,51 @@ Dependent: project-2
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
format: 'json',
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(1)
|
||||
expect(stripAnsi(output as unknown as string)).toBe(JSON.stringify({
|
||||
'is-negative': {
|
||||
current: '1.0.0',
|
||||
latest: '2.1.0',
|
||||
wanted: '1.0.0',
|
||||
isDeprecated: false,
|
||||
dependencyType: 'devDependencies',
|
||||
dependentPackages: [
|
||||
{
|
||||
name: 'project-3',
|
||||
location: path.resolve('project-3'),
|
||||
},
|
||||
],
|
||||
},
|
||||
'is-positive': {
|
||||
current: '2.0.0',
|
||||
latest: '3.1.0',
|
||||
wanted: '2.0.0',
|
||||
isDeprecated: false,
|
||||
dependencyType: 'dependencies',
|
||||
dependentPackages: [
|
||||
{
|
||||
name: 'project-2',
|
||||
location: path.resolve('project-2'),
|
||||
},
|
||||
],
|
||||
},
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
{
|
||||
const { output, exitCode } = await outdated.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
dir: process.cwd(),
|
||||
format: 'list',
|
||||
long: true,
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
table: false,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(1)
|
||||
@@ -205,6 +247,36 @@ https://github.com/kevva/is-positive#readme
|
||||
}
|
||||
})
|
||||
|
||||
test('pnpm recursive outdated: format json when there are no outdated dependencies', async () => {
|
||||
preparePackages([
|
||||
{
|
||||
name: 'project-1',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'project-2',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'project-3',
|
||||
version: '1.0.0',
|
||||
},
|
||||
])
|
||||
|
||||
const { allProjects, selectedProjectsGraph } = await readProjects(process.cwd(), [])
|
||||
const { output, exitCode } = await outdated.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
dir: process.cwd(),
|
||||
format: 'json',
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(0)
|
||||
expect(stripAnsi(output)).toBe('{}')
|
||||
})
|
||||
|
||||
test('pnpm recursive outdated in workspace with shared lockfile', async () => {
|
||||
preparePackages([
|
||||
{
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
{
|
||||
"path": "../../privatePackages/prepare"
|
||||
},
|
||||
{
|
||||
"path": "../../privatePackages/test-fixtures"
|
||||
},
|
||||
{
|
||||
"path": "../cli-utils"
|
||||
},
|
||||
|
||||
1213
pnpm-lock.yaml
generated
1213
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user