Skip to content

Commit

Permalink
feat: show publish date on --verbose (#425)
Browse files Browse the repository at this point in the history
* feat: show publish date on --verbose

* test: strip ansi for tests

* fix: use right version for looking up publish date
  • Loading branch information
mdonnalley authored May 8, 2024
1 parent f808607 commit f608c37
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 44 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"author": "Salesforce",
"bugs": "https://github.com/oclif/plugin-version/issues",
"dependencies": {
"@oclif/core": "^3.26.5"
"@oclif/core": "^3.26.5",
"ansis": "^3.2.0"
},
"devDependencies": {
"@commitlint/config-conventional": "^18",
Expand Down
132 changes: 102 additions & 30 deletions src/commands/version.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,87 @@
import {Command, Flags, Interfaces} from '@oclif/core'
import {Ansis} from 'ansis'
import {exec} from 'node:child_process'
import {EOL} from 'node:os'

const ansis = new Ansis()

export type VersionDetail = {
pluginVersions?: string[]
} & Omit<Interfaces.VersionDetails, 'pluginVersions'>

type NpmDetails = {
date: string
'dist-tags': Record<string, string>
name: string
time: Record<string, string>
version: string
versions: string[]
}

async function getNpmDetails(pkg: string): Promise<NpmDetails | false> {
return new Promise((resolve) => {
exec(`npm view ${pkg} --json`, (error, stdout) => {
if (error) {
resolve(false)
} else {
resolve(JSON.parse(stdout) as NpmDetails)
}
})
})
}

function daysAgo(date: string): number {
const now = new Date()
const then = new Date(date)
const diff = now.getTime() - then.getTime()
return Math.floor(diff / (1000 * 60 * 60 * 24))
}

function humanReadableDate(date: string): string {
return new Date(date).toDateString()
}

async function formatPlugins(
config: Interfaces.Config,
plugins: Record<string, Interfaces.PluginVersionDetail>,
): Promise<string[]> {
const sorted = Object.entries(plugins)
.map(([name, plugin]) => ({name, ...plugin}))
.sort((a, b) => (a.name > b.name ? 1 : -1))

return Promise.all(
sorted.map(async (plugin) => {
const base =
`${getFriendlyName(config, plugin.name)} ${ansis.dim(plugin.version)} ${ansis.dim(`(${plugin.type})`)} ${
plugin.type === 'link' ? ansis.dim(plugin.root) : ''
}`.trim()
if (plugin.type === 'user') {
const npmDetails = await getNpmDetails(plugin.name)
const publishedString = npmDetails
? ansis.dim(
` published ${daysAgo(npmDetails.time[plugin.version])} days ago (${humanReadableDate(npmDetails.time[plugin.version])})`,
)
: ''
const notLatestWarning =
npmDetails && plugin.version !== npmDetails['dist-tags'].latest
? ansis.red(` (latest is ${npmDetails['dist-tags'].latest})`)
: ''
return `${base}${publishedString}${notLatestWarning}`
}

return base
}),
)
}

function getFriendlyName(config: Interfaces.Config, name: string): string {
const {scope} = config.pjson.oclif
if (!scope) return name
const match = name.match(`@${scope}/plugin-(.+)`)
if (!match) return name
return match[1]
}

export default class Version extends Command {
static enableJsonFlag = true

Expand All @@ -23,59 +100,54 @@ export default class Version extends Command {

let output = `${this.config.userAgent}`
if (flags.verbose) {
versionDetail.pluginVersions = this.formatPlugins(pluginVersions ?? {})
const details = await getNpmDetails(this.config.pjson.name)

const cliPublishedString = details
? ansis.dim(
` published ${daysAgo(details.time[details.version])} days ago (${humanReadableDate(details.time[details.version])})`,
)
: ''
const notLatestWarning =
details && this.config.version !== details['dist-tags'].latest
? ansis.red(` (latest is ${details['dist-tags'].latest})`)
: ''
versionDetail.pluginVersions = await formatPlugins(this.config, pluginVersions ?? {})
versionDetail.shell ??= 'unknown'

output = ` CLI Version:
\t${versionDetail.cliVersion}
output = ` ${ansis.bold('CLI Version')}:
\t${versionDetail.cliVersion}${cliPublishedString}${notLatestWarning}
Architecture:
${ansis.bold('Architecture')}:
\t${versionDetail.architecture}
Node Version:
${ansis.bold('Node Version')}:
\t${versionDetail.nodeVersion}
Plugin Version:
\t${flags.verbose ? (versionDetail.pluginVersions ?? []).join(EOL + '\t') : ''}
${ansis.bold('Plugin Version')}:
\t${(versionDetail.pluginVersions ?? []).join(EOL + '\t')}
OS and Version:
${ansis.bold('OS and Version')}:
\t${versionDetail.osVersion}
Shell:
${ansis.bold('Shell')}:
\t${versionDetail.shell}
Root Path:
${ansis.bold('Root Path')}:
\t${versionDetail.rootPath}
`
}

this.log(output)

return flags.verbose
? versionDetail
? {
...versionDetail,
pluginVersions: versionDetail.pluginVersions?.map((plugin) => ansis.strip(plugin)),
}
: {
architecture: versionDetail.architecture,
cliVersion: versionDetail.cliVersion,
nodeVersion: versionDetail.nodeVersion,
}
}

private formatPlugins(plugins: Record<string, Interfaces.PluginVersionDetail>): string[] {
return Object.entries(plugins)
.map(([name, plugin]) => ({name, ...plugin}))
.sort((a, b) => (a.name > b.name ? 1 : -1))
.map((plugin) =>
`${this.getFriendlyName(plugin.name)} ${plugin.version} (${plugin.type}) ${
plugin.type === 'link' ? plugin.root : ''
}`.trim(),
)
}

private getFriendlyName(name: string): string {
const {scope} = this.config.pjson.oclif
if (!scope) return name
const match = name.match(`@${scope}/plugin-(.+)`)
if (!match) return name
return match[1]
}
}
30 changes: 17 additions & 13 deletions test/commands/version.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {expect, test} from '@oclif/test'
import {Ansis} from 'ansis'
import {readFileSync} from 'node:fs'
import {release as osRelease, type as osType, userInfo as osUserInfo} from 'node:os'
import {resolve, sep} from 'node:path'

const ansis = new Ansis()

const pjson = JSON.parse(readFileSync(resolve('package.json'), 'utf8'))

const getShell = () => osUserInfo().shell?.split(sep)?.pop() || 'unknown'
Expand All @@ -22,19 +25,20 @@ describe('version', () => {
.stdout()
.command(['version', '--verbose'])
.end('runs version --verbose', (output) => {
expect(output.stdout).to.contain(' CLI Version:')
expect(output.stdout).to.contain(`\t@oclif/plugin-version/${pjson.version}`)
expect(output.stdout).to.contain(' Architecture:')
expect(output.stdout).to.contain(`\t${process.platform}-${process.arch}`)
expect(output.stdout).to.contain(' Node Version:')
expect(output.stdout).to.contain(`\tnode-${process.version}`)
expect(output.stdout).to.contain(' Plugin Version:')
expect(output.stdout).to.contain(' OS and Version:')
expect(output.stdout).to.contain(`\t${osType()} ${osRelease()}`)
expect(output.stdout).to.contain(' Shell:')
expect(output.stdout).to.contain(`\t${getShell()}`)
expect(output.stdout).to.contain(' Root Path:')
expect(output.stdout).to.contain(`\t${process.cwd()}`)
const stdout = ansis.strip(output.stdout)
expect(stdout).to.contain(' CLI Version:')
expect(stdout).to.contain(`\t@oclif/plugin-version/${pjson.version}`)
expect(stdout).to.contain(' Architecture:')
expect(stdout).to.contain(`\t${process.platform}-${process.arch}`)
expect(stdout).to.contain(' Node Version:')
expect(stdout).to.contain(`\tnode-${process.version}`)
expect(stdout).to.contain(' Plugin Version:')
expect(stdout).to.contain(' OS and Version:')
expect(stdout).to.contain(`\t${osType()} ${osRelease()}`)
expect(stdout).to.contain(' Shell:')
expect(stdout).to.contain(`\t${getShell()}`)
expect(stdout).to.contain(' Root Path:')
expect(stdout).to.contain(`\t${process.cwd()}`)
})

test
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2431,6 +2431,11 @@ ansicolors@~0.3.2:
resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979"
integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==

ansis@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.2.0.tgz#0e050c5be94784f32ffdac4b84fccba064aeae4b"
integrity sha512-Yk3BkHH9U7oPyCN3gL5Tc7CpahG/+UFv/6UG03C311Vy9lzRmA5uoxDTpU9CO3rGHL6KzJz/pdDeXZCZ5Mu/Sg==

anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
Expand Down

0 comments on commit f608c37

Please sign in to comment.