Skip to content

Commit 8bb2e3e

Browse files
committed
feat: dotenv component.
1 parent d43b881 commit 8bb2e3e

17 files changed

+315
-241
lines changed

.pre-commit-config.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ repos:
4646
standards/README.md|
4747
di/README.md|
4848
fp/README.md|
49+
dotenv/README.md|
4950
hex/.*/README.md|
5051
CODE_OF_CONDUCT.md|
5152
CONTRIBUTING.md|

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ runtime.listen(router); // or runtime.execute(fn);
5959
| 📑 [cool/standards](standards/) | Abstraction | |
6060
| ⚙️ [cool/di](di/) | Manager | Dependency injection container |
6161
| 🧱 [cool/fp](fp/) | Functions Library | Tools for functional programming |
62+
| 🔐 [cool/dotenv](dotenv/) | Manager | Load configurations from environment |
6263

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

7474
See the respective component page to figure out its specific usage.

dotenv/README.md

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# 🔐 [cool/dotenv](./)
2+
3+
## Component Information
4+
5+
cool/dotenv helps you load configurations from `.env.*` files and environment
6+
variables, a practice based on the 12-Factor App methodology which recommends
7+
separating configuration from code.
8+
9+
For further details such as requirements, license information and support guide,
10+
please see [main cool repository](https://github.com/eser/cool).
11+
12+
## Environment Variables
13+
14+
Environment variables are variables that are available in all command line
15+
sessions and affect the behavior of the applications on your system. They are
16+
essential for managing the configuration of your applications separate from your
17+
code.
18+
19+
The environment variables are loaded from the following files:
20+
21+
- Environment variables
22+
- `.env.$(ENV).local` - Local overrides of environment-specific settings.
23+
- `.env.local` - Local overrides. This file is loaded for all environments
24+
**except "test"**.
25+
- `.env.$(ENV)` - Environment-specific settings.
26+
- `.env` - The Original®
27+
28+
These files are loaded in the order listed above. The first value set (either
29+
from file or environment variable) takes precedence. That means you can use the
30+
`.env` file to store default values for all environments and
31+
`.env.development.local` to override them for development.
32+
33+
## Usage
34+
35+
With cool/dotenv, you may load environment configurations.
36+
37+
### Loading environment variables
38+
39+
**Basic usage:**
40+
41+
```ts
42+
import { load } from "$cool/dotenv/mod.ts";
43+
44+
const vars = await load();
45+
console.log(vars);
46+
```
47+
48+
**Load from different directory:**
49+
50+
```ts
51+
import { load } from "$cool/dotenv/mod.ts";
52+
53+
const vars = await load({ baseDir: "./config" });
54+
console.log(vars);
55+
```
56+
57+
### Configure an options object with environment reader
58+
59+
**Basic usage:**
60+
61+
```ts
62+
import { configure, env } from "$cool/dotenv/mod.ts";
63+
64+
const options = await configure(
65+
(reader, acc) => {
66+
acc["env"] = reader[env];
67+
acc["port"] = reader.readInt("PORT", 8080);
68+
69+
return acc;
70+
},
71+
{},
72+
);
73+
```
74+
75+
**With custom interfaces:**
76+
77+
```ts
78+
import { configure, env } from "$cool/dotenv/mod.ts";
79+
80+
interface Options {
81+
env: string;
82+
port: number;
83+
}
84+
85+
const options = await configure<Options>(
86+
(reader, acc) => {
87+
acc.env = reader[env];
88+
acc.port = reader.readInt("PORT", 8080);
89+
90+
return acc;
91+
},
92+
{},
93+
);
94+
```

dotenv/base.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// public symbols
2+
export const env = Symbol("env");
3+
4+
// public constants
5+
export const defaultEnvVar = "ENV";
6+
export const defaultEnvValue = "development";
7+
8+
// public types
9+
export type EnvMap = Map<typeof env | string, string>;

dotenv/loader.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as stdDotenv from "https://deno.land/std@0.200.0/dotenv/mod.ts";
2+
import { defaultEnvValue, defaultEnvVar, env, type EnvMap } from "./base.ts";
3+
4+
// interface definitions
5+
export interface LoaderOptions {
6+
baseDir?: string;
7+
defaultEnvVar?: string;
8+
defaultEnvValue?: string;
9+
}
10+
11+
// public functions
12+
export const parseEnvString = (
13+
rawDotenv: string,
14+
): ReturnType<typeof stdDotenv.parse> => {
15+
return stdDotenv.parse(rawDotenv);
16+
};
17+
18+
export const parseEnvFromFile = async (
19+
filepath: string,
20+
): Promise<ReturnType<typeof parseEnvString>> => {
21+
try {
22+
const data = await Deno.readFile(filepath);
23+
const decoded = new TextDecoder("utf-8").decode(data);
24+
const escaped = decodeURIComponent(decoded);
25+
26+
const result = parseEnvString(escaped);
27+
28+
return result;
29+
} catch (e) {
30+
if (e instanceof Deno.errors.NotFound) {
31+
return {};
32+
}
33+
34+
throw e;
35+
}
36+
};
37+
38+
export const load = async (
39+
options?: LoaderOptions,
40+
): Promise<EnvMap> => {
41+
const options_ = {
42+
baseDir: ".",
43+
defaultEnvVar: defaultEnvVar,
44+
defaultEnvValue: defaultEnvValue,
45+
...(options ?? {}),
46+
};
47+
48+
const sysVars = (typeof Deno !== "undefined") ? Deno.env.toObject() : {};
49+
const envName = sysVars[options_.defaultEnvVar] ?? options_.defaultEnvValue;
50+
51+
const vars = new Map<typeof env | string, string>();
52+
vars.set(env, envName);
53+
54+
const envImport = (entries: Record<string, string>) => {
55+
for (const [key, value] of Object.entries(entries)) {
56+
vars.set(key, value);
57+
}
58+
};
59+
60+
console.log(`${options_.baseDir}/.env`);
61+
envImport(await parseEnvFromFile(`${options_.baseDir}/.env`));
62+
envImport(await parseEnvFromFile(`${options_.baseDir}/.env.${envName}`));
63+
if (envName !== "test") {
64+
envImport(await parseEnvFromFile(`${options_.baseDir}/.env.local`));
65+
}
66+
envImport(
67+
await parseEnvFromFile(`${options_.baseDir}/.env.${envName}.local`),
68+
);
69+
70+
envImport(sysVars);
71+
72+
return vars;
73+
};

dotenv/mod.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./base.ts";
2+
export * from "./loader.ts";
3+
export * from "./options.ts";

dotenv/options.ts

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { defaultEnvValue, env, type EnvMap } from "./base.ts";
2+
import { load, type LoaderOptions } from "./loader.ts";
3+
4+
// interface definitions
5+
export interface BaseEnvVariables {
6+
[env]: string;
7+
}
8+
9+
export type EnvVariables = BaseEnvVariables & Record<string, unknown>;
10+
11+
export interface EnvReader {
12+
[env]: string;
13+
readString<T extends string>(key: string, defaultValue: T): T;
14+
readString<T extends string>(key: string): T | undefined;
15+
readEnum<T extends string>(key: string, values: T[], defaultValue: T): T;
16+
readEnum<T extends string>(key: string, values: T[]): T | undefined;
17+
readInt<T extends number>(key: string, defaultValue: T): T;
18+
readInt<T extends number>(key: string): T | undefined;
19+
readBool<T extends boolean>(key: string, defaultValue: T): T;
20+
readBool<T extends boolean>(key: string): T | undefined;
21+
}
22+
23+
export type Promisable<T> = PromiseLike<T> | T;
24+
export type ConfigureFn<T = EnvVariables> = (
25+
reader: EnvReader,
26+
target: T,
27+
) => Promisable<T | void>;
28+
29+
// public functions
30+
export const createEnvReader = (state: EnvMap): EnvReader => {
31+
return {
32+
[env]: state.get(env) ?? defaultEnvValue,
33+
readString: <T extends string>(
34+
key: string,
35+
defaultValue?: T,
36+
): T | undefined => {
37+
return state.get(key) as T ?? defaultValue;
38+
},
39+
readEnum: <T extends string>(
40+
key: string,
41+
values: T[],
42+
defaultValue?: T,
43+
): T | undefined => {
44+
const value = state.get(key);
45+
46+
if (value === undefined) {
47+
return defaultValue;
48+
}
49+
50+
if (values.includes(value as T)) {
51+
return value as T;
52+
}
53+
54+
return defaultValue;
55+
},
56+
readInt: <T extends number>(
57+
key: string,
58+
defaultValue?: T,
59+
): T | undefined => {
60+
const value = state.get(key);
61+
62+
if (value === undefined) {
63+
return defaultValue;
64+
}
65+
66+
return parseInt(value, 10) as T;
67+
},
68+
readBool: <T extends boolean>(
69+
key: string,
70+
defaultValue?: T,
71+
): T | undefined => {
72+
const value = state.get(key);
73+
74+
if (value === undefined) {
75+
return defaultValue;
76+
}
77+
78+
const sanitizedValue = value.trim().toLowerCase();
79+
80+
if (["1", "true", true].includes(sanitizedValue)) {
81+
return true as T;
82+
}
83+
84+
return false as T;
85+
},
86+
};
87+
};
88+
89+
export const configure = async <T>(
90+
configureFn: ConfigureFn<T>,
91+
target?: Partial<T>,
92+
options?: LoaderOptions,
93+
): Promise<T | undefined> => {
94+
const envMap = await load(options);
95+
const reader = createEnvReader(envMap);
96+
97+
const result = await configureFn(reader, target as T);
98+
99+
return result ?? Promise.resolve(target as T);
100+
};
101+
102+
export { type LoaderOptions };

hex/mod.ts

-4
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,12 @@ import metadata from "../metadata.json" assert { type: "json" };
22

33
export * as cli from "./cli/mod.ts";
44
export * as data from "./data/mod.ts";
5-
export * as di from "./di/mod.ts";
65
export * as environment from "./environment/mod.ts";
76
export * as formatters from "./formatters/mod.ts";
8-
export * as fp from "./fp/mod.ts";
97
export * as functions from "./functions/mod.ts";
108
export * as generator from "./service/mod.ts";
119
export * as i18n from "./i18n/mod.ts";
12-
export * as options from "./options/mod.ts";
1310
export * as service from "./service/mod.ts";
14-
export * as standards from "./standards/mod.ts";
1511
export * as stdx from "./stdx/mod.ts";
1612
export * as web from "./web/mod.ts";
1713

hex/options/deps.ts

-1
This file was deleted.

hex/options/env.ts

-65
This file was deleted.

hex/options/mod.ts

-2
This file was deleted.

0 commit comments

Comments
 (0)