diff --git a/package.json b/package.json index 35de901..fcb6f2a 100755 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@drizzle-team/brocli", "type": "module", "author": "Drizzle Team", - "version": "0.2.2", + "version": "0.2.3", "description": "Typed CLI command runner", "license": "Apache-2.0", "sideEffects": false, @@ -26,6 +26,7 @@ "devDependencies": { "@arethetypeswrong/cli": "^0.15.3", "@originjs/vite-plugin-commonjs": "^1.0.3", + "@types/clone": "^2.1.4", "@types/node": "^20.12.13", "dprint": "^0.46.2", "tsup": "^8.1.0", @@ -51,5 +52,8 @@ "types": "./index.d.ts", "default": "./index.js" } + }, + "dependencies": { + "clone": "^2.1.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d43d9b..39dc250 100755 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + clone: + specifier: ^2.1.2 + version: 2.1.2 devDependencies: '@arethetypeswrong/cli': specifier: ^0.15.3 @@ -14,6 +18,9 @@ importers: '@originjs/vite-plugin-commonjs': specifier: ^1.0.3 version: 1.0.3 + '@types/clone': + specifier: ^2.1.4 + version: 2.1.4 '@types/node': specifier: ^20.12.13 version: 20.12.13 @@ -644,6 +651,9 @@ packages: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} + '@types/clone@2.1.4': + resolution: {integrity: sha512-NKRWaEGaVGVLnGLB2GazvDaZnyweW9FJLLFL5LhywGJB3aqGMT9R/EUoJoSRP4nzofYnZysuDmrEJtJdAqUOtQ==} + '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -776,6 +786,10 @@ packages: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1950,6 +1964,8 @@ snapshots: '@sindresorhus/is@4.6.0': {} + '@types/clone@2.1.4': {} + '@types/estree@1.0.5': {} '@types/fs-extra@11.0.4': @@ -2092,6 +2108,8 @@ snapshots: optionalDependencies: '@colors/colors': 1.5.0 + clone@2.1.2: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 diff --git a/src/command-core.ts b/src/command-core.ts index 9aba2d8..6cf28fd 100755 --- a/src/command-core.ts +++ b/src/command-core.ts @@ -1,3 +1,4 @@ +import clone from 'clone'; import { BroCliError } from './brocli-error'; import { type GenericBuilderInternals, @@ -106,11 +107,19 @@ const invalidStringSyntax = (matchedName: string) => { }; const enumViolation = (matchedName: string, data: string | undefined, values: [string, ...string[]]) => { - return new Error(``); + return new Error( + `Invalid value: value for the argument '${matchedName}' must be either one of the following: ${ + values.join(', ') + }; Received: ${data}`, + ); }; const enumViolationPos = (matchedName: string, data: string | undefined, values: [string, ...string[]]) => { - return new Error(``); + return new Error( + `Invalid value: value for the argument '${matchedName}' must be either one of the following: ${ + values.join(', ') + }; Received: ${data}`, + ); }; const invalidNumberSyntax = (matchedName: string) => { @@ -149,11 +158,13 @@ const generatePrefix = (name: string) => name.startsWith('-') ? name : name.leng const validateOptions = >( config: TOptionConfig, ): ProcessedOptions => { + const cloned = clone(config); + const entries: [string, GenericBuilderInternalsFields][] = []; const storedNames: Record = {}; - const cfgEntries = Object.entries(config); + const cfgEntries = Object.entries(cloned); for (const [key, value] of cfgEntries) { const cfg = value._.config; @@ -468,9 +479,10 @@ const helpCommand = (commands: Command[], helpHandler: Function | string) => }); const validateCommands = (commands: Command[]) => { + const cloned = clone(commands); const storedNames: Record = {}; - for (const cmd of commands) { + for (const cmd of cloned) { const storageVals = Object.values(storedNames); for (const storage of storageVals) { @@ -502,13 +514,13 @@ const validateCommands = (commands: Command[]) => { : [cmd.name]; } - return commands; + return cloned; }; /** * Separated for testing purposes */ -export const rawCli = (commands: Command[], config?: BroCliConfig) => { +export const rawCli = async (commands: Command[], config?: BroCliConfig) => { let options: Record | undefined; let cmd: RawCommand; @@ -520,7 +532,7 @@ export const rawCli = (commands: Command[], config?: BroCliConfig) => { const cmds = [...rawCmds, helpCommand(rawCmds, helpHandler)]; let args = argSource.slice(2, argSource.length); - if (!args.length) return executeOrLog(helpHandler); + if (!args.length) return await executeOrLog(helpHandler); const helpIndex = args.findIndex((arg) => arg === '--help' || arg === '-h'); if (helpIndex !== -1 && (helpIndex > 0 ? args[helpIndex - 1]?.startsWith('-') ? false : true : true)) { @@ -539,7 +551,7 @@ export const rawCli = (commands: Command[], config?: BroCliConfig) => { command = command ?? getCommand(cmds, args).command; } - return command ? executeOrLog(command.help) : executeOrLog(helpHandler); + return command ? await executeOrLog(command.help) : await executeOrLog(helpHandler); } const versionIndex = args.findIndex((arg) => arg === '--version' || arg === '-v'); @@ -554,7 +566,7 @@ export const rawCli = (commands: Command[], config?: BroCliConfig) => { options = parseOptions(command, args); cmd = command; - cmd.handler(options); + await cmd.handler(options); return undefined; }; @@ -565,9 +577,9 @@ export const rawCli = (commands: Command[], config?: BroCliConfig) => { * * @param argSource - source of cli arguments, optionally passed as a parameter for testing purposes and compatibility with custom environments */ -export const runCli = (commands: Command[], config?: BroCliConfig) => { +export const runCli = async (commands: Command[], config?: BroCliConfig) => { try { - rawCli(commands, config); + await rawCli(commands, config); } catch (e) { if (e instanceof BroCliError) throw e; diff --git a/tests/commands.test.ts b/tests/commands.test.ts index b9e7bba..955cd0d 100755 --- a/tests/commands.test.ts +++ b/tests/commands.test.ts @@ -31,7 +31,7 @@ const generateOps = { debug: boolean('dbg').alias('g').hidden(), }; -const generateHandler = (options: TypeOf) => { +const generateHandler = async (options: TypeOf) => { storage.options = options; storage.command = 'generate'; }; @@ -55,7 +55,7 @@ const cFirstOps = { commands.push(command({ name: 'c-first', options: cFirstOps, - handler: (options) => { + handler: async (options) => { storage.options = options; storage.command = 'c-first'; }, @@ -86,7 +86,7 @@ beforeEach(() => { describe('Option parsing tests', (it) => { it('Required options & defaults', async () => { - runCli(commands, { argSource: getArgs('generate', '--dialect=pg') }); + await runCli(commands, { argSource: getArgs('generate', '--dialect=pg') }); expect(storage).toStrictEqual({ command: 'generate', @@ -107,7 +107,7 @@ describe('Option parsing tests', (it) => { }); it('All options by name', async () => { - runCli( + await runCli( commands, { argSource: getArgs( @@ -146,7 +146,7 @@ describe('Option parsing tests', (it) => { }); it('All options by alias', async () => { - runCli( + await runCli( commands, { argSource: getArgs( @@ -186,38 +186,46 @@ describe('Option parsing tests', (it) => { }); it('Missing required options', async () => { - expect(() => runCli(commands, { argSource: getArgs('generate') })).toThrowError( + await expect(async () => await runCli(commands, { argSource: getArgs('generate') })).rejects.toThrowError( new Error(`Command 'generate' is missing following required options: --dialect [-d, -dlc]`), ); }); it('Unrecognized options', async () => { - expect(() => runCli(commands, { argSource: getArgs('generate', '--dialect=pg', '--unknown-one', '-m') })) + await expect(async () => + await runCli(commands, { argSource: getArgs('generate', '--dialect=pg', '--unknown-one', '-m') }) + ) + .rejects .toThrowError( new Error(`Unrecognized options for command 'generate': --unknown-one`), ); }); it('Wrong type: string to boolean', async () => { - expect(() => runCli(commands, { argSource: getArgs('generate', '--dialect=pg', '-def=somevalue') })).toThrowError( - new Error( - `Invalid syntax: boolean type argument '-def' must have it's value passed in the following formats: -def= | -def | -def.\nAllowed values: true, false, 0, 1`, - ), - ); + await expect(async () => + await runCli(commands, { argSource: getArgs('generate', '--dialect=pg', '-def=somevalue') }) + ) + .rejects + .toThrowError( + new Error( + `Invalid syntax: boolean type argument '-def' must have it's value passed in the following formats: -def= | -def | -def.\nAllowed values: true, false, 0, 1`, + ), + ); }); it('Wrong type: boolean to string', async () => { - expect(() => runCli(commands, { argSource: getArgs('generate', '--dialect=pg', '-ds') })).toThrowError( - new Error( - `Invalid syntax: string type argument '-ds' must have it's value passed in the following formats: -ds= | -ds `, - ), - ); + await expect(async () => await runCli(commands, { argSource: getArgs('generate', '--dialect=pg', '-ds') })).rejects + .toThrowError( + new Error( + `Invalid syntax: string type argument '-ds' must have it's value passed in the following formats: -ds= | -ds `, + ), + ); }); }); describe('Command parsing tests', (it) => { it('Get the right command, no args', async () => { - runCli(commands, { argSource: getArgs('c-first') }); + await runCli(commands, { argSource: getArgs('c-first') }); expect(storage).toStrictEqual({ command: 'c-first', @@ -231,7 +239,7 @@ describe('Command parsing tests', (it) => { }); it('Get the right command, command before args', async () => { - runCli(commands, { + await runCli(commands, { argSource: getArgs('c-second', '--flag', '--string=strval', '--stealth', '--sstring=Hidden string'), }); @@ -247,7 +255,7 @@ describe('Command parsing tests', (it) => { }); it('Get the right command, command between args', async () => { - runCli(commands, { + await runCli(commands, { argSource: getArgs('--flag', '--string=strval', 'c-second', '--stealth', '--sstring=Hidden string'), }); @@ -263,7 +271,7 @@ describe('Command parsing tests', (it) => { }); it('Get the right command, command after args', async () => { - runCli(commands, { + await runCli(commands, { argSource: getArgs('--flag', '--string=strval', '--stealth', '--sstring=Hidden string', 'c-second'), }); @@ -279,9 +287,11 @@ describe('Command parsing tests', (it) => { }); it('Unknown command', async () => { - expect(() => runCli(commands, { argSource: getArgs('unknown', '--somearg=somevalue', '-f') })).toThrowError( - new Error(`Unable to recognize any of the commands.\nUse 'help' command to list all commands.`), - ); + await expect(async () => await runCli(commands, { argSource: getArgs('unknown', '--somearg=somevalue', '-f') })) + .rejects + .toThrowError( + new Error(`Unable to recognize any of the commands.\nUse 'help' command to list all commands.`), + ); }); }); @@ -393,50 +403,50 @@ describe('Option definition tests', (it) => { describe('Command definition tests', (it) => { it('Duplicate names', async () => { - expect(() => { + await expect(async () => { const cmd = command({ name: 'c-first', handler: () => '', }); - runCli([...commands, cmd]); - }).toThrowError(); + await runCli([...commands, cmd]); + }).rejects.toThrowError(); }); it('Duplicate aliases', async () => { - expect(() => { + await expect(async () => { const cmd = command({ name: 'c-third', aliases: ['g'], handler: () => '', }); - runCli([...commands, cmd]); - }).toThrowError(); + await runCli([...commands, cmd]); + }).rejects.toThrowError(); }); it('Name repeats alias', async () => { - expect(() => { + await expect(async () => { const cmd = command({ name: 'gen', aliases: ['c4'], handler: () => '', }); - runCli([...commands, cmd]); - }).toThrowError(); + await runCli([...commands, cmd]); + }).rejects.toThrowError(); }); it('Alias repeats name', async () => { - expect(() => { + await expect(async () => { const cmd = command({ name: 'c-fifth', aliases: ['generate'], handler: () => '', }); - runCli([...commands, cmd]); - }).toThrowError(); + await runCli([...commands, cmd]); + }).rejects.toThrowError(); }); it('Duplicate names in same command', async () => { @@ -479,7 +489,7 @@ describe('Command definition tests', (it) => { ).toThrowError(); }); - it('Using handler function', () => { + it('Using handler function', async () => { const opts = { flag: boolean().alias('f', 'fl').desc('Boolean value'), string: string().alias('s', 'str').desc('String value'), @@ -497,7 +507,7 @@ describe('Command definition tests', (it) => { }), }); - runCli([cmd], { + await runCli([cmd], { argSource: getArgs('c-tenth', '-f', '-j', 'false', '--string=strval'), });