c12 4.0.0-beta.1 → 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
@@ -16,7 +16,7 @@ c12 (pronounced as /siːtwelv/, like c-twelve) is a smart configuration loader.
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.
@@ -172,7 +191,9 @@ Custom import function used to load configuration files. By default, c12 uses na
172
191
  ```js
173
192
  import { createJiti } from "jiti";
174
193
 
175
- const jiti = createJiti(import.meta.url, { /* jiti options */ });
194
+ const jiti = createJiti(import.meta.url, {
195
+ /* jiti options */
196
+ });
176
197
 
177
198
  const { config } = await loadConfig({
178
199
  import: (id) => jiti.import(id),
@@ -315,6 +336,14 @@ Layers:
315
336
 
316
337
  ## Extending config layer from remote sources
317
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
+
318
347
  You can also extend configuration from remote sources such as npm or github.
319
348
 
320
349
  In the repo, there should be a `config.ts` (or `config.{name}.ts`) file to be considered as a valid config layer.
package/dist/index.d.mts CHANGED
@@ -31,6 +31,22 @@ interface DotenvOptions {
31
31
  * An object describing environment variables (key, value pairs).
32
32
  */
33
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;
34
50
  }
35
51
  type Env = typeof process.env;
36
52
  /**
@@ -168,4 +184,4 @@ interface WatchConfigOptions<T extends UserInputConfig = UserInputConfig, MT ext
168
184
  }
169
185
  declare function watchConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta>(options: WatchConfigOptions<T, MT>): Promise<ConfigWatcher<T, MT>>;
170
186
  //#endregion
171
- 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,7 +1,7 @@
1
1
  import { t as debounce } from "./_chunks/libs/perfect-debounce.mjs";
2
- import { existsSync, promises, statSync } from "node:fs";
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import * as nodeUtil from "node:util";
3
4
  import { basename, dirname, extname, join, normalize, resolve } from "pathe";
4
- import * as dotenv from "dotenv";
5
5
  import { readFile, rm } from "node:fs/promises";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import { homedir } from "node:os";
@@ -15,7 +15,8 @@ async function setupDotenv(options) {
15
15
  cwd: options.cwd,
16
16
  fileName: options.fileName ?? ".env",
17
17
  env: targetEnvironment,
18
- interpolate: options.interpolate ?? true
18
+ interpolate: options.interpolate ?? true,
19
+ expandFileReferences: options.expandFileReferences ?? true
19
20
  });
20
21
  const dotenvVars = getDotEnvVars(targetEnvironment);
21
22
  for (const key in environment) {
@@ -34,16 +35,39 @@ async function loadDotenv(options) {
34
35
  for (const file of dotenvFiles) {
35
36
  const dotenvFile = resolve(cwd, file);
36
37
  if (!statSync(dotenvFile, { throwIfNoEntry: false })?.isFile()) continue;
37
- const parsed = dotenv.parse(await promises.readFile(dotenvFile, "utf8"));
38
+ const parsed = await readEnvFile(dotenvFile);
38
39
  for (const key in parsed) {
39
40
  if (key in environment && !dotenvVars.has(key)) continue;
40
41
  environment[key] = parsed[key];
41
42
  dotenvVars.add(key);
42
43
  }
43
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
+ }
44
57
  if (options.interpolate) interpolate(environment);
45
58
  return environment;
46
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
+ }
47
71
  function interpolate(target, source = {}, parse = (v) => v) {
48
72
  function getValue(key) {
49
73
  return source[key] === void 0 ? target[key] : source[key];
@@ -263,7 +287,9 @@ async function resolveConfig(source, options, sourceOptions = {}) {
263
287
  const customProviderKeys = Object.keys(sourceOptions.giget?.providers || {}).map((key) => `${key}:`);
264
288
  const gigetPrefixes = customProviderKeys.length > 0 ? [...new Set([...customProviderKeys, ...GIGET_PREFIXES])] : GIGET_PREFIXES;
265
289
  if (options.giget !== false && gigetPrefixes.some((prefix) => source.startsWith(prefix))) {
266
- const { downloadTemplate } = await import("giget");
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
+ });
267
293
  const { digest } = await import("./_chunks/libs/ohash.mjs").then((n) => n.n);
268
294
  const cloneName = source.replace(/\W+/g, "_").split("_").splice(0, 3).join("_") + "_" + digest(source).slice(0, 10).replace(/[-_]/g, "");
269
295
  let cloneDir;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c12",
3
- "version": "4.0.0-beta.1",
3
+ "version": "4.0.0-beta.2",
4
4
  "description": "Smart Config Loader",
5
5
  "license": "MIT",
6
6
  "repository": "unjs/c12",
@@ -26,9 +26,7 @@
26
26
  "dependencies": {
27
27
  "confbox": "^0.2.4",
28
28
  "defu": "^6.1.4",
29
- "dotenv": "^17.2.4",
30
29
  "exsolve": "^1.0.8",
31
- "giget": "^3.1.2",
32
30
  "pathe": "^2.0.3",
33
31
  "pkg-types": "^2.3.0",
34
32
  "rc9": "^3.0.0"
@@ -40,8 +38,10 @@
40
38
  "automd": "^0.4.3",
41
39
  "changelogen": "^0.6.2",
42
40
  "chokidar": "^5.0.0",
41
+ "dotenv": "^17.2.4",
43
42
  "eslint-config-unjs": "^0.6.2",
44
43
  "expect-type": "^1.3.0",
44
+ "giget": "^3.1.2",
45
45
  "jiti": "^2.6.1",
46
46
  "magicast": "^0.5.2",
47
47
  "obuild": "^0.4.27",
@@ -54,10 +54,15 @@
54
54
  },
55
55
  "peerDependencies": {
56
56
  "chokidar": "^5",
57
+ "dotenv": "*",
58
+ "giget": "*",
57
59
  "jiti": "*",
58
60
  "magicast": "*"
59
61
  },
60
62
  "peerDependenciesMeta": {
63
+ "dotenv": {
64
+ "optional": true
65
+ },
61
66
  "magicast": {
62
67
  "optional": true
63
68
  },
@@ -66,6 +71,9 @@
66
71
  },
67
72
  "jiti": {
68
73
  "optional": true
74
+ },
75
+ "giget": {
76
+ "optional": true
69
77
  }
70
78
  },
71
79
  "packageManager": "pnpm@10.28.2"