@bintvn/lite-env 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +192 -0
- package/dist/index.cjs +200 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +166 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -0
- package/src/helper.ts +137 -0
- package/src/index.ts +76 -0
- package/src/types.ts +21 -0
- package/tsconfig.json +26 -0
- package/tsup.config.ts +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# lite-env
|
|
2
|
+
|
|
3
|
+
`lite-env` is a small TypeScript-first environment loader for Node.js. It reads `.env` and `.env.{NODE_ENV}`, merges them, parses values into runtime types, and returns a type-safe object inferred from your schema.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Loads `.env` and `.env.{NODE_ENV}`
|
|
8
|
+
- Infers the return type from the schema you pass to `loadEnv`
|
|
9
|
+
- Parses common value types: `string`, `number`, `boolean`, `array`, `object`, `buffer`, and `date`
|
|
10
|
+
- Can optionally reject unknown keys in your env files
|
|
11
|
+
- Stores the parsed result in a global cache to avoid reloading
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install lite-env
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
Create your env files:
|
|
22
|
+
|
|
23
|
+
```env
|
|
24
|
+
# .env
|
|
25
|
+
APP_NAME=lite-env
|
|
26
|
+
PORT=3000
|
|
27
|
+
DEBUG=false
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```env
|
|
31
|
+
# .env.development
|
|
32
|
+
DEBUG=true
|
|
33
|
+
TAGS=api,dev,local
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Use `loadEnv` in your app:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { loadEnv } from 'lite-env'
|
|
40
|
+
|
|
41
|
+
const env = loadEnv({
|
|
42
|
+
APP_NAME: 'string',
|
|
43
|
+
PORT: 'number',
|
|
44
|
+
DEBUG: 'boolean',
|
|
45
|
+
TAGS: 'array',
|
|
46
|
+
}, true, 'development')
|
|
47
|
+
|
|
48
|
+
env.APP_NAME
|
|
49
|
+
// string
|
|
50
|
+
|
|
51
|
+
env.PORT
|
|
52
|
+
// number
|
|
53
|
+
|
|
54
|
+
env.DEBUG
|
|
55
|
+
// boolean
|
|
56
|
+
|
|
57
|
+
env.TAGS
|
|
58
|
+
// string[]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## How It Works
|
|
62
|
+
|
|
63
|
+
`loadEnv` looks for:
|
|
64
|
+
|
|
65
|
+
- `.env`
|
|
66
|
+
- `.env.{NODE_ENV}`
|
|
67
|
+
|
|
68
|
+
The values from `.env.{NODE_ENV}` override values from `.env`.
|
|
69
|
+
|
|
70
|
+
If `NODE_ENV` is not provided:
|
|
71
|
+
|
|
72
|
+
- it uses `process.env.NODE_ENV` if available
|
|
73
|
+
- otherwise it defaults to `production`
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### `loadEnv(schema, bypassUnknownEnvKeys?, NODE_ENV?)`
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
function loadEnv<TSchema extends AllowedEnvKeys = {}>(
|
|
81
|
+
schema?: TSchema,
|
|
82
|
+
bypassUnknownEnvKeys?: boolean,
|
|
83
|
+
NODE_ENV?: string
|
|
84
|
+
): LoadedEnv<TSchema>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### Parameters
|
|
88
|
+
|
|
89
|
+
- `schema`: a map of env keys to parser kinds
|
|
90
|
+
- `bypassUnknownEnvKeys`: when `true`, unknown env keys are allowed; when `false`, unknown keys throw an error
|
|
91
|
+
- `NODE_ENV`: optional environment name used to resolve `.env.{NODE_ENV}`
|
|
92
|
+
|
|
93
|
+
#### Return Value
|
|
94
|
+
|
|
95
|
+
Returns a parsed object with types inferred from the schema.
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const env = loadEnv({
|
|
101
|
+
PORT: 'number',
|
|
102
|
+
ENABLE_LOGS: 'boolean',
|
|
103
|
+
START_DATE: 'date',
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// inferred type:
|
|
107
|
+
// {
|
|
108
|
+
// PORT: number
|
|
109
|
+
// ENABLE_LOGS: boolean
|
|
110
|
+
// START_DATE: Date
|
|
111
|
+
// }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Supported Types
|
|
115
|
+
|
|
116
|
+
`lite-env` currently supports the following schema kinds:
|
|
117
|
+
|
|
118
|
+
| Schema value | Result type | Notes |
|
|
119
|
+
| --- | --- | --- |
|
|
120
|
+
| `string` | `string` | Raw string |
|
|
121
|
+
| `number` | `number` | Uses `Number(value)` |
|
|
122
|
+
| `boolean` | `boolean` | Accepts `true`, `false`, `1`, `0` |
|
|
123
|
+
| `array` | `string[]` | Splits by comma |
|
|
124
|
+
| `object` | `unknown` | Uses `JSON.parse` |
|
|
125
|
+
| `buffer` | `Buffer` | Uses `Buffer.from(value, 'base64')` |
|
|
126
|
+
| `date` | `Date` | Uses `new Date(value)` |
|
|
127
|
+
|
|
128
|
+
## Type Inference
|
|
129
|
+
|
|
130
|
+
The main goal of `lite-env` is type-safe inference from the input schema.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const env = loadEnv({
|
|
134
|
+
DB_HOST: 'string',
|
|
135
|
+
DB_PORT: 'number',
|
|
136
|
+
DB_SSL: 'boolean',
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
TypeScript infers:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
{
|
|
144
|
+
DB_HOST: string
|
|
145
|
+
DB_PORT: number
|
|
146
|
+
DB_SSL: boolean
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`object` currently resolves to `unknown` on purpose, so the public API stays simple.
|
|
151
|
+
|
|
152
|
+
## Unknown Key Behavior
|
|
153
|
+
|
|
154
|
+
By default, `bypassUnknownEnvKeys` is `true`.
|
|
155
|
+
|
|
156
|
+
That means:
|
|
157
|
+
|
|
158
|
+
- extra keys in your env files are allowed
|
|
159
|
+
- raw merged env values are copied into `process.env`
|
|
160
|
+
|
|
161
|
+
If you want strict validation, pass `false`:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const env = loadEnv({
|
|
165
|
+
PORT: 'number',
|
|
166
|
+
DEBUG: 'boolean',
|
|
167
|
+
}, false)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
In strict mode, any key found in the env files but not declared in the schema will throw an error.
|
|
171
|
+
|
|
172
|
+
## Notes
|
|
173
|
+
|
|
174
|
+
- Parsed results are cached in `globalThis.liteEnv`
|
|
175
|
+
- If you call `loadEnv()` multiple times with different schemas in the same runtime, the cached result is reused
|
|
176
|
+
- Missing declared keys are currently skipped instead of throwing
|
|
177
|
+
- `.env` must exist
|
|
178
|
+
- `.env.{NODE_ENV}` must also exist
|
|
179
|
+
|
|
180
|
+
## Example Project Structure
|
|
181
|
+
|
|
182
|
+
```txt
|
|
183
|
+
your-project/
|
|
184
|
+
├─ .env
|
|
185
|
+
├─ .env.development
|
|
186
|
+
├─ src/
|
|
187
|
+
│ └─ index.ts
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
ISC
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// src/index.ts
|
|
30
|
+
var index_exports = {};
|
|
31
|
+
__export(index_exports, {
|
|
32
|
+
loadEnv: () => loadEnv
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(index_exports);
|
|
35
|
+
var import_path = __toESM(require("path"), 1);
|
|
36
|
+
var import_fs2 = require("fs");
|
|
37
|
+
|
|
38
|
+
// src/helper.ts
|
|
39
|
+
var import_fs = require("fs");
|
|
40
|
+
function toString(value) {
|
|
41
|
+
return String(value);
|
|
42
|
+
}
|
|
43
|
+
function toNumber(value) {
|
|
44
|
+
const parsed = Number(value);
|
|
45
|
+
if (Number.isNaN(parsed))
|
|
46
|
+
throw new Error("Value is not supported for number");
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
function toArray(value) {
|
|
50
|
+
if (!value.includes(","))
|
|
51
|
+
throw new Error("Valur is not supported for array");
|
|
52
|
+
return value.split(",");
|
|
53
|
+
}
|
|
54
|
+
function toBoolean(value) {
|
|
55
|
+
if (value === "true" || value === "1")
|
|
56
|
+
return true;
|
|
57
|
+
if (value === "false" || value === "0")
|
|
58
|
+
return false;
|
|
59
|
+
throw new Error("Value is not supported for boolean");
|
|
60
|
+
}
|
|
61
|
+
function toObject(value) {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(value);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error("Value is not supported for object");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function toBuffer(value, encoding) {
|
|
69
|
+
try {
|
|
70
|
+
return Buffer.from(value, encoding || "base64");
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new Error("Value is not supported for buffer");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function toDate(value) {
|
|
76
|
+
const parsed = new Date(value);
|
|
77
|
+
if (Number.isNaN(parsed.getTime()))
|
|
78
|
+
throw new Error("Value is not supported for date");
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
function parseEnvValue(kind, value) {
|
|
82
|
+
switch (kind) {
|
|
83
|
+
case "string":
|
|
84
|
+
return toString(value);
|
|
85
|
+
case "number":
|
|
86
|
+
return toNumber(value);
|
|
87
|
+
case "boolean":
|
|
88
|
+
return toBoolean(value);
|
|
89
|
+
case "array":
|
|
90
|
+
return toArray(value);
|
|
91
|
+
case "object":
|
|
92
|
+
return toObject(value);
|
|
93
|
+
case "buffer":
|
|
94
|
+
return toBuffer(value);
|
|
95
|
+
case "date":
|
|
96
|
+
return toDate(value);
|
|
97
|
+
default: {
|
|
98
|
+
const exhaustiveCheck = kind;
|
|
99
|
+
throw new Error(`Unsupported env kind: ${exhaustiveCheck}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function safeParse(allowedEnvKeys, values) {
|
|
104
|
+
const data = {};
|
|
105
|
+
const error = [];
|
|
106
|
+
for (const key of Object.keys(allowedEnvKeys)) {
|
|
107
|
+
const value = values[key];
|
|
108
|
+
if (value === void 0) continue;
|
|
109
|
+
try {
|
|
110
|
+
data[key] = parseEnvValue(allowedEnvKeys[key], value);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
if (e instanceof Error)
|
|
113
|
+
error.push(`${String(key)}: ${e.message}`);
|
|
114
|
+
else
|
|
115
|
+
error.push(`${String(key)}: Unknown error occurred`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
success: error.length === 0,
|
|
120
|
+
data,
|
|
121
|
+
error
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function formatError(message) {
|
|
125
|
+
return message.join("\n");
|
|
126
|
+
}
|
|
127
|
+
function parseRawEnv(rawEnv) {
|
|
128
|
+
const result = {};
|
|
129
|
+
const lines = rawEnv.split(/\r?\n/);
|
|
130
|
+
for (const line of lines) {
|
|
131
|
+
const normalized = line.trim();
|
|
132
|
+
if (!normalized || normalized.startsWith("#")) continue;
|
|
133
|
+
const separatorIndex = normalized.indexOf("=");
|
|
134
|
+
if (separatorIndex <= 0) continue;
|
|
135
|
+
const key = normalized.slice(0, separatorIndex).trim();
|
|
136
|
+
const value = normalized.slice(separatorIndex + 1).trim();
|
|
137
|
+
if (!key) continue;
|
|
138
|
+
result[key] = value;
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
function readEnvFile(filePath) {
|
|
143
|
+
if (!(0, import_fs.existsSync)(filePath)) return false;
|
|
144
|
+
return parseRawEnv((0, import_fs.readFileSync)(filePath, "utf-8"));
|
|
145
|
+
}
|
|
146
|
+
function getUnknownEnvKeys(source, allowedEnvKeys) {
|
|
147
|
+
return Object.keys(source).filter((key) => !allowedEnvKeys.has(key));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/index.ts
|
|
151
|
+
function loadEnv(allowedEnvKeys = {}, bypassUnknownEnvKeys = true, NODE_ENV) {
|
|
152
|
+
if (globalThis.liteEnv)
|
|
153
|
+
return globalThis.liteEnv;
|
|
154
|
+
if (NODE_ENV) {
|
|
155
|
+
process.env.NODE_ENV = NODE_ENV;
|
|
156
|
+
} else {
|
|
157
|
+
if (!process.env.NODE_ENV) {
|
|
158
|
+
process.env.NODE_ENV = "production";
|
|
159
|
+
}
|
|
160
|
+
NODE_ENV = process.env.NODE_ENV;
|
|
161
|
+
}
|
|
162
|
+
const defaultEnvPath = import_path.default.join(process.cwd(), ".env");
|
|
163
|
+
const nodeEnvPath = import_path.default.join(process.cwd(), `.env.${NODE_ENV}`);
|
|
164
|
+
if (!(0, import_fs2.existsSync)(defaultEnvPath))
|
|
165
|
+
throw new Error("Default Environment file not found");
|
|
166
|
+
if (!(0, import_fs2.existsSync)(nodeEnvPath))
|
|
167
|
+
throw new Error(`Environment file not found: .env.${NODE_ENV}`);
|
|
168
|
+
const defaultEnvSource = readEnvFile(defaultEnvPath);
|
|
169
|
+
const nodeEnvSource = readEnvFile(nodeEnvPath);
|
|
170
|
+
const mergedEnv = {};
|
|
171
|
+
if (defaultEnvSource)
|
|
172
|
+
Object.assign(mergedEnv, defaultEnvSource);
|
|
173
|
+
if (nodeEnvSource)
|
|
174
|
+
Object.assign(mergedEnv, nodeEnvSource);
|
|
175
|
+
if (!bypassUnknownEnvKeys) {
|
|
176
|
+
const unknownKeys = [.../* @__PURE__ */ new Set([...getUnknownEnvKeys(mergedEnv, new Set(Object.keys(allowedEnvKeys)))])];
|
|
177
|
+
if (unknownKeys.length > 0)
|
|
178
|
+
throw new Error(`Unknown environment variables in env files: ${unknownKeys.join(", ")}`);
|
|
179
|
+
} else {
|
|
180
|
+
process.env = {
|
|
181
|
+
...process.env,
|
|
182
|
+
...mergedEnv
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const parsedEnv = safeParse(allowedEnvKeys, mergedEnv);
|
|
186
|
+
if (!parsedEnv.success)
|
|
187
|
+
throw new Error(`Environment validation failed: ${formatError(parsedEnv.error)}`);
|
|
188
|
+
globalThis.liteEnv = parsedEnv.data;
|
|
189
|
+
process.env = {
|
|
190
|
+
...process.env,
|
|
191
|
+
...parsedEnv.data
|
|
192
|
+
};
|
|
193
|
+
console.log("Loaded env file", `${defaultEnvPath}, ${nodeEnvPath}`);
|
|
194
|
+
return parsedEnv.data;
|
|
195
|
+
}
|
|
196
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
197
|
+
0 && (module.exports = {
|
|
198
|
+
loadEnv
|
|
199
|
+
});
|
|
200
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/helper.ts"],"sourcesContent":["import path from 'path'\r\nimport { existsSync } from 'fs'\r\nimport { formatError, getUnknownEnvKeys, readEnvFile, safeParse } from './helper.js'\r\nimport { AllowedEnvKeys, LoadedEnv } from \"./types.js\";\r\n\r\nexport type { AllowedEnvKeys, EnvKind, EnvKindMap, LoadedEnv } from \"./types.js\"\r\n\r\ndeclare global {\r\n var liteEnv: unknown\r\n}\r\n\r\nexport function loadEnv<TSchema extends AllowedEnvKeys = {}>(\r\n allowedEnvKeys: TSchema = {} as TSchema,\r\n bypassUnknownEnvKeys: boolean = true,\r\n NODE_ENV?: string\r\n): LoadedEnv<TSchema> {\r\n if (globalThis.liteEnv)\r\n return globalThis.liteEnv as LoadedEnv<TSchema>\r\n\r\n if (NODE_ENV) {\r\n process.env.NODE_ENV = NODE_ENV\r\n } else {\r\n if (!process.env.NODE_ENV) {\r\n process.env.NODE_ENV = 'production'\r\n }\r\n\r\n NODE_ENV = process.env.NODE_ENV\r\n }\r\n\r\n const defaultEnvPath = path.join(process.cwd(), '.env')\r\n const nodeEnvPath = path.join(process.cwd(), `.env.${NODE_ENV}`)\r\n\r\n if (!existsSync(defaultEnvPath))\r\n throw new Error('Default Environment file not found')\r\n\r\n if (!existsSync(nodeEnvPath))\r\n throw new Error(`Environment file not found: .env.${NODE_ENV}`)\r\n\r\n const defaultEnvSource = readEnvFile(defaultEnvPath)\r\n const nodeEnvSource = readEnvFile(nodeEnvPath)\r\n\r\n const mergedEnv: Record<string, string> = {}\r\n\r\n if (defaultEnvSource)\r\n Object.assign(mergedEnv, defaultEnvSource)\r\n if (nodeEnvSource)\r\n Object.assign(mergedEnv, nodeEnvSource)\r\n\r\n if (!bypassUnknownEnvKeys) {\r\n const unknownKeys = [...new Set([...getUnknownEnvKeys(mergedEnv, new Set(Object.keys(allowedEnvKeys)))])]\r\n\r\n if (unknownKeys.length > 0)\r\n throw new Error(`Unknown environment variables in env files: ${unknownKeys.join(', ')}`)\r\n } else {\r\n process.env = {\r\n ...process.env,\r\n ...mergedEnv\r\n }\r\n }\r\n\r\n const parsedEnv = safeParse(allowedEnvKeys, mergedEnv)\r\n\r\n if (!parsedEnv.success)\r\n throw new Error(`Environment validation failed: ${formatError(parsedEnv.error)}`)\r\n\r\n globalThis.liteEnv = parsedEnv.data\r\n\r\n process.env = {\r\n ...process.env,\r\n ...parsedEnv.data\r\n }\r\n\r\n console.log('Loaded env file', `${defaultEnvPath}, ${nodeEnvPath}`)\r\n\r\n return parsedEnv.data as LoadedEnv<TSchema>\r\n}\r\n","import { existsSync, readFileSync } from \"fs\"\nimport { AllowedEnvKeys, EnvKind, EnvKindMap, LoadedEnv } from \"./types.js\"\n\r\nexport function toString(value: string): string {\r\n return String(value)\r\n}\r\n\r\nexport function toNumber(value: string): number {\n const parsed = Number(value)\n if (Number.isNaN(parsed))\n throw new Error('Value is not supported for number')\n\n return parsed\n}\n\r\nexport function toArray(value: string): string[] {\r\n if (!value.includes(','))\r\n throw new Error('Valur is not supported for array')\r\n\r\n return value.split(',')\r\n}\r\n\r\nexport function toBoolean(value: string): boolean {\n if (value === 'true' || value === '1')\n return true\n\n if (value === 'false' || value === '0')\n return false\n\n throw new Error('Value is not supported for boolean')\n}\n\r\nexport function toObject<T>(value: string): T {\r\n try {\r\n return JSON.parse(value)\r\n } catch (error) {\r\n throw new Error('Value is not supported for object')\r\n }\r\n}\r\n\r\nexport function toBuffer(value: string, encoding?: BufferEncoding): Buffer {\r\n try {\r\n return Buffer.from(value, encoding || 'base64')\r\n } catch (error) {\r\n throw new Error('Value is not supported for buffer')\r\n }\r\n}\r\n\r\nexport function toDate(value: string): Date {\n const parsed = new Date(value)\n if (Number.isNaN(parsed.getTime()))\n throw new Error('Value is not supported for date')\n\n return parsed\n}\n\nfunction parseEnvValue<TKind extends EnvKind>(kind: TKind, value: string): EnvKindMap[TKind] {\n switch (kind) {\n case \"string\":\n return toString(value) as EnvKindMap[TKind]\n case \"number\":\n return toNumber(value) as EnvKindMap[TKind]\n case \"boolean\":\n return toBoolean(value) as EnvKindMap[TKind]\n case \"array\":\n return toArray(value) as EnvKindMap[TKind]\n case \"object\":\n return toObject(value) as EnvKindMap[TKind]\n case \"buffer\":\n return toBuffer(value) as EnvKindMap[TKind]\n case \"date\":\n return toDate(value) as EnvKindMap[TKind]\n default: {\n const exhaustiveCheck: never = kind\n throw new Error(`Unsupported env kind: ${exhaustiveCheck}`)\n }\n }\n}\n\nexport function safeParse<TSchema extends AllowedEnvKeys>(allowedEnvKeys: TSchema, values: Record<string, string>) {\n const data: Partial<LoadedEnv<TSchema>> = {}\n const error: string[] = []\n\n for (const key of Object.keys(allowedEnvKeys) as Array<keyof TSchema>) {\n const value = values[key as string]\n if (value === undefined) continue\n\n try {\n data[key] = parseEnvValue(allowedEnvKeys[key], value)\n } catch (e) {\n if (e instanceof Error)\n error.push(`${String(key)}: ${e.message}`)\n else\n error.push(`${String(key)}: Unknown error occurred`)\n }\n }\n\n return {\n success: error.length === 0,\n data: data as LoadedEnv<TSchema>,\n error\n }\n}\n\r\nexport function formatError(message: string[]): string {\r\n return message.join('\\n')\r\n}\r\n\r\nexport function parseRawEnv(rawEnv: string): Record<string, string> {\r\n const result: Record<string, string> = {}\r\n const lines = rawEnv.split(/\\r?\\n/)\r\n\r\n for (const line of lines) {\r\n const normalized = line.trim()\r\n if (!normalized || normalized.startsWith('#')) continue\r\n\r\n const separatorIndex = normalized.indexOf('=')\r\n if (separatorIndex <= 0) continue\r\n\r\n const key = normalized.slice(0, separatorIndex).trim()\r\n const value = normalized.slice(separatorIndex + 1).trim()\r\n if (!key) continue\r\n\r\n result[key] = value\r\n }\r\n\r\n return result\r\n}\r\n\r\nexport function readEnvFile(filePath: string): false | Record<string, string> {\r\n if (!existsSync(filePath)) return false\r\n return parseRawEnv(readFileSync(filePath, 'utf-8'))\r\n}\r\n\r\nexport function getUnknownEnvKeys(source: Record<string, string>, allowedEnvKeys: Set<string>): string[] {\r\n return Object.keys(source).filter((key) => !allowedEnvKeys.has(key))\r\n}\r\n"],"mappings":"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","names":["import_fs","path"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
type EnvKindMap = {
|
|
2
|
+
string: string;
|
|
3
|
+
number: number;
|
|
4
|
+
boolean: boolean;
|
|
5
|
+
array: string[];
|
|
6
|
+
object: unknown;
|
|
7
|
+
buffer: Buffer;
|
|
8
|
+
date: Date;
|
|
9
|
+
};
|
|
10
|
+
type EnvKind = keyof EnvKindMap;
|
|
11
|
+
type AllowedEnvKeys = Record<string, EnvKind>;
|
|
12
|
+
type LoadedEnv<TSchema extends AllowedEnvKeys> = {
|
|
13
|
+
[K in keyof TSchema]: EnvKindMap[TSchema[K]];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
declare global {
|
|
17
|
+
var liteEnv: unknown;
|
|
18
|
+
}
|
|
19
|
+
declare function loadEnv<TSchema extends AllowedEnvKeys = {}>(allowedEnvKeys?: TSchema, bypassUnknownEnvKeys?: boolean, NODE_ENV?: string): LoadedEnv<TSchema>;
|
|
20
|
+
|
|
21
|
+
export { type AllowedEnvKeys, type EnvKind, type EnvKindMap, type LoadedEnv, loadEnv };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
type EnvKindMap = {
|
|
2
|
+
string: string;
|
|
3
|
+
number: number;
|
|
4
|
+
boolean: boolean;
|
|
5
|
+
array: string[];
|
|
6
|
+
object: unknown;
|
|
7
|
+
buffer: Buffer;
|
|
8
|
+
date: Date;
|
|
9
|
+
};
|
|
10
|
+
type EnvKind = keyof EnvKindMap;
|
|
11
|
+
type AllowedEnvKeys = Record<string, EnvKind>;
|
|
12
|
+
type LoadedEnv<TSchema extends AllowedEnvKeys> = {
|
|
13
|
+
[K in keyof TSchema]: EnvKindMap[TSchema[K]];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
declare global {
|
|
17
|
+
var liteEnv: unknown;
|
|
18
|
+
}
|
|
19
|
+
declare function loadEnv<TSchema extends AllowedEnvKeys = {}>(allowedEnvKeys?: TSchema, bypassUnknownEnvKeys?: boolean, NODE_ENV?: string): LoadedEnv<TSchema>;
|
|
20
|
+
|
|
21
|
+
export { type AllowedEnvKeys, type EnvKind, type EnvKindMap, type LoadedEnv, loadEnv };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { existsSync as existsSync2 } from "fs";
|
|
4
|
+
|
|
5
|
+
// src/helper.ts
|
|
6
|
+
import { existsSync, readFileSync } from "fs";
|
|
7
|
+
function toString(value) {
|
|
8
|
+
return String(value);
|
|
9
|
+
}
|
|
10
|
+
function toNumber(value) {
|
|
11
|
+
const parsed = Number(value);
|
|
12
|
+
if (Number.isNaN(parsed))
|
|
13
|
+
throw new Error("Value is not supported for number");
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
16
|
+
function toArray(value) {
|
|
17
|
+
if (!value.includes(","))
|
|
18
|
+
throw new Error("Valur is not supported for array");
|
|
19
|
+
return value.split(",");
|
|
20
|
+
}
|
|
21
|
+
function toBoolean(value) {
|
|
22
|
+
if (value === "true" || value === "1")
|
|
23
|
+
return true;
|
|
24
|
+
if (value === "false" || value === "0")
|
|
25
|
+
return false;
|
|
26
|
+
throw new Error("Value is not supported for boolean");
|
|
27
|
+
}
|
|
28
|
+
function toObject(value) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(value);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error("Value is not supported for object");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function toBuffer(value, encoding) {
|
|
36
|
+
try {
|
|
37
|
+
return Buffer.from(value, encoding || "base64");
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new Error("Value is not supported for buffer");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function toDate(value) {
|
|
43
|
+
const parsed = new Date(value);
|
|
44
|
+
if (Number.isNaN(parsed.getTime()))
|
|
45
|
+
throw new Error("Value is not supported for date");
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
function parseEnvValue(kind, value) {
|
|
49
|
+
switch (kind) {
|
|
50
|
+
case "string":
|
|
51
|
+
return toString(value);
|
|
52
|
+
case "number":
|
|
53
|
+
return toNumber(value);
|
|
54
|
+
case "boolean":
|
|
55
|
+
return toBoolean(value);
|
|
56
|
+
case "array":
|
|
57
|
+
return toArray(value);
|
|
58
|
+
case "object":
|
|
59
|
+
return toObject(value);
|
|
60
|
+
case "buffer":
|
|
61
|
+
return toBuffer(value);
|
|
62
|
+
case "date":
|
|
63
|
+
return toDate(value);
|
|
64
|
+
default: {
|
|
65
|
+
const exhaustiveCheck = kind;
|
|
66
|
+
throw new Error(`Unsupported env kind: ${exhaustiveCheck}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function safeParse(allowedEnvKeys, values) {
|
|
71
|
+
const data = {};
|
|
72
|
+
const error = [];
|
|
73
|
+
for (const key of Object.keys(allowedEnvKeys)) {
|
|
74
|
+
const value = values[key];
|
|
75
|
+
if (value === void 0) continue;
|
|
76
|
+
try {
|
|
77
|
+
data[key] = parseEnvValue(allowedEnvKeys[key], value);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
if (e instanceof Error)
|
|
80
|
+
error.push(`${String(key)}: ${e.message}`);
|
|
81
|
+
else
|
|
82
|
+
error.push(`${String(key)}: Unknown error occurred`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
success: error.length === 0,
|
|
87
|
+
data,
|
|
88
|
+
error
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function formatError(message) {
|
|
92
|
+
return message.join("\n");
|
|
93
|
+
}
|
|
94
|
+
function parseRawEnv(rawEnv) {
|
|
95
|
+
const result = {};
|
|
96
|
+
const lines = rawEnv.split(/\r?\n/);
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
const normalized = line.trim();
|
|
99
|
+
if (!normalized || normalized.startsWith("#")) continue;
|
|
100
|
+
const separatorIndex = normalized.indexOf("=");
|
|
101
|
+
if (separatorIndex <= 0) continue;
|
|
102
|
+
const key = normalized.slice(0, separatorIndex).trim();
|
|
103
|
+
const value = normalized.slice(separatorIndex + 1).trim();
|
|
104
|
+
if (!key) continue;
|
|
105
|
+
result[key] = value;
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
function readEnvFile(filePath) {
|
|
110
|
+
if (!existsSync(filePath)) return false;
|
|
111
|
+
return parseRawEnv(readFileSync(filePath, "utf-8"));
|
|
112
|
+
}
|
|
113
|
+
function getUnknownEnvKeys(source, allowedEnvKeys) {
|
|
114
|
+
return Object.keys(source).filter((key) => !allowedEnvKeys.has(key));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/index.ts
|
|
118
|
+
function loadEnv(allowedEnvKeys = {}, bypassUnknownEnvKeys = true, NODE_ENV) {
|
|
119
|
+
if (globalThis.liteEnv)
|
|
120
|
+
return globalThis.liteEnv;
|
|
121
|
+
if (NODE_ENV) {
|
|
122
|
+
process.env.NODE_ENV = NODE_ENV;
|
|
123
|
+
} else {
|
|
124
|
+
if (!process.env.NODE_ENV) {
|
|
125
|
+
process.env.NODE_ENV = "production";
|
|
126
|
+
}
|
|
127
|
+
NODE_ENV = process.env.NODE_ENV;
|
|
128
|
+
}
|
|
129
|
+
const defaultEnvPath = path.join(process.cwd(), ".env");
|
|
130
|
+
const nodeEnvPath = path.join(process.cwd(), `.env.${NODE_ENV}`);
|
|
131
|
+
if (!existsSync2(defaultEnvPath))
|
|
132
|
+
throw new Error("Default Environment file not found");
|
|
133
|
+
if (!existsSync2(nodeEnvPath))
|
|
134
|
+
throw new Error(`Environment file not found: .env.${NODE_ENV}`);
|
|
135
|
+
const defaultEnvSource = readEnvFile(defaultEnvPath);
|
|
136
|
+
const nodeEnvSource = readEnvFile(nodeEnvPath);
|
|
137
|
+
const mergedEnv = {};
|
|
138
|
+
if (defaultEnvSource)
|
|
139
|
+
Object.assign(mergedEnv, defaultEnvSource);
|
|
140
|
+
if (nodeEnvSource)
|
|
141
|
+
Object.assign(mergedEnv, nodeEnvSource);
|
|
142
|
+
if (!bypassUnknownEnvKeys) {
|
|
143
|
+
const unknownKeys = [.../* @__PURE__ */ new Set([...getUnknownEnvKeys(mergedEnv, new Set(Object.keys(allowedEnvKeys)))])];
|
|
144
|
+
if (unknownKeys.length > 0)
|
|
145
|
+
throw new Error(`Unknown environment variables in env files: ${unknownKeys.join(", ")}`);
|
|
146
|
+
} else {
|
|
147
|
+
process.env = {
|
|
148
|
+
...process.env,
|
|
149
|
+
...mergedEnv
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const parsedEnv = safeParse(allowedEnvKeys, mergedEnv);
|
|
153
|
+
if (!parsedEnv.success)
|
|
154
|
+
throw new Error(`Environment validation failed: ${formatError(parsedEnv.error)}`);
|
|
155
|
+
globalThis.liteEnv = parsedEnv.data;
|
|
156
|
+
process.env = {
|
|
157
|
+
...process.env,
|
|
158
|
+
...parsedEnv.data
|
|
159
|
+
};
|
|
160
|
+
console.log("Loaded env file", `${defaultEnvPath}, ${nodeEnvPath}`);
|
|
161
|
+
return parsedEnv.data;
|
|
162
|
+
}
|
|
163
|
+
export {
|
|
164
|
+
loadEnv
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/helper.ts"],"sourcesContent":["import path from 'path'\r\nimport { existsSync } from 'fs'\r\nimport { formatError, getUnknownEnvKeys, readEnvFile, safeParse } from './helper.js'\r\nimport { AllowedEnvKeys, LoadedEnv } from \"./types.js\";\r\n\r\nexport type { AllowedEnvKeys, EnvKind, EnvKindMap, LoadedEnv } from \"./types.js\"\r\n\r\ndeclare global {\r\n var liteEnv: unknown\r\n}\r\n\r\nexport function loadEnv<TSchema extends AllowedEnvKeys = {}>(\r\n allowedEnvKeys: TSchema = {} as TSchema,\r\n bypassUnknownEnvKeys: boolean = true,\r\n NODE_ENV?: string\r\n): LoadedEnv<TSchema> {\r\n if (globalThis.liteEnv)\r\n return globalThis.liteEnv as LoadedEnv<TSchema>\r\n\r\n if (NODE_ENV) {\r\n process.env.NODE_ENV = NODE_ENV\r\n } else {\r\n if (!process.env.NODE_ENV) {\r\n process.env.NODE_ENV = 'production'\r\n }\r\n\r\n NODE_ENV = process.env.NODE_ENV\r\n }\r\n\r\n const defaultEnvPath = path.join(process.cwd(), '.env')\r\n const nodeEnvPath = path.join(process.cwd(), `.env.${NODE_ENV}`)\r\n\r\n if (!existsSync(defaultEnvPath))\r\n throw new Error('Default Environment file not found')\r\n\r\n if (!existsSync(nodeEnvPath))\r\n throw new Error(`Environment file not found: .env.${NODE_ENV}`)\r\n\r\n const defaultEnvSource = readEnvFile(defaultEnvPath)\r\n const nodeEnvSource = readEnvFile(nodeEnvPath)\r\n\r\n const mergedEnv: Record<string, string> = {}\r\n\r\n if (defaultEnvSource)\r\n Object.assign(mergedEnv, defaultEnvSource)\r\n if (nodeEnvSource)\r\n Object.assign(mergedEnv, nodeEnvSource)\r\n\r\n if (!bypassUnknownEnvKeys) {\r\n const unknownKeys = [...new Set([...getUnknownEnvKeys(mergedEnv, new Set(Object.keys(allowedEnvKeys)))])]\r\n\r\n if (unknownKeys.length > 0)\r\n throw new Error(`Unknown environment variables in env files: ${unknownKeys.join(', ')}`)\r\n } else {\r\n process.env = {\r\n ...process.env,\r\n ...mergedEnv\r\n }\r\n }\r\n\r\n const parsedEnv = safeParse(allowedEnvKeys, mergedEnv)\r\n\r\n if (!parsedEnv.success)\r\n throw new Error(`Environment validation failed: ${formatError(parsedEnv.error)}`)\r\n\r\n globalThis.liteEnv = parsedEnv.data\r\n\r\n process.env = {\r\n ...process.env,\r\n ...parsedEnv.data\r\n }\r\n\r\n console.log('Loaded env file', `${defaultEnvPath}, ${nodeEnvPath}`)\r\n\r\n return parsedEnv.data as LoadedEnv<TSchema>\r\n}\r\n","import { existsSync, readFileSync } from \"fs\"\nimport { AllowedEnvKeys, EnvKind, EnvKindMap, LoadedEnv } from \"./types.js\"\n\r\nexport function toString(value: string): string {\r\n return String(value)\r\n}\r\n\r\nexport function toNumber(value: string): number {\n const parsed = Number(value)\n if (Number.isNaN(parsed))\n throw new Error('Value is not supported for number')\n\n return parsed\n}\n\r\nexport function toArray(value: string): string[] {\r\n if (!value.includes(','))\r\n throw new Error('Valur is not supported for array')\r\n\r\n return value.split(',')\r\n}\r\n\r\nexport function toBoolean(value: string): boolean {\n if (value === 'true' || value === '1')\n return true\n\n if (value === 'false' || value === '0')\n return false\n\n throw new Error('Value is not supported for boolean')\n}\n\r\nexport function toObject<T>(value: string): T {\r\n try {\r\n return JSON.parse(value)\r\n } catch (error) {\r\n throw new Error('Value is not supported for object')\r\n }\r\n}\r\n\r\nexport function toBuffer(value: string, encoding?: BufferEncoding): Buffer {\r\n try {\r\n return Buffer.from(value, encoding || 'base64')\r\n } catch (error) {\r\n throw new Error('Value is not supported for buffer')\r\n }\r\n}\r\n\r\nexport function toDate(value: string): Date {\n const parsed = new Date(value)\n if (Number.isNaN(parsed.getTime()))\n throw new Error('Value is not supported for date')\n\n return parsed\n}\n\nfunction parseEnvValue<TKind extends EnvKind>(kind: TKind, value: string): EnvKindMap[TKind] {\n switch (kind) {\n case \"string\":\n return toString(value) as EnvKindMap[TKind]\n case \"number\":\n return toNumber(value) as EnvKindMap[TKind]\n case \"boolean\":\n return toBoolean(value) as EnvKindMap[TKind]\n case \"array\":\n return toArray(value) as EnvKindMap[TKind]\n case \"object\":\n return toObject(value) as EnvKindMap[TKind]\n case \"buffer\":\n return toBuffer(value) as EnvKindMap[TKind]\n case \"date\":\n return toDate(value) as EnvKindMap[TKind]\n default: {\n const exhaustiveCheck: never = kind\n throw new Error(`Unsupported env kind: ${exhaustiveCheck}`)\n }\n }\n}\n\nexport function safeParse<TSchema extends AllowedEnvKeys>(allowedEnvKeys: TSchema, values: Record<string, string>) {\n const data: Partial<LoadedEnv<TSchema>> = {}\n const error: string[] = []\n\n for (const key of Object.keys(allowedEnvKeys) as Array<keyof TSchema>) {\n const value = values[key as string]\n if (value === undefined) continue\n\n try {\n data[key] = parseEnvValue(allowedEnvKeys[key], value)\n } catch (e) {\n if (e instanceof Error)\n error.push(`${String(key)}: ${e.message}`)\n else\n error.push(`${String(key)}: Unknown error occurred`)\n }\n }\n\n return {\n success: error.length === 0,\n data: data as LoadedEnv<TSchema>,\n error\n }\n}\n\r\nexport function formatError(message: string[]): string {\r\n return message.join('\\n')\r\n}\r\n\r\nexport function parseRawEnv(rawEnv: string): Record<string, string> {\r\n const result: Record<string, string> = {}\r\n const lines = rawEnv.split(/\\r?\\n/)\r\n\r\n for (const line of lines) {\r\n const normalized = line.trim()\r\n if (!normalized || normalized.startsWith('#')) continue\r\n\r\n const separatorIndex = normalized.indexOf('=')\r\n if (separatorIndex <= 0) continue\r\n\r\n const key = normalized.slice(0, separatorIndex).trim()\r\n const value = normalized.slice(separatorIndex + 1).trim()\r\n if (!key) continue\r\n\r\n result[key] = value\r\n }\r\n\r\n return result\r\n}\r\n\r\nexport function readEnvFile(filePath: string): false | Record<string, string> {\r\n if (!existsSync(filePath)) return false\r\n return parseRawEnv(readFileSync(filePath, 'utf-8'))\r\n}\r\n\r\nexport function getUnknownEnvKeys(source: Record<string, string>, allowedEnvKeys: Set<string>): string[] {\r\n return Object.keys(source).filter((key) => !allowedEnvKeys.has(key))\r\n}\r\n"],"mappings":"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","names":["existsSync","existsSync"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bintvn/lite-env",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TypeScript based environment variable loader.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"util",
|
|
7
|
+
"helper",
|
|
8
|
+
"environment",
|
|
9
|
+
"typescript"
|
|
10
|
+
],
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"author": "Bintan <hello@bintvn.co>",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.cjs",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"require": "./dist/index.cjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
26
|
+
"typecheck": "tsc --noEmit --project tsconfig.json",
|
|
27
|
+
"prepare": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^25.9.3",
|
|
31
|
+
"tsup": "^8.5.1",
|
|
32
|
+
"typescript": "^6.0.3"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/helper.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs"
|
|
2
|
+
import { AllowedEnvKeys, EnvKind, EnvKindMap, LoadedEnv } from "./types.js"
|
|
3
|
+
|
|
4
|
+
export function toString(value: string): string {
|
|
5
|
+
return String(value)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function toNumber(value: string): number {
|
|
9
|
+
const parsed = Number(value)
|
|
10
|
+
if (Number.isNaN(parsed))
|
|
11
|
+
throw new Error('Value is not supported for number')
|
|
12
|
+
|
|
13
|
+
return parsed
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function toArray(value: string): string[] {
|
|
17
|
+
if (!value.includes(','))
|
|
18
|
+
throw new Error('Valur is not supported for array')
|
|
19
|
+
|
|
20
|
+
return value.split(',')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function toBoolean(value: string): boolean {
|
|
24
|
+
if (value === 'true' || value === '1')
|
|
25
|
+
return true
|
|
26
|
+
|
|
27
|
+
if (value === 'false' || value === '0')
|
|
28
|
+
return false
|
|
29
|
+
|
|
30
|
+
throw new Error('Value is not supported for boolean')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function toObject<T>(value: string): T {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(value)
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw new Error('Value is not supported for object')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function toBuffer(value: string, encoding?: BufferEncoding): Buffer {
|
|
42
|
+
try {
|
|
43
|
+
return Buffer.from(value, encoding || 'base64')
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new Error('Value is not supported for buffer')
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function toDate(value: string): Date {
|
|
50
|
+
const parsed = new Date(value)
|
|
51
|
+
if (Number.isNaN(parsed.getTime()))
|
|
52
|
+
throw new Error('Value is not supported for date')
|
|
53
|
+
|
|
54
|
+
return parsed
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseEnvValue<TKind extends EnvKind>(kind: TKind, value: string): EnvKindMap[TKind] {
|
|
58
|
+
switch (kind) {
|
|
59
|
+
case "string":
|
|
60
|
+
return toString(value) as EnvKindMap[TKind]
|
|
61
|
+
case "number":
|
|
62
|
+
return toNumber(value) as EnvKindMap[TKind]
|
|
63
|
+
case "boolean":
|
|
64
|
+
return toBoolean(value) as EnvKindMap[TKind]
|
|
65
|
+
case "array":
|
|
66
|
+
return toArray(value) as EnvKindMap[TKind]
|
|
67
|
+
case "object":
|
|
68
|
+
return toObject(value) as EnvKindMap[TKind]
|
|
69
|
+
case "buffer":
|
|
70
|
+
return toBuffer(value) as EnvKindMap[TKind]
|
|
71
|
+
case "date":
|
|
72
|
+
return toDate(value) as EnvKindMap[TKind]
|
|
73
|
+
default: {
|
|
74
|
+
const exhaustiveCheck: never = kind
|
|
75
|
+
throw new Error(`Unsupported env kind: ${exhaustiveCheck}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function safeParse<TSchema extends AllowedEnvKeys>(allowedEnvKeys: TSchema, values: Record<string, string>) {
|
|
81
|
+
const data: Partial<LoadedEnv<TSchema>> = {}
|
|
82
|
+
const error: string[] = []
|
|
83
|
+
|
|
84
|
+
for (const key of Object.keys(allowedEnvKeys) as Array<keyof TSchema>) {
|
|
85
|
+
const value = values[key as string]
|
|
86
|
+
if (value === undefined) continue
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
data[key] = parseEnvValue(allowedEnvKeys[key], value)
|
|
90
|
+
} catch (e) {
|
|
91
|
+
if (e instanceof Error)
|
|
92
|
+
error.push(`${String(key)}: ${e.message}`)
|
|
93
|
+
else
|
|
94
|
+
error.push(`${String(key)}: Unknown error occurred`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
success: error.length === 0,
|
|
100
|
+
data: data as LoadedEnv<TSchema>,
|
|
101
|
+
error
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function formatError(message: string[]): string {
|
|
106
|
+
return message.join('\n')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function parseRawEnv(rawEnv: string): Record<string, string> {
|
|
110
|
+
const result: Record<string, string> = {}
|
|
111
|
+
const lines = rawEnv.split(/\r?\n/)
|
|
112
|
+
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
const normalized = line.trim()
|
|
115
|
+
if (!normalized || normalized.startsWith('#')) continue
|
|
116
|
+
|
|
117
|
+
const separatorIndex = normalized.indexOf('=')
|
|
118
|
+
if (separatorIndex <= 0) continue
|
|
119
|
+
|
|
120
|
+
const key = normalized.slice(0, separatorIndex).trim()
|
|
121
|
+
const value = normalized.slice(separatorIndex + 1).trim()
|
|
122
|
+
if (!key) continue
|
|
123
|
+
|
|
124
|
+
result[key] = value
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function readEnvFile(filePath: string): false | Record<string, string> {
|
|
131
|
+
if (!existsSync(filePath)) return false
|
|
132
|
+
return parseRawEnv(readFileSync(filePath, 'utf-8'))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function getUnknownEnvKeys(source: Record<string, string>, allowedEnvKeys: Set<string>): string[] {
|
|
136
|
+
return Object.keys(source).filter((key) => !allowedEnvKeys.has(key))
|
|
137
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { existsSync } from 'fs'
|
|
3
|
+
import { formatError, getUnknownEnvKeys, readEnvFile, safeParse } from './helper.js'
|
|
4
|
+
import { AllowedEnvKeys, LoadedEnv } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export type { AllowedEnvKeys, EnvKind, EnvKindMap, LoadedEnv } from "./types.js"
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
var liteEnv: unknown
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function loadEnv<TSchema extends AllowedEnvKeys = {}>(
|
|
13
|
+
allowedEnvKeys: TSchema = {} as TSchema,
|
|
14
|
+
bypassUnknownEnvKeys: boolean = true,
|
|
15
|
+
NODE_ENV?: string
|
|
16
|
+
): LoadedEnv<TSchema> {
|
|
17
|
+
if (globalThis.liteEnv)
|
|
18
|
+
return globalThis.liteEnv as LoadedEnv<TSchema>
|
|
19
|
+
|
|
20
|
+
if (NODE_ENV) {
|
|
21
|
+
process.env.NODE_ENV = NODE_ENV
|
|
22
|
+
} else {
|
|
23
|
+
if (!process.env.NODE_ENV) {
|
|
24
|
+
process.env.NODE_ENV = 'production'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
NODE_ENV = process.env.NODE_ENV
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const defaultEnvPath = path.join(process.cwd(), '.env')
|
|
31
|
+
const nodeEnvPath = path.join(process.cwd(), `.env.${NODE_ENV}`)
|
|
32
|
+
|
|
33
|
+
if (!existsSync(defaultEnvPath))
|
|
34
|
+
throw new Error('Default Environment file not found')
|
|
35
|
+
|
|
36
|
+
if (!existsSync(nodeEnvPath))
|
|
37
|
+
throw new Error(`Environment file not found: .env.${NODE_ENV}`)
|
|
38
|
+
|
|
39
|
+
const defaultEnvSource = readEnvFile(defaultEnvPath)
|
|
40
|
+
const nodeEnvSource = readEnvFile(nodeEnvPath)
|
|
41
|
+
|
|
42
|
+
const mergedEnv: Record<string, string> = {}
|
|
43
|
+
|
|
44
|
+
if (defaultEnvSource)
|
|
45
|
+
Object.assign(mergedEnv, defaultEnvSource)
|
|
46
|
+
if (nodeEnvSource)
|
|
47
|
+
Object.assign(mergedEnv, nodeEnvSource)
|
|
48
|
+
|
|
49
|
+
if (!bypassUnknownEnvKeys) {
|
|
50
|
+
const unknownKeys = [...new Set([...getUnknownEnvKeys(mergedEnv, new Set(Object.keys(allowedEnvKeys)))])]
|
|
51
|
+
|
|
52
|
+
if (unknownKeys.length > 0)
|
|
53
|
+
throw new Error(`Unknown environment variables in env files: ${unknownKeys.join(', ')}`)
|
|
54
|
+
} else {
|
|
55
|
+
process.env = {
|
|
56
|
+
...process.env,
|
|
57
|
+
...mergedEnv
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const parsedEnv = safeParse(allowedEnvKeys, mergedEnv)
|
|
62
|
+
|
|
63
|
+
if (!parsedEnv.success)
|
|
64
|
+
throw new Error(`Environment validation failed: ${formatError(parsedEnv.error)}`)
|
|
65
|
+
|
|
66
|
+
globalThis.liteEnv = parsedEnv.data
|
|
67
|
+
|
|
68
|
+
process.env = {
|
|
69
|
+
...process.env,
|
|
70
|
+
...parsedEnv.data
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log('Loaded env file', `${defaultEnvPath}, ${nodeEnvPath}`)
|
|
74
|
+
|
|
75
|
+
return parsedEnv.data as LoadedEnv<TSchema>
|
|
76
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ENV = {
|
|
2
|
+
NODE_ENV: string
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export type EnvKindMap = {
|
|
6
|
+
string: string
|
|
7
|
+
number: number
|
|
8
|
+
boolean: boolean
|
|
9
|
+
array: string[]
|
|
10
|
+
object: unknown
|
|
11
|
+
buffer: Buffer
|
|
12
|
+
date: Date
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type EnvKind = keyof EnvKindMap
|
|
16
|
+
|
|
17
|
+
export type AllowedEnvKeys = Record<string, EnvKind>
|
|
18
|
+
|
|
19
|
+
export type LoadedEnv<TSchema extends AllowedEnvKeys> = {
|
|
20
|
+
[K in keyof TSchema]: EnvKindMap[TSchema[K]]
|
|
21
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"ignoreDeprecations": "6.0",
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"tsBuildInfoFile": "./dist/.tsbuildinfo",
|
|
7
|
+
"types": [
|
|
8
|
+
"node"
|
|
9
|
+
],
|
|
10
|
+
"noImplicitAny": true,
|
|
11
|
+
"module": "NodeNext",
|
|
12
|
+
"moduleResolution": "NodeNext",
|
|
13
|
+
"paths": {
|
|
14
|
+
"@/*": [
|
|
15
|
+
"./src/*"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"include": [
|
|
20
|
+
"src/**/*"
|
|
21
|
+
],
|
|
22
|
+
"exclude": [
|
|
23
|
+
"node_modules",
|
|
24
|
+
"dist"
|
|
25
|
+
]
|
|
26
|
+
}
|