nestjs-env-getter 1.0.0-beta.3 → 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/CHANGELOG.md +16 -0
- package/README.md +14 -1
- package/dist/app-config/app-config.module.d.ts +2 -2
- package/dist/env-getter/env-getter.service.js +9 -9
- package/dist/shared/utils/env-parser/env-parser.utils.d.ts +41 -0
- package/dist/shared/utils/env-parser/env-parser.utils.js +442 -0
- package/dist/shared/utils/env-parser/index.d.ts +2 -0
- package/dist/shared/utils/env-parser/index.js +24 -0
- package/dist/shared/utils/env-parser/types.d.ts +51 -0
- package/dist/shared/utils/env-parser/types.js +2 -0
- package/dist/shared/utils/index.d.ts +1 -0
- package/dist/shared/utils/index.js +15 -0
- package/package.json +17 -19
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ All notable changes to this project will be documented in this file.
|
|
|
7
7
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
8
8
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
9
9
|
|
|
10
|
+
## [1.0.0] - 2026-01-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Custom .env Parser**: Replaced `dotenv` dependency with a robust, custom-built parser.
|
|
15
|
+
- Full support for multiline strings (e.g., private keys).
|
|
16
|
+
- Variable interpolation/expansion (e.g., `APP_URL=${HOST}:${PORT}`).
|
|
17
|
+
- Detection of circular variable references.
|
|
18
|
+
- Detailed error reporting for malformed lines and unterminated quotes.
|
|
19
|
+
- Support for `export` prefix and miscellaneous quoting styles (single/double).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **Dependency Removal**: Removed `dotenv` to reduce external dependencies and control parsing logic directly.
|
|
24
|
+
- **Example Project**: Updated `nestjs-server` example to demonstrate new parser features and diverse variable types.
|
|
25
|
+
|
|
10
26
|
## [1.0.0-beta.3] - 2025-11-26
|
|
11
27
|
|
|
12
28
|
### Added
|
package/README.md
CHANGED
|
@@ -8,9 +8,22 @@
|
|
|
8
8
|
- **Built-in Validation**: Validate variables against allowed values or use custom validation functions.
|
|
9
9
|
- **Required vs. Optional**: Clearly distinguish between required variables that terminate the process if missing and optional ones with default values.
|
|
10
10
|
- **Error Handling**: Automatically terminates the process with a descriptive error message if a required variable is missing or invalid.
|
|
11
|
-
- **`.env` Support**:
|
|
11
|
+
- **`.env` Support**: Built-in parser with variable interpolation, multiline strings, and system env priority.
|
|
12
12
|
- **Configuration Scaffolding**: Includes a CLI helper to quickly set up a centralized configuration file.
|
|
13
13
|
|
|
14
|
+
## Environment File Parsing
|
|
15
|
+
|
|
16
|
+
The module includes a zero-dependency `.env` file parser. Key behaviors:
|
|
17
|
+
|
|
18
|
+
- **System environment priority**: Variables already set in `process.env` are NOT overwritten by `.env` file values. This ensures CI/CD and Docker environment injections take precedence over local files.
|
|
19
|
+
- **Variable interpolation**: Use `${VAR_NAME}` syntax to reference other variables.
|
|
20
|
+
- **Quoting rules**:
|
|
21
|
+
- **Unquoted**: Value ends at `#` (comment) or end of line.
|
|
22
|
+
- **Single quotes** (`'`): Raw literal, no escape processing.
|
|
23
|
+
- **Double quotes** (`"`): Supports `\n`, `\t`, `\\`, `\"` escapes and multiline values.
|
|
24
|
+
- **`export` prefix**: Automatically stripped for shell compatibility.
|
|
25
|
+
- **Empty values**: `KEY=` results in an empty string (not undefined).
|
|
26
|
+
|
|
14
27
|
## Installation
|
|
15
28
|
|
|
16
29
|
```bash
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { DynamicModule, Provider, Type, InjectionToken, OptionalFactoryDependency } from "@nestjs/common";
|
|
1
|
+
import { DynamicModule, Provider, Type, InjectionToken, OptionalFactoryDependency, ModuleMetadata } from "@nestjs/common";
|
|
2
2
|
export interface AppConfigModuleOptions {
|
|
3
3
|
useClass?: Type<unknown>;
|
|
4
4
|
useFactory?: (...args: unknown[]) => unknown;
|
|
5
5
|
inject?: (InjectionToken | OptionalFactoryDependency)[];
|
|
6
|
-
imports?:
|
|
6
|
+
imports?: ModuleMetadata["imports"];
|
|
7
7
|
providers?: Provider[];
|
|
8
8
|
}
|
|
9
9
|
export declare class AppConfigModule {
|
|
@@ -10,7 +10,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.EnvGetterService = void 0;
|
|
13
|
-
const
|
|
13
|
+
const utils_1 = require("../shared/utils");
|
|
14
14
|
const events_1 = require("events");
|
|
15
15
|
const fs_1 = require("fs");
|
|
16
16
|
const path_1 = require("path");
|
|
@@ -34,11 +34,11 @@ let EnvGetterService = class EnvGetterService {
|
|
|
34
34
|
*/
|
|
35
35
|
safeObjectCopy(source) {
|
|
36
36
|
const copy = Object.create(null);
|
|
37
|
-
Object.keys(source)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
37
|
+
for (const key of Object.keys(source)) {
|
|
38
|
+
if (!this.isUnsafeKey(key)) {
|
|
39
|
+
copy[key] = source[key];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
42
|
return copy;
|
|
43
43
|
}
|
|
44
44
|
constructor() {
|
|
@@ -49,7 +49,7 @@ let EnvGetterService = class EnvGetterService {
|
|
|
49
49
|
* Emits 'updated' and 'error' events globally for all config files.
|
|
50
50
|
*/
|
|
51
51
|
this.events = new events_1.EventEmitter();
|
|
52
|
-
(0,
|
|
52
|
+
(0, utils_1.loadEnvFile)(".env", { quiet: true });
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
55
|
* Lifecycle hook called when the module is being destroyed.
|
|
@@ -434,8 +434,8 @@ let EnvGetterService = class EnvGetterService {
|
|
|
434
434
|
*/
|
|
435
435
|
stopProcess(message) {
|
|
436
436
|
// eslint-disable-next-line no-console
|
|
437
|
-
console.
|
|
438
|
-
|
|
437
|
+
console.error(`\x1b[31m${message}\x1b[0m`);
|
|
438
|
+
throw new Error(message);
|
|
439
439
|
}
|
|
440
440
|
/**
|
|
441
441
|
* Checks if an environment variable exists.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type EnvParseOptions, type EnvParseResult } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Parses environment variables from a string.
|
|
4
|
+
* @param content - The .env file content as a string.
|
|
5
|
+
* @param options - Parsing options.
|
|
6
|
+
* @returns An {@link EnvParseResult} containing parsed variables, errors, and success flag.
|
|
7
|
+
* @throws {Error} If parsing fails and options.accumulate is false.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseEnvString(content: string, options?: EnvParseOptions): EnvParseResult;
|
|
10
|
+
/**
|
|
11
|
+
* Parses a single .env file.
|
|
12
|
+
* @param filePath - Path to the .env file.
|
|
13
|
+
* @param options - Parsing options.
|
|
14
|
+
* @returns An {@link EnvParseResult} with variables, errors, and success status.
|
|
15
|
+
* @throws {Error} If file is missing or reading fails and options.accumulate is false.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseEnvFile(filePath: string, options?: EnvParseOptions): EnvParseResult;
|
|
18
|
+
/**
|
|
19
|
+
* Parses multiple .env files with cascading priority (later files override earlier ones).
|
|
20
|
+
* @param filePaths - Array of .env file paths in order of priority, last wins.
|
|
21
|
+
* @param options - Parsing options.
|
|
22
|
+
* @returns Combined {@link EnvParseResult} from all files.
|
|
23
|
+
* @throws {Error} If any file parsing fails and options.accumulate is false.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseEnvFiles(filePaths: string[], options?: EnvParseOptions): EnvParseResult;
|
|
26
|
+
/**
|
|
27
|
+
* Loads environment variables from a .env file into `process.env`.
|
|
28
|
+
* By default, existing system environment variables take precedence (they are NOT overwritten).
|
|
29
|
+
* Use `{ override: true }` to force overwrite.
|
|
30
|
+
* @param filePath - Path to the .env file (defaults to `.env`).
|
|
31
|
+
* @param options - Parsing and loading options.
|
|
32
|
+
* @throws {Error} If parsing fails and options.accumulate is false.
|
|
33
|
+
*/
|
|
34
|
+
export declare function loadEnvFile(filePath?: string, options?: EnvParseOptions): void;
|
|
35
|
+
/**
|
|
36
|
+
* Loads multiple .env files into `process.env` with cascading priority.
|
|
37
|
+
* @param filePaths - Array of .env file paths.
|
|
38
|
+
* @param options - Parsing and loading options.
|
|
39
|
+
* @throws {Error} If any file parsing fails and options.accumulate is false.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadEnvFiles(filePaths: string[], options?: EnvParseOptions): void;
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseEnvString = parseEnvString;
|
|
4
|
+
exports.parseEnvFile = parseEnvFile;
|
|
5
|
+
exports.parseEnvFiles = parseEnvFiles;
|
|
6
|
+
exports.loadEnvFile = loadEnvFile;
|
|
7
|
+
exports.loadEnvFiles = loadEnvFiles;
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
/**
|
|
10
|
+
* Regular expression to validate environment variable keys.
|
|
11
|
+
* Keys must start with a letter or underscore, followed by alphanumeric characters or underscores.
|
|
12
|
+
*/
|
|
13
|
+
const KEY_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
14
|
+
// Variable interpolation pattern: ${VAR_NAME}
|
|
15
|
+
const INTERPOLATION_REGEX = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
16
|
+
/**
|
|
17
|
+
* Creates an {@link EnvParseError} object.
|
|
18
|
+
* @param lineNumber - The line number where the error occurred.
|
|
19
|
+
* @param rawContent - The raw line content.
|
|
20
|
+
* @param message - Human‑readable error message.
|
|
21
|
+
* @param type - The type of parsing error.
|
|
22
|
+
* @returns An {@link EnvParseError} instance.
|
|
23
|
+
*/
|
|
24
|
+
function createError(lineNumber, rawContent, message, type) {
|
|
25
|
+
return { lineNumber, rawContent, message, type };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Processes escape sequences in double-quoted strings.
|
|
29
|
+
* @param value - The raw string containing escape sequences.
|
|
30
|
+
* @returns The string with escape sequences interpreted.
|
|
31
|
+
*/
|
|
32
|
+
function processEscapes(value) {
|
|
33
|
+
let result = "";
|
|
34
|
+
let i = 0;
|
|
35
|
+
while (i < value.length) {
|
|
36
|
+
if (value[i] === "\\" && i + 1 < value.length) {
|
|
37
|
+
const next = value[i + 1];
|
|
38
|
+
switch (next) {
|
|
39
|
+
case "n":
|
|
40
|
+
result += "\n";
|
|
41
|
+
i += 2;
|
|
42
|
+
break;
|
|
43
|
+
case "t":
|
|
44
|
+
result += "\t";
|
|
45
|
+
i += 2;
|
|
46
|
+
break;
|
|
47
|
+
case "r":
|
|
48
|
+
result += "\r";
|
|
49
|
+
i += 2;
|
|
50
|
+
break;
|
|
51
|
+
case "\\":
|
|
52
|
+
result += "\\";
|
|
53
|
+
i += 2;
|
|
54
|
+
break;
|
|
55
|
+
case '"':
|
|
56
|
+
result += '"';
|
|
57
|
+
i += 2;
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
// Unknown escape, keep as-is
|
|
61
|
+
result += value[i];
|
|
62
|
+
i += 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
result += value[i];
|
|
67
|
+
i += 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Detects circular references during variable interpolation.
|
|
74
|
+
* @param varName - The variable name being resolved.
|
|
75
|
+
* @param resolutionStack - Set of variable names already in the resolution chain.
|
|
76
|
+
* @returns An error message if a circular reference is found, otherwise null.
|
|
77
|
+
*/
|
|
78
|
+
function detectCircularReference(varName, resolutionStack) {
|
|
79
|
+
if (resolutionStack.has(varName)) {
|
|
80
|
+
return `Circular reference detected: ${[...resolutionStack, varName].join(" -> ")}`;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Expands variable interpolation in a value.
|
|
86
|
+
* Uses a two‑pass strategy: first checks local variables, then system environment variables.
|
|
87
|
+
* @param value - The string value to expand.
|
|
88
|
+
* @param variables - Map of local variables defined in the .env file.
|
|
89
|
+
* @param systemEnv - System environment variables.
|
|
90
|
+
* @param resolutionStack - Set tracking variables already visited to detect cycles.
|
|
91
|
+
* @param quiet - If true, suppresses warnings for undefined variables.
|
|
92
|
+
* @returns An object containing the expanded result and optionally a circular reference error.
|
|
93
|
+
*/
|
|
94
|
+
function expandVariables(value, variables, systemEnv, resolutionStack = new Set(), quiet = false) {
|
|
95
|
+
let result = value;
|
|
96
|
+
let match;
|
|
97
|
+
const regex = new RegExp(INTERPOLATION_REGEX.source, "g");
|
|
98
|
+
while ((match = regex.exec(value)) !== null) {
|
|
99
|
+
const fullMatch = match[0];
|
|
100
|
+
const varName = match[1];
|
|
101
|
+
if (!varName)
|
|
102
|
+
continue;
|
|
103
|
+
// Check for circular reference
|
|
104
|
+
const circularError = detectCircularReference(varName, resolutionStack);
|
|
105
|
+
if (circularError) {
|
|
106
|
+
return { result: value, circularError };
|
|
107
|
+
}
|
|
108
|
+
// Lookup order: local variables first, then system env
|
|
109
|
+
let replacement;
|
|
110
|
+
if (varName in variables) {
|
|
111
|
+
// Recursively expand the referenced variable
|
|
112
|
+
const newStack = new Set(resolutionStack);
|
|
113
|
+
newStack.add(varName);
|
|
114
|
+
const expanded = expandVariables(variables[varName], variables, systemEnv, newStack, quiet);
|
|
115
|
+
if (expanded.circularError) {
|
|
116
|
+
return expanded;
|
|
117
|
+
}
|
|
118
|
+
replacement = expanded.result;
|
|
119
|
+
}
|
|
120
|
+
else if (varName in systemEnv && systemEnv[varName] !== undefined) {
|
|
121
|
+
replacement = systemEnv[varName];
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// Variable not found - leave as empty string
|
|
125
|
+
if (!quiet) {
|
|
126
|
+
// eslint-disable-next-line no-console
|
|
127
|
+
console.warn(`[env-parser] Warning: Variable '${varName}' is not defined`);
|
|
128
|
+
}
|
|
129
|
+
replacement = "";
|
|
130
|
+
}
|
|
131
|
+
result = result.replace(fullMatch, replacement);
|
|
132
|
+
}
|
|
133
|
+
return { result };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Parses a single line from a .env file and extracts a key‑value pair.
|
|
137
|
+
* Returns `null` for comments or empty lines.
|
|
138
|
+
* @param line - The raw line text.
|
|
139
|
+
* @param lineNumber - The line number in the file (1‑based).
|
|
140
|
+
* @param buffer - Current multiline buffer state, if any.
|
|
141
|
+
* @returns An object describing the parsed result, any errors, and buffer state.
|
|
142
|
+
*/
|
|
143
|
+
function parseLine(line, lineNumber, buffer) {
|
|
144
|
+
var _a, _b, _c, _d;
|
|
145
|
+
// If we're in multiline mode, continue accumulating
|
|
146
|
+
if (buffer) {
|
|
147
|
+
if (buffer.quoteChar === "'") {
|
|
148
|
+
const closingIndex = line.indexOf("'");
|
|
149
|
+
if (closingIndex !== -1) {
|
|
150
|
+
// Found closing single quote
|
|
151
|
+
buffer.value += "\n" + line.substring(0, closingIndex);
|
|
152
|
+
return { key: buffer.key, value: buffer.value, isComplete: true, buffer: null };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else if (buffer.quoteChar === '"') {
|
|
156
|
+
// Double-quoted multiline - scan for non-escaped quote
|
|
157
|
+
let i = 0;
|
|
158
|
+
let collected = "";
|
|
159
|
+
while (i < line.length) {
|
|
160
|
+
if (line[i] === "\\" && i + 1 < line.length) {
|
|
161
|
+
// Preserve escape sequence
|
|
162
|
+
collected += ((_a = line[i]) !== null && _a !== void 0 ? _a : "") + ((_b = line[i + 1]) !== null && _b !== void 0 ? _b : "");
|
|
163
|
+
i += 2;
|
|
164
|
+
}
|
|
165
|
+
else if (line[i] === '"') {
|
|
166
|
+
// Found closing double quote
|
|
167
|
+
buffer.value += "\n" + collected;
|
|
168
|
+
return { key: buffer.key, value: processEscapes(buffer.value), isComplete: true, buffer: null };
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
collected += line[i];
|
|
172
|
+
i++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Not found on this line, append entire line (literal including escapes)
|
|
176
|
+
buffer.value += "\n" + line;
|
|
177
|
+
return { isComplete: false, buffer };
|
|
178
|
+
}
|
|
179
|
+
// Continue accumulating for other cases (should be handled above but for safety)
|
|
180
|
+
buffer.value += "\n" + line;
|
|
181
|
+
return { isComplete: false, buffer };
|
|
182
|
+
}
|
|
183
|
+
// Trim leading whitespace (but preserve for error reporting)
|
|
184
|
+
const trimmedLine = line.trimStart();
|
|
185
|
+
// Skip empty lines and full-line comments
|
|
186
|
+
if (!trimmedLine || trimmedLine.startsWith("#")) {
|
|
187
|
+
return { isComplete: true, buffer: null };
|
|
188
|
+
}
|
|
189
|
+
// Strip 'export ' prefix if present
|
|
190
|
+
let workingLine = trimmedLine;
|
|
191
|
+
if (workingLine.startsWith("export ")) {
|
|
192
|
+
workingLine = workingLine.substring(7).trimStart();
|
|
193
|
+
}
|
|
194
|
+
// Find the equals sign
|
|
195
|
+
const equalsIndex = workingLine.indexOf("=");
|
|
196
|
+
if (equalsIndex === -1) {
|
|
197
|
+
return {
|
|
198
|
+
isComplete: true,
|
|
199
|
+
buffer: null,
|
|
200
|
+
error: createError(lineNumber, line, `Line ${lineNumber}: Missing '=' in assignment`, "MALFORMED_LINE"),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// Extract key and validate
|
|
204
|
+
const key = workingLine.substring(0, equalsIndex).trim();
|
|
205
|
+
if (!KEY_REGEX.test(key)) {
|
|
206
|
+
return {
|
|
207
|
+
isComplete: true,
|
|
208
|
+
buffer: null,
|
|
209
|
+
error: createError(lineNumber, line, `Line ${lineNumber}: Invalid key '${key}'. Keys must start with a letter or underscore and contain only alphanumeric characters and underscores.`, "INVALID_KEY"),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// Extract raw value (everything after =)
|
|
213
|
+
let rawValue = workingLine.substring(equalsIndex + 1);
|
|
214
|
+
// Determine value type based on first character
|
|
215
|
+
const firstChar = rawValue.charAt(0);
|
|
216
|
+
if (firstChar === "'") {
|
|
217
|
+
// Single-quoted value - raw literal
|
|
218
|
+
const closingIndex = rawValue.indexOf("'", 1);
|
|
219
|
+
if (closingIndex === -1) {
|
|
220
|
+
// Check if it's multiline or unterminated
|
|
221
|
+
return {
|
|
222
|
+
isComplete: false,
|
|
223
|
+
buffer: { key, value: rawValue.substring(1), quoteChar: "'", startLine: lineNumber },
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const value = rawValue.substring(1, closingIndex);
|
|
227
|
+
return { key, value, isComplete: true, buffer: null };
|
|
228
|
+
}
|
|
229
|
+
else if (firstChar === '"') {
|
|
230
|
+
// Double-quoted value - supports escapes
|
|
231
|
+
// Find the closing quote, accounting for escaped quotes
|
|
232
|
+
let i = 1;
|
|
233
|
+
let value = "";
|
|
234
|
+
let foundClosing = false;
|
|
235
|
+
while (i < rawValue.length) {
|
|
236
|
+
if (rawValue[i] === "\\" && i + 1 < rawValue.length) {
|
|
237
|
+
// Escape sequence - keep it for later processing
|
|
238
|
+
value += ((_c = rawValue[i]) !== null && _c !== void 0 ? _c : "") + ((_d = rawValue[i + 1]) !== null && _d !== void 0 ? _d : "");
|
|
239
|
+
i += 2;
|
|
240
|
+
}
|
|
241
|
+
else if (rawValue[i] === '"') {
|
|
242
|
+
foundClosing = true;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
value += rawValue[i];
|
|
247
|
+
i += 1;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (!foundClosing) {
|
|
251
|
+
// Multiline double-quoted string
|
|
252
|
+
return {
|
|
253
|
+
isComplete: false,
|
|
254
|
+
buffer: { key, value, quoteChar: '"', startLine: lineNumber },
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
// Process escape sequences
|
|
258
|
+
const processedValue = processEscapes(value);
|
|
259
|
+
return { key, value: processedValue, isComplete: true, buffer: null };
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
// Unquoted value - trim whitespace and stop at comment
|
|
263
|
+
rawValue = rawValue.trim();
|
|
264
|
+
// Find inline comment (# not inside quotes)
|
|
265
|
+
const commentIndex = rawValue.indexOf("#");
|
|
266
|
+
if (commentIndex !== -1) {
|
|
267
|
+
rawValue = rawValue.substring(0, commentIndex).trimEnd();
|
|
268
|
+
}
|
|
269
|
+
return { key, value: rawValue, isComplete: true, buffer: null };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Parses environment variables from a string.
|
|
274
|
+
* @param content - The .env file content as a string.
|
|
275
|
+
* @param options - Parsing options.
|
|
276
|
+
* @returns An {@link EnvParseResult} containing parsed variables, errors, and success flag.
|
|
277
|
+
* @throws {Error} If parsing fails and options.accumulate is false.
|
|
278
|
+
*/
|
|
279
|
+
function parseEnvString(content, options = {}) {
|
|
280
|
+
var _a;
|
|
281
|
+
const { accumulate = false, systemEnv = process.env, quiet = false } = options;
|
|
282
|
+
// Normalize line endings
|
|
283
|
+
const normalizedContent = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
284
|
+
const lines = normalizedContent.split("\n");
|
|
285
|
+
const variables = {};
|
|
286
|
+
const errors = [];
|
|
287
|
+
let buffer = null;
|
|
288
|
+
for (let i = 0; i < lines.length; i++) {
|
|
289
|
+
const lineNumber = i + 1;
|
|
290
|
+
const line = lines[i];
|
|
291
|
+
const result = parseLine(line !== null && line !== void 0 ? line : "", lineNumber, buffer);
|
|
292
|
+
if (result.error) {
|
|
293
|
+
if (accumulate) {
|
|
294
|
+
errors.push(result.error);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
throw new Error(result.error.message);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (result.isComplete && result.key !== undefined) {
|
|
301
|
+
variables[result.key] = (_a = result.value) !== null && _a !== void 0 ? _a : "";
|
|
302
|
+
}
|
|
303
|
+
buffer = result.buffer;
|
|
304
|
+
}
|
|
305
|
+
// Check for unterminated multiline string
|
|
306
|
+
if (buffer) {
|
|
307
|
+
const error = createError(buffer.startLine, `Started at line ${buffer.startLine}`, `Unterminated ${buffer.quoteChar === '"' ? "double" : "single"}-quoted string starting at line ${buffer.startLine}`, "UNTERMINATED_QUOTE");
|
|
308
|
+
if (accumulate) {
|
|
309
|
+
errors.push(error);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
throw new Error(error.message);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Second pass: variable interpolation
|
|
316
|
+
const expandedVariables = {};
|
|
317
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
318
|
+
const { result, circularError } = expandVariables(value, variables, systemEnv, new Set([key]), quiet);
|
|
319
|
+
if (circularError) {
|
|
320
|
+
const error = createError(0, key, circularError, "CIRCULAR_REFERENCE");
|
|
321
|
+
if (accumulate) {
|
|
322
|
+
errors.push(error);
|
|
323
|
+
expandedVariables[key] = value; // Keep original value on error
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
throw new Error(error.message);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
expandedVariables[key] = result;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
variables: expandedVariables,
|
|
335
|
+
errors,
|
|
336
|
+
success: errors.length === 0,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Parses a single .env file.
|
|
341
|
+
* @param filePath - Path to the .env file.
|
|
342
|
+
* @param options - Parsing options.
|
|
343
|
+
* @returns An {@link EnvParseResult} with variables, errors, and success status.
|
|
344
|
+
* @throws {Error} If file is missing or reading fails and options.accumulate is false.
|
|
345
|
+
*/
|
|
346
|
+
function parseEnvFile(filePath, options = {}) {
|
|
347
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
348
|
+
const error = createError(0, filePath, `File not found: ${filePath}`, "FILE_READ_ERROR");
|
|
349
|
+
if (options.accumulate) {
|
|
350
|
+
return { variables: {}, errors: [error], success: false };
|
|
351
|
+
}
|
|
352
|
+
throw new Error(error.message);
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
const content = (0, fs_1.readFileSync)(filePath, "utf-8");
|
|
356
|
+
return parseEnvString(content, options);
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
// parseEnvString throws for all parse errors (unterminated quotes, invalid keys, etc.)
|
|
360
|
+
if (err instanceof Error) {
|
|
361
|
+
// Check if this is a file-system error (not a parsing error)
|
|
362
|
+
const isFileSystemError = err.message.includes("ENOENT") ||
|
|
363
|
+
err.message.includes("EACCES") ||
|
|
364
|
+
err.message.includes("EPERM") ||
|
|
365
|
+
err.message.startsWith("Error reading file");
|
|
366
|
+
if (!isFileSystemError) {
|
|
367
|
+
throw err; // Re-throw parsing errors
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const error = createError(0, filePath, `Error reading file ${filePath}: ${err instanceof Error ? err.message : String(err)}`, "FILE_READ_ERROR");
|
|
371
|
+
if (options.accumulate) {
|
|
372
|
+
return { variables: {}, errors: [error], success: false };
|
|
373
|
+
}
|
|
374
|
+
throw new Error(error.message);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Parses multiple .env files with cascading priority (later files override earlier ones).
|
|
379
|
+
* @param filePaths - Array of .env file paths in order of priority, last wins.
|
|
380
|
+
* @param options - Parsing options.
|
|
381
|
+
* @returns Combined {@link EnvParseResult} from all files.
|
|
382
|
+
* @throws {Error} If any file parsing fails and options.accumulate is false.
|
|
383
|
+
*/
|
|
384
|
+
function parseEnvFiles(filePaths, options = {}) {
|
|
385
|
+
const allVariables = {};
|
|
386
|
+
const allErrors = [];
|
|
387
|
+
for (const filePath of filePaths) {
|
|
388
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
389
|
+
continue; // Skip non-existent files in multi-file mode
|
|
390
|
+
}
|
|
391
|
+
const result = parseEnvFile(filePath, { ...options, accumulate: true });
|
|
392
|
+
// Merge variables (later files override)
|
|
393
|
+
Object.assign(allVariables, result.variables);
|
|
394
|
+
// Collect errors
|
|
395
|
+
allErrors.push(...result.errors);
|
|
396
|
+
}
|
|
397
|
+
if (!options.accumulate && allErrors.length > 0) {
|
|
398
|
+
throw new Error(allErrors[0].message);
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
variables: allVariables,
|
|
402
|
+
errors: allErrors,
|
|
403
|
+
success: allErrors.length === 0,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Loads environment variables from a .env file into `process.env`.
|
|
408
|
+
* By default, existing system environment variables take precedence (they are NOT overwritten).
|
|
409
|
+
* Use `{ override: true }` to force overwrite.
|
|
410
|
+
* @param filePath - Path to the .env file (defaults to `.env`).
|
|
411
|
+
* @param options - Parsing and loading options.
|
|
412
|
+
* @throws {Error} If parsing fails and options.accumulate is false.
|
|
413
|
+
*/
|
|
414
|
+
function loadEnvFile(filePath = ".env", options = {}) {
|
|
415
|
+
const { override = false } = options;
|
|
416
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
417
|
+
// Silently skip if file doesn't exist (matches dotenv behavior)
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const result = parseEnvFile(filePath, options);
|
|
421
|
+
for (const [key, value] of Object.entries(result.variables)) {
|
|
422
|
+
// Respect immutable system env rule: don't overwrite existing vars unless override=true
|
|
423
|
+
if (override || !(key in process.env)) {
|
|
424
|
+
process.env[key] = value;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Loads multiple .env files into `process.env` with cascading priority.
|
|
430
|
+
* @param filePaths - Array of .env file paths.
|
|
431
|
+
* @param options - Parsing and loading options.
|
|
432
|
+
* @throws {Error} If any file parsing fails and options.accumulate is false.
|
|
433
|
+
*/
|
|
434
|
+
function loadEnvFiles(filePaths, options = {}) {
|
|
435
|
+
const { override = false } = options;
|
|
436
|
+
const result = parseEnvFiles(filePaths, options);
|
|
437
|
+
for (const [key, value] of Object.entries(result.variables)) {
|
|
438
|
+
if (override || !(key in process.env)) {
|
|
439
|
+
process.env[key] = value;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.loadEnvFiles = exports.loadEnvFile = exports.parseEnvFiles = exports.parseEnvFile = exports.parseEnvString = void 0;
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
19
|
+
var env_parser_utils_1 = require("./env-parser.utils");
|
|
20
|
+
Object.defineProperty(exports, "parseEnvString", { enumerable: true, get: function () { return env_parser_utils_1.parseEnvString; } });
|
|
21
|
+
Object.defineProperty(exports, "parseEnvFile", { enumerable: true, get: function () { return env_parser_utils_1.parseEnvFile; } });
|
|
22
|
+
Object.defineProperty(exports, "parseEnvFiles", { enumerable: true, get: function () { return env_parser_utils_1.parseEnvFiles; } });
|
|
23
|
+
Object.defineProperty(exports, "loadEnvFile", { enumerable: true, get: function () { return env_parser_utils_1.loadEnvFile; } });
|
|
24
|
+
Object.defineProperty(exports, "loadEnvFiles", { enumerable: true, get: function () { return env_parser_utils_1.loadEnvFiles; } });
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for parsing environment files.
|
|
3
|
+
*/
|
|
4
|
+
export interface EnvParseOptions {
|
|
5
|
+
/**
|
|
6
|
+
* If true, .env file values will override existing system environment variables.
|
|
7
|
+
* By default (false), system env takes precedence over .env file values.
|
|
8
|
+
* @default false
|
|
9
|
+
*/
|
|
10
|
+
override?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* If true, collect all errors instead of throwing on first error.
|
|
13
|
+
* Useful for pre-flight validation of the entire file.
|
|
14
|
+
* @default false
|
|
15
|
+
*/
|
|
16
|
+
accumulate?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* System environment variables to use for variable interpolation lookup.
|
|
19
|
+
* Defaults to process.env.
|
|
20
|
+
*/
|
|
21
|
+
systemEnv?: Record<string, string | undefined>;
|
|
22
|
+
/**
|
|
23
|
+
* If true, suppress console warnings for undefined variable references.
|
|
24
|
+
* @default false
|
|
25
|
+
*/
|
|
26
|
+
quiet?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Represents a parsing error with context information.
|
|
30
|
+
*/
|
|
31
|
+
export interface EnvParseError {
|
|
32
|
+
/** Line number where the error occurred (1-indexed). */
|
|
33
|
+
lineNumber: number;
|
|
34
|
+
/** Raw content of the problematic line. */
|
|
35
|
+
rawContent: string;
|
|
36
|
+
/** Human-readable error message. */
|
|
37
|
+
message: string;
|
|
38
|
+
/** Error type for programmatic handling. */
|
|
39
|
+
type: "UNTERMINATED_QUOTE" | "INVALID_KEY" | "MALFORMED_LINE" | "CIRCULAR_REFERENCE" | "FILE_READ_ERROR";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Result of parsing environment content.
|
|
43
|
+
*/
|
|
44
|
+
export interface EnvParseResult {
|
|
45
|
+
/** Parsed environment variables as key-value pairs. */
|
|
46
|
+
variables: Record<string, string>;
|
|
47
|
+
/** List of errors encountered during parsing (populated when accumulate=true). */
|
|
48
|
+
errors: EnvParseError[];
|
|
49
|
+
/** Whether parsing completed successfully (no errors). */
|
|
50
|
+
success: boolean;
|
|
51
|
+
}
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
2
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
17
|
exports.parseTimePeriod = exports.isTimePeriod = void 0;
|
|
4
18
|
var time_period_utils_1 = require("./time-period.utils");
|
|
5
19
|
Object.defineProperty(exports, "isTimePeriod", { enumerable: true, get: function () { return time_period_utils_1.isTimePeriod; } });
|
|
6
20
|
Object.defineProperty(exports, "parseTimePeriod", { enumerable: true, get: function () { return time_period_utils_1.parseTimePeriod; } });
|
|
21
|
+
__exportStar(require("./env-parser"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nestjs-env-getter",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Environment variables getter for Nestjs applications",
|
|
5
5
|
"author": "Ivan Baha",
|
|
6
6
|
"private": false,
|
|
@@ -37,28 +37,26 @@
|
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@nestjs/common": ">=9"
|
|
39
39
|
},
|
|
40
|
-
"dependencies": {
|
|
41
|
-
"dotenv": "^17.2.3"
|
|
42
|
-
},
|
|
40
|
+
"dependencies": {},
|
|
43
41
|
"devDependencies": {
|
|
44
|
-
"@eslint/js": "^9.39.
|
|
45
|
-
"@nestjs/cli": "^11.0.
|
|
46
|
-
"@nestjs/common": "^11.1.
|
|
47
|
-
"@nestjs/core": "^11.1.
|
|
48
|
-
"@nestjs/testing": "^11.1.
|
|
42
|
+
"@eslint/js": "^9.39.2",
|
|
43
|
+
"@nestjs/cli": "^11.0.16",
|
|
44
|
+
"@nestjs/common": "^11.1.12",
|
|
45
|
+
"@nestjs/core": "^11.1.12",
|
|
46
|
+
"@nestjs/testing": "^11.1.12",
|
|
49
47
|
"@types/jest": "^30.0.0",
|
|
50
|
-
"@types/node": "^
|
|
51
|
-
"eslint": "^9.39.
|
|
52
|
-
"eslint-config-prettier": "^10.
|
|
53
|
-
"eslint-plugin-jsdoc": "^
|
|
54
|
-
"eslint-plugin-prettier": "^5.
|
|
55
|
-
"globals": "^
|
|
48
|
+
"@types/node": "^25.0.9",
|
|
49
|
+
"eslint": "^9.39.2",
|
|
50
|
+
"eslint-config-prettier": "^10.1.8",
|
|
51
|
+
"eslint-plugin-jsdoc": "^62.0.1",
|
|
52
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
53
|
+
"globals": "^17.0.0",
|
|
56
54
|
"jest": "^30.2.0",
|
|
57
|
-
"prettier": "^3.
|
|
55
|
+
"prettier": "^3.8.0",
|
|
58
56
|
"reflect-metadata": "^0.2.2",
|
|
59
57
|
"rxjs": "^7.8.2",
|
|
60
|
-
"ts-jest": "^29.4.
|
|
61
|
-
"typescript": "^5.
|
|
62
|
-
"typescript-eslint": "^8.
|
|
58
|
+
"ts-jest": "^29.4.6",
|
|
59
|
+
"typescript": "^5.9.3",
|
|
60
|
+
"typescript-eslint": "^8.53.0"
|
|
63
61
|
}
|
|
64
62
|
}
|