npm package: @freakynit/configurator
Description: Type-safe YAML configuration loader for Node.js with environment variable templating via Nunjucks. Supports local overrides, deep merging, caching, and customizable environment handling for clean and flexible configuration management.
Author: freakynit
Latest version: 1.0.0

# Configuration Loader

A type-safe YAML configuration loader for Node.js with environment variable templating support using Nunjucks. Supports configuration file merging and caching for optimal performance.

## Features

- Type-safe configuration loading with TypeScript generics
- YAML-based configuration files
- Nunjucks templating for environment variable interpolation
- Local configuration overrides (config.local.yaml)
- Configuration caching for improved performance
- Deep merging of configuration objects
- Built-in `default` filter for fallback values
- Custom error handling with ConfigError

## Installation

```bash
npm install "@freakynit/configurator"
```

## Quick Start

Create an example `config.yaml` file in your project root:

```yaml
app:
  name: MyApp
  environment: {{ NODE_ENV | default('development') }}
  port: {{ PORT | default('3000') }}
  host: {{ HOST | default('localhost') }}

database:
  host: {{ DB_HOST | default('localhost') }}
  port: {{ DB_PORT | default('5432') }}
  name: {{ DB_NAME | default('myapp_db') }}
  username: {{ DB_USER | default('postgres') }}
  password: {{ DB_PASSWORD | default('') }}
```

Load your configuration:

```typescript
import { getConfig } from '@freakynit/configurator';

interface AppConfig {
  app: {
    name: string;
    environment: string;
    port: number;
    host: string;
  };
  database: {
    host: string;
    port: number;
    name: string;
    username: string;
    password: string;
  };
}

const config = getConfig<AppConfig>();
console.log(`Starting ${config.app.name} on port ${config.app.port}`);
```

## Usage Examples

### Basic Configuration Loading

```typescript
import { getConfig, ConfigError } from '@freakynit/configurator';

interface AppConfig {
  app: {
    name: string;
    environment: string;
    port: number;
  };
}

try {
  const config = getConfig<AppConfig>();

  console.log(`Starting ${config.app.name} on port ${config.app.port}`);
  console.log(`Environment: ${config.app.environment}`);
} catch (error) {
  if (error instanceof ConfigError) {
    console.error('Configuration error:', error.message);
    if (error.cause) {
      console.error('Caused by:', error.cause);
    }
  }
  process.exit(1);
}
```

### Custom Configuration Files

```typescript
const config = getConfig<AppConfig>({
  configFile: 'config.production.yaml',
  localConfigFile: 'config.production.local.yaml',
  cache: true
});
```

### Using Custom Environment Variables

```typescript
const customEnv = {
  NODE_ENV: 'staging',
  PORT: '9000',
  DB_HOST: 'staging-db.example.com'
};

const config = getConfig<AppConfig>({
  env: customEnv,
  cache: false  // Don't cache when using custom env
});
```

### Accessing Specific Configuration Sections

```typescript
function getDatabaseConfig(): AppConfig['database'] {
  const config = getConfig<AppConfig>();
  return config.database;
}

const dbConfig = getDatabaseConfig();
console.log(`Database: ${dbConfig.username}@${dbConfig.host}:${dbConfig.port}`);
```

### Environment-Specific Initialization

```typescript
async function initializeApp() {
  const config = getConfig<AppConfig>();

  if (config.app.environment === 'production') {
    console.log('Initializing production environment');
    // Production-specific setup
  } else if (config.app.environment === 'development') {
    console.log('Initializing development environment');
    // Development-specific setup
  }

  return config;
}
```

### Configuration Reloading

```typescript
import { clearConfigCache, getConfig } from '@freakynit/configurator';

function reloadConfig() {
  clearConfigCache();
  const newConfig = getConfig<AppConfig>();
  console.log('Configuration reloaded');
  return newConfig;
}
```

### Feature Flag Checking

```typescript
interface AppConfig {
  features: {
    enableNewUI: boolean;
    enableBetaFeatures: boolean;
    maxUploadSize: number;
  };
}

function isFeatureEnabled(feature: keyof AppConfig['features']): boolean {
  const config = getConfig<AppConfig>();
  return config.features[feature] as boolean;
}

if (isFeatureEnabled('enableNewUI')) {
  console.log('New UI is enabled');
}
```

### Validating Required Configuration

```typescript
function validateConfig(config: AppConfig): void {
  const requiredKeys = [
    { path: 'app.name', value: config.app.name },
    { path: 'database.host', value: config.database.host }
  ];

  const missing = requiredKeys.filter(key => !key.value);

  if (missing.length > 0) {
    throw new Error(`Missing required configuration: ${missing.map(k => k.path).join(', ')}`);
  }
}

try {
  const config = getConfig<AppConfig>();
  validateConfig(config);
  console.log('Configuration is valid');
} catch (error) {
  console.error('Invalid configuration:', error);
  process.exit(1);
}
```

## Local Configuration Overrides

1. Create a `config.local.yaml` file to override settings without modifying the main config file. 
2. You can use this file to keep credentials. This file is typically added to `.gitignore`. 
3. This way, each of your developers can override only the needed sections with their own test credentials without touching the main config file.

```yaml
# config.local.yaml
database:
  password: my-secret-password

api:
  openai:
    key: sk-your-api-key-here
```

The local configuration is deep-merged with the base configuration, allowing partial overrides while keeping the rest of the base configuration intact.

## API Reference

### `getConfig<T>(options?: ConfigOptions): T`

Loads and parses the YAML configuration with type safety.

**Parameters:**
- `options.configFile` (string, optional): Path to the base configuration file. Default: `'config.yaml'`
- `options.localConfigFile` (string, optional): Path to the local configuration file. Default: `'config.local.yaml'`
- `options.cache` (boolean, optional): Whether to cache the configuration. Default: `true`
- `options.env` (Record<string, string>, optional): Custom environment variables for templating. Default: `process.env`

**Returns:** The parsed configuration object with type `T`

**Throws:** `ConfigError` if configuration files cannot be read or parsed

### `clearConfigCache(): void`

Clears the cached configuration, forcing a reload on the next `getConfig()` call.

### `ConfigError`

Custom error class for configuration-related errors.

**Properties:**
- `message`: Error message
- `cause`: Original error that caused the configuration error (if any)

## Templating Syntax

The configuration loader uses Nunjucks templating with environment variables:

```yaml
# Basic variable with default value
port: {{ PORT | default('3000') }}

# Variable without default (will be empty string if not set)
api_key: {{ API_KEY | default('') }}

# Boolean values
debug: {{ DEBUG | default('false') }}
```

## License

[MIT](LICENSE)