From 9a74aac01f77c30c61f55dbd5d9b3038b206027a Mon Sep 17 00:00:00 2001 From: Dan Levy Date: Mon, 21 Nov 2022 16:00:18 -0700 Subject: [PATCH 1/3] Fixing type detection for easyConfig --- src/easy-config.test.ts | 28 +++++++++++----------------- src/easy-config.ts | 26 +++++++++++++++----------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/easy-config.test.ts b/src/easy-config.test.ts index 9f25290..fc6901d 100644 --- a/src/easy-config.test.ts +++ b/src/easy-config.test.ts @@ -1,18 +1,19 @@ import { easyConfig } from './easy-config'; import { setEnvKey, mockArgv } from './test/utils'; -describe('autoConfig core functionality', () => { +const parsePort = (port: number) => ( + (port = parseInt(`${port ?? 1234}`, 10)), + port > 1000 && port < 60000 ? port : null +); +describe('easyConfig', () => { + describe('environment variables', () => { let resetEnv: () => void; beforeEach(() => (resetEnv = setEnvKey('PORT', '8080'))); afterEach(() => resetEnv()); it('can parse minimal example', () => { - const parsePort = (port: number) => ( - (port = parseInt(`${port ?? 1234}`, 10)), - port > 1000 && port < 60000 ? port : null - ); const config = easyConfig({ port: ['--port', 'PORT'], }); @@ -20,30 +21,23 @@ describe('autoConfig core functionality', () => { }); }); - describe('command arguments', () => { + describe('callback function', () => { let restoreArgv: () => void; beforeEach(() => (restoreArgv = mockArgv(['--port', '8080']))); afterEach(() => restoreArgv()); - it('can parse minimal example', () => { - const parsePort = (port: number) => ( - (port = parseInt(`${port ?? 1234}`, 10)), - port > 1000 && port < 60000 ? port : null - ); + it('can set default via callback', () => { const config = easyConfig({ port: ['--port', (s: string) => `${s}`], }); expect(config.port).toBe('8080'); }); - it('can apply trailing callback', () => { - const parsePort = (port: number) => ( - (port = parseInt(`${port ?? 1234}`, 10)), - port > 1000 && port < 60000 ? port : null - ); + it('can infer type from callback return type', () => { const config = easyConfig({ port: ['--port', 'PORT', parseInt], }); - expect(config.port).toBe(8080); + expect(typeof config.port).toBe('number'); + expect(config.port).toEqual(8080); }); }); }); diff --git a/src/easy-config.ts b/src/easy-config.ts index 13853d3..b5b69f2 100644 --- a/src/easy-config.ts +++ b/src/easy-config.ts @@ -1,14 +1,14 @@ -import mapValues from 'lodash.mapvalues'; -import { getEnvAndArgs, stripDashesSlashes } from './utils'; -import type { ConfigInputsRaw } from './types'; +import mapValues from "lodash.mapvalues"; +import { getEnvAndArgs, stripDashesSlashes } from "./utils"; +import type { ConfigInputsRaw } from "./types"; /** * ArgsList lists the arguments & env vars to read from. - * + * * To transform the value, add trailing callback function(s) to modify the value & type. * * ### Examples - * + * * - `['--port', (s) => s?.toString() || '']` * - `['PORT', parseInt]` * - `['--port', '-p', parseInt]` @@ -31,21 +31,25 @@ type ReturnType = T extends (...args: any[]) => infer R : TFallback; export function easyConfig( - config: TConfig, + config: Readonly, { cliArgs = process.argv, envKeys = process.env }: ConfigInputsRaw = {} ): { - [K in keyof TConfig]: ReturnType, string>; + [K in keyof TConfig]: Tail extends Function + ? ReturnType> + : string; } { const { cliArgs: cliParams, envKeys: envParams } = getEnvAndArgs({ cliArgs, envKeys, }); return mapValues(config, (argsList, key) => { - let currentValue = undefined; + type ExtractedReturnType = Tail extends Function ? ReturnType> : string; + + let currentValue: ExtractedReturnType | string | undefined; for (let arg of argsList) { - if (typeof arg === 'function' && currentValue !== undefined) { + if (typeof arg === "function" && currentValue !== undefined) { currentValue = arg(currentValue); - } else if (typeof arg === 'string') { + } else if (typeof arg === "string") { // Skip if currentValue is defined. if (currentValue !== undefined) continue; if (cliParams?.[stripDashesSlashes(arg)]) @@ -53,6 +57,6 @@ export function easyConfig( if (envParams?.[arg]) currentValue = envParams?.[arg]; } } - return currentValue; + return currentValue!; }); } From 894f30a668d442e034adc8e786c3927bce6f75ac Mon Sep 17 00:00:00 2001 From: Dan Levy Date: Mon, 21 Nov 2022 16:52:19 -0700 Subject: [PATCH 2/3] No errors, still not propagating types... --- src/easy-config.test.ts | 25 +++++++++++++++------ src/easy-config.ts | 50 ++++++++++++++++++++++++----------------- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/src/easy-config.test.ts b/src/easy-config.test.ts index fc6901d..84b835b 100644 --- a/src/easy-config.test.ts +++ b/src/easy-config.test.ts @@ -1,12 +1,15 @@ import { easyConfig } from './easy-config'; import { setEnvKey, mockArgv } from './test/utils'; -const parsePort = (port: number) => ( - (port = parseInt(`${port ?? 1234}`, 10)), - port > 1000 && port < 60000 ? port : null -); +const parsePort = (value: unknown): number | never => { + let port = parseInt(`${value}`, 10); + if (port >= 1000 && port < 65635) { + return port; + } + throw Error(`Invalid port: ${port}. Must be between 1000-65635.`); +} + describe('easyConfig', () => { - describe('environment variables', () => { let resetEnv: () => void; @@ -28,10 +31,18 @@ describe('easyConfig', () => { it('can set default via callback', () => { const config = easyConfig({ - port: ['--port', (s: string) => `${s}`], + userId: ['--user-id', (s: string) => parseInt(s) ?? 123456], }); - expect(config.port).toBe('8080'); + expect(config.userId).toBe(123456); + }); + + it('can set default via callback', () => { + const config = easyConfig({ + id: ['--id', (s: string) => parseInt(s) ?? 123456], + }); + expect(config.id).toBe('8080'); }); + it('can infer type from callback return type', () => { const config = easyConfig({ port: ['--port', 'PORT', parseInt], diff --git a/src/easy-config.ts b/src/easy-config.ts index b5b69f2..73ead4e 100644 --- a/src/easy-config.ts +++ b/src/easy-config.ts @@ -31,32 +31,42 @@ type ReturnType = T extends (...args: any[]) => infer R : TFallback; export function easyConfig( - config: Readonly, + config: TConfig, { cliArgs = process.argv, envKeys = process.env }: ConfigInputsRaw = {} -): { - [K in keyof TConfig]: Tail extends Function - ? ReturnType> - : string; -} { +) { const { cliArgs: cliParams, envKeys: envParams } = getEnvAndArgs({ cliArgs, envKeys, }); - return mapValues(config, (argsList, key) => { - type ExtractedReturnType = Tail extends Function ? ReturnType> : string; + type ConfigReturnType = Tail extends Function + ? ReturnType, string> + : string; - let currentValue: ExtractedReturnType | string | undefined; - for (let arg of argsList) { - if (typeof arg === "function" && currentValue !== undefined) { - currentValue = arg(currentValue); - } else if (typeof arg === "string") { - // Skip if currentValue is defined. - if (currentValue !== undefined) continue; - if (cliParams?.[stripDashesSlashes(arg)]) - currentValue = cliParams?.[stripDashesSlashes(arg)]; - if (envParams?.[arg]) currentValue = envParams?.[arg]; + return Object.entries(config).reduce<{ + [K in keyof TConfig]: ConfigReturnType | string; + }>( + (results, [name, argsList]) => { + let currentValue: ConfigReturnType | string | undefined = + undefined; + for (let arg of argsList) { + if (typeof arg === "function") { + currentValue = arg(currentValue); + } else if (typeof arg === "string") { + // Skip if currentValue is defined. + if (currentValue !== undefined) continue; + if (cliParams?.[stripDashesSlashes(arg)]) + currentValue = cliParams?.[stripDashesSlashes(arg)]; + if (envParams?.[arg]) currentValue = envParams?.[arg]; + } + // if (currentValue !== undefined) { + results[name as keyof TConfig] = currentValue!; + // } } + return results; + // return currentValue as Tail extends Function ? ReturnType, string> : string; + }, + {} as { + [K in keyof TConfig]: string | ReturnType, string>; } - return currentValue!; - }); + )!; } From a10693fca86a27b894f351bedabe850ca21ee593 Mon Sep 17 00:00:00 2001 From: Dan Levy Date: Wed, 30 Aug 2023 08:22:48 -0600 Subject: [PATCH 3/3] progress --- .gitignore | 1 + .vscode/launch.json | 26 --------- README.md | 33 ++++++----- package.json | 6 +- src/easy-config.test.ts | 2 +- src/easy-config.ts | 119 ++++++++++++++++++++++++++-------------- 6 files changed, 100 insertions(+), 87 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index b002ae8..39991a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.vscode # Logs logs *.log diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index f86e8c0..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "name": "vscode-jest-tests", - "request": "launch", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": [ - "--runInBand", - "--watchAll=false" - ], - "cwd": "${workspaceFolder}", - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "windows": { - "program": "${workspaceFolder}/node_modules/jest/bin/jest" - } - } - - ] -} \ No newline at end of file diff --git a/README.md b/README.md index 61e21da..4f477a9 100644 --- a/README.md +++ b/README.md @@ -36,23 +36,22 @@ As described in [12 Factor App's chapter on Config](https://12factor.net/config) > Table of Contents -- [auto-config 🛠✨](#auto-config-) - - [Intro](#intro) - - [Why](#why) - - [Install](#install) - - [Example: AWS Access Config](#example-aws-access-config) - - [Example: Web App with Database Config](#example-web-app-with-database-config) - - [Example: Linux Move Command Arguments](#example-linux-move-command-arguments) - - [Example: Feature Flags](#example-feature-flags) - - [Example: Runtime Usage Behavior](#example-runtime-usage-behavior) - - [Command line arguments](#command-line-arguments) - - [Mix of environment and command arguments](#mix-of-environment-and-command-arguments) - - [Single-letter flag arguments](#single-letter-flag-arguments) - - [Error on required fields](#error-on-required-fields) - - [CLI Help Output](#cli-help-output) - - [TODO](#todo) - - [Add Shorthand Object Support](#add-shorthand-object-support) - - [Credit and References](#credit-and-references) +- [Intro](#intro) +- [Why](#why) +- [Install](#install) +- [Example: AWS Access Config](#example-aws-access-config) +- [Example: Web App with Database Config](#example-web-app-with-database-config) +- [Example: Linux Move Command Arguments](#example-linux-move-command-arguments) +- [Example: Feature Flags](#example-feature-flags) +- [Example: Runtime Usage Behavior](#example-runtime-usage-behavior) + - [Command line arguments](#command-line-arguments) + - [Mix of environment and command arguments](#mix-of-environment-and-command-arguments) + - [Single-letter flag arguments](#single-letter-flag-arguments) + - [Error on required fields](#error-on-required-fields) + - [CLI Help Output](#cli-help-output) +- [TODO](#todo) + - [Add Shorthand Object Support](#add-shorthand-object-support) +- [Credit and References](#credit-and-references) ## Install diff --git a/package.json b/package.json index fbe9aa9..78d52b6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@elite-libs/auto-config", "description": "A Unified Config & Arguments Library for Node.js. Featuring support for environment variables, command line arguments, and JSON files!", - "version": "1.4.0", + "version": "1.4.1", "type": "commonjs", "homepage": "https://github.com/elite-libs/auto-config", "repository": { @@ -9,8 +9,8 @@ "url": "https://github.com/elite-libs/auto-config.git" }, "source": "./src/index.ts", - "exports": "./dist/main.js", - "main": "./dist/main.js", + "exports": "./dist/index.js", + "main": "./dist/index.js", "module": "./dist/module.js", "types": "./dist/index.d.ts", "private": false, diff --git a/src/easy-config.test.ts b/src/easy-config.test.ts index 84b835b..08f920a 100644 --- a/src/easy-config.test.ts +++ b/src/easy-config.test.ts @@ -45,7 +45,7 @@ describe('easyConfig', () => { it('can infer type from callback return type', () => { const config = easyConfig({ - port: ['--port', 'PORT', parseInt], + port: ['--port', 'PORT', Number], }); expect(typeof config.port).toBe('number'); expect(config.port).toEqual(8080); diff --git a/src/easy-config.ts b/src/easy-config.ts index 73ead4e..2244329 100644 --- a/src/easy-config.ts +++ b/src/easy-config.ts @@ -16,19 +16,50 @@ import type { ConfigInputsRaw } from "./types"; * - `['PORT', Number]` * */ -type ArgsList = - | Readonly<[string, ...string[]]> - | Readonly<[...string[], Function]>; -type Tail> = T extends [ - head: any, - ...tail: infer __Tail -] - ? __Tail - : unknown; -type ReturnType = T extends (...args: any[]) => infer R - ? R - : TFallback; +type Tail = T extends [head: any, ...tail: infer Tail_] + ? Tail_ + : never; + +type Last = T extends { "0": infer Item } + ? Item + : T extends [item: any, ...rest: infer Rest] + ? Last + : string; + +type TailCallback = (value?: string) => TReturnType; +type ArgsListCallbackFirst = [TailCallback, ...string[]] | [string, ...string[]]; +type ArgsListCallback = [...string[], TailCallback] | [string, ...string[]]; +type ArgsList = [...string[], (...args: any) => any] | [string, ...string[]]; + +// type Tail = T extends [ +// head: any, +// ...tail: infer __Last +// ] +// ? __Last +// : unknown; +// type ReturnType = T extends (...args: any[]) => infer R +// ? R +// : TFallback; + +type Head = T extends [] ? never : T[0]; + +export type EasyConfigResults< + TConfig extends { [K in keyof TConfig]: ArgsList } +> = { + [K in keyof TConfig]: Tail extends (...args: any) => infer R + ? Tail + : string; +}; + +export type EasyConfigResultsFn< + TConfig extends { [K in keyof TConfig]: ArgsList } +> = { + [K in keyof TConfig]: Head extends (...args: any) => infer R + ? R + : string; +}; + export function easyConfig( config: TConfig, @@ -38,35 +69,43 @@ export function easyConfig( cliArgs, envKeys, }); - type ConfigReturnType = Tail extends Function - ? ReturnType, string> - : string; + // type ConfigReturnType = Last extends (value: string) => unknown + // ? ReturnType> + // : string; + + // function loadArgsApplyCallback( argsList: ArgsListCallback, ): TReturnType { + function loadArgsApplyCallback(argsList: ArgsList) { + let currentValue: undefined | string | number | boolean | Date = undefined; - return Object.entries(config).reduce<{ - [K in keyof TConfig]: ConfigReturnType | string; - }>( - (results, [name, argsList]) => { - let currentValue: ConfigReturnType | string | undefined = - undefined; - for (let arg of argsList) { - if (typeof arg === "function") { - currentValue = arg(currentValue); - } else if (typeof arg === "string") { - // Skip if currentValue is defined. - if (currentValue !== undefined) continue; - if (cliParams?.[stripDashesSlashes(arg)]) - currentValue = cliParams?.[stripDashesSlashes(arg)]; - if (envParams?.[arg]) currentValue = envParams?.[arg]; - } - // if (currentValue !== undefined) { - results[name as keyof TConfig] = currentValue!; - // } + for (let arg of argsList) { + if (currentValue != undefined && typeof arg === "string") { + // already found a match, skip to next arg in case we find a function + continue; + } + if (typeof arg === "string") { + arg = stripDashesSlashes(arg); + currentValue = cliParams?.[stripDashesSlashes(arg)] ?? envParams?.[arg]; + } + if (typeof arg === 'function') { + currentValue = arg(currentValue); + break; } - return results; - // return currentValue as Tail extends Function ? ReturnType, string> : string; - }, - {} as { - [K in keyof TConfig]: string | ReturnType, string>; } - )!; + if (typeof currentValue === 'bigint') return currentValue as bigint; + if (typeof currentValue === 'boolean') return currentValue as boolean; + if (typeof currentValue === 'function') return currentValue as (...args: any[]) => any; + if (typeof currentValue === 'number') return currentValue as number; + if (typeof currentValue === 'object') return currentValue as object; + if (typeof currentValue === 'string') return currentValue as string; + if (typeof currentValue === 'symbol') return currentValue as symbol; + if (typeof currentValue === 'undefined') return currentValue as undefined; + return currentValue as any; + + } + return Object.entries(config).reduce((results, field) => { + const [name, argsList] = field; + results[name as keyof TConfig] = loadArgsApplyCallback(argsList); + return results; + // return currentValue as Last extends (value: string) => unknown ? ReturnType, string> : string; + }, {} as EasyConfigResults)!; }