c12
Smart configuration loader for JavaScript and TypeScript that merges config files, RC files, package.json fields, dotenv, and remote extends into one resolved config.
Repository Health
Technical Analysis
c12 is a configuration loading library for JavaScript and TypeScript projects, built by the UnJS team behind Nuxt, Nitro, and other tools. It resolves a project’s config by merging multiple sources — an explicit config file, RC files, package.json fields, environment variables, and remote layers — using a fixed priority order, so callers get one deeply-merged object regardless of which combination of sources exists.
Beyond simple merging, c12 supports config “extends” chains that can pull in local directories or remote GitHub/GitLab/npm sources via giget, environment-specific overrides through reserved $test/$development/$production/$env keys, and a watchConfig mode that hot-reloads consumers when any of the resolved files change. Version 4 significantly reduced install size and cold-start time by trimming c12’s dependency graph and moving several loaders (chokidar, giget, jiti, dotenv, magicast) to optional peer dependencies that are only pulled in when a project actually uses that feature.
What You Get
- Multi-source config resolution - merges overrides, main config file, RC files, package.json fields, and defaults in a fixed, documented priority order.
- Format-agnostic file loading - loads
.js/.ts/.mjs/.cjs/.mts/.ctsvia native import (with jiti fallback) plus.json/.jsonc/.json5/.yaml/.yml/.tomlvia confbox parsers. - Config extends chains - recursively extend from local paths or remote GitHub/GitLab/bitbucket/npm sources through giget, with per-layer options.
- Environment-specific overrides - reserved
$test,$development,$production, and$envkeys are applied automatically based onenvName. - Live config watching -
watchConfigreloads consumers on file add/change/remove withonWatch/acceptHMR/onUpdatehooks. - Programmatic config updates -
updateConfig(via magicast) creates or edits config files as an AST rather than raw text.
Common Use Cases
- Framework config loading - a CLI or framework like Nuxt or Nitro loads its
nuxt.config.ts/nitro.config.tsplus any RC overrides with oneloadConfigcall. - Themeable config layers - a tool lets users
extendsa shared base config (local or from a GitHub template) for design systems or monorepo presets. - Environment-aware app config - an app defines
$production/$developmentoverrides in one config file instead of maintaining separate files per environment. - Dev-server hot reload - a build tool uses
watchConfigto pick up config edits without a full process restart.
Under The Hood
Architecture
c12 exposes a single entry (src/index.ts) that barrels four independently-testable modules: loader.ts (the core loadConfig/resolveConfig pipeline), watch.ts (a thin chokidar wrapper around loadConfig for HMR), dotenv.ts (env-file parsing/interpolation), and update.ts (magicast-based AST config mutation) — types.ts holds all shared type definitions with no runtime code, keeping the type surface separable from behavior. The core flow in loader.ts is a straight-line pipeline: normalize options, build a rawConfigs record keyed by ConfigSource (overrides/main/rc/packageJson/defaultConfig), resolve each source (awaiting function-valued sources against a ResolvableConfigContext), merge them via defu in a fixed precedence order, then recursively call extendConfig to walk the extends key and stitch remote/local layers into config._layers before a final merge pass folds every layer back into the top-level config. resolveConfig itself branches on source shape (a custom options.resolve hook, giget URI prefixes gh:/gitlab:/bitbucket:/https:, an npm-package-name regex, then a plain filesystem path) before dispatching to either an ASYNC_LOADERS format parser (YAML/JSONC/JSON5/TOML via confbox) or a native import() with a jiti fallback for legacy TS/CJS syntax — dependency injection appears via optional import/resolve/resolveModule/merger callbacks that let downstream frameworks override any stage without forking the loader. The ConfigSource union type and the ordered rawConfigs iteration are load-bearing for the documented merge-priority guarantee; changing that iteration order would be a breaking change for every consumer relying on override semantics.
Tech Stack
c12 is a pure-ESM TypeScript library (type: module, an exports map with .mjs outputs only, no CJS build) targeting Node with esnext/nodenext module resolution and strict TypeScript settings (strict, noUncheckedIndexedAccess, verbatimModuleSyntax). Its runtime dependency graph is deliberately small — confbox for YAML/JSONC/JSON5/TOML parsing, defu for deep-merge, exsolve for module path resolution, pathe for cross-platform paths, pkg-types for reading package.json/workspace roots, and rc9 for RC-file read/write — with heavier capabilities (chokidar for watching, giget for remote git extends, jiti for legacy TS/CJS loading, dotenv, magicast for AST-based config updates) declared as optional peer dependencies and dynamically imported only when a caller actually exercises that path, which is how the project reports install size dropping substantially between major versions. Build tooling is obuild for bundling the dist output, oxlint/oxfmt (Rust-based) for linting/formatting instead of ESLint/Prettier, vitest with v8 coverage for tests, and a CI matrix (GitHub Actions) running lint/build/type-check on Ubuntu plus a Windows-only vitest pass to catch path-separator regressions.
Code Quality
Tests live under test/ and are fixture-driven rather than mock-heavy: loader.test.ts exercises loadConfig/watchConfig against real files under test/fixture/ (JS/TS/JSON/JSONC/JSON5/YAML/TOML variants, a node_modules-resolved npm-style layer, nested .config directories, array-exporting configs), global-rc.test.ts and loader-giget.test.ts isolate the RC and remote-extends paths, and dotenv.test.ts/update.test.ts cover the other two modules — coverage is measured via vitest’s v8 reporter and enforced in CI on both Ubuntu and Windows, the latter specifically to catch path-normalization regressions given the codebase’s explicit path-normalization helpers. Error handling favors explicit, actionable messages over silent failure: resolveConfig throws with an install hint when giget or jiti aren’t present as peer deps, though extendConfig currently logs a warning rather than throwing on an unresolvable extends source, a behavior the code itself flags as pending a breaking-change fix. Typing is strict throughout and generics thread the user config shape and meta shape through nearly every exported function so consumers get typed loadConfig results; naming is consistent and low-abbreviation, and linting/formatting is enforced via an automated bot that pushes lint fixes back to pull requests.
API Design
The public surface is intentionally small — loadConfig, watchConfig, loadDotenv, setupDotenv, updateConfig, createDefineConfig, and a SUPPORTED_EXTENSIONS constant — and every loader function accepts a single options object rather than positional arguments, so adding a new option is additive and non-breaking. Getting started requires zero config: calling loadConfig with an empty options object immediately resolves a merged config object from whatever sources exist in the working directory, and createDefineConfig lets a downstream framework generate its own typed defineConfig helper in one line, with a reserved $meta namespace that’s automatically stripped from the resolved config and exposed separately as layer metadata — a small but effective convention for separating options for the loader from options about a layer. Documentation is thorough for a library this size: the README documents every option individually with runnable examples (dotenv interpolation, secret-file expansion, environment-specific overrides, remote extends with auth), and a separate contributor-facing document covers internal architecture, the dependency table, and code conventions.
Used by 4 apps in this directory
Dify
No Code Platforms · AI Development · Developer Tools
Visual LLM workflow platform with RAG pipelines, agent capabilities, and model management for building production AI applications.
Papra
Bookmarks Archiving
Self-hosted document archiving with email ingestion, OCR full-text search, and pluggable storage — store once, find anything.
Trigger.dev
Automation · AI Development · Developer Tools
Build and deploy fully-managed AI agents and background jobs in TypeScript — with no timeouts, durable retries, real-time observability, and elastic scaling built in.
Unlighthouse
Developer Tools
An open-source CLI that scans your entire website with Google Lighthouse, using smart sampling and a modern dashboard UI to audit every page instead of one URL at a time.