diff --git a/src/flags.ts b/src/flags.ts index 47345957c..fdfd0ac94 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -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 extends Array ? never : T /** @@ -156,7 +157,9 @@ export const version = (opts: Partial> = {}): BooleanFlag { if (argv.length === 0 && !config.isSingleCommandCLI) return true @@ -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 } diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 000000000..617e97773 --- /dev/null +++ b/src/version.ts @@ -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 +} + +export class Version extends VersionBase { + public async showVersion(): Promise { + 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 { + 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 +} diff --git a/test/version/_test-version-class-identifier.ts b/test/version/_test-version-class-identifier.ts new file mode 100644 index 000000000..b5b1fb0f4 --- /dev/null +++ b/test/version/_test-version-class-identifier.ts @@ -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 { + console.log('custom version output from named export') + } +} diff --git a/test/version/_test-version-class.ts b/test/version/_test-version-class.ts new file mode 100644 index 000000000..fd0165ca4 --- /dev/null +++ b/test/version/_test-version-class.ts @@ -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 { + console.log('custom version output') + } +} diff --git a/test/version/version.test.ts b/test/version/version.test.ts new file mode 100644 index 000000000..3c7d632fc --- /dev/null +++ b/test/version/version.test.ts @@ -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:', + ) + }) + }) +})