1import arg, { Spec } from 'arg'; 2 3import { replaceValue } from './array'; 4import { CommandError } from './errors'; 5 6/** 7 * Enables the resolution of arguments that can either be a string or a boolean. 8 * 9 * @param args arguments that were passed to the command. 10 * @param rawMap raw map of arguments that are passed to the command. 11 * @param extraArgs extra arguments and aliases that should be resolved as string or boolean. 12 * @returns parsed arguments and project root. 13 */ 14export async function resolveStringOrBooleanArgsAsync( 15 args: string[], 16 rawMap: arg.Spec, 17 extraArgs: arg.Spec 18) { 19 // Assert any missing arguments 20 assertUnknownArgs( 21 { 22 ...rawMap, 23 ...extraArgs, 24 }, 25 args 26 ); 27 28 // Collapse aliases into fully qualified arguments. 29 args = collapseAliases(extraArgs, args); 30 31 // Resolve all of the string or boolean arguments and the project root. 32 return _resolveStringOrBooleanArgs({ ...rawMap, ...extraArgs }, args); 33} 34 35export function _resolveStringOrBooleanArgs(arg: Spec, args: string[]) { 36 // Default project root, if a custom one is defined then it will overwrite this. 37 let projectRoot: string = '.'; 38 // The resolved arguments. 39 const settings: Record<string, string | boolean | undefined> = {}; 40 41 // Create a list of possible arguments, this will filter out aliases. 42 const possibleArgs = Object.entries(arg) 43 .filter(([, value]) => typeof value !== 'string') 44 .map(([key]) => key); 45 46 // Loop over arguments in reverse order so we can resolve if a value belongs to a flag. 47 for (let i = args.length - 1; i > -1; i--) { 48 const value = args[i]; 49 // At this point we should have converted all aliases to fully qualified arguments. 50 if (value.startsWith('--')) { 51 // If we ever find an argument then it must be a boolean because we are checking in reverse 52 // and removing arguments from the array if we find a string. 53 settings[value] = true; 54 } else { 55 // Get the previous argument in the array. 56 const nextValue = i > 0 ? args[i - 1] : null; 57 if (nextValue && possibleArgs.includes(nextValue)) { 58 settings[nextValue] = value; 59 i--; 60 } else if ( 61 // If the last value is not a flag and it doesn't have a recognized flag before it (instead having a string value or nothing) 62 // then it must be the project root. 63 i === 64 args.length - 1 65 ) { 66 projectRoot = value; 67 } else { 68 // This will asserts if two strings are passed in a row and not at the end of the line. 69 throw new CommandError('BAD_ARGS', `Unknown argument: ${value}`); 70 } 71 } 72 } 73 74 return { 75 args: settings, 76 projectRoot, 77 }; 78} 79 80/** Convert all aliases to fully qualified flag names. */ 81export function collapseAliases(arg: Spec, args: string[]): string[] { 82 const aliasMap = getAliasTuples(arg); 83 84 for (const [arg, alias] of aliasMap) { 85 args = replaceValue(args, arg, alias); 86 } 87 88 // Assert if there are duplicate flags after we collapse the aliases. 89 assertDuplicateArgs(args, aliasMap); 90 return args; 91} 92 93/** Assert that the spec has unknown arguments. */ 94export function assertUnknownArgs(arg: Spec, args: string[]) { 95 const allowedArgs = Object.keys(arg); 96 const unknownArgs = args.filter((arg) => !allowedArgs.includes(arg) && arg.startsWith('-')); 97 if (unknownArgs.length > 0) { 98 throw new CommandError(`Unknown arguments: ${unknownArgs.join(', ')}`); 99 } 100} 101 102function getAliasTuples(arg: Spec): [string, string][] { 103 return Object.entries(arg).filter(([, value]) => typeof value === 'string') as [string, string][]; 104} 105 106/** Asserts that a duplicate flag has been used, this naively throws without knowing if an alias or flag were used as the duplicate. */ 107export function assertDuplicateArgs(args: string[], argNameAliasTuple: [string, string][]) { 108 for (const [argName, argNameAlias] of argNameAliasTuple) { 109 if (args.filter((a) => [argName, argNameAlias].includes(a)).length > 1) { 110 throw new CommandError( 111 'BAD_ARGS', 112 `Can only provide one instance of ${argName} or ${argNameAlias}` 113 ); 114 } 115 } 116} 117