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

Override yargs help output with custom help renderer #229

Merged
merged 11 commits into from
Apr 18, 2018
Merged
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
226 changes: 124 additions & 102 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
"globby": "^6.0.0",
"inquirer": "^4.0.2",
"pkg-dir": "^2.0.0",
"slice-ansi": "^1.0.0",
"string-width": "^2.1.1",
"tslib": "~1.8.1",
"typings-core": "^2.3.3",
"update-notifier": "^2.3.0",
Expand Down Expand Up @@ -75,10 +77,10 @@
"grunt-tslint": "5.0.1",
"husky": "0.14.3",
"intern": "~4.1.0",
"mockery": "^2.1.0",
"sinon": "^4.1.3",
"lint-staged": "6.0.0",
"mockery": "^2.1.0",
"prettier": "1.9.2",
"sinon": "^4.1.3",
"tslint": "5.2.0",
"typescript": "~2.6.1"
},
Expand Down
35 changes: 19 additions & 16 deletions src/CommandHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ import * as yargs from 'yargs';
import { ConfigurationHelperFactory } from './configurationHelper';
import HelperFactory from './Helper';
import template from './template';
import { CommandHelper, Command, CommandsMap } from './interfaces';

function getCommand(commandsMap: CommandsMap, group: string, commandName?: string): Command | undefined {
const commandKey = commandName ? `${group}-${commandName}` : group;
return commandsMap.get(commandKey);
}
import { CommandHelper, GroupMap } from './interfaces';
import { getCommand } from './command';

export type RenderFilesConfig = {
src: string;
Expand All @@ -19,12 +15,12 @@ export type RenderFilesConfig = {
* allowing commands to call one another. Provides 'run' and 'exists' functions
*/
export class SingleCommandHelper implements CommandHelper {
private _commandsMap: CommandsMap;
private _groupMap: GroupMap;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funny that we're renaming these to "commands" in the README and changing them FROM commands in the code 😂

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it’s because we already have a commands map that is the value of the group map. Traditionally it’s easier to understand these things as different (as they really are) when it comes to code. Open to other name suggestions though.

private _configurationFactory: ConfigurationHelperFactory;
private _context: any;

constructor(commandsMap: CommandsMap, context: any, configurationHelperFactory: ConfigurationHelperFactory) {
this._commandsMap = commandsMap;
constructor(commandsMap: GroupMap, context: any, configurationHelperFactory: ConfigurationHelperFactory) {
this._groupMap = commandsMap;
this._context = context;
this._configurationFactory = configurationHelperFactory;
}
Expand All @@ -36,18 +32,25 @@ export class SingleCommandHelper implements CommandHelper {
}

run(group: string, commandName?: string, args?: yargs.Argv): Promise<any> {
const command = getCommand(this._commandsMap, group, commandName);
if (command) {
const helper = new HelperFactory(this, yargs, this._context, this._configurationFactory);
return command.run(helper.sandbox(group, command.name), args);
} else {
try {
const command = getCommand(this._groupMap, group, commandName);
if (command) {
const helper = new HelperFactory(this, yargs, this._context, this._configurationFactory);
return command.run(helper.sandbox(group, command.name), args);
} else {
return Promise.reject(new Error('The command does not exist'));
}
} catch {
return Promise.reject(new Error('The command does not exist'));
}
}

exists(group: string, commandName?: string) {
const command = getCommand(this._commandsMap, group, commandName);
return !!command;
try {
return !!getCommand(this._groupMap, group, commandName);
} catch {
return false;
}
}
}

Expand Down
33 changes: 5 additions & 28 deletions src/allCommands.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,23 @@
import { loadCommands, enumerateInstalledCommands, enumerateBuiltInCommands } from './loadCommands';
import { LoadedCommands } from './interfaces';
import { GroupMap } from './interfaces';
import { initCommandLoader, createBuiltInCommandLoader } from './command';
import config from './config';

const commands: LoadedCommands = {
commandsMap: new Map(),
yargsCommandNames: new Map()
};

let loaded = false;

export function reset(): void {
commands.commandsMap = new Map();
commands.yargsCommandNames = new Map();
loaded = false;
}

export async function loadExternalCommands(): Promise<LoadedCommands> {
export async function loadExternalCommands(): Promise<GroupMap> {
const installedCommandLoader = initCommandLoader(config.searchPrefixes);
const installedCommandsPaths = await enumerateInstalledCommands(config);
return await loadCommands(installedCommandsPaths, installedCommandLoader);
}

export async function loadBuiltInCommands(): Promise<LoadedCommands> {
export async function loadBuiltInCommands(): Promise<GroupMap> {
const builtInCommandLoader = createBuiltInCommandLoader();
const builtInCommandsPaths = await enumerateBuiltInCommands(config);
return await loadCommands(builtInCommandsPaths, builtInCommandLoader);
}

export default async function loadAllCommands(): Promise<LoadedCommands> {
if (loaded) {
return Promise.resolve(commands);
}

export default async function loadAllCommands(): Promise<GroupMap> {
const builtInCommands = await loadBuiltInCommands();
const installedCommands = await loadExternalCommands();

commands.commandsMap = new Map([...installedCommands.commandsMap, ...builtInCommands.commandsMap]);
commands.yargsCommandNames = new Map([
...installedCommands.yargsCommandNames,
...builtInCommands.yargsCommandNames
]);
loaded = true;
return Promise.resolve(commands);
return Promise.resolve(new Map([...installedCommands, ...builtInCommands]));
}
44 changes: 21 additions & 23 deletions src/command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Command, CommandWrapper, CommandsMap } from './interfaces';
const cliui = require('cliui');
import { Command, CommandWrapper, GroupMap } from './interfaces';

/**
* Function to create a loader instance, this allows the config to be injected
Expand All @@ -16,7 +15,7 @@ export function initCommandLoader(searchPrefixes: string[]): (path: string) => C

try {
const command = convertModuleToCommand(module);
const { description, register, run, alias, eject } = command;
const { description, register, run, alias, eject, global = false } = command;
// derive the group and name from the module directory name, e.g. dojo-cli-group-name
const [, group, name] = <string[]>commandRegExp.exec(path);

Expand All @@ -26,6 +25,8 @@ export function initCommandLoader(searchPrefixes: string[]): (path: string) => C
alias,
description,
register,
installed: true,
global: group === 'create' && name === 'app' ? true : global,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should move this into a utility or something so that the next time we need to force a global like this we don't have to modify the logic in here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a totally temporary thing, this PR introduces an optional global property on the API that commands can mark whether that are global and then we can remove this logic completely

run,
path,
eject
Expand All @@ -46,12 +47,14 @@ export function createBuiltInCommandLoader(): (path: string) => CommandWrapper {
try {
const command = convertModuleToCommand(module);
// derive the name and group of the built in commands from the command itself (these are optional props)
const { name = '', group = '', alias, description, register, run } = command;
const { name = '', group = '', alias, description, register, run, global = false } = command;

return {
name,
group,
alias,
global,
installed: true,
description,
register,
run,
Expand All @@ -75,24 +78,19 @@ export function convertModuleToCommand(module: any): Command {
}
}

export function getGroupDescription(commandNames: Set<string>, commands: CommandsMap): string {
const numCommands = commandNames.size;
if (numCommands > 1) {
return getMultiCommandDescription(commandNames, commands);
} else {
const { description } = <CommandWrapper>commands.get(Array.from(commandNames.keys())[0]);
return description;
export function getCommand(groupMap: GroupMap, groupName: string, commandName?: string) {
const commandMap = groupMap.get(groupName);
if (!commandMap) {
throw new Error(`Unable to find command group: ${groupName}`);
}
}

function getMultiCommandDescription(commandNames: Set<string>, commands: CommandsMap): string {
const descriptions = Array.from(commandNames.keys(), (commandName) => {
const { name, description } = <CommandWrapper>commands.get(commandName);
return `${name} \t${description}`;
});
const ui = cliui({
width: 80
});
ui.div(descriptions.join('\n'));
return ui.toString();
if (commandName) {
const command = commandMap.get(commandName);
if (!command) {
throw new Error(`Unable to find command: ${commandName} for group: ${groupName}`);
}
return command;
}
return [...commandMap.values()].find((wrapper) => {
return !!wrapper.default;
})!;
}
17 changes: 10 additions & 7 deletions src/commands/eject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,20 @@ async function run(helper: Helper, args: EjectArgs): Promise<any> {
throw Error('Aborting eject');
}
return loadExternalCommands().then(async (commands) => {
let toEject = new Set<EjectableCommandWrapper>();
commands.forEach((commandMap, group) => {
toEject = [...commandMap.values()].reduce((toEject, command) => {
if (isEjectableCommandWrapper(command)) {
toEject.add(command);
}
return toEject;
}, new Set<EjectableCommandWrapper>());
});
const npmPackages: NpmPackage = {
dependencies: {},
devDependencies: {}
};

const toEject = [...commands.commandsMap.values()].reduce((toEject, command) => {
if (isEjectableCommandWrapper(command)) {
toEject.add(command);
}
return toEject;
}, new Set<EjectableCommandWrapper>());

if (toEject.size) {
const allHints: string[] = [];
[...toEject].forEach((command) => {
Expand Down Expand Up @@ -106,5 +108,6 @@ export default {
group: 'eject',
description: 'disconnect your project from dojo cli commands',
register,
global: false,
run
};
15 changes: 9 additions & 6 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ async function run(helper: Helper, args: {}) {
json = JSON.parse(file);
}

const { commandsMap } = await loadExternalCommands();
const groupMap = await loadExternalCommands();
const values = [];

for (let [, value] of commandsMap.entries()) {
const name = `${value.group}-${value.name}`;
if (values.indexOf(value) === -1 && json[name] === undefined) {
json[name] = {};
values.push(value);
for (let [, commandMap] of groupMap.entries()) {
for (let [, value] of commandMap.entries()) {
const name = `${value.group}-${value.name}`;
if (values.indexOf(name) === -1 && json[name] === undefined) {
json[name] = {};
values.push(name);
}
}
}

Expand All @@ -39,5 +41,6 @@ export default {
group: 'init',
description: 'create a .dojorc file',
run,
global: false,
register: () => {}
};
26 changes: 15 additions & 11 deletions src/commands/version.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Helper, OptionsHelper, CommandsMap, NpmPackageDetails } from '../interfaces';
import { Helper, OptionsHelper, GroupMap, NpmPackageDetails } from '../interfaces';
import { join } from 'path';
import { Argv } from 'yargs';
import chalk from 'chalk';
Expand Down Expand Up @@ -154,7 +154,7 @@ function readPackageDetails(packageDir: string): PackageDetails {
* @param {CommandsMap} commandsMap
* @returns {{name, version, group}[]}
*/
function buildVersions(commandsMap: CommandsMap): ModuleVersion[] {
function buildVersions(groupMap: GroupMap): ModuleVersion[] {
/*
* commandsMap comes in as a map of [command-name, command]. The command has a default command,
* the map will actually contain two entries for one command, on for the default command, one for the real,
Expand All @@ -163,11 +163,13 @@ function buildVersions(commandsMap: CommandsMap): ModuleVersion[] {
* Loop over commandsMap and create a new map with one entry per command, then loop over each entry and extract
* the package details.
*/
const consolidatedCommands = [
...new Map<string, string>(
[...commandsMap].map(([, command]) => <[string, string]>[command.path, command.group])
)
];

const consolidatedCommands = [];
for (let [, commandMap] of groupMap.entries()) {
for (let [, value] of commandMap.entries()) {
consolidatedCommands.push([value.path, value.group]);
}
}

const versionInfo = consolidatedCommands
.map(([path, group]) => {
Expand Down Expand Up @@ -195,10 +197,10 @@ function buildVersions(commandsMap: CommandsMap): ModuleVersion[] {
* @param {boolean} checkOutdated should we check if there is a later stable version available for the command
* @returns {string} the stdout output
*/
function createVersionsString(commandsMap: CommandsMap, checkOutdated: boolean): Promise<string> {
function createVersionsString(groupMap: GroupMap, checkOutdated: boolean): Promise<string> {
const packagePath = pkgDir.sync(__dirname);
const myPackageDetails = readPackageDetails(packagePath); // fetch the cli's package details
const versions: ModuleVersion[] = buildVersions(commandsMap);
const versions: ModuleVersion[] = buildVersions(groupMap);
if (checkOutdated) {
return areCommandsOutdated(versions).then(
(commandVersions: ModuleVersion[]) => createOutput(myPackageDetails, commandVersions),
Expand All @@ -213,8 +215,8 @@ function createVersionsString(commandsMap: CommandsMap, checkOutdated: boolean):

function run(helper: Helper, args: VersionArgs): Promise<any> {
return allCommands()
.then((commands) => {
return createVersionsString(commands.commandsMap, args.outdated);
.then((groupMap) => {
return createVersionsString(groupMap, args.outdated);
})
.then(console.log);
}
Expand All @@ -224,5 +226,7 @@ export default {
group: 'version',
description: 'provides version information for all installed commands and the cli itself',
register,
global: true,
installed: true,
run
};
Loading