Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: POC for allowing flexible command taxonomy #376

Merged
merged 30 commits into from
Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
91799db
feat: POC for allowing flexible command taxonomy
mdonnalley Feb 16, 2022
d0c5cd9
chore: improve implementation
mdonnalley Feb 17, 2022
62359f1
chore: fix tests
mdonnalley Feb 17, 2022
f84fb09
chore: clean up
mdonnalley Feb 17, 2022
625e336
fix: only run command_incomplete if there are matches
mdonnalley Feb 17, 2022
3e0fb4d
feat: support tax-free aliases and narrow matches based on provided f…
mdonnalley Feb 17, 2022
13f9194
chore: clean up
mdonnalley Feb 21, 2022
630d870
chore: clean up
mdonnalley Feb 21, 2022
d3aa805
chore: add some notes
mdonnalley Feb 21, 2022
e3587dc
perf: add PermutationIndex class
mdonnalley Feb 22, 2022
a1b8040
perf: improve PermutationIndex
mdonnalley Feb 22, 2022
088d07b
chore: clean up
mdonnalley Feb 23, 2022
2983e44
perf: use Maps for everything
mdonnalley Feb 23, 2022
f0d02a6
chore: clean up
mdonnalley Feb 23, 2022
ed163e9
chore: update unit tests
mdonnalley Feb 23, 2022
64a5916
chore: update error message
mdonnalley Feb 23, 2022
27121b5
fix: collateSpacedCmdIDFromArgs
mdonnalley Feb 23, 2022
f7cb70f
fix: command array
mdonnalley Feb 23, 2022
48c5072
fix: commandIDs
mdonnalley Feb 23, 2022
602a7c6
chore: remove _commands cache
mdonnalley Feb 23, 2022
2cbe865
chore: expose methods for getting defined commands
mdonnalley Feb 25, 2022
0838047
fix: dont append permutations to this.commands
mdonnalley Feb 25, 2022
c4907c8
chore: code review
mdonnalley Feb 28, 2022
0fe0916
chore: add tests
mdonnalley Feb 28, 2022
7c920de
Update test/config/util.test.ts
mdonnalley Mar 2, 2022
4fdaf3a
chore: code review
mdonnalley Mar 2, 2022
42c47e3
chore: code review
mdonnalley Mar 2, 2022
aea6b89
chore: update tests
mdonnalley Mar 2, 2022
58abdbd
feat: support topic permutations
mdonnalley Mar 9, 2022
5769410
Merge branch 'main' into mdonnalley/flexible-taxonomy-poc
mdonnalley Mar 10, 2022
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
323 changes: 235 additions & 88 deletions src/config/config.ts

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions src/config/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,10 @@ export class Plugin implements IPlugin {
this.hooks = mapValues(this.pjson.oclif.hooks || {}, i => Array.isArray(i) ? i : [i])

this.manifest = await this._manifest(Boolean(this.options.ignoreManifest), Boolean(this.options.errorOnManifestCreate))
this.commands = Object.entries(this.manifest.commands)
this.commands = Object
.entries(this.manifest.commands)
.map(([id, c]) => ({...c, pluginAlias: this.alias, pluginType: this.type, load: async () => this.findCommand(id, {must: true})}))
this.commands.sort((a, b) => a.id.localeCompare(b.id))
.sort((a, b) => a.id.localeCompare(b.id))
}

get topics(): Topic[] {
Expand Down
65 changes: 62 additions & 3 deletions src/config/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ export function compact<T>(a: (T | undefined)[]): T[] {
}

export function uniq<T>(arr: T[]): T[] {
return arr.filter((a, i) => {
return !arr.find((b, j) => j > i && b === a)
})
return [...new Set(arr)].sort()
}

function displayWarnings() {
Expand All @@ -61,3 +59,64 @@ export function Debug(...scope: string[]): (..._: any) => void {
if (d.enabled) displayWarnings()
return (...args: any[]) => d(...args)
}

// Adapted from https://github.com/angus-c/just/blob/master/packages/array-permutations/index.js
export function getPermutations(arr: string[]): Array<string[]> {
if (arr.length === 0) return []
if (arr.length === 1) return [arr]

const output = []
const partialPermutations = getPermutations(arr.slice(1))
const first = arr[0]

for (let i = 0, len = partialPermutations.length; i < len; i++) {
const partial = partialPermutations[i]

for (let j = 0, len2 = partial.length; j <= len2; j++) {
const start = partial.slice(0, j)
const end = partial.slice(j)
const merged = start.concat(first, end)

output.push(merged)
}
}

return output
}

export function getCommandIdPermutations(commandId: string): string[] {
return getPermutations(commandId.split(':')).flatMap(c => c.join(':'))
}

/**
* Return an array of ids that represent all the usable combinations that a user could enter.
*
* For example, if the command ids are:
* - foo:bar:baz
* - one:two:three
* Then the usable ids would be:
* - foo
* - foo:bar
* - foo:bar:baz
* - one
* - one:two
* - one:two:three
*
* This allows us to determine which parts of the argv array belong to the command id whenever the topicSeparator is a space.
*
* @param commandIds string[]
* @returns string[]
*/
export function collectUsableIds(commandIds: string[]): string[] {
const usuableIds: string[] = []
for (const id of commandIds) {
const parts = id.split(':')
while (parts.length > 0) {
const name = parts.join(':')
if (name) usuableIds.push(name)
parts.pop()
}
}

return uniq(usuableIds).sort()
}
20 changes: 11 additions & 9 deletions src/help/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,12 @@ import {error} from '../errors'
import CommandHelp from './command'
import RootHelp from './root'
import {compact, sortBy, uniqBy} from '../util'
import {standardizeIDFromArgv} from './util'
import {getHelpFlagAdditions, standardizeIDFromArgv} from './util'
import {HelpFormatter} from './formatter'
import {Plugin} from '../config/plugin'
import {toCached} from '../config/config'
export {CommandHelp} from './command'
export {standardizeIDFromArgv, loadHelpClass} from './util'

const helpFlags = ['--help']

export function getHelpFlagAdditions(config: Interfaces.Config): string[] {
const additionalHelpFlags = config.pjson.oclif.additionalHelpFlags ?? []
return [...new Set([...helpFlags, ...additionalHelpFlags]).values()]
}
export {standardizeIDFromArgv, loadHelpClass, getHelpFlagAdditions} from './util'

function getHelpSubject(args: string[], config: Interfaces.Config): string | undefined {
// for each help flag that starts with '--' create a new flag with same name sans '--'
Expand Down Expand Up @@ -91,6 +84,7 @@ export class Help extends HelpBase {
}

public async showHelp(argv: string[]) {
const originalArgv = argv.slice(1)
argv = argv.filter(arg => !getHelpFlagAdditions(this.config).includes(arg))

if (this.config.topicSeparator !== ':') argv = standardizeIDFromArgv(argv, this.config)
Expand Down Expand Up @@ -123,6 +117,14 @@ export class Help extends HelpBase {
return
}

if (this.config.flexibleTaxonomy) {
const matches = this.config.findMatches(subject, originalArgv)
if (matches.length > 0) {
const result = await this.config.runHook('command_incomplete', {id: subject, argv: originalArgv, matches})
RodEsp marked this conversation as resolved.
Show resolved Hide resolved
if (result.successes.length > 0) return
}
}

error(`Command ${subject} not found.`)
}

Expand Down
23 changes: 13 additions & 10 deletions src/help/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as ejs from 'ejs'
import {Config as IConfig, HelpOptions} from '../interfaces'
import {Help, HelpBase} from '.'
import ModuleLoader from '../module-loader'
import {collectUsableIds} from '../config/util'

interface HelpBaseDerived {
new(config: IConfig, opts?: Partial<HelpOptions>): HelpBase;
Expand Down Expand Up @@ -38,24 +39,20 @@ export function template(context: any): (t: string) => string {
function collateSpacedCmdIDFromArgs(argv: string[], config: IConfig): string[] {
if (argv.length === 1) return argv

const ids = new Set(config.commandIDs.concat(config.topics.map(t => t.name)))

const ids = collectUsableIds(config.commandIDs)
const findId = (argv: string[]): string | undefined => {
const final: string[] = []
const idPresent = (id: string) => ids.has(id)
const idPresent = (id: string) => ids.includes(id)
const isFlag = (s: string) => s.startsWith('-')
const isArgWithValue = (s: string) => s.includes('=')
const finalizeId = (s?: string) => s ? [...final, s].join(':') : final.join(':')

const hasSubCommandsWithArgs = () => {
const id = finalizeId()
/**
* Get a list of sub commands for the current command id. A command is returned as a subcommand under either
* of these conditions:
* 1. the `id` start with the current command id.
* 2. any of the aliases start with the current command id.
*/
const subCommands = config.commands.filter(c => (c.id).startsWith(id) || c.aliases.some(a => a.startsWith(id)))
if (!id) return false
// Get a list of sub commands for the current command id. A command is returned as a subcommand if the `id` starts with the current command id.
RodEsp marked this conversation as resolved.
Show resolved Hide resolved
// e.g. `foo:bar` is a subcommand of `foo`
const subCommands = config.commands.filter(c => (c.id).startsWith(id))
return Boolean(subCommands.find(cmd => cmd.strict === false || cmd.args?.length > 0))
}

Expand Down Expand Up @@ -95,3 +92,9 @@ export function standardizeIDFromArgv(argv: string[], config: IConfig): string[]
else if (config.topicSeparator !== ':') argv[0] = toStandardizedId(argv[0], config)
return argv
}

export function getHelpFlagAdditions(config: IConfig): string[] {
const helpFlags = ['--help']
const additionalHelpFlags = config.pjson.oclif.additionalHelpFlags ?? []
return [...new Set([...helpFlags, ...additionalHelpFlags]).values()]
}
4 changes: 4 additions & 0 deletions src/interfaces/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export interface Config {
plugins: Plugin[];
binPath?: string;
valid: boolean;
flexibleTaxonomy?: boolean;
topicSeparator: ':' | ' ';
readonly commands: Command.Plugin[];
readonly topics: Topic[];
Expand All @@ -96,10 +97,13 @@ export interface Config {
runCommand<T = unknown>(id: string, argv?: string[]): Promise<T>;
runCommand<T = unknown>(id: string, argv?: string[], cachedCommand?: Command.Plugin): Promise<T>;
runHook<T extends keyof Hooks>(event: T, opts: Hooks[T]['options'], timeout?: number): Promise<Hook.Result<Hooks[T]['return']>>;
getAllCommandIDs(): string[]
getAllCommands(): Command.Plugin[]
findCommand(id: string, opts: { must: true }): Command.Plugin;
findCommand(id: string, opts?: { must: boolean }): Command.Plugin | undefined;
findTopic(id: string, opts: { must: true }): Topic;
findTopic(id: string, opts?: { must: boolean }): Topic | undefined;
findMatches(id: string, argv: string[]): Command.Plugin[];
scopedEnvVar(key: string): string | undefined;
scopedEnvVarKey(key: string): string;
scopedEnvVarTrue(key: string): boolean;
Expand Down
5 changes: 5 additions & 0 deletions src/interfaces/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export interface Hooks {
options: {id: string; argv?: string[]};
return: unknown;
};
'command_incomplete': {
options: {id: string; argv: string[], matches: Command.Plugin[]};
return: unknown;
};
'plugins:preinstall': {
options: {
plugin: { name: string; tag: string; type: 'npm' } | { url: string; type: 'repo' };
Expand All @@ -55,6 +59,7 @@ export namespace Hook {
export type Preupdate = Hook<'preupdate'>
export type Update = Hook<'update'>
export type CommandNotFound = Hook<'command_not_found'>
export type CommandIncomplete = Hook<'command_incomplete'>

export interface Context {
config: Config;
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/pjson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export namespace PJSON {
schema?: number;
description?: string;
topicSeparator?: ':' | ' ';
flexibleTaxonomy?: boolean;
hooks?: { [name: string]: (string | string[]) };
commands?: string;
default?: string;
Expand Down Expand Up @@ -77,6 +78,7 @@ export namespace PJSON {
npmRegistry?: string;
scope?: string;
dirname?: string;
flexibleTaxonomy?: boolean;
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export async function run(argv = process.argv.slice(2), options?: Interfaces.Loa
// find & run command
const cmd = config.findCommand(id)
if (!cmd) {
const topic = config.findTopic(id)
const topic = config.flexibleTaxonomy ? null : config.findTopic(id)
if (topic) return config.runCommand('help', [id])
RodEsp marked this conversation as resolved.
Show resolved Hide resolved
if (config.pjson.oclif.default) {
id = config.pjson.oclif.default
Expand Down
Loading