Bunli
PackagesPlugin Packages

@bunli/plugin-config

Configuration loading and merging plugin for Bunli

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()] as const,
});

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', '.config/{{name}}.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",
    description: "Show CLI information",
    handler: async ({ context, runtime }) => {
      // Access config via plugin store
      if (context?.store.config) {
        console.log("Config loaded:", context.store.config);
      }
    },
  }),
);

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)

On this page