nestjs-env-getter 1.0.0-beta.2 → 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 CHANGED
@@ -7,6 +7,33 @@ 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
+
26
+ ## [1.0.0-beta.3] - 2025-11-26
27
+
28
+ ### Added
29
+
30
+ - Improved file watcher resilience: re-establishes the file watcher after each successful update to handle atomic file replacements (e.g., Vault Agent credential rotations).
31
+
32
+ ### Changed
33
+
34
+ - Increased default `debounceMs` from `200` to `350` ms to reduce noisy re-parses on rapid file updates.
35
+ - Documentation and unit tests updated to cover file replacement and re-establishment behavior.
36
+
10
37
  ## [1.0.0-beta.2] - 2025-10-17
11
38
 
12
39
  ### 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**: Automatically loads environment variables from a `.env` file using `dotenv`.
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
@@ -272,7 +285,7 @@ Load and validate configuration from JSON files with automatic file watching and
272
285
  { enabled: false },
273
286
  );
274
287
 
275
- // Custom debounce timing (default is 200ms)
288
+ // Custom debounce timing (default is 350ms)
276
289
  const config = this.envGetter.getRequiredConfigFromFile(
277
290
  "config.json",
278
291
  undefined,
@@ -381,14 +394,14 @@ When the file watcher detects changes, it updates the **same object instance** i
381
394
  **File Watcher Options:**
382
395
 
383
396
  - `enabled` (boolean, default: `true`): Enable or disable automatic file watching
384
- - `debounceMs` (number, default: `200`): Delay in milliseconds before re-reading the file after a change
397
+ - `debounceMs` (number, default: `350`): Delay in milliseconds before re-reading the file after a change
385
398
 
386
399
  **Features:**
387
400
 
388
401
  - ✅ Supports both absolute and relative paths (relative to `process.cwd()`)
389
402
  - ✅ Automatic JSON parsing and validation
390
403
  - ✅ **Hot-reload with reference preservation** - updates existing object instances in-place
391
- - ✅ File watching with automatic change detection
404
+ - ✅ File watching with automatic change detection and file replacement support (e.g., Vault agent flow)
392
405
  - ✅ Debouncing to prevent excessive re-reads
393
406
  - ✅ Class-based validation with constructor pattern
394
407
  - ✅ **Graceful error handling for optional configs** - missing files or JSON parsing errors return default values
@@ -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?: any[];
6
+ imports?: ModuleMetadata["imports"];
7
7
  providers?: Provider[];
8
8
  }
9
9
  export declare class AppConfigModule {
@@ -345,6 +345,7 @@ export declare class EnvGetterService implements OnModuleDestroy {
345
345
  * Sets up a file watcher for a configuration file.
346
346
  * - Watches for file changes and automatically re-reads and updates the cached config.
347
347
  * - Applies debouncing to avoid excessive re-reads.
348
+ * - Re-establishes the watcher after each successful update to handle file replacements (e.g., Vault agent flow).
348
349
  * - Emits 'updated' or 'error' events on re-parse.
349
350
  * @param filePath - The absolute path to the config file.
350
351
  * @param cls - (Optional) A class constructor to validate and instantiate the parsed config.
@@ -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 dotenv_1 = require("dotenv");
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
- .filter((key) => !this.isUnsafeKey(key))
39
- .forEach((key) => {
40
- copy[key] = source[key];
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, dotenv_1.config)({ quiet: true });
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.log("\x1b[31m%s\x1b[0m", message);
438
- process.exit(1);
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.
@@ -599,6 +599,7 @@ let EnvGetterService = class EnvGetterService {
599
599
  * Sets up a file watcher for a configuration file.
600
600
  * - Watches for file changes and automatically re-reads and updates the cached config.
601
601
  * - Applies debouncing to avoid excessive re-reads.
602
+ * - Re-establishes the watcher after each successful update to handle file replacements (e.g., Vault agent flow).
602
603
  * - Emits 'updated' or 'error' events on re-parse.
603
604
  * @param filePath - The absolute path to the config file.
604
605
  * @param cls - (Optional) A class constructor to validate and instantiate the parsed config.
@@ -610,20 +611,43 @@ let EnvGetterService = class EnvGetterService {
610
611
  var _a, _b, _c;
611
612
  const watcherOptions = {
612
613
  enabled: (_a = options === null || options === void 0 ? void 0 : options.enabled) !== null && _a !== void 0 ? _a : true,
613
- debounceMs: (_b = options === null || options === void 0 ? void 0 : options.debounceMs) !== null && _b !== void 0 ? _b : 200,
614
+ debounceMs: (_b = options === null || options === void 0 ? void 0 : options.debounceMs) !== null && _b !== void 0 ? _b : 350,
614
615
  breakOnError: (_c = options === null || options === void 0 ? void 0 : options.breakOnError) !== null && _c !== void 0 ? _c : true,
615
616
  };
616
- if (!watcherOptions.enabled || this.fileWatchers.has(filePath))
617
+ if (!watcherOptions.enabled)
617
618
  return;
619
+ // Close existing watcher if present (for re-establishment)
620
+ const existingWatcher = this.fileWatchers.get(filePath);
621
+ if (existingWatcher) {
622
+ existingWatcher.close();
623
+ this.fileWatchers.delete(filePath);
624
+ }
618
625
  let debounceTimer = null;
619
626
  const watcher = (0, fs_1.watch)(filePath, (eventType) => {
620
- if (eventType === "change") {
627
+ // Handle both "change" and "rename" events to catch file replacements
628
+ if (eventType === "change" || eventType === "rename") {
621
629
  if (debounceTimer)
622
630
  clearTimeout(debounceTimer);
623
631
  debounceTimer = setTimeout(() => {
632
+ // Verify file still exists before attempting to read
633
+ if (!(0, fs_1.existsSync)(filePath)) {
634
+ // File was deleted, emit error event
635
+ const errorEvent = {
636
+ filePath,
637
+ error: new Error(`Config file '${filePath}' was deleted`),
638
+ timestamp: Date.now(),
639
+ };
640
+ this.events.emit(`error:${filePath}`, errorEvent);
641
+ return;
642
+ }
624
643
  try {
625
644
  // Re-parse the config (isInitialLoad = false, with breakOnError setting)
626
645
  this.readAndParseConfigFile(filePath, cls, false, watcherOptions.breakOnError, isOptional);
646
+ // Close the old watcher
647
+ watcher.close();
648
+ this.fileWatchers.delete(filePath);
649
+ // Re-establish the watcher to handle file replacement scenarios (e.g., Vault agent)
650
+ this.setupFileWatcher(filePath, cls, options, isOptional);
627
651
  // Emit file-specific success event
628
652
  const event = {
629
653
  filePath,
@@ -9,7 +9,7 @@ export type FileWatcherOptions = {
9
9
  enabled?: boolean;
10
10
  /**
11
11
  * Debounce delay in milliseconds before re-reading the file after a change is detected.
12
- * @default 200
12
+ * @default 350
13
13
  */
14
14
  debounceMs?: number;
15
15
  /**
@@ -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,2 @@
1
+ export * from "./types";
2
+ export { parseEnvString, parseEnvFile, parseEnvFiles, loadEnvFile, loadEnvFiles } from "./env-parser.utils";
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1 +1,2 @@
1
1
  export { isTimePeriod, parseTimePeriod } from "./time-period.utils";
2
+ export * from "./env-parser";
@@ -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-beta.2",
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.26.0",
45
- "@nestjs/cli": "^11.0.10",
46
- "@nestjs/common": "^11.1.6",
47
- "@nestjs/core": "^11.1.6",
48
- "@nestjs/testing": "^11.1.6",
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": "^24.7.0",
51
- "eslint": "^9.26.0",
52
- "eslint-config-prettier": "^10.0.3",
53
- "eslint-plugin-jsdoc": "^61.0.0",
54
- "eslint-plugin-prettier": "^5.4.0",
55
- "globals": "^16.1.0",
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.5.3",
55
+ "prettier": "^3.8.0",
58
56
  "reflect-metadata": "^0.2.2",
59
57
  "rxjs": "^7.8.2",
60
- "ts-jest": "^29.3.2",
61
- "typescript": "^5.8.3",
62
- "typescript-eslint": "^8.32.0"
58
+ "ts-jest": "^29.4.6",
59
+ "typescript": "^5.9.3",
60
+ "typescript-eslint": "^8.53.0"
63
61
  }
64
62
  }