Bunli
Packages/Plugin Packages

@bunli/plugin-config

Configuration loading and merging plugin for Bunli

@bunli/plugin-config

The config plugin provides automatic configuration loading and merging from multiple sources, making it easy to support user preferences and project-specific settings.

Installation

bun add @bunli/plugin-config

Features

  • Load configuration from multiple file sources
  • Support for JSON and RC file formats
  • Deep or shallow merging strategies
  • Template variable substitution
  • User home directory support
  • Flexible source patterns

Basic Usage

import { createCLI } from '@bunli/core'
import { configMergerPlugin } from '@bunli/plugin-config'

const cli = await createCLI({
  name: 'my-cli',
  plugins: [
    configMergerPlugin()
  ]
})

Configuration Sources

By default, the plugin looks for configuration in these locations (in order):

  1. ~/.config/{{name}}/config.json - User config directory
  2. .{{name}}rc - Project RC file
  3. .{{name}}rc.json - Project RC file (JSON)
  4. .config/{{name}}.json - Project config directory

The {{name}} template is replaced with your CLI name.

Options

interface ConfigPluginOptions {
  /**
   * Config file sources to load
   * Supports template variables: {{name}} for app name
   * Default: ['~/.config/{{name}}/config.json', '.{{name}}rc', '.{{name}}rc.json']
   */
  sources?: string[]
  
  /**
   * Merge strategy
   * - 'deep': Recursively merge objects (default)
   * - 'shallow': Only merge top-level properties
   */
  mergeStrategy?: 'shallow' | 'deep'
  
  /**
   * Whether to stop on first found config
   * Default: false (loads and merges all found configs)
   */
  stopOnFirst?: boolean
}

Examples

Custom Sources

configMergerPlugin({
  sources: [
    '~/.myapp/config.json',
    './myapp.config.json',
    './config/myapp.json'
  ]
})

Shallow Merge

configMergerPlugin({
  mergeStrategy: 'shallow'
})

Stop on First Config

configMergerPlugin({
  sources: [
    './local.config.json',
    '~/.config/{{name}}/config.json'
  ],
  stopOnFirst: true // Use local config if it exists
})

Configuration File Format

Configuration files should be valid JSON:

{
  "name": "my-app",
  "version": "2.0.0",
  "description": "Overridden by config",
  "customField": {
    "nested": {
      "value": 123
    }
  }
}

Merge Behavior

Deep Merge (Default)

With deep merge, nested objects are recursively merged:

// File 1: ~/.config/myapp/config.json
{
  "api": {
    "url": "https://api.example.com",
    "timeout": 5000
  }
}

// File 2: .myapprc.json
{
  "api": {
    "timeout": 10000,
    "retries": 3
  }
}

// Result:
{
  "api": {
    "url": "https://api.example.com",
    "timeout": 10000,
    "retries": 3
  }
}

Shallow Merge

With shallow merge, only top-level properties are merged:

// File 1: ~/.config/myapp/config.json
{
  "api": {
    "url": "https://api.example.com",
    "timeout": 5000
  }
}

// File 2: .myapprc.json
{
  "api": {
    "timeout": 10000,
    "retries": 3
  }
}

// Result:
{
  "api": {
    "timeout": 10000,
    "retries": 3
  }
  // Note: url is lost with shallow merge
}

Best Practices

1. Order Sources by Priority

Place more specific configs later in the sources array:

configMergerPlugin({
  sources: [
    '~/.config/{{name}}/config.json',  // User defaults
    '.{{name}}rc.json',                 // Project config
    '.{{name}}rc.local.json'            // Local overrides
  ]
})

2. Document Config Schema

Create a config schema for your users:

interface MyAppConfig {
  api: {
    url: string
    timeout?: number
    retries?: number
  }
  features: {
    experimental?: boolean
    telemetry?: boolean
  }
}

3. Provide Config Examples

Include example configuration files in your project:

// .myapprc.example.json
{
  "api": {
    "url": "https://api.example.com",
    "timeout": 10000
  },
  "features": {
    "experimental": false,
    "telemetry": true
  }
}

4. Handle Missing Configs

The plugin handles missing config files gracefully:

// No error thrown if config files don't exist
const cli = await createCLI({
  name: 'my-cli',
  plugins: [
    configMergerPlugin({
      sources: [
        './optional-config.json',
        '~/.config/{{name}}/config.json'
      ]
    })
  ]
})

Integration with Commands

Merged configuration is available in your CLI config:

const cli = await createCLI({
  name: 'my-cli',
  version: '1.0.0',
  plugins: [configMergerPlugin()]
})

// Config values can override CLI properties
cli.command(defineCommand({
  name: 'info',
  handler: async ({ cli }) => {
    console.log(`Name: ${cli.name}`)
    console.log(`Version: ${cli.version}`)
    // These might be overridden by config files
  }
}))

Debugging

The plugin uses the logger to provide debugging information:

// Enable debug logging to see which configs are loaded
process.env.DEBUG = 'bunli:*'

configMergerPlugin({
  sources: ['./config.json', '~/.config/app.json']
})

// Output:
// [bunli:plugin] Config file not found: ./config.json
// [bunli:plugin] Loaded config from ~/.config/app.json
// [bunli:plugin] Merged 1 config file(s)