diff --git a/src/command.test.ts b/src/command.test.ts new file mode 100644 index 0000000..fd0322a --- /dev/null +++ b/src/command.test.ts @@ -0,0 +1,29 @@ +import { autoConfig } from './index'; +import { mockArgv, setEnvKey } from './test/utils'; + +import minimist from 'minimist'; +console.log('process.argv', process.argv); +console.log('minimist', minimist(process.argv.slice(2))) + +describe('autoCommand CLI functionality', () => { + test('handles sub-commands', () => { + const resetEnv = setEnvKey('PORT', '8080'); + + const command = autoConfig({ + port: { + help: 'The port to listen on.', + args: ['--port', 'PORT'], + type: 'number', + required: true, + }, + debugMode: { + args: ['--debug', 'DEBUG', '--debugMode', 'DEBUG_MODE'], + type: 'boolean', + default: true, + }, + }); + resetEnv(); + // expect(config.port).toBe(8080); + // expect(config.debugMode).toBe(true); + }); +}); diff --git a/src/command.ts b/src/command.ts new file mode 100644 index 0000000..811addf --- /dev/null +++ b/src/command.ts @@ -0,0 +1,49 @@ +import debug from 'debug'; +import { autoConfig } from 'src'; +import { CommandOption, CompleteConfig, OptionTypeConfig } from './types'; + +type Commands = { + [commandName: string]: CompleteConfig; + // TODO: Add Commands +}; +type OptionTypes = NonNullable; + +type AutoCommandOptions = { + cliArgs: string[]; + envKeys: Record; +}; + +const defaultOptions = { + cliArgs: process.argv.slice(2), + envKeys: process.env, +} as const; + +export default function autoCommand( + commandConfig: Commands, + { + cliArgs = process.argv.slice(2), + envKeys = process.env, + }: AutoCommandOptions = defaultOptions +) { + const debugLog = debug('auto-command'); + debugLog(`Starting Command Processor for args: ${cliArgs.join(', ')}`); + const availableCommands = Object.keys(commandConfig); + debugLog('availableCommands', availableCommands); + // TODO + // 1. check for sub-commands in argv + // 2. if sub-command found, split the argv, and use autoConfig on the following arguments + const baseCommand = cliArgs[0]; + debugLog(`Looking for base command: ${baseCommand}`); + if (baseCommand && availableCommands.includes(baseCommand)) { + debugLog(`Found base command: ${baseCommand}`); + const subCommandConfig = commandConfig[baseCommand]; + debugLog('subCommandConfig', subCommandConfig); + autoConfig(subCommandConfig, { + overrides: {cliArgs, envKeys} + }) + // if (typeof subCommandConfig === 'object') { + // return autoCommand(subCommandConfig); + // } + } + // const matchingCommand = process.argv.find(arg => availableCommands.includes(arg)); +} diff --git a/src/index.test.ts b/src/index.test.ts index 7845ff8..26487e4 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -10,7 +10,7 @@ beforeEach(() => { processExitSpy.mockClear(); }); -describe('core features', () => { +describe.only('core features', () => { test('loads environment variables', () => { const resetPort = setEnvKey('PORT', '8080'); const config = autoConfig({ diff --git a/src/index.ts b/src/index.ts index 3945059..8ee0786 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,19 +12,30 @@ import { } from './utils'; import type { CommandOption, + CompleteConfig, ConfigInputsParsed, + ConfigInputsRaw, ConfigResults, } from './types'; import { optionsHelp } from './render'; export { easyConfig } from './easy-config'; +export const SupportedDataTypes = ['string', 'number', 'boolean', 'array', 'date']; + +interface InputOverrides { + overrides: ConfigInputsRaw; +} + export const autoConfig = function < - TInput extends { [K in keyof TInput]: CommandOption } ->(config: TInput) { + TInput extends CompleteConfig +>(config: TInput, { + overrides +}: InputOverrides = { overrides: {} }) { const debugLog = debug('auto-config'); debugLog('START: Loading runtime environment & command line arguments.'); - let { cliArgs, envKeys } = getEnvAndArgs(); + let { cliArgs, envKeys } = getEnvAndArgs({ ...overrides }); + if (debugLog.enabled) { debugLog('runtime.cliArgs', JSON.stringify(cliArgs)); debugLog('runtime.envKeys', JSON.stringify(envKeys)); diff --git a/src/types.ts b/src/types.ts index c87f435..0d13d6f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,6 +55,9 @@ export type OptionTypeConfig = max?: number; }; + +export type CompleteConfig = { [K in keyof TInput]: CommandOption }; + // type Flatten = Type extends Array ? Item : Type; // type GetEnumOption = TOption extends { enum: Array } ? EnumItem : never; export type PrimitiveTypes = string | number | boolean | Date | null; @@ -74,8 +77,8 @@ export type ConfigInputsRaw = { }; export type ConfigInputsParsed = { - cliArgs?: minimist.ParsedArgs; - envKeys?: NodeJS.ProcessEnv; + cliArgs: minimist.ParsedArgs; + envKeys: NodeJS.ProcessEnv; }; export type ConfigResults< diff --git a/src/utils.ts b/src/utils.ts index ae4111f..da201b4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,10 +8,18 @@ import type { OptionTypeConfig, } from './types'; + const debugLog = debug('auto-config:utils'); +/** + * ⚠️ **Warning:** Very permissive boolean coercion. + * + * 1. Normalizes inputs to string, trim, and lower case. + * 2. Returns true if input matches any: `1`, `on`, `t`, `true`, `y`, and `yes` + * + */ export function toBoolean(value: any) { - value = value.toString().toLowerCase(); + value = `${value}`.toString().trim().toLowerCase(); return ( value === 'true' || value === 't' || @@ -21,9 +29,6 @@ export function toBoolean(value: any) { value === 'on' ); } -// export function isNestedObject(obj: unknown) { -// return isObject(obj) && !Array.isArray(obj) && keys(obj).length > 0; -// } export function applyType( value: string, @@ -57,16 +62,16 @@ export function cleanupStringList( return processed as string[]; } -export const stripDashes = (str: string = '') => str.replace(/^-+/gi, ''); +export const stripDashes = (str: string = '') => str.replace(/^(-|--)/gi, ''); export const stripDashesSlashes = (str: string = '') => str.replace(/^[-\/]+/g, ''); export function getEnvAndArgs({ - cliArgs = process.argv, + cliArgs = process.argv.slice(2), envKeys = process.env, }: ConfigInputsRaw = {}): ConfigInputsParsed { debugLog('extractEnvArgs.cliArgs', cliArgs); - debugLog('extractEnvArgs.envKeys', envKeys); + // debugLog('extractEnvArgs.envKeys', envKeys); let cliParsed: ReturnType | undefined = undefined; @@ -75,7 +80,8 @@ export function getEnvAndArgs({ // path to node & the .js file we're executing. cliArgs = process.argv.filter((arg, i) => !(i < 2 && isAbsolute(arg))); cliParsed = minimist(cliArgs); + debugLog('cliParsed.minimist', cliArgs); } - return { cliArgs: cliParsed, envKeys }; + return { cliArgs: cliParsed!, envKeys }; } diff --git a/test.mjs b/test.mjs new file mode 100644 index 0000000..89cba37 --- /dev/null +++ b/test.mjs @@ -0,0 +1,4 @@ +import minimist from 'minimist'; +console.log('process.argv', process.argv); +console.log('minimist', minimist(process.argv.slice(2))) + diff --git a/tsconfig.json b/tsconfig.json index 1a5c4a3..883873f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,10 +32,10 @@ }, "include": [ - "examples/**/*.ts", "src/**/*.ts", "src/**/*.tsx", - "src/**/*.d.ts" + "src/**/*.d.ts", + "examples/**/*.ts", // "client/**/*.ts", // "client/**/*.tsx", // "client/**/*.d.ts",