Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.vscode
# Logs
logs
*.log
Expand Down
26 changes: 0 additions & 26 deletions .vscode/launch.json

This file was deleted.

33 changes: 16 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"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": {
"type": "git",
"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,
Expand Down
47 changes: 26 additions & 21 deletions src/easy-config.test.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,54 @@
import { easyConfig } from './easy-config';
import { setEnvKey, mockArgv } from './test/utils';

describe('autoConfig core functionality', () => {

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;
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'],
});
expect(config.port).toBe('8080');
});
});

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}`],
userId: ['--user-id', (s: string) => parseInt(s) ?? 123456],
});
expect(config.port).toBe('8080');
expect(config.userId).toBe(123456);
});
it('can apply trailing callback', () => {
const parsePort = (port: number) => (
(port = parseInt(`${port ?? 1234}`, 10)),
port > 1000 && port < 60000 ? port : null
);

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],
port: ['--port', 'PORT', Number],
});
expect(config.port).toBe(8080);
expect(typeof config.port).toBe('number');
expect(config.port).toEqual(8080);
});
});
});
117 changes: 85 additions & 32 deletions src/easy-config.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,111 @@
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]`
* - `['--devMode', '--dev', toBoolean]`
* - `['PORT', Number]`
*
*/
type ArgsList =
| Readonly<[string, ...string[]]>
| Readonly<[...string[], Function]>;

type Tail<T extends any[] | Readonly<any[]>> = T extends [
head: any,
...tail: infer __Tail
]
? __Tail
: unknown;
type ReturnType<T, TFallback = unknown> = T extends (...args: any[]) => infer R
? R
: TFallback;

type Tail<T extends any[]> = T extends [head: any, ...tail: infer Tail_]
? Tail_
: never;

type Last<T> = T extends { "0": infer Item }
? Item
: T extends [item: any, ...rest: infer Rest]
? Last<Rest>
: string;

type TailCallback<TReturnType> = (value?: string) => TReturnType;
type ArgsListCallbackFirst<TReturnType> = [TailCallback<TReturnType>, ...string[]] | [string, ...string[]];
type ArgsListCallback<TReturnType> = [...string[], TailCallback<TReturnType>] | [string, ...string[]];
type ArgsList = [...string[], (...args: any) => any] | [string, ...string[]];

// type Tail<T extends any[] | any[]> = T extends [
// head: any,
// ...tail: infer __Last
// ]
// ? __Last
// : unknown;
// type ReturnType<T, TFallback = unknown> = T extends (...args: any[]) => infer R
// ? R
// : TFallback;

type Head<T extends any[]> = T extends [] ? never : T[0];

export type EasyConfigResults<
TConfig extends { [K in keyof TConfig]: ArgsList }
> = {
[K in keyof TConfig]: Tail<TConfig[K]> extends (...args: any) => infer R
? Tail<TConfig[K]>
: string;
};

export type EasyConfigResultsFn<
TConfig extends { [K in keyof TConfig]: ArgsList }
> = {
[K in keyof TConfig]: Head<TConfig[K]> extends (...args: any) => infer R
? R
: string;
};


export function easyConfig<TConfig extends { [K in keyof TConfig]: ArgsList }>(
config: TConfig,
{ cliArgs = process.argv, envKeys = process.env }: ConfigInputsRaw = {}
): {
[K in keyof TConfig]: ReturnType<Tail<TConfig[K]>, string>;
} {
) {
const { cliArgs: cliParams, envKeys: envParams } = getEnvAndArgs({
cliArgs,
envKeys,
});
return mapValues(config, (argsList, key) => {
let currentValue = undefined;
// type ConfigReturnType<TArgs extends ArgsList> = Last<TArgs> extends (value: string) => unknown
// ? ReturnType<Last<TArgs>>
// : string;

// function loadArgsApplyCallback<TReturnType>( argsList: ArgsListCallback<TReturnType>, ): TReturnType {
function loadArgsApplyCallback(argsList: ArgsList) {
let currentValue: undefined | string | number | boolean | Date = undefined;

for (let arg of argsList) {
if (typeof arg === 'function' && currentValue !== undefined) {
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);
} 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];
break;
}
}
return currentValue;
});
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<ArgsList>(config).reduce((results, field) => {
const [name, argsList] = field;
results[name as keyof TConfig] = loadArgsApplyCallback(argsList);
return results;
// return currentValue as Last<typeof argsList> extends (value: string) => unknown ? ReturnType<Last<typeof argsList>, string> : string;
}, {} as EasyConfigResults<TConfig>)!;
}