460 lines
12 KiB
Markdown
460 lines
12 KiB
Markdown
# ⚙️ c12
|
||
|
||
<!-- automd:badges color=yellow codecov -->
|
||
|
||
[](https://npmjs.com/package/c12)
|
||
[](https://npm.chart.dev/c12)
|
||
[](https://codecov.io/gh/unjs/c12)
|
||
|
||
<!-- /automd -->
|
||
|
||
c12 (pronounced as /siːtwelv/, like c-twelve) is a smart configuration loader.
|
||
|
||
## ✅ Features
|
||
|
||
- `.js`, `.ts`, `.mjs`, `.cjs`, `.mts`, `.cts` `.json` config loader with [unjs/jiti](https://jiti.unjs.io)
|
||
- `.jsonc`, `.json5`, `.yaml`, `.yml`, `.toml` config loader with [unjs/confbox](https://confbox.unjs.io)
|
||
- `.config/` directory support following [config dir proposal](https://github.com/pi0/config-dir)
|
||
- `.rc` config support with [unjs/rc9](https://github.com/unjs/rc9)
|
||
- `.env` support with [dotenv](https://www.npmjs.com/package/dotenv)
|
||
- Multiple sources merged with [unjs/defu](https://github.com/unjs/defu)
|
||
- Reads config from the nearest `package.json` file
|
||
- [Extends configurations](https://github.com/unjs/c12#extending-configuration) from multiple local or git sources
|
||
- Overwrite with [environment-specific configuration](#environment-specific-configuration)
|
||
- Config watcher with auto-reload and HMR support
|
||
- Create or update configuration files with [magicast](https://github.com/unjs/magicast)
|
||
|
||
## 🦴 Used by
|
||
|
||
- [Nuxt](https://nuxt.com/)
|
||
- [Nitro](https://nitro.build/)
|
||
- [Unbuild](https://unbuild.unjs.io)
|
||
- [Automd](https://automd.unjs.io)
|
||
- [Changelogen](https://changelogen.unjs.io)
|
||
- [RemixKit](https://github.com/jrestall/remix-kit)
|
||
- [Hey API](https://github.com/hey-api/openapi-ts)
|
||
- [kysely-ctl](https://github.com/kysely-org/kysely-ctl)
|
||
- [Prisma](https://github.com/prisma/prisma)
|
||
|
||
## Usage
|
||
|
||
Install package:
|
||
|
||
```sh
|
||
# ✨ Auto-detect
|
||
npx nypm install c12
|
||
```
|
||
|
||
Import:
|
||
|
||
```js
|
||
// ESM import
|
||
import { loadConfig, watchConfig } from "c12";
|
||
|
||
// or using dynamic import
|
||
const { loadConfig, watchConfig } = await import("c12");
|
||
```
|
||
|
||
Load configuration:
|
||
|
||
```js
|
||
// Get loaded config
|
||
const { config } = await loadConfig({});
|
||
|
||
// Get resolved config and extended layers
|
||
const { config, configFile, layers } = await loadConfig({});
|
||
```
|
||
|
||
## Loading priority
|
||
|
||
c12 merged config sources with [unjs/defu](https://github.com/unjs/defu) by below order:
|
||
|
||
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
|
||
|
||
## Options
|
||
|
||
### `cwd`
|
||
|
||
Resolve configuration from this working directory. The default is `process.cwd()`
|
||
|
||
### `name`
|
||
|
||
Configuration base name. The default is `config`.
|
||
|
||
### `configFile`
|
||
|
||
Configuration file name without extension. Default is generated from `name` (f.e., if `name` is `foo`, the config file will be => `foo.config`).
|
||
|
||
### `rcFile`
|
||
|
||
RC Config file name. Default is generated from `name` (name=foo => `.foorc`).
|
||
|
||
Set to `false` to disable loading RC config.
|
||
|
||
### `globalRC`
|
||
|
||
Load RC config from the workspace directory and the user's home directory. Only enabled when `rcFile` is provided. Set to `false` to disable this functionality.
|
||
|
||
### `dotenv`
|
||
|
||
Loads `.env` file if enabled. It is disabled by default.
|
||
|
||
### `packageJson`
|
||
|
||
Loads config from nearest `package.json` file. It is disabled by default.
|
||
|
||
If `true` value is passed, c12 uses `name` field from `package.json`.
|
||
|
||
You can also pass either a string or an array of strings as a value to use those fields.
|
||
|
||
### `defaults`
|
||
|
||
Specify default configuration. It has the **lowest** priority and is applied **after extending** config.
|
||
|
||
### `defaultConfig`
|
||
|
||
Specify default configuration. It is applied **before** extending config.
|
||
|
||
### `overrides`
|
||
|
||
Specify override configuration. It has the **highest** priority and is applied **before extending** config.
|
||
|
||
### `omit$Keys`
|
||
|
||
Exclude environment-specific and built-in keys start with `$` in the resolved config. The default is `false`.
|
||
|
||
### `jiti`
|
||
|
||
Custom [unjs/jiti](https://github.com/unjs/jiti) instance used to import configuration files.
|
||
|
||
### `jitiOptions`
|
||
|
||
Custom [unjs/jiti](https://github.com/unjs/jiti) options to import configuration files.
|
||
|
||
### `giget`
|
||
|
||
Options passed to [unjs/giget](https://github.com/unjs/giget) when extending layer from git source.
|
||
|
||
### `merger`
|
||
|
||
Custom options merger function. Default is [defu](https://github.com/unjs/defu).
|
||
|
||
**Note:** Custom merge function should deeply merge options with arguments high -> low priority.
|
||
|
||
### `envName`
|
||
|
||
Environment name used for [environment specific configuration](#environment-specific-configuration).
|
||
|
||
The default is `process.env.NODE_ENV`. You can set `envName` to `false` or an empty string to disable the feature.
|
||
|
||
### `context`
|
||
|
||
Context object passed to dynamic config functions.
|
||
|
||
### `resolve`
|
||
|
||
You can define a custom function that resolves the config.
|
||
|
||
### `configFileRequired`
|
||
|
||
If this option is set to `true`, loader fails if the main config file does not exists.
|
||
|
||
## Extending configuration
|
||
|
||
If resolved config contains a `extends` key, it will be used to extend the configuration.
|
||
|
||
Extending can be nested and each layer can extend from one base or more.
|
||
|
||
The final config is merged result of extended options and user options with [unjs/defu](https://github.com/unjs/defu).
|
||
|
||
Each item in extends is a string that can be either an absolute or relative path to the current config file pointing to a config file for extending or the directory containing the config file.
|
||
If it starts with either `github:`, `gitlab:`, `bitbucket:`, or `https:`, c12 automatically clones it.
|
||
|
||
For custom merging strategies, you can directly access each layer with `layers` property.
|
||
|
||
**Example:**
|
||
|
||
```js
|
||
// config.ts
|
||
export default {
|
||
colors: {
|
||
primary: "user_primary",
|
||
},
|
||
extends: ["./theme"],
|
||
};
|
||
```
|
||
|
||
```js
|
||
// config.dev.ts
|
||
export default {
|
||
dev: true,
|
||
};
|
||
```
|
||
|
||
```js
|
||
// theme/config.ts
|
||
export default {
|
||
extends: "../base",
|
||
colors: {
|
||
primary: "theme_primary",
|
||
secondary: "theme_secondary",
|
||
},
|
||
};
|
||
```
|
||
|
||
```js
|
||
// base/config.ts
|
||
export default {
|
||
colors: {
|
||
primary: "base_primary",
|
||
text: "base_text",
|
||
},
|
||
};
|
||
```
|
||
|
||
The loaded configuration would look like this:
|
||
|
||
```js
|
||
const config = {
|
||
dev: true,
|
||
colors: {
|
||
primary: "user_primary",
|
||
secondary: "theme_secondary",
|
||
text: "base_text",
|
||
},
|
||
};
|
||
```
|
||
|
||
Layers:
|
||
|
||
```js
|
||
[
|
||
{
|
||
config: {
|
||
/* theme config */
|
||
},
|
||
configFile: "/path/to/theme/config.ts",
|
||
cwd: "/path/to/theme ",
|
||
},
|
||
{
|
||
config: {
|
||
/* base config */
|
||
},
|
||
configFile: "/path/to/base/config.ts",
|
||
cwd: "/path/to/base",
|
||
},
|
||
{
|
||
config: {
|
||
/* dev config */
|
||
},
|
||
configFile: "/path/to/config.dev.ts",
|
||
cwd: "/path/",
|
||
},
|
||
];
|
||
```
|
||
|
||
## Extending config layer from remote sources
|
||
|
||
You can also extend configuration from remote sources such as npm or github.
|
||
|
||
In the repo, there should be a `config.ts` (or `config.{name}.ts`) file to be considered as a valid config layer.
|
||
|
||
**Example:** Extend from a github repository
|
||
|
||
```js
|
||
// config.ts
|
||
export default {
|
||
extends: "gh:user/repo",
|
||
};
|
||
```
|
||
|
||
**Example:** Extend from a github repository with branch and subpath
|
||
|
||
```js
|
||
// config.ts
|
||
export default {
|
||
extends: "gh:user/repo/theme#dev",
|
||
};
|
||
```
|
||
|
||
**Example:** Extend a private repository and install dependencies:
|
||
|
||
```js
|
||
// config.ts
|
||
export default {
|
||
extends: ["gh:user/repo", { auth: process.env.GITHUB_TOKEN, install: true }],
|
||
};
|
||
```
|
||
|
||
You can pass more options to `giget: {}` in layer config or disable it by setting it to `false`.
|
||
|
||
Refer to [unjs/giget](https://giget.unjs.io) for more information.
|
||
|
||
## Environment-specific configuration
|
||
|
||
Users can define environment-specific configuration using these config keys:
|
||
|
||
- `$test: {...}`
|
||
- `$development: {...}`
|
||
- `$production: {...}`
|
||
- `$env: { [env]: {...} }`
|
||
|
||
c12 tries to match [`envName`](#envname) and override environment config if specified.
|
||
|
||
**Note:** Environment will be applied when extending each configuration layer. This way layers can provide environment-specific configuration.
|
||
|
||
**Example:**
|
||
|
||
```js
|
||
export default {
|
||
// Default configuration
|
||
logLevel: "info",
|
||
|
||
// Environment overrides
|
||
$test: { logLevel: "silent" },
|
||
$development: { logLevel: "warning" },
|
||
$production: { logLevel: "error" },
|
||
$env: {
|
||
staging: { logLevel: "debug" },
|
||
},
|
||
};
|
||
```
|
||
|
||
## Watching configuration
|
||
|
||
you can use `watchConfig` instead of `loadConfig` to load config and watch for changes, add and removals in all expected configuration paths and auto reload with new config.
|
||
|
||
### Lifecycle hooks
|
||
|
||
- `onWatch`: This function is always called when config is updated, added, or removed before attempting to reload the config.
|
||
- `acceptHMR`: By implementing this function, you can compare old and new functions and return `true` if a full reload is not needed.
|
||
- `onUpdate`: This function is always called after the new config is updated. If `acceptHMR` returns true, it will be skipped.
|
||
|
||
```ts
|
||
import { watchConfig } from "c12";
|
||
|
||
const config = watchConfig({
|
||
cwd: ".",
|
||
// chokidarOptions: {}, // Default is { ignoreInitial: true }
|
||
// debounce: 200 // Default is 100. You can set it to false to disable debounced watcher
|
||
onWatch: (event) => {
|
||
console.log("[watcher]", event.type, event.path);
|
||
},
|
||
acceptHMR({ oldConfig, newConfig, getDiff }) {
|
||
const diff = getDiff();
|
||
if (diff.length === 0) {
|
||
console.log("No config changed detected!");
|
||
return true; // No changes!
|
||
}
|
||
},
|
||
onUpdate({ oldConfig, newConfig, getDiff }) {
|
||
const diff = getDiff();
|
||
console.log("Config updated:\n" + diff.map((i) => i.toJSON()).join("\n"));
|
||
},
|
||
});
|
||
|
||
console.log("watching config files:", config.watchingFiles);
|
||
console.log("initial config", config.config);
|
||
|
||
// Stop watcher when not needed anymore
|
||
// await config.unwatch();
|
||
```
|
||
|
||
## Updating config
|
||
|
||
> [!NOTE]
|
||
> This feature is experimental
|
||
|
||
Update or create a new configuration files.
|
||
|
||
Add `magicast` peer dependency:
|
||
|
||
```sh
|
||
# ✨ Auto-detect
|
||
npx nypm install -D magicast
|
||
```
|
||
|
||
Import util from `c12/update`
|
||
|
||
```js
|
||
const { configFile, created } = await updateConfig({
|
||
cwd: ".",
|
||
configFile: "foo.config",
|
||
onCreate: ({ configFile }) => {
|
||
// You can prompt user if wants to create a new config file and return false to cancel
|
||
console.log(`Creating new config file in ${configFile}...`);
|
||
return "export default { test: true }";
|
||
},
|
||
onUpdate: (config) => {
|
||
// You can update the config contents just like an object
|
||
config.test2 = false;
|
||
},
|
||
});
|
||
|
||
console.log(`Config file ${created ? "created" : "updated"} in ${configFile}`);
|
||
```
|
||
|
||
## Configuration functions
|
||
|
||
You can use a function to define your configuration dynamically based on context.
|
||
|
||
```ts
|
||
// config.ts
|
||
export default (ctx) => {
|
||
return {
|
||
apiUrl: ctx?.dev ? "http://localhost:3000" : "https://api.example.com",
|
||
};
|
||
};
|
||
```
|
||
|
||
```ts
|
||
// Usage
|
||
import { loadConfig } from "c12";
|
||
|
||
const config = await loadConfig({
|
||
context: { dev: true },
|
||
});
|
||
```
|
||
|
||
## Contribution
|
||
|
||
<details>
|
||
<summary>Local development</summary>
|
||
|
||
- Clone this repository
|
||
- Install the latest LTS version of [Node.js](https://nodejs.org/en/)
|
||
- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
|
||
- Install dependencies using `pnpm install`
|
||
- Run tests using `pnpm dev` or `pnpm test`
|
||
|
||
</details>
|
||
|
||
<!-- /automd -->
|
||
|
||
## License
|
||
|
||
<!-- automd:contributors license=MIT author="pi0" -->
|
||
|
||
Published under the [MIT](https://github.com/unjs/c12/blob/main/LICENSE) license.
|
||
Made by [@pi0](https://github.com/pi0) and [community](https://github.com/unjs/c12/graphs/contributors) 💛
|
||
<br><br>
|
||
<a href="https://github.com/unjs/c12/graphs/contributors">
|
||
<img src="https://contrib.rocks/image?repo=unjs/c12" />
|
||
</a>
|
||
|
||
<!-- /automd -->
|
||
|
||
<!-- automd:with-automd -->
|
||
|
||
---
|
||
|
||
_🤖 auto updated with [automd](https://automd.unjs.io)_
|
||
|
||
<!-- /automd -->
|