chore: remove old files

chore: add gh-pages deploy script
This commit is contained in:
2023-11-24 15:17:28 +02:00
committed by Chen Asraf
parent 64545ae750
commit edbde02618
15 changed files with 126 additions and 1938 deletions

View File

@@ -1,120 +0,0 @@
import { CommandDef, ExampleDef, HelpDef, MainDef, Named, OptionDef } from "./types"
import { ArrayOr, asArray } from "./utils"
// prettier-ignore
export const colorList = [
"reset", "bold", "dim", "italic", "underline", "inverse", "hidden", "strikethrough", "black", "red", "green",
"yellow", "blue", "magenta", "cyan", "white", "gray", "bgBlack", "bgRed", "bgGreen", "bgYellow", "bgBlue",
"bgMagenta", "bgCyan", "bgWhite",
]
class AssertionError extends Error {}
function assert(condition: any, message?: string): void {
if (!condition) {
throw new AssertionError(message)
}
}
function nullOr(condition: any, condition2: any): boolean {
return [null, undefined].includes(condition) || condition2
}
function assertRequired(obj: any, prefix: string, name: string): void {
assert(![undefined, null].includes(obj), `${prefix}: ${name} must be provided`)
}
function assertType(obj: any, prefix: string, name: string, options: { type: string; required?: boolean }): void {
if (options.required) {
assertRequired(obj, prefix, name)
} else {
try {
assertRequired(obj, prefix, name)
} catch (e) {
return
}
}
assert(typeof obj === options.type, `${prefix}: ${name} must be ${options.type}`)
}
function assertNumber(
obj: number | null | undefined,
prefix: string,
name: string,
options: { min?: number; max?: number; required?: boolean }
): void {
assertType(obj, prefix, name, { required: options.required, type: "number" })
if (!options.required && [null, undefined].includes(obj as any)) {
return
}
if (typeof options.max === "number") {
assert(obj! <= options.max, `${prefix}: ${name} must be ≤ ${options.max}`)
}
if (typeof options.min === "number") {
assert(obj! >= options.min, `${prefix}: ${name} must be ≥ ${options.min}`)
}
}
function assertColor(color: ArrayOr<string> | undefined, prefix: string, name: string) {
assert(
nullOr(
color,
asArray(color).every((c) => colorList.includes(c!))
),
`${prefix}: ${name} must be string or array of strings. Accepted values: ` + colorList.join(", ")
)
}
function assertAliases(def: Named, allDefs: Named[], prefix: string) {
assert(
!def.aliases || def.aliases.every((a) => !allDefs.find((opt) => [opt.name, ...(opt.aliases ?? [])].includes(a))),
`${prefix}: Aliases must be unique`
)
}
export function assertHelp(help: HelpDef) {
assertType(help.binName, "Help", "Binary Name", { type: "string" })
assertColor(help.normalColors, "Help", "Normal colors")
assertColor(help.bodyColors, "Help", "Body colors")
assertColor(help.titleColors, "Help", "Title colors")
assertColor(help.subtitleColors, "Help", "Subtitle colors")
assertColor(help.highlightColors, "Help", "Highlight colors")
assertType(help.footer, "Help", "Footer", { type: "string" })
assertType(help.header, "Help", "Header", { type: "string" })
assertType(help.optionNameSeparator, "Help", "Option Name Separator", { type: "string" })
assertType(help.commandNameSeparator, "Help", "Command Name Separator", { type: "string" })
assertNumber(help.printWidth, "Help", "Print Width", { min: 0 })
assertType(help.exampleInputPrefix, "Help", "Example Input Prefix", { type: "string" })
assertType(help.exampleOutputPrefix, "Help", "Example Output Prefix", { type: "string" })
assertType(help.usageExample, "Help", "Usage Example", { type: "string" })
assertType(help.useGlobalColumns, "Help", "Use Global Columns", { type: "boolean" })
assertType(help.includeDefaults, "Help", "Include Defaults", { type: "boolean" })
assertType(help.useColors, "Help", "Use Colors", { type: "boolean" })
}
export function assertCommand(command: CommandDef<any>, allCommands: CommandDef<any>[]): void {
assertType(command.name, "Command", "Name", { required: true, type: "string" })
assertAliases(command, allCommands, "Command")
assertType(command.run, "Command", "Run", { required: true, type: "function" })
}
export function assertOption(option: OptionDef<any, any>, allOptions: OptionDef<any, any>[]): void {
assert(option.name, "Option: Name must be provided")
assert(typeof option.name === "string", "Option: Name must be string")
assertAliases(option, allOptions, "Option")
assertType(option.boolean, "Option", "Default Value", { type: "boolean" })
assertType(option.parse, "Option", "Parse", { type: "function" })
}
export function assertExample(example: ExampleDef) {
assertType(example.input, "Example", "Input", { required: true, type: "string" })
assertType(example.output, "Example", "Output", { type: "string" })
assertType(example.description, "Example", "Description", { type: "string" })
}
export function assertMain(run: MainDef<any>) {
assertType(run, "Main", "Main", { required: true, type: "function" })
}

View File

@@ -1,18 +0,0 @@
export class RequiredError extends Error {
fieldName!: string
cmdName!: string
constructor(fieldName: string, cmdName: string) {
super(
`Option: \`${fieldName}\` is required${
cmdName !== "all" ? ` for command: \`${cmdName}\`` : ""
}, but was not defined. Try using: \`--${fieldName} {value}\``
)
this.fieldName = fieldName
this.cmdName = cmdName
}
public static isRequiredError(e: any): e is RequiredError {
return e.fieldName && e.cmdName
}
}

View File

@@ -1,555 +0,0 @@
#!/usr/bin/env node
import chalk from "chalk"
import merge from "lodash/merge"
import camelCase from "lodash/camelCase"
import path from "path"
import { OptionsBase, CommandDef, HelpDef, MainDef, OptionDef, ExampleDef } from "./types"
import { ArrayOr, asArray, colorCount, COLOR_CODE_LEN, wrap } from "./utils"
import { RequiredError } from "./errors"
import { assertCommand, assertExample, assertHelp, assertMain, assertOption } from "./assertions"
export class Massarg<Options> {
private _main?: MainDef<Options>
private _options: OptionDef<Options, any>[] = []
private _commands: CommandDef<Options>[] = []
private _runCommand?: CommandDef<Options>
private _examples: ExampleDef[] = []
private _maxNameLen = 0
/**
* These are the parsed options passed via args. They will only be available after using `parse()` or `printHelp()`,
* or when returned by `parseArgs()`. */
public data: Options & OptionsBase = { help: false, extras: [] as string[] } as Options & OptionsBase
private _help: Required<HelpDef> = {
binName: undefined as any,
normalColors: "dim",
highlightColors: "yellow",
titleColors: ["bold", "white"],
subtitleColors: ["bold", "dim"],
bodyColors: "white",
printWidth: 80,
header: "",
footer: "",
commandNameSeparator: " | ",
optionNameSeparator: "|",
useGlobalColumns: true,
usageExample: "[command] [options]",
useColors: true,
includeDefaults: true,
exampleInputPrefix: "$",
exampleOutputPrefix: "➜",
}
private _requiredOptions: Record<"all" | string, Record<string, boolean>> = {}
constructor() {
this.option({
name: "help",
aliases: ["h"],
description: "Display help information",
parse: Boolean,
})
}
/** Define the main command to run when no commands are passed. */
public main(run: MainDef<Options>): Massarg<Options> {
assertMain(run)
this._main = run
return this
}
/** Add option to be parsed */
public option<Value>(option: OptionDef<Options, Value>): Massarg<Options> {
let defaultValue = option.defaultValue as any
// detect boolean values
option.boolean ??= (option.parse as any) === Boolean || [true, false].includes(defaultValue)
// detect array values
option.array ??= Array.isArray(defaultValue)
// default parser
option.parse ??= (option.boolean ? this._isTruthy : (a: any) => a) as any
assertOption(option, this._options)
if (option.array && defaultValue === undefined) {
defaultValue = []
}
this._options.push(option)
this._prepareRequired(option)
return this
}
/** Add example line to be added to the help text. */
public example(example: ExampleDef): Massarg<Options> {
assertExample(example)
this._examples.push(example as ExampleDef)
return this
}
/** Add command to be run */
public command(command: CommandDef<Options>): Massarg<Options> {
assertCommand(command, this._commands)
this._commands.push(command)
for (const opt of this._commandOptions(command)) {
this._prepareRequired(opt)
}
return this
}
/** Set options for behavior of the help text print. */
public help(help: HelpDef): Massarg<Options> {
assertHelp(help)
this._help = merge(this._help, help)
return this
}
/**
* Print the help text without being required to pass option.
*
* @param args If args weren't already parsed, you can add them here
*/
public printHelp(args?: string[]): void {
console.log(this.getHelpString(args).join("\n"))
}
/**
* Get the help text as an array of lines. Useful for manipulating the response or querying before displaying
* to the user.
*/
public getHelpString(args?: string[]): string[] {
const lines: string[] = []
if (args?.length) {
this.parseArgs(args)
}
const { bodyColors, highlightColors, normalColors, titleColors, binName, usageExample } = this._help
lines.push(
[
this.color(titleColors, "Usage:"),
this.color(highlightColors, binName ?? path.basename(process.argv[1])),
this.color(normalColors, usageExample),
].join(" ")
)
lines.push("")
if (this._help.header) {
lines.push(this.color(bodyColors, this._help.header))
lines.push("")
}
if (this._commands.length) {
lines.push(this.color(titleColors, "Commands:"))
lines.push("")
lines.push(...this._printCommands())
}
lines.push(...this._printOptions())
if (this._examples.length) {
lines.push(this.color(titleColors, "Examples:"))
lines.push("")
lines.push(...this._printExamples())
}
if (this._help.footer) {
lines.push(this.color(bodyColors, this._help.footer))
lines.push("")
}
return lines
}
/**
* Parse the arguments without running the commands related to them. Useful for testing or querying the data from the
* args manually, if it is for some reason not enough to parse it normally through defining commands.
* @param args Arguments to parse. Defaults to `process.argv`
* @returns Parsed options
*/
public parseArgs(args = process.argv): Options & OptionsBase {
for (const option of this._options) {
if (option.defaultValue !== undefined) {
this._addOptionToData(option, option.defaultValue)
} else if (option.array) {
this._pushToArrayData(option)
}
}
for (let i = 0; i < args.length; i++) {
const arg = args[i]
const option = this._options.find((o) => `--${o.name}` === arg || o.aliases?.map((a) => `-${a}`).includes(arg))
if (option) {
let tempValue: any
const hasNextToken = args.length > i + 1
const nextTokenIsValue = hasNextToken && !args[i + 1].startsWith("-")
if (option.boolean && (!hasNextToken || !nextTokenIsValue)) {
// parse boolean args w/o value
tempValue = true
} else if (!hasNextToken || !nextTokenIsValue) {
// non-boolean args with no value
throw new TypeError(`Missing value for: ${option.name}`)
} else {
// any args (incl. boolean) with value
tempValue = args[i + 1]
args.shift()
}
const value = option.parse!(tempValue, this.data)
this._addOptionToData(option, value)
// continue
}
const command = this._commands.find((o) => o.name === arg || o.aliases?.includes(arg))
let justFoundCommand = false
if (command) {
if (!this._runCommand) {
this._runCommand = command
justFoundCommand = true
}
}
if (!option && (!command || (command && !justFoundCommand))) {
const defOpts = this._options.filter((o) => o.isDefault)
if (defOpts.length) {
for (const option of defOpts) {
this._addOptionToData(option, option.parse!(arg, this.data))
}
} else {
this.data.extras.push(arg)
}
}
}
return this.data
}
/**
* Parse the given args, running any relevant commands in the process.
*
* @param args args to parse. Defaults to `process.argv`
*/
public parse(args?: string[]): void {
this.parseArgs(args)
if (this.data.help) {
this.printHelp()
return
}
try {
if (this._runCommand) {
this._ensureRequired(this._runCommand)
this._runCommand.run(this.data)
} else if (this._main) {
this._ensureRequired()
this._main(this.data)
} else {
this._ensureRequired()
}
} catch (e: any) {
if (RequiredError.isRequiredError(e)) {
console.error(chalk.red`${e.message}`)
process.exit(1)
}
throw e
}
return
}
private _prepareRequired(options: OptionDef<Options, any>) {
if (options.required) {
if (options.commands?.length) {
for (const command of this._optionCommands(options)) {
this._requiredOptions[command.name] ??= {}
this._requiredOptions[command.name][options.name] = true
}
} else {
this._requiredOptions["all"] ??= {}
this._requiredOptions["all"][options.name] = true
}
}
}
private _printExamples(): string[] {
const lines: string[] = []
const { normalColors, highlightColors, bodyColors, titleColors } = this._help
for (const example of this._examples) {
if (example.description) {
lines.push(
...wrap(this.color(titleColors, example.description), {
colorCount: this.colorCount(titleColors),
indent: 2,
printWidth: this._help.printWidth,
})
)
lines.push("")
}
lines.push(
...wrap(
[this.color(normalColors, this._help.exampleInputPrefix), this.color(highlightColors, example.input)].join(
" "
),
{
colorCount: this.colorCount(highlightColors),
firstLineIndent: 2,
indent: 3 + this._help.exampleInputPrefix.length,
// indent: this.colorCount(normalColors) + 4,
printWidth: this._help.printWidth,
}
)
)
if (example.output) {
lines.push(
...wrap(
[this.color(normalColors, this._help.exampleOutputPrefix), this.color(bodyColors, example.output)].join(
" "
),
{
colorCount: this.colorCount(bodyColors),
firstLineIndent: 2,
indent: 3 + this._help.exampleOutputPrefix.length,
// indent: this.colorCount(normalColors) + 4,
printWidth: this._help.printWidth,
}
)
)
}
lines.push("")
}
return lines
}
private _isTruthy(v: any): boolean {
v = String(v).toLowerCase()
return ["1", "true", "yes", "y", "on"].includes(v) || !["0", "false", "no", "n", "off"].includes(v)
}
private _ensureRequired(cmd?: CommandDef<Options>) {
const cmdName = cmd?.name ?? "all"
for (const optName in this._requiredOptions[cmdName]) {
if (this._requiredOptions[cmdName][optName]) {
throw new RequiredError(optName, cmdName)
}
}
}
private _addOptionToData(option: OptionDef<Options, any>, value: any) {
const _d: Record<string, any> = this.data
const set = (value: any) => {
_d[option.name] = value
_d[camelCase(option.name)] = value
option.aliases?.forEach((a) => (_d[a] = value))
}
const push = (value: any) => {
this._pushToArrayData(option, value)
}
if (!option.array) {
// single value
set(value)
} else {
// multiple values
if (Array.isArray(value) && value.length) {
for (const el of value) {
push(el)
}
} else if (!Array.isArray(value)) {
push(value)
}
}
if (value !== option.defaultValue && value !== undefined) {
for (const key in this._requiredOptions) {
this._requiredOptions[key][option.name] = false
}
}
}
private _pushToArrayData(option: OptionDef<Options, any>, value?: any) {
const _d: Record<string, any> = this.data
const ccSame = camelCase(option.name) === option.name
_d[option.name] ??= []
_d[camelCase(option.name)] ??= []
option.aliases?.forEach((a) => (_d[a] ??= []))
if (value !== undefined) {
_d[option.name].push(value)
if (!ccSame) {
_d[camelCase(option.name)].push(value)
}
option.aliases?.forEach((a) => _d[a].push(value))
}
}
private _getWrappedLines(
list: Array<{ name: string; description?: string; additionalColorCount?: number }>
): string[] {
const { normalColors, highlightColors } = this._help
const lines: string[] = []
let maxNameLen = this._help.useGlobalColumns ? this._maxNameLen ?? 0 : 0
for (const item of list) {
if (item.name.length > maxNameLen) {
maxNameLen = item.name.length
}
}
if (this._help.useGlobalColumns) {
this._maxNameLen = maxNameLen
}
const ARG_SPACE_LEN = 2
const INDENT_LEN = 2
const nameFullSize = maxNameLen + ARG_SPACE_LEN + INDENT_LEN
for (const item of list) {
const cmdName = this.color(highlightColors, `${item.name}`).padEnd(
nameFullSize + this.colorCount(highlightColors) * COLOR_CODE_LEN,
" "
)
const cmdDesc = this.color(normalColors, item.description ?? "")
for (const line of wrap(cmdName + cmdDesc, {
indent: nameFullSize + INDENT_LEN,
colorCount: this.colorCount(
normalColors,
highlightColors,
item.additionalColorCount ? new Array({ length: item.additionalColorCount }) : []
),
firstLineIndent: INDENT_LEN,
printWidth: this._help.printWidth,
})) {
lines.push(line)
}
lines.push("")
}
return lines
}
private _printCommands(): string[] {
return this._getWrappedLines(
this._commands.map((c) => ({ name: this._fullCmdName(c), description: c.description }))
)
}
private _printOptions(): string[] {
const lines: string[] = []
const { titleColors, subtitleColors } = this._help
const commandOpts: string[] = []
for (const cmd of this._commands) {
const opts = this._commandOptions(cmd)
if (opts.length) {
commandOpts.push(this.color(subtitleColors, `${cmd.name}:`))
commandOpts.push("")
for (const line of this._getWrappedLines(
opts.map((c) => ({
name: this._fullOptName(c),
description: this._optionDescription(c),
additionalColorCount: c.defaultValue !== undefined ? 1 : 0,
}))
)) {
commandOpts.push(line)
}
}
}
lines.push(this.color(titleColors, commandOpts.length ? "Command Options:" : "Options:"))
lines.push("")
for (const line of commandOpts) {
lines.push(line)
}
const globalOpts = this._globalOptions()
if (globalOpts.length) {
if (commandOpts.length) {
lines.push(this.color(titleColors, "Global Options:"))
lines.push("")
}
for (const line of this._getWrappedLines(
globalOpts.map((c) => ({ name: this._fullOptName(c), description: this._optionDescription(c) }))
)) {
lines.push(line)
}
}
return lines
}
private _optionDescription(c: OptionDef<Options, any>): string | undefined {
if (c.defaultValue === undefined || !this._help.includeDefaults) {
return c.description
}
return [c.description!, this.color(this._help.bodyColors, `(default: ${c.defaultValue.toString().trim()})`)]
.filter(Boolean)
.join(" ")
}
private _fullCmdName(cmd: CommandDef<Options>) {
return [cmd.name, ...(cmd.aliases ?? [])].join(this._help.commandNameSeparator)
}
private _fullOptName(opt: OptionDef<Options, any>) {
return [`--${opt.name}`, ...(opt.aliases ?? []).map((a) => `-${a}`)].join(this._help.optionNameSeparator)
}
private _commandOptions(cmd: CommandDef<Options>): OptionDef<Options, any>[] {
return this._options.filter(
(o) =>
(asArray(o.commands).length && asArray(o.commands).includes(cmd.name)) ||
cmd.aliases?.some((a) => asArray(o.commands).includes(a))
)
}
private _optionCommands(opt: OptionDef<Options, any>): OptionDef<Options, any>[] {
return this._commands.filter((c) => {
return asArray(opt.commands).some((_c) => {
return [c.name, ...(c.aliases ?? [])].includes(_c!)
})
})
}
private _globalOptions(): OptionDef<Options, any>[] {
return this._options.filter((o) => !o.commands)
}
private color(color: ArrayOr<keyof typeof chalk>, ...text: any[]): string {
if (!this._help.useColors) {
return text.join(" ")
}
let output: string = undefined as any
for (const c of asArray(color)) {
output = (chalk[c as keyof typeof chalk] as typeof chalk.dim)(...(output ? [output] : text))
}
return chalk.reset(output)
}
private colorCount(...colors: any[]): number {
if (!this._help.useColors) {
return 0
}
return colorCount(...colors)
}
}
export function massarg<T>() {
return new Massarg<T>()
}
export default massarg

View File

@@ -1,60 +0,0 @@
import massarg from "."
massarg()
.help({
// printWidth: 0,
binName: "my-cmd",
useGlobalColumns: true,
header: "This is the header",
footer: "This is the footer",
usageExample: "command [options]",
})
.option({
name: "bool",
aliases: ["b"],
defaultValue: false,
commands: ["do", "cc"],
description: "This is a boolean arg. Supply it without value or with 1 to set as true, or set value 0 for false",
required: true,
parse: Boolean,
})
.option({
name: "number",
aliases: ["n"],
description: "This is a number arg, if you include this option, you must supply it with a value.",
defaultValue: 0,
commands: "do",
parse: (v) => parseInt(v),
})
.command({
name: "do-something",
description: "This command does something.",
aliases: ["do", "d"],
run: console.log.bind(undefined, "do"),
})
.command({
name: "my-custom-command",
description:
"This is another command that does something. It's a different one just to see more available. This " +
"description is just to fill more lines.",
aliases: ["cc", "c"],
run: console.log.bind(undefined, "do"),
})
.main(console.log.bind(undefined, "main"))
.example({
input: "my-cmd test --one --two",
description: "This is how you do it",
output: "42",
})
// .command({
// name: "cmd",
// description: "Command",
// run: () => void 0,
// })
// .option({
// name: "number",
// description: "Number value",
// commands: "cmd",
// parse: (v) => parseInt(v),
// })
.parse()

View File

@@ -1,138 +0,0 @@
import capitalize from "lodash/capitalize"
import massarg from "../src"
import { colorList } from "../src/assertions"
import { HelpDef } from "../src/types"
describe("Assertions", () => {
test("should assert main command input", () => {
expect(() => massarg().main(undefined as any)).toThrow("Main: Main must be provided")
expect(() => massarg().main(1 as any)).toThrow("Main: Main must be function")
})
test("should assert command input", () => {
expect(() =>
massarg().command({
name: 1 as any,
run: () => void 0,
})
).toThrow("Command: Name must be string")
expect(() =>
massarg().command({
name: undefined as any,
run: () => void 0,
})
).toThrow("Command: Name must be provided")
expect(() =>
massarg().command({
name: "cmd",
run: undefined as any,
})
).toThrow("Command: Run must be provided")
expect(() =>
massarg().command({
name: "cmd",
run: 1 as any,
})
).toThrow("Command: Run must be function")
expect(() =>
massarg()
.command({
name: "cmd",
run: () => void 0,
})
.command({
name: "cmd2",
aliases: ["cmd"],
run: () => void 0,
})
).toThrow("Command: Aliases must be unique")
})
test("should assert option input", () => {
expect(() =>
massarg().option({
name: 1 as any,
})
).toThrow("Option: Name must be string")
expect(() =>
massarg().option({
name: undefined as any,
})
).toThrow("Option: Name must be provided")
expect(() =>
massarg().option({
name: "opt",
parse: 1 as any,
})
).toThrow("Option: Parse must be function")
expect(() =>
massarg()
.option({
name: "opt",
})
.option({
name: "opt2",
aliases: ["opt"],
})
).toThrow("Option: Aliases must be unique")
})
describe("help input assertions", () => {
const strings: Partial<Record<keyof HelpDef, string>> = {
binName: "Binary Name",
optionNameSeparator: "Option Name Separator",
commandNameSeparator: "Command Name Separator",
header: "Header",
footer: "Footer",
exampleInputPrefix: "Example Input Prefix",
exampleOutputPrefix: "Example Output Prefix",
usageExample: "Usage Example",
}
const bools: Partial<Record<keyof HelpDef, string>> = {
includeDefaults: "Include Defaults",
useColors: "Use Colors",
useGlobalColumns: "Use Global Columns",
}
for (const k of Object.getOwnPropertyNames(strings)) {
test(`should assert string ${k}`, () => {
expect(() => massarg().help({ [k as keyof HelpDef]: 1 as any })).toThrow(
`Help: ${strings[k as keyof HelpDef]} must be string`
)
})
}
for (const k of Object.getOwnPropertyNames(bools)) {
test(`should assert bool ${k}`, () => {
expect(() => massarg().help({ [k as keyof HelpDef]: 1 as any })).toThrow(
`Help: ${bools[k as keyof HelpDef]} must be bool`
)
})
}
test("should assert other help input params", () => {
const colors = ["normal", "body", "title", "subtitle", "highlight"]
for (const color of colors) {
expect(() =>
massarg().help({
[color + "Colors"]: [1],
})
).toThrow(
`Help: ${capitalize(color)} colors must be string or array of strings. Accepted values: ` +
colorList.join(", ")
)
}
expect(() => massarg().help({ printWidth: -1 })).toThrow("Help: Print Width must be ≥ 0")
expect(() => massarg().help({ printWidth: "a" as any })).toThrow("Help: Print Width must be number")
})
})
})

View File

@@ -1,112 +0,0 @@
import massarg from "../src"
describe("Commands", () => {
test("should run command", () => {
const cmd = jest.fn()
massarg()
.command({
name: "cmd",
aliases: ["c"],
run: cmd,
})
.parse(["cmd"])
expect(cmd).toHaveBeenCalledTimes(1)
})
test("should run command with alias", () => {
const cmd = jest.fn()
massarg()
.command({
name: "cmd",
aliases: ["c"],
run: cmd,
})
.parse(["c"])
expect(cmd).toHaveBeenCalledTimes(1)
})
test("should consume the first command, then treat the rest as extras", () => {
const cmd = jest.fn()
const opt = jest.fn()
const def = massarg()
.command({
name: "cmd",
aliases: ["c"],
run: cmd,
})
.command({
name: "opt",
aliases: ["o"],
run: opt,
})
const options = def.parseArgs(["cmd", "opt"])
def.parse(["cmd", "opt"])
expect(cmd).toHaveBeenCalledTimes(1)
expect(opt).not.toHaveBeenCalled()
expect(options.extras).toContain("opt")
})
test("should consume the first command, then pass to default", () => {
const cmd1 = jest.fn()
const cmd2 = jest.fn()
const def = massarg<{ opt: string }>()
.command({
name: "cmd1",
aliases: ["c1"],
run: cmd1,
})
.command({
name: "cmd2",
aliases: ["c2"],
run: cmd2,
})
.option({
name: "opt",
isDefault: true,
aliases: ["o"],
commands: ["c1"],
required: true,
})
const options = def.parseArgs(["cmd1", "opt"])
def.parse(["cmd1", "opt"])
expect(cmd1).toHaveBeenCalledTimes(1)
expect(cmd2).not.toHaveBeenCalled()
expect(options.opt).toBe("opt")
})
test("should consume the first command, then pass to default when option name matches command name", () => {
const cmd1 = jest.fn()
const opt = jest.fn()
const def = massarg<{ opt: string }>()
.command({
name: "cmd1",
aliases: ["c1"],
run: cmd1,
})
.command({
name: "opt",
aliases: ["o"],
run: opt,
})
.option({
name: "opt",
isDefault: true,
aliases: ["o"],
commands: ["c1"],
required: true,
})
const options = def.parseArgs(["cmd1", "opt"])
def.parse(["cmd1", "opt"])
expect(cmd1).toHaveBeenCalledTimes(1)
expect(opt).not.toHaveBeenCalled()
expect(options.opt).toBe("opt")
})
})

View File

@@ -1,108 +0,0 @@
import massarg from "../src"
describe("Examples", () => {
describe("basic parse", () => {
test("should parse input only", () => {
const helpStr = massarg()
.help({ binName: "test", useColors: false })
.example({
input: "my-cmd --number 10",
})
.getHelpString()
.join("\n")
expect(helpStr).toBe(
"Usage: test [command] [options]" +
"\n\n" +
"Options:" +
"\n\n" +
" --help|-h Display help information" +
"\n\n" +
"Examples:" +
"\n\n" +
" $ my-cmd --number 10" +
"\n"
)
})
test("should parse output", () => {
const helpStr = massarg()
.help({ binName: "test", useColors: false })
.example({
input: "my-cmd --number 10",
output: "Your number is 10 which is an integer",
})
.getHelpString()
.join("\n")
expect(helpStr).toBe(
"Usage: test [command] [options]" +
"\n\n" +
"Options:" +
"\n\n" +
" --help|-h Display help information" +
"\n\n" +
"Examples:" +
"\n\n" +
" $ my-cmd --number 10" +
"\n" +
" ➜ Your number is 10 which is an integer" +
"\n"
)
})
})
test("should wrap input properly", () => {
const helpStr = massarg()
.help({ binName: "test", useColors: false })
.example({
input: "my-cmd --number 10 --another-number 20 --and-yet-another-number 30 --what-about-another 40",
})
.getHelpString()
.join("\n")
expect(helpStr).toBe(
"Usage: test [command] [options]" +
"\n\n" +
"Options:" +
"\n\n" +
" --help|-h Display help information" +
"\n\n" +
"Examples:" +
"\n\n" +
" $ my-cmd --number 10 --another-number 20 --and-yet-another-number 30" +
"\n" +
" --what-about-another 40" +
"\n"
)
})
test("should wrap description properly", () => {
const helpStr = massarg()
.help({ binName: "test", useColors: false })
.example({
input: "my-cmd --number 10",
description:
"This is a really long test. Very long text indeed, which should eventually span to more than 1 line, so indentation can be tested.",
})
.getHelpString()
.join("\n")
expect(helpStr).toBe(
"Usage: test [command] [options]" +
"\n\n" +
"Options:" +
"\n\n" +
" --help|-h Display help information" +
"\n\n" +
"Examples:" +
"\n\n" +
" This is a really long test. Very long text indeed, which should eventually" +
"\n" +
" span to more than 1 line, so indentation can be tested." +
"\n\n" +
" $ my-cmd --number 10" +
"\n"
)
})
})

View File

@@ -1,14 +0,0 @@
import massarg from "../src"
describe("Extras", () => {
test("should take extra values unparsed", () => {
const options = massarg()
.option({
name: "number",
parse: (v) => parseInt(v),
})
.parseArgs(["--number", "10", "app name"])
expect(options).toHaveProperty("number", 10)
expect(options).toHaveProperty("extras", ["app name"])
})
})

View File

@@ -1,174 +0,0 @@
import massarg from "../src"
describe("Print Help", () => {
test("should print to console", () => {
const mock = jest.spyOn(console, "log").mockImplementation(() => void 0)
massarg()
.help({ binName: "test", useColors: false })
.option({
name: "number",
description: "Number value",
parse: (v) => parseInt(v),
})
.printHelp()
expect(mock).toBeCalled()
mock.mockRestore()
})
test("should print header & footer", () => {
const helpStr = massarg()
.help({ binName: "test", useColors: false, header: "This is a header", footer: "This is a footer" })
.option({
name: "number",
description: "Number value",
parse: (v) => parseInt(v),
})
.getHelpString()
.join("\n")
expect(helpStr).toBe(
"Usage: test [command] [options]" +
"\n\n" +
"This is a header" +
"\n\n" +
"Options:" +
"\n\n" +
" --help|-h Display help information" +
"\n\n" +
" --number Number value" +
"\n\n" +
"This is a footer" +
"\n"
)
})
test("should print default value", () => {
const helpStr = massarg()
.help({ binName: "test", useColors: false })
.option({
name: "number",
description: "Number value",
defaultValue: 0,
parse: (v) => parseInt(v),
})
.getHelpString()
.join("\n")
expect(helpStr).toBe(
"Usage: test [command] [options]" +
"\n\n" +
"Options:" +
"\n\n" +
" --help|-h Display help information" +
"\n\n" +
" --number Number value (default: 0)" +
"\n"
)
})
test("should print help without command options", () => {
const helpStr = massarg()
.help({ binName: "test", useColors: false })
.option({
name: "number",
description: "Number value",
parse: (v) => parseInt(v),
})
.getHelpString()
.join("\n")
expect(helpStr).toBe(
"Usage: test [command] [options]" +
"\n\n" +
"Options:" +
"\n\n" +
" --help|-h Display help information" +
"\n\n" +
" --number Number value" +
"\n"
)
})
test("should not throw when passing args", () => {
expect(() =>
massarg()
.help({ binName: "test", useColors: false })
.option({
name: "number",
description: "Number value",
parse: (v) => parseInt(v),
})
.getHelpString(["--help"])
.join("\n")
).not.toThrow()
})
test("should print help correctly with only global options", () => {
const helpStr = massarg()
.help({ binName: "test", useColors: false })
.command({
name: "cmd",
description: "Command",
run: () => void 0,
})
.option({
name: "number",
description: "Number value",
parse: (v) => parseInt(v),
})
.getHelpString()
.join("\n")
expect(helpStr).toBe(
"Usage: test [command] [options]" +
"\n\n" +
"Commands:" +
"\n\n" +
" cmd Command" +
"\n\n" +
"Options:" +
"\n\n" +
" --help|-h Display help information" +
"\n\n" +
" --number Number value" +
"\n"
)
})
test("should print help correctly with command options", () => {
const helpStr = massarg()
.help({ binName: "test", useColors: false })
.command({
name: "cmd",
description: "Command",
run: () => void 0,
})
.option({
name: "number",
description: "Number value",
commands: "cmd",
parse: (v) => parseInt(v),
})
.getHelpString()
.join("\n")
expect(helpStr).toBe(
"Usage: test [command] [options]" +
"\n\n" +
"Commands:" +
"\n\n" +
" cmd Command" +
"\n\n" +
"Command Options:" +
"\n\n" +
"cmd:" +
"\n\n" +
" --number Number value" +
"\n\n" +
"Global Options:" +
"\n\n" +
" --help|-h Display help information" +
"\n"
)
})
})

View File

@@ -1,268 +0,0 @@
import chalk from "chalk"
import massarg from "../src"
import { OptionDef } from "../src/types"
describe("Options", () => {
describe("basics", () => {
test("should parse properly", () => {
const options = massarg()
.option({
name: "number",
parse: (v) => parseInt(v),
})
.parseArgs(["--number", "10"])
expect(options).toHaveProperty("number", 10)
})
test("should read from alias", () => {
const options = massarg()
.option({
name: "number",
aliases: ["n"],
parse: (v) => parseInt(v),
})
.parseArgs(["-n", "10"])
expect(options).toHaveProperty("number", 10)
expect(options).toHaveProperty("n", 10)
})
})
describe("parsing", () => {
test("should camelCase names", () => {
const options = massarg()
.option({
name: "is-number",
aliases: ["n"],
parse: (v) => parseInt(v),
})
.parseArgs(["--is-number", "10"])
expect(options).toHaveProperty("isNumber", 10)
expect(options).toHaveProperty("is-number", 10)
expect(options).toHaveProperty("n", 10)
})
describe("bool", () => {
test("should parse bool in correct forms", () => {
const boolOpt = {
name: "bool",
aliases: ["b"],
boolean: true,
} as OptionDef<any, any>
const defOpt = {
name: "default",
aliases: ["d"],
isDefault: true,
}
const numOpt = {
name: "num",
aliases: ["n"],
parse: Number,
}
expect(massarg().option(boolOpt).parseArgs(["--bool"])).toHaveProperty("bool", true)
// 1/0
expect(massarg().option(boolOpt).parseArgs(["--bool", "1"])).toHaveProperty("bool", true)
expect(massarg().option(boolOpt).option(defOpt).parseArgs(["--bool", "1", "def"])).toHaveProperty("bool", true)
expect(massarg().option(boolOpt).parseArgs(["--bool", "0"])).toHaveProperty("bool", false)
// on/off
expect(massarg().option(boolOpt).parseArgs(["--bool", "on"])).toHaveProperty("bool", true)
expect(massarg().option(boolOpt).option(defOpt).parseArgs(["--bool", "on", "def"])).toHaveProperty("bool", true)
expect(massarg().option(boolOpt).parseArgs(["--bool", "off"])).toHaveProperty("bool", false)
// true/false
expect(massarg().option(boolOpt).parseArgs(["--bool", "true"])).toHaveProperty("bool", true)
expect(massarg().option(boolOpt).option(defOpt).parseArgs(["--bool", "true", "def"])).toHaveProperty(
"bool",
true
)
expect(massarg().option(boolOpt).parseArgs(["--bool", "false"])).toHaveProperty("bool", false)
// yes/no
expect(massarg().option(boolOpt).parseArgs(["--bool", "yes"])).toHaveProperty("bool", true)
expect(massarg().option(boolOpt).option(defOpt).parseArgs(["--bool", "yes", "def"])).toHaveProperty(
"bool",
true
)
expect(massarg().option(boolOpt).parseArgs(["--bool", "no"])).toHaveProperty("bool", false)
// [none]
expect(
massarg().option(boolOpt).option(defOpt).option(numOpt).parseArgs(["--bool", "someName"])
).toHaveProperty("bool", true)
expect(
massarg().option(boolOpt).option(defOpt).option(numOpt).parseArgs(["--bool", "--num", "1", "someName"])
).toHaveProperty("bool", true)
// [none] - negation
expect(
massarg().option(boolOpt).option(defOpt).option(numOpt).parseArgs(["--no-bool", "--num", "1", "someName"])
).toHaveProperty("bool", true)
expect(
massarg().option(boolOpt).option(defOpt).option(numOpt).parseArgs(["-!b", "--num", "1", "someName"])
).toHaveProperty("bool", true)
})
})
describe("array", () => {
test("should parse array in correct forms", () => {
const opts = {
name: "array",
array: true,
} as OptionDef<any, any>
const arr0el = massarg().option(opts).parseArgs([])
expect(arr0el).toHaveProperty("array", [])
const arr1el = massarg().option(opts).parseArgs(["--array", "something"])
expect(arr1el).toHaveProperty("array", ["something"])
const arr2el = massarg().option(opts).parseArgs(["--array", "something", "--array", "another"])
expect(arr2el).toHaveProperty("array", ["something", "another"])
})
})
test("should expect value when not bool", () => {
expect(() =>
massarg()
.option({
name: "number",
parse: (v) => parseInt(v),
})
.parseArgs(["--number"])
).toThrow("Missing value for: number")
})
describe("isDefault", () => {
test("should parse default with one default", () => {
const options = massarg()
.option({
name: "number",
isDefault: true,
parse: (v) => parseInt(v),
})
.option({
name: "bool",
isDefault: false,
boolean: true,
})
.parseArgs(["1"])
expect(options).toHaveProperty("number", 1)
expect(options).not.toHaveProperty("bool", true)
})
test("should parse default with multiple defaults", () => {
const options = massarg()
.option({
name: "number",
isDefault: true,
parse: (v) => parseInt(v),
})
.option({
name: "bool",
isDefault: true,
boolean: true,
})
.parseArgs(["1"])
expect(options).toHaveProperty("number", 1)
expect(options).toHaveProperty("bool", true)
})
test("should parse default with command", () => {
const options = massarg()
.command({
name: "cmd",
run: () => void 0,
})
.option({
name: "number",
isDefault: true,
parse: (v) => parseInt(v),
})
.parseArgs(["cmd", "1"])
expect(options).toHaveProperty("number", 1)
})
})
describe("required", () => {
const mockProcessExit = jest.spyOn(process, "exit").mockImplementation((code) => {
throw new Error(`Process.exit(${code})`) // Forces the code to throw instead of exit
})
beforeEach(() => {
mockProcessExit.mockClear()
})
test("should throw on missing required value", () => {
const mockConsoleError = jest.spyOn(console, "error").mockImplementation(() => void 0)
const mockConsoleLog = jest.spyOn(console, "log").mockImplementation(() => void 0)
expect(() =>
massarg()
.option({
name: "number",
parse: (v) => parseInt(v),
required: true,
})
.parse(["--not-number", "abcdefg"])
).toThrow("Process.exit(1)")
expect(mockConsoleError).toBeCalledWith(
chalk.red`Option: \`number\` is required, but was not defined. Try using: \`--number \{value\}\``
)
mockConsoleError.mockRestore()
mockConsoleLog.mockRestore()
})
test("should not throw on existing required value for command", () => {
const mockConsoleError = jest.spyOn(console, "error").mockImplementation(() => void 0)
const mockConsoleLog = jest.spyOn(console, "log").mockImplementation(() => void 0)
expect(() =>
massarg()
.option({
name: "number",
parse: (v) => parseInt(v),
commands: "cmd",
required: true,
})
.command({
name: "cmd",
run: () => void 0,
})
.parse(["cmd", "--number", "10"])
).not.toThrow("Option: `number` is required, but was not defined. Try using: `--number {value}`")
mockConsoleError.mockRestore()
mockConsoleLog.mockRestore()
})
test("should throw on missing required value for command", () => {
const mockConsoleError = jest.spyOn(console, "error").mockImplementation(() => void 0)
const mockConsoleLog = jest.spyOn(console, "log").mockImplementation(() => void 0)
expect(() =>
massarg()
.option({
name: "number",
parse: (v) => parseInt(v),
commands: "cmd",
required: true,
})
.command({
name: "cmd",
run: () => void 0,
})
.parse(["cmd"])
).toThrow("Process.exit(1)")
expect(mockConsoleError).toBeCalledWith(
chalk.red`Option: \`number\` is required for command: \`cmd\`, but was not defined. Try using: \`--number \{value\}\``
)
mockConsoleError.mockRestore()
mockConsoleLog.mockRestore()
})
})
})
})

View File

@@ -1,21 +0,0 @@
import chalk from "chalk"
import { color, colorCount } from "../src/utils"
describe("Utils", () => {
describe("Color", () => {
test("should reset color around text", () => {
expect(color("green", "test")).toBe(chalk.reset`${chalk.green`test`}`)
})
test("should work with arrays", () => {
expect(color(["green", "bgGray"], "test")).toBe(chalk.reset`${chalk.bgGray`${chalk.green`test`}`}`)
})
})
describe("Color Count", () => {
test("should parse correct color counts", () => {
expect(colorCount(["dim", "yellow"])).toBe(2)
expect(colorCount(["dim", "yellow"], "green")).toBe(3)
expect(colorCount(["dim", "yellow"], "green", ["red"])).toBe(4)
})
})
})

View File

@@ -1,261 +0,0 @@
import { ArrayOr } from "./utils"
import chalk from "chalk"
export type OptionsBase = {
/** When `true`, will output help text and exit the process without error */
help: boolean
/** This contains arguments that were not taken by any command or option. */
extras: string[]
}
export type MainDef<Options> = (options: Options & OptionsBase) => void
export interface Named {
name: string
aliases?: string[]
}
export interface OptionDef<Options, Value> extends Named {
/** Option name. When using in CLI, you will use `--name`, e.g. `--my-option`. */
name: string
/**
* When `true`, any args placed without name will be applied to this option. When more than one arg is supplied
* this way, only the last given will be used (unless the option is an array type).
* When more than one option has this turned on, they will all be given these values. Use carefully. */
isDefault?: boolean
/**
* In addition to primary name, you may also define aliases. Aliases when used in CLI should only be prefixed with
* 1 hypen, as such: `-o`
*/
aliases?: string[]
/** Description of option to display in help text. */
description?: string
/** Default value to use when none is supplied. */
defaultValue?: Value
/**
* A boolean field may be supplied without value, which will cause it to parse as `true`.
* Additional parsing may still be done using the `parse` option.
*/
boolean?: boolean
/**
* An array field will collect any inputs to it into a list. Each item in the list will be parsed with `parse` before
* being added.
*/
array?: boolean
/**
* A required option will throw an error if it's not passed as input. If you attach this option to a specific command
* (or multiple commands), it will only be required when using that command. If not, it will be required for any
* command.
*/
required?: boolean
/**
* Commands this option is relevant for. You may use either name or alias of command, but in the help text, only the
* name will be shown as the section title.
*
* If you supply none, this option will be a global option by default.
*
* **Please note** this does not affect parsing: every option will be available when the program is run. This is only
* for organizing it properly in the help text. But as long as you don't use that option in a command that doesn't
* need o use it, nothing should work differently.
*/
commands?: ArrayOr<string>
/**
* Use this function to decide what to do with the arg that was passed. When ommitted, the alue will be returned
* as-is, which is always a string. You may cast or convert your input here so that it will be available already
* parsed when it's needed.
*
* @param value The string arg that was passed
* @param options Any already-parsed options in the current context. The order is not guaranteed, so some args will
* not necessarily be parsed before this one.
*/
parse?(value: string, options: Options & OptionsBase): Value
}
export interface CommandDef<T> extends Named {
/**
* Command name. When using in CLI, you will use `name` without any prefixes, unlike options.
* Also, the first command that was parsed will run, and the others will be skipped.
*/
name: string
/** In addition to primary name, you may also define aliases. */
aliases?: string[]
/** Description of command to display in help text. */
description?: string
/**
* This is the function that runs this command.
*
* @param options All the parsed options (or defaults) that were passed in the CLI will be available here.
*/
run(options: T & OptionsBase): void
}
export interface HelpDef {
/**
* Desired width to accommodate to when outputting help to the shell.
* Descriptions that are longer than this value will be wrapped into the next line.
*
* Default: `80`. Use `0` to disable wrapping. */
printWidth?: number
/**
* The name of your application binary. Massarg attempts to infer this using the args, but you may override this
* if you don't like the inferred value.
*/
binName?: string
/**
* A single color or array of colors to use on normal text (descriptions, usage example, etc.)
*
* The colors are passed to `chalk`, so you can use any color `chalk` supports, including foreground and
* background colors.
*
* **Please note** that combining colors may break wrapping, so please test your help output before releasing.
*
* Defaults to `"dim"`
*/
normalColors?: ArrayOr<keyof typeof chalk>
/**
* A single color or array of colors to use on highlighted text (command names, option names, binary name, etc)
*
* The colors are passed to `chalk`, so you can use any color `chalk` supports, including foreground and
* background colors.
*
* **Please note** that combining colors may break wrapping, so please test your help output before releasing.
*
* Defaults to `"yellow"`
*/
highlightColors?: ArrayOr<keyof typeof chalk>
/**
* A single color or array of colors to use on title text ("Options", "Usage", etc)
*
* The colors are passed to `chalk`, so you can use any color `chalk` supports, including foreground and
* background colors.
*
* **Please note** that combining colors may break wrapping, so please test your help output before releasing.
*
* Defaults to `"white"`
*/
titleColors?: ArrayOr<keyof typeof chalk>
/**
* A single color or array of colors to use on subtitle text (e.g. command titles for non-gloal options)
*
* The colors are passed to `chalk`, so you can use any color `chalk` supports, including foreground and
* background colors.
*
* **Please note** that combining colors may break wrapping, so please test your help output before releasing.
*
* Defaults to `["bold", "dim"]`
*/
subtitleColors?: ArrayOr<keyof typeof chalk>
/**
* A single color or array of colors to use on body text (e.g. header and footer)
*
* The colors are passed to `chalk`, so you can use any color `chalk` supports, including foreground and
* background colors.
*
* **Please note** that combining colors may break wrapping, so please test your help output before releasing.
*
* Defaults to `["white"]`
*/
bodyColors?: ArrayOr<keyof typeof chalk>
/**
* Additional content to display below the usage line, and above the rest.
*/
header?: string
/**
* Additional content to display below the commands and options, at the very bottom.
*/
footer?: string
/**
* Separator for command name & its aliases.
*
* Defaults to `" | "`, e.g. for command `"my-cmd"` with aliases `["m", "c"]`, the ourput will be:
*
* ```
* my-cmd | m | c
* ```
*/
commandNameSeparator?: string
/**
* Separator for option name & its aliases.
*
* Defaults to `"|"`, e.g. for option `"my-bool"` with aliases `["m", "b"]`, the ourput will be:
*
* ```
* --my-bool|m|b
* ```
*/
optionNameSeparator?: string
/**
* When `true`, all the command and option names' and descritions' columns will align with each other.
* When `false`, they will all be aligned only within their own category. This can save you white-space when
* you have some long-named options or commands that cause all others to have too much.
*
* Defaults to `false`.
*/
useGlobalColumns?: boolean
/**
* Text to be shown next to the binary name, on the Usage line.
*
* Defaults to `"[command] [options]"`, which outputs:
*
* ```
* my-bin [comman] [options]
* ```
*/
usageExample?: string
/**
* When disabled, all colors in the output will be disabled.
*/
useColors?: boolean
/**
* When disabled, the default values will not be appended to the help text of each option.
*/
includeDefaults?: boolean
/**
* The prefix at the start of the line when presenting examples, at the input (top) line.
*/
exampleInputPrefix?: string
/**
* The prefix at the start of the line when presenting examples, at the output (bottom) line.
*/
exampleOutputPrefix?: string
}
export interface ExampleDef {
/** The input line - to show examples of parameters, commands, etc */
input: string
/** The output line - to show the output of whatever `input` is regarding. */
output?: string
/** An optional description which will be used as a title. */
description?: string
}

View File

@@ -1,88 +0,0 @@
import chalk from "chalk"
// import chunk from "lodash/chunk"
import repeat from "lodash/repeat"
import merge from "lodash/merge"
export function color(color: ArrayOr<keyof typeof chalk>, ...text: any[]): string {
let output: string = undefined as any
for (const c of asArray(color)) {
output = (chalk[c as keyof typeof chalk] as typeof chalk.dim)(...(output ? [output] : text))
}
return chalk.reset(output)
}
export function colorCount(...colors: any[]): number {
return asArray(colors).reduce((all, colorSet) => all + asArray(colorSet).length, 0)
}
export interface WrapOptions {
indent?: number
firstLineIndent?: number
printWidth?: number
colorCount?: number
}
export function wrap(text: string, options?: WrapOptions): string[] {
const _opts = merge(
{
printWidth: 100,
indent: 0,
colorCount: 0,
} as WrapOptions,
options
) as Required<WrapOptions>
const indentSize = _opts.indent ?? 0
const firstIndentSize = _opts.firstLineIndent ?? indentSize
const maxLineLength = _opts.printWidth - firstIndentSize + COLOR_CODE_LEN * _opts.colorCount
function indent(i: number, l: string): string {
return repeat(" ", i === 0 ? firstIndentSize : indentSize) + l
}
if (!_opts.printWidth || maxLineLength <= 0) {
return text.split("\n").map((l, i) => indent(i, l))
}
let lines = chunk(text, maxLineLength).map((l, i) => indent(i, l))
lines = [
lines[0],
...chunk(
lines
.slice(1)
.map((l) => l.trim())
.join(" ")
.trim(),
maxLineLength - indentSize - COLOR_CODE_LEN
).map((l, i) => indent(i + 1, l)),
].filter((l) => l.trim().length)
return lines
}
export const COLOR_CODE_LEN = color("yellow", " ").length - 1
function chunk(text: string, len: number): string[] {
const arr = text.split(" ")
const result = []
let subStr = arr[0]
for (let i = 1; i < arr.length; i++) {
let word = arr[i]
if (subStr.length + word.length + 1 <= len) {
subStr = subStr + " " + word
} else {
result.push(subStr)
subStr = word
}
}
if (subStr.length) {
result.push(subStr)
}
return result
}
export type ArrayOr<T> = T | T[]
export function asArray<T>(obj: T | T[]): T[] {
return Array.isArray(obj) ? obj ?? [] : obj ? [obj] : []
}

View File

@@ -19,7 +19,8 @@
"dev": "tsc --watch",
"cmd": "ts-node src/sample.ts",
"test": "jest",
"docgen": "typedoc --out docs src/**/*.ts --exclude src/sample.ts --plugin typedoc-plugin-zod --theme default",
"doc": "typedoc --out docs src/**/*.ts --exclude src/sample.ts --plugin typedoc-plugin-zod --theme default",
"deploy-doc": "pnpm doc && gh-pages -d docs",
"semantic-release": "semantic-release"
},
"devDependencies": {
@@ -27,6 +28,7 @@
"@semantic-release/git": "^10.0.1",
"@types/jest": "^29.5.8",
"@types/node": "^20.9.2",
"gh-pages": "^6.1.0",
"jest": "^29.7.0",
"semantic-release": "^22.0.8",
"ts-jest": "^29.1.1",

123
pnpm-lock.yaml generated
View File

@@ -22,6 +22,9 @@ devDependencies:
'@types/node':
specifier: ^20.9.2
version: 20.9.2
gh-pages:
specifier: ^6.1.0
version: 6.1.0
jest:
specifier: ^29.7.0
version: 29.7.0(@types/node@20.9.2)(ts-node@10.9.1)
@@ -1189,6 +1192,22 @@ packages:
resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==}
dev: true
/array-union@1.0.2:
resolution: {integrity: sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==}
engines: {node: '>=0.10.0'}
dependencies:
array-uniq: 1.0.3
dev: true
/array-uniq@1.0.3:
resolution: {integrity: sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==}
engines: {node: '>=0.10.0'}
dev: true
/async@3.2.5:
resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
dev: true
/babel-jest@29.7.0(@babel/core@7.23.3):
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -1451,6 +1470,15 @@ packages:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true
/commander@11.1.0:
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
engines: {node: '>=16'}
dev: true
/commondir@1.0.1:
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
dev: true
/compare-func@2.0.0:
resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==}
dependencies:
@@ -1642,6 +1670,10 @@ packages:
resolution: {integrity: sha512-soytjxwbgcCu7nh5Pf4S2/4wa6UIu+A3p03U2yVr53qGxi1/VTR3ENI+p50v+UxqqZAfl48j3z55ud7VHIOr9w==}
dev: true
/email-addresses@5.0.0:
resolution: {integrity: sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==}
dev: true
/emittery@0.13.1:
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
engines: {node: '>=12'}
@@ -1782,6 +1814,20 @@ packages:
is-unicode-supported: 2.0.0
dev: true
/filename-reserved-regex@2.0.0:
resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==}
engines: {node: '>=4'}
dev: true
/filenamify@4.3.0:
resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==}
engines: {node: '>=8'}
dependencies:
filename-reserved-regex: 2.0.0
strip-outer: 1.0.1
trim-repeated: 1.0.0
dev: true
/fill-range@7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'}
@@ -1789,6 +1835,15 @@ packages:
to-regex-range: 5.0.1
dev: true
/find-cache-dir@3.3.2:
resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
engines: {node: '>=8'}
dependencies:
commondir: 1.0.1
make-dir: 3.1.0
pkg-dir: 4.2.0
dev: true
/find-up-simple@1.0.0:
resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==}
engines: {node: '>=18'}
@@ -1878,6 +1933,20 @@ packages:
engines: {node: '>=16'}
dev: true
/gh-pages@6.1.0:
resolution: {integrity: sha512-MdXigvqN3I66Y+tAZsQJMzpBWQOI1snD6BYuECmP+GEdryYMMOQvzn4AConk/+qNg/XIuQhB1xNGrl3Rmj1iow==}
engines: {node: '>=10'}
hasBin: true
dependencies:
async: 3.2.5
commander: 11.1.0
email-addresses: 5.0.0
filenamify: 4.3.0
find-cache-dir: 3.3.2
fs-extra: 11.1.1
globby: 6.1.0
dev: true
/git-log-parser@1.2.0:
resolution: {integrity: sha512-rnCVNfkTL8tdNryFuaY0fYiBWEBcgF748O6ZI61rslBvr2o7U65c2/6npCRqH40vuAhtgtDiqLTJjBVdrejCzA==}
dependencies:
@@ -1924,6 +1993,17 @@ packages:
unicorn-magic: 0.1.0
dev: true
/globby@6.1.0:
resolution: {integrity: sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==}
engines: {node: '>=0.10.0'}
dependencies:
array-union: 1.0.2
glob: 7.2.3
object-assign: 4.1.1
pify: 2.3.0
pinkie-promise: 2.0.1
dev: true
/graceful-fs@4.2.10:
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
dev: true
@@ -2839,6 +2919,13 @@ packages:
resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
dev: true
/make-dir@3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'}
dependencies:
semver: 6.3.1
dev: true
/make-dir@4.0.0:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
@@ -3084,6 +3171,11 @@ packages:
- which
- write-file-atomic
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
dev: true
/once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
@@ -3265,11 +3357,28 @@ packages:
engines: {node: '>=8.6'}
dev: true
/pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
dev: true
/pify@3.0.0:
resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
engines: {node: '>=4'}
dev: true
/pinkie-promise@2.0.1:
resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==}
engines: {node: '>=0.10.0'}
dependencies:
pinkie: 2.0.4
dev: true
/pinkie@2.0.4:
resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==}
engines: {node: '>=0.10.0'}
dev: true
/pirates@4.0.6:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'}
@@ -3685,6 +3794,13 @@ packages:
engines: {node: '>=8'}
dev: true
/strip-outer@1.0.1:
resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==}
engines: {node: '>=0.10.0'}
dependencies:
escape-string-regexp: 1.0.5
dev: true
/supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
@@ -3779,6 +3895,13 @@ packages:
resolution: {integrity: sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==}
dev: true
/trim-repeated@1.0.0:
resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==}
engines: {node: '>=0.10.0'}
dependencies:
escape-string-regexp: 1.0.5
dev: true
/ts-jest@29.1.1(@babel/core@7.23.3)(jest@29.7.0)(typescript@5.2.2):
resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}