@theunwalked/cardigantime 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -1
- package/dist/cardigantime.cjs +250 -29
- package/dist/cardigantime.cjs.map +1 -1
- package/dist/cardigantime.d.ts +17 -0
- package/dist/cardigantime.js +17 -0
- package/dist/cardigantime.js.map +1 -1
- package/dist/constants.js +2 -1
- package/dist/constants.js.map +1 -1
- package/dist/read.js +74 -3
- package/dist/read.js.map +1 -1
- package/dist/types.d.ts +42 -0
- package/dist/types.js.map +1 -1
- package/dist/util/hierarchical.d.ts +25 -11
- package/dist/util/hierarchical.js +157 -25
- package/dist/util/hierarchical.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -233,6 +233,60 @@ debug: true
|
|
|
233
233
|
./myapp --debug
|
|
234
234
|
```
|
|
235
235
|
|
|
236
|
+
### Advanced Usage Examples
|
|
237
|
+
|
|
238
|
+
#### Basic Configuration with Path Resolution
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { create } from '@theunwalked/cardigantime';
|
|
242
|
+
import { z } from 'zod';
|
|
243
|
+
|
|
244
|
+
const MyConfigSchema = z.object({
|
|
245
|
+
apiKey: z.string().min(1),
|
|
246
|
+
timeout: z.number().default(5000),
|
|
247
|
+
debug: z.boolean().default(false),
|
|
248
|
+
contextDirectories: z.array(z.string()).optional(),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const cardigantime = create({
|
|
252
|
+
defaults: {
|
|
253
|
+
configDirectory: './config',
|
|
254
|
+
configFile: 'myapp.yaml',
|
|
255
|
+
// Resolve relative paths in contextDirectories relative to config file location
|
|
256
|
+
pathResolution: {
|
|
257
|
+
pathFields: ['contextDirectories'],
|
|
258
|
+
resolvePathArray: ['contextDirectories']
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
configShape: MyConfigSchema.shape,
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
#### Hierarchical Configuration with Custom Array Overlap
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
const cardigantime = create({
|
|
269
|
+
defaults: {
|
|
270
|
+
configDirectory: '.myapp',
|
|
271
|
+
configFile: 'config.yaml',
|
|
272
|
+
fieldOverlaps: {
|
|
273
|
+
'features': 'append', // Accumulate features from all levels
|
|
274
|
+
'excludePatterns': 'prepend', // Higher precedence patterns come first
|
|
275
|
+
'api.endpoints': 'append', // Nested field configuration
|
|
276
|
+
'security.allowedOrigins': 'append' // Security settings accumulate
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
configShape: MyConfigSchema.shape,
|
|
280
|
+
features: ['config', 'hierarchical'], // Enable hierarchical discovery
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
This configuration enables powerful composition scenarios where:
|
|
285
|
+
- **Features** from all configuration levels are combined (e.g., base features + project features + local features)
|
|
286
|
+
- **Exclude patterns** are layered with local patterns taking precedence
|
|
287
|
+
- **API endpoints** can be extended at each level
|
|
288
|
+
- **Security settings** accumulate for maximum flexibility
|
|
289
|
+
|
|
236
290
|
## Core Concepts
|
|
237
291
|
|
|
238
292
|
### 1. Configuration Sources & Precedence
|
|
@@ -297,6 +351,9 @@ database:
|
|
|
297
351
|
ssl: false
|
|
298
352
|
logging:
|
|
299
353
|
level: info
|
|
354
|
+
features:
|
|
355
|
+
- auth
|
|
356
|
+
- basic-logging
|
|
300
357
|
|
|
301
358
|
# /home/user/projects/myproject/.kodrdriv/config.yaml (Level 1)
|
|
302
359
|
database:
|
|
@@ -304,14 +361,19 @@ database:
|
|
|
304
361
|
ssl: true
|
|
305
362
|
api:
|
|
306
363
|
timeout: 5000
|
|
364
|
+
features:
|
|
365
|
+
- advanced-logging
|
|
366
|
+
- metrics
|
|
307
367
|
|
|
308
368
|
# /home/user/projects/myproject/submodule/.kodrdriv/config.yaml (Level 0)
|
|
309
369
|
database:
|
|
310
370
|
host: dev.example.com
|
|
311
371
|
logging:
|
|
312
372
|
level: debug
|
|
373
|
+
features:
|
|
374
|
+
- debug-mode
|
|
313
375
|
|
|
314
|
-
# Final merged configuration:
|
|
376
|
+
# Final merged configuration (with default array behavior):
|
|
315
377
|
database:
|
|
316
378
|
host: dev.example.com # From Level 0 (highest precedence)
|
|
317
379
|
port: 5433 # From Level 1
|
|
@@ -320,6 +382,79 @@ api:
|
|
|
320
382
|
timeout: 5000 # From Level 1
|
|
321
383
|
logging:
|
|
322
384
|
level: debug # From Level 0 (highest precedence)
|
|
385
|
+
features:
|
|
386
|
+
- debug-mode # From Level 0 (arrays override by default)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
#### Configurable Array Overlap Behavior
|
|
390
|
+
|
|
391
|
+
By default, arrays in hierarchical configurations follow the **override** behavior - arrays from higher precedence levels completely replace arrays from lower precedence levels. However, you can configure custom overlap behavior for array fields:
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
const cardigantime = create({
|
|
395
|
+
defaults: {
|
|
396
|
+
configDirectory: '.kodrdriv',
|
|
397
|
+
configFile: 'config.yaml',
|
|
398
|
+
fieldOverlaps: {
|
|
399
|
+
'features': 'append', // Combine features by appending
|
|
400
|
+
'excludePatterns': 'prepend', // Combine exclude patterns by prepending
|
|
401
|
+
'middlewares': 'override' // Override middlewares (default behavior)
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
configShape: MyConfigSchema.shape,
|
|
405
|
+
features: ['config', 'hierarchical'],
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**Available Overlap Modes:**
|
|
410
|
+
|
|
411
|
+
- **`override`** (default): Higher precedence arrays completely replace lower precedence arrays
|
|
412
|
+
- **`append`**: Higher precedence array elements are appended to lower precedence arrays
|
|
413
|
+
- **`prepend`**: Higher precedence array elements are prepended to lower precedence arrays
|
|
414
|
+
|
|
415
|
+
**Example with Custom Array Overlap:**
|
|
416
|
+
|
|
417
|
+
```yaml
|
|
418
|
+
# /home/user/projects/.kodrdriv/config.yaml (Level 2)
|
|
419
|
+
features: ['auth', 'basic-logging']
|
|
420
|
+
excludePatterns: ['*.tmp', '*.cache']
|
|
421
|
+
|
|
422
|
+
# /home/user/projects/myproject/.kodrdriv/config.yaml (Level 1)
|
|
423
|
+
features: ['advanced-logging', 'metrics']
|
|
424
|
+
excludePatterns: ['*.log']
|
|
425
|
+
|
|
426
|
+
# /home/user/projects/myproject/submodule/.kodrdriv/config.yaml (Level 0)
|
|
427
|
+
features: ['debug-mode']
|
|
428
|
+
excludePatterns: ['*.debug']
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
With the configuration above (`features: 'append'`, `excludePatterns: 'prepend'`):
|
|
432
|
+
|
|
433
|
+
```yaml
|
|
434
|
+
# Final merged configuration:
|
|
435
|
+
features:
|
|
436
|
+
- auth # From Level 2
|
|
437
|
+
- basic-logging # From Level 2
|
|
438
|
+
- advanced-logging # From Level 1
|
|
439
|
+
- metrics # From Level 1
|
|
440
|
+
- debug-mode # From Level 0 (appended)
|
|
441
|
+
excludePatterns:
|
|
442
|
+
- "*.debug" # From Level 0 (prepended first)
|
|
443
|
+
- "*.log" # From Level 1 (prepended second)
|
|
444
|
+
- "*.tmp" # From Level 2
|
|
445
|
+
- "*.cache" # From Level 2
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**Nested Field Paths:**
|
|
449
|
+
|
|
450
|
+
You can configure overlap behavior for nested array fields using dot notation:
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
fieldOverlaps: {
|
|
454
|
+
'api.endpoints': 'append',
|
|
455
|
+
'database.migrations': 'prepend',
|
|
456
|
+
'config.features.experimental': 'override'
|
|
457
|
+
}
|
|
323
458
|
```
|
|
324
459
|
|
|
325
460
|
#### Enabling Hierarchical Discovery
|
package/dist/cardigantime.cjs
CHANGED
|
@@ -187,7 +187,8 @@ class ArgumentError extends Error {
|
|
|
187
187
|
*/ const DEFAULT_OPTIONS = {
|
|
188
188
|
configFile: DEFAULT_CONFIG_FILE,
|
|
189
189
|
isRequired: false,
|
|
190
|
-
encoding: DEFAULT_ENCODING
|
|
190
|
+
encoding: DEFAULT_ENCODING,
|
|
191
|
+
pathResolution: undefined
|
|
191
192
|
};
|
|
192
193
|
/**
|
|
193
194
|
* Default features enabled when creating a Cardigantime instance.
|
|
@@ -415,6 +416,62 @@ const create$1 = (params)=>{
|
|
|
415
416
|
};
|
|
416
417
|
};
|
|
417
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Resolves relative paths in configuration values relative to the configuration file's directory.
|
|
421
|
+
*/ function resolveConfigPaths$1(config, configDir, pathFields = [], resolvePathArray = []) {
|
|
422
|
+
if (!config || typeof config !== 'object' || pathFields.length === 0) {
|
|
423
|
+
return config;
|
|
424
|
+
}
|
|
425
|
+
const resolvedConfig = {
|
|
426
|
+
...config
|
|
427
|
+
};
|
|
428
|
+
for (const fieldPath of pathFields){
|
|
429
|
+
const value = getNestedValue$1(resolvedConfig, fieldPath);
|
|
430
|
+
if (value !== undefined) {
|
|
431
|
+
const shouldResolveArrayElements = resolvePathArray.includes(fieldPath);
|
|
432
|
+
const resolvedValue = resolvePathValue$1(value, configDir, shouldResolveArrayElements);
|
|
433
|
+
setNestedValue$1(resolvedConfig, fieldPath, resolvedValue);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return resolvedConfig;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Gets a nested value from an object using dot notation.
|
|
440
|
+
*/ function getNestedValue$1(obj, path) {
|
|
441
|
+
return path.split('.').reduce((current, key)=>current === null || current === void 0 ? void 0 : current[key], obj);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Sets a nested value in an object using dot notation.
|
|
445
|
+
*/ function setNestedValue$1(obj, path, value) {
|
|
446
|
+
const keys = path.split('.');
|
|
447
|
+
const lastKey = keys.pop();
|
|
448
|
+
const target = keys.reduce((current, key)=>{
|
|
449
|
+
if (!(key in current)) {
|
|
450
|
+
current[key] = {};
|
|
451
|
+
}
|
|
452
|
+
return current[key];
|
|
453
|
+
}, obj);
|
|
454
|
+
target[lastKey] = value;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Resolves a path value (string or array of strings) relative to the config directory.
|
|
458
|
+
*/ function resolvePathValue$1(value, configDir, resolveArrayElements) {
|
|
459
|
+
if (typeof value === 'string') {
|
|
460
|
+
return resolveSinglePath$1(value, configDir);
|
|
461
|
+
}
|
|
462
|
+
if (Array.isArray(value) && resolveArrayElements) {
|
|
463
|
+
return value.map((item)=>typeof item === 'string' ? resolveSinglePath$1(item, configDir) : item);
|
|
464
|
+
}
|
|
465
|
+
return value;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Resolves a single path string relative to the config directory if it's a relative path.
|
|
469
|
+
*/ function resolveSinglePath$1(pathStr, configDir) {
|
|
470
|
+
if (!pathStr || path.isAbsolute(pathStr)) {
|
|
471
|
+
return pathStr;
|
|
472
|
+
}
|
|
473
|
+
return path.resolve(configDir, pathStr);
|
|
474
|
+
}
|
|
418
475
|
/**
|
|
419
476
|
* Discovers configuration directories by traversing up the directory tree.
|
|
420
477
|
*
|
|
@@ -493,8 +550,10 @@ const create$1 = (params)=>{
|
|
|
493
550
|
* @param configFileName Name of the configuration file
|
|
494
551
|
* @param encoding File encoding
|
|
495
552
|
* @param logger Optional logger
|
|
553
|
+
* @param pathFields Optional array of field names that contain paths to be resolved
|
|
554
|
+
* @param resolvePathArray Optional array of field names whose array elements should all be resolved as paths
|
|
496
555
|
* @returns Promise resolving to parsed configuration object or null if not found
|
|
497
|
-
*/ async function loadConfigFromDirectory(configDir, configFileName, encoding = 'utf8', logger) {
|
|
556
|
+
*/ async function loadConfigFromDirectory(configDir, configFileName, encoding = 'utf8', logger, pathFields, resolvePathArray) {
|
|
498
557
|
const storage = create$1({
|
|
499
558
|
log: (logger === null || logger === void 0 ? void 0 : logger.debug) || (()=>{})
|
|
500
559
|
});
|
|
@@ -514,8 +573,13 @@ const create$1 = (params)=>{
|
|
|
514
573
|
const yamlContent = await storage.readFile(configFilePath, encoding);
|
|
515
574
|
const parsedYaml = yaml__namespace.load(yamlContent);
|
|
516
575
|
if (parsedYaml !== null && typeof parsedYaml === 'object') {
|
|
576
|
+
let config = parsedYaml;
|
|
577
|
+
// Apply path resolution if configured
|
|
578
|
+
if (pathFields && pathFields.length > 0) {
|
|
579
|
+
config = resolveConfigPaths$1(config, configDir, pathFields, resolvePathArray || []);
|
|
580
|
+
}
|
|
517
581
|
logger === null || logger === void 0 ? void 0 : logger.verbose(`Successfully loaded config from: ${configFilePath}`);
|
|
518
|
-
return
|
|
582
|
+
return config;
|
|
519
583
|
} else {
|
|
520
584
|
logger === null || logger === void 0 ? void 0 : logger.debug(`Config file contains invalid format: ${configFilePath}`);
|
|
521
585
|
return null;
|
|
@@ -526,24 +590,26 @@ const create$1 = (params)=>{
|
|
|
526
590
|
}
|
|
527
591
|
}
|
|
528
592
|
/**
|
|
529
|
-
* Deep merges multiple configuration objects with proper precedence.
|
|
593
|
+
* Deep merges multiple configuration objects with proper precedence and configurable array overlap behavior.
|
|
530
594
|
*
|
|
531
595
|
* Objects are merged from lowest precedence to highest precedence,
|
|
532
596
|
* meaning that properties in later objects override properties in earlier objects.
|
|
533
|
-
* Arrays
|
|
597
|
+
* Arrays can be merged using different strategies based on the fieldOverlaps configuration.
|
|
534
598
|
*
|
|
535
599
|
* @param configs Array of configuration objects, ordered from lowest to highest precedence
|
|
600
|
+
* @param fieldOverlaps Configuration for how array fields should be merged (optional)
|
|
536
601
|
* @returns Merged configuration object
|
|
537
602
|
*
|
|
538
603
|
* @example
|
|
539
604
|
* ```typescript
|
|
540
605
|
* const merged = deepMergeConfigs([
|
|
541
|
-
* { api: { timeout: 5000 },
|
|
542
|
-
* { api: { retries: 3 }, features: ['
|
|
543
|
-
* ]
|
|
544
|
-
* //
|
|
606
|
+
* { api: { timeout: 5000 }, features: ['auth'] }, // Lower precedence
|
|
607
|
+
* { api: { retries: 3 }, features: ['analytics'] }, // Higher precedence
|
|
608
|
+
* ], {
|
|
609
|
+
* 'features': 'append' // Results in features: ['auth', 'analytics']
|
|
610
|
+
* });
|
|
545
611
|
* ```
|
|
546
|
-
*/ function deepMergeConfigs(configs) {
|
|
612
|
+
*/ function deepMergeConfigs(configs, fieldOverlaps) {
|
|
547
613
|
if (configs.length === 0) {
|
|
548
614
|
return {};
|
|
549
615
|
}
|
|
@@ -553,16 +619,18 @@ const create$1 = (params)=>{
|
|
|
553
619
|
};
|
|
554
620
|
}
|
|
555
621
|
return configs.reduce((merged, current)=>{
|
|
556
|
-
return deepMergeTwo(merged, current);
|
|
622
|
+
return deepMergeTwo(merged, current, fieldOverlaps);
|
|
557
623
|
}, {});
|
|
558
624
|
}
|
|
559
625
|
/**
|
|
560
|
-
* Deep merges two objects with proper precedence.
|
|
626
|
+
* Deep merges two objects with proper precedence and configurable array overlap behavior.
|
|
561
627
|
*
|
|
562
628
|
* @param target Target object (lower precedence)
|
|
563
629
|
* @param source Source object (higher precedence)
|
|
630
|
+
* @param fieldOverlaps Configuration for how array fields should be merged (optional)
|
|
631
|
+
* @param currentPath Current field path for nested merging (used internally)
|
|
564
632
|
* @returns Merged object
|
|
565
|
-
*/ function deepMergeTwo(target, source) {
|
|
633
|
+
*/ function deepMergeTwo(target, source, fieldOverlaps, currentPath = '') {
|
|
566
634
|
// Handle null/undefined
|
|
567
635
|
if (source == null) return target;
|
|
568
636
|
if (target == null) return source;
|
|
@@ -570,11 +638,17 @@ const create$1 = (params)=>{
|
|
|
570
638
|
if (typeof source !== 'object' || typeof target !== 'object') {
|
|
571
639
|
return source; // Source takes precedence
|
|
572
640
|
}
|
|
573
|
-
// Handle arrays
|
|
641
|
+
// Handle arrays with configurable overlap behavior
|
|
574
642
|
if (Array.isArray(source)) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
643
|
+
if (Array.isArray(target) && fieldOverlaps) {
|
|
644
|
+
const overlapMode = getOverlapModeForPath(currentPath, fieldOverlaps);
|
|
645
|
+
return mergeArrays(target, source, overlapMode);
|
|
646
|
+
} else {
|
|
647
|
+
// Default behavior: replace entirely
|
|
648
|
+
return [
|
|
649
|
+
...source
|
|
650
|
+
];
|
|
651
|
+
}
|
|
578
652
|
}
|
|
579
653
|
if (Array.isArray(target)) {
|
|
580
654
|
return source; // Source object replaces target array
|
|
@@ -585,17 +659,72 @@ const create$1 = (params)=>{
|
|
|
585
659
|
};
|
|
586
660
|
for(const key in source){
|
|
587
661
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
662
|
+
const fieldPath = currentPath ? `${currentPath}.${key}` : key;
|
|
588
663
|
if (Object.prototype.hasOwnProperty.call(result, key) && typeof result[key] === 'object' && typeof source[key] === 'object' && !Array.isArray(source[key]) && !Array.isArray(result[key])) {
|
|
589
664
|
// Recursively merge nested objects
|
|
590
|
-
result[key] = deepMergeTwo(result[key], source[key]);
|
|
665
|
+
result[key] = deepMergeTwo(result[key], source[key], fieldOverlaps, fieldPath);
|
|
591
666
|
} else {
|
|
592
|
-
//
|
|
593
|
-
|
|
667
|
+
// Handle arrays and primitives with overlap consideration
|
|
668
|
+
if (Array.isArray(source[key]) && Array.isArray(result[key]) && fieldOverlaps) {
|
|
669
|
+
const overlapMode = getOverlapModeForPath(fieldPath, fieldOverlaps);
|
|
670
|
+
result[key] = mergeArrays(result[key], source[key], overlapMode);
|
|
671
|
+
} else {
|
|
672
|
+
// Replace with source value (higher precedence)
|
|
673
|
+
result[key] = source[key];
|
|
674
|
+
}
|
|
594
675
|
}
|
|
595
676
|
}
|
|
596
677
|
}
|
|
597
678
|
return result;
|
|
598
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* Determines the overlap mode for a given field path.
|
|
682
|
+
*
|
|
683
|
+
* @param fieldPath The current field path (dot notation)
|
|
684
|
+
* @param fieldOverlaps Configuration mapping field paths to overlap modes
|
|
685
|
+
* @returns The overlap mode to use for this field path
|
|
686
|
+
*/ function getOverlapModeForPath(fieldPath, fieldOverlaps) {
|
|
687
|
+
// Check for exact match first
|
|
688
|
+
if (fieldPath in fieldOverlaps) {
|
|
689
|
+
return fieldOverlaps[fieldPath];
|
|
690
|
+
}
|
|
691
|
+
// Check for any parent path matches (for nested configurations)
|
|
692
|
+
const pathParts = fieldPath.split('.');
|
|
693
|
+
for(let i = pathParts.length - 1; i > 0; i--){
|
|
694
|
+
const parentPath = pathParts.slice(0, i).join('.');
|
|
695
|
+
if (parentPath in fieldOverlaps) {
|
|
696
|
+
return fieldOverlaps[parentPath];
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Default to override if no specific configuration found
|
|
700
|
+
return 'override';
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Merges two arrays based on the specified overlap mode.
|
|
704
|
+
*
|
|
705
|
+
* @param targetArray The lower precedence array
|
|
706
|
+
* @param sourceArray The higher precedence array
|
|
707
|
+
* @param mode The overlap mode to use
|
|
708
|
+
* @returns The merged array
|
|
709
|
+
*/ function mergeArrays(targetArray, sourceArray, mode) {
|
|
710
|
+
switch(mode){
|
|
711
|
+
case 'append':
|
|
712
|
+
return [
|
|
713
|
+
...targetArray,
|
|
714
|
+
...sourceArray
|
|
715
|
+
];
|
|
716
|
+
case 'prepend':
|
|
717
|
+
return [
|
|
718
|
+
...sourceArray,
|
|
719
|
+
...targetArray
|
|
720
|
+
];
|
|
721
|
+
case 'override':
|
|
722
|
+
default:
|
|
723
|
+
return [
|
|
724
|
+
...sourceArray
|
|
725
|
+
];
|
|
726
|
+
}
|
|
727
|
+
}
|
|
599
728
|
/**
|
|
600
729
|
* Loads configurations from multiple directories and merges them with proper precedence.
|
|
601
730
|
*
|
|
@@ -614,15 +743,19 @@ const create$1 = (params)=>{
|
|
|
614
743
|
* configDirName: '.kodrdriv',
|
|
615
744
|
* configFileName: 'config.yaml',
|
|
616
745
|
* startingDir: '/project/subdir',
|
|
617
|
-
* maxLevels: 5
|
|
746
|
+
* maxLevels: 5,
|
|
747
|
+
* fieldOverlaps: {
|
|
748
|
+
* 'features': 'append',
|
|
749
|
+
* 'excludePatterns': 'prepend'
|
|
750
|
+
* }
|
|
618
751
|
* });
|
|
619
752
|
*
|
|
620
|
-
* // result.config contains merged configuration
|
|
753
|
+
* // result.config contains merged configuration with custom array merging
|
|
621
754
|
* // result.discoveredDirs shows where configs were found
|
|
622
755
|
* // result.errors contains any non-fatal errors
|
|
623
756
|
* ```
|
|
624
757
|
*/ async function loadHierarchicalConfig(options) {
|
|
625
|
-
const { configFileName, encoding = 'utf8', logger } = options;
|
|
758
|
+
const { configFileName, encoding = 'utf8', logger, pathFields, resolvePathArray, fieldOverlaps } = options;
|
|
626
759
|
logger === null || logger === void 0 ? void 0 : logger.verbose('Starting hierarchical configuration loading');
|
|
627
760
|
// Discover all configuration directories
|
|
628
761
|
const discoveredDirs = await discoverConfigDirectories(options);
|
|
@@ -643,7 +776,7 @@ const create$1 = (params)=>{
|
|
|
643
776
|
].sort((a, b)=>b.level - a.level);
|
|
644
777
|
for (const dir of sortedDirs){
|
|
645
778
|
try {
|
|
646
|
-
const config = await loadConfigFromDirectory(dir.path, configFileName, encoding, logger);
|
|
779
|
+
const config = await loadConfigFromDirectory(dir.path, configFileName, encoding, logger, pathFields, resolvePathArray);
|
|
647
780
|
if (config !== null) {
|
|
648
781
|
configs.push(config);
|
|
649
782
|
logger === null || logger === void 0 ? void 0 : logger.debug(`Loaded config from level ${dir.level}: ${dir.path}`);
|
|
@@ -656,8 +789,8 @@ const create$1 = (params)=>{
|
|
|
656
789
|
logger === null || logger === void 0 ? void 0 : logger.debug(errorMsg);
|
|
657
790
|
}
|
|
658
791
|
}
|
|
659
|
-
// Merge all configurations with proper precedence
|
|
660
|
-
const mergedConfig = deepMergeConfigs(configs);
|
|
792
|
+
// Merge all configurations with proper precedence and configurable array overlap
|
|
793
|
+
const mergedConfig = deepMergeConfigs(configs, fieldOverlaps);
|
|
661
794
|
logger === null || logger === void 0 ? void 0 : logger.verbose(`Hierarchical loading complete. Merged ${configs.length} configurations`);
|
|
662
795
|
return {
|
|
663
796
|
config: mergedConfig,
|
|
@@ -675,6 +808,68 @@ const create$1 = (params)=>{
|
|
|
675
808
|
*/ function clean(obj) {
|
|
676
809
|
return Object.fromEntries(Object.entries(obj).filter(([_, v])=>v !== undefined));
|
|
677
810
|
}
|
|
811
|
+
/**
|
|
812
|
+
* Resolves relative paths in configuration values relative to the configuration file's directory.
|
|
813
|
+
*
|
|
814
|
+
* @param config - The configuration object to process
|
|
815
|
+
* @param configDir - The directory containing the configuration file
|
|
816
|
+
* @param pathFields - Array of field names (using dot notation) that contain paths to be resolved
|
|
817
|
+
* @param resolvePathArray - Array of field names whose array elements should all be resolved as paths
|
|
818
|
+
* @returns The configuration object with resolved paths
|
|
819
|
+
*/ function resolveConfigPaths(config, configDir, pathFields = [], resolvePathArray = []) {
|
|
820
|
+
if (!config || typeof config !== 'object' || pathFields.length === 0) {
|
|
821
|
+
return config;
|
|
822
|
+
}
|
|
823
|
+
const resolvedConfig = {
|
|
824
|
+
...config
|
|
825
|
+
};
|
|
826
|
+
for (const fieldPath of pathFields){
|
|
827
|
+
const value = getNestedValue(resolvedConfig, fieldPath);
|
|
828
|
+
if (value !== undefined) {
|
|
829
|
+
const shouldResolveArrayElements = resolvePathArray.includes(fieldPath);
|
|
830
|
+
const resolvedValue = resolvePathValue(value, configDir, shouldResolveArrayElements);
|
|
831
|
+
setNestedValue(resolvedConfig, fieldPath, resolvedValue);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return resolvedConfig;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Gets a nested value from an object using dot notation.
|
|
838
|
+
*/ function getNestedValue(obj, path) {
|
|
839
|
+
return path.split('.').reduce((current, key)=>current === null || current === void 0 ? void 0 : current[key], obj);
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Sets a nested value in an object using dot notation.
|
|
843
|
+
*/ function setNestedValue(obj, path, value) {
|
|
844
|
+
const keys = path.split('.');
|
|
845
|
+
const lastKey = keys.pop();
|
|
846
|
+
const target = keys.reduce((current, key)=>{
|
|
847
|
+
if (!(key in current)) {
|
|
848
|
+
current[key] = {};
|
|
849
|
+
}
|
|
850
|
+
return current[key];
|
|
851
|
+
}, obj);
|
|
852
|
+
target[lastKey] = value;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Resolves a path value (string or array of strings) relative to the config directory.
|
|
856
|
+
*/ function resolvePathValue(value, configDir, resolveArrayElements) {
|
|
857
|
+
if (typeof value === 'string') {
|
|
858
|
+
return resolveSinglePath(value, configDir);
|
|
859
|
+
}
|
|
860
|
+
if (Array.isArray(value) && resolveArrayElements) {
|
|
861
|
+
return value.map((item)=>typeof item === 'string' ? resolveSinglePath(item, configDir) : item);
|
|
862
|
+
}
|
|
863
|
+
return value;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Resolves a single path string relative to the config directory if it's a relative path.
|
|
867
|
+
*/ function resolveSinglePath(pathStr, configDir) {
|
|
868
|
+
if (!pathStr || path__namespace.isAbsolute(pathStr)) {
|
|
869
|
+
return pathStr;
|
|
870
|
+
}
|
|
871
|
+
return path__namespace.resolve(configDir, pathStr);
|
|
872
|
+
}
|
|
678
873
|
/**
|
|
679
874
|
* Validates and secures a user-provided path to prevent path traversal attacks.
|
|
680
875
|
*
|
|
@@ -758,7 +953,7 @@ const create$1 = (params)=>{
|
|
|
758
953
|
* // config is fully typed based on your schema
|
|
759
954
|
* ```
|
|
760
955
|
*/ const read = async (args, options)=>{
|
|
761
|
-
var _options_defaults;
|
|
956
|
+
var _options_defaults, _options_defaults_pathResolution;
|
|
762
957
|
const logger = options.logger;
|
|
763
958
|
const rawConfigDir = args.configDirectory || ((_options_defaults = options.defaults) === null || _options_defaults === void 0 ? void 0 : _options_defaults.configDirectory);
|
|
764
959
|
if (!rawConfigDir) {
|
|
@@ -771,6 +966,7 @@ const create$1 = (params)=>{
|
|
|
771
966
|
if (options.features.includes('hierarchical')) {
|
|
772
967
|
logger.verbose('Hierarchical configuration discovery enabled');
|
|
773
968
|
try {
|
|
969
|
+
var _options_defaults_pathResolution1, _options_defaults_pathResolution2;
|
|
774
970
|
// Extract the config directory name from the path for hierarchical discovery
|
|
775
971
|
const configDirName = path__namespace.basename(resolvedConfigDir);
|
|
776
972
|
const startingDir = path__namespace.dirname(resolvedConfigDir);
|
|
@@ -780,7 +976,10 @@ const create$1 = (params)=>{
|
|
|
780
976
|
configFileName: options.defaults.configFile,
|
|
781
977
|
startingDir,
|
|
782
978
|
encoding: options.defaults.encoding,
|
|
783
|
-
logger
|
|
979
|
+
logger,
|
|
980
|
+
pathFields: (_options_defaults_pathResolution1 = options.defaults.pathResolution) === null || _options_defaults_pathResolution1 === void 0 ? void 0 : _options_defaults_pathResolution1.pathFields,
|
|
981
|
+
resolvePathArray: (_options_defaults_pathResolution2 = options.defaults.pathResolution) === null || _options_defaults_pathResolution2 === void 0 ? void 0 : _options_defaults_pathResolution2.resolvePathArray,
|
|
982
|
+
fieldOverlaps: options.defaults.fieldOverlaps
|
|
784
983
|
});
|
|
785
984
|
rawFileConfig = hierarchicalResult.config;
|
|
786
985
|
if (hierarchicalResult.discoveredDirs.length > 0) {
|
|
@@ -805,8 +1004,13 @@ const create$1 = (params)=>{
|
|
|
805
1004
|
logger.verbose('Using single directory configuration loading');
|
|
806
1005
|
rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
|
|
807
1006
|
}
|
|
1007
|
+
// Apply path resolution if configured
|
|
1008
|
+
let processedConfig = rawFileConfig;
|
|
1009
|
+
if ((_options_defaults_pathResolution = options.defaults.pathResolution) === null || _options_defaults_pathResolution === void 0 ? void 0 : _options_defaults_pathResolution.pathFields) {
|
|
1010
|
+
processedConfig = resolveConfigPaths(rawFileConfig, resolvedConfigDir, options.defaults.pathResolution.pathFields, options.defaults.pathResolution.resolvePathArray || []);
|
|
1011
|
+
}
|
|
808
1012
|
const config = clean({
|
|
809
|
-
...
|
|
1013
|
+
...processedConfig,
|
|
810
1014
|
...{
|
|
811
1015
|
configDirectory: resolvedConfigDir
|
|
812
1016
|
}
|
|
@@ -1175,6 +1379,7 @@ class ConfigurationError extends Error {
|
|
|
1175
1379
|
* @param pOptions.defaults.configFile - Name of the configuration file (optional, defaults to 'config.yaml')
|
|
1176
1380
|
* @param pOptions.defaults.isRequired - Whether the config directory must exist (optional, defaults to false)
|
|
1177
1381
|
* @param pOptions.defaults.encoding - File encoding for reading config files (optional, defaults to 'utf8')
|
|
1382
|
+
* @param pOptions.defaults.pathResolution - Configuration for resolving relative paths in config values relative to the config file's directory (optional)
|
|
1178
1383
|
* @param pOptions.features - Array of features to enable (optional, defaults to ['config'])
|
|
1179
1384
|
* @param pOptions.configShape - Zod schema shape defining your configuration structure (required)
|
|
1180
1385
|
* @param pOptions.logger - Custom logger implementation (optional, defaults to console logger)
|
|
@@ -1189,15 +1394,31 @@ class ConfigurationError extends Error {
|
|
|
1189
1394
|
* apiKey: z.string().min(1),
|
|
1190
1395
|
* timeout: z.number().default(5000),
|
|
1191
1396
|
* debug: z.boolean().default(false),
|
|
1397
|
+
* contextDirectories: z.array(z.string()).optional(),
|
|
1192
1398
|
* });
|
|
1193
1399
|
*
|
|
1194
1400
|
* const cardigantime = create({
|
|
1195
1401
|
* defaults: {
|
|
1196
1402
|
* configDirectory: './config',
|
|
1197
1403
|
* configFile: 'myapp.yaml',
|
|
1404
|
+
* // Resolve relative paths in contextDirectories relative to config file location
|
|
1405
|
+
* pathResolution: {
|
|
1406
|
+
* pathFields: ['contextDirectories'],
|
|
1407
|
+
* resolvePathArray: ['contextDirectories']
|
|
1408
|
+
* },
|
|
1409
|
+
* // Configure how array fields are merged in hierarchical mode
|
|
1410
|
+
* fieldOverlaps: {
|
|
1411
|
+
* 'features': 'append', // Accumulate features from all levels
|
|
1412
|
+
* 'excludePatterns': 'prepend' // Higher precedence patterns come first
|
|
1413
|
+
* }
|
|
1198
1414
|
* },
|
|
1199
1415
|
* configShape: MyConfigSchema.shape,
|
|
1416
|
+
* features: ['config', 'hierarchical'], // Enable hierarchical discovery
|
|
1200
1417
|
* });
|
|
1418
|
+
*
|
|
1419
|
+
* // If config file is at ../config/myapp.yaml and contains:
|
|
1420
|
+
* // contextDirectories: ['./context', './data']
|
|
1421
|
+
* // These paths will be resolved relative to ../config/ directory
|
|
1201
1422
|
* ```
|
|
1202
1423
|
*/ const create = (pOptions)=>{
|
|
1203
1424
|
const defaults = {
|