c12 3.3.3 → 4.0.0-beta.2

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
@@ -12,11 +12,11 @@ c12 (pronounced as /siːtwelv/, like c-twelve) is a smart configuration loader.
12
12
 
13
13
  ## ✅ Features
14
14
 
15
- - `.js`, `.ts`, `.mjs`, `.cjs`, `.mts`, `.cts` `.json` config loader with [unjs/jiti](https://jiti.unjs.io)
15
+ - `.js`, `.ts`, `.mjs`, `.cjs`, `.mts`, `.cts` `.json` config loader with customizable import or [unjs/jiti](https://jiti.unjs.io) fallback.
16
16
  - `.jsonc`, `.json5`, `.yaml`, `.yml`, `.toml` config loader with [unjs/confbox](https://confbox.unjs.io)
17
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
- - `.env` support with [dotenv](https://www.npmjs.com/package/dotenv)
19
+ - `.env` support with variable interpolation and `_FILE` references resolution
20
20
  - Multiple sources merged with [unjs/defu](https://github.com/unjs/defu)
21
21
  - Reads config from the nearest `package.json` file
22
22
  - [Extends configurations](https://github.com/unjs/c12#extending-configuration) from multiple local or git sources
@@ -139,6 +139,25 @@ console.log(config.config.connectionPoolMax); // "10"
139
139
  console.log(config.config.databaseURL); // "<...localhost...>"
140
140
  ```
141
141
 
142
+ #### `expandFileReferences`
143
+
144
+ Enabled by default. Environment variables ending with `_FILE` are resolved by reading the file at the specified path and assigning its trimmed content to the base key (without the `_FILE` suffix). This is useful for container secrets (e.g. Docker, Kubernetes) where sensitive values are mounted as files. Set to `false` to disable.
145
+
146
+ ```ini
147
+ # .env
148
+ DATABASE_PASSWORD_FILE="/run/secrets/db_password"
149
+ ```
150
+
151
+ ```ts
152
+ import { loadConfig } from "c12";
153
+
154
+ const config = await loadConfig({
155
+ dotenv: true,
156
+ });
157
+
158
+ // DATABASE_PASSWORD is now set to the contents of /run/secrets/db_password
159
+ ```
160
+
142
161
  ### `packageJson`
143
162
 
144
163
  Loads config from nearest `package.json` file. It is disabled by default.
@@ -163,13 +182,35 @@ Specify override configuration. It has the **highest** priority and is applied *
163
182
 
164
183
  Exclude environment-specific and built-in keys start with `$` in the resolved config. The default is `false`.
165
184
 
166
- ### `jiti`
185
+ ### `import`
167
186
 
168
- Custom [unjs/jiti](https://github.com/unjs/jiti) instance used to import configuration files.
187
+ Custom import function used to load configuration files. By default, c12 uses native `import()` with [unjs/jiti](https://github.com/unjs/jiti) as fallback.
169
188
 
170
- ### `jitiOptions`
189
+ **Example:** Using a custom [jiti](https://github.com/unjs/jiti) instance with options:
171
190
 
172
- Custom [unjs/jiti](https://github.com/unjs/jiti) options to import configuration files.
191
+ ```js
192
+ import { createJiti } from "jiti";
193
+
194
+ const jiti = createJiti(import.meta.url, {
195
+ /* jiti options */
196
+ });
197
+
198
+ const { config } = await loadConfig({
199
+ import: (id) => jiti.import(id),
200
+ });
201
+ ```
202
+
203
+ ### `resolveModule`
204
+
205
+ Custom resolver for picking which export to use from the loaded module. Default: `(mod) => mod.default || mod`.
206
+
207
+ **Example:** Using a named export:
208
+
209
+ ```js
210
+ const { config } = await loadConfig({
211
+ resolveModule: (mod) => mod.myConfig,
212
+ });
213
+ ```
173
214
 
174
215
  ### `giget`
175
216
 
@@ -295,6 +336,14 @@ Layers:
295
336
 
296
337
  ## Extending config layer from remote sources
297
338
 
339
+ > [!NOTE]
340
+ > Extending from remote sources requires the [`giget`](https://giget.unjs.io) peer dependency to be installed.
341
+ >
342
+ > ```sh
343
+ > # ✨ Auto-detect
344
+ > npx nypm install giget
345
+ > ```
346
+
298
347
  You can also extend configuration from remote sources such as npm or github.
299
348
 
300
349
  In the repo, there should be a `config.ts` (or `config.{name}.ts`) file to be considered as a valid config layer.
@@ -364,6 +413,14 @@ export default {
364
413
 
365
414
  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.
366
415
 
416
+ > [!NOTE]
417
+ > Watching requires the [`chokidar`](https://github.com/paulmillr/chokidar) peer dependency to be installed.
418
+ >
419
+ > ```sh
420
+ > # ✨ Auto-detect
421
+ > npx nypm install chokidar
422
+ > ```
423
+
367
424
  ### Lifecycle hooks
368
425
 
369
426
  - `onWatch`: This function is always called when config is updated, added, or removed before attempting to reload the config.
@@ -0,0 +1,33 @@
1
+ # Licenses of Bundled Dependencies
2
+
3
+ The published artifact additionally contains code with the following licenses:
4
+ MIT
5
+
6
+ # Bundled Dependencies
7
+
8
+ ## ohash, perfect-debounce
9
+
10
+ License: MIT
11
+ Repositories: https://github.com/unjs/ohash, https://github.com/unjs/perfect-debounce
12
+
13
+ > MIT License
14
+ >
15
+ > Copyright (c) Pooya Parsa <pooya@pi0.io>
16
+ >
17
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
18
+ > of this software and associated documentation files (the "Software"), to deal
19
+ > in the Software without restriction, including without limitation the rights
20
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21
+ > copies of the Software, and to permit persons to whom the Software is
22
+ > furnished to do so, subject to the following conditions:
23
+ >
24
+ > The above copyright notice and this permission notice shall be included in all
25
+ > copies or substantial portions of the Software.
26
+ >
27
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
+ > SOFTWARE.
@@ -0,0 +1,30 @@
1
+ //#region node_modules/.pnpm/ohash@2.0.11/node_modules/ohash/dist/utils/index.d.mts
2
+ /**
3
+ * Calculates the difference between two objects and returns a list of differences.
4
+ *
5
+ * @param {any} obj1 - The first object to compare.
6
+ * @param {any} obj2 - The second object to compare.
7
+ * @param {HashOptions} [opts={}] - Configuration options for hashing the objects. See {@link HashOptions}.
8
+ * @returns {DiffEntry[]} An array with the differences between the two objects.
9
+ */
10
+ declare function diff(obj1: any, obj2: any): DiffEntry[];
11
+ declare class DiffEntry {
12
+ key: string;
13
+ type: "changed" | "added" | "removed";
14
+ newValue: DiffHashedObject;
15
+ oldValue?: DiffHashedObject | undefined;
16
+ constructor(key: string, type: "changed" | "added" | "removed", newValue: DiffHashedObject, oldValue?: DiffHashedObject | undefined);
17
+ toString(): string;
18
+ toJSON(): string;
19
+ }
20
+ declare class DiffHashedObject {
21
+ key: string;
22
+ value: any;
23
+ hash?: string | undefined;
24
+ props?: Record<string, DiffHashedObject> | undefined;
25
+ constructor(key: string, value: any, hash?: string | undefined, props?: Record<string, DiffHashedObject> | undefined);
26
+ toString(): string;
27
+ toJSON(): string;
28
+ }
29
+ //#endregion
30
+ export { diff as t };
@@ -0,0 +1,182 @@
1
+ import "node:module";
2
+ import { createHash } from "node:crypto";
3
+ var __defProp = Object.defineProperty;
4
+ var __exportAll = (all, no_symbols) => {
5
+ let target = {};
6
+ for (var name in all) __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true
9
+ });
10
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
11
+ return target;
12
+ };
13
+ function serialize(o) {
14
+ return typeof o == "string" ? `'${o}'` : new c().serialize(o);
15
+ }
16
+ const c = /* @__PURE__ */ function() {
17
+ class o {
18
+ #t = /* @__PURE__ */ new Map();
19
+ compare(t, r) {
20
+ const e = typeof t, n = typeof r;
21
+ return e === "string" && n === "string" ? t.localeCompare(r) : e === "number" && n === "number" ? t - r : String.prototype.localeCompare.call(this.serialize(t, true), this.serialize(r, true));
22
+ }
23
+ serialize(t, r) {
24
+ if (t === null) return "null";
25
+ switch (typeof t) {
26
+ case "string": return r ? t : `'${t}'`;
27
+ case "bigint": return `${t}n`;
28
+ case "object": return this.$object(t);
29
+ case "function": return this.$function(t);
30
+ }
31
+ return String(t);
32
+ }
33
+ serializeObject(t) {
34
+ const r = Object.prototype.toString.call(t);
35
+ if (r !== "[object Object]") return this.serializeBuiltInType(r.length < 10 ? `unknown:${r}` : r.slice(8, -1), t);
36
+ const e = t.constructor, n = e === Object || e === void 0 ? "" : e.name;
37
+ if (n !== "" && globalThis[n] === e) return this.serializeBuiltInType(n, t);
38
+ if (typeof t.toJSON == "function") {
39
+ const i = t.toJSON();
40
+ return n + (i !== null && typeof i == "object" ? this.$object(i) : `(${this.serialize(i)})`);
41
+ }
42
+ return this.serializeObjectEntries(n, Object.entries(t));
43
+ }
44
+ serializeBuiltInType(t, r) {
45
+ const e = this["$" + t];
46
+ if (e) return e.call(this, r);
47
+ if (typeof r?.entries == "function") return this.serializeObjectEntries(t, r.entries());
48
+ throw new Error(`Cannot serialize ${t}`);
49
+ }
50
+ serializeObjectEntries(t, r) {
51
+ const e = Array.from(r).sort((i, a) => this.compare(i[0], a[0]));
52
+ let n = `${t}{`;
53
+ for (let i = 0; i < e.length; i++) {
54
+ const [a, l] = e[i];
55
+ n += `${this.serialize(a, true)}:${this.serialize(l)}`, i < e.length - 1 && (n += ",");
56
+ }
57
+ return n + "}";
58
+ }
59
+ $object(t) {
60
+ let r = this.#t.get(t);
61
+ return r === void 0 && (this.#t.set(t, `#${this.#t.size}`), r = this.serializeObject(t), this.#t.set(t, r)), r;
62
+ }
63
+ $function(t) {
64
+ const r = Function.prototype.toString.call(t);
65
+ return r.slice(-15) === "[native code] }" ? `${t.name || ""}()[native]` : `${t.name}(${t.length})${r.replace(/\s*\n\s*/g, "")}`;
66
+ }
67
+ $Array(t) {
68
+ let r = "[";
69
+ for (let e = 0; e < t.length; e++) r += this.serialize(t[e]), e < t.length - 1 && (r += ",");
70
+ return r + "]";
71
+ }
72
+ $Date(t) {
73
+ try {
74
+ return `Date(${t.toISOString()})`;
75
+ } catch {
76
+ return "Date(null)";
77
+ }
78
+ }
79
+ $ArrayBuffer(t) {
80
+ return `ArrayBuffer[${new Uint8Array(t).join(",")}]`;
81
+ }
82
+ $Set(t) {
83
+ return `Set${this.$Array(Array.from(t).sort((r, e) => this.compare(r, e)))}`;
84
+ }
85
+ $Map(t) {
86
+ return this.serializeObjectEntries("Map", t.entries());
87
+ }
88
+ }
89
+ for (const s of [
90
+ "Error",
91
+ "RegExp",
92
+ "URL"
93
+ ]) o.prototype["$" + s] = function(t) {
94
+ return `${s}(${t})`;
95
+ };
96
+ for (const s of [
97
+ "Int8Array",
98
+ "Uint8Array",
99
+ "Uint8ClampedArray",
100
+ "Int16Array",
101
+ "Uint16Array",
102
+ "Int32Array",
103
+ "Uint32Array",
104
+ "Float32Array",
105
+ "Float64Array"
106
+ ]) o.prototype["$" + s] = function(t) {
107
+ return `${s}[${t.join(",")}]`;
108
+ };
109
+ for (const s of ["BigInt64Array", "BigUint64Array"]) o.prototype["$" + s] = function(t) {
110
+ return `${s}[${t.join("n,")}${t.length > 0 ? "n" : ""}]`;
111
+ };
112
+ return o;
113
+ }();
114
+ const e = globalThis.process?.getBuiltinModule?.("crypto")?.hash, r = "sha256", s = "base64url";
115
+ function digest(t) {
116
+ if (e) return e(r, t, s);
117
+ const o = createHash(r).update(t);
118
+ return globalThis.process?.versions?.webcontainer ? o.digest().toString(s) : o.digest(s);
119
+ }
120
+ var dist_exports = /* @__PURE__ */ __exportAll({ digest: () => digest });
121
+ var utils_exports = /* @__PURE__ */ __exportAll({ diff: () => diff });
122
+ function diff(obj1, obj2) {
123
+ return _diff(_toHashedObject(obj1), _toHashedObject(obj2));
124
+ }
125
+ function _diff(h1, h2) {
126
+ const diffs = [];
127
+ const allProps = /* @__PURE__ */ new Set([...Object.keys(h1.props || {}), ...Object.keys(h2.props || {})]);
128
+ if (h1.props && h2.props) for (const prop of allProps) {
129
+ const p1 = h1.props[prop];
130
+ const p2 = h2.props[prop];
131
+ if (p1 && p2) diffs.push(..._diff(h1.props?.[prop], h2.props?.[prop]));
132
+ else if (p1 || p2) diffs.push(new DiffEntry((p2 || p1).key, p1 ? "removed" : "added", p2, p1));
133
+ }
134
+ if (allProps.size === 0 && h1.hash !== h2.hash) diffs.push(new DiffEntry((h2 || h1).key, "changed", h2, h1));
135
+ return diffs;
136
+ }
137
+ function _toHashedObject(obj, key = "") {
138
+ if (obj && typeof obj !== "object") return new DiffHashedObject(key, obj, serialize(obj));
139
+ const props = {};
140
+ const hashes = [];
141
+ for (const _key in obj) {
142
+ props[_key] = _toHashedObject(obj[_key], key ? `${key}.${_key}` : _key);
143
+ hashes.push(props[_key].hash);
144
+ }
145
+ return new DiffHashedObject(key, obj, `{${hashes.join(":")}}`, props);
146
+ }
147
+ var DiffEntry = class {
148
+ constructor(key, type, newValue, oldValue) {
149
+ this.key = key;
150
+ this.type = type;
151
+ this.newValue = newValue;
152
+ this.oldValue = oldValue;
153
+ }
154
+ toString() {
155
+ return this.toJSON();
156
+ }
157
+ toJSON() {
158
+ switch (this.type) {
159
+ case "added": return `Added \`${this.key}\``;
160
+ case "removed": return `Removed \`${this.key}\``;
161
+ case "changed": return `Changed \`${this.key}\` from \`${this.oldValue?.toString() || "-"}\` to \`${this.newValue.toString()}\``;
162
+ }
163
+ }
164
+ };
165
+ var DiffHashedObject = class {
166
+ constructor(key, value, hash, props) {
167
+ this.key = key;
168
+ this.value = value;
169
+ this.hash = hash;
170
+ this.props = props;
171
+ }
172
+ toString() {
173
+ if (this.props) return `{${Object.keys(this.props).join(",")}}`;
174
+ else return JSON.stringify(this.value);
175
+ }
176
+ toJSON() {
177
+ const k = this.key || ".";
178
+ if (this.props) return `${k}({${Object.keys(this.props).join(",")}})`;
179
+ return `${k}(${this.value})`;
180
+ }
181
+ };
182
+ export { dist_exports as n, utils_exports as t };
@@ -0,0 +1,68 @@
1
+ const DEBOUNCE_DEFAULTS = { trailing: true };
2
+ function debounce(fn, wait = 25, options = {}) {
3
+ options = {
4
+ ...DEBOUNCE_DEFAULTS,
5
+ ...options
6
+ };
7
+ if (!Number.isFinite(wait)) throw new TypeError("Expected `wait` to be a finite number");
8
+ let leadingValue;
9
+ let timeout;
10
+ let resolveList = [];
11
+ let currentPromise;
12
+ let trailingArgs;
13
+ const applyFn = (_this, args) => {
14
+ currentPromise = _applyPromised(fn, _this, args);
15
+ currentPromise.finally(() => {
16
+ currentPromise = null;
17
+ if (options.trailing && trailingArgs && !timeout) {
18
+ const promise = applyFn(_this, trailingArgs);
19
+ trailingArgs = null;
20
+ return promise;
21
+ }
22
+ });
23
+ return currentPromise;
24
+ };
25
+ const debounced = function(...args) {
26
+ if (options.trailing) trailingArgs = args;
27
+ if (currentPromise) return currentPromise;
28
+ return new Promise((resolve) => {
29
+ const shouldCallNow = !timeout && options.leading;
30
+ clearTimeout(timeout);
31
+ timeout = setTimeout(() => {
32
+ timeout = null;
33
+ const promise = options.leading ? leadingValue : applyFn(this, args);
34
+ trailingArgs = null;
35
+ for (const _resolve of resolveList) _resolve(promise);
36
+ resolveList = [];
37
+ }, wait);
38
+ if (shouldCallNow) {
39
+ leadingValue = applyFn(this, args);
40
+ resolve(leadingValue);
41
+ } else resolveList.push(resolve);
42
+ });
43
+ };
44
+ const _clearTimeout = (timer) => {
45
+ if (timer) {
46
+ clearTimeout(timer);
47
+ timeout = null;
48
+ }
49
+ };
50
+ debounced.isPending = () => !!timeout;
51
+ debounced.cancel = () => {
52
+ _clearTimeout(timeout);
53
+ resolveList = [];
54
+ trailingArgs = null;
55
+ };
56
+ debounced.flush = () => {
57
+ _clearTimeout(timeout);
58
+ if (!trailingArgs || currentPromise) return;
59
+ const args = trailingArgs;
60
+ trailingArgs = null;
61
+ return applyFn(this, args);
62
+ };
63
+ return debounced;
64
+ }
65
+ async function _applyPromised(fn, _this, args) {
66
+ return await fn.apply(_this, args);
67
+ }
68
+ export { debounce as t };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,6 @@
1
- import { Jiti, JitiOptions } from "jiti";
1
+ import { t as diff } from "./_chunks/libs/ohash.mjs";
2
2
  import { ChokidarOptions } from "chokidar";
3
3
  import { DownloadTemplateOptions } from "giget";
4
- import { diff } from "ohash/utils";
5
4
 
6
5
  //#region src/dotenv.d.ts
7
6
  interface DotenvOptions {
@@ -32,6 +31,22 @@ interface DotenvOptions {
32
31
  * An object describing environment variables (key, value pairs).
33
32
  */
34
33
  env?: NodeJS.ProcessEnv;
34
+ /**
35
+ * Resolve `_FILE` suffixed environment variables by reading the file at the
36
+ * specified path and assigning its trimmed content to the base key.
37
+ *
38
+ * This is useful for container secrets (e.g. Docker, Kubernetes) where
39
+ * sensitive values are mounted as files.
40
+ *
41
+ * @default true
42
+ *
43
+ * @example
44
+ * ```env
45
+ * DATABASE_PASSWORD_FILE="/run/secrets/db_password"
46
+ * # resolves to DATABASE_PASSWORD=<contents of /run/secrets/db_password>
47
+ * ```
48
+ */
49
+ expandFileReferences?: boolean;
35
50
  }
36
51
  type Env = typeof process.env;
37
52
  /**
@@ -125,8 +140,10 @@ interface LoadConfigOptions<T extends UserInputConfig = UserInputConfig, MT exte
125
140
  /** Context passed to config functions */
126
141
  context?: ConfigFunctionContext;
127
142
  resolve?: (id: string, options: LoadConfigOptions<T, MT>) => null | undefined | ResolvedConfig<T, MT> | Promise<ResolvedConfig<T, MT> | undefined | null>;
128
- jiti?: Jiti;
129
- jitiOptions?: JitiOptions;
143
+ /** Custom import function used to load configuration files */
144
+ import?: (id: string) => Promise<unknown>;
145
+ /** Custom resolver for picking which export to use from the loaded module. Default: `(mod) => mod.default || mod` */
146
+ resolveModule?: (mod: any) => any;
130
147
  giget?: false | DownloadTemplateOptions;
131
148
  merger?: (...sources: Array<T | null | undefined>) => T;
132
149
  extend?: false | {
@@ -167,4 +184,4 @@ interface WatchConfigOptions<T extends UserInputConfig = UserInputConfig, MT ext
167
184
  }
168
185
  declare function watchConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta>(options: WatchConfigOptions<T, MT>): Promise<ConfigWatcher<T, MT>>;
169
186
  //#endregion
170
- export { C12InputConfig, ConfigFunctionContext, ConfigLayer, ConfigLayerMeta, ConfigSource, ConfigWatcher, DefineConfig, DotenvOptions, Env, InputConfig, LoadConfigOptions, ResolvableConfig, ResolvableConfigContext, ResolvedConfig, SUPPORTED_EXTENSIONS, SourceOptions, UserInputConfig, WatchConfigOptions, createDefineConfig, loadConfig, loadDotenv, setupDotenv, watchConfig };
187
+ export { C12InputConfig, ConfigFunctionContext, ConfigLayer, ConfigLayerMeta, ConfigSource, type ConfigWatcher, DefineConfig, type DotenvOptions, type Env, InputConfig, LoadConfigOptions, ResolvableConfig, ResolvableConfigContext, ResolvedConfig, SUPPORTED_EXTENSIONS, SourceOptions, UserInputConfig, type WatchConfigOptions, createDefineConfig, loadConfig, loadDotenv, setupDotenv, watchConfig };
package/dist/index.mjs CHANGED
@@ -1,29 +1,22 @@
1
- import { existsSync, promises, statSync } from "node:fs";
1
+ import { t as debounce } from "./_chunks/libs/perfect-debounce.mjs";
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import * as nodeUtil from "node:util";
2
4
  import { basename, dirname, extname, join, normalize, resolve } from "pathe";
3
- import * as dotenv from "dotenv";
4
5
  import { readFile, rm } from "node:fs/promises";
5
6
  import { pathToFileURL } from "node:url";
6
7
  import { homedir } from "node:os";
7
8
  import { resolveModulePath } from "exsolve";
8
- import { createJiti } from "jiti";
9
9
  import * as rc9 from "rc9";
10
10
  import { defu } from "defu";
11
11
  import { findWorkspaceDir, readPackageJSON } from "pkg-types";
12
- import { debounce } from "perfect-debounce";
13
-
14
- //#region src/dotenv.ts
15
- /**
16
- * Load and interpolate environment variables into `process.env`.
17
- * If you need more control (or access to the values), consider using `loadDotenv` instead
18
- *
19
- */
20
12
  async function setupDotenv(options) {
21
13
  const targetEnvironment = options.env ?? process.env;
22
14
  const environment = await loadDotenv({
23
15
  cwd: options.cwd,
24
16
  fileName: options.fileName ?? ".env",
25
17
  env: targetEnvironment,
26
- interpolate: options.interpolate ?? true
18
+ interpolate: options.interpolate ?? true,
19
+ expandFileReferences: options.expandFileReferences ?? true
27
20
  });
28
21
  const dotenvVars = getDotEnvVars(targetEnvironment);
29
22
  for (const key in environment) {
@@ -32,7 +25,6 @@ async function setupDotenv(options) {
32
25
  }
33
26
  return environment;
34
27
  }
35
- /** Load environment variables into an object. */
36
28
  async function loadDotenv(options) {
37
29
  const environment = Object.create(null);
38
30
  const cwd = resolve(options.cwd || ".");
@@ -43,29 +35,52 @@ async function loadDotenv(options) {
43
35
  for (const file of dotenvFiles) {
44
36
  const dotenvFile = resolve(cwd, file);
45
37
  if (!statSync(dotenvFile, { throwIfNoEntry: false })?.isFile()) continue;
46
- const parsed = dotenv.parse(await promises.readFile(dotenvFile, "utf8"));
38
+ const parsed = await readEnvFile(dotenvFile);
47
39
  for (const key in parsed) {
48
40
  if (key in environment && !dotenvVars.has(key)) continue;
49
41
  environment[key] = parsed[key];
50
42
  dotenvVars.add(key);
51
43
  }
52
44
  }
45
+ if (options.expandFileReferences !== false) {
46
+ for (const key in environment) if (key.endsWith("_FILE")) {
47
+ const targetKey = key.slice(0, -5);
48
+ if (environment[targetKey] === void 0) {
49
+ const filePath = environment[key];
50
+ if (filePath && statSync(filePath, { throwIfNoEntry: false })?.isFile()) {
51
+ environment[targetKey] = readFileSync(filePath, "utf8").trim();
52
+ dotenvVars.add(targetKey);
53
+ }
54
+ }
55
+ }
56
+ }
53
57
  if (options.interpolate) interpolate(environment);
54
58
  return environment;
55
59
  }
60
+ let _parseEnv = nodeUtil.parseEnv;
61
+ async function readEnvFile(path) {
62
+ const src = readFileSync(path, "utf8");
63
+ if (!_parseEnv) try {
64
+ const dotenv = await import("dotenv");
65
+ _parseEnv = (src) => dotenv.parse(src);
66
+ } catch {
67
+ throw new Error("Failed to parse .env file: `node:util.parseEnv` is not available and `dotenv` package is not installed. Please upgrade your runtime or install `dotenv` as a dependency.");
68
+ }
69
+ return _parseEnv(src);
70
+ }
56
71
  function interpolate(target, source = {}, parse = (v) => v) {
57
72
  function getValue(key) {
58
73
  return source[key] === void 0 ? target[key] : source[key];
59
74
  }
60
- function interpolate$1(value, parents = []) {
75
+ function interpolate(value, parents = []) {
61
76
  if (typeof value !== "string") return value;
62
77
  return parse((value.match(/(.?\${?(?:[\w:]+)?}?)/g) || []).reduce((newValue, match) => {
63
78
  const parts = /(.?)\${?([\w:]+)?}?/g.exec(match) || [];
64
79
  const prefix = parts[1];
65
- let value$1, replacePart;
80
+ let value, replacePart;
66
81
  if (prefix === "\\") {
67
82
  replacePart = parts[0] || "";
68
- value$1 = replacePart.replace(String.raw`\$`, "$");
83
+ value = replacePart.replace(String.raw`\$`, "$");
69
84
  } else {
70
85
  const key = parts[2];
71
86
  replacePart = (parts[0] || "").slice(prefix.length);
@@ -73,22 +88,19 @@ function interpolate(target, source = {}, parse = (v) => v) {
73
88
  console.warn(`Please avoid recursive environment variables ( loop: ${parents.join(" > ")} > ${key} )`);
74
89
  return "";
75
90
  }
76
- value$1 = getValue(key);
77
- value$1 = interpolate$1(value$1, [...parents, key]);
91
+ value = getValue(key);
92
+ value = interpolate(value, [...parents, key]);
78
93
  }
79
- return value$1 === void 0 ? newValue : newValue.replace(replacePart, value$1);
94
+ return value === void 0 ? newValue : newValue.replace(replacePart, value);
80
95
  }, value));
81
96
  }
82
- for (const key in target) target[key] = interpolate$1(getValue(key));
97
+ for (const key in target) target[key] = interpolate(getValue(key));
83
98
  }
84
99
  function getDotEnvVars(targetEnvironment) {
85
100
  const globalRegistry = globalThis.__c12_dotenv_vars__ ||= /* @__PURE__ */ new Map();
86
101
  if (!globalRegistry.has(targetEnvironment)) globalRegistry.set(targetEnvironment, /* @__PURE__ */ new Set());
87
102
  return globalRegistry.get(targetEnvironment);
88
103
  }
89
-
90
- //#endregion
91
- //#region src/loader.ts
92
104
  const _normalize = (p) => p?.replace(/\\/g, "/");
93
105
  const ASYNC_LOADERS = {
94
106
  ".yaml": () => import("confbox/yaml").then((r) => r.parseYAML),
@@ -122,12 +134,6 @@ async function loadConfig(options) {
122
134
  ...options.extend
123
135
  };
124
136
  const _merger = options.merger || defu;
125
- options.jiti = options.jiti || createJiti(join(options.cwd, options.configFile), {
126
- interopDefault: true,
127
- moduleCache: false,
128
- extensions: [...SUPPORTED_EXTENSIONS],
129
- ...options.jitiOptions
130
- });
131
137
  const r = {
132
138
  config: {},
133
139
  cwd: options.cwd,
@@ -274,15 +280,17 @@ const GIGET_PREFIXES = [
274
280
  const NPM_PACKAGE_RE = /^(@[\da-z~-][\d._a-z~-]*\/)?[\da-z~-][\d._a-z~-]*($|\/.*)/;
275
281
  async function resolveConfig(source, options, sourceOptions = {}) {
276
282
  if (options.resolve) {
277
- const res$1 = await options.resolve(source, options);
278
- if (res$1) return res$1;
283
+ const res = await options.resolve(source, options);
284
+ if (res) return res;
279
285
  }
280
286
  const _merger = options.merger || defu;
281
287
  const customProviderKeys = Object.keys(sourceOptions.giget?.providers || {}).map((key) => `${key}:`);
282
288
  const gigetPrefixes = customProviderKeys.length > 0 ? [...new Set([...customProviderKeys, ...GIGET_PREFIXES])] : GIGET_PREFIXES;
283
289
  if (options.giget !== false && gigetPrefixes.some((prefix) => source.startsWith(prefix))) {
284
- const { downloadTemplate } = await import("giget");
285
- const { digest } = await import("ohash");
290
+ const { downloadTemplate } = await import("giget").catch((error) => {
291
+ throw new Error(`Extending config from \`${source}\` requires \`giget\` peer dependency to be installed.\n\nInstall it with: \`npx nypm i giget\``, { cause: error });
292
+ });
293
+ const { digest } = await import("./_chunks/libs/ohash.mjs").then((n) => n.n);
286
294
  const cloneName = source.replace(/\W+/g, "_").split("_").splice(0, 3).join("_") + "_" + digest(source).slice(0, 10).replace(/[-_]/g, "");
287
295
  let cloneDir;
288
296
  const localNodeModules = resolve(options.cwd, "node_modules");
@@ -317,7 +325,22 @@ async function resolveConfig(source, options, sourceOptions = {}) {
317
325
  res._configFile = res.configFile;
318
326
  const configFileExt = extname(res.configFile) || "";
319
327
  if (configFileExt in ASYNC_LOADERS) res.config = (await ASYNC_LOADERS[configFileExt]())(await readFile(res.configFile, "utf8"));
320
- else res.config = await options.jiti.import(res.configFile, { default: true });
328
+ else {
329
+ const _resolveModule = options.resolveModule || ((mod) => mod.default || mod);
330
+ if (options.import) res.config = _resolveModule(await options.import(res.configFile));
331
+ else res.config = await import(res.configFile).then(_resolveModule, async (error) => {
332
+ const { createJiti } = await import("jiti").catch(() => {
333
+ throw new Error(`Failed to load config file \`${res.configFile}\`: ${error?.message}. Hint install \`jiti\` for compatibility.`, { cause: error });
334
+ });
335
+ const jiti = createJiti(join(options.cwd || ".", options.configFile || "/"), {
336
+ interopDefault: true,
337
+ moduleCache: false,
338
+ extensions: [...SUPPORTED_EXTENSIONS]
339
+ });
340
+ options.import = (id) => jiti.import(id);
341
+ return _resolveModule(await options.import(res.configFile));
342
+ });
343
+ }
321
344
  if (typeof res.config === "function") res.config = await res.config(options.context);
322
345
  if (options.envName) {
323
346
  const envConfig = {
@@ -343,15 +366,9 @@ function tryResolve(id, options) {
343
366
  });
344
367
  return res ? normalize(res) : void 0;
345
368
  }
346
-
347
- //#endregion
348
- //#region src/types.ts
349
369
  function createDefineConfig() {
350
370
  return (input) => input;
351
371
  }
352
-
353
- //#endregion
354
- //#region src/watch.ts
355
372
  const eventMap = {
356
373
  add: "created",
357
374
  change: "updated",
@@ -372,7 +389,7 @@ async function watchConfig(options) {
372
389
  options.packageJson && resolve(l.cwd, "package.json")
373
390
  ]).filter(Boolean))];
374
391
  const watch = await import("chokidar").then((r) => r.watch || r.default || r);
375
- const { diff } = await import("ohash/utils");
392
+ const { diff } = await import("./_chunks/libs/ohash.mjs").then((n) => n.t);
376
393
  const _fswatcher = watch(watchingFiles, {
377
394
  ignoreInitial: true,
378
395
  ...options.chokidarOptions
@@ -414,6 +431,4 @@ async function watchConfig(options) {
414
431
  return config[prop];
415
432
  } });
416
433
  }
417
-
418
- //#endregion
419
- export { SUPPORTED_EXTENSIONS, createDefineConfig, loadConfig, loadDotenv, setupDotenv, watchConfig };
434
+ export { SUPPORTED_EXTENSIONS, createDefineConfig, loadConfig, loadDotenv, setupDotenv, watchConfig };
package/dist/update.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import * as magicast0 from "magicast";
1
+ import * as magicast from "magicast";
2
2
 
3
3
  //#region src/update.d.ts
4
4
  /**
@@ -10,7 +10,7 @@ interface UpdateConfigResult {
10
10
  created?: boolean;
11
11
  }
12
12
  type MaybePromise<T> = T | Promise<T>;
13
- type MagicAstOptions = Exclude<Parameters<(typeof magicast0)["parseModule"]>[1], undefined>;
13
+ type MagicAstOptions = Exclude<Parameters<(typeof magicast)["parseModule"]>[1], undefined>;
14
14
  interface UpdateConfigOptions {
15
15
  /**
16
16
  * Current working directory
package/dist/update.mjs CHANGED
@@ -4,13 +4,10 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import "node:url";
5
5
  import "node:os";
6
6
  import { join, normalize } from "pathe";
7
- import "jiti";
8
7
  import "rc9";
9
8
  import "defu";
10
9
  import "pkg-types";
11
10
  import { dirname, extname } from "node:path";
12
-
13
- //#region src/loader.ts
14
11
  const SUPPORTED_EXTENSIONS = Object.freeze([
15
12
  ".js",
16
13
  ".ts",
@@ -25,9 +22,6 @@ const SUPPORTED_EXTENSIONS = Object.freeze([
25
22
  ".yml",
26
23
  ".toml"
27
24
  ]);
28
-
29
- //#endregion
30
- //#region src/update.ts
31
25
  const UPDATABLE_EXTS = [
32
26
  ".js",
33
27
  ".ts",
@@ -36,9 +30,6 @@ const UPDATABLE_EXTS = [
36
30
  ".mts",
37
31
  ".cts"
38
32
  ];
39
- /**
40
- * @experimental Update a config file or create a new one.
41
- */
42
33
  async function updateConfig(opts) {
43
34
  const { parseModule } = await import("magicast");
44
35
  let configFile = tryResolve(`./${opts.configFile}`, opts.cwd, SUPPORTED_EXTENSIONS) || tryResolve(`./.config/${opts.configFile}`, opts.cwd, SUPPORTED_EXTENSIONS) || tryResolve(`./.config/${opts.configFile.split(".")[0]}`, opts.cwd, SUPPORTED_EXTENSIONS);
@@ -75,6 +66,4 @@ function tryResolve(path, cwd, extensions) {
75
66
  });
76
67
  return res ? normalize(res) : void 0;
77
68
  }
78
-
79
- //#endregion
80
- export { updateConfig };
69
+ export { updateConfig };
package/package.json CHANGED
@@ -1,69 +1,80 @@
1
1
  {
2
2
  "name": "c12",
3
- "version": "3.3.3",
3
+ "version": "4.0.0-beta.2",
4
4
  "description": "Smart Config Loader",
5
- "repository": "unjs/c12",
6
5
  "license": "MIT",
7
- "sideEffects": false,
8
- "type": "module",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.mts",
12
- "default": "./dist/index.mjs"
13
- },
14
- "./update": {
15
- "types": "./dist/update.d.mts",
16
- "default": "./dist/update.mjs"
17
- }
18
- },
19
- "types": "./dist/index.d.mts",
6
+ "repository": "unjs/c12",
20
7
  "files": [
21
8
  "dist"
22
9
  ],
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "types": "./dist/index.d.mts",
13
+ "exports": {
14
+ ".": "./dist/index.mjs",
15
+ "./update": "./dist/update.mjs"
16
+ },
23
17
  "scripts": {
24
18
  "build": "obuild",
25
19
  "dev": "vitest dev",
26
- "lint": "eslint . && prettier -c src test",
27
- "lint:fix": "automd && eslint . --fix && prettier -w src test",
28
- "release": "pnpm build && pnpm test && changelogen --release --push --publish",
20
+ "lint": "oxlint . && oxfmt --check src test",
21
+ "lint:fix": "automd && oxlint . --fix && oxfmt src test",
22
+ "release": "pnpm test && pnpm build && changelogen --release --prerelease --publish && git push --follow-tags",
29
23
  "test": "pnpm lint && vitest run --coverage && pnpm test:types",
30
- "test:types": "tsc --noEmit"
24
+ "test:types": "tsgo --noEmit"
31
25
  },
32
26
  "dependencies": {
33
- "chokidar": "^5.0.0",
34
- "confbox": "^0.2.2",
27
+ "confbox": "^0.2.4",
35
28
  "defu": "^6.1.4",
36
- "dotenv": "^17.2.3",
37
29
  "exsolve": "^1.0.8",
38
- "giget": "^2.0.0",
39
- "jiti": "^2.6.1",
40
- "ohash": "^2.0.11",
41
30
  "pathe": "^2.0.3",
42
- "perfect-debounce": "^2.0.0",
43
31
  "pkg-types": "^2.3.0",
44
- "rc9": "^2.1.2"
32
+ "rc9": "^3.0.0"
45
33
  },
46
34
  "devDependencies": {
47
- "@types/node": "^25.0.2",
48
- "@vitest/coverage-v8": "^4.0.15",
49
- "automd": "^0.4.2",
35
+ "@types/node": "^25.2.1",
36
+ "@typescript/native-preview": "^7.0.0-dev.20260206.1",
37
+ "@vitest/coverage-v8": "^4.0.18",
38
+ "automd": "^0.4.3",
50
39
  "changelogen": "^0.6.2",
51
- "eslint": "^9.39.2",
52
- "eslint-config-unjs": "^0.5.0",
40
+ "chokidar": "^5.0.0",
41
+ "dotenv": "^17.2.4",
42
+ "eslint-config-unjs": "^0.6.2",
53
43
  "expect-type": "^1.3.0",
54
- "magicast": "^0.5.1",
55
- "obuild": "^0.4.8",
56
- "prettier": "^3.7.4",
44
+ "giget": "^3.1.2",
45
+ "jiti": "^2.6.1",
46
+ "magicast": "^0.5.2",
47
+ "obuild": "^0.4.27",
48
+ "ohash": "^2.0.11",
49
+ "oxfmt": "^0.28.0",
50
+ "oxlint": "^1.43.0",
51
+ "perfect-debounce": "^2.1.0",
57
52
  "typescript": "^5.9.3",
58
- "vitest": "^4.0.15"
53
+ "vitest": "^4.0.18"
59
54
  },
60
55
  "peerDependencies": {
56
+ "chokidar": "^5",
57
+ "dotenv": "*",
58
+ "giget": "*",
59
+ "jiti": "*",
61
60
  "magicast": "*"
62
61
  },
63
62
  "peerDependenciesMeta": {
63
+ "dotenv": {
64
+ "optional": true
65
+ },
64
66
  "magicast": {
65
67
  "optional": true
68
+ },
69
+ "chokidar": {
70
+ "optional": true
71
+ },
72
+ "jiti": {
73
+ "optional": true
74
+ },
75
+ "giget": {
76
+ "optional": true
66
77
  }
67
78
  },
68
- "packageManager": "pnpm@10.26.0"
79
+ "packageManager": "pnpm@10.28.2"
69
80
  }