@theunwalked/cardigantime 0.0.7 → 0.0.8

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.
@@ -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 parsedYaml;
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;
@@ -622,7 +686,7 @@ const create$1 = (params)=>{
622
686
  * // result.errors contains any non-fatal errors
623
687
  * ```
624
688
  */ async function loadHierarchicalConfig(options) {
625
- const { configFileName, encoding = 'utf8', logger } = options;
689
+ const { configFileName, encoding = 'utf8', logger, pathFields, resolvePathArray } = options;
626
690
  logger === null || logger === void 0 ? void 0 : logger.verbose('Starting hierarchical configuration loading');
627
691
  // Discover all configuration directories
628
692
  const discoveredDirs = await discoverConfigDirectories(options);
@@ -643,7 +707,7 @@ const create$1 = (params)=>{
643
707
  ].sort((a, b)=>b.level - a.level);
644
708
  for (const dir of sortedDirs){
645
709
  try {
646
- const config = await loadConfigFromDirectory(dir.path, configFileName, encoding, logger);
710
+ const config = await loadConfigFromDirectory(dir.path, configFileName, encoding, logger, pathFields, resolvePathArray);
647
711
  if (config !== null) {
648
712
  configs.push(config);
649
713
  logger === null || logger === void 0 ? void 0 : logger.debug(`Loaded config from level ${dir.level}: ${dir.path}`);
@@ -675,6 +739,68 @@ const create$1 = (params)=>{
675
739
  */ function clean(obj) {
676
740
  return Object.fromEntries(Object.entries(obj).filter(([_, v])=>v !== undefined));
677
741
  }
742
+ /**
743
+ * Resolves relative paths in configuration values relative to the configuration file's directory.
744
+ *
745
+ * @param config - The configuration object to process
746
+ * @param configDir - The directory containing the configuration file
747
+ * @param pathFields - Array of field names (using dot notation) that contain paths to be resolved
748
+ * @param resolvePathArray - Array of field names whose array elements should all be resolved as paths
749
+ * @returns The configuration object with resolved paths
750
+ */ function resolveConfigPaths(config, configDir, pathFields = [], resolvePathArray = []) {
751
+ if (!config || typeof config !== 'object' || pathFields.length === 0) {
752
+ return config;
753
+ }
754
+ const resolvedConfig = {
755
+ ...config
756
+ };
757
+ for (const fieldPath of pathFields){
758
+ const value = getNestedValue(resolvedConfig, fieldPath);
759
+ if (value !== undefined) {
760
+ const shouldResolveArrayElements = resolvePathArray.includes(fieldPath);
761
+ const resolvedValue = resolvePathValue(value, configDir, shouldResolveArrayElements);
762
+ setNestedValue(resolvedConfig, fieldPath, resolvedValue);
763
+ }
764
+ }
765
+ return resolvedConfig;
766
+ }
767
+ /**
768
+ * Gets a nested value from an object using dot notation.
769
+ */ function getNestedValue(obj, path) {
770
+ return path.split('.').reduce((current, key)=>current === null || current === void 0 ? void 0 : current[key], obj);
771
+ }
772
+ /**
773
+ * Sets a nested value in an object using dot notation.
774
+ */ function setNestedValue(obj, path, value) {
775
+ const keys = path.split('.');
776
+ const lastKey = keys.pop();
777
+ const target = keys.reduce((current, key)=>{
778
+ if (!(key in current)) {
779
+ current[key] = {};
780
+ }
781
+ return current[key];
782
+ }, obj);
783
+ target[lastKey] = value;
784
+ }
785
+ /**
786
+ * Resolves a path value (string or array of strings) relative to the config directory.
787
+ */ function resolvePathValue(value, configDir, resolveArrayElements) {
788
+ if (typeof value === 'string') {
789
+ return resolveSinglePath(value, configDir);
790
+ }
791
+ if (Array.isArray(value) && resolveArrayElements) {
792
+ return value.map((item)=>typeof item === 'string' ? resolveSinglePath(item, configDir) : item);
793
+ }
794
+ return value;
795
+ }
796
+ /**
797
+ * Resolves a single path string relative to the config directory if it's a relative path.
798
+ */ function resolveSinglePath(pathStr, configDir) {
799
+ if (!pathStr || path__namespace.isAbsolute(pathStr)) {
800
+ return pathStr;
801
+ }
802
+ return path__namespace.resolve(configDir, pathStr);
803
+ }
678
804
  /**
679
805
  * Validates and secures a user-provided path to prevent path traversal attacks.
680
806
  *
@@ -758,7 +884,7 @@ const create$1 = (params)=>{
758
884
  * // config is fully typed based on your schema
759
885
  * ```
760
886
  */ const read = async (args, options)=>{
761
- var _options_defaults;
887
+ var _options_defaults, _options_defaults_pathResolution;
762
888
  const logger = options.logger;
763
889
  const rawConfigDir = args.configDirectory || ((_options_defaults = options.defaults) === null || _options_defaults === void 0 ? void 0 : _options_defaults.configDirectory);
764
890
  if (!rawConfigDir) {
@@ -771,6 +897,7 @@ const create$1 = (params)=>{
771
897
  if (options.features.includes('hierarchical')) {
772
898
  logger.verbose('Hierarchical configuration discovery enabled');
773
899
  try {
900
+ var _options_defaults_pathResolution1, _options_defaults_pathResolution2;
774
901
  // Extract the config directory name from the path for hierarchical discovery
775
902
  const configDirName = path__namespace.basename(resolvedConfigDir);
776
903
  const startingDir = path__namespace.dirname(resolvedConfigDir);
@@ -780,7 +907,9 @@ const create$1 = (params)=>{
780
907
  configFileName: options.defaults.configFile,
781
908
  startingDir,
782
909
  encoding: options.defaults.encoding,
783
- logger
910
+ logger,
911
+ 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
784
913
  });
785
914
  rawFileConfig = hierarchicalResult.config;
786
915
  if (hierarchicalResult.discoveredDirs.length > 0) {
@@ -805,8 +934,13 @@ const create$1 = (params)=>{
805
934
  logger.verbose('Using single directory configuration loading');
806
935
  rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
807
936
  }
937
+ // Apply path resolution if configured
938
+ let processedConfig = rawFileConfig;
939
+ if ((_options_defaults_pathResolution = options.defaults.pathResolution) === null || _options_defaults_pathResolution === void 0 ? void 0 : _options_defaults_pathResolution.pathFields) {
940
+ processedConfig = resolveConfigPaths(rawFileConfig, resolvedConfigDir, options.defaults.pathResolution.pathFields, options.defaults.pathResolution.resolvePathArray || []);
941
+ }
808
942
  const config = clean({
809
- ...rawFileConfig,
943
+ ...processedConfig,
810
944
  ...{
811
945
  configDirectory: resolvedConfigDir
812
946
  }
@@ -1175,6 +1309,7 @@ class ConfigurationError extends Error {
1175
1309
  * @param pOptions.defaults.configFile - Name of the configuration file (optional, defaults to 'config.yaml')
1176
1310
  * @param pOptions.defaults.isRequired - Whether the config directory must exist (optional, defaults to false)
1177
1311
  * @param pOptions.defaults.encoding - File encoding for reading config files (optional, defaults to 'utf8')
1312
+ * @param pOptions.defaults.pathResolution - Configuration for resolving relative paths in config values relative to the config file's directory (optional)
1178
1313
  * @param pOptions.features - Array of features to enable (optional, defaults to ['config'])
1179
1314
  * @param pOptions.configShape - Zod schema shape defining your configuration structure (required)
1180
1315
  * @param pOptions.logger - Custom logger implementation (optional, defaults to console logger)
@@ -1189,15 +1324,25 @@ class ConfigurationError extends Error {
1189
1324
  * apiKey: z.string().min(1),
1190
1325
  * timeout: z.number().default(5000),
1191
1326
  * debug: z.boolean().default(false),
1327
+ * contextDirectories: z.array(z.string()).optional(),
1192
1328
  * });
1193
1329
  *
1194
1330
  * const cardigantime = create({
1195
1331
  * defaults: {
1196
1332
  * configDirectory: './config',
1197
1333
  * configFile: 'myapp.yaml',
1334
+ * // Resolve relative paths in contextDirectories relative to config file location
1335
+ * pathResolution: {
1336
+ * pathFields: ['contextDirectories'],
1337
+ * resolvePathArray: ['contextDirectories']
1338
+ * }
1198
1339
  * },
1199
1340
  * configShape: MyConfigSchema.shape,
1200
1341
  * });
1342
+ *
1343
+ * // If config file is at ../config/myapp.yaml and contains:
1344
+ * // contextDirectories: ['./context', './data']
1345
+ * // These paths will be resolved relative to ../config/ directory
1201
1346
  * ```
1202
1347
  */ const create = (pOptions)=>{
1203
1348
  const defaults = {