c12 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@ c12 (pronounced as /siːtwelv/, like c-twelve) is a smart configuration loader.
14
14
 
15
15
  - `.js`, `.ts`, `.mjs`, `.cjs`, `.mts`, `.cts` `.json` config loader with [unjs/jiti](https://jiti.unjs.io)
16
16
  - `.jsonc`, `.json5`, `.yaml`, `.yml`, `.toml` config loader with [unjs/confbox](https://confbox.unjs.io)
17
- - `.config/` directory support following [config dir proposal](https://github.com/pi0/config-dir)
17
+ - `.config/` directory support ([config dir proposal](https://github.com/pi0/config-dir))
18
18
  - `.rc` config support with [unjs/rc9](https://github.com/unjs/rc9)
19
19
  - `.env` support with [dotenv](https://www.npmjs.com/package/dotenv)
20
20
  - Multiple sources merged with [unjs/defu](https://github.com/unjs/defu)
@@ -26,43 +26,24 @@ c12 (pronounced as /siːtwelv/, like c-twelve) is a smart configuration loader.
26
26
 
27
27
  ## 🦴 Used by
28
28
 
29
- - [Nuxt](https://nuxt.com/)
30
- - [Nitro](https://nitro.build/)
31
- - [Unbuild](https://unbuild.unjs.io)
32
- - [Automd](https://automd.unjs.io)
33
- - [Changelogen](https://changelogen.unjs.io)
34
- - [RemixKit](https://github.com/jrestall/remix-kit)
35
29
  - [Hey API](https://github.com/hey-api/openapi-ts)
36
- - [kysely-ctl](https://github.com/kysely-org/kysely-ctl)
30
+ - [Kysely](https://github.com/kysely-org/kysely-ctl)
31
+ - [Nitro](https://nitro.build/)
32
+ - [Nuxt](https://nuxt.com/)
33
+ - [Prisma](https://github.com/prisma/prisma)
34
+ - [Trigger.dev](https://github.com/triggerdotdev/trigger.dev)
35
+ - [UnJS](https://github.com/unjs)
36
+ - [WXT](https://github.com/wxt-dev/wxt)
37
37
 
38
38
  ## Usage
39
39
 
40
40
  Install package:
41
41
 
42
- <!-- automd:pm-install -->
43
-
44
42
  ```sh
45
43
  # ✨ Auto-detect
46
44
  npx nypm install c12
47
-
48
- # npm
49
- npm install c12
50
-
51
- # yarn
52
- yarn add c12
53
-
54
- # pnpm
55
- pnpm install c12
56
-
57
- # bun
58
- bun install c12
59
-
60
- # deno
61
- deno install c12
62
45
  ```
63
46
 
64
- <!-- /automd -->
65
-
66
47
  Import:
67
48
 
68
49
  ```js
@@ -121,7 +102,42 @@ Load RC config from the workspace directory and the user's home directory. Only
121
102
 
122
103
  ### `dotenv`
123
104
 
124
- Loads `.env` file if enabled. It is disabled by default.
105
+ Loads `.env` file when `true` or an options object is passed. It is disabled by default.
106
+
107
+ Supports loading multiple files that extend eachother in left-to-right order when a `fileName`s array of relative/absolute paths is passed in the options object.
108
+
109
+ **Example:**
110
+
111
+ ```ini
112
+ # .env
113
+ CONNECTION_POOL_MAX="10"
114
+ DATABASE_URL="<...rds...>"
115
+ ```
116
+
117
+ ```ini
118
+ # .env.local
119
+ DATABASE_URL="<...localhost...>"
120
+ ```
121
+
122
+ ```js
123
+ export default {
124
+ connectionPoolMax: process.env.CONNECTION_POOL_MAX,
125
+ databaseURL: process.env.DATABASE_URL,
126
+ };
127
+ ```
128
+
129
+ ```ts
130
+ import { loadConfig } from "c12";
131
+
132
+ const config = await loadConfig({
133
+ dotenv: {
134
+ fileName: [".env", ".env.local"],
135
+ },
136
+ });
137
+
138
+ console.log(config.config.connectionPoolMax); // "10"
139
+ console.log(config.config.databaseURL); // "<...localhost...>"
140
+ ```
125
141
 
126
142
  ### `packageJson`
127
143
 
@@ -171,10 +187,17 @@ Environment name used for [environment specific configuration](#environment-spec
171
187
 
172
188
  The default is `process.env.NODE_ENV`. You can set `envName` to `false` or an empty string to disable the feature.
173
189
 
190
+ ### `context`
191
+
192
+ Context object passed to dynamic config functions.
193
+
174
194
  ### `resolve`
175
195
 
176
196
  You can define a custom function that resolves the config.
177
197
 
198
+ ### `configFileRequired`
199
+
200
+ If this option is set to `true`, loader fails if the main config file does not exists.
178
201
 
179
202
  ## Extending configuration
180
203
 
@@ -386,30 +409,11 @@ Update or create a new configuration files.
386
409
 
387
410
  Add `magicast` peer dependency:
388
411
 
389
- <!-- automd:pm-install name="magicast" dev -->
390
-
391
412
  ```sh
392
413
  # ✨ Auto-detect
393
414
  npx nypm install -D magicast
394
-
395
- # npm
396
- npm install -D magicast
397
-
398
- # yarn
399
- yarn add -D magicast
400
-
401
- # pnpm
402
- pnpm install -D magicast
403
-
404
- # bun
405
- bun install -D magicast
406
-
407
- # deno
408
- deno install --dev magicast
409
415
  ```
410
416
 
411
- <!-- /automd -->
412
-
413
417
  Import util from `c12/update`
414
418
 
415
419
  ```js
@@ -430,6 +434,28 @@ const { configFile, created } = await updateConfig({
430
434
  console.log(`Config file ${created ? "created" : "updated"} in ${configFile}`);
431
435
  ```
432
436
 
437
+ ## Configuration functions
438
+
439
+ You can use a function to define your configuration dynamically based on context.
440
+
441
+ ```ts
442
+ // config.ts
443
+ export default (ctx) => {
444
+ return {
445
+ apiUrl: ctx?.dev ? "http://localhost:3000" : "https://api.example.com",
446
+ };
447
+ };
448
+ ```
449
+
450
+ ```ts
451
+ // Usage
452
+ import { loadConfig } from "c12";
453
+
454
+ const config = await loadConfig({
455
+ context: { dev: true },
456
+ });
457
+ ```
458
+
433
459
  ## Contribution
434
460
 
435
461
  <details>
package/dist/index.d.mts CHANGED
@@ -6,13 +6,16 @@ import { diff } from 'ohash/utils';
6
6
  interface DotenvOptions {
7
7
  /**
8
8
  * The project root directory (either absolute or relative to the current working directory).
9
+ *
10
+ * Defaults to `options.cwd` in `loadConfig` context, or `process.cwd()` when used as standalone.
9
11
  */
10
- cwd: string;
12
+ cwd?: string;
11
13
  /**
12
- * What file to look in for environment variables (either absolute or relative
14
+ * What file or files to look in for environment variables (either absolute or relative
13
15
  * to the current working directory). For example, `.env`.
16
+ * With the array type, the order enforce the env loading priority (last one overrides).
14
17
  */
15
- fileName?: string;
18
+ fileName?: string | string[];
16
19
  /**
17
20
  * Whether to interpolate variables within .env.
18
21
  *
@@ -92,8 +95,12 @@ interface ResolvedConfig<T extends UserInputConfig = UserInputConfig, MT extends
92
95
  config: T;
93
96
  layers?: ConfigLayer<T, MT>[];
94
97
  cwd?: string;
98
+ _configFile?: string;
95
99
  }
96
100
  type ConfigSource = "overrides" | "main" | "rc" | "packageJson" | "defaultConfig";
101
+ interface ConfigFunctionContext {
102
+ [key: string]: any;
103
+ }
97
104
  interface ResolvableConfigContext<T extends UserInputConfig = UserInputConfig> {
98
105
  configs: Record<ConfigSource, T | null | undefined>;
99
106
  rawConfigs: Record<ConfigSource, ResolvableConfig<T> | null | undefined>;
@@ -113,6 +120,8 @@ interface LoadConfigOptions<T extends UserInputConfig = UserInputConfig, MT exte
113
120
  defaultConfig?: ResolvableConfig<T>;
114
121
  overrides?: ResolvableConfig<T>;
115
122
  omit$Keys?: boolean;
123
+ /** Context passed to config functions */
124
+ context?: ConfigFunctionContext;
116
125
  resolve?: (id: string, options: LoadConfigOptions<T, MT>) => null | undefined | ResolvedConfig<T, MT> | Promise<ResolvedConfig<T, MT> | undefined | null>;
117
126
  jiti?: Jiti;
118
127
  jitiOptions?: JitiOptions;
@@ -121,6 +130,7 @@ interface LoadConfigOptions<T extends UserInputConfig = UserInputConfig, MT exte
121
130
  extend?: false | {
122
131
  extendKey?: string | string[];
123
132
  };
133
+ configFileRequired?: boolean;
124
134
  }
125
135
  type DefineConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> = (input: InputConfig<T, MT>) => InputConfig<T, MT>;
126
136
  declare function createDefineConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta>(): DefineConfig<T, MT>;
@@ -154,4 +164,4 @@ interface WatchConfigOptions<T extends UserInputConfig = UserInputConfig, MT ext
154
164
  declare function watchConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta>(options: WatchConfigOptions<T, MT>): Promise<ConfigWatcher<T, MT>>;
155
165
 
156
166
  export { SUPPORTED_EXTENSIONS, createDefineConfig, loadConfig, loadDotenv, setupDotenv, watchConfig };
157
- export type { C12InputConfig, ConfigLayer, ConfigLayerMeta, ConfigSource, ConfigWatcher, DefineConfig, DotenvOptions, Env, InputConfig, LoadConfigOptions, ResolvableConfig, ResolvableConfigContext, ResolvedConfig, SourceOptions, UserInputConfig, WatchConfigOptions };
167
+ export type { C12InputConfig, ConfigFunctionContext, ConfigLayer, ConfigLayerMeta, ConfigSource, ConfigWatcher, DefineConfig, DotenvOptions, Env, InputConfig, LoadConfigOptions, ResolvableConfig, ResolvableConfigContext, ResolvedConfig, SourceOptions, UserInputConfig, WatchConfigOptions };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { l as loadConfig, S as SUPPORTED_EXTENSIONS } from './shared/c12.BXpNC6YI.mjs';
2
- export { a as loadDotenv, s as setupDotenv } from './shared/c12.BXpNC6YI.mjs';
1
+ import { l as loadConfig, S as SUPPORTED_EXTENSIONS } from './shared/c12.Bzgyhsy6.mjs';
2
+ export { a as loadDotenv, s as setupDotenv } from './shared/c12.Bzgyhsy6.mjs';
3
3
  import { debounce } from 'perfect-debounce';
4
4
  import { resolve } from 'pathe';
5
5
  import 'node:fs';
@@ -31,10 +31,16 @@ async function setupDotenv(options) {
31
31
  }
32
32
  async function loadDotenv(options) {
33
33
  const environment = /* @__PURE__ */ Object.create(null);
34
- const dotenvFile = resolve(options.cwd, options.fileName);
34
+ const cwd = resolve(options.cwd || ".");
35
+ const _fileName = options.fileName || ".env";
36
+ const dotenvFiles = typeof _fileName === "string" ? [_fileName] : _fileName;
35
37
  const dotenvVars = getDotEnvVars(options.env || {});
36
38
  Object.assign(environment, options.env);
37
- if (statSync(dotenvFile, { throwIfNoEntry: false })?.isFile()) {
39
+ for (const file of dotenvFiles) {
40
+ const dotenvFile = resolve(cwd, file);
41
+ if (!statSync(dotenvFile, { throwIfNoEntry: false })?.isFile()) {
42
+ continue;
43
+ }
38
44
  const parsed = dotenv.parse(await promises.readFile(dotenvFile, "utf8"));
39
45
  for (const key in parsed) {
40
46
  if (key in environment && !dotenvVars.has(key)) {
@@ -144,7 +150,8 @@ async function loadConfig(options) {
144
150
  config: {},
145
151
  cwd: options.cwd,
146
152
  configFile: resolve(options.cwd, options.configFile),
147
- layers: []
153
+ layers: [],
154
+ _configFile: void 0
148
155
  };
149
156
  const rawConfigs = {
150
157
  overrides: options.overrides,
@@ -163,6 +170,7 @@ async function loadConfig(options) {
163
170
  if (_mainConfig.configFile) {
164
171
  rawConfigs.main = _mainConfig.config;
165
172
  r.configFile = _mainConfig.configFile;
173
+ r._configFile = _mainConfig._configFile;
166
174
  }
167
175
  if (_mainConfig.meta) {
168
176
  r.meta = _mainConfig.meta;
@@ -194,18 +202,22 @@ async function loadConfig(options) {
194
202
  const value = rawConfigs[key];
195
203
  configs[key] = await (typeof value === "function" ? value({ configs, rawConfigs }) : value);
196
204
  }
197
- r.config = _merger(
198
- configs.overrides,
199
- configs.main,
200
- configs.rc,
201
- configs.packageJson,
202
- configs.defaultConfig
203
- );
204
- if (options.extend) {
205
- await extendConfig(r.config, options);
206
- r.layers = r.config._layers;
207
- delete r.config._layers;
208
- r.config = _merger(r.config, ...r.layers.map((e) => e.config));
205
+ if (Array.isArray(configs.main)) {
206
+ r.config = configs.main;
207
+ } else {
208
+ r.config = _merger(
209
+ configs.overrides,
210
+ configs.main,
211
+ configs.rc,
212
+ configs.packageJson,
213
+ configs.defaultConfig
214
+ );
215
+ if (options.extend) {
216
+ await extendConfig(r.config, options);
217
+ r.layers = r.config._layers;
218
+ delete r.config._layers;
219
+ r.config = _merger(r.config, ...r.layers.map((e) => e.config));
220
+ }
209
221
  }
210
222
  const baseLayers = [
211
223
  configs.overrides && {
@@ -231,6 +243,9 @@ async function loadConfig(options) {
231
243
  }
232
244
  }
233
245
  }
246
+ if (options.configFileRequired && !r._configFile) {
247
+ throw new Error(`Required config (${r.configFile}) cannot be resolved.`);
248
+ }
234
249
  return r;
235
250
  }
236
251
  async function extendConfig(config, options) {
@@ -295,6 +310,7 @@ const GIGET_PREFIXES = [
295
310
  ];
296
311
  const NPM_PACKAGE_RE = /^(@[\da-z~-][\d._a-z~-]*\/)?[\da-z~-][\d._a-z~-]*($|\/.*)/;
297
312
  async function resolveConfig(source, options, sourceOptions = {}) {
313
+ const originalSource = source;
298
314
  if (options.resolve) {
299
315
  const res2 = await options.resolve(source, options);
300
316
  if (res2) {
@@ -352,10 +368,11 @@ async function resolveConfig(source, options, sourceOptions = {}) {
352
368
  res.configFile = tryResolve(resolve(cwd, source), options) || tryResolve(
353
369
  resolve(cwd, ".config", source.replace(/\.config$/, "")),
354
370
  options
355
- ) || tryResolve(resolve(cwd, ".config", source), options) || source;
371
+ ) || tryResolve(resolve(cwd, ".config", source), options) || tryResolve(resolve(options.cwd || cwd, originalSource), options) || source;
356
372
  if (!existsSync(res.configFile)) {
357
373
  return res;
358
374
  }
375
+ res._configFile = res.configFile;
359
376
  const configFileExt = extname(res.configFile) || "";
360
377
  if (configFileExt in ASYNC_LOADERS) {
361
378
  const asyncLoader = await ASYNC_LOADERS[configFileExt]();
@@ -367,7 +384,7 @@ async function resolveConfig(source, options, sourceOptions = {}) {
367
384
  });
368
385
  }
369
386
  if (typeof res.config === "function") {
370
- res.config = await res.config();
387
+ res.config = await res.config(options.context);
371
388
  }
372
389
  if (options.envName) {
373
390
  const envConfig = {
package/dist/update.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { resolveModulePath } from 'exsolve';
2
- import { S as SUPPORTED_EXTENSIONS } from './shared/c12.BXpNC6YI.mjs';
2
+ import { S as SUPPORTED_EXTENSIONS } from './shared/c12.Bzgyhsy6.mjs';
3
3
  import { join, normalize } from 'pathe';
4
4
  import { mkdir, writeFile, readFile } from 'node:fs/promises';
5
5
  import { dirname, extname } from 'node:path';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c12",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "description": "Smart Config Loader",
5
5
  "repository": "unjs/c12",
6
6
  "license": "MIT",
@@ -34,28 +34,28 @@
34
34
  "chokidar": "^4.0.3",
35
35
  "confbox": "^0.2.2",
36
36
  "defu": "^6.1.4",
37
- "dotenv": "^16.6.1",
37
+ "dotenv": "^17.2.2",
38
38
  "exsolve": "^1.0.7",
39
39
  "giget": "^2.0.0",
40
- "jiti": "^2.4.2",
40
+ "jiti": "^2.5.1",
41
41
  "ohash": "^2.0.11",
42
42
  "pathe": "^2.0.3",
43
- "perfect-debounce": "^1.0.0",
44
- "pkg-types": "^2.2.0",
43
+ "perfect-debounce": "^2.0.0",
44
+ "pkg-types": "^2.3.0",
45
45
  "rc9": "^2.1.2"
46
46
  },
47
47
  "devDependencies": {
48
- "@types/node": "^24.0.13",
48
+ "@types/node": "^24.4.0",
49
49
  "@vitest/coverage-v8": "^3.2.4",
50
50
  "automd": "^0.4.0",
51
51
  "changelogen": "^0.6.2",
52
- "eslint": "^9.31.0",
52
+ "eslint": "^9.35.0",
53
53
  "eslint-config-unjs": "^0.5.0",
54
54
  "expect-type": "^1.2.2",
55
55
  "magicast": "^0.3.5",
56
56
  "prettier": "^3.6.2",
57
- "typescript": "^5.8.3",
58
- "unbuild": "^3.5.0",
57
+ "typescript": "^5.9.2",
58
+ "unbuild": "^3.6.1",
59
59
  "vitest": "^3.2.4"
60
60
  },
61
61
  "peerDependencies": {
@@ -66,5 +66,5 @@
66
66
  "optional": true
67
67
  }
68
68
  },
69
- "packageManager": "pnpm@10.13.1"
69
+ "packageManager": "pnpm@10.16.1"
70
70
  }