Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {CLIError} from './errors'
import {loadHelpClass} from './help'
import {BooleanFlag, CustomOptions, FlagDefinition, OptionFlag} from './interfaces'
import {dirExists, fileExists} from './util/fs'
import {loadVersionClass} from './version'

type NotArray<T> = T extends Array<any> ? never : T
/**
Expand Down Expand Up @@ -156,7 +157,9 @@ export const version = (opts: Partial<BooleanFlag<boolean>> = {}): BooleanFlag<v
description: 'Show CLI version.',
...opts,
async parse(_, ctx) {
ctx.log(ctx.config.userAgent)
const VersionClass = await loadVersionClass(ctx.config)
const versionInstance = new VersionClass(ctx.config)
await versionInstance.showVersion()
ctx.exit(0)
},
})
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ export {Performance} from './performance'
export {type Settings, settings} from './settings'
export {toConfiguredId, toStandardizedId} from './util/ids'
export {ux} from './ux'
export {loadVersionClass, Version, VersionBase} from './version'
17 changes: 13 additions & 4 deletions src/interfaces/pjson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,22 @@ export type HookOptions = {
identifier: string
}

export type HelpLocationOptions = {
export type ClassLocationOptions = {
/**
* The file path containing help class.
* The file path containing the class.
*/
target: string
/**
* The name of the export to use when loading the help class from the `target` file. Defaults to `default`.
* The name of the export to use when loading the class from the `target` file. Defaults to `default`.
*/
identifier: string
}

/**
* @deprecated Use {@link ClassLocationOptions} instead.
*/
export type HelpLocationOptions = ClassLocationOptions

export type S3Templates = {
baseDir?: string
manifest?: string
Expand Down Expand Up @@ -175,7 +180,11 @@ export type OclifConfiguration = {
/**
* The location of your custom help class.
*/
helpClass?: string | HelpLocationOptions
helpClass?: string | ClassLocationOptions
/**
* The location of your custom version class.
*/
versionClass?: string | ClassLocationOptions
/**
* Options for the help output.
*/
Expand Down
6 changes: 4 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as Interfaces from './interfaces'
import {getLogger, setLogger} from './logger'
import {OCLIF_MARKER_OWNER, Performance} from './performance'
import {SINGLE_COMMAND_CLI_SYMBOL} from './symbols'
import {ux} from './ux'
import {loadVersionClass} from './version'

export const helpAddition = (argv: string[], config: Interfaces.Config): boolean => {
if (argv.length === 0 && !config.isSingleCommandCLI) return true
Expand Down Expand Up @@ -74,7 +74,9 @@ export async function run(argv?: string[], options?: Interfaces.LoadOptions): Pr

// display version if applicable
if (versionAddition(argv, config)) {
ux.stdout(config.userAgent)
const VersionClass = await loadVersionClass(config)
const version = new VersionClass(config)
await version.showVersion()
await runFinally()
return
}
Expand Down
50 changes: 50 additions & 0 deletions src/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {tsPath} from './config/ts-path'
import * as Interfaces from './interfaces'
import {ClassLocationOptions} from './interfaces/pjson'
import {load} from './module-loader'
import {ux} from './ux'

export abstract class VersionBase {
constructor(protected config: Interfaces.Config) {}

/**
* Show the CLI version information.
*/
public abstract showVersion(): Promise<void>
}

export class Version extends VersionBase {
public async showVersion(): Promise<void> {
ux.stdout(this.config.userAgent)
}
}

interface VersionBaseDerived {
new (config: Interfaces.Config): VersionBase
}

function extractClass(exported: any): VersionBaseDerived {
return exported && exported.default ? exported.default : exported
}

function determineLocation(versionClass: string | ClassLocationOptions): ClassLocationOptions {
if (typeof versionClass === 'string') return {identifier: 'default', target: versionClass}
if (!versionClass.identifier) return {...versionClass, identifier: 'default'}
return versionClass
}

export async function loadVersionClass(config: Interfaces.Config): Promise<VersionBaseDerived> {
if (config.pjson.oclif?.versionClass) {
const {identifier, target} = determineLocation(config.pjson.oclif?.versionClass)
try {
const path = (await tsPath(config.root, target)) ?? target
const module = await load(config, path)
const versionClass = module[identifier] ?? (identifier === 'default' ? extractClass(module) : undefined)
return extractClass(versionClass)
} catch (error: any) {
throw new Error(`Unable to load configured version class "${target}", failed with message:\n${error.message}`)
}
}

return Version
}
12 changes: 12 additions & 0 deletions test/version/_test-version-class-identifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// `loadVersionClass` tests require an oclif project for testing so
// it is re-using the setup here to be able to do a lookup for
// this sample version class file in tests, although it is not needed
// for ../version itself.

import {VersionBase} from '../../src'

export class MyVersion extends VersionBase {
async showVersion(): Promise<void> {
console.log('custom version output from named export')
}
}
12 changes: 12 additions & 0 deletions test/version/_test-version-class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// `loadVersionClass` tests require an oclif project for testing so
// it is re-using the setup here to be able to do a lookup for
// this sample version class file in tests, although it is not needed
// for ../version itself.

import {VersionBase} from '../../src'

export default class CustomVersion extends VersionBase {
async showVersion(): Promise<void> {
console.log('custom version output')
}
}
63 changes: 63 additions & 0 deletions test/version/version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {expect} from 'chai'
import {resolve} from 'node:path'

import {Config} from '../../src'
import {loadVersionClass} from '../../src/version'
import configuredVersionClass from './_test-version-class'
import {MyVersion} from './_test-version-class-identifier'

describe('loadVersionClass', () => {
let config: Config

beforeEach(async () => {
config = await Config.load()
})

it('defaults to the native version class', async () => {
delete config.pjson.oclif.versionClass

const versionClass = await loadVersionClass(config)
expect(versionClass).not.be.undefined
expect(versionClass.prototype.showVersion)
})

it('loads version class defined in pjson.oclif.versionClass', async () => {
config.pjson.oclif.versionClass = '../test/version/_test-version-class'
config.root = resolve(__dirname, '..')

expect(configuredVersionClass).to.not.be.undefined
expect(await loadVersionClass(config)).to.deep.equal(configuredVersionClass)
})

it('loads version class defined using target but no identifier', async () => {
config.pjson.oclif.versionClass = {
target: '../test/version/_test-version-class',
// @ts-expect-error for testing purposes
identifier: undefined,
}
config.root = resolve(__dirname, '..')

expect(configuredVersionClass).to.not.be.undefined
expect(await loadVersionClass(config)).to.deep.equal(configuredVersionClass)
})

it('loads version class defined using target and identifier', async () => {
config.pjson.oclif.versionClass = {
target: '../test/version/_test-version-class-identifier',
identifier: 'MyVersion',
}
config.root = resolve(__dirname, '..')

expect(MyVersion).to.not.be.undefined
expect(await loadVersionClass(config)).to.deep.equal(MyVersion)
})

describe('error cases', () => {
it('throws an error when failing to load the version class defined in pjson.oclif.versionClass', async () => {
config.pjson.oclif.versionClass = './lib/does-not-exist-version-class'
await expect(loadVersionClass(config)).to.be.rejectedWith(
'Unable to load configured version class "./lib/does-not-exist-version-class", failed with message:',
)
})
})
})