diff --git a/examples/01-basic-flags/package.json b/examples/01-basic-flags/package.json index f90fff6..7d78c5b 100644 --- a/examples/01-basic-flags/package.json +++ b/examples/01-basic-flags/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "npm run build && node lib/index.js", + "start": "node --enable-source-maps lib/index.js", "build": "tsc" }, "keywords": [], diff --git a/examples/02-error-handling/package.json b/examples/02-error-handling/package.json index 9771a41..4f2ea31 100644 --- a/examples/02-error-handling/package.json +++ b/examples/02-error-handling/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "npm run build && node lib/index.js", + "start": "node --enable-source-maps lib/index.js", "build": "tsc" }, "keywords": [], diff --git a/examples/03-simple-commands/package.json b/examples/03-simple-commands/package.json index ebd4a96..399a17d 100644 --- a/examples/03-simple-commands/package.json +++ b/examples/03-simple-commands/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "npm run build && node lib/index.js", + "start": "node --enable-source-maps lib/index.js", "build": "tsc" }, "keywords": [], diff --git a/examples/04-package-manager/package.json b/examples/04-package-manager/package.json index ca31683..3a67355 100644 --- a/examples/04-package-manager/package.json +++ b/examples/04-package-manager/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "npm run build && node lib/index.js", + "start": "node --enable-source-maps lib/index.js", "build": "tsc" }, "keywords": [], diff --git a/examples/05-application-config/package.json b/examples/05-application-config/package.json index 75c806f..8a322b3 100644 --- a/examples/05-application-config/package.json +++ b/examples/05-application-config/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "npm run build && node lib/index.js", + "start": "node --enable-source-maps lib/index.js", "build": "tsc" }, "keywords": [], diff --git a/examples/06-builtin-commands/package.json b/examples/06-builtin-commands/package.json index 4ab779f..3057b32 100644 --- a/examples/06-builtin-commands/package.json +++ b/examples/06-builtin-commands/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "npm run build && node lib/index.js", + "start": "node --enable-source-maps lib/index.js", "build": "tsc" }, "keywords": [], diff --git a/examples/07-prompting/package.json b/examples/07-prompting/package.json index e48c98e..a557da1 100644 --- a/examples/07-prompting/package.json +++ b/examples/07-prompting/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "npm run build && node lib/index.js", + "start": "node --enable-source-maps lib/index.js", "build": "tsc" }, "keywords": [], diff --git a/examples/07-prompting/src/index.ts b/examples/07-prompting/src/index.ts index e89bca2..d505d8e 100644 --- a/examples/07-prompting/src/index.ts +++ b/examples/07-prompting/src/index.ts @@ -9,12 +9,6 @@ export const parserOpts: ParserOpts = { programVersion: 'v1' } -// Provide a custom resolver for the username key. -// This does have the downside that it will *always* try and resolve the key -// whether the user provides the flag or not. -// -// If this distinction matters, use an Argument and override the `resolveDefault` method -// to control the behaviour dependant on specificity class UsernamePromptResolver extends Resolver { private readonly rl: readline.Interface constructor (id: string) { @@ -25,9 +19,9 @@ class UsernamePromptResolver extends Resolver { }) } - async keyExists (key: string): Promise { + async keyExists (key: string, userDidPassArg: boolean): Promise { // We only care about resolving our username argument - return key === 'username' + return key === 'username' && userDidPassArg } async resolveKey (): Promise { diff --git a/examples/08-command-context/package-lock.json b/examples/08-command-context/package-lock.json new file mode 100644 index 0000000..e595318 --- /dev/null +++ b/examples/08-command-context/package-lock.json @@ -0,0 +1,47 @@ +{ + "name": "08-command-context", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "08-command-context", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "args.ts": "file:../..", + "typescript": "^5.1.6" + } + }, + "node_modules/args.ts": { + "version": "1.1.1", + "resolved": "file:../..", + "license": "OSL-3.0", + "engines": { + "node": "18.x" + } + }, + "node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + }, + "dependencies": { + "args.ts": { + "version": "1.1.1" + }, + "typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==" + } + } +} diff --git a/examples/08-command-context/package.json b/examples/08-command-context/package.json new file mode 100644 index 0000000..f74a4bf --- /dev/null +++ b/examples/08-command-context/package.json @@ -0,0 +1,17 @@ +{ + "name": "08-command-context", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node --enable-source-maps lib/index.js", + "build": "tsc" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "args.ts": "file:../..", + "typescript": "^5.1.6" + } +} diff --git a/examples/08-command-context/src/index.ts b/examples/08-command-context/src/index.ts new file mode 100644 index 0000000..89ddf67 --- /dev/null +++ b/examples/08-command-context/src/index.ts @@ -0,0 +1,54 @@ +#!/usr/bin/env node +/* eslint-disable @typescript-eslint/explicit-function-return-type */ + +import { Args, Command, CommandRunner, ParserOpts, util } from 'args.ts' + +export const parserOpts: ParserOpts = { + programName: '08-command-context', + programDescription: 'description', + programVersion: 'v1' +} + +interface Context { + value: string +} + +abstract class BaseCommand extends Command { + abstract runWithContext: CommandRunner + + run = (): never => { + throw new TypeError('run called, expected runWithContext') + } +} + +class ConcreteCommand extends BaseCommand { + runWithContext: CommandRunner = this.runner(async (args, context) => { + console.log('Ran with context:', context) + }) + + args = (parser: Args<{}>) => { + return parser + } +} + +async function main (): Promise { + const parser = new Args(parserOpts) + .command(['cmd'], new ConcreteCommand({ + parserOpts, + description: 'command' + })) + + const result = await parser.parse(util.makeArgs()) + + if (result.mode !== 'command') { + console.error('Did not get command back') + return + } + + const command = result.command + await command.runWithContext({}, { + value: 'the context value!' + }) +} + +main().catch(console.error) diff --git a/examples/08-command-context/tsconfig.json b/examples/08-command-context/tsconfig.json new file mode 100644 index 0000000..f9cb42a --- /dev/null +++ b/examples/08-command-context/tsconfig.json @@ -0,0 +1,72 @@ +{ + "exclude": [ + "node_modules", + "test", + "lib" + ], + "compilerOptions": { + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + "lib": ["ESNext"], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "lib", /* Redirect output structure to the directory. */ + "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } + } + \ No newline at end of file diff --git a/src/args.ts b/src/args.ts index 3dcf104..8b67ae5 100644 --- a/src/args.ts +++ b/src/args.ts @@ -16,10 +16,10 @@ interface FoundCommand { executionResult: unknown } -interface ReturnedCommand { +interface ReturnedCommand { mode: 'command' - command: Command - parsedArgs: T + command: TCommand + parsedArgs: TArgs } interface ParsedArgs { @@ -27,7 +27,7 @@ interface ParsedArgs { args: T } -type ParseSuccess = FoundCommand | ReturnedCommand | ParsedArgs +type ParseSuccess = FoundCommand | ReturnedCommand | ParsedArgs export interface DefaultArgTypes { [k: string]: CoercedValue ['--']?: string @@ -52,7 +52,7 @@ export interface ArgsState { * * It will hold all the state needed to parse inputs. This state is modified through the various helper methods defined on this class. */ -export class Args { +export class Args { public readonly opts: StoredParserOpts public readonly _state: ArgsState @@ -116,11 +116,11 @@ export class Args { * @param inherit - Whether to inherit arguments from this configuration into the parser * @returns this */ - public command ( - [name, ...aliases]: [`${TName}`, ...string[]], - command: TCommand, + public command ( + [name, ...aliases]: [string, ...string[]], + command: T, inherit = false - ): Args { + ): Args { if (this._state.commands.has(name)) { throw new CommandError(`command '${name}' already declared`) } @@ -183,6 +183,7 @@ export class Args { }) } + // @ts-expect-error erased commands can't resolve into concrete type return this } @@ -408,8 +409,8 @@ export class Args { * @param executeCommands - Whether to execute discovered commands, or return them * @returns The result of the parse */ - public async parseToResult (argString: string | string[], executeCommands = false): Promise, ParseError | CoercionError[] | CommandError>> { - this.opts.logger.debug(`Beginning parse of input '${argString}'`) + public async parseToResult (argString: string | string[], executeCommands = false): Promise, ParseError | CoercionError[] | CommandError>> { + this.opts.logger.internal(`Beginning parse of input '${argString}'`) const tokenResult = tokenise(Array.isArray(argString) ? argString.join(' ') : argString) @@ -495,6 +496,7 @@ export class Args { } // Command was found, return it + // @ts-expect-error erased commands can't resolve into concrete type return Ok({ mode: 'command', parsedArgs: this.intoObject(args, rest?.value), @@ -509,7 +511,7 @@ export class Args { * @param executeCommands - Whether to execute discovered commands, or return them * @returns The result of the parse, never an error */ - public async parse (argString: string | string[], executeCommands = false): Promise> { + public async parse (argString: string | string[], executeCommands = false): Promise> { const result = await this.parseToResult(argString, executeCommands) if (!result.ok) { diff --git a/src/builder/builtin.ts b/src/builder/builtin.ts index 70e8fc6..65f4d92 100644 --- a/src/builder/builtin.ts +++ b/src/builder/builtin.ts @@ -58,8 +58,21 @@ export abstract class Builtin { * @returns The generated help string */ public helpInfo (): string { - return `${this.commandTriggers.map(cmd => `${cmd} <...args>`).join(', ')} | ${this.argumentTriggers.map(arg => `--${arg}`).join(', ')}` + const commands = this.commandTriggers.map(cmd => `${cmd} <...args>`).join(', ') + const args = this.argumentTriggers.map(arg => `--${arg}`).join(', ') + + if (commands && args) { + return `${commands} | ${args}` + } + + if (commands) { + return commands + } + + if (args) { + return args + } + + return `${this.constructor.name} | no triggers` } } - -export type BuiltinType = 'help' | 'completion' | 'version' | 'fallback' diff --git a/src/builder/command.ts b/src/builder/command.ts index 7aeba97..d794247 100644 --- a/src/builder/command.ts +++ b/src/builder/command.ts @@ -2,13 +2,16 @@ import { Args, DefaultArgTypes } from '../args' import { CommandError } from '../error' import { InternalCommand } from '../internal/parse/types' import { CommandOpts, StoredCommandOpts, defaultCommandOpts, defaultParserOpts } from '../opts' -import { ArgType } from '../util' +import { ArgType, Logger } from '../util' + +export type CommandRunner = (args: ArgType>, ...rest: TRest) => Promise /** * Base class for all commands, including subcommands. Any user implemented command must extend from this class. */ export abstract class Command { public readonly opts: StoredCommandOpts + protected readonly log: Logger constructor ( opts: CommandOpts @@ -21,6 +24,8 @@ export abstract class Command { ...opts.parserOpts } } + + this.log = this.opts.parserOpts.logger } /** @@ -31,15 +36,17 @@ export abstract class Command { // Must use any for it to accept the subtyping this function actually performs // Black magic happens later on to extract the real subtype out of this `any` abstract args: (parser: Args) => Args - abstract run: (args: ArgType>) => Promise + abstract run: CommandRunner /** * Creates a runner function for use with {@link Command#run}. This exists to provide type inference to the callback, which is not available without a function call. * @param runFn - the run function * @returns - the run implementation */ - runner (runFn: (args: (ArgType> & DefaultArgTypes)) => Promise): (args: ArgType>) => Promise { - return async (args) => await runFn(args) + runner ( + runFn: (args: (ArgType> & DefaultArgTypes), ...rest: T) => Promise + ): (args: ArgType>, ...rest: T) => Promise { + return async (args, ...rest) => await runFn(args, ...rest) } /** diff --git a/src/builder/default-resolvers.ts b/src/builder/default-resolvers.ts index e80ee7e..2568e63 100644 --- a/src/builder/default-resolvers.ts +++ b/src/builder/default-resolvers.ts @@ -3,7 +3,7 @@ import { StoredParserOpts } from '../opts' import { Resolver } from './resolver' export class EnvironmentResolver extends Resolver { - async keyExists (key: string, opts: StoredParserOpts): Promise { + async keyExists (key: string, _: boolean, opts: StoredParserOpts): Promise { const envKey = `${opts.environmentPrefix}_${key.toUpperCase()}` const platform = currentPlatform() return platform.getEnv(envKey) !== undefined diff --git a/src/builder/resolver.ts b/src/builder/resolver.ts index 559ca7a..87fd1d3 100644 --- a/src/builder/resolver.ts +++ b/src/builder/resolver.ts @@ -13,9 +13,10 @@ export abstract class Resolver { /** * Determine whether this resolver can resolve the provided key. * @param key - The key to check + * @param userDidPassArg - Whether the user provided an argument or not * @param opts - The parser opts */ - abstract keyExists (key: string, opts: StoredParserOpts): Promise + abstract keyExists (key: string, userDidPassArg: boolean, opts: StoredParserOpts): Promise /** * Resolve the provided key to its string value. * diff --git a/src/internal/parse/coerce.ts b/src/internal/parse/coerce.ts index b66a733..24c6ea6 100644 --- a/src/internal/parse/coerce.ts +++ b/src/internal/parse/coerce.ts @@ -90,7 +90,7 @@ async function resolveArgumentDefault ( const key = argument.type === 'flag' ? argument.longFlag : argument.key for (const resolver of resolvers) { - if (await resolver.keyExists(key, opts)) { + if (await resolver.keyExists(key, false, opts)) { const value = await resolver.resolveKey(key, opts) if (!value) { diff --git a/src/internal/parse/schematic-validation.ts b/src/internal/parse/schematic-validation.ts index ad9889f..d6884fe 100644 --- a/src/internal/parse/schematic-validation.ts +++ b/src/internal/parse/schematic-validation.ts @@ -37,6 +37,8 @@ export async function validateFlagSchematically ( } } + const userDidProvideArgs = (foundFlags ?? []).length > 0 + let { resolveDefault, optional, dependencies, conflicts, exclusive, requiredUnlessPresent } = argument.inner._state const [specifiedDefault, unspecifiedDefault] = await Promise.all([resolveDefault('specified'), resolveDefault('unspecified')]) @@ -44,7 +46,7 @@ export async function validateFlagSchematically ( let resolversHaveValue = false for (const resolver of resolvers) { - if (await resolver.keyExists(argument.longFlag, opts)) { + if (await resolver.keyExists(argument.longFlag, userDidProvideArgs, opts)) { resolversHaveValue = true } } @@ -110,7 +112,7 @@ export async function validatePositionalSchematically ( let resolversHaveValue = false for (const middleware of resolvers) { - if (await middleware.keyExists(argument.key, opts)) { + if (await middleware.keyExists(argument.key, foundFlag !== undefined, opts)) { resolversHaveValue = true } } diff --git a/src/internal/util.ts b/src/internal/util.ts index e742a06..581e715 100644 --- a/src/internal/util.ts +++ b/src/internal/util.ts @@ -17,11 +17,11 @@ export function getAliasDenotion (alias: FlagAlias): string { } } -const flagValidationRegex = /-+(?:[a-z]+)/ +const flagValidationRegex = /-+(?:[a-zA-Z]+)/ export function internaliseFlagString (flag: string): ['long' | 'short', string] { if (!flagValidationRegex.test(flag)) { - throw new SchemaError(`flags must match '--abcdef...' or '-abcdef' got '${flag}'`) + throw new SchemaError(`flags must match '--abcdefABCDEF' or '-abcdefABCDEF' got '${flag}'`) } // Long flag diff --git a/src/util/help.ts b/src/util/help.ts index 70043b2..f113a91 100644 --- a/src/util/help.ts +++ b/src/util/help.ts @@ -21,9 +21,9 @@ export function generateHelp (parser: Args<{}>): string { if (value.aliases.length) { if (isMultiType) { - return `[--${value.longFlag}${value.aliases.map(getAliasDenotion).join(' | ')}<${value.inner.type}...>]` + return `[--${value.longFlag} | ${value.aliases.map(getAliasDenotion).join(' | ')} <${value.inner.type}...>]` } - return `[--${value.longFlag}${value.aliases.map(getAliasDenotion).join(' | ')}<${value.inner.type}>]` + return `[--${value.longFlag} | ${value.aliases.map(getAliasDenotion).join(' | ')} <${value.inner.type}>]` } return `[--${value.longFlag} <${value.inner.type}>]` } else { @@ -36,9 +36,9 @@ export function generateHelp (parser: Args<{}>): string { if (value.aliases.length) { if (isMultiType) { - return `(--${value.longFlag}${value.aliases.map(getAliasDenotion).join(' | ')}<${value.inner.type}...>)` + return `(--${value.longFlag} | ${value.aliases.map(getAliasDenotion).join(' | ')} <${value.inner.type}...>)` } - return `(--${value.longFlag}${value.aliases.map(getAliasDenotion).join(' | ')}<${value.inner.type}>)` + return `(--${value.longFlag} | ${value.aliases.map(getAliasDenotion).join(' | ')} <${value.inner.type}>)` } return `(--${value.longFlag} <${value.inner.type}>)` diff --git a/src/util/logging.ts b/src/util/logging.ts index 619bec1..40c5f74 100644 --- a/src/util/logging.ts +++ b/src/util/logging.ts @@ -2,6 +2,7 @@ interface Stringifiable { toString: () => string } type LoggingFunction = (...args: Stringifiable[]) => T const LEVEL_TO_CONSOLE: Record (...args: unknown[]) => void> = { + internal: () => console.trace, trace: () => console.trace, debug: () => console.debug, info: () => console.log, @@ -11,7 +12,8 @@ const LEVEL_TO_CONSOLE: Record (...args: unknown[]) => void> = { } const LEVEL_TO_NUMBER: Record = { - trace: 0, + internal: 0, + trace: 1, debug: 10, info: 20, warn: 30, @@ -22,13 +24,14 @@ const LEVEL_TO_NUMBER: Record = { /** * The levels which a {@link Logger} can operate at. */ -export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' +export type LogLevel = 'internal' | 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' /** * The logging class used internally to (configurably) inform users about library behaviour. * This is a thin wrapper around the {@link console}, and should generally be set to something above 'info' in production. */ export class Logger { + internal = this.makeLevelFunc('internal', false) trace = this.makeLevelFunc('trace', false) debug = this.makeLevelFunc('debug', false) info = this.makeLevelFunc('info', false) @@ -51,12 +54,12 @@ export class Logger { const ourLevel = LEVEL_TO_NUMBER[this.level] const targetLevel = LEVEL_TO_NUMBER[level] - if (ourLevel >= targetLevel) { + if (ourLevel > targetLevel) { return } - const fn = LEVEL_TO_CONSOLE[this.level]() - fn(`[${this.name}]`, new Date().toISOString(), ':', ...args) + const fn = LEVEL_TO_CONSOLE[level]() + fn(`[${level.toUpperCase()}]`.padEnd(7), `[${this.name}]`, new Date().toISOString(), ':', ...args) if (exit) { process.exit() diff --git a/test/parsing/utils.ts b/test/parsing/utils.ts index ab946f6..3874939 100644 --- a/test/parsing/utils.ts +++ b/test/parsing/utils.ts @@ -1,6 +1,6 @@ import assert from 'assert' -import { ArgsState, MinimalArgument, StoredParserOpts, defaultCommandOpts } from '../../src' +import { ArgsState, Command, MinimalArgument, StoredParserOpts, defaultCommandOpts, defaultParserOpts } from '../../src' import { CoercedArguments, coerce } from '../../src/internal/parse/coerce' import { tokenise } from '../../src/internal/parse/lexer' import { ParsedArguments, parse } from '../../src/internal/parse/parser' @@ -20,17 +20,18 @@ export function makeInternalCommand ( aliases: aliases ?? [], isBase: true, inner: { + log: defaultParserOpts.logger, _subcommands: subcommands ?? {}, - args: p => p, + args: (p: any) => p, opts: { description: description ?? `${name} command description`, parserOpts: opts, ...defaultCommandOpts }, - run: p => p, - runner: p => p, - subcommand: p => ({} as any) - }, + run: (p: any) => p, + runner: (p: any) => p, + subcommand: (p: any) => ({} as any) + } as unknown as Command, parser: ({} as any) } } diff --git a/test/schema/validation.test.ts b/test/schema/validation.test.ts index a914d8c..e90b20e 100644 --- a/test/schema/validation.test.ts +++ b/test/schema/validation.test.ts @@ -30,7 +30,7 @@ describe('Schema validation', () => { expect(() => { // @ts-expect-error we are testing runtime validation, for JS users, or people who dont like playing by the rules parser.arg(['-1'], a.string()) - }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdef...' or '-abcdef' got '-1'"`) + }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdefABCDEF' or '-abcdefABCDEF' got '-1'"`) }) it('rejects positionals not prefixed by <', () => { @@ -57,7 +57,7 @@ describe('Schema validation', () => { expect(() => { // @ts-expect-error we are testing runtime validation, for JS users, or people who dont like playing by the rules parser.arg(['--flag', '1'], a.string()) - }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdef...' or '-abcdef' got '1'"`) + }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdefABCDEF' or '-abcdefABCDEF' got '1'"`) }) it('rejects long flags that do not have a valid ID', () => { @@ -65,7 +65,7 @@ describe('Schema validation', () => { expect(() => { parser.arg(['--1'], a.string()) - }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdef...' or '-abcdef' got '--1'"`) + }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdefABCDEF' or '-abcdefABCDEF' got '--1'"`) }) it('rejects short flags that do not have a valid ID', () => { @@ -73,7 +73,7 @@ describe('Schema validation', () => { expect(() => { parser.arg(['--flag', '-1'], a.string()) - }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdef...' or '-abcdef' got '-1'"`) + }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdefABCDEF' or '-abcdefABCDEF' got '-1'"`) }) it('rejects duplicate long flags', () => { diff --git a/test/util.test.ts b/test/util.test.ts index d4166ff..89541f4 100644 --- a/test/util.test.ts +++ b/test/util.test.ts @@ -135,7 +135,7 @@ describe('Help generation utils', () => { expect(util.generateHelp(parser)).toMatchInlineSnapshot(` "program-name - program description -Usage: program-name [--flag-f] [--opt-multi-o] (--opt-req-r) (--enum-e) (--long ) [--long-optional ] [] +Usage: program-name [--flag | -f ] [--opt-multi | -o ] (--opt-req | -r ) (--enum | -e ) (--long ) [--long-optional ] [] Commands: program-name [help, nohelp] (--cmd-arg )