Skip to content

Commit

Permalink
feat: add import strategy for command discovery
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonnalley committed Feb 8, 2024
1 parent 6b9af08 commit 2ca6815
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 3 deletions.
70 changes: 67 additions & 3 deletions src/config/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {inspect} from 'node:util'
import {Command} from '../command'
import {CLIError, error} from '../errors'
import {Manifest} from '../interfaces/manifest'
import {PJSON} from '../interfaces/pjson'
import {CommandDiscovery, PJSON} from '../interfaces/pjson'
import {Plugin as IPlugin, PluginOptions} from '../interfaces/plugin'
import {Topic} from '../interfaces/topic'
import {loadWithData, loadWithDataFromManifest} from '../module-loader'
import {load, loadWithData, loadWithDataFromManifest} from '../module-loader'
import {OCLIF_MARKER_OWNER, Performance} from '../performance'
import {cacheCommand} from '../util/cache-command'
import {findRoot} from '../util/find-root'
Expand Down Expand Up @@ -56,6 +56,35 @@ function processCommandIds(files: string[]): string[] {
})
}

function determineCommandDiscoveryStrategy(
commandDiscovery: CommandDiscovery | undefined,
commands: string | undefined,
): CommandDiscovery {
if (commandDiscovery) {
if (commandDiscovery.strategy === 'import') {
if (!commandDiscovery.file)
throw new CLIError('commandDiscovery.exportName is required when using import strategy')
return commandDiscovery
}

if (commandDiscovery.strategy === 'glob') {
if (!commandDiscovery.directory) {
if (!commands) throw new CLIError('commands is required when using glob strategy')
return {directory: commands, globPatterns: GLOB_PATTERNS, strategy: 'glob'}
}

return commandDiscovery
}
}

if (commands) return {directory: commands, globPatterns: GLOB_PATTERNS, strategy: 'glob'}
throw new CLIError('either oclif.commandDiscovery or oclif.commands are required to be set in package.json')
}

type CommandExportModule = {
default: Record<string, Command.Class>
}

export class Plugin implements IPlugin {
alias!: string

Expand Down Expand Up @@ -103,6 +132,8 @@ export class Plugin implements IPlugin {
// eslint-disable-next-line new-cap
protected _debug = Debug()

private commandDiscoveryOpts: CommandDiscovery | undefined
private commandExportModule: CommandExportModule | undefined
private flexibleTaxonomy!: boolean

constructor(public options: PluginOptions) {}
Expand All @@ -122,6 +153,16 @@ export class Plugin implements IPlugin {
})

const fetch = async () => {
const commandsFromExport = await this.loadCommandsFromExport()
if (commandsFromExport) {
const cmd = commandsFromExport[id]
if (!cmd) return

cmd.id = id
cmd.plugin = this
return cmd
}

const commandsDir = await this.getCommandsDir()
if (!commandsDir) return
let module
Expand Down Expand Up @@ -183,6 +224,13 @@ export class Plugin implements IPlugin {

this.hooks = Object.fromEntries(Object.entries(this.pjson.oclif.hooks ?? {}).map(([k, v]) => [k, castArray(v)]))

this.commandDiscoveryOpts = determineCommandDiscoveryStrategy(
this.pjson.oclif?.commandDiscovery,
this.pjson.oclif?.commands,
)

this._debug('command discovery options', this.commandDiscoveryOpts)

this.manifest = await this._manifest()
this.commands = Object.entries(this.manifest.commands)
.map(([id, c]) => ({
Expand Down Expand Up @@ -284,12 +332,19 @@ export class Plugin implements IPlugin {
}

private async getCommandIDs(): Promise<string[]> {
const commandsFromExport = await this.loadCommandsFromExport()
if (commandsFromExport) {
const ids = Object.keys(commandsFromExport)
this._debug('found commands', ids)
return ids
}

const commandsDir = await this.getCommandsDir()
if (!commandsDir) return []

const marker = Performance.mark(OCLIF_MARKER_OWNER, `plugin.getCommandIDs#${this.name}`, {plugin: this.name})
this._debug(`loading IDs from ${commandsDir}`)
const files = await globby(GLOB_PATTERNS, {cwd: commandsDir})
const files = await globby(this.commandDiscoveryOpts?.globPatterns ?? GLOB_PATTERNS, {cwd: commandsDir})
const ids = processCommandIds(files)
this._debug('found commands', ids)
marker?.addDetails({count: ids.length})
Expand All @@ -303,6 +358,15 @@ export class Plugin implements IPlugin {
return this.commandsDir
}

private async loadCommandsFromExport(): Promise<Record<string, Command.Class> | undefined> {
if (this.commandDiscoveryOpts?.strategy === 'import' && this.commandDiscoveryOpts.file) {
if (this.commandExportModule) return this.commandExportModule.default
const filePath = await tsPath(this.root, this.commandDiscoveryOpts.file, this)
this.commandExportModule = await load<CommandExportModule>(this, filePath)
return this.commandExportModule?.default
}
}

private warn(err: CLIError | Error | string, scope?: string): void {
if (this.warned) return
if (typeof err === 'string') err = new Error(err)
Expand Down
43 changes: 43 additions & 0 deletions src/interfaces/pjson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,57 @@ export interface PJSON {
version: string
}

export type CommandDiscovery = {
/**
* The strategy to use for loading commands.
*
* - `glob` will use `glob` to find command files in the specified `directory`.
* - `import` will use `import` (or `require` for CJS) to load the commands from the root of the plugin.
*/
strategy: 'glob' | 'import'
/**
* The directory to use to find command files. This is only used when `strategy` is `glob`.
*/
directory?: string
/**
* The glob patterns to use to find command files. This is only used when `strategy` is `glob`.
*/
globPatterns?: string[]
/**
* The name of the file that contains the exported commands. This is only used when `strategy` is `import`.
*
* This export must be the default export and an object with keys that are the command names
* and values that are the command classes.
*
* @example
* ```typescript
* // in src/index.ts
* import Hello from './commands/hello/index.js'
* import HelloWorld from './commands/hello/world.js'
*
* export default {
* hello: Hello,
* 'hello:world': HelloWorld,
* }
* ```
*/
file?: string
}

export namespace PJSON {
export interface Plugin extends PJSON {
name: string
oclif: PJSON['oclif'] & {
additionalHelpFlags?: string[]
additionalVersionFlags?: string[]
aliases?: {[name: string]: null | string}
/**
* The directory to use to find command files.
*
* @deprecated Use `commandDiscovery` instead.
*/
commands?: string
commandDiscovery?: CommandDiscovery
default?: string
description?: string
devPlugins?: string[]
Expand Down

0 comments on commit 2ca6815

Please sign in to comment.