c12

repository·main·Indexed 21 days ago

https://github.com/unjs/c12

A smart configuration loader (v4.0.0-beta.5) that supports multiple formats, dynamic configuration via functions, and automatic detection of configuration files. It features configuration watching (HMR) via watchConfig, programmatic updates with updateConfig, and support for local or remote extensions using the extends key. c12 handles environment variables through dotenv integration, including variable interpolation and file reference expansion for container secrets.

Tokens
7.5K
Snippets
24
Records
33
Agent score
74%

What's inside c12

  1. How c12 loading priority works

    main

    c12 merges multiple configuration sources using defu. The priority (from highest to lowest) is:

    1. Config overrides passed by options
    2. Config file in CWD
    3. RC file in CWD
    4. Global RC file in the user's home directory
    5. Config from package.json
    6. Default config passed by options
    7. Extended config layers
  2. Use environment-specific configuration keys

    main

    c12 allows defining configuration that only applies in specific environments using special keys. These are applied when extending each configuration layer.

    Supported keys:

    • $test: {...}
    • $development: {...}
    • $production: {...}
    • $env: { [env_name]: {...} }

    The environment is determined by the envName option (defaults to process.env.NODE_ENV).

    export default {
      // Default configuration
      logLevel: "info",
    
      // Environment overrides
      $test: { logLevel: "silent" },
      $development: { logLevel: "warning" },
      $production: { logLevel: "error" },
      $env: {
        staging: { logLevel: "debug" },
      },
    };
    export default {
      logLevel: "info",
      $test: { logLevel: "silent" },
      $development: { logLevel: "warning" },
      $production: { logLevel: "error" },
      $env: {
        staging: { logLevel: "debug" },
      },
    };
  3. Migrate from c12 v3 to v4

    main

    Version 4 introduces significant improvements in size and performance, specifically regarding TypeScript loading speed.

    Key Changes & Peer Dependencies:

    • Size: Reduced from ~3.44MB to ~380kB.
    • TypeScript: Loading is significantly faster. If you require legacy TypeScript support (mixed ESM/CJS, no import extensions, etc.), install jiti as a peer dependency or provide a custom import config.
    • Extends: If you use the extends feature with remote or git sources, install giget as a peer dependency.
    • Watching: If using watchConfig, install chokidar as a peer dependency.
    • Dotenv: Uses native runtime features. You may need to add dotenv as a peer dependency only for legacy or Deno support.
  4. Install c12

    main

    Install the c12 package using your preferred package manager. Using nypm will automatically detect your package manager.

    # ✨ Auto-detect
    npx nypm install c12
    npx nypm install c12
  5. Extend configuration using the `extends` key

    main

    If a resolved configuration contains an extends key, c12 will load and merge those additional layers. This supports nested extensions and multiple sources.

    Local extensions

    Items in extends can be absolute or relative paths to a config file or a directory.

    // config.ts
    export default {
      colors: { primary: "user_primary" },
      extends: ["./theme"],
    };

    Remote extensions

    To extend from remote sources (GitHub, GitLab, Bitbucket, or HTTPS), you must install the giget peer dependency:

    npx nypm install giget

    Remote sources are identified by prefixes like gh:, gl:, bb:, or https:.

    // Extend from a github repository
    export default {
      extends: "gh:user/repo",
    };
    
    // Extend with branch and subpath
    export default {
      extends: "gh:user/repo/theme#dev",
    };
    
    // Extend a private repository with auth and dependency installation
    export default {
      extends: ["gh:user/repo", { auth: process.env.GITHUB_TOKEN, install: true }],
    };
    // config.ts
    export default {
      extends: "gh:user/repo",
    };
  6. Configure dotenv and environment variables

    main

    By default, .env loading is disabled. Enable it by passing true or an options object to the dotenv key in loadConfig.

    Loading multiple files

    You can pass an array of file names to dotenv.fileName to load multiple files that extend each other (left-to-right order).

    import { loadConfig } from "c12";
    
    const config = await loadConfig({
      dotenv: {
        fileName: [".env", ".env.local"],
      },
    });

    Resolving file references (_FILE suffix)

    If dotenv.expandFileReferences is set to true, environment variables ending in _FILE will be resolved by reading the content of the file at that path. This is useful for container secrets.

    # .env
    DATABASE_PASSWORD_FILE="/run/secrets/db_password"
    import { loadConfig } from "c12";
    
    const config = await loadConfig({
      dotenv: {
        expandFileReferences: true,
      },
    });
    
    // DATABASE_PASSWORD is now set to the contents of /run/secrets/db_password
  7. Use environment-specific configuration overrides

    main

    c12 supports environment-specific configuration blocks using the $ prefix. When options.envName is set (it defaults to process.env.NODE_ENV), c12 will look for keys matching $${envName} or $env.${envName} and merge them into the main configuration.

    Example:

    export default {
      port: 3000,
      $production: {
        port: 80
      }
    }

    If NODE_ENV is production, the resolved port will be 80.

    export default {
      port: 3000,
      $production: {
        port: 80
      }
    }
  8. Use `expandFileReferences` for container secrets

    main

    When expandFileReferences is set to true, c12 can resolve environment variables that point to file paths (common in Docker or Kubernetes secrets). If a key ends with _FILE, the value of that key is treated as a path, and the content of that file is assigned to the base key (the key without the _FILE suffix).

    Example .env content:

    DATABASE_PASSWORD_FILE="/run/secrets/db_password"

    Resulting environment:

    DATABASE_PASSWORD="<contents of /run/secrets/db_password>"
    await setupDotenv({
      expandFileReferences: true
    });
  9. Understand the `ResolvableConfig` type

    main

    A ResolvableConfig is a flexible type used for defaultConfig, overrides, or custom configuration functions. It can be:

    1. A plain object (the configuration itself).
    2. A Promise resolving to a configuration object.
    3. A function that receives a ResolvableConfigContext and returns a configuration object (or a Promise of one).

    The ResolvableConfigContext provides access to configs (a record of configurations from different sources like rc, packageJson, etc.) and rawConfigs (the unmerged, resolvable versions of those configs).

  10. Extend configuration using the extends feature

    main

    c12 allows your configuration to inherit from other files or even remote sources. By default, it looks for a key named extends in your configuration object. The values in this array can be:

    1. Local file paths: Relative or absolute paths to other config files.
    2. NPM packages: Package names that c12 will attempt to resolve.
    3. Remote URIs: Using giget prefixes like github:, gitlab:, https://, etc. (requires giget peer dependency).

    Example configuration:

    export default {
      extends: ['./base.config.ts', 'my-npm-config-package']
    }
    export default {
      extends: ['./base.config.ts', 'my-npm-config-package']
    }
  11. How the ConfigWatcher proxy works

    main

    The ConfigWatcher returned by watchConfig is a Proxy. This allows you to interact with it as if it were the ResolvedConfig itself.

    When you access a property on the watcher:

    1. If the property exists on the watcher's utility surface (watchingFiles or unwatch), it returns that value.
    2. Otherwise, it transparently proxies the request to the current ResolvedConfig.

    This ensures that as the configuration reloads internally during a watch cycle, the watcher object always provides access to the most recent configuration values without needing to manually re-query the watcher.

  12. Define dynamic configurations using functions

    main

    You can define your configuration as a function that accepts a context object. This allows you to return different configuration values based on the environment or other runtime context.

    Example Configuration (config.ts):

    export default (ctx) => {
      return {
        apiUrl: ctx?.dev ? "http://localhost:3000" : "https://api.example.com",
      };
    };

    Loading the dynamic config: Pass the context object into loadConfig to populate the ctx argument in your configuration function.

    // config.ts
    export default (ctx) => {
      return {
        apiUrl: ctx?.dev ? "http://localhost:3000" : "https://api.example.com",
      };
    };
    
    // Usage
    import { loadConfig } from "c12";
    
    const config = await loadConfig({
      context: { dev: true },
    });