Skip to content

ElsiKora/Configer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

12 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

project-logo

βš™οΈ Configer

Production-grade config loader with cosmiconfig-level capabilities and a clean original API

ElsiKora TypeScript Node.js npm Rollup Vitest ESLint Prettier GitHub Actions

πŸ’‘ Highlights

  • 🎯 Deterministic config resolution with 4 search strategies (none, project, workspace, global) and 40+ default search places per module name
  • πŸ“¦ First-class support for 10+ formats β€” JSON, YAML, TOML, JSON5, JSONC, .env, JS/TS modules, and package.json properties β€” with both async and sync loaders
  • πŸ”— Config inheritance ($import/extends), environment overlays ($env/$development), schema validation, and plugin hooks β€” all built-in with zero config
  • πŸ—οΈ Clean Architecture internals with full DI container, making every component testable, replaceable, and independently extensible

πŸ“š Table of Contents

πŸ“– Description

Configer is a typed, extensible configuration loader for Node.js that resolves config files across project, workspace, and global scopes β€” with deterministic search plans, config inheritance chains, environment overlays, schema validation, and plugin lifecycle hooks.

Built with clean architecture principles (domain β†’ application β†’ infrastructure β†’ presentation layers) and powered by a dependency injection container, Configer delivers the flexibility of cosmiconfig with stronger type safety, explicit error codes, and a modular internal design.

Real-World Use Cases

  • CLI Tools & Dev Tooling: Automatically discover .toolrc, .toolrc.yaml, tool.config.ts, or package.json properties β€” just like ESLint, Prettier, and Babel do β€” but with full TypeScript generics and schema validation out of the box.
  • Monorepo Configuration: Use the workspace search strategy to traverse from any nested package up to the workspace root, merging shared base configs with per-package overrides.
  • Multi-Environment Apps: Define $development, $production, or $env blocks directly in config files. Configer strips inactive environments and deep-merges the active one β€” no extra tooling required.
  • Plugin Ecosystems: Extend the config lifecycle with beforeFind, afterRead, and onError hooks to inject defaults, validate against external schemas, or log diagnostics.
  • Library Authors: Ship a typed config contract (IConfigOptions<TEntity>) so consumers get autocomplete and compile-time safety for your tool's configuration.

πŸ› οΈ Tech Stack

Category Technologies
Language TypeScript
Runtime Node.js >= 20.0.0
Build Tool Rollup, TypeScript Compiler
Testing Vitest, V8 Coverage
Linting ESLint, Prettier
Package Manager npm
CI/CD Semantic Release, Husky, Commitlint
Dependencies yaml, smol-toml, json5, jsonc-parser, dotenv, @elsikora/cladi

πŸš€ Features

  • ✨ **Async & Sync Clients β€” createConfiger() for async IO with watch support; createConfigerSync() for synchronous-only flows with strict fail-fast guarantees**
  • ✨ **10+ Built-in Loaders β€” JSON, JSON5, JSONC, YAML, TOML, .env, JS (.js/.cjs/.mjs), TypeScript (.ts/.cts/.mts), and package.json property extraction**
  • ✨ **4 Search Strategies β€” none (current directory), project (up to package.json), workspace (up to monorepo root), global (including XDG config home)**
  • ✨ **Config Inheritance β€” Merge parent configs via extends or $import directives with circular reference detection and source chain tracking**
  • ✨ **Environment Overlays β€” $development, $production, or $env map blocks are deep-merged for the active environment and stripped from output**
  • ✨ **Built-in Schema Validation β€” Declare required fields, types, defaults, nested objects, array items, and custom validators without external dependencies**
  • ✨ **Plugin Lifecycle Hooks β€” beforeFind, afterFind, beforeRead, afterRead, and onError hooks for extending every stage of config resolution**
  • ✨ **Custom Loaders β€” Register async/sync loaders for any file extension (.ini, .xml, .custom) via the loaders option**
  • ✨ **Watch Mode β€” File system watcher with debounced refresh, automatic cache invalidation, and clean IWatchHandle.close() cleanup**
  • ✨ **Caching Layer β€” Separate find/read caches with granular clearFindCache(), clearReadCache(), and clearCaches() controls**
  • ✨ **Stable Error Codes β€” Every failure throws ConfigError with a machine-readable CODE and human-readable SUGGESTIONS array**
  • ✨ **Clean Architecture β€” Domain-driven layers (domain β†’ application β†’ infrastructure β†’ presentation) with full DI container for testability**

πŸ— Architecture

System Architecture

flowchart TD
    presentation[Presentation Layer]
    application[Application Layer]
    domain[Domain Layer]
    infrastructure[Infrastructure Layer]
    presentation --> application
    presentation --> infrastructure
    application --> domain
    infrastructure --> application
    infrastructure --> domain
    subgraph presentation
        createConfiger[createConfiger]
        createConfigerSync[createConfigerSync]
    end
    subgraph infrastructure
        diContainer[DI Container]
        loaderRegistry[Loader Registry]
        searchResolvers[Search Resolvers]
        cacheAdapter[Memory Cache]
        schemaValidator[Schema Validator]
        deepMerge[Deep Merge]
        fileWatcher[File Watcher]
    end
    subgraph application
        resolveSearchPlan[ResolveSearchPlanUseCase]
    end
    subgraph domain
        configClient[IConfigClient]
        configResult[IConfigResult]
        configError[ConfigError]
        pluginContract[IConfigPlugin]
    end
Loading

Data Flow

sequenceDiagram
    participant User
    participant Client as ConfigClient
    participant Plugins as Plugin Hooks
    participant SearchPlan as ResolveSearchPlan
    participant Cache as MemoryCache
    participant Loader as LoaderRegistry
    participant Merge as DeepMerge
    participant Schema as SchemaValidator
    User->>Client: findConfig(searchFrom?)
    Client->>Plugins: beforeFind(context)
    Client->>Cache: getFindResult(key)
    alt Cache Hit
        Cache-->>Client: cached result
    else Cache Miss
        Client->>SearchPlan: execute(options)
        SearchPlan-->>Client: candidateFilepaths[]
        loop Each candidate
            Client->>Loader: load(filepath, content)
            Loader-->>Client: raw config
            Client->>Merge: resolveInheritance(extends/$import)
            Merge-->>Client: merged config
            Client->>Client: resolveEnvironmentOverrides($env)
            Client->>Schema: validate(config, descriptor)
            Schema-->>Client: validated config
        end
        Client->>Cache: setFindResult(key, result)
    end
    Client->>Plugins: afterFind(result, context)
    Client-->>User: IConfigResult or null
Loading

πŸ“ Project Structure

Click to expand
Configer/
β”œβ”€β”€ docs/
β”‚   β”œβ”€β”€ api-reference/
β”‚   β”‚   β”œβ”€β”€ error-codes/
β”‚   β”‚   β”œβ”€β”€ factories/
β”‚   β”‚   β”œβ”€β”€ interfaces/
β”‚   β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”œβ”€β”€ _meta.js
β”‚   β”‚   └── page.mdx
β”‚   β”œβ”€β”€ core-concepts/
β”‚   β”‚   β”œβ”€β”€ caching-and-watch/
β”‚   β”‚   β”œβ”€β”€ environment-overrides/
β”‚   β”‚   β”œβ”€β”€ inheritance-and-merge/
β”‚   β”‚   β”œβ”€β”€ resolution-flow/
β”‚   β”‚   β”œβ”€β”€ schema-validation/
β”‚   β”‚   β”œβ”€β”€ _meta.js
β”‚   β”‚   └── page.mdx
β”‚   β”œβ”€β”€ getting-started/
β”‚   β”‚   β”œβ”€β”€ first-client/
β”‚   β”‚   β”œβ”€β”€ first-config-read/
β”‚   β”‚   β”œβ”€β”€ installation/
β”‚   β”‚   β”œβ”€β”€ sync-mode/
β”‚   β”‚   β”œβ”€β”€ _meta.js
β”‚   β”‚   └── page.mdx
β”‚   β”œβ”€β”€ guides/
β”‚   β”‚   β”œβ”€β”€ custom-loaders/
β”‚   β”‚   β”œβ”€β”€ custom-search-places/
β”‚   β”‚   β”œβ”€β”€ package-json-property/
β”‚   β”‚   β”œβ”€β”€ plugins/
β”‚   β”‚   β”œβ”€β”€ watch-mode/
β”‚   β”‚   β”œβ”€β”€ _meta.js
β”‚   β”‚   └── page.mdx
β”‚   β”œβ”€β”€ _meta.js
β”‚   └── page.mdx
β”œβ”€β”€ examples/
β”‚   β”œβ”€β”€ 01-basic-usage/
β”‚   β”‚   └── main.ts
β”‚   β”œβ”€β”€ 02-search-strategies/
β”‚   β”‚   └── main.ts
β”‚   β”œβ”€β”€ 03-file-formats/
β”‚   β”‚   └── main.ts
β”‚   β”œβ”€β”€ 04-inheritance/
β”‚   β”‚   └── main.ts
β”‚   β”œβ”€β”€ 05-environment-overrides/
β”‚   β”‚   └── main.ts
β”‚   β”œβ”€β”€ 06-plugins/
β”‚   β”‚   └── main.ts
β”‚   β”œβ”€β”€ 07-schema-validation/
β”‚   β”‚   └── main.ts
β”‚   β”œβ”€β”€ 08-custom-loaders/
β”‚   β”‚   └── main.ts
β”‚   β”œβ”€β”€ 09-watch-mode/
β”‚   β”‚   └── main.ts
β”‚   β”œβ”€β”€ 10-package-json-property/
β”‚   β”‚   └── main.ts
β”‚   β”œβ”€β”€ README.md
β”‚   └── tsconfig.json
β”œβ”€β”€ scripts/
β”‚   β”œβ”€β”€ clean.function.js
β”‚   └── validate-documentation.function.js
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ application/
β”‚   β”‚   β”œβ”€β”€ dto/
β”‚   β”‚   β”œβ”€β”€ interface/
β”‚   β”‚   β”œβ”€β”€ use-case/
β”‚   β”‚   └── index.ts
β”‚   β”œβ”€β”€ domain/
β”‚   β”‚   β”œβ”€β”€ entity/
β”‚   β”‚   β”œβ”€β”€ error/
β”‚   β”‚   β”œβ”€β”€ type/
β”‚   β”‚   └── index.ts
β”‚   β”œβ”€β”€ infrastructure/
β”‚   β”‚   β”œβ”€β”€ adapter/
β”‚   β”‚   β”œβ”€β”€ cache/
β”‚   β”‚   β”œβ”€β”€ di/
β”‚   β”‚   β”œβ”€β”€ loader/
β”‚   β”‚   β”œβ”€β”€ resolver/
β”‚   β”‚   β”œβ”€β”€ service/
β”‚   β”‚   β”œβ”€β”€ watcher/
β”‚   β”‚   └── index.ts
β”‚   β”œβ”€β”€ presentation/
β”‚   β”‚   β”œβ”€β”€ function/
β”‚   β”‚   └── index.ts
β”‚   └── index.ts
β”œβ”€β”€ test/
β”‚   β”œβ”€β”€ e2e/
β”‚   β”‚   β”œβ”€β”€ configer-error-paths.end-to-end.test.ts
β”‚   β”‚   └── configer-integration.end-to-end.test.ts
β”‚   └── unit/
β”‚       β”œβ”€β”€ application/
β”‚       β”œβ”€β”€ domain/
β”‚       β”œβ”€β”€ infrastructure/
β”‚       └── presentation/
β”œβ”€β”€ commitlint.config.js
β”œβ”€β”€ eslint.config.js
β”œβ”€β”€ generated-logo.png
β”œβ”€β”€ lint-staged.config.js
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ package.json
β”œβ”€β”€ prettier.config.js
β”œβ”€β”€ README.md
β”œβ”€β”€ release.config.js
β”œβ”€β”€ rollup.config.js
β”œβ”€β”€ tsconfig.build.json
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ vitest.config.js
β”œβ”€β”€ vitest.end-to-end.config.js
└── vitest.unit.config.js

πŸ“‹ Prerequisites

  • Node.js >= 20.0.0
  • npm >= 9.0.0 (or pnpm / yarn equivalent)
  • ESM-compatible project ("type": "module" in package.json)

πŸ›  Installation

# Using npm
npm install @elsikora/configer

# Using pnpm
pnpm add @elsikora/configer

# Using yarn
yarn add @elsikora/configer


Verify the installation:

ts
import { createConfiger } from '@elsikora/configer';

const client = createConfiger({ moduleName: 'my-app' });
console.log(typeof client.findConfig === 'function'); // true

πŸ’‘ Usage

Basic Async Usage

import { createConfiger } from '@elsikora/configer';
import type { IConfigClient, IConfigResult } from '@elsikora/configer';

type AppConfig = {
  serviceName: string;
  isFeatureEnabled: boolean;
};

const client: IConfigClient<AppConfig> = createConfiger<AppConfig>({
  moduleName: 'my-app',
  cwd: process.cwd(),
});

const result: IConfigResult<AppConfig> | null = await client.findConfig();

if (result) {
  console.log(result.filepath); // /project/.my-apprc.json
  console.log(result.config?.serviceName); // "billing-api"
}

Sync Mode

import { createConfigerSync } from '@elsikora/configer';

const client = createConfigerSync<{ retries: number }>({
  moduleName: 'my-app',
});

const result = client.findConfig();
console.log(result?.config?.retries); // 3

Search Strategies

// Search only the current directory
const client = createConfiger({ moduleName: 'app', searchStrategy: 'none' });

// Ascend until nearest package.json
const client = createConfiger({ moduleName: 'app', searchStrategy: 'project' });

// Ascend until workspace root (.git, pnpm-workspace.yaml, etc.)
const client = createConfiger({ moduleName: 'app', searchStrategy: 'workspace' });

// Traverse project + workspace + global config directory
const client = createConfiger({ moduleName: 'app', searchStrategy: 'global' });

Config Inheritance

Create a base config (base.config.json):

{ "logLevel": "info", "retries": 1 }

Extend it in your app config (.my-apprc.json):

{
  "extends": "./base.config.json",
  "retries": 3
}

Result: { logLevel: "info", retries: 3 } with sources tracking the full chain.

Environment Overrides

{
  "apiUrl": "https://api.default.com",
  "$development": { "apiUrl": "https://dev.api.com" },
  "$env": {
    "production": { "apiUrl": "https://prod.api.com" }
  }
}
const client = createConfiger({
  moduleName: 'app',
  envName: 'development', // or reads NODE_ENV automatically
});

Schema Validation

import type { ISchemaDescriptor } from '@elsikora/configer';

const schema: ISchemaDescriptor<{ serviceName: string; retryCount: number }> = {
  type: 'object',
  properties: {
    serviceName: { type: 'string', isRequired: true },
    retryCount: { type: 'number', defaultValue: 3 },
  },
  shouldAllowUnknownProperties: false,
};

const client = createConfiger({
  moduleName: 'app',
  schema,
});

Plugin Hooks

const client = createConfiger({
  moduleName: 'app',
  plugins: [
    {
      name: 'source-marker',
      afterRead: (result) => ({
        ...result,
        config: { ...result.config, _loadedFrom: result.filepath },
      }),
      onError: (error) => console.error('Config error:', error.message),
    },
  ],
});

Custom Loaders

const client = createConfiger({
  moduleName: 'app',
  searchPlaces: ['config.ini'],
  shouldMergeSearchPlaces: false,
  loaders: {
    '.ini': {
      asyncLoader: (_filepath, content) => parseIni(content),
      syncLoader: (_filepath, content) => parseIni(content),
    },
  },
});

Watch Mode

const handle = client.watchConfig((error, result) => {
  if (error) return console.error(error);
  console.log('Config changed:', result?.config);
});

// Cleanup on shutdown
process.on('SIGTERM', () => handle.close());

Reading from package.json

const client = createConfiger({
  moduleName: 'my-tool',
  packageProperty: 'config.myTool', // or ['config', 'myTool']
  searchPlaces: ['package.json'],
  shouldMergeSearchPlaces: false,
});

πŸ›£ Roadmap

Click to expand
Task / Feature Status
Core async/sync config client with search strategies βœ… Done
Built-in loaders for JSON, YAML, TOML, JSON5, JSONC, .env, JS/TS βœ… Done
Config inheritance via extends and $import with cycle detection βœ… Done
Environment overrides ($env, $development, $production) βœ… Done
Built-in schema validation with nested objects and arrays βœ… Done
Plugin lifecycle hooks (beforeFind, afterRead, onError) βœ… Done
Watch mode with debounced file system monitoring βœ… Done
Clean Architecture with full DI container βœ… Done
Comprehensive unit and e2e test suites βœ… Done
MDX documentation site with guides and API reference βœ… Done
Remote config source support (HTTP/S3) 🚧 In Progress
Config encryption/decryption plugin 🚧 In Progress
JSON Schema / Zod adapter for schema validation 🚧 In Progress
Config diff and migration tooling 🚧 In Progress

❓ FAQ

Click to expand

How is Configer different from cosmiconfig?

Configer is inspired by cosmiconfig but built from scratch with TypeScript generics, Clean Architecture internals, and additional features: config inheritance, environment overlays, schema validation, plugin hooks, and watch mode β€” all built-in without extra packages.

Can I migrate from cosmiconfig?

  • Replace cosmiconfig(moduleName, options) β†’ createConfiger({ moduleName, ...options })
  • Replace search() β†’ findConfig()
  • Replace load(filepath) β†’ readConfig(filepath)
  • Use createConfigerSync for synchronous API
  • Move custom transforms into plugins (beforeRead, afterRead, onError)

What file formats are supported out of the box?

JSON, JSON5, JSONC, YAML (.yaml/.yml), TOML, .env, JavaScript modules (.js/.cjs/.mjs), TypeScript modules (.ts/.cts/.mts), and package.json with nested property extraction.

Can I use this in a CommonJS project?

Configer is published as ESM only. Your project needs "type": "module" in package.json or you can use dynamic import() from CJS code.

When should I use the sync client vs async client?

Use createConfigerSync() only when your runtime strictly requires synchronous behavior (e.g., synchronous CLI startup). The sync client cannot load .mjs, .mts, or .ts files, and plugin hooks must not return Promises. For all other cases, prefer createConfiger() (async).

How does the search strategy work?

  • none: Only searches the start directory
  • project: Ascends directories until it finds a package.json
  • workspace: Ascends until a workspace root marker (.git, pnpm-workspace.yaml, turbo.json, etc.)
  • global: Combines project/workspace traversal with $XDG_CONFIG_HOME/<moduleName> or ~/.config/<moduleName>

How do I handle errors?

All errors are instances of ConfigError with a stable .CODE property for programmatic handling and a .SUGGESTIONS array with actionable fix recommendations. Use error.CODE for branching logic and error.message for user-facing output.

πŸ”’ License

This project is licensed under MIT.

πŸ™ Acknowledgments


Back to Top

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors