diff --git a/index.js b/index.js index 7c2fef3..918d2f5 100644 --- a/index.js +++ b/index.js @@ -31,6 +31,13 @@ const { ok } = require('node:assert'); * @typedef {import('./types.d.ts').FlagAny} FlagAny */ +class InputError extends Error { + context = null; +}; + +class UnsetFlagValueType {} +const UNSET_FLAG_VALUE = new UnsetFlagValueType(); + /** * @param {string} key * @param {JSTypes} type @@ -90,10 +97,10 @@ function namedValidateOrDefaultIfUnset(objName, obj, keyName, keyType, defaultVa */ function validateAndFillDefaults(opts) { - const allowedTypeofDefaultType = ["string", "number", "boolean"]; + const allowedTypeofDefaultType = ["string", "number", "bigint", "boolean"]; const validFlagTypes = ['any', ...allowedTypeofDefaultType]; - const allowedBehaviorOnInputErrorTypes = ['throw', 'log', 'log&exit']; + const allowedBehaviorOnInputErrorTypes = ['throw', 'log', 'log&exit', 'ignore']; validateOrDefaultIfUnset(opts, 'warningLogger', 'function', console.log); validateOrDefaultIfUnset(opts, 'behaviorOnInputError', 'string', 'throw'); @@ -296,21 +303,20 @@ function getShorthandFlag(opts, flagKeyInput) { /** * @related runFlagValueTypeResolving * @param {string} input - * @returns {"number"|"boolean"|"string"} + * @returns {"number"|"bigint"|"boolean"|"string"} */ function getCastableType(input) { if(input.toLowerCase() === 'true' || input.toLowerCase() === 'false') return 'boolean'; /// @ts-ignore + if(input.length >= 2 && input.endsWith('n') && !isNaN(input.slice(0, -1))) + return 'bigint'; + /// @ts-ignore if(!isNaN(input)) return 'number'; return 'string'; } -class InputError extends Error { - context = null; -}; - /** * @param {DefaultParserOptsType} opts * @param {{ error: InputError | Error, message: string, exitCode: number }} errorObj @@ -347,7 +353,9 @@ function isAssignedValueTypeAssignable(opts, meta, flag, argv) { } const castableType = getCastableType(meta.flagAssignedValue); - if(flagConfig !== undefined && flagConfig.type !== 'any' && flagConfig.type !== castableType) { + if(flagConfig !== undefined && flagConfig.type !== 'any' && flagConfig.type !== 'string' && flagConfig.type !== castableType) { + if(flagConfig.type === 'bigint' && castableType === 'number') + return true; const message = `Value "${meta.flagAssignedValue}" (type:"${castableType}") is not assignable to required type "${flagConfig.type}".`; const error = new InputError(message); error.context = { flag: flagConfig, flagType: flagConfig.type, flagMatch: meta.flagKey, value: meta.flagAssignedValue, assignableType: castableType, argv }; @@ -403,60 +411,113 @@ function getAllShorthandFlags(opts) { } /** + * @modifies flag + * And uh, yeah bit of a heavy refactor given it's a biggggg chicken and egg situation, where I need the type of the flag value (assigned or given or default), but to get the + * proper flag config, I first need to run the negated boolean flag, so uh yeah, need to run allat twice :D + * ### To be run before any flag value assignments (flag.value always expected to be string) * @param {DefaultParserOptsType} opts - * @param {string} flagKey - * @param {any} flagVal + * @param {InternalFlagsFlagObj} flag */ -function runFullFlagAutomaticBooleanNegation(opts, flagKey, flagVal) { - /** Make sure flag isn't set in our vals */ - if(!hasFullFlag(opts, flagKey) && opts.automaticBooleanFlagNegation.enabled && typeof flagVal === 'boolean' && flagKey.length > 2) { - if(flagKey.length > 3 && flagKey.toLowerCase().startsWith('no-')) { - flagKey = flagKey.slice(3); - flagVal = !flagVal; - } - else if(opts.automaticBooleanFlagNegation.allowUnspacedNegatedFlags && flagKey.toLowerCase().startsWith('no')) { - flagKey = flagKey.slice(2); - flagVal = !flagVal; - } - } - return { flagKey, flagVal } -} +function runAutomaticBooleanFlagNegation(opts, flag) { + if(opts.automaticBooleanFlagNegation.enabled !== true) + return false; + if(flag.isShorthand && opts.automaticBooleanFlagNegation.shorthandNegation !== true) + return false; -/** - * @param {DefaultParserOptsType} opts - * @param {string} flagKey - * @param {any} flagVal - */ -function runShorthandFlagAutomaticBooleanNegation(opts, flagKey, flagVal) { - /** Make sure flag isn't set in our vals */ - if(!hasFullFlag(opts, flagKey) && opts.automaticBooleanFlagNegation.enabled && opts.automaticBooleanFlagNegation.shorthandNegation && typeof flagVal === 'boolean' && flagKey.length >= 2) { - if(opts.automaticBooleanFlagNegation.allowUnspacedNegatedFlags && flagKey.toLowerCase().startsWith('n')) { - flagKey = flagKey.slice(1); - flagVal = !flagVal; + /** @readonly */ + let newKey = null; + /** @readonly */ + let flagConfig = null; + let negated = false; + + + if(!flag.isShorthand) { + /** Make sure flag isn't set in our vals */ + if(!hasFullFlag(opts, flag.key) && flag.key.length > 2) { + + if(flag.key.length > 3 && flag.key.toLowerCase().startsWith('no-')) { + newKey = flag.key.slice(3); + negated = true; + flagConfig = getFullFlag(opts, newKey); + } + else if(opts.automaticBooleanFlagNegation.allowUnspacedNegatedFlags && flag.key.toLowerCase().startsWith('no')) { + newKey = flag.key.slice(2); + negated = true; + flagConfig = getFullFlag(opts, newKey); + } + } + } else { + /** Make sure flag isn't set in our vals */ + if(!hasShorthandFlag(opts, flag.key) && flag.key.length >= 2) { + if(flag.key.toLowerCase().startsWith('n')) { + newKey = flag.key.slice(1); + negated = true; + flagConfig = getShorthandFlag(opts, newKey); + } } } - return { flagKey, flagVal } + if(negated) { + /** @readonly */ + let flagType = opts.defaultFlagType; + if(typeof flagConfig !== 'undefined') + flagType = flagConfig.type; + + /** @type {ReturnType} */ + let castableType = 'boolean'; + if(flag.value !== UNSET_FLAG_VALUE) + /// @ts-ignore + castableType = getCastableType(flag.value); + + if((flagType === 'boolean' || flagType === 'any') && castableType === 'boolean') { + flag.key = newKey; + flag.flagConfig = flagConfig; + flag.lookedupFlagConfig = true; + if(flag.value === UNSET_FLAG_VALUE) + flag.value = false; + else { + /** @type {string} */ + /// @ts-ignore + let flagVal = flag.value; + let boolVal = runFlagValueTypeResolving(opts, flagVal, undefined); + flag.value = !boolVal; + } + return true; + } + } + return false; } /** * @related getCastableType - * @param {DefaultParserOptsType} opts - * @param {string} flagKey - * @param {any} flagVal + * ### Warning: This does not check the flag type despite passing in a flagConfig! + * @param {DefaultParserOptsType} opts + * @param {string} flagVal + * @param {ReturnType} flagConfig */ -function runFlagValueTypeResolving(opts, flagKey, flagVal) { +function runFlagValueTypeResolving(opts, flagVal, flagConfig) { + /** @type {any} */ + let resolvedFlagVal = flagVal; + /** boolean checking */ if(flagVal.toLowerCase() === 'true') - flagVal = true; + resolvedFlagVal = true; else if(flagVal.toLowerCase() === 'false') - flagVal = false; + resolvedFlagVal = false; else if(opts.resolveFlagValueTypes) { - /** number checking */ + /** bigint checking */ /// @ts-ignore - if(!isNaN(flagVal)) - flagVal = Number(flagVal); + if(flagVal.length >= 2 && flagVal.endsWith('n') && !isNaN(flagVal.slice(0, -1))) + resolvedFlagVal = BigInt(flagVal.slice(0, -1)); + /** number & bigint checking */ + /// @ts-ignore + else if(!isNaN(flagVal)) { + if(typeof flagConfig === 'undefined' || flagConfig.type === 'number' || flagConfig.type === 'any') + resolvedFlagVal = Number(flagVal); + else if (typeof flagConfig !== 'undefined' && flagConfig.type === 'bigint') + resolvedFlagVal = BigInt(flagVal); + } } - return { flagVal }; + return resolvedFlagVal; } //#endregion SubworkFunctions @@ -586,9 +647,6 @@ function parseShorthandFlags(shorthandFlags, shorthandString) { //#region end SecondaryParsers //#endregion end SecondaryParsers -class UnsetFlagValueType {} -const UNSET_FLAG_VALUE = new UnsetFlagValueType(); - /** * @interface * @typedef {{ key: string, value: typeof UNSET_FLAG_VALUE | number | boolean | string, isShorthand: boolean, lookedupFlagConfig: boolean, flagConfig: ReturnType }} InternalFlagsFlagObj @@ -688,10 +746,10 @@ function parser(argv, opts) { let flagType = opts.defaultFlagType; if(flagConfig !== undefined && flagConfig.type !== undefined) flagType = flagConfig.type; - - const { flagVal } = runFlagValueTypeResolving(opts, flag.key, arg); - if(flagType === typeof flagVal || flagType === 'any') + const castableType = getCastableType(arg); + + if(['string', 'any', castableType].includes(flagType) || (castableType === 'number' && flagType === 'bigint')) flag.value = arg; else input.push(arg); @@ -707,6 +765,9 @@ function parser(argv, opts) { const flagReturnObj = {}; /** Some (not all, some is run in previous loop) Flag processing (like checking types, which we can't run in the argv.forEach) and return object building */ flags.forEach((flag) => { + /** Only on type flagVal */ + runAutomaticBooleanFlagNegation(opts, flag); + /** Process some flag values */ let { key: flagKey, value: flagVal } = flag; @@ -722,10 +783,9 @@ function parser(argv, opts) { ok(flagConfig !== null, 'flagConfig should always be undefined if not found, and would only be in the wrong state of null if not reassigned at a later point (which should be impossible'); if(flagVal === UNSET_FLAG_VALUE) { - /** Setting defaults */ - /** @type {number|string|boolean} */ - flagVal = true; + let flagType = opts.defaultFlagType if(flagConfig !== undefined) { + flagType = flagConfig.type; if(flagConfig.requiredValue === true) { const message = `Flag "${flagConfig.name}" is missing a required value of type "${flagConfig.type}"!`; const error = new InputError(message); @@ -740,15 +800,23 @@ function parser(argv, opts) { if(typeof flagConfig.default !== 'undefined' && flagConfig.type !== 'boolean') flagVal = flagConfig.default; } + if(flagVal === UNSET_FLAG_VALUE) { + switch(flagType) { + case 'any': + case 'boolean': + flagVal = true; + break; + case "bigint": + case "number": + case "string": + flagVal = null; + break; + } + } } if(typeof flagVal === 'string') - ({ flagVal } = runFlagValueTypeResolving(opts, flagKey, flagVal)); - /** Only on type flagVal */ - if(flag.isShorthand) - ({ flagKey, flagVal } = runShorthandFlagAutomaticBooleanNegation(opts, flagKey, flagVal)); - else - ({ flagKey, flagVal } = runFullFlagAutomaticBooleanNegation(opts, flagKey, flagVal)); + flagVal = runFlagValueTypeResolving(opts, flagVal, flagConfig); flag.key = flagKey; @@ -764,7 +832,7 @@ function parser(argv, opts) { flagReturnObj[realFlagKey] = flag.value; }) - /** Setting defaults if missing */ + /** Setting missing flags */ Object.entries(opts.flags).forEach(([flagName, flagConfig]) => { if(typeof flagReturnObj[flagName] !== 'undefined') return; diff --git a/types.d.ts b/types.d.ts index 9a55891..71f34af 100644 --- a/types.d.ts +++ b/types.d.ts @@ -9,7 +9,7 @@ interface FlagT { requiredValue?: boolean }; -export type FlagAny = FlagT | FlagT | FlagT | FlagT +export type FlagAny = FlagT | FlagT | FlagT | FlagT | FlagT export interface FlagsI { [key: readonly string]: FlagAny } @@ -47,7 +47,7 @@ export interface ParserOpts { /** Override to change how warnings are emitted @default console.warn */ warningLogger?: (log: string) => void; /** Default type for either unknown flags or flags without an explicit type being set @default "any" */ - defaultFlagType?: "string" | "boolean" | "number" | "any"; + defaultFlagType?: "string" | "boolean" | "bigint" | "number" | "any"; /** * Behavior when input does not follow the provided flags constraints (eg: flag assigned value (--flag=value) not being the correct type). *