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 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
- # nestjs-env-getter
2
- The Nestjs Module for getting environment variables at app start
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();