Skip to content

Commit

Permalink
feat: dotenv component.
Browse files Browse the repository at this point in the history
  • Loading branch information
eser committed Sep 11, 2023
1 parent d43b881 commit 8bb2e3e
Show file tree
Hide file tree
Showing 17 changed files with 315 additions and 241 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ repos:
standards/README.md|
di/README.md|
fp/README.md|
dotenv/README.md|
hex/.*/README.md|
CODE_OF_CONDUCT.md|
CONTRIBUTING.md|
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ runtime.listen(router); // or runtime.execute(fn);
| 📑 [cool/standards](standards/) | Abstraction | |
| ⚙️ [cool/di](di/) | Manager | Dependency injection container |
| 🧱 [cool/fp](fp/) | Functions Library | Tools for functional programming |
| 🔐 [cool/dotenv](dotenv/) | Manager | Load configurations from environment |

<!--
| [hex/StdX](hex/stdx/) | Functions Library | Encriched Standard Library |
Expand All @@ -68,7 +69,6 @@ runtime.listen(router); // or runtime.execute(fn);
| [hex/CLI](hex/cli/) | Manager | CLI library |
| [hex/Functions](hex/functions/) | Manager | Functions runtime |
| [hex/I18N](hex/i18n/) | Manager | Internationalization library |
| [hex/Options](hex/options/) | Manager | Configuration library |
-->

See the respective component page to figure out its specific usage.
Expand Down
94 changes: 94 additions & 0 deletions dotenv/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# 🔐 [cool/dotenv](./)

## Component Information

cool/dotenv helps you load configurations from `.env.*` files and environment
variables, a practice based on the 12-Factor App methodology which recommends
separating configuration from code.

For further details such as requirements, license information and support guide,
please see [main cool repository](https://github.com/eser/cool).

## Environment Variables

Environment variables are variables that are available in all command line
sessions and affect the behavior of the applications on your system. They are
essential for managing the configuration of your applications separate from your
code.

The environment variables are loaded from the following files:

- Environment variables
- `.env.$(ENV).local` - Local overrides of environment-specific settings.
- `.env.local` - Local overrides. This file is loaded for all environments
**except "test"**.
- `.env.$(ENV)` - Environment-specific settings.
- `.env` - The Original®

These files are loaded in the order listed above. The first value set (either
from file or environment variable) takes precedence. That means you can use the
`.env` file to store default values for all environments and
`.env.development.local` to override them for development.

## Usage

With cool/dotenv, you may load environment configurations.

### Loading environment variables

**Basic usage:**

```ts
import { load } from "$cool/dotenv/mod.ts";

const vars = await load();
console.log(vars);
```

**Load from different directory:**

```ts
import { load } from "$cool/dotenv/mod.ts";

const vars = await load({ baseDir: "./config" });
console.log(vars);
```

### Configure an options object with environment reader

**Basic usage:**

```ts
import { configure, env } from "$cool/dotenv/mod.ts";

const options = await configure(
(reader, acc) => {
acc["env"] = reader[env];
acc["port"] = reader.readInt("PORT", 8080);

return acc;
},
{},
);
```

**With custom interfaces:**

```ts
import { configure, env } from "$cool/dotenv/mod.ts";

interface Options {
env: string;
port: number;
}

const options = await configure<Options>(
(reader, acc) => {
acc.env = reader[env];
acc.port = reader.readInt("PORT", 8080);

return acc;
},
{},
);
```
9 changes: 9 additions & 0 deletions dotenv/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// public symbols
export const env = Symbol("env");

// public constants
export const defaultEnvVar = "ENV";
export const defaultEnvValue = "development";

// public types
export type EnvMap = Map<typeof env | string, string>;
73 changes: 73 additions & 0 deletions dotenv/loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as stdDotenv from "https://deno.land/[email protected]/dotenv/mod.ts";
import { defaultEnvValue, defaultEnvVar, env, type EnvMap } from "./base.ts";

// interface definitions
export interface LoaderOptions {
baseDir?: string;
defaultEnvVar?: string;
defaultEnvValue?: string;
}

// public functions
export const parseEnvString = (
rawDotenv: string,
): ReturnType<typeof stdDotenv.parse> => {
return stdDotenv.parse(rawDotenv);
};

export const parseEnvFromFile = async (
filepath: string,
): Promise<ReturnType<typeof parseEnvString>> => {
try {
const data = await Deno.readFile(filepath);
const decoded = new TextDecoder("utf-8").decode(data);
const escaped = decodeURIComponent(decoded);

const result = parseEnvString(escaped);

return result;
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
return {};
}

throw e;
}
};

export const load = async (
options?: LoaderOptions,
): Promise<EnvMap> => {
const options_ = {
baseDir: ".",
defaultEnvVar: defaultEnvVar,
defaultEnvValue: defaultEnvValue,
...(options ?? {}),
};

const sysVars = (typeof Deno !== "undefined") ? Deno.env.toObject() : {};
const envName = sysVars[options_.defaultEnvVar] ?? options_.defaultEnvValue;

const vars = new Map<typeof env | string, string>();
vars.set(env, envName);

const envImport = (entries: Record<string, string>) => {
for (const [key, value] of Object.entries(entries)) {
vars.set(key, value);
}
};

console.log(`${options_.baseDir}/.env`);
envImport(await parseEnvFromFile(`${options_.baseDir}/.env`));
envImport(await parseEnvFromFile(`${options_.baseDir}/.env.${envName}`));
if (envName !== "test") {
envImport(await parseEnvFromFile(`${options_.baseDir}/.env.local`));
}
envImport(
await parseEnvFromFile(`${options_.baseDir}/.env.${envName}.local`),
);

envImport(sysVars);

return vars;
};
3 changes: 3 additions & 0 deletions dotenv/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./base.ts";
export * from "./loader.ts";
export * from "./options.ts";
102 changes: 102 additions & 0 deletions dotenv/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { defaultEnvValue, env, type EnvMap } from "./base.ts";
import { load, type LoaderOptions } from "./loader.ts";

// interface definitions
export interface BaseEnvVariables {
[env]: string;
}

export type EnvVariables = BaseEnvVariables & Record<string, unknown>;

export interface EnvReader {
[env]: string;
readString<T extends string>(key: string, defaultValue: T): T;
readString<T extends string>(key: string): T | undefined;
readEnum<T extends string>(key: string, values: T[], defaultValue: T): T;
readEnum<T extends string>(key: string, values: T[]): T | undefined;
readInt<T extends number>(key: string, defaultValue: T): T;
readInt<T extends number>(key: string): T | undefined;
readBool<T extends boolean>(key: string, defaultValue: T): T;
readBool<T extends boolean>(key: string): T | undefined;
}

export type Promisable<T> = PromiseLike<T> | T;
export type ConfigureFn<T = EnvVariables> = (
reader: EnvReader,
target: T,
) => Promisable<T | void>;

// public functions
export const createEnvReader = (state: EnvMap): EnvReader => {
return {
[env]: state.get(env) ?? defaultEnvValue,
readString: <T extends string>(
key: string,
defaultValue?: T,
): T | undefined => {
return state.get(key) as T ?? defaultValue;
},
readEnum: <T extends string>(
key: string,
values: T[],
defaultValue?: T,
): T | undefined => {
const value = state.get(key);

if (value === undefined) {
return defaultValue;
}

if (values.includes(value as T)) {
return value as T;
}

return defaultValue;
},
readInt: <T extends number>(
key: string,
defaultValue?: T,
): T | undefined => {
const value = state.get(key);

if (value === undefined) {
return defaultValue;
}

return parseInt(value, 10) as T;
},
readBool: <T extends boolean>(
key: string,
defaultValue?: T,
): T | undefined => {
const value = state.get(key);

if (value === undefined) {
return defaultValue;
}

const sanitizedValue = value.trim().toLowerCase();

if (["1", "true", true].includes(sanitizedValue)) {
return true as T;
}

return false as T;
},
};
};

export const configure = async <T>(
configureFn: ConfigureFn<T>,
target?: Partial<T>,
options?: LoaderOptions,
): Promise<T | undefined> => {
const envMap = await load(options);
const reader = createEnvReader(envMap);

const result = await configureFn(reader, target as T);

return result ?? Promise.resolve(target as T);
};

export { type LoaderOptions };
4 changes: 0 additions & 4 deletions hex/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@ import metadata from "../metadata.json" assert { type: "json" };

export * as cli from "./cli/mod.ts";
export * as data from "./data/mod.ts";
export * as di from "./di/mod.ts";
export * as environment from "./environment/mod.ts";
export * as formatters from "./formatters/mod.ts";
export * as fp from "./fp/mod.ts";
export * as functions from "./functions/mod.ts";
export * as generator from "./service/mod.ts";
export * as i18n from "./i18n/mod.ts";
export * as options from "./options/mod.ts";
export * as service from "./service/mod.ts";
export * as standards from "./standards/mod.ts";
export * as stdx from "./stdx/mod.ts";
export * as web from "./web/mod.ts";

Expand Down
1 change: 0 additions & 1 deletion hex/options/deps.ts

This file was deleted.

65 changes: 0 additions & 65 deletions hex/options/env.ts

This file was deleted.

2 changes: 0 additions & 2 deletions hex/options/mod.ts

This file was deleted.

Loading

0 comments on commit 8bb2e3e

Please sign in to comment.