Skip to content

Commit

Permalink
Implemented global flags
Browse files Browse the repository at this point in the history
  • Loading branch information
Sukairo-02 committed Nov 5, 2024
1 parent 65d1432 commit 99ecf49
Show file tree
Hide file tree
Showing 5 changed files with 459 additions and 23 deletions.
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,9 +403,12 @@ run(commands, {

return false;
},
hook: (event, command) => {
if(event === 'before') console.log(`Command '${command.name}' started`)
if(event === 'after') console.log(`Command '${command.name}' succesfully finished it's work`)
globals: {
flag: boolean('gflag').description('Global flag').default(false)
},
hook: (event, command, globals) => {
if(event === 'before') console.log(`Command '${command.name}' started with flag ${globals.flag}`)
if(event === 'after') console.log(`Command '${command.name}' succesfully finished it's work with flag ${globals.flag}`)
}
})
```
Expand Down Expand Up @@ -445,7 +448,11 @@ Return:
`true` | `Promise<true>` if you consider event processed
`false` | `Promise<false>` to redirect event to default theme

- `hook(event: EventType, command: Command)` - function that's used to execute code before and after every command's `transform` and `handler` execution
- `globals` - global options that could be processed in `hook`
:warning: - positionals are not allowed in `globals`
:warning: - names and aliases must not overlap with options of commands

- `hook(event: EventType, command: Command, options: TypeOf<typeof globals>)` - function that's used to execute code before and after every command's `transform` and `handler` execution

### Additional functions

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@drizzle-team/brocli",
"type": "module",
"author": "Drizzle Team",
"version": "0.10.2",
"version": "0.11.0",
"description": "Modern type-safe way of building CLIs",
"license": "Apache-2.0",
"sideEffects": false,
Expand Down
180 changes: 172 additions & 8 deletions src/command-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { defaultEventHandler, type EventHandler, eventHandlerWrapper } from './e
import {
type GenericBuilderInternals,
type GenericBuilderInternalsFields,
type GenericBuilderInternalsLimited,
type OutputType,
type ProcessedBuilderConfig,
type ProcessedOptions,
Expand Down Expand Up @@ -35,14 +36,22 @@ export type CommandsInfo = Record<string, CommandInfo>;

export type EventType = 'before' | 'after';

export type BroCliConfig = {
export type BroCliConfig<
TOpts extends Record<string, GenericBuilderInternalsLimited> | undefined = undefined,
TOptsData = TOpts extends Record<string, GenericBuilderInternals> ? TypeOf<TOpts> : undefined,
> = {
name?: string;
description?: string;
argSource?: string[];
help?: string | Function;
version?: string | Function;
omitKeysOfUndefinedOptions?: boolean;
hook?: (event: EventType, command: Command) => any;
globals?: TOpts;
hook?: (
event: EventType,
command: Command,
options: TOptsData,
) => any;
theme?: EventHandler;
};

Expand Down Expand Up @@ -648,6 +657,7 @@ const parseOptions = (
omitKeysOfUndefinedOptions?: boolean,
): Record<string, OutputType> | 'help' | 'version' | undefined => {
const options = command.options;
let noOpts = !options;

const optEntries = Object.entries(options ?? {} as Exclude<typeof options, undefined>).map(
(opt) => [opt[0], opt[1].config] as [string, ProcessedBuilderConfig],
Expand Down Expand Up @@ -715,6 +725,72 @@ const parseOptions = (
});
}

return noOpts ? undefined : result;
};

const parseGlobals = (
command: Command,
globals: ProcessedOptions<Record<string, GenericBuilderInternals>> | undefined,
args: string[],
cliName: string | undefined,
cliDescription: string | undefined,
omitKeysOfUndefinedOptions?: boolean,
): Record<string, OutputType> | 'help' | 'version' | undefined => {
if (!globals) return undefined;

const optEntries = Object.entries(globals).map(
(opt) => [opt[0], opt[1].config] as [string, ProcessedBuilderConfig],
);

const result: Record<string, OutputType> = {};
const missingRequiredArr: string[][] = [];

for (let i = 0; i < args.length; ++i) {
const arg = args[i]!;
const nextArg = args[i + 1];

const {
data,
name,
option,
skipNext,
isHelp,
isVersion,
} = parseArg(command, optEntries, [], arg, nextArg, cliName, cliDescription);
if (skipNext) ++i;

if (isHelp) return 'help';
if (isVersion) return 'version';
if (!option) continue;
delete args[i];
if (skipNext) delete args[i - 1];

result[name!] = data;
}

for (const [optKey, option] of optEntries) {
const data = result[optKey] ?? option.default;

if (!omitKeysOfUndefinedOptions) {
result[optKey] = data;
} else {
if (data !== undefined) result[optKey] = data;
}

if (option.isRequired && result[optKey] === undefined) missingRequiredArr.push([option.name!, ...option.aliases]);
}

if (missingRequiredArr.length) {
throw new BroCliError(undefined, {
type: 'error',
violation: 'missing_args_error',
name: cliName,
description: cliDescription,
command,
missing: missingRequiredArr as [string[], ...string[][]],
});
}

return Object.keys(result).length ? result : undefined;
};

Expand Down Expand Up @@ -765,6 +841,62 @@ const validateCommands = (commands: Command[], parent?: Command) => {
return commands;
};

const validateGlobalsInner = (
commands: Command[],
globals: GenericBuilderInternalsFields[],
) => {
for (const c of commands) {
const { options } = c;
if (!options) continue;

for (const { config: opt } of Object.values(options)) {
const foundNameOverlap = globals.find(({ config: g }) => g.name === opt.name);
if (foundNameOverlap) {
throw new BroCliError(
`Global options overlap with option '${opt.name}' of command '${getCommandNameWithParents(c)}' on name`,
);
}

let foundAliasOverlap = opt.aliases.find((a) => globals.find(({ config: g }) => g.name === a))
?? globals.find(({ config: g }) => opt.aliases.find((a) => a === g.name));
if (!foundAliasOverlap) {
for (const { config: g } of globals) {
foundAliasOverlap = g.aliases.find((gAlias) => opt.name === gAlias);

if (foundAliasOverlap) break;
}
}
if (!foundAliasOverlap) {
for (const { config: g } of globals) {
foundAliasOverlap = g.aliases.find((gAlias) => opt.aliases.find((a) => a === gAlias));

if (foundAliasOverlap) break;
}
}

if (foundAliasOverlap) {
throw new BroCliError(
`Global options overlap with option '${opt.name}' of command '${
getCommandNameWithParents(c)
}' on alias '${foundAliasOverlap}'`,
);
}
}

if (c.subcommands) validateGlobalsInner(c.subcommands, globals);
}
};

const validateGlobals = (
commands: Command[],
globals: ProcessedOptions<Record<string, GenericBuilderInternals>> | undefined,
) => {
if (!globals) return;
const globalEntries = Object.values(globals);

validateGlobalsInner(commands, globalEntries);
};

const removeByIndex = <T>(arr: T[], idx: number): T[] => [...arr.slice(0, idx), ...arr.slice(idx + 1, arr.length)];

/**
Expand All @@ -774,7 +906,12 @@ const removeByIndex = <T>(arr: T[], idx: number): T[] => [...arr.slice(0, idx),
*
* @param config - additional settings
*/
export const run = async (commands: Command[], config?: BroCliConfig): Promise<void> => {
export const run = async <
TOpts extends Record<string, GenericBuilderInternalsLimited> | undefined = undefined,
>(
commands: Command[],
config?: BroCliConfig<TOpts>,
): Promise<void> => {
const eventHandler = config?.theme
? eventHandlerWrapper(config.theme)
: defaultEventHandler;
Expand All @@ -784,9 +921,12 @@ export const run = async (commands: Command[], config?: BroCliConfig): Promise<v
const omitKeysOfUndefinedOptions = config?.omitKeysOfUndefinedOptions ?? false;
const cliName = config?.name;
const cliDescription = config?.description;
const globals = config?.globals;

try {
const processedCmds = validateCommands(commands);
const processedGlobals = globals ? validateOptions(globals) : undefined;
if (processedGlobals) validateGlobals(processedCmds, processedGlobals);

let args = argSource.slice(2, argSource.length);
if (!args.length) {
Expand All @@ -795,6 +935,7 @@ export const run = async (commands: Command[], config?: BroCliConfig): Promise<v
description: cliDescription,
name: cliName,
commands: processedCmds,
globals: processedGlobals,
});
}

Expand All @@ -812,13 +953,15 @@ export const run = async (commands: Command[], config?: BroCliConfig): Promise<v
description: cliDescription,
name: cliName,
command: command,
globals: processedGlobals,
});
} else {
return help !== undefined ? await executeOrLog(help) : await eventHandler({
type: 'global_help',
description: cliDescription,
name: cliName,
commands: processedCmds,
globals: processedGlobals,
});
}
}
Expand All @@ -840,6 +983,7 @@ export const run = async (commands: Command[], config?: BroCliConfig): Promise<v
description: cliDescription,
name: cliName,
commands: processedCmds,
globals: processedGlobals,
});
}

Expand All @@ -859,6 +1003,7 @@ export const run = async (commands: Command[], config?: BroCliConfig): Promise<v
description: cliDescription,
name: cliName,
command: helpCommand,
globals: processedGlobals,
})
: help !== undefined
? await executeOrLog(help)
Expand All @@ -867,20 +1012,38 @@ export const run = async (commands: Command[], config?: BroCliConfig): Promise<v
description: cliDescription,
name: cliName,
commands: processedCmds,
globals: processedGlobals,
});
}

const optionResult = parseOptions(command, newArgs, cliName, cliDescription, omitKeysOfUndefinedOptions);
const gOptionResult = parseGlobals(
command,
processedGlobals,
newArgs,
cliName,
cliDescription,
omitKeysOfUndefinedOptions,
);
const optionResult = gOptionResult && (gOptionResult === 'help' || gOptionResult === 'version')
? gOptionResult
: parseOptions(
command,
globals ? newArgs.filter((a) => a !== undefined) : newArgs,
cliName,
cliDescription,
omitKeysOfUndefinedOptions,
);

if (optionResult === 'help') {
if (optionResult === 'help' || gOptionResult === 'help') {
return command.help !== undefined ? await executeOrLog(command.help) : await eventHandler({
type: 'command_help',
description: cliDescription,
name: cliName,
command: command,
globals: processedGlobals,
});
}
if (optionResult === 'version') {
if (optionResult === 'version' || gOptionResult === 'version') {
return version !== undefined ? await executeOrLog(version) : await eventHandler({
type: 'version',
name: cliName,
Expand All @@ -889,16 +1052,17 @@ export const run = async (commands: Command[], config?: BroCliConfig): Promise<v
}

if (command.handler) {
if (config?.hook) await config.hook('before', command);
if (config?.hook) await config.hook('before', command, gOptionResult as any);
await command.handler(command.transform ? await command.transform(optionResult) : optionResult);
if (config?.hook) await config.hook('after', command);
if (config?.hook) await config.hook('after', command, gOptionResult as any);
return;
} else {
return command.help !== undefined ? await executeOrLog(command.help) : await eventHandler({
type: 'command_help',
description: cliDescription,
name: cliName,
command: command,
globals: processedGlobals,
});
}
} catch (e) {
Expand Down
Loading

0 comments on commit 99ecf49

Please sign in to comment.