Skip to content

Commit

Permalink
feat: transform output name for options
Browse files Browse the repository at this point in the history
  • Loading branch information
chenasraf committed Nov 24, 2023
1 parent 4c9e981 commit 6ee8cf2
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 159 deletions.
31 changes: 24 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,32 @@ documentation of every option.
const parser = massarg({
name: 'my-cli',
description: "Does really amazing stuff, you wouldn't believe!",
bindHelpCommand: true,
}) // or: new Massarg()
.main((options) => console.log('main command', options))
.command({
name: 'foo',
description: 'a sub command',
aliases: ['f'],
run: (options) => console.log('foo command'),
})
.command(
massarg({
name: 'sub',
description: 'a sub command',
name: 'bar',
description: 'another sub command',
aliases: ['s'],
run: (options) => console.log('sub command', options),
run: (options) => console.log('bar command', options),
}).option({
name: 'file',
description: 'Filename to use',
aliases: ['f'],
parse: (filename) => path.resolve(process.cwd(), filename),
}),
)
.option({
name: 'my-string',
description: 'A string argument',
aliases: ['s'],
})
.flag({
name: 'flag',
description: 'a flag that will be related to any command (main or sub)',
Expand All @@ -93,8 +103,12 @@ const parser = massarg({
output: 'Sub command: flag is true',
})
.help({
binName: 'my-cli-app',
footer: 'Copyright © 2021 Me, Myself and I',
bindCommand: true,
footerText: `Copyright © ${new Date().getFullYear()} Me, Myself and I`,
titleStyle: {
bold: true,
color: 'brightWhite',
},
})
```

Expand Down Expand Up @@ -122,7 +136,10 @@ $ ./mybin
# Main command runs without options

$ ./mybin --my-string "Some string"
# Main command runs with option { myString: "Some string" }
# Main command runs with options { myString: "Some string" }

$ ./mybin foo
# Foo sub command run with options {}
```

## Commands
Expand Down
18 changes: 11 additions & 7 deletions src/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export type HelpItem = {
name: string
aliases: string[]
description: string
hidden?: boolean
}

export class HelpGenerator {
Expand Down Expand Up @@ -271,13 +272,16 @@ function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>(
...config
}: Partial<T> = {},
): string {
const rows = items.map((o) => {
const name = `${namePrefix}${o.name}${
o.aliases.length ? ` | ${aliasPrefix}${o.aliases.join(`|${aliasPrefix}`)}` : ''
}`
const description = o.description
return { name, description }
})
const rows = items
.map((o) => {
const name = `${namePrefix}${o.name}${
o.aliases.length ? ` | ${aliasPrefix}${o.aliases.join(`|${aliasPrefix}`)}` : ''
}`
const description = o.description
const hidden = o.hidden || false
return { name, description, hidden }
})
.filter((r) => !r.hidden)
const maxNameLength = Math.max(...rows.map((o) => o.name.length))
const nameStyle = (name: string) => format(name, config.nameStyle)
const descStyle = (desc: string) => format(desc, config.descriptionStyle)
Expand Down
9 changes: 8 additions & 1 deletion src/option.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'zod'
import { isZodError, ParseError } from './error'
import { toCamelCase } from './utils'

export const OptionConfig = <T extends z.ZodType>(type: T) =>
z.object({
Expand Down Expand Up @@ -35,6 +36,10 @@ export const OptionConfig = <T extends z.ZodType>(type: T) =>
* option.
*/
isDefault: z.boolean().optional(),
/** Whether the option is hidden. Hidden options are not displayed in the help output. */
hidden: z.boolean().optional(),
/** Specify a custom name for the output, which will be used when parsing the args. */
outputName: z.string().optional(),
})
export type OptionConfig<T = unknown> = z.infer<ReturnType<typeof OptionConfig<z.ZodType<T>>>>

Expand Down Expand Up @@ -108,6 +113,7 @@ export class MassargOption<T = unknown> {
parse: (value: string) => T
isArray: boolean
isDefault: boolean
outputName?: string

constructor(options: OptionConfig<T>) {
OptionConfig(z.any()).parse(options)
Expand All @@ -118,6 +124,7 @@ export class MassargOption<T = unknown> {
this.parse = options.parse ?? ((x) => x as unknown as T)
this.isArray = options.array ?? false
this.isDefault = options.isDefault ?? false
this.outputName = options.outputName
}

static fromTypedConfig<T = unknown>(config: TypedOptionConfig<T>): MassargOption<T> {
Expand All @@ -143,7 +150,7 @@ export class MassargOption<T = unknown> {
argv.shift()
input = argv.shift()!
const value = this.parse(input)
return { key: this.name, value, argv }
return { key: this.outputName || toCamelCase(this.name), value, argv }
} catch (e) {
if (isZodError(e)) {
throw new ParseError({
Expand Down
27 changes: 27 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,30 @@ export function deepMerge<T1, T2>(obj1: T1, obj2: T2): NonNullable<T1> & NonNull
}
return res
}
/**
* Splits a name into words, using camelCase, PascalCase, snake_case, and kebab-case or
* regular spaced strings.
*/
export function splitWords(str: string): string[] {
return str
.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/([a-zA-Z])([0-9])/g, '$1 $2')
.replace(/([0-9])([a-zA-Z])/g, '$1 $2')
.replace(/([a-z])([_-])/g, '$1 $2')
.replace(/([_-])([a-zA-Z])/g, '$1 $2')
.split(/[_-]/)
.map((s) => s.trim())
.filter(Boolean)
}

export function toCamelCase(str: string): string {
return splitWords(str)
.map((s, i) => (i === 0 ? s : s[0].toUpperCase() + s.slice(1)))
.join('')
}

export function toPascalCase(str: string): string {
return splitWords(str)
.map((s) => s[0].toUpperCase() + s.slice(1))
.join('')
}
145 changes: 1 addition & 144 deletions test/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ test('constructor', () => {
expect(massarg(opts)).toBeInstanceOf(MassargCommand)
})

describe('command', () => {
describe('sub command', () => {
test('add', () => {
const command = massarg(opts)
expect(command.command).toBeInstanceOf(Function)
Expand All @@ -36,149 +36,6 @@ describe('command', () => {
})
})

describe('option', () => {
test('add', () => {
const command = massarg(opts)
expect(command.option).toBeInstanceOf(Function)
expect(
command.option({ name: 'test2', description: 'test2', aliases: [], defaultValue: '' }),
).toBeInstanceOf(MassargCommand)
})
test('validate', () => {
expect(() =>
massarg(opts).option({
name: 'test2',
description: 123 as any,
aliases: [],
defaultValue: '',
}),
).toThrow('Expected string, received number')
})
test('add duplicate', () => {
expect(() =>
massarg(opts)
.option({
name: 'test2',
description: 'test2',
aliases: [],
defaultValue: '',
})
.option({
name: 'test2',
description: 'test2',
aliases: [],
defaultValue: '',
}),
).toThrow('Option "test2" already exists')
})
test('add 2 defaults', () => {
expect(() =>
massarg(opts)
.option({
name: 'test',
description: 'test2',
aliases: [],
isDefault: true,
})
.option({
name: 'test2',
description: 'test2',
aliases: [],
isDefault: true,
}),
).toThrow(
'Option "test2" cannot be set as default because option "test" is already set as default',
)
})
})

describe('flag', () => {
test('add', () => {
const command = massarg(opts)
expect(command.flag).toBeInstanceOf(Function)
expect(command.flag({ name: 'test2', description: 'test2', aliases: [] })).toBeInstanceOf(
MassargCommand,
)
})
test('add duplicate', () => {
expect(() =>
massarg(opts)
.flag({ name: 'test2', description: 'test2', aliases: [] })
.flag({ name: 'test2', description: 'test2', aliases: [] }),
).toThrow('Flag "test2" already exists')
})
test('validate', () => {
expect(() =>
massarg(opts).flag({
name: 'test2',
description: 123 as any,
aliases: [],
}),
).toThrow('Expected string, received number')
})
})

describe('example', () => {
test('example', () => {
const command = massarg(opts)
expect(command.example).toBeInstanceOf(Function)
expect(command.example({ description: 'test', input: '', output: '' })).toBeInstanceOf(
MassargCommand,
)
})
})

describe('help', () => {
test('default value', () => {
const command = massarg(opts)
expect(command.helpConfig).toEqual(defaultHelpConfig)
})

test('init', () => {
const command = massarg(opts).help({
bindOption: true,
optionOptions: {
namePrefix: '__',
},
})
expect(command.help).toBeInstanceOf(Function)
expect(command.helpConfig).toHaveProperty('bindOption', true)
expect(command.helpConfig).toHaveProperty('optionOptions.namePrefix', '__')
expect(command.helpConfig).toHaveProperty('optionOptions.aliasPrefix', '-')
expect(command.helpConfig).toHaveProperty('optionOptions.nameStyle.color', 'yellow')
})

test('binds command', () => {
const command = massarg(opts).help({
bindCommand: true,
})
expect(command.help).toBeInstanceOf(Function)
expect(command.helpConfig).toHaveProperty('bindCommand', true)
expect(command.commands.find((o) => o.name === 'help')).toBeTruthy()
})

test('binds option', () => {
const command = massarg(opts).help({
bindOption: true,
})
expect(command.help).toBeInstanceOf(Function)
expect(command.helpConfig).toHaveProperty('bindOption', true)
expect(command.options.find((o) => o.name === 'help')).toBeTruthy()
})

test('help string', () => {
const command = massarg(opts)
expect(command.helpString()).toContain(`Usage:`)
})

test('print help', () => {
const log = jest.spyOn(console, 'log').mockImplementation(() => {})
const command = massarg(opts)
command.printHelp()
expect(log).toHaveBeenCalled()
})
})

describe('getArgs', () => {
test('basic', () => {
expect(massarg(opts).getArgs([])).toEqual({})
Expand Down
14 changes: 14 additions & 0 deletions test/example.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { MassargCommand } from '../src/command'
import { massarg } from '../src/index'

const opts = {
name: 'test',
description: 'test',
}
test('example', () => {
const command = massarg(opts)
expect(command.example).toBeInstanceOf(Function)
expect(command.example({ description: 'test', input: '', output: '' })).toBeInstanceOf(
MassargCommand,
)
})
Loading

0 comments on commit 6ee8cf2

Please sign in to comment.