Skip to content

Commit 255d2f5

Browse files
committed
test: tests & bug fixes
1 parent ecd7ced commit 255d2f5

6 files changed

Lines changed: 560 additions & 165 deletions

File tree

jest.config.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,18 @@ export default {
2323
// collectCoverageFrom: undefined,
2424

2525
// The directory where Jest should output its coverage files
26-
coverageDirectory: "coverage",
26+
coverageDirectory: 'coverage',
2727

2828
// An array of regexp pattern strings used to skip coverage collection
2929
// coveragePathIgnorePatterns: [
3030
// "/node_modules/"
3131
// ],
3232

3333
// Indicates which provider should be used to instrument code for coverage
34-
coverageProvider: "v8",
34+
coverageProvider: 'v8',
3535

3636
// A list of reporter names that Jest uses when writing coverage reports
37-
coverageReporters: ["json-summary", "json", "text", "lcov", "clover"],
37+
coverageReporters: ['json-summary', 'json', 'text', 'lcov', 'clover'],
3838

3939
// An object that configures minimum threshold enforcement for coverage results
4040
// coverageThreshold: undefined,
@@ -88,7 +88,7 @@ export default {
8888
// notifyMode: "failure-change",
8989

9090
// A preset that is used as a base for Jest's configuration
91-
preset: "ts-jest",
91+
preset: 'ts-jest',
9292

9393
// Run tests from one or more projects
9494
// projects: undefined,
@@ -147,9 +147,7 @@ export default {
147147
// ],
148148

149149
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
150-
// testPathIgnorePatterns: [
151-
// "/node_modules/"
152-
// ],
150+
testPathIgnorePatterns: ['/node_modules/', '/_old/'],
153151

154152
// The regexp pattern or array of patterns that Jest uses to detect test files
155153
// testRegex: [],

src/color.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import z from 'zod'
2+
import { zodEnumFromObjKeys } from './utils'
3+
14
export const ansiStyles = {
25
reset: '\x1b[0m',
36
bold: '\x1b[1m',
@@ -24,12 +27,14 @@ export const ansiColors = {
2427
brightWhite: '\x1b[97m',
2528
}
2629

27-
export type StringStyle = {
28-
color?: keyof typeof ansiColors
29-
bold?: boolean
30-
underline?: boolean
31-
reset?: boolean
32-
}
30+
export const StringStyle = z.object({
31+
bold: z.boolean().optional(),
32+
underline: z.boolean().optional(),
33+
color: zodEnumFromObjKeys(ansiColors).optional(),
34+
reset: z.boolean().optional(),
35+
})
36+
37+
export type StringStyle = z.infer<typeof StringStyle>
3338

3439
export function format(string: string, style: StringStyle = {}): string {
3540
const { color, bold, underline, reset } = style

src/command.ts

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { z } from 'zod'
22
import { isZodError, ParseError, ValidationError } from './error'
3-
import { HelpGenerator } from './help'
3+
import { defaultHelpConfig, HelpConfig, HelpGenerator } from './help'
44
import MassargOption, {
55
MassargFlag,
66
OptionConfig,
77
TypedOptionConfig,
88
MassargHelpFlag,
99
} from './option'
10-
import { setOrPush } from './utils'
10+
import { setOrPush, deepMerge } from './utils'
1111
import MassargExample, { ExampleConfig } from './example'
1212

1313
export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
@@ -26,18 +26,7 @@ export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
2626
.function()
2727
.args(args, z.any())
2828
.returns(z.union([z.promise(z.void()), z.void()])) as z.ZodType<Runner<z.infer<RunArgs>>>,
29-
/**
30-
* Whether to bind the help command to this command
31-
*
32-
* Set this to `true` to automatically add a `help` command to this command's subcommands.
33-
*/
34-
bindHelpCommand: z.boolean().optional(),
35-
/**
36-
* Whether to bind the help option to this command
37-
*
38-
* Set this to `true` to automatically add a `--help` option to this command's options.
39-
*/
40-
bindHelpOption: z.boolean().optional(),
29+
helpConfig: HelpConfig.optional(),
4130
// argsHint: z.string().optional(),
4231
})
4332

@@ -59,19 +48,15 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
5948
options: MassargOption[] = []
6049
examples: MassargExample[] = []
6150
args: Partial<Args> = {}
51+
helpConfig: Required<HelpConfig>
6252

6353
constructor(options: CommandConfig<Args>) {
6454
CommandConfig(z.any()).parse(options)
6555
this.name = options.name
6656
this.description = options.description
6757
this.aliases = options.aliases ?? []
6858
this._run = options.run
69-
if (options.bindHelpCommand) {
70-
this.command(new MassargHelpCommand())
71-
}
72-
if (options.bindHelpOption) {
73-
this.option(new MassargHelpFlag())
74-
}
59+
this.helpConfig = HelpConfig.required().parse(defaultHelpConfig)
7560
}
7661

7762
command<A extends ArgsObject = Args>(config: CommandConfig<A>): MassargCommand<Args>
@@ -110,15 +95,13 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
11095
): MassargCommand<Args> {
11196
try {
11297
const flag = config instanceof MassargFlag ? config : new MassargFlag(config)
113-
if (flag.isDefault) {
114-
const defaultOption = this.options.find((o) => o.isDefault)
115-
if (defaultOption) {
116-
throw new ValidationError({
117-
code: 'duplicate_default_option',
118-
message: `Option "${flag.name}" cannot be set as default because option "${defaultOption.name}" is already set as default`,
119-
path: [this.name, flag.name],
120-
})
121-
}
98+
const existing = this.options.find((c) => c.name === flag.name)
99+
if (existing) {
100+
throw new ValidationError({
101+
code: 'duplicate_flag',
102+
message: `Flag "${flag.name}" already exists`,
103+
path: [this.name, flag.name],
104+
})
122105
}
123106
this.options.push(flag as MassargOption)
124107
return this
@@ -140,6 +123,14 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
140123
try {
141124
const option =
142125
config instanceof MassargOption ? config : MassargOption.fromTypedConfig(config)
126+
const existing = this.options.find((c) => c.name === option.name)
127+
if (existing) {
128+
throw new ValidationError({
129+
code: 'duplicate_option',
130+
message: `Option "${option.name}" already exists`,
131+
path: [this.name, option.name],
132+
})
133+
}
143134
if (option.isDefault) {
144135
const defaultOption = this.options.find((o) => o.isDefault)
145136
if (defaultOption) {
@@ -169,6 +160,20 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
169160
return this
170161
}
171162

163+
help(config: HelpConfig): MassargCommand<Args> {
164+
this.helpConfig = HelpConfig.required().parse(
165+
deepMerge(defaultHelpConfig, config) as HelpConfig,
166+
)
167+
168+
if (this.helpConfig.bindCommand) {
169+
this.command(new MassargHelpCommand())
170+
}
171+
if (this.helpConfig.bindOption) {
172+
this.option(new MassargHelpFlag())
173+
}
174+
return this
175+
}
176+
172177
main<A extends ArgsObject = Args>(run: Runner<A>): MassargCommand<Args> {
173178
this._run = run
174179
return this
@@ -217,46 +222,50 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
217222
let _args: Args = { ...this.args, ...args } as Args
218223
let _argv = [...argv]
219224
const _a = this.args as Record<string, string[]>
225+
226+
// fill defaults
227+
for (const option of this.options) {
228+
if (option.defaultValue !== undefined && _a[option.name] === undefined) {
229+
_args[option.name as keyof Args] = option.defaultValue as Args[keyof Args]
230+
}
231+
}
232+
233+
// parse options
220234
while (_argv.length) {
221235
const arg = _argv.shift()!
222236
const found = this.options.some((o) => o._isOption(arg))
223237
if (found) {
224-
const option = this.options.find((o) => o._match(arg))
225-
if (!option) {
226-
throw new ValidationError({
227-
path: [MassargOption.getName(arg)],
228-
code: 'unknown_option',
229-
message: 'Unknown option',
230-
})
231-
}
232-
const res = option._parseDetails(argv)
233-
_args[res.key as keyof Args] = setOrPush<Args[keyof Args]>(
234-
res.value,
235-
_args[res.key as keyof Args],
236-
option.isArray,
237-
)
238+
_argv = this.parseOption(arg, _argv)
239+
_args = { ..._args, ...this.args }
238240
continue
239241
}
240242

241243
const command = this.commands.find((c) => c.name === arg || c.aliases.includes(arg))
242244
if (command) {
245+
// this is dry run, just exit
243246
if (!parseCommands) {
244247
break
245248
}
249+
// this is real run, parse command, pass unparsed args
246250
return command.parse(_argv, this.args, parent ?? this)
247251
}
252+
// default option - passes arg value even without flag name
248253
const defaultOption = this.options.find((o) => o.isDefault)
249254
if (defaultOption) {
250255
_argv = this.parseOption(`--${defaultOption.name}`, [arg, ..._argv])
251256
continue
252257
}
258+
// not parsed by any step, add to extra key
253259
_a.extra ??= []
254260
_a.extra.push(arg)
255261
}
262+
this.args = { ...this.args, ..._args }
263+
// dry run, just exit
256264
if (!parseCommands) {
257-
return _args
265+
return this.args as Args
258266
}
259-
this.args = { ...this.args, ..._args }
267+
268+
// no sub command found, run main command
260269
if (this._run) {
261270
this._run(this.args, parent ?? this)
262271
}

0 commit comments

Comments
 (0)