nestjs-env-getter 0.0.0-beta2 → 1.0.0-beta.1
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 +46 -0
- package/README.md +401 -2
- package/bin/nestjs-env-getter.mjs +253 -0
- package/dist/app-config/app-config.module.d.ts +12 -0
- package/dist/app-config/app-config.module.js +51 -0
- package/dist/env-getter/env-getter.module.js +2 -4
- package/dist/env-getter/env-getter.service.d.ts +196 -58
- package/dist/env-getter/env-getter.service.js +471 -44
- package/dist/env-getter/types/array-validator.type.d.ts +1 -1
- package/dist/env-getter/types/config-events.type.d.ts +62 -0
- package/dist/env-getter/types/config-events.type.js +2 -0
- package/dist/env-getter/types/file-watcher-options.type.d.ts +22 -0
- package/dist/env-getter/types/file-watcher-options.type.js +2 -0
- package/dist/env-getter/types/index.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/package.json +19 -12
- package/src/app-config/app.config.ts.example +37 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
<!-- markdownlint-disable MD024 -->
|
|
4
|
+
|
|
5
|
+
All notable changes to this project will be documented in this file.
|
|
6
|
+
|
|
7
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
8
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
9
|
+
|
|
10
|
+
## [1.0.0-beta.1] - 2025-10-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Configuration from Files**:
|
|
15
|
+
- Added `getRequiredConfigFromFile` and `getOptionalConfigFromFile` to read, parse, and validate JSON configuration files.
|
|
16
|
+
- Implemented automatic file watching with hot-reload, which updates configuration in-place without requiring an application restart.
|
|
17
|
+
- Configuration objects are enhanced with `on`, `once`, and `off` methods for subscribing to file change events.
|
|
18
|
+
- **Graceful Error Handling**:
|
|
19
|
+
- `getOptionalConfigFromFile` now gracefully handles missing files or JSON parsing errors by returning a default value or `undefined`, preventing process termination.
|
|
20
|
+
- Process now only terminates on critical errors, such as validation failures in class-based configs.
|
|
21
|
+
- **Project Scaffolding and Examples**:
|
|
22
|
+
- Added an example NestJS application demonstrating usage with a MongoDB connection.
|
|
23
|
+
- Included executable scripts for bootstrapping the library.
|
|
24
|
+
- Integrated `AppConfigModule` to streamline configuration setup.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Updated all dependencies to their latest versions.
|
|
29
|
+
|
|
30
|
+
## [0.1.0] - 2025-05-08
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Bumped version.
|
|
35
|
+
- Bumped development dependencies and changed the registry.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Updated the GitHub Action for publishing.
|
|
40
|
+
- Fixed issues in the CI/CD workflows.
|
|
41
|
+
|
|
42
|
+
## [0.0.0-beta1] - 2025-02-04
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- Initial release with total refactoring.
|
package/README.md
CHANGED
|
@@ -1,2 +1,401 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# NestJS ENV Getter
|
|
2
|
+
|
|
3
|
+
`nestjs-env-getter` is a powerful and lightweight NestJS module designed to streamline the process of retrieving and validating environment variables. It ensures your application starts with a correct and type-safe configuration by providing a robust API for handling various data types and validation scenarios.
|
|
4
|
+
|
|
5
|
+
## Key Features
|
|
6
|
+
|
|
7
|
+
- **Type-Safe Retrieval**: Get environment variables as strings, numbers, booleans, URLs, objects, arrays, or time periods.
|
|
8
|
+
- **Built-in Validation**: Validate variables against allowed values or use custom validation functions.
|
|
9
|
+
- **Required vs. Optional**: Clearly distinguish between required variables that terminate the process if missing and optional ones with default values.
|
|
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`.
|
|
12
|
+
- **Configuration Scaffolding**: Includes a CLI helper to quickly set up a centralized configuration file.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install nestjs-env-getter
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
The recommended way to use `nestjs-env-getter` is by creating a centralized configuration service.
|
|
23
|
+
|
|
24
|
+
### 1. Create a Configuration Service
|
|
25
|
+
|
|
26
|
+
Create a file `src/app.config.ts` to define and validate all your environment variables.
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// src/app.config.ts
|
|
30
|
+
import { Injectable } from "@nestjs/common";
|
|
31
|
+
import { EnvGetterService } from "nestjs-env-getter";
|
|
32
|
+
|
|
33
|
+
@Injectable()
|
|
34
|
+
export class AppConfig {
|
|
35
|
+
readonly port: number;
|
|
36
|
+
readonly mongoConnectionString: string;
|
|
37
|
+
readonly nodeEnv: string;
|
|
38
|
+
|
|
39
|
+
constructor(private readonly envGetter: EnvGetterService) {
|
|
40
|
+
this.port = this.envGetter.getRequiredNumericEnv("PORT");
|
|
41
|
+
this.mongoConnectionString = this.envGetter.getRequiredEnv(
|
|
42
|
+
"MONGO_CONNECTION_STRING",
|
|
43
|
+
);
|
|
44
|
+
this.nodeEnv = this.envGetter.getRequiredEnv("NODE_ENV", [
|
|
45
|
+
"development",
|
|
46
|
+
"production",
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. Register the Configuration
|
|
53
|
+
|
|
54
|
+
In your `AppModule`, import `AppConfigModule` and provide your custom `AppConfig` class. This makes `AppConfig` a singleton provider available for dependency injection across your application.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// src/app.module.ts
|
|
58
|
+
import { Module } from "@nestjs/common";
|
|
59
|
+
import { AppConfigModule } from "nestjs-env-getter";
|
|
60
|
+
import { AppConfig } from "./app.config";
|
|
61
|
+
|
|
62
|
+
@Module({
|
|
63
|
+
imports: [
|
|
64
|
+
// Register the custom AppConfig class
|
|
65
|
+
AppConfigModule.forRoot({ useClass: AppConfig }),
|
|
66
|
+
],
|
|
67
|
+
})
|
|
68
|
+
export class AppModule {}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 3. Use the Configuration
|
|
72
|
+
|
|
73
|
+
Now you can inject `AppConfig` anywhere in your application.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { Injectable } from "@nestjs/common";
|
|
77
|
+
import { AppConfig } from "./app.config";
|
|
78
|
+
|
|
79
|
+
@Injectable()
|
|
80
|
+
export class DatabaseService {
|
|
81
|
+
private readonly connectionString: string;
|
|
82
|
+
|
|
83
|
+
constructor(private readonly config: AppConfig) {
|
|
84
|
+
this.connectionString = config.mongoConnectionString;
|
|
85
|
+
// Use the connection string...
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This approach centralizes your environment variable management, making your application cleaner and easier to maintain.
|
|
91
|
+
|
|
92
|
+
## CLI Helper
|
|
93
|
+
|
|
94
|
+
You can scaffold the configuration file and module setup automatically with the built-in CLI command. Run this in the root of your NestJS project:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npx nestjs-env-getter init
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This command (shorthand `npx nestjs-env-getter i`) will:
|
|
101
|
+
|
|
102
|
+
1. Create `src/app.config.ts` from a template.
|
|
103
|
+
2. Import `AppConfigModule.forRoot({ useClass: AppConfig })` into your `src/app.module.ts`.
|
|
104
|
+
|
|
105
|
+
## API Reference (`EnvGetterService`)
|
|
106
|
+
|
|
107
|
+
Here are detailed examples of the methods available in `EnvGetterService`.
|
|
108
|
+
|
|
109
|
+
### String
|
|
110
|
+
|
|
111
|
+
- **`getRequiredEnv(name: string, allowed?: string[]): string`**
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Throws an error if DB_HOST is not set
|
|
115
|
+
const dbHost = this.envGetter.getRequiredEnv("DB_HOST");
|
|
116
|
+
|
|
117
|
+
// Throws an error if NODE_ENV is not 'development' or 'production'
|
|
118
|
+
const nodeEnv = this.envGetter.getRequiredEnv("NODE_ENV", [
|
|
119
|
+
"development",
|
|
120
|
+
"production",
|
|
121
|
+
]);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- **`getOptionalEnv(name: string, default?: string, allowed?: string[]): string | undefined`**
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// Returns 'info' if LOG_LEVEL is not set
|
|
128
|
+
const logLevel = this.envGetter.getOptionalEnv("LOG_LEVEL", "info");
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Number
|
|
132
|
+
|
|
133
|
+
- **`getRequiredNumericEnv(name: string): number`**
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// Throws an error if PORT is not a valid number
|
|
137
|
+
const port = this.envGetter.getRequiredNumericEnv("PORT");
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- **`getOptionalNumericEnv(name: string, default?: number): number | undefined`**
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// Returns 3000 if TIMEOUT is not set or not a valid number
|
|
144
|
+
const timeout = this.envGetter.getOptionalNumericEnv("TIMEOUT", 3000);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Boolean
|
|
148
|
+
|
|
149
|
+
- **`getRequiredBooleanEnv(name: string): boolean`**
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// Throws an error if DEBUG_MODE is not 'true' or 'false'
|
|
153
|
+
const isDebug = this.envGetter.getRequiredBooleanEnv("DEBUG_MODE");
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- **`getOptionalBooleanEnv(name: string, default?: boolean): boolean | undefined`**
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// Returns false if ENABLE_SSL is not set
|
|
160
|
+
const useSsl = this.envGetter.getOptionalBooleanEnv("ENABLE_SSL", false);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### URL
|
|
164
|
+
|
|
165
|
+
- **`getRequiredURL(name: string): URL`**
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// Throws an error if API_URL is not a valid URL
|
|
169
|
+
const apiUrl = this.envGetter.getRequiredURL("API_URL");
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- **`getOptionalURL(name: string, default?: URL): URL | undefined`**
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
const defaultUrl = new URL("https://fallback.example.com");
|
|
176
|
+
// Returns defaultUrl if CDN_URL is not set or not a valid URL
|
|
177
|
+
const cdnUrl = this.envGetter.getOptionalURL("CDN_URL", defaultUrl);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Time Period
|
|
181
|
+
|
|
182
|
+
Parses a string like `'10s'`, `'5m'`, `'2h'`, `'1d'` into a numeric value.
|
|
183
|
+
|
|
184
|
+
- **`getRequiredTimePeriod(name: string, resultIn: 'ms' | 's' | 'm' | 'h' | 'd' = 'ms'): number`**
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// Parses '30s' into 30000
|
|
188
|
+
const cacheTtl = this.envGetter.getRequiredTimePeriod("CACHE_TTL");
|
|
189
|
+
|
|
190
|
+
// Parses '2h' into 2
|
|
191
|
+
const sessionHours = this.envGetter.getRequiredTimePeriod(
|
|
192
|
+
"SESSION_DURATION",
|
|
193
|
+
"h",
|
|
194
|
+
);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
- **`getOptionalTimePeriod(name: string, default: string, resultIn: ...): number`**
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// Returns 60 (seconds) if JWT_EXPIRES_IN is not set
|
|
201
|
+
const expiresIn = this.envGetter.getOptionalTimePeriod(
|
|
202
|
+
"JWT_EXPIRES_IN",
|
|
203
|
+
"1m",
|
|
204
|
+
"s",
|
|
205
|
+
);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Object (from JSON string)
|
|
209
|
+
|
|
210
|
+
- **`getRequiredObject<T>(name: string, cls?: ClassConstructor<T>): T`**
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// ENV: '{"user": "admin", "port": 5432}'
|
|
214
|
+
const dbConfig = this.envGetter.getRequiredObject<{
|
|
215
|
+
user: string;
|
|
216
|
+
port: number;
|
|
217
|
+
}>("DB_CONFIG");
|
|
218
|
+
console.log(dbConfig.port); // 5432
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Array (from JSON string)
|
|
222
|
+
|
|
223
|
+
- **`getRequiredArray<T>(name: string, validate?: (el: T) => boolean | string): T[]`**
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// ENV: '["192.168.1.1", "10.0.0.1"]'
|
|
227
|
+
const allowedIPs = this.envGetter.getRequiredArray<string>("ALLOWED_IPS");
|
|
228
|
+
|
|
229
|
+
// With validation
|
|
230
|
+
const servicePorts = this.envGetter.getRequiredArray<number>(
|
|
231
|
+
"SERVICE_PORTS",
|
|
232
|
+
(port) => {
|
|
233
|
+
if (typeof port !== "number" || port < 1024) {
|
|
234
|
+
return `Port ${port} is invalid. Must be a number >= 1024.`;
|
|
235
|
+
}
|
|
236
|
+
return true;
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Configuration from JSON Files
|
|
242
|
+
|
|
243
|
+
Load and validate configuration from JSON files with automatic file watching and hot-reload support.
|
|
244
|
+
|
|
245
|
+
- **`getRequiredConfigFromFile<T>(filePath: string, cls?: ClassConstructor<T>, watcherOptions?: FileWatcherOptions): T`**
|
|
246
|
+
|
|
247
|
+
Reads a required JSON configuration file. Automatically watches for file changes and updates the cached config.
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// With class validation
|
|
251
|
+
class MongoCredentials {
|
|
252
|
+
connectionString: string;
|
|
253
|
+
|
|
254
|
+
constructor(data: any) {
|
|
255
|
+
if (!data.connectionString) {
|
|
256
|
+
throw new Error("connectionString is required");
|
|
257
|
+
}
|
|
258
|
+
this.connectionString = data.connectionString;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const mongoCreds = this.envGetter.getRequiredConfigFromFile(
|
|
263
|
+
"configs/mongo-creds.json",
|
|
264
|
+
MongoCredentials,
|
|
265
|
+
);
|
|
266
|
+
console.log(mongoCreds.connectionString);
|
|
267
|
+
|
|
268
|
+
// Disable file watching
|
|
269
|
+
const staticConfig = this.envGetter.getRequiredConfigFromFile(
|
|
270
|
+
"config.json",
|
|
271
|
+
undefined,
|
|
272
|
+
{ enabled: false },
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// Custom debounce timing (default is 200ms)
|
|
276
|
+
const config = this.envGetter.getRequiredConfigFromFile(
|
|
277
|
+
"config.json",
|
|
278
|
+
undefined,
|
|
279
|
+
{ debounceMs: 1000 },
|
|
280
|
+
);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
- **`getOptionalConfigFromFile<T>(filePath: string, defaultValue?: T, cls?: ClassConstructor<T>, watcherOptions?: FileWatcherOptions): T | undefined`**
|
|
284
|
+
|
|
285
|
+
Reads an optional JSON configuration file. Returns the default value if the file doesn't exist or cannot be parsed as JSON. Only terminates the process if validation fails (when using a class constructor).
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// With class validation
|
|
289
|
+
class TestConfig {
|
|
290
|
+
testConfigStringFromFile: string;
|
|
291
|
+
|
|
292
|
+
constructor(data: any) {
|
|
293
|
+
if (!data.testConfigStringFromFile) {
|
|
294
|
+
throw new Error("testConfigStringFromFile is required");
|
|
295
|
+
}
|
|
296
|
+
this.testConfigStringFromFile = data.testConfigStringFromFile;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const testConfig = this.envGetter.getOptionalConfigFromFile(
|
|
301
|
+
"configs/test-configs.json",
|
|
302
|
+
TestConfig,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// With default value - gracefully handles missing files and JSON parsing errors
|
|
306
|
+
const config = this.envGetter.getOptionalConfigFromFile(
|
|
307
|
+
"optional-config.json",
|
|
308
|
+
{ fallbackValue: "default" },
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// Note: Optional config files are graceful - missing files or JSON parsing errors
|
|
312
|
+
// return the default value (or undefined). Only validation errors crash the process.
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
#### ⚠️ Important: Error Handling Behavior
|
|
316
|
+
|
|
317
|
+
**Required vs Optional Config Files:**
|
|
318
|
+
|
|
319
|
+
- **`getRequiredConfigFromFile`**: Crashes the process if the file is missing, has invalid JSON, or fails validation.
|
|
320
|
+
- **`getOptionalConfigFromFile`**: Only crashes the process on validation errors (when using class constructors). Missing files or JSON parsing errors gracefully return the default value or `undefined`.
|
|
321
|
+
|
|
322
|
+
This design ensures that optional configurations truly are optional - your application won't crash due to missing or malformed optional config files, but validation constraints are still enforced when files are present and parsable.
|
|
323
|
+
|
|
324
|
+
#### ⚠️ Important: Using Config Values by Reference
|
|
325
|
+
|
|
326
|
+
To ensure your application automatically receives updated config values when files change, you **must store config objects by reference**, not by extracting primitive values:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// ✅ CORRECT - Store the object reference
|
|
330
|
+
@Injectable()
|
|
331
|
+
export class AppConfig {
|
|
332
|
+
mongoConfigs: MongoCredentials; // Store the entire object
|
|
333
|
+
testConfig?: TestConfig; // Store the entire object
|
|
334
|
+
|
|
335
|
+
constructor(protected readonly envGetter: EnvGetterService) {
|
|
336
|
+
// Store references to config objects
|
|
337
|
+
this.mongoConfigs = this.envGetter.getRequiredConfigFromFile(
|
|
338
|
+
"configs/mongo-creds.json",
|
|
339
|
+
MongoCredentials,
|
|
340
|
+
);
|
|
341
|
+
this.testConfig = this.envGetter.getOptionalConfigFromFile(
|
|
342
|
+
"configs/test-configs.json",
|
|
343
|
+
TestConfig,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Access via the reference - values will update automatically
|
|
349
|
+
@Module({
|
|
350
|
+
imports: [
|
|
351
|
+
MongooseModule.forRootAsync({
|
|
352
|
+
useFactory: (config: AppConfig) => ({
|
|
353
|
+
uri: config.mongoConfigs.connectionString, // Access via reference
|
|
354
|
+
}),
|
|
355
|
+
inject: [AppConfig],
|
|
356
|
+
}),
|
|
357
|
+
],
|
|
358
|
+
})
|
|
359
|
+
export class AppModule {}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
// ❌ WRONG - Extracting primitive values breaks hot-reload
|
|
364
|
+
@Injectable()
|
|
365
|
+
export class AppConfig {
|
|
366
|
+
mongoConnectionString: string; // Primitive value - won't update!
|
|
367
|
+
|
|
368
|
+
constructor(protected readonly envGetter: EnvGetterService) {
|
|
369
|
+
const mongoCreds = this.envGetter.getRequiredConfigFromFile(
|
|
370
|
+
"configs/mongo-creds.json",
|
|
371
|
+
MongoCredentials,
|
|
372
|
+
);
|
|
373
|
+
// This extracts the VALUE at startup time - it won't update!
|
|
374
|
+
this.mongoConnectionString = mongoCreds.connectionString;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
When the file watcher detects changes, it updates the **same object instance** in memory. If you store the object reference, your application automatically sees the updated values. If you extract primitive values (strings, numbers, booleans), they become snapshots that never update.
|
|
380
|
+
|
|
381
|
+
**File Watcher Options:**
|
|
382
|
+
|
|
383
|
+
- `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
|
|
385
|
+
|
|
386
|
+
**Features:**
|
|
387
|
+
|
|
388
|
+
- ✅ Supports both absolute and relative paths (relative to `process.cwd()`)
|
|
389
|
+
- ✅ Automatic JSON parsing and validation
|
|
390
|
+
- ✅ **Hot-reload with reference preservation** - updates existing object instances in-place
|
|
391
|
+
- ✅ File watching with automatic change detection
|
|
392
|
+
- ✅ Debouncing to prevent excessive re-reads
|
|
393
|
+
- ✅ Class-based validation with constructor pattern
|
|
394
|
+
- ✅ **Graceful error handling for optional configs** - missing files or JSON parsing errors return default values
|
|
395
|
+
- ✅ Process termination only on validation errors (required configs crash on any error)
|
|
396
|
+
- ✅ **Event methods attached to default values** - consistent API even when files don't exist
|
|
397
|
+
- ✅ No application restart needed - changes apply immediately when using object references
|
|
398
|
+
|
|
399
|
+
## License
|
|
400
|
+
|
|
401
|
+
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/* eslint-disable no-console */
|
|
4
|
+
|
|
5
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const COMMANDS = {
|
|
10
|
+
HELP: new Set(["help", "--help", "-h"]),
|
|
11
|
+
INIT: new Set(["init", "i"]),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const MODULE_PREFIX = "[nestjs-env-getter]";
|
|
15
|
+
const PACKAGE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Prints an informational message prefixed with the package tag.
|
|
19
|
+
* @param {string} message - The message to print.
|
|
20
|
+
*/
|
|
21
|
+
function logInfo(message) {
|
|
22
|
+
console.log(`${MODULE_PREFIX} ${message}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Prints an error message prefixed with the package tag.
|
|
27
|
+
* @param {string} message - The message to print.
|
|
28
|
+
*/
|
|
29
|
+
function logError(message) {
|
|
30
|
+
console.error(`${MODULE_PREFIX} ${message}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Displays the CLI usage help in the console.
|
|
35
|
+
*/
|
|
36
|
+
function printHelp() {
|
|
37
|
+
logInfo("Usage: nestjs-env-getter <command>");
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log("Commands:");
|
|
40
|
+
console.log(" help Show this help.");
|
|
41
|
+
console.log(" init | i Scaffold AppConfig integration in the current project.");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolves the project paths that will be used by the CLI.
|
|
46
|
+
* @param {string} cwd - Current working directory.
|
|
47
|
+
* @returns {{ cwd: string, srcDir: string, appModulePath: string, appConfigPath: string, templateConfigPath: string }} Resolved paths.
|
|
48
|
+
*/
|
|
49
|
+
function resolveProjectPaths(cwd) {
|
|
50
|
+
const srcDir = join(cwd, "src");
|
|
51
|
+
return {
|
|
52
|
+
cwd,
|
|
53
|
+
srcDir,
|
|
54
|
+
appModulePath: join(srcDir, "app.module.ts"),
|
|
55
|
+
appConfigPath: join(srcDir, "app.config.ts"),
|
|
56
|
+
templateConfigPath: join(PACKAGE_DIR, "..", "src", "app-config", "app.config.ts.example"),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Ensures that the NestJS project has the expected structure.
|
|
62
|
+
* @param {ReturnType<typeof resolveProjectPaths>} paths - Resolved project paths.
|
|
63
|
+
*/
|
|
64
|
+
function ensureSrcAndAppModule(paths) {
|
|
65
|
+
if (!existsSync(paths.srcDir)) {
|
|
66
|
+
logError('Unable to find "src" directory in the current working directory.');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!existsSync(paths.appModulePath)) {
|
|
71
|
+
logError('Unable to find "src/app.module.ts". Are you in a NestJS project root?');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Copies the template config file if it does not exist.
|
|
78
|
+
* @param {ReturnType<typeof resolveProjectPaths>} paths - Resolved project paths.
|
|
79
|
+
* @returns {boolean} True when a new config file is created.
|
|
80
|
+
*/
|
|
81
|
+
function ensureAppConfigFile(paths) {
|
|
82
|
+
if (existsSync(paths.appConfigPath)) {
|
|
83
|
+
logInfo("Skipped creating src/app.config.ts because it already exists.");
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!existsSync(paths.templateConfigPath)) {
|
|
88
|
+
logError("Template config file is missing inside nestjs-env-getter package.");
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
copyFileSync(paths.templateConfigPath, paths.appConfigPath);
|
|
93
|
+
logInfo("Created src/app.config.ts based on the package template.");
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Ensures the app.module.ts file wires up the AppConfigModule.
|
|
99
|
+
* @param {ReturnType<typeof resolveProjectPaths>} paths - Resolved project paths.
|
|
100
|
+
* @returns {boolean} True when the module file is updated.
|
|
101
|
+
*/
|
|
102
|
+
function updateAppModule(paths) {
|
|
103
|
+
const content = readFileSync(paths.appModulePath, "utf8");
|
|
104
|
+
|
|
105
|
+
if (/AppConfigModule\.forRoot/.test(content) || /AppConfigModule\.forRootAsync/.test(content)) {
|
|
106
|
+
logInfo("Detected existing AppConfigModule setup in app.module.ts. No changes applied.");
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let updated = content;
|
|
111
|
+
|
|
112
|
+
updated = ensureAppConfigModuleImport(updated);
|
|
113
|
+
updated = ensureAppConfigImport(updated);
|
|
114
|
+
updated = insertConfigModuleCall(updated);
|
|
115
|
+
|
|
116
|
+
writeFileSync(paths.appModulePath, updated, "utf8");
|
|
117
|
+
logInfo("Updated src/app.module.ts with AppConfigModule.forRoot({ useClass: AppConfig }).");
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Ensures the AppConfigModule import exists.
|
|
123
|
+
* @param {string} source - File contents.
|
|
124
|
+
* @returns {string} Updated source.
|
|
125
|
+
*/
|
|
126
|
+
function ensureAppConfigModuleImport(source) {
|
|
127
|
+
const importRegex = /import\s+{([^}]+)}\s+from\s+["']nestjs-env-getter["'];?/;
|
|
128
|
+
const match = source.match(importRegex);
|
|
129
|
+
|
|
130
|
+
if (match) {
|
|
131
|
+
const imports = match[1]
|
|
132
|
+
.split(",")
|
|
133
|
+
.map((item) => item.trim())
|
|
134
|
+
.filter(Boolean);
|
|
135
|
+
|
|
136
|
+
if (!imports.includes("AppConfigModule")) {
|
|
137
|
+
imports.push("AppConfigModule");
|
|
138
|
+
const uniqueImports = Array.from(new Set(imports));
|
|
139
|
+
const replacement = `import { ${uniqueImports.join(", ")} } from 'nestjs-env-getter';`;
|
|
140
|
+
return source.replace(importRegex, replacement);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return source;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const lastImportIndex = findLastImportIndex(source);
|
|
147
|
+
const importLine = "import { AppConfigModule } from 'nestjs-env-getter';\n";
|
|
148
|
+
if (lastImportIndex === -1) {
|
|
149
|
+
return `${importLine}${source}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return `${source.slice(0, lastImportIndex)}${importLine}${source.slice(lastImportIndex)}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Ensures the AppConfig import exists.
|
|
157
|
+
* @param {string} source - File contents.
|
|
158
|
+
* @returns {string} Updated source.
|
|
159
|
+
*/
|
|
160
|
+
function ensureAppConfigImport(source) {
|
|
161
|
+
const appConfigImportRegex = /import\s+{[^}]*AppConfig[^}]*}\s+from\s+["'][^"']*app\.config["'];?/;
|
|
162
|
+
if (appConfigImportRegex.test(source)) {
|
|
163
|
+
return source;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const lastImportIndex = findLastImportIndex(source);
|
|
167
|
+
const importLine = "import { AppConfig } from './app.config';\n";
|
|
168
|
+
|
|
169
|
+
if (lastImportIndex === -1) {
|
|
170
|
+
return `${importLine}${source}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return `${source.slice(0, lastImportIndex)}${importLine}${source.slice(lastImportIndex)}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Finds the index immediately after the last import statement.
|
|
178
|
+
* @param {string} source - File contents.
|
|
179
|
+
* @returns {number} Index of the last import, or -1 when not found.
|
|
180
|
+
*/
|
|
181
|
+
function findLastImportIndex(source) {
|
|
182
|
+
const importMatches = [...source.matchAll(/import[^;]+;\s*/g)];
|
|
183
|
+
if (importMatches.length === 0) {
|
|
184
|
+
return -1;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const lastMatch = importMatches[importMatches.length - 1];
|
|
188
|
+
return lastMatch.index + lastMatch[0].length;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Injects the AppConfigModule.forRoot call at the top of the module imports array.
|
|
193
|
+
* @param {string} source - File contents.
|
|
194
|
+
* @returns {string} Updated source.
|
|
195
|
+
*/
|
|
196
|
+
function insertConfigModuleCall(source) {
|
|
197
|
+
const importsArrayRegex = /(imports\s*:\s*\[)([\s\S]*?)(\n\s*\])/m;
|
|
198
|
+
const match = source.match(importsArrayRegex);
|
|
199
|
+
|
|
200
|
+
if (!match) {
|
|
201
|
+
logError("Could not locate the imports array in app.module.ts to add AppConfigModule.");
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const [fullMatch, start, middle, end] = match;
|
|
206
|
+
const lineIndentMatch = start.match(/^(\s*)imports/m);
|
|
207
|
+
const lineIndent = lineIndentMatch ? lineIndentMatch[1] : "";
|
|
208
|
+
const entryIndent = `${lineIndent} `;
|
|
209
|
+
const insertion = `\n${entryIndent}AppConfigModule.forRoot({ useClass: AppConfig }),`;
|
|
210
|
+
|
|
211
|
+
const newMiddle = `${insertion}${middle}`;
|
|
212
|
+
const replacement = `${start}${newMiddle}${end}`;
|
|
213
|
+
|
|
214
|
+
return source.replace(fullMatch, replacement);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Executes the init command workflow.
|
|
219
|
+
*/
|
|
220
|
+
function runInitCommand() {
|
|
221
|
+
const paths = resolveProjectPaths(process.cwd());
|
|
222
|
+
|
|
223
|
+
ensureSrcAndAppModule(paths);
|
|
224
|
+
const createdConfig = ensureAppConfigFile(paths);
|
|
225
|
+
const updatedModule = updateAppModule(paths);
|
|
226
|
+
|
|
227
|
+
if (!createdConfig && !updatedModule) {
|
|
228
|
+
logInfo("No changes were necessary. Project already configured.");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* CLI entrypoint.
|
|
234
|
+
*/
|
|
235
|
+
function run() {
|
|
236
|
+
const [, , rawCommand] = process.argv;
|
|
237
|
+
|
|
238
|
+
if (!rawCommand || COMMANDS.HELP.has(rawCommand)) {
|
|
239
|
+
printHelp();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (COMMANDS.INIT.has(rawCommand)) {
|
|
244
|
+
runInitCommand();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
logError(`Unknown command: ${rawCommand}`);
|
|
249
|
+
printHelp();
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
run();
|