Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enum type enhancement #4

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"lodash.keys": "^4.2.0",
"lodash.mapvalues": "^4.6.0",
"minimist": "^1.2.5",
"type-fest": "^2.11.2",
"zod": "^3.11.6"
},
"files": [
Expand Down
16 changes: 14 additions & 2 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {Split, LiteralUnion, IterableElement} from 'type-fest';
import { autoConfig } from './index';
import { mockArgv, setEnvKey } from './test/utils';

Expand Down Expand Up @@ -172,7 +173,7 @@ describe('handles enum options', () => {
featureFlagA: {
args: ['FEATURE_FLAG_A'],
type: 'enum',
enum: ['variant1', 'variant2'],
enum: 'variant1,variant2',
},
});

Expand All @@ -181,11 +182,22 @@ describe('handles enum options', () => {
resetEnv();
});
test('supports enum default values', () => {
const vars = ['variant1', 'variant2', 'variant3', 'variant4']
const objVars = {variant1: 'variant1', variant2: 'variant2', variant3: 'variant3', variant4: 'variant4'}
const varJoin = vars.join(',');
const varCsv = 'variant1,variant2,variant3,variant4';
let opt: undefined | Split<typeof varJoin, ','>[number] = undefined;
let csvOpt: undefined | Split<typeof varCsv, ','>[number] = undefined;
csvOpt = 'v';
opt = 'v';
let iterTest: LiteralUnion<Readonly<IterableElement<typeof vars>>, string> = 'variant';
iterTest = 'v';

const config = autoConfig({
featureFlagA: {
args: ['FEATURE_FLAG_A'],
type: 'enum',
enum: ['variant1', 'variant2'],
enum: ['variant1', 'variant2', 'variant3', 'variant4'],
default: 'variant1',
},
});
Expand Down
134 changes: 68 additions & 66 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import * as z from "zod";
import minimist from "minimist";
import { applyType, cleanupStringList, stripDashes } from "./utils";
import { CommandOption, ConfigInputs, ConfigResults } from "./types";
import isString from "lodash.isstring";
import { optionsHelp } from "./render";
import debug from "debug";
import chalk from "chalk";
import path from "path";
import * as z from 'zod';
import minimist from 'minimist';
import { applyType, cleanupStringList, stripDashes } from './utils';
import { CommandOption, ConfigInputs, ConfigResults, Undefinedable } from './types';
import isString from 'lodash.isstring';
import { optionsHelp } from './render';
import debug from 'debug';
import chalk from 'chalk';
import path from 'path';

export const autoConfig = function <
TInput extends { [K in keyof TInput]: CommandOption }
TInput extends { [K in keyof TInput]: TInput[K]["enum"] extends [string, ...string[]] ? CommandOption<TInput[K]["enum"]> : CommandOption<any> }
>(config: TInput) {
const debugLog = debug("auto-config");
debugLog("START: Loading runtime environment & command line arguments.");
const debugLog = debug('auto-config');
debugLog('START: Loading runtime environment & command line arguments.');
let { cliArgs, envKeys } = extractEnvArgs();
if (debugLog.enabled) {
debugLog("runtime.cliArgs", JSON.stringify(cliArgs));
debugLog("runtime.envKeys", JSON.stringify(envKeys));
debugLog("config.keys", Object.keys(config).sort().join(", "));
debugLog('runtime.cliArgs', JSON.stringify(cliArgs));
debugLog('runtime.envKeys', JSON.stringify(envKeys));
debugLog('config.keys', Object.keys(config).sort().join(', '));
}

checkSpecialArgs(cliArgs, config);
Expand All @@ -28,14 +28,14 @@ export const autoConfig = function <
cliArgs,
envKeys,
});
debugLog("commandOptions=", commandOptions);
debugLog('commandOptions=', commandOptions);

const results = verifySchema(schemaObject, commandOptions, {
cliArgs,
envKeys,
});

debugLog("DONE", JSON.stringify(commandOptions));
debugLog('DONE', JSON.stringify(commandOptions));
return commandOptions;
};

Expand All @@ -45,7 +45,7 @@ function buildSchema<TInput extends { [K in keyof TInput]: CommandOption }>(
const schemaObject = z.object(
Object.entries<CommandOption>(config).reduce(
(schema, [name, commandOption]) => {
commandOption.type = commandOption.type || "string";
commandOption.type = commandOption.type || 'string';
schema[name as keyof TInput] = getOptionSchema({ commandOption });
return schema;
},
Expand All @@ -59,17 +59,17 @@ function verifySchema<TInput extends { [K in keyof TInput]: CommandOption }>(
config: ConfigResults<TInput>,
inputs: ConfigInputs
): Record<string, unknown> {
const debugLog = debug("auto-config:verifySchema");
const debugLog = debug('auto-config:verifySchema');
// verify schema
const parseResults = schema.safeParse(config);
debugLog("parse success?", parseResults.success);
debugLog('parse success?', parseResults.success);
if (!parseResults.success) {
const { issues } = parseResults.error;
debugLog("parse success?", parseResults.success);
debugLog('parse success?', parseResults.success);
const fieldErrors = issues.reduce((groupedResults, issue) => {
groupedResults[issue.message] = groupedResults[issue.message] || [];
groupedResults[issue.message].push(
issue.path.join(".") + " " + issue.code
issue.path.join('.') + ' ' + issue.code
);
return groupedResults;
}, {} as Record<string, string[]>);
Expand All @@ -82,7 +82,7 @@ function verifySchema<TInput extends { [K in keyof TInput]: CommandOption }>(
);
Object.entries(fieldErrors).forEach(([message, errors]) => {
console.error(
` - ${chalk.magentaBright(message)}: ${errors.join(", ")}`
` - ${chalk.magentaBright(message)}: ${errors.join(', ')}`
);
});
return process.exit(1);
Expand All @@ -100,18 +100,18 @@ function assembleConfigResults<
const commandOptions = Object.entries<CommandOption>(config).reduce(
(conf, [name, opt]) => {
if (opt) {
opt.type = opt.type || "string";
opt.type = opt.type || 'string';
const v = getOptionValue({
commandOption: opt,
inputCliArgs: cliArgs,
inputEnvKeys: envKeys,
});
conf[name as Keys] = v as any;
// if (!opt.type || opt.type === 'string')
if (opt.type === "number") conf[name as Keys] = v as any;
if (opt.type === "boolean") conf[name as Keys] = v as any;
if (opt.type === "array") conf[name as Keys] = v as any;
if (opt.type === "date") conf[name as Keys] = new Date(v as any) as any;
if (opt.type === 'number') conf[name as Keys] = v as any;
if (opt.type === 'boolean') conf[name as Keys] = v as any;
if (opt.type === 'array') conf[name as Keys] = v as any;
if (opt.type === 'date') conf[name as Keys] = new Date(v as any) as any;
}
return conf;
},
Expand All @@ -133,10 +133,10 @@ function checkSpecialArgs(
) {
if (args.version) {
// const pkg = getPackageJson(process.cwd());
const version = process.env.npm_package_version || "unknown";
const version = process.env.npm_package_version || 'unknown';

if (version) {
console.log("Version:", version);
console.log('Version:', version);
return process.exit(0);
}
console.error(`No package.json found from path ${__dirname}`);
Expand All @@ -146,7 +146,7 @@ function checkSpecialArgs(
const pkgName =
process.env.npm_package_name ||
path.basename(path.dirname(process.argv[1])) ||
"This app";
'This app';
console.log(
`\n${chalk.underline.bold.greenBright(
pkgName
Expand All @@ -163,49 +163,51 @@ function getOptionSchema({
commandOption: CommandOption;
}) {
let zType =
opt.type === "array"
opt.type === 'array'
? z.array(z.string())
: opt.type === "enum"
? z.enum(opt.enum)
: z[opt.type || "string"]();
if (opt.type === "boolean") {
: opt.type === 'enum'
? opt.enum && z.enum(opt.enum!)
: z[opt.type || 'string']();
if (opt.type === 'boolean') {
// @ts-ignore
zType = zType.default(opt.default || false);
} else {
// @ts-ignore
if (!opt.required && !("min" in opt)) zType = zType.optional();
if (!opt.required && !('min' in opt)) zType = zType.optional();
}
// @ts-ignore
if (opt.default !== undefined) zType = zType.default(opt.default);

if ("min" in opt && typeof opt.min === "number" && "min" in zType)
zType = zType.min(opt.min);
if ("max" in opt && typeof opt.max === "number" && "max" in zType)
zType = zType.max(opt.max);
if ("gte" in opt && typeof opt.gte === "number" && "gte" in zType)
zType = zType.gte(opt.gte);
if ("lte" in opt && typeof opt.lte === "number" && "lte" in zType)
zType = zType.lte(opt.lte);
if ("gt" in opt && typeof opt.gt === "number" && "gt" in zType)
zType = zType.gt(opt.gt);
if ("lt" in opt && typeof opt.lt === "number" && "lt" in zType)
zType = zType.lt(opt.lt);
if (typeof zType === 'object') {
if ('min' in opt && typeof opt.min === 'number' && 'min' in zType)
zType = zType.min(opt.min);
if ('max' in opt && typeof opt.max === 'number' && 'max' in zType)
zType = zType.max(opt.max);
if ('gte' in opt && typeof opt.gte === 'number' && 'gte' in zType)
zType = zType.gte(opt.gte);
if ('lte' in opt && typeof opt.lte === 'number' && 'lte' in zType)
zType = zType.lte(opt.lte);
if ('gt' in opt && typeof opt.gt === 'number' && 'gt' in zType)
zType = zType.gt(opt.gt);
if ('lt' in opt && typeof opt.lt === 'number' && 'lt' in zType)
zType = zType.lt(opt.lt);
}

return zType;
return zType!;
}

function extractArgs(args: string[]) {
return args.reduce(
(result, arg) => {
if (arg.startsWith("--")) {
if (arg.startsWith('--')) {
result.cliArgs.push(arg);
return result;
}
if (arg.startsWith("-")) {
if (arg.startsWith('-')) {
result.cliFlag.push(arg);
return result;
}
if (typeof arg === "string" && arg.length > 0) result.envKeys.push(arg);
if (typeof arg === 'string' && arg.length > 0) result.envKeys.push(arg);
return result;
},
{
Expand All @@ -225,40 +227,40 @@ function getOptionValue({
inputCliArgs: minimist.ParsedArgs;
inputEnvKeys: NodeJS.ProcessEnv;
}) {
const debugLog = debug("auto-config:getOption");
const debugLog = debug('auto-config:getOption');
let { args, default: defaultValue } = commandOption;
args = cleanupStringList(args);

const { cliArgs, cliFlag, envKeys } = extractArgs(args);
debugLog("args", args.join(", "));
debugLog("cliArgs", cliArgs);
debugLog("cliFlag", cliFlag);
debugLog("envKeys", envKeys);
debugLog("inputCliArgs:", inputCliArgs);
debugLog('args', args.join(', '));
debugLog('cliArgs', cliArgs);
debugLog('cliFlag', cliFlag);
debugLog('envKeys', envKeys);
debugLog('inputCliArgs:', inputCliArgs);
// debugLog('inputEnvKeys:', Object.keys(inputEnvKeys).filter((k) => !k.startsWith('npm')).sort());
debugLog("Checking.cliArgs:", [...cliFlag, ...cliArgs]);
debugLog('Checking.cliArgs:', [...cliFlag, ...cliArgs]);
// Match CLI args
let argNameMatch = stripDashes(
[...cliFlag, ...cliArgs].find(
(key) => typeof key === "string" && inputCliArgs[stripDashes(key)]
(key) => typeof key === 'string' && inputCliArgs[stripDashes(key)]
)
);
debugLog("argNameMatch:", argNameMatch);
debugLog('argNameMatch:', argNameMatch);
const matchingArg = isString(argNameMatch)
? inputCliArgs[argNameMatch]
: undefined;
debugLog("argValueMatch:", matchingArg);
debugLog('argValueMatch:', matchingArg);
if (matchingArg) return applyType(matchingArg, commandOption.type);

// Match env vars
const envNameMatch = [...envKeys].find(
(key) => typeof key === "string" && inputEnvKeys[key]
(key) => typeof key === 'string' && inputEnvKeys[key]
);
debugLog("envNameMatch:", envNameMatch);
debugLog('envNameMatch:', envNameMatch);
const matchingEnv = isString(envNameMatch)
? inputEnvKeys[envNameMatch as any]
: undefined;
debugLog("envValueMatch:", matchingEnv);
debugLog('envValueMatch:', matchingEnv);
if (matchingEnv) return applyType(matchingEnv, commandOption.type);

if (commandOption.default != undefined)
Expand Down
Loading