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

Environment loading upgrades #61

Merged
merged 5 commits into from
Aug 21, 2024
Merged
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 cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ words:
- darkcyan
- darkgoldenrod
- darkgray
- unmock
- darkgreen
- darkgrey
- darkkhaki
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"repository": {
"url": "git+https://github.com/Digital-Alchemy-TS/core"
},
"version": "24.8.2",
"version": "24.8.3",
"author": {
"url": "https://github.com/zoe-codez",
"name": "Zoe Codez"
Expand Down Expand Up @@ -103,6 +103,9 @@
"text",
"cobertura"
],
"coveragePathIgnorePatterns": [
"src/testing/"
],
"preset": "ts-jest",
"testEnvironment": "node",
"moduleFileExtensions": [
Expand Down
4 changes: 3 additions & 1 deletion src/extensions/wiring.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ async function Bootstrap<
// * Wire in various shutdown events
processEvents.forEach((callback, event) => {
process.on(event, callback);
logger.trace({ event, name: Bootstrap }, "shutdown event");
logger.trace({ event, name: Bootstrap }, "register shutdown event");
});

// * Add in libraries
Expand Down Expand Up @@ -402,6 +402,8 @@ async function Bootstrap<
CONSTRUCT[i.name] = `${Date.now() - start}ms`;
});

logger.trace({ name: Bootstrap }, `library wiring complete`);

// * Finally the application
if (options.bootLibrariesFirst) {
logger.warn({ name: Bootstrap }, `bootLibrariesFirst`);
Expand Down
92 changes: 37 additions & 55 deletions src/helpers/config-environment-loader.helper.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,73 @@
import { config } from "dotenv";
import { existsSync } from "fs";
import minimist from "minimist";
import { join } from "path";
import { argv, cwd, env } from "process";
import { argv, env } from "process";

import { is, ServiceMap } from "..";
import {
AbstractConfig,
ConfigLoaderParams,
ConfigLoaderReturn,
findKey,
iSearchKey,
loadDotenv,
ModuleConfiguration,
parseConfig,
} from "./config.helper";

export async function ConfigLoaderEnvironment<
S extends ServiceMap = ServiceMap,
C extends ModuleConfiguration = ModuleConfiguration,
>({ configs, internal, logger }: ConfigLoaderParams<S, C>): ConfigLoaderReturn {
const { envFile } = internal.boot.options;
if (!is.empty(envFile) || existsSync(join(cwd(), ".env"))) {
const file = envFile ?? ".env";
logger.trace({ file }, `loading env file`);
config({ override: true, path: envFile ?? ".env" });
}
const environmentKeys = Object.keys(env);
const CLI_SWITCHES = minimist(argv);
const switchKeys = Object.keys(CLI_SWITCHES);

const environmentKeys = Object.keys(env);
const out: Partial<AbstractConfig> = {};

// * merge dotenv into local vars
// accounts for `--env-file` switches, and whatever is passed in via bootstrap
loadDotenv(internal, CLI_SWITCHES, logger);

// * go through all module
configs.forEach((configuration, project) => {
const cleanedProject = project.replaceAll("-", "_");

// * run through each config for module
Object.keys(configuration).forEach((key) => {
// > things to search for
// - MODULE_NAME_CONFIG_KEY (module + key, ex: app_NODE_ENV)
// - CONFIG_KEY (only key, ex: NODE_ENV)
const noAppPath = `${cleanedProject}_${key}`;
const search = [noAppPath, key];
const configPath = `${project}.${key}`;

// #MARK: cli switches
// Find an applicable switch
// * (preferred) Find an applicable cli switch
const flag = findKey(search, switchKeys);
if (flag) {
const formattedFlag = switchKeys.find((key) =>
search.some((line) =>
key.match(
new RegExp(
`^${line.replaceAll(new RegExp("[-_]", "gi"), "[-_]?")}$`,
"gi",
),
),
),
const formattedFlag = iSearchKey(flag, switchKeys);
internal.utils.object.set(
out,
configPath,
parseConfig(configuration[key], CLI_SWITCHES[formattedFlag]),
);
logger.trace(
{
flag: formattedFlag,
name: ConfigLoaderEnvironment,
path: configPath,
},
`load config from [cli switch]`,
);
if (is.string(formattedFlag)) {
internal.utils.object.set(
out,
configPath,
CLI_SWITCHES[formattedFlag],
);
logger.trace(
{
flag: formattedFlag,
name: ConfigLoaderEnvironment,
path: configPath,
},
`load config from [cli switch]`,
);
}
return;
}

// #MARK: environment variables
// Find an environment variable
// * (fallback) Find an environment variable
const environment = findKey(search, environmentKeys);
if (is.empty(environment)) {
return;
}
const environmentName = environmentKeys.find((key) =>
search.some((line) =>
key.match(
new RegExp(
`^${line.replaceAll(new RegExp("[-_]", "gi"), "[-_]?")}$`,
"gi",
),
),
),
);
if (is.string(environmentName)) {
internal.utils.object.set(out, configPath, env[environmentName]);
if (!is.empty(environment)) {
const environmentName = iSearchKey(environment, environmentKeys);
internal.utils.object.set(
out,
configPath,
parseConfig(configuration[key], env[environmentName]),
);
logger.trace(
{
name: ConfigLoaderEnvironment,
Expand Down
95 changes: 93 additions & 2 deletions src/helpers/config.helper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { config } from "dotenv";
import { existsSync } from "fs";
import { ParsedArgs } from "minimist";
import { isAbsolute, join, normalize } from "path";
import { cwd } from "process";

import { ILogger, InternalDefinition, is } from "..";
import { ApplicationDefinition, ServiceMap } from "./wiring.helper";

Expand All @@ -12,10 +18,11 @@ export type ProjectConfigTypes =
export type AnyConfig =
| StringConfig<string>
| BooleanConfig
| InternalConfig<unknown>
| InternalConfig<object>
| NumberConfig
| RecordConfig
| StringArrayConfig;

export interface BaseConfig {
/**
* If no other values are provided, what value should be injected?
Expand Down Expand Up @@ -57,7 +64,7 @@ export interface BooleanConfig extends BaseConfig {
*
* TODO: JSON schema magic for validation / maybe config builder help
*/
export type InternalConfig<VALUE extends unknown> = BaseConfig & {
export type InternalConfig<VALUE extends object> = BaseConfig & {
default: VALUE;
type: "internal";
};
Expand Down Expand Up @@ -177,3 +184,87 @@ export function findKey<T extends string>(source: T[], find: T[]) {
})
);
}

export function iSearchKey(target: string, source: string[]) {
return source.find((key) =>
key.match(
new RegExp(
`^${target.replaceAll(new RegExp("[-_]", "gi"), "[-_]?")}$`,
"gi",
),
),
);
}

/**
* priorities:
* - --env-file
* - bootstrap envFile
* - cwd/.env (default file)
*/
export function loadDotenv(
internal: InternalDefinition,
CLI_SWITCHES: ParsedArgs,
logger: ILogger,
) {
let { envFile } = internal.boot.options;
const switchKeys = Object.keys(CLI_SWITCHES);
const searched = iSearchKey("env-file", switchKeys);

// --env-file > bootstrap
if (!is.empty(CLI_SWITCHES[searched])) {
envFile = CLI_SWITCHES[searched];
}

let file: string;

// * was provided an --env-file or something via boot
if (!is.empty(envFile)) {
const checkFile = isAbsolute(envFile)
? normalize(envFile)
: join(cwd(), envFile);
if (existsSync(checkFile)) {
file = checkFile;
} else {
logger.warn(
{ checkFile, envFile, name: loadDotenv },
"invalid target for dotenv file",
);
}
}

// * attempt default file
if (is.empty(file)) {
const defaultFile = join(cwd(), ".env");
if (existsSync(defaultFile)) {
file = defaultFile;
} else {
logger.debug({ name: loadDotenv }, "no .env found");
}
}

// ? each of the steps above verified the path as valid
if (!is.empty(file)) {
logger.trace({ file, name: loadDotenv }, `loading env file`);
config({ override: true, path: file });
}
}

export function parseConfig(config: AnyConfig, value: string) {
switch (config.type) {
case "string": {
return value;
}
case "number": {
return Number(value);
}
case "string[]":
case "record":
case "internal": {
return JSON.parse(value);
}
case "boolean": {
return ["y", "true"].includes(value.toLowerCase());
}
}
}
2 changes: 1 addition & 1 deletion src/helpers/wiring.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ export type BootstrapOptions = {
*
* Default: `.env`
*/
envFile?: string | string[];
envFile?: string;
};

export const WIRE_PROJECT = Symbol.for("wire-project");
Expand Down
Loading
Loading