@theunwalked/cardigantime 0.0.8 → 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 +99 -23
- package/dist/cardigantime.cjs.map +1 -1
- package/dist/cardigantime.d.ts +6 -0
- package/dist/cardigantime.js +6 -0
- package/dist/cardigantime.js.map +1 -1
- package/dist/read.js +2 -1
- package/dist/read.js.map +1 -1
- package/dist/types.d.ts +30 -0
- package/dist/types.js.map +1 -1
- package/dist/util/hierarchical.d.ts +18 -10
- package/dist/util/hierarchical.js +91 -22
- 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
|
@@ -590,24 +590,26 @@ const create$1 = (params)=>{
|
|
|
590
590
|
}
|
|
591
591
|
}
|
|
592
592
|
/**
|
|
593
|
-
* Deep merges multiple configuration objects with proper precedence.
|
|
593
|
+
* Deep merges multiple configuration objects with proper precedence and configurable array overlap behavior.
|
|
594
594
|
*
|
|
595
595
|
* Objects are merged from lowest precedence to highest precedence,
|
|
596
596
|
* meaning that properties in later objects override properties in earlier objects.
|
|
597
|
-
* Arrays
|
|
597
|
+
* Arrays can be merged using different strategies based on the fieldOverlaps configuration.
|
|
598
598
|
*
|
|
599
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)
|
|
600
601
|
* @returns Merged configuration object
|
|
601
602
|
*
|
|
602
603
|
* @example
|
|
603
604
|
* ```typescript
|
|
604
605
|
* const merged = deepMergeConfigs([
|
|
605
|
-
* { api: { timeout: 5000 },
|
|
606
|
-
* { api: { retries: 3 }, features: ['
|
|
607
|
-
* ]
|
|
608
|
-
* //
|
|
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
|
+
* });
|
|
609
611
|
* ```
|
|
610
|
-
*/ function deepMergeConfigs(configs) {
|
|
612
|
+
*/ function deepMergeConfigs(configs, fieldOverlaps) {
|
|
611
613
|
if (configs.length === 0) {
|
|
612
614
|
return {};
|
|
613
615
|
}
|
|
@@ -617,16 +619,18 @@ const create$1 = (params)=>{
|
|
|
617
619
|
};
|
|
618
620
|
}
|
|
619
621
|
return configs.reduce((merged, current)=>{
|
|
620
|
-
return deepMergeTwo(merged, current);
|
|
622
|
+
return deepMergeTwo(merged, current, fieldOverlaps);
|
|
621
623
|
}, {});
|
|
622
624
|
}
|
|
623
625
|
/**
|
|
624
|
-
* Deep merges two objects with proper precedence.
|
|
626
|
+
* Deep merges two objects with proper precedence and configurable array overlap behavior.
|
|
625
627
|
*
|
|
626
628
|
* @param target Target object (lower precedence)
|
|
627
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)
|
|
628
632
|
* @returns Merged object
|
|
629
|
-
*/ function deepMergeTwo(target, source) {
|
|
633
|
+
*/ function deepMergeTwo(target, source, fieldOverlaps, currentPath = '') {
|
|
630
634
|
// Handle null/undefined
|
|
631
635
|
if (source == null) return target;
|
|
632
636
|
if (target == null) return source;
|
|
@@ -634,11 +638,17 @@ const create$1 = (params)=>{
|
|
|
634
638
|
if (typeof source !== 'object' || typeof target !== 'object') {
|
|
635
639
|
return source; // Source takes precedence
|
|
636
640
|
}
|
|
637
|
-
// Handle arrays
|
|
641
|
+
// Handle arrays with configurable overlap behavior
|
|
638
642
|
if (Array.isArray(source)) {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
+
}
|
|
642
652
|
}
|
|
643
653
|
if (Array.isArray(target)) {
|
|
644
654
|
return source; // Source object replaces target array
|
|
@@ -649,17 +659,72 @@ const create$1 = (params)=>{
|
|
|
649
659
|
};
|
|
650
660
|
for(const key in source){
|
|
651
661
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
662
|
+
const fieldPath = currentPath ? `${currentPath}.${key}` : key;
|
|
652
663
|
if (Object.prototype.hasOwnProperty.call(result, key) && typeof result[key] === 'object' && typeof source[key] === 'object' && !Array.isArray(source[key]) && !Array.isArray(result[key])) {
|
|
653
664
|
// Recursively merge nested objects
|
|
654
|
-
result[key] = deepMergeTwo(result[key], source[key]);
|
|
665
|
+
result[key] = deepMergeTwo(result[key], source[key], fieldOverlaps, fieldPath);
|
|
655
666
|
} else {
|
|
656
|
-
//
|
|
657
|
-
|
|
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
|
+
}
|
|
658
675
|
}
|
|
659
676
|
}
|
|
660
677
|
}
|
|
661
678
|
return result;
|
|
662
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
|
+
}
|
|
663
728
|
/**
|
|
664
729
|
* Loads configurations from multiple directories and merges them with proper precedence.
|
|
665
730
|
*
|
|
@@ -678,15 +743,19 @@ const create$1 = (params)=>{
|
|
|
678
743
|
* configDirName: '.kodrdriv',
|
|
679
744
|
* configFileName: 'config.yaml',
|
|
680
745
|
* startingDir: '/project/subdir',
|
|
681
|
-
* maxLevels: 5
|
|
746
|
+
* maxLevels: 5,
|
|
747
|
+
* fieldOverlaps: {
|
|
748
|
+
* 'features': 'append',
|
|
749
|
+
* 'excludePatterns': 'prepend'
|
|
750
|
+
* }
|
|
682
751
|
* });
|
|
683
752
|
*
|
|
684
|
-
* // result.config contains merged configuration
|
|
753
|
+
* // result.config contains merged configuration with custom array merging
|
|
685
754
|
* // result.discoveredDirs shows where configs were found
|
|
686
755
|
* // result.errors contains any non-fatal errors
|
|
687
756
|
* ```
|
|
688
757
|
*/ async function loadHierarchicalConfig(options) {
|
|
689
|
-
const { configFileName, encoding = 'utf8', logger, pathFields, resolvePathArray } = options;
|
|
758
|
+
const { configFileName, encoding = 'utf8', logger, pathFields, resolvePathArray, fieldOverlaps } = options;
|
|
690
759
|
logger === null || logger === void 0 ? void 0 : logger.verbose('Starting hierarchical configuration loading');
|
|
691
760
|
// Discover all configuration directories
|
|
692
761
|
const discoveredDirs = await discoverConfigDirectories(options);
|
|
@@ -720,8 +789,8 @@ const create$1 = (params)=>{
|
|
|
720
789
|
logger === null || logger === void 0 ? void 0 : logger.debug(errorMsg);
|
|
721
790
|
}
|
|
722
791
|
}
|
|
723
|
-
// Merge all configurations with proper precedence
|
|
724
|
-
const mergedConfig = deepMergeConfigs(configs);
|
|
792
|
+
// Merge all configurations with proper precedence and configurable array overlap
|
|
793
|
+
const mergedConfig = deepMergeConfigs(configs, fieldOverlaps);
|
|
725
794
|
logger === null || logger === void 0 ? void 0 : logger.verbose(`Hierarchical loading complete. Merged ${configs.length} configurations`);
|
|
726
795
|
return {
|
|
727
796
|
config: mergedConfig,
|
|
@@ -909,7 +978,8 @@ const create$1 = (params)=>{
|
|
|
909
978
|
encoding: options.defaults.encoding,
|
|
910
979
|
logger,
|
|
911
980
|
pathFields: (_options_defaults_pathResolution1 = options.defaults.pathResolution) === null || _options_defaults_pathResolution1 === void 0 ? void 0 : _options_defaults_pathResolution1.pathFields,
|
|
912
|
-
resolvePathArray: (_options_defaults_pathResolution2 = options.defaults.pathResolution) === null || _options_defaults_pathResolution2 === void 0 ? void 0 : _options_defaults_pathResolution2.resolvePathArray
|
|
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
|
|
913
983
|
});
|
|
914
984
|
rawFileConfig = hierarchicalResult.config;
|
|
915
985
|
if (hierarchicalResult.discoveredDirs.length > 0) {
|
|
@@ -1335,9 +1405,15 @@ class ConfigurationError extends Error {
|
|
|
1335
1405
|
* pathResolution: {
|
|
1336
1406
|
* pathFields: ['contextDirectories'],
|
|
1337
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
|
|
1338
1413
|
* }
|
|
1339
1414
|
* },
|
|
1340
1415
|
* configShape: MyConfigSchema.shape,
|
|
1416
|
+
* features: ['config', 'hierarchical'], // Enable hierarchical discovery
|
|
1341
1417
|
* });
|
|
1342
1418
|
*
|
|
1343
1419
|
* // If config file is at ../config/myapp.yaml and contains:
|