konfeeg 0.0.0 → 0.0.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 +63 -32
- package/dist/index.cjs +195 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +258 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +37 -12
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +17 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +27 -8
package/README.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Validated, strongly-typed config for Node and the browser. Define a schema once; values are resolved, coerced, and validated at startup — missing or invalid values throw immediately.
|
|
4
4
|
|
|
5
|
-
>
|
|
5
|
+
> API is inspired by [convict](https://github.com/mozilla/node-convict/tree/master/packages/convict), but this package:
|
|
6
|
+
>
|
|
7
|
+
> - works in the browser (as well as node)
|
|
8
|
+
> - supports environment-specific values without requiring use of global env vars
|
|
9
|
+
> - supports `import.meta.env` (e.g. for Vite)
|
|
6
10
|
|
|
7
11
|
---
|
|
8
12
|
|
|
@@ -11,7 +15,7 @@ Validated, strongly-typed config for Node and the browser. Define a schema once;
|
|
|
11
15
|
```ts
|
|
12
16
|
import { createEnvironmentConfig } from "konfeeg"
|
|
13
17
|
|
|
14
|
-
// 1. Declare
|
|
18
|
+
// 1. Declare the names of your environments
|
|
15
19
|
type MyEnvs = {
|
|
16
20
|
dev?: unknown // optional (?) = per-env value may be omitted
|
|
17
21
|
staging: unknown // required = must supply a value
|
|
@@ -28,6 +32,26 @@ const config = createEnvironmentConfig<MyEnvs>()("staging", {
|
|
|
28
32
|
staging: "https://staging-api.example.com",
|
|
29
33
|
production: "https://api.example.com",
|
|
30
34
|
},
|
|
35
|
+
logLevel: {
|
|
36
|
+
doc: "Minimum log level",
|
|
37
|
+
format: ["debug", "info", "warn", "error"] as const, // Must be one of these literals
|
|
38
|
+
processEnv: "LOG_LEVEL",
|
|
39
|
+
dev: "debug",
|
|
40
|
+
staging: "info",
|
|
41
|
+
production: "warn",
|
|
42
|
+
},
|
|
43
|
+
port: {
|
|
44
|
+
doc: "HTTP port to listen on",
|
|
45
|
+
format: Number, // Numeric strings (e.g. from env vars) are coerced
|
|
46
|
+
processEnv: "PORT",
|
|
47
|
+
value: 3000,
|
|
48
|
+
},
|
|
49
|
+
allowedOrigins: {
|
|
50
|
+
doc: "CORS allow-list",
|
|
51
|
+
format: Array, // Value must be an array
|
|
52
|
+
staging: ["https://staging.example.com"],
|
|
53
|
+
production: ["https://example.com", "https://admin.example.com"],
|
|
54
|
+
},
|
|
31
55
|
mongo: {
|
|
32
56
|
dbName: {
|
|
33
57
|
doc: "Mongo database name",
|
|
@@ -35,34 +59,56 @@ const config = createEnvironmentConfig<MyEnvs>()("staging", {
|
|
|
35
59
|
processEnv: "MONGO_DB_NAME",
|
|
36
60
|
value: "my-app-db", // static fallback (lowest priority)
|
|
37
61
|
},
|
|
38
|
-
|
|
39
|
-
doc: "Mongo
|
|
40
|
-
format:
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
poolSize: {
|
|
63
|
+
doc: "Max connections in the Mongo pool",
|
|
64
|
+
format: Number,
|
|
65
|
+
optional: true, // missing value resolves to `default` instead of throwing
|
|
66
|
+
default: 10,
|
|
43
67
|
},
|
|
44
68
|
},
|
|
45
69
|
})
|
|
46
70
|
|
|
47
71
|
config.env // "staging"
|
|
48
72
|
config.apiUrl // string (validated as URL)
|
|
73
|
+
config.logLevel // "debug" | "info" | "warn" | "error"
|
|
74
|
+
config.port // number
|
|
75
|
+
config.allowedOrigins // any[]
|
|
49
76
|
config.mongo.dbName // string
|
|
77
|
+
config.mongo.poolSize // number
|
|
50
78
|
```
|
|
51
79
|
|
|
80
|
+
> [!important]
|
|
81
|
+
> The above example hardcodes the active environment (`"staging"`) for clarity, but you'll want that to be dynamic in a real app:
|
|
82
|
+
>
|
|
83
|
+
> ```ts
|
|
84
|
+
> type WhichEnvsAreRequired = {
|
|
85
|
+
> local?: unknown // optional (?) = per-env value may be omitted
|
|
86
|
+
> nonprod: unknown // required = must supply a value
|
|
87
|
+
> prod: unknown
|
|
88
|
+
> }
|
|
89
|
+
>
|
|
90
|
+
> // type AppEnvironment = keyof WhichEnvsAreRequired // "local" | "nonprod" | "prod"
|
|
91
|
+
>
|
|
92
|
+
> const appEnv = import.meta.env.VITE_APP_ENV as AppEnvironment
|
|
93
|
+
> if (!appEnv) throw new Error("VITE_APP_ENV is required")
|
|
94
|
+
>
|
|
95
|
+
> const config = createEnvironmentConfig<WhichEnvsAreRequired>()(appEnv, { ... })
|
|
96
|
+
> ```
|
|
97
|
+
|
|
52
98
|
---
|
|
53
99
|
|
|
54
100
|
## Schema fields
|
|
55
101
|
|
|
56
|
-
| Field
|
|
57
|
-
|
|
|
58
|
-
| `doc`
|
|
59
|
-
| `format`
|
|
60
|
-
| `value`
|
|
61
|
-
| `processEnv`
|
|
62
|
-
| `importMetaEnv`
|
|
63
|
-
| `optional`
|
|
64
|
-
| `default`
|
|
65
|
-
| env keys (`dev`, `staging`…) | optional | Per-environment value overrides
|
|
102
|
+
| Field | Required | Description |
|
|
103
|
+
| --------------------------------- | -------- | ------------------------------------------------------------------------------------- |
|
|
104
|
+
| `doc` | required | Human-readable description |
|
|
105
|
+
| `format` | optional | Validation format — see below. If omitted, no validation is applied; the resolved type is inferred from `value`/per-env fields, or `any` if none are declared |
|
|
106
|
+
| `value` | optional | Constant shared across all environments (lowest priority) |
|
|
107
|
+
| `processEnv` | optional | `process.env` key — runtime override (highest priority) |
|
|
108
|
+
| `importMetaEnv` | optional | `import.meta.env` key — runtime override (highest priority) |
|
|
109
|
+
| `optional` | optional | When `true`, missing value will not throw. Resolves to `default` field or `undefined` |
|
|
110
|
+
| `default` | optional | Fallback to replace `undefined` when `optional: true` and no value is found |
|
|
111
|
+
| env keys (e.g. `dev`, `staging`…) | optional | Per-environment value overrides. _(These are the env names that you pass in in)_ |
|
|
66
112
|
|
|
67
113
|
---
|
|
68
114
|
|
|
@@ -93,21 +139,6 @@ When multiple sources are declared on the same entry, the highest-priority sourc
|
|
|
93
139
|
|
|
94
140
|
---
|
|
95
141
|
|
|
96
|
-
## Defining environments
|
|
97
|
-
|
|
98
|
-
Declare a plain object type. Required properties mean every entry that supplies per-env values must include that env. Optional properties (`?`) may be omitted.
|
|
99
|
-
|
|
100
|
-
```ts
|
|
101
|
-
type MyEnvs = {
|
|
102
|
-
dev?: unknown // optional — per-env value may be omitted
|
|
103
|
-
integ?: unknown
|
|
104
|
-
staging: unknown // required — every entry must supply a value
|
|
105
|
-
production: unknown
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
142
|
## Fallbacks
|
|
112
143
|
|
|
113
144
|
When an entry has no value for the active environment, resolution can fall back to another env's value. Chains are transitive. Only affects per-env resolution (priority 2) — runtime env vars still win.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/format.ts
|
|
3
|
+
function validateAndCoerce(value, format, fullKey, errors) {
|
|
4
|
+
switch (format) {
|
|
5
|
+
case String:
|
|
6
|
+
if (typeof value !== "string") errors.push(`${fullKey}: must be a string`);
|
|
7
|
+
break;
|
|
8
|
+
case Number:
|
|
9
|
+
value = Number(value);
|
|
10
|
+
if (isNaN(value)) errors.push(`${fullKey}: must be a number`);
|
|
11
|
+
break;
|
|
12
|
+
case Boolean:
|
|
13
|
+
if (typeof value !== "boolean" && value !== "true" && value !== "false" && value !== 1 && value !== 0) errors.push(`${fullKey}: must be a boolean`);
|
|
14
|
+
value = value === "true" ? true : value === "false" ? false : Boolean(value);
|
|
15
|
+
break;
|
|
16
|
+
case Array:
|
|
17
|
+
if (!Array.isArray(value)) errors.push(`${fullKey}: must be an array`);
|
|
18
|
+
break;
|
|
19
|
+
case "url":
|
|
20
|
+
try {
|
|
21
|
+
new URL(value);
|
|
22
|
+
} catch {
|
|
23
|
+
errors.push(`${fullKey}: must be a valid URL; found "${value}"`);
|
|
24
|
+
}
|
|
25
|
+
break;
|
|
26
|
+
default: if (format instanceof Array) {
|
|
27
|
+
if (!format.includes(value)) errors.push(`${fullKey}: must be one of: [${format.join(", ")}]`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/create-config.ts
|
|
34
|
+
/**
|
|
35
|
+
* Create a resolved, validated config for the given environment.
|
|
36
|
+
*
|
|
37
|
+
* Curried so the envs declaration is bound on the first call and the
|
|
38
|
+
* schema is inferred (giving you autocomplete) on the second call.
|
|
39
|
+
*
|
|
40
|
+
* @typeParam E - The envs shape describing required/optional environments.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* type MyEnvs = {
|
|
45
|
+
* dev?: unknown
|
|
46
|
+
* staging: unknown
|
|
47
|
+
* production: unknown
|
|
48
|
+
* }
|
|
49
|
+
* const config = createEnvironmentConfig<MyEnvs>()('dev', {
|
|
50
|
+
* port: { doc: 'Port', format: Number, value: 3000 },
|
|
51
|
+
* })
|
|
52
|
+
* config.port // number
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @example Fallback environments
|
|
56
|
+
* ```ts
|
|
57
|
+
* // When running in `dev`, any entry that does not declare a `dev` value
|
|
58
|
+
* // falls back to the entry's `integ` value.
|
|
59
|
+
* const config = createEnvironmentConfig<MyEnvs>()(
|
|
60
|
+
* 'dev',
|
|
61
|
+
* {
|
|
62
|
+
* apiUrl: {
|
|
63
|
+
* doc: 'API URL',
|
|
64
|
+
* format: 'url',
|
|
65
|
+
* integ: 'https://integ.example.com',
|
|
66
|
+
* staging: 'https://staging.example.com',
|
|
67
|
+
* production: 'https://api.example.com',
|
|
68
|
+
* },
|
|
69
|
+
* },
|
|
70
|
+
* { fallbacks: { dev: 'integ' } },
|
|
71
|
+
* )
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
function createEnvironmentConfig() {
|
|
75
|
+
return (env, inputConfig, options) => buildConfig(env, inputConfig, options);
|
|
76
|
+
}
|
|
77
|
+
function buildConfig(env, inputConfig, options) {
|
|
78
|
+
const errors = [];
|
|
79
|
+
const envChain = resolveFallbackChain(env, options?.fallbacks);
|
|
80
|
+
function processConfig(config, keyPrefix) {
|
|
81
|
+
const output = {};
|
|
82
|
+
for (const [key, entry] of Object.entries(config)) {
|
|
83
|
+
if (key === "env") throw new Error(`Config key "env" is reserved and cannot be used. It will already be present by default.`);
|
|
84
|
+
const fullKey = keyPrefix ? `${keyPrefix}.${key}` : key;
|
|
85
|
+
if (!("doc" in entry)) {
|
|
86
|
+
output[key] = processConfig(entry, fullKey);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const configEntry = entry;
|
|
90
|
+
let value = "value" in configEntry ? configEntry.value : void 0;
|
|
91
|
+
for (const candidateEnv of envChain) {
|
|
92
|
+
const envValue = configEntry[candidateEnv];
|
|
93
|
+
if (envValue !== void 0) {
|
|
94
|
+
value = envValue;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if ("processEnv" in configEntry) {
|
|
99
|
+
const runtimeOverride = typeof process !== "undefined" && process.env ? process.env[configEntry.processEnv] : void 0;
|
|
100
|
+
if (runtimeOverride !== void 0) value = runtimeOverride;
|
|
101
|
+
} else if ("importMetaEnv" in configEntry) {
|
|
102
|
+
const runtimeOverride = {}.env ? {}.env[configEntry.importMetaEnv] : void 0;
|
|
103
|
+
if (runtimeOverride !== void 0) value = runtimeOverride;
|
|
104
|
+
}
|
|
105
|
+
const hasValueSource = value !== void 0 || "processEnv" in configEntry || "importMetaEnv" in configEntry || configEntry.optional;
|
|
106
|
+
if (value === void 0 && !hasValueSource) {
|
|
107
|
+
errors.push(`${fullKey}: No value source declared and "optional" is not set.`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (value === void 0) if (configEntry.optional) {
|
|
111
|
+
value = configEntry.default;
|
|
112
|
+
if (value === void 0) {
|
|
113
|
+
output[key] = void 0;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
errors.push(`${fullKey}: Missing required config value in environment ${env}`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
value = validateAndCoerce(value, configEntry.format, fullKey, errors);
|
|
121
|
+
output[key] = value;
|
|
122
|
+
}
|
|
123
|
+
return output;
|
|
124
|
+
}
|
|
125
|
+
let outputConfig = processConfig(inputConfig, "");
|
|
126
|
+
if (errors.length > 0) {
|
|
127
|
+
console.error("Environment config validation failed", errors);
|
|
128
|
+
throw new Error(`Environment config validation failed:\n${errors.join("\n")}`);
|
|
129
|
+
}
|
|
130
|
+
outputConfig = {
|
|
131
|
+
env,
|
|
132
|
+
...outputConfig
|
|
133
|
+
};
|
|
134
|
+
return outputConfig;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Build the ordered list of environments to consult for per-environment
|
|
138
|
+
* value resolution. The active env is always first; each subsequent entry
|
|
139
|
+
* is the fallback target declared for the previous env. Throws if the
|
|
140
|
+
* chain is cyclic.
|
|
141
|
+
*/
|
|
142
|
+
function resolveFallbackChain(env, fallbacks) {
|
|
143
|
+
const chain = [env];
|
|
144
|
+
if (!fallbacks) return chain;
|
|
145
|
+
const seen = new Set([env]);
|
|
146
|
+
let current = env;
|
|
147
|
+
while (fallbacks[current] !== void 0) {
|
|
148
|
+
const next = fallbacks[current];
|
|
149
|
+
if (seen.has(next)) throw new Error(`Circular fallback chain detected: ${[...chain, next].join(" -> ")}`);
|
|
150
|
+
seen.add(next);
|
|
151
|
+
chain.push(next);
|
|
152
|
+
current = next;
|
|
153
|
+
}
|
|
154
|
+
return chain;
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/define-config.ts
|
|
158
|
+
/**
|
|
159
|
+
* Like {@link createEnvironmentConfig}, but binds the schema first and the
|
|
160
|
+
* environment later. Useful when the active environment is not known at
|
|
161
|
+
* schema-definition time.
|
|
162
|
+
*
|
|
163
|
+
* @typeParam E - The envs shape describing required/optional environments.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```ts
|
|
167
|
+
* type MyEnvs = {
|
|
168
|
+
* dev?: unknown
|
|
169
|
+
* staging: unknown
|
|
170
|
+
* production: unknown
|
|
171
|
+
* }
|
|
172
|
+
* const buildConfig = defineEnvironmentConfig<MyEnvs>()({
|
|
173
|
+
* port: { doc: 'Port', format: Number, value: 3000 },
|
|
174
|
+
* })
|
|
175
|
+
* const config = buildConfig('dev')
|
|
176
|
+
* ```
|
|
177
|
+
*
|
|
178
|
+
* @example Fallback environments
|
|
179
|
+
* ```ts
|
|
180
|
+
* const buildConfig = defineEnvironmentConfig<MyEnvs>()(
|
|
181
|
+
* { apiUrl: { doc: 'API URL', format: 'url', staging: 'https://staging' } },
|
|
182
|
+
* { fallbacks: { dev: 'staging' } },
|
|
183
|
+
* )
|
|
184
|
+
* const config = buildConfig('dev') // apiUrl resolved from `staging`
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
function defineEnvironmentConfig() {
|
|
188
|
+
const create = createEnvironmentConfig();
|
|
189
|
+
return (inputConfig, options) => (env) => create(env, inputConfig, options);
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
exports.createEnvironmentConfig = createEnvironmentConfig;
|
|
193
|
+
exports.defineEnvironmentConfig = defineEnvironmentConfig;
|
|
194
|
+
|
|
195
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/format.ts","../src/create-config.ts","../src/define-config.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\n\nexport function validateAndCoerce(\n value: any,\n format: any,\n fullKey: string,\n errors: string[],\n): any {\n switch (format) {\n case String:\n if (typeof value !== \"string\") {\n errors.push(`${fullKey}: must be a string`)\n }\n break\n case Number:\n value = Number(value)\n if (isNaN(value))\n errors.push(`${fullKey}: must be a number`)\n break\n case Boolean:\n if (\n typeof value !== \"boolean\" &&\n value !== \"true\" &&\n value !== \"false\" &&\n value !== 1 &&\n value !== 0\n ) {\n errors.push(`${fullKey}: must be a boolean`)\n }\n value =\n value === \"true\" ? true : value === \"false\" ? false : Boolean(value)\n break\n case Array:\n if (!Array.isArray(value)) {\n errors.push(`${fullKey}: must be an array`)\n }\n break\n case \"url\":\n try {\n new URL(value)\n } catch {\n errors.push(\n `${fullKey}: must be a valid URL; found \"${value}\"`,\n )\n }\n break\n default:\n if (format instanceof Array) {\n if (!format.includes(value)) {\n errors.push(\n `${fullKey}: must be one of: [${format.join(\", \")}]`,\n )\n }\n }\n }\n\n return value\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { validateAndCoerce } from \"./format.js\"\nimport type {\n ConfigGroup,\n ResolveConfigGroup,\n ValidateSchema,\n} from \"./types.js\"\nimport type {\n CreateConfigOptions,\n EnvName,\n EnvsShape,\n Fallbacks,\n} from \"./util-types.js\"\n\n/**\n * Create a resolved, validated config for the given environment.\n *\n * Curried so the envs declaration is bound on the first call and the\n * schema is inferred (giving you autocomplete) on the second call.\n *\n * @typeParam E - The envs shape describing required/optional environments.\n *\n * @example\n * ```ts\n * type MyEnvs = {\n * dev?: unknown\n * staging: unknown\n * production: unknown\n * }\n * const config = createEnvironmentConfig<MyEnvs>()('dev', {\n * port: { doc: 'Port', format: Number, value: 3000 },\n * })\n * config.port // number\n * ```\n *\n * @example Fallback environments\n * ```ts\n * // When running in `dev`, any entry that does not declare a `dev` value\n * // falls back to the entry's `integ` value.\n * const config = createEnvironmentConfig<MyEnvs>()(\n * 'dev',\n * {\n * apiUrl: {\n * doc: 'API URL',\n * format: 'url',\n * integ: 'https://integ.example.com',\n * staging: 'https://staging.example.com',\n * production: 'https://api.example.com',\n * },\n * },\n * { fallbacks: { dev: 'integ' } },\n * )\n * ```\n */\nexport function createEnvironmentConfig<E extends EnvsShape>() {\n return <const G extends ConfigGroup<E>>(\n env: EnvName<E>,\n inputConfig: ValidateSchema<G, E>,\n options?: CreateConfigOptions<E>,\n ): ResolveConfigGroup<G> & { env: EnvName<E> } =>\n buildConfig<E, G>(env, inputConfig as unknown as G, options)\n}\n\nfunction buildConfig<E extends EnvsShape, G extends ConfigGroup<E>>(\n env: EnvName<E>,\n inputConfig: G,\n options?: CreateConfigOptions<E>,\n): ResolveConfigGroup<G> & { env: EnvName<E> } {\n const errors: string[] = []\n\n // Resolve the per-environment lookup chain once for the active env.\n // Throws synchronously on a circular fallback chain.\n const envChain = resolveFallbackChain<E>(env, options?.fallbacks)\n\n function processConfig(\n config: ConfigGroup<E>,\n keyPrefix: string,\n ): Record<string, any> {\n const output: Record<string, any> = {}\n\n for (const [key, entry] of Object.entries(config)) {\n if (key === \"env\") {\n throw new Error(\n `Config key \"env\" is reserved and cannot be used. It will already be present by default.`,\n )\n }\n\n const fullKey = keyPrefix ? `${keyPrefix}.${key}` : key\n\n if (!(\"doc\" in entry)) {\n output[key] = processConfig(entry as ConfigGroup<E>, fullKey)\n continue\n }\n\n const configEntry = entry as any\n\n // Value resolution — sources are evaluated in ascending priority order.\n // The highest-priority source that resolves to a defined value wins.\n //\n // Priority │ Source\n // ─────────┼────────────────────────────────────────────────────────────────\n // 1 (low) │ Static `value` — same value across all environments\n // 2 │ Per-environment field, walking the fallback chain\n // │ — overrides the static value for that specific environment\n // 3 (high) │ Runtime env var via `processEnv` or `importMetaEnv`\n // │ — always wins; intended for secrets and local dev overrides\n\n // Priority 1: static value (lowest precedence)\n let value: any = \"value\" in configEntry ? configEntry.value : undefined\n\n // Priority 2: per-environment value, walking the fallback chain.\n // The first env in the chain with a defined value wins.\n for (const candidateEnv of envChain) {\n const envValue = configEntry[candidateEnv]\n if (envValue !== undefined) {\n value = envValue\n break\n }\n }\n\n // Priority 3: runtime env var (highest precedence — always wins when defined)\n if (\"processEnv\" in configEntry) {\n const runtimeOverride =\n // @ts-expect-error process may not be defined in browser builds\n typeof process !== \"undefined\" && process.env\n ? // @ts-expect-error process may not be defined in browser builds\n process.env[configEntry.processEnv as string]\n : undefined\n if (runtimeOverride !== undefined) value = runtimeOverride\n } else if (\"importMetaEnv\" in configEntry) {\n const runtimeOverride =\n // @ts-expect-error import.meta.env may not be defined in Node builds\n typeof import.meta !== \"undefined\" && import.meta.env\n ? // @ts-expect-error import.meta.env may not be defined in Node builds\n import.meta.env[configEntry.importMetaEnv as string]\n : undefined\n if (runtimeOverride !== undefined) value = runtimeOverride\n }\n\n const hasValueSource =\n value !== undefined ||\n \"processEnv\" in configEntry ||\n \"importMetaEnv\" in configEntry ||\n configEntry.optional\n\n if (value === undefined && !hasValueSource) {\n errors.push(\n `${fullKey}: No value source declared and \"optional\" is not set.`,\n )\n continue\n }\n\n if (value === undefined) {\n if (configEntry.optional) {\n value = configEntry.default\n if (value === undefined) {\n output[key] = undefined\n continue\n }\n } else {\n errors.push(\n `${fullKey}: Missing required config value in environment ${env}`,\n )\n continue\n }\n }\n\n //\n // Format validation and coercion\n //\n value = validateAndCoerce(value, configEntry.format, fullKey, errors)\n\n output[key] = value\n }\n\n return output\n }\n\n let outputConfig = processConfig(inputConfig, \"\")\n\n if (errors.length > 0) {\n console.error(\"Environment config validation failed\", errors)\n throw new Error(\n `Environment config validation failed:\\n${errors.join(\"\\n\")}`,\n )\n }\n\n outputConfig = {\n env,\n ...outputConfig,\n }\n return outputConfig as ResolveConfigGroup<G> & { env: EnvName<E> }\n}\n\n/**\n * Build the ordered list of environments to consult for per-environment\n * value resolution. The active env is always first; each subsequent entry\n * is the fallback target declared for the previous env. Throws if the\n * chain is cyclic.\n */\nfunction resolveFallbackChain<E extends EnvsShape>(\n env: EnvName<E>,\n fallbacks: Fallbacks<E> | undefined,\n): EnvName<E>[] {\n const chain: EnvName<E>[] = [env]\n if (!fallbacks) return chain\n\n const seen = new Set<string>([env])\n let current: EnvName<E> = env\n while (fallbacks[current] !== undefined) {\n const next = fallbacks[current] as EnvName<E>\n if (seen.has(next)) {\n throw new Error(\n `Circular fallback chain detected: ${[...chain, next].join(\" -> \")}`,\n )\n }\n seen.add(next)\n chain.push(next)\n current = next\n }\n return chain\n}\n","import { createEnvironmentConfig } from \"./create-config.js\"\nimport type {\n ConfigGroup,\n ResolveConfigGroup,\n ValidateSchema,\n} from \"./types.js\"\nimport type { CreateConfigOptions, EnvName, EnvsShape } from \"./util-types.js\"\n\n/**\n * Like {@link createEnvironmentConfig}, but binds the schema first and the\n * environment later. Useful when the active environment is not known at\n * schema-definition time.\n *\n * @typeParam E - The envs shape describing required/optional environments.\n *\n * @example\n * ```ts\n * type MyEnvs = {\n * dev?: unknown\n * staging: unknown\n * production: unknown\n * }\n * const buildConfig = defineEnvironmentConfig<MyEnvs>()({\n * port: { doc: 'Port', format: Number, value: 3000 },\n * })\n * const config = buildConfig('dev')\n * ```\n *\n * @example Fallback environments\n * ```ts\n * const buildConfig = defineEnvironmentConfig<MyEnvs>()(\n * { apiUrl: { doc: 'API URL', format: 'url', staging: 'https://staging' } },\n * { fallbacks: { dev: 'staging' } },\n * )\n * const config = buildConfig('dev') // apiUrl resolved from `staging`\n * ```\n */\nexport function defineEnvironmentConfig<E extends EnvsShape>() {\n const create = createEnvironmentConfig<E>()\n return <const G extends ConfigGroup<E>>(\n inputConfig: ValidateSchema<G, E>,\n options?: CreateConfigOptions<E>,\n ): ((env: EnvName<E>) => ResolveConfigGroup<G> & { env: EnvName<E> }) =>\n (env: EnvName<E>) =>\n create(env, inputConfig as any, options)\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
//#region src/util-types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Declaration of the environments a config supports: an object whose keys
|
|
4
|
+
* are environment names, with required envs as required properties and
|
|
5
|
+
* optional envs as optional properties.
|
|
6
|
+
*
|
|
7
|
+
* The property value type is not used by the library — only the keys and
|
|
8
|
+
* their required/optional status matter — so `unknown` (or any other type)
|
|
9
|
+
* is fine.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* type MyEnvs = {
|
|
14
|
+
* dev?: unknown
|
|
15
|
+
* integ?: unknown
|
|
16
|
+
* staging: unknown
|
|
17
|
+
* production: unknown
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
type EnvsShape = Record<string, any>;
|
|
22
|
+
type RequiredKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? never : K }[keyof T] & string;
|
|
23
|
+
type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T] & string;
|
|
24
|
+
/** Union of all environment names declared on `E` (required ∪ optional). */
|
|
25
|
+
type EnvName<E extends EnvsShape> = keyof E & string;
|
|
26
|
+
/**
|
|
27
|
+
* Per-environment value shape for a value of type `T`:
|
|
28
|
+
* required envs are required, optional envs are optional.
|
|
29
|
+
*/
|
|
30
|
+
type PerEnv<E extends EnvsShape, T> = { [K in RequiredKeys<E>]: T } & { [K in OptionalKeys<E>]?: T };
|
|
31
|
+
/**
|
|
32
|
+
* Map of environment names to a fallback environment name.
|
|
33
|
+
*
|
|
34
|
+
* If a per-environment value is not declared for the active environment on
|
|
35
|
+
* a given config entry, resolution falls back to the value declared for the
|
|
36
|
+
* environment named here. Fallbacks chain transitively until a value is
|
|
37
|
+
* found or the chain ends.
|
|
38
|
+
*
|
|
39
|
+
* Both keys and values must be names declared on the envs shape `E`.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* type MyEnvs = {
|
|
44
|
+
* dev?: unknown
|
|
45
|
+
* integ?: unknown
|
|
46
|
+
* staging: unknown
|
|
47
|
+
* production: unknown
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* // When running in `dev`, fall back to `integ`; if `integ` is also
|
|
51
|
+
* // missing a value, fall back to `staging`.
|
|
52
|
+
* const fallbacks: Fallbacks<MyEnvs> = {
|
|
53
|
+
* dev: 'integ',
|
|
54
|
+
* integ: 'staging',
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
type Fallbacks<E extends EnvsShape> = Partial<Record<EnvName<E>, EnvName<E>>>;
|
|
59
|
+
/**
|
|
60
|
+
* Options accepted as the third argument to `createEnvironmentConfig` (and
|
|
61
|
+
* the second argument to `defineEnvironmentConfig`).
|
|
62
|
+
*/
|
|
63
|
+
type CreateConfigOptions<E extends EnvsShape> = {
|
|
64
|
+
/**
|
|
65
|
+
* Map of environment names to a fallback environment name. See
|
|
66
|
+
* {@link Fallbacks} for details.
|
|
67
|
+
*/
|
|
68
|
+
fallbacks?: Fallbacks<E>;
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/types.d.ts
|
|
72
|
+
type NoEnvKeys<E extends EnvsShape> = { [K in EnvName<E>]?: never };
|
|
73
|
+
type RuntimeSourceOptional<T> = {
|
|
74
|
+
value?: T;
|
|
75
|
+
processEnv?: never;
|
|
76
|
+
importMetaEnv?: never;
|
|
77
|
+
} | {
|
|
78
|
+
value?: T;
|
|
79
|
+
processEnv: string;
|
|
80
|
+
importMetaEnv?: never;
|
|
81
|
+
} | {
|
|
82
|
+
value?: T;
|
|
83
|
+
processEnv?: never;
|
|
84
|
+
importMetaEnv: string;
|
|
85
|
+
};
|
|
86
|
+
type ValueSourceRequired<T> = {
|
|
87
|
+
value: T;
|
|
88
|
+
processEnv?: never;
|
|
89
|
+
importMetaEnv?: never;
|
|
90
|
+
} | {
|
|
91
|
+
value?: T;
|
|
92
|
+
processEnv: string;
|
|
93
|
+
importMetaEnv?: never;
|
|
94
|
+
} | {
|
|
95
|
+
value?: T;
|
|
96
|
+
processEnv?: never;
|
|
97
|
+
importMetaEnv: string;
|
|
98
|
+
} | {
|
|
99
|
+
optional: true;
|
|
100
|
+
value?: T;
|
|
101
|
+
processEnv?: never;
|
|
102
|
+
importMetaEnv?: never;
|
|
103
|
+
default: T;
|
|
104
|
+
};
|
|
105
|
+
type ValueSource<T, E extends EnvsShape> = (PerEnv<E, T> & RuntimeSourceOptional<T>) | (NoEnvKeys<E> & ValueSourceRequired<T>);
|
|
106
|
+
type ConfigEntryBase<T, E extends EnvsShape> = {
|
|
107
|
+
doc: string;
|
|
108
|
+
optional?: boolean;
|
|
109
|
+
} & ValueSource<T, E>;
|
|
110
|
+
type ConfigGroup<E extends EnvsShape> = {
|
|
111
|
+
[key: string]: ConfigEntry<any, E> | ConfigGroup<E>;
|
|
112
|
+
};
|
|
113
|
+
type MaybeOptionalUndefined<E, T> = E extends {
|
|
114
|
+
optional: true;
|
|
115
|
+
} ? E extends {
|
|
116
|
+
default: any;
|
|
117
|
+
} ? T : T | undefined : T;
|
|
118
|
+
type ReservedEntryKeys = "doc" | "format" | "optional" | "default" | "value" | "processEnv" | "importMetaEnv";
|
|
119
|
+
type NeverToAny<T> = [T] extends [never] ? any : T;
|
|
120
|
+
type UntypedResolved<E> = NeverToAny<(E extends {
|
|
121
|
+
value: infer V;
|
|
122
|
+
} ? V : never) | (E extends {
|
|
123
|
+
default: infer D;
|
|
124
|
+
} ? D : never) | (E extends Record<string, any> ? E[Exclude<keyof E, ReservedEntryKeys>] : never)>;
|
|
125
|
+
type ResolveEntryType<E> = E extends {
|
|
126
|
+
format: StringConstructor;
|
|
127
|
+
} ? MaybeOptionalUndefined<E, string> : E extends {
|
|
128
|
+
format: NumberConstructor;
|
|
129
|
+
} ? MaybeOptionalUndefined<E, number> : E extends {
|
|
130
|
+
format: BooleanConstructor;
|
|
131
|
+
} ? MaybeOptionalUndefined<E, boolean> : E extends {
|
|
132
|
+
format: 'url';
|
|
133
|
+
} ? MaybeOptionalUndefined<E, string> : E extends {
|
|
134
|
+
format: (infer F)[];
|
|
135
|
+
} ? MaybeOptionalUndefined<E, F> : E extends {
|
|
136
|
+
format: ArrayConstructor;
|
|
137
|
+
} ? MaybeOptionalUndefined<E, any[]> : MaybeOptionalUndefined<E, UntypedResolved<E>>;
|
|
138
|
+
type ResolveConfigGroup<G> = { [K in keyof G]: G[K] extends {
|
|
139
|
+
doc: string;
|
|
140
|
+
} ? ResolveEntryType<G[K]> : ResolveConfigGroup<G[K]> };
|
|
141
|
+
type ConfigEntry<T, E extends EnvsShape> = UntypedEntry<E> | StringEntry<E> | NumberEntry<E> | BooleanEntry<E> | ArrayEntry<T, E> | EnumEntry<T, E> | UrlEntry<E>;
|
|
142
|
+
type StringEntry<E extends EnvsShape> = ConfigEntryBase<string, E> & {
|
|
143
|
+
format: StringConstructor;
|
|
144
|
+
default?: string;
|
|
145
|
+
};
|
|
146
|
+
type NumberEntry<E extends EnvsShape> = ConfigEntryBase<number, E> & {
|
|
147
|
+
format: NumberConstructor;
|
|
148
|
+
default?: number;
|
|
149
|
+
};
|
|
150
|
+
type BooleanEntry<E extends EnvsShape> = ConfigEntryBase<boolean, E> & {
|
|
151
|
+
format: BooleanConstructor;
|
|
152
|
+
default?: boolean;
|
|
153
|
+
};
|
|
154
|
+
type ArrayEntry<T, E extends EnvsShape> = ConfigEntryBase<T[], E> & {
|
|
155
|
+
format: ArrayConstructor;
|
|
156
|
+
default?: T[];
|
|
157
|
+
};
|
|
158
|
+
type EnumEntry<T, E extends EnvsShape> = ConfigEntryBase<T, E> & {
|
|
159
|
+
format: T[];
|
|
160
|
+
default?: T;
|
|
161
|
+
};
|
|
162
|
+
type UrlEntry<E extends EnvsShape> = ConfigEntryBase<string, E> & {
|
|
163
|
+
format: "url";
|
|
164
|
+
default?: string;
|
|
165
|
+
};
|
|
166
|
+
type UntypedEntry<E extends EnvsShape> = ConfigEntryBase<any, E> & {
|
|
167
|
+
format?: never;
|
|
168
|
+
default?: any;
|
|
169
|
+
};
|
|
170
|
+
type ValidateSchema<G, E extends EnvsShape> = { [K in keyof G]: G[K] extends {
|
|
171
|
+
format: readonly (infer V)[];
|
|
172
|
+
} ? { [P in keyof G[K]]: P extends EnvName<E> | "value" | "default" ? G[K][P] extends V | undefined ? G[K][P] : V : G[K][P] } : G[K] extends {
|
|
173
|
+
format: unknown;
|
|
174
|
+
} ? G[K] : G[K] extends {
|
|
175
|
+
doc: string;
|
|
176
|
+
} ? G[K] : ValidateSchema<G[K], E> };
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/create-config.d.ts
|
|
179
|
+
/**
|
|
180
|
+
* Create a resolved, validated config for the given environment.
|
|
181
|
+
*
|
|
182
|
+
* Curried so the envs declaration is bound on the first call and the
|
|
183
|
+
* schema is inferred (giving you autocomplete) on the second call.
|
|
184
|
+
*
|
|
185
|
+
* @typeParam E - The envs shape describing required/optional environments.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```ts
|
|
189
|
+
* type MyEnvs = {
|
|
190
|
+
* dev?: unknown
|
|
191
|
+
* staging: unknown
|
|
192
|
+
* production: unknown
|
|
193
|
+
* }
|
|
194
|
+
* const config = createEnvironmentConfig<MyEnvs>()('dev', {
|
|
195
|
+
* port: { doc: 'Port', format: Number, value: 3000 },
|
|
196
|
+
* })
|
|
197
|
+
* config.port // number
|
|
198
|
+
* ```
|
|
199
|
+
*
|
|
200
|
+
* @example Fallback environments
|
|
201
|
+
* ```ts
|
|
202
|
+
* // When running in `dev`, any entry that does not declare a `dev` value
|
|
203
|
+
* // falls back to the entry's `integ` value.
|
|
204
|
+
* const config = createEnvironmentConfig<MyEnvs>()(
|
|
205
|
+
* 'dev',
|
|
206
|
+
* {
|
|
207
|
+
* apiUrl: {
|
|
208
|
+
* doc: 'API URL',
|
|
209
|
+
* format: 'url',
|
|
210
|
+
* integ: 'https://integ.example.com',
|
|
211
|
+
* staging: 'https://staging.example.com',
|
|
212
|
+
* production: 'https://api.example.com',
|
|
213
|
+
* },
|
|
214
|
+
* },
|
|
215
|
+
* { fallbacks: { dev: 'integ' } },
|
|
216
|
+
* )
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
declare function createEnvironmentConfig<E extends EnvsShape>(): <const G extends ConfigGroup<E>>(env: EnvName<E>, inputConfig: ValidateSchema<G, E>, options?: CreateConfigOptions<E>) => ResolveConfigGroup<G> & {
|
|
220
|
+
env: EnvName<E>;
|
|
221
|
+
};
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/define-config.d.ts
|
|
224
|
+
/**
|
|
225
|
+
* Like {@link createEnvironmentConfig}, but binds the schema first and the
|
|
226
|
+
* environment later. Useful when the active environment is not known at
|
|
227
|
+
* schema-definition time.
|
|
228
|
+
*
|
|
229
|
+
* @typeParam E - The envs shape describing required/optional environments.
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```ts
|
|
233
|
+
* type MyEnvs = {
|
|
234
|
+
* dev?: unknown
|
|
235
|
+
* staging: unknown
|
|
236
|
+
* production: unknown
|
|
237
|
+
* }
|
|
238
|
+
* const buildConfig = defineEnvironmentConfig<MyEnvs>()({
|
|
239
|
+
* port: { doc: 'Port', format: Number, value: 3000 },
|
|
240
|
+
* })
|
|
241
|
+
* const config = buildConfig('dev')
|
|
242
|
+
* ```
|
|
243
|
+
*
|
|
244
|
+
* @example Fallback environments
|
|
245
|
+
* ```ts
|
|
246
|
+
* const buildConfig = defineEnvironmentConfig<MyEnvs>()(
|
|
247
|
+
* { apiUrl: { doc: 'API URL', format: 'url', staging: 'https://staging' } },
|
|
248
|
+
* { fallbacks: { dev: 'staging' } },
|
|
249
|
+
* )
|
|
250
|
+
* const config = buildConfig('dev') // apiUrl resolved from `staging`
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
declare function defineEnvironmentConfig<E extends EnvsShape>(): <const G extends ConfigGroup<E>>(inputConfig: ValidateSchema<G, E>, options?: CreateConfigOptions<E>) => ((env: EnvName<E>) => ResolveConfigGroup<G> & {
|
|
254
|
+
env: EnvName<E>;
|
|
255
|
+
});
|
|
256
|
+
//#endregion
|
|
257
|
+
export { type CreateConfigOptions, type EnvName, type EnvsShape, type Fallbacks, type PerEnv, createEnvironmentConfig, defineEnvironmentConfig };
|
|
258
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/util-types.ts","../src/types.ts","../src/create-config.ts","../src/define-config.ts"],"mappings":";;AAoBA;;;;AAA8B;AAAa;;;;;;;;;;;;;KAA/B,SAAA,GAAY,MAAM;AAAA,KAEzB,YAAA,oBAES,CAAA,gBAAiB,IAAA,CAAK,CAAA,EAAG,CAAA,YAAa,CAAA,SAC5C,CAAA;AAAA,KAGH,YAAA,oBAES,CAAA,gBAAiB,IAAA,CAAK,CAAA,EAAG,CAAA,IAAK,CAAA,iBACpC,CAAA;AANC;AAAA,KAUG,OAAA,WAAkB,SAAA,UAAmB,CAAC;;;;;KAMtC,MAAA,WAAiB,SAAA,eAAwB,YAAA,CAAa,CAAA,IAAK,CAAA,aAC/D,YAAA,CAAa,CAAA,KAAM,CAAA;;;;;;;;;;;AAXlB;AAIT;;;;;;;;AAAkD;AAMlD;;;;;;;KA+BY,SAAA,WAAoB,SAAA,IAAa,OAAA,CAC3C,MAAA,CAAO,OAAA,CAAQ,CAAA,GAAI,OAAA,CAAQ,CAAA;;;;;KAOjB,mBAAA,WAA8B,SAAA;EAvCb;;;;EA4C3B,SAAA,GAAY,SAAA,CAAU,CAAA;AAAA;;;KCjFnB,SAAA,WAAoB,SAAA,YAAqB,OAAA,CAAQ,CAAA;AAAA,KAGjD,qBAAA;EACC,KAAA,GAAQ,CAAA;EAAG,UAAA;EAAoB,aAAA;AAAA;EAC/B,KAAA,GAAQ,CAAA;EAAG,UAAA;EAAoB,aAAA;AAAA;EAC/B,KAAA,GAAQ,CAAA;EAAG,UAAA;EAAoB,aAAA;AAAA;AAAA,KAIhC,mBAAA;EACC,KAAA,EAAO,CAAA;EAAG,UAAA;EAAoB,aAAA;AAAA;EAC9B,KAAA,GAAQ,CAAA;EAAG,UAAA;EAAoB,aAAA;AAAA;EAC/B,KAAA,GAAQ,CAAA;EAAG,UAAA;EAAoB,aAAA;AAAA;EAC/B,QAAA;EAAgB,KAAA,GAAQ,CAAA;EAAG,UAAA;EAAoB,aAAA;EAAuB,OAAA,EAAS,CAAA;AAAA;AAAA,KAKhF,WAAA,cAAyB,SAAA,KACzB,MAAA,CAAO,CAAA,EAAG,CAAA,IAAK,qBAAA,CAAsB,CAAA,MACrC,SAAA,CAAU,CAAA,IAAK,mBAAA,CAAoB,CAAA;AAAA,KAE5B,eAAA,cAA6B,SAAA;EACvC,GAAA;EACA,QAAA;AAAA,IACE,WAAA,CAAY,CAAA,EAAG,CAAA;AAAA,KAEP,WAAA,WAAsB,SAAA;EAAA,CAC/B,GAAA,WAAc,WAAA,MAAiB,CAAA,IAAK,WAAA,CAAY,CAAA;AAAA;AAAA,KAK9C,sBAAA,SACH,CAAA;EAAY,QAAA;AAAA,IACR,CAAA;EAAY,OAAA;AAAA,IACV,CAAA,GACA,CAAA,eACF,CAAA;AAAA,KAGD,iBAAA;AAAA,KAUA,UAAA,OAAiB,CAAA,0BAA2B,CAAC;AAAA,KAM7C,eAAA,MAAqB,UAAA,EACrB,CAAA;EAAY,KAAA;AAAA,IAAmB,CAAA,aAC/B,CAAA;EAAY,OAAA;AAAA,IAAqB,CAAA,aACjC,CAAA,SAAU,MAAA,gBAAsB,CAAA,CAAE,OAAA,OAAc,CAAA,EAAG,iBAAA;AAAA,KAI5C,gBAAA,MACV,CAAA;EAAW,MAAA,EAAQ,iBAAA;AAAA,IAAqB,sBAAA,CAAuB,CAAA,YAC/D,CAAA;EAAW,MAAA,EAAQ,iBAAA;AAAA,IAAqB,sBAAA,CAAuB,CAAA,YAC/D,CAAA;EAAW,MAAA,EAAQ,kBAAA;AAAA,IAAsB,sBAAA,CAAuB,CAAA,aAChE,CAAA;EAAW,MAAA;AAAA,IAAiB,sBAAA,CAAuB,CAAA,YACnD,CAAA;EAAW,MAAA;AAAA,IAAuB,sBAAA,CAAuB,CAAA,EAAG,CAAA,IAC5D,CAAA;EAAW,MAAA,EAAQ,gBAAA;AAAA,IAAoB,sBAAA,CAAuB,CAAA,WAC9D,sBAAA,CAAuB,CAAA,EAAG,eAAA,CAAgB,CAAA;AAAA,KAEhC,kBAAA,oBACE,CAAA,GAAI,CAAA,CAAE,CAAA;EAAa,GAAA;AAAA,IAC3B,gBAAA,CAAiB,CAAA,CAAE,CAAA,KACnB,kBAAA,CAAmB,CAAA,CAAE,CAAA;AAAA,KAOf,WAAA,cAAyB,SAAA,IACjC,YAAA,CAAa,CAAA,IACb,WAAA,CAAY,CAAA,IACZ,WAAA,CAAY,CAAA,IACZ,YAAA,CAAa,CAAA,IACb,UAAA,CAAW,CAAA,EAAG,CAAA,IACd,SAAA,CAAU,CAAA,EAAG,CAAA,IACb,QAAA,CAAS,CAAA;AAAA,KAER,WAAA,WAAsB,SAAA,IAAa,eAAA,SAAwB,CAAA;EAC9D,MAAA,EAAQ,iBAAA;EACR,OAAA;AAAA;AAAA,KAGG,WAAA,WAAsB,SAAA,IAAa,eAAA,SAAwB,CAAA;EAC9D,MAAA,EAAQ,iBAAA;EACR,OAAA;AAAA;AAAA,KAGG,YAAA,WAAuB,SAAA,IAAa,eAAA,UAAyB,CAAA;EAChE,MAAA,EAAQ,kBAAA;EACR,OAAA;AAAA;AAAA,KAGG,UAAA,cAAwB,SAAA,IAAa,eAAA,CAAgB,CAAA,IAAK,CAAA;EAC7D,MAAA,EAAQ,gBAAA;EACR,OAAA,GAAU,CAAA;AAAA;AAAA,KAGP,SAAA,cAAuB,SAAA,IAAa,eAAA,CAAgB,CAAA,EAAG,CAAA;EAC1D,MAAA,EAAQ,CAAA;EACR,OAAA,GAAU,CAAA;AAAA;AAAA,KAGP,QAAA,WAAmB,SAAA,IAAa,eAAA,SAAwB,CAAA;EAC3D,MAAA;EACA,OAAA;AAAA;AAAA,KAGG,YAAA,WAAuB,SAAA,IAAa,eAAA,MAAqB,CAAA;EAC5D,MAAA;EACA,OAAA;AAAA;AAAA,KAMU,cAAA,cAA4B,SAAA,kBAC1B,CAAA,GAAI,CAAA,CAAE,CAAA;EAAa,MAAA;AAAA,kBAEb,CAAA,CAAE,CAAA,IAAK,CAAA,SAAU,OAAA,CAAQ,CAAA,0BACjC,CAAA,CAAE,CAAA,EAAG,CAAA,UAAW,CAAA,eACd,CAAA,CAAE,CAAA,EAAG,CAAA,IACL,CAAA,GACF,CAAA,CAAE,CAAA,EAAG,CAAA,MAEX,CAAA,CAAE,CAAA;EAAa,MAAA;AAAA,IACb,CAAA,CAAE,CAAA,IACF,CAAA,CAAE,CAAA;EAAa,GAAA;AAAA,IACb,CAAA,CAAE,CAAA,IACF,cAAA,CAAe,CAAA,CAAE,CAAA,GAAI,CAAA;;;;;;ADhID;AAAa;;;;;;;;;;;;;;;;;;;;AAKlC;AAAA;;;;;;;;;;;;;;;iBE8BO,uBAAA,WAAkC,SAAA,sBACxB,WAAA,CAAY,CAAA,GAClC,GAAA,EAAK,OAAA,CAAQ,CAAA,GACb,WAAA,EAAa,cAAA,CAAe,CAAA,EAAG,CAAA,GAC/B,OAAA,GAAU,mBAAA,CAAoB,CAAA,MAC7B,kBAAA,CAAmB,CAAA;EAAO,GAAA,EAAK,OAAA,CAAQ,CAAA;AAAA;;;;;;AFxCd;AAAa;;;;;;;;;;;;;;;;;;;;AAKlC;AAAA;;;;iBGYO,uBAAA,WAAkC,SAAA,sBAExB,WAAA,CAAY,CAAA,GAClC,WAAA,EAAa,cAAA,CAAe,CAAA,EAAG,CAAA,GAC/B,OAAA,GAAU,mBAAA,CAAoB,CAAA,QAC3B,GAAA,EAAK,OAAA,CAAQ,CAAA,MAAO,kBAAA,CAAmB,CAAA;EAAO,GAAA,EAAK,OAAA,CAAQ,CAAA;AAAA"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region
|
|
1
|
+
//#region src/util-types.d.ts
|
|
2
2
|
/**
|
|
3
3
|
* Declaration of the environments a config supports: an object whose keys
|
|
4
4
|
* are environment names, with required envs as required properties and
|
|
@@ -68,7 +68,7 @@ type CreateConfigOptions<E extends EnvsShape> = {
|
|
|
68
68
|
fallbacks?: Fallbacks<E>;
|
|
69
69
|
};
|
|
70
70
|
//#endregion
|
|
71
|
-
//#region
|
|
71
|
+
//#region src/types.d.ts
|
|
72
72
|
type NoEnvKeys<E extends EnvsShape> = { [K in EnvName<E>]?: never };
|
|
73
73
|
type RuntimeSourceOptional<T> = {
|
|
74
74
|
value?: T;
|
|
@@ -95,6 +95,12 @@ type ValueSourceRequired<T> = {
|
|
|
95
95
|
value?: T;
|
|
96
96
|
processEnv?: never;
|
|
97
97
|
importMetaEnv: string;
|
|
98
|
+
} | {
|
|
99
|
+
optional: true;
|
|
100
|
+
value?: T;
|
|
101
|
+
processEnv?: never;
|
|
102
|
+
importMetaEnv?: never;
|
|
103
|
+
default: T;
|
|
98
104
|
};
|
|
99
105
|
type ValueSource<T, E extends EnvsShape> = (PerEnv<E, T> & RuntimeSourceOptional<T>) | (NoEnvKeys<E> & ValueSourceRequired<T>);
|
|
100
106
|
type ConfigEntryBase<T, E extends EnvsShape> = {
|
|
@@ -104,19 +110,31 @@ type ConfigEntryBase<T, E extends EnvsShape> = {
|
|
|
104
110
|
type ConfigGroup<E extends EnvsShape> = {
|
|
105
111
|
[key: string]: ConfigEntry<any, E> | ConfigGroup<E>;
|
|
106
112
|
};
|
|
113
|
+
type MaybeOptionalUndefined<E, T> = E extends {
|
|
114
|
+
optional: true;
|
|
115
|
+
} ? E extends {
|
|
116
|
+
default: any;
|
|
117
|
+
} ? T : T | undefined : T;
|
|
118
|
+
type ReservedEntryKeys = "doc" | "format" | "optional" | "default" | "value" | "processEnv" | "importMetaEnv";
|
|
119
|
+
type NeverToAny<T> = [T] extends [never] ? any : T;
|
|
120
|
+
type UntypedResolved<E> = NeverToAny<(E extends {
|
|
121
|
+
value: infer V;
|
|
122
|
+
} ? V : never) | (E extends {
|
|
123
|
+
default: infer D;
|
|
124
|
+
} ? D : never) | (E extends Record<string, any> ? E[Exclude<keyof E, ReservedEntryKeys>] : never)>;
|
|
107
125
|
type ResolveEntryType<E> = E extends {
|
|
108
126
|
format: StringConstructor;
|
|
109
|
-
} ? string : E extends {
|
|
127
|
+
} ? MaybeOptionalUndefined<E, string> : E extends {
|
|
110
128
|
format: NumberConstructor;
|
|
111
|
-
} ? number : E extends {
|
|
129
|
+
} ? MaybeOptionalUndefined<E, number> : E extends {
|
|
112
130
|
format: BooleanConstructor;
|
|
113
|
-
} ? boolean : E extends {
|
|
131
|
+
} ? MaybeOptionalUndefined<E, boolean> : E extends {
|
|
114
132
|
format: 'url';
|
|
115
|
-
} ? string : E extends {
|
|
133
|
+
} ? MaybeOptionalUndefined<E, string> : E extends {
|
|
116
134
|
format: (infer F)[];
|
|
117
|
-
} ? F : E extends {
|
|
135
|
+
} ? MaybeOptionalUndefined<E, F> : E extends {
|
|
118
136
|
format: ArrayConstructor;
|
|
119
|
-
} ? any[] :
|
|
137
|
+
} ? MaybeOptionalUndefined<E, any[]> : MaybeOptionalUndefined<E, UntypedResolved<E>>;
|
|
120
138
|
type ResolveConfigGroup<G> = { [K in keyof G]: G[K] extends {
|
|
121
139
|
doc: string;
|
|
122
140
|
} ? ResolveEntryType<G[K]> : ResolveConfigGroup<G[K]> };
|
|
@@ -149,8 +167,15 @@ type UntypedEntry<E extends EnvsShape> = ConfigEntryBase<any, E> & {
|
|
|
149
167
|
format?: never;
|
|
150
168
|
default?: any;
|
|
151
169
|
};
|
|
170
|
+
type ValidateSchema<G, E extends EnvsShape> = { [K in keyof G]: G[K] extends {
|
|
171
|
+
format: readonly (infer V)[];
|
|
172
|
+
} ? { [P in keyof G[K]]: P extends EnvName<E> | "value" | "default" ? G[K][P] extends V | undefined ? G[K][P] : V : G[K][P] } : G[K] extends {
|
|
173
|
+
format: unknown;
|
|
174
|
+
} ? G[K] : G[K] extends {
|
|
175
|
+
doc: string;
|
|
176
|
+
} ? G[K] : ValidateSchema<G[K], E> };
|
|
152
177
|
//#endregion
|
|
153
|
-
//#region
|
|
178
|
+
//#region src/create-config.d.ts
|
|
154
179
|
/**
|
|
155
180
|
* Create a resolved, validated config for the given environment.
|
|
156
181
|
*
|
|
@@ -191,11 +216,11 @@ type UntypedEntry<E extends EnvsShape> = ConfigEntryBase<any, E> & {
|
|
|
191
216
|
* )
|
|
192
217
|
* ```
|
|
193
218
|
*/
|
|
194
|
-
declare function createEnvironmentConfig<E extends EnvsShape>(): <G extends ConfigGroup<E>>(env: EnvName<E>, inputConfig: G, options?: CreateConfigOptions<E>) => ResolveConfigGroup<G> & {
|
|
219
|
+
declare function createEnvironmentConfig<E extends EnvsShape>(): <const G extends ConfigGroup<E>>(env: EnvName<E>, inputConfig: ValidateSchema<G, E>, options?: CreateConfigOptions<E>) => ResolveConfigGroup<G> & {
|
|
195
220
|
env: EnvName<E>;
|
|
196
221
|
};
|
|
197
222
|
//#endregion
|
|
198
|
-
//#region
|
|
223
|
+
//#region src/define-config.d.ts
|
|
199
224
|
/**
|
|
200
225
|
* Like {@link createEnvironmentConfig}, but binds the schema first and the
|
|
201
226
|
* environment later. Useful when the active environment is not known at
|
|
@@ -225,7 +250,7 @@ declare function createEnvironmentConfig<E extends EnvsShape>(): <G extends Conf
|
|
|
225
250
|
* const config = buildConfig('dev') // apiUrl resolved from `staging`
|
|
226
251
|
* ```
|
|
227
252
|
*/
|
|
228
|
-
declare function defineEnvironmentConfig<E extends EnvsShape>(): <G extends ConfigGroup<E>>(inputConfig: G, options?: CreateConfigOptions<E>) => ((env: EnvName<E>) => ResolveConfigGroup<G> & {
|
|
253
|
+
declare function defineEnvironmentConfig<E extends EnvsShape>(): <const G extends ConfigGroup<E>>(inputConfig: ValidateSchema<G, E>, options?: CreateConfigOptions<E>) => ((env: EnvName<E>) => ResolveConfigGroup<G> & {
|
|
229
254
|
env: EnvName<E>;
|
|
230
255
|
});
|
|
231
256
|
//#endregion
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/util-types.ts","../src/types.ts","../src/create-config.ts","../src/define-config.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
//#region
|
|
1
|
+
//#region src/format.ts
|
|
2
2
|
function validateAndCoerce(value, format, fullKey, errors) {
|
|
3
3
|
switch (format) {
|
|
4
4
|
case String:
|
|
5
|
-
if (typeof value !== "string") errors.push(
|
|
5
|
+
if (typeof value !== "string") errors.push(`${fullKey}: must be a string`);
|
|
6
6
|
break;
|
|
7
7
|
case Number:
|
|
8
8
|
value = Number(value);
|
|
9
|
-
if (isNaN(value)) errors.push(
|
|
9
|
+
if (isNaN(value)) errors.push(`${fullKey}: must be a number`);
|
|
10
10
|
break;
|
|
11
11
|
case Boolean:
|
|
12
|
-
if (typeof value !== "boolean" && value !== "true" && value !== "false" && value !== 1 && value !== 0) errors.push(
|
|
12
|
+
if (typeof value !== "boolean" && value !== "true" && value !== "false" && value !== 1 && value !== 0) errors.push(`${fullKey}: must be a boolean`);
|
|
13
13
|
value = value === "true" ? true : value === "false" ? false : Boolean(value);
|
|
14
14
|
break;
|
|
15
15
|
case Array:
|
|
16
|
-
if (!Array.isArray(value)) errors.push(
|
|
16
|
+
if (!Array.isArray(value)) errors.push(`${fullKey}: must be an array`);
|
|
17
17
|
break;
|
|
18
18
|
case "url":
|
|
19
19
|
try {
|
|
20
20
|
new URL(value);
|
|
21
21
|
} catch {
|
|
22
|
-
errors.push(
|
|
22
|
+
errors.push(`${fullKey}: must be a valid URL; found "${value}"`);
|
|
23
23
|
}
|
|
24
24
|
break;
|
|
25
25
|
default: if (format instanceof Array) {
|
|
26
|
-
if (!format.includes(value)) errors.push(
|
|
26
|
+
if (!format.includes(value)) errors.push(`${fullKey}: must be one of: [${format.join(", ")}]`);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
return value;
|
|
30
30
|
}
|
|
31
31
|
//#endregion
|
|
32
|
-
//#region
|
|
32
|
+
//#region src/create-config.ts
|
|
33
33
|
/**
|
|
34
34
|
* Create a resolved, validated config for the given environment.
|
|
35
35
|
*
|
|
@@ -101,9 +101,9 @@ function buildConfig(env, inputConfig, options) {
|
|
|
101
101
|
const runtimeOverride = typeof import.meta !== "undefined" && import.meta.env ? import.meta.env[configEntry.importMetaEnv] : void 0;
|
|
102
102
|
if (runtimeOverride !== void 0) value = runtimeOverride;
|
|
103
103
|
}
|
|
104
|
-
const hasValueSource = value !== void 0 || "processEnv" in configEntry || "importMetaEnv" in configEntry;
|
|
104
|
+
const hasValueSource = value !== void 0 || "processEnv" in configEntry || "importMetaEnv" in configEntry || configEntry.optional;
|
|
105
105
|
if (value === void 0 && !hasValueSource) {
|
|
106
|
-
errors.push(
|
|
106
|
+
errors.push(`${fullKey}: No value source declared and "optional" is not set.`);
|
|
107
107
|
continue;
|
|
108
108
|
}
|
|
109
109
|
if (value === void 0) if (configEntry.optional) {
|
|
@@ -113,7 +113,7 @@ function buildConfig(env, inputConfig, options) {
|
|
|
113
113
|
continue;
|
|
114
114
|
}
|
|
115
115
|
} else {
|
|
116
|
-
errors.push(
|
|
116
|
+
errors.push(`${fullKey}: Missing required config value in environment ${env}`);
|
|
117
117
|
continue;
|
|
118
118
|
}
|
|
119
119
|
value = validateAndCoerce(value, configEntry.format, fullKey, errors);
|
|
@@ -121,12 +121,15 @@ function buildConfig(env, inputConfig, options) {
|
|
|
121
121
|
}
|
|
122
122
|
return output;
|
|
123
123
|
}
|
|
124
|
-
|
|
124
|
+
let outputConfig = processConfig(inputConfig, "");
|
|
125
125
|
if (errors.length > 0) {
|
|
126
126
|
console.error("Environment config validation failed", errors);
|
|
127
127
|
throw new Error(`Environment config validation failed:\n${errors.join("\n")}`);
|
|
128
128
|
}
|
|
129
|
-
outputConfig
|
|
129
|
+
outputConfig = {
|
|
130
|
+
env,
|
|
131
|
+
...outputConfig
|
|
132
|
+
};
|
|
130
133
|
return outputConfig;
|
|
131
134
|
}
|
|
132
135
|
/**
|
|
@@ -150,7 +153,7 @@ function resolveFallbackChain(env, fallbacks) {
|
|
|
150
153
|
return chain;
|
|
151
154
|
}
|
|
152
155
|
//#endregion
|
|
153
|
-
//#region
|
|
156
|
+
//#region src/define-config.ts
|
|
154
157
|
/**
|
|
155
158
|
* Like {@link createEnvironmentConfig}, but binds the schema first and the
|
|
156
159
|
* environment later. Useful when the active environment is not known at
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../lib/format.ts","../lib/create-config.ts","../lib/define-config.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\n\nexport function validateAndCoerce(\n value: any,\n format: any,\n fullKey: string,\n errors: string[],\n): any {\n switch (format) {\n case String:\n if (typeof value !== \"string\") {\n errors.push(`Config value for ${fullKey} must be a string`)\n }\n break\n case Number:\n value = Number(value)\n if (isNaN(value))\n errors.push(`Config value for ${fullKey} must be a number`)\n break\n case Boolean:\n if (\n typeof value !== \"boolean\" &&\n value !== \"true\" &&\n value !== \"false\" &&\n value !== 1 &&\n value !== 0\n ) {\n errors.push(`Config value for ${fullKey} must be a boolean`)\n }\n value =\n value === \"true\" ? true : value === \"false\" ? false : Boolean(value)\n break\n case Array:\n if (!Array.isArray(value)) {\n errors.push(`Config value for ${fullKey} must be an array`)\n }\n break\n case \"url\":\n try {\n new URL(value)\n } catch {\n errors.push(\n `Config value for ${fullKey} must be a valid URL; found \"${value}\"`,\n )\n }\n break\n default:\n if (format instanceof Array) {\n if (!format.includes(value)) {\n errors.push(\n `Config value for ${fullKey} must be one of: [${format.join(\", \")}]`,\n )\n }\n }\n }\n\n return value\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport type {\n EnvsShape,\n EnvName,\n CreateConfigOptions,\n Fallbacks,\n} from \"./util-types.js\"\nimport type { ConfigGroup, ResolveConfigGroup } from \"./types.js\"\nimport { validateAndCoerce } from \"./format.js\"\n\n/**\n * Create a resolved, validated config for the given environment.\n *\n * Curried so the envs declaration is bound on the first call and the\n * schema is inferred (giving you autocomplete) on the second call.\n *\n * @typeParam E - The envs shape describing required/optional environments.\n *\n * @example\n * ```ts\n * type MyEnvs = {\n * dev?: unknown\n * staging: unknown\n * production: unknown\n * }\n * const config = createEnvironmentConfig<MyEnvs>()('dev', {\n * port: { doc: 'Port', format: Number, value: 3000 },\n * })\n * config.port // number\n * ```\n *\n * @example Fallback environments\n * ```ts\n * // When running in `dev`, any entry that does not declare a `dev` value\n * // falls back to the entry's `integ` value.\n * const config = createEnvironmentConfig<MyEnvs>()(\n * 'dev',\n * {\n * apiUrl: {\n * doc: 'API URL',\n * format: 'url',\n * integ: 'https://integ.example.com',\n * staging: 'https://staging.example.com',\n * production: 'https://api.example.com',\n * },\n * },\n * { fallbacks: { dev: 'integ' } },\n * )\n * ```\n */\nexport function createEnvironmentConfig<E extends EnvsShape>() {\n return <G extends ConfigGroup<E>>(\n env: EnvName<E>,\n inputConfig: G,\n options?: CreateConfigOptions<E>,\n ): ResolveConfigGroup<G> & { env: EnvName<E> } =>\n buildConfig<E, G>(env, inputConfig, options)\n}\n\nfunction buildConfig<E extends EnvsShape, G extends ConfigGroup<E>>(\n env: EnvName<E>,\n inputConfig: G,\n options?: CreateConfigOptions<E>,\n): ResolveConfigGroup<G> & { env: EnvName<E> } {\n const errors: string[] = []\n\n // Resolve the per-environment lookup chain once for the active env.\n // Throws synchronously on a circular fallback chain.\n const envChain = resolveFallbackChain<E>(env, options?.fallbacks)\n\n function processConfig(\n config: ConfigGroup<E>,\n keyPrefix: string,\n ): Record<string, any> {\n const output: Record<string, any> = {}\n\n for (const [key, entry] of Object.entries(config)) {\n if (key === \"env\") {\n throw new Error(\n `Config key \"env\" is reserved and cannot be used. It will already be present by default.`,\n )\n }\n\n const fullKey = keyPrefix ? `${keyPrefix}.${key}` : key\n\n if (!(\"doc\" in entry)) {\n output[key] = processConfig(entry as ConfigGroup<E>, fullKey)\n continue\n }\n\n const configEntry = entry as any\n\n // Value resolution — sources are evaluated in ascending priority order.\n // The highest-priority source that resolves to a defined value wins.\n //\n // Priority │ Source\n // ─────────┼────────────────────────────────────────────────────────────────\n // 1 (low) │ Static `value` — same value across all environments\n // 2 │ Per-environment field, walking the fallback chain\n // │ — overrides the static value for that specific environment\n // 3 (high) │ Runtime env var via `processEnv` or `importMetaEnv`\n // │ — always wins; intended for secrets and local dev overrides\n\n // Priority 1: static value (lowest precedence)\n let value: any = \"value\" in configEntry ? configEntry.value : undefined\n\n // Priority 2: per-environment value, walking the fallback chain.\n // The first env in the chain with a defined value wins.\n for (const candidateEnv of envChain) {\n const envValue = configEntry[candidateEnv]\n if (envValue !== undefined) {\n value = envValue\n break\n }\n }\n\n // Priority 3: runtime env var (highest precedence — always wins when defined)\n if (\"processEnv\" in configEntry) {\n const runtimeOverride =\n // @ts-expect-error process may not be defined in browser builds\n typeof process !== \"undefined\" && process.env\n ? // @ts-expect-error process may not be defined in browser builds\n process.env[configEntry.processEnv as string]\n : undefined\n if (runtimeOverride !== undefined) value = runtimeOverride\n } else if (\"importMetaEnv\" in configEntry) {\n const runtimeOverride =\n // @ts-expect-error import.meta.env may not be defined in Node builds\n typeof import.meta !== \"undefined\" && import.meta.env\n ? // @ts-expect-error import.meta.env may not be defined in Node builds\n import.meta.env[configEntry.importMetaEnv as string]\n : undefined\n if (runtimeOverride !== undefined) value = runtimeOverride\n }\n\n const hasValueSource =\n value !== undefined ||\n \"processEnv\" in configEntry ||\n \"importMetaEnv\" in configEntry\n\n if (value === undefined && !hasValueSource) {\n errors.push(\n `No value source declared for ${fullKey}. Supply a value using environment names, \"value\", \"processEnv\", or \"importMetaEnv\".`,\n )\n continue\n }\n\n if (value === undefined) {\n if (configEntry.optional) {\n value = configEntry.default\n if (value === undefined) {\n output[key] = undefined\n continue\n }\n } else {\n errors.push(\n `Missing required config value for ${fullKey} in environment ${env}`,\n )\n continue\n }\n }\n\n //\n // Format validation and coercion\n //\n value = validateAndCoerce(value, configEntry.format, fullKey, errors)\n\n output[key] = value\n }\n\n return output\n }\n\n const outputConfig = processConfig(inputConfig, \"\")\n\n if (errors.length > 0) {\n console.error(\"Environment config validation failed\", errors)\n throw new Error(\n `Environment config validation failed:\\n${errors.join(\"\\n\")}`,\n )\n }\n\n outputConfig.env = env\n return outputConfig as ResolveConfigGroup<G> & { env: EnvName<E> }\n}\n\n/**\n * Build the ordered list of environments to consult for per-environment\n * value resolution. The active env is always first; each subsequent entry\n * is the fallback target declared for the previous env. Throws if the\n * chain is cyclic.\n */\nfunction resolveFallbackChain<E extends EnvsShape>(\n env: EnvName<E>,\n fallbacks: Fallbacks<E> | undefined,\n): EnvName<E>[] {\n const chain: EnvName<E>[] = [env]\n if (!fallbacks) return chain\n\n const seen = new Set<string>([env])\n let current: EnvName<E> = env\n while (fallbacks[current] !== undefined) {\n const next = fallbacks[current] as EnvName<E>\n if (seen.has(next)) {\n throw new Error(\n `Circular fallback chain detected: ${[...chain, next].join(\" -> \")}`,\n )\n }\n seen.add(next)\n chain.push(next)\n current = next\n }\n return chain\n}\n","import type { EnvsShape, EnvName, CreateConfigOptions } from \"./util-types.js\"\nimport type { ConfigGroup, ResolveConfigGroup } from \"./types.js\"\nimport { createEnvironmentConfig } from \"./create-config.js\"\n\n/**\n * Like {@link createEnvironmentConfig}, but binds the schema first and the\n * environment later. Useful when the active environment is not known at\n * schema-definition time.\n *\n * @typeParam E - The envs shape describing required/optional environments.\n *\n * @example\n * ```ts\n * type MyEnvs = {\n * dev?: unknown\n * staging: unknown\n * production: unknown\n * }\n * const buildConfig = defineEnvironmentConfig<MyEnvs>()({\n * port: { doc: 'Port', format: Number, value: 3000 },\n * })\n * const config = buildConfig('dev')\n * ```\n *\n * @example Fallback environments\n * ```ts\n * const buildConfig = defineEnvironmentConfig<MyEnvs>()(\n * { apiUrl: { doc: 'API URL', format: 'url', staging: 'https://staging' } },\n * { fallbacks: { dev: 'staging' } },\n * )\n * const config = buildConfig('dev') // apiUrl resolved from `staging`\n * ```\n */\nexport function defineEnvironmentConfig<E extends EnvsShape>() {\n const create = createEnvironmentConfig<E>()\n return <G extends ConfigGroup<E>>(\n inputConfig: G,\n options?: CreateConfigOptions<E>,\n ): ((env: EnvName<E>) => ResolveConfigGroup<G> & { env: EnvName<E> }) =>\n (env: EnvName<E>) =>\n create(env, inputConfig, options)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/format.ts","../src/create-config.ts","../src/define-config.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\n\nexport function validateAndCoerce(\n value: any,\n format: any,\n fullKey: string,\n errors: string[],\n): any {\n switch (format) {\n case String:\n if (typeof value !== \"string\") {\n errors.push(`${fullKey}: must be a string`)\n }\n break\n case Number:\n value = Number(value)\n if (isNaN(value))\n errors.push(`${fullKey}: must be a number`)\n break\n case Boolean:\n if (\n typeof value !== \"boolean\" &&\n value !== \"true\" &&\n value !== \"false\" &&\n value !== 1 &&\n value !== 0\n ) {\n errors.push(`${fullKey}: must be a boolean`)\n }\n value =\n value === \"true\" ? true : value === \"false\" ? false : Boolean(value)\n break\n case Array:\n if (!Array.isArray(value)) {\n errors.push(`${fullKey}: must be an array`)\n }\n break\n case \"url\":\n try {\n new URL(value)\n } catch {\n errors.push(\n `${fullKey}: must be a valid URL; found \"${value}\"`,\n )\n }\n break\n default:\n if (format instanceof Array) {\n if (!format.includes(value)) {\n errors.push(\n `${fullKey}: must be one of: [${format.join(\", \")}]`,\n )\n }\n }\n }\n\n return value\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { validateAndCoerce } from \"./format.js\"\nimport type {\n ConfigGroup,\n ResolveConfigGroup,\n ValidateSchema,\n} from \"./types.js\"\nimport type {\n CreateConfigOptions,\n EnvName,\n EnvsShape,\n Fallbacks,\n} from \"./util-types.js\"\n\n/**\n * Create a resolved, validated config for the given environment.\n *\n * Curried so the envs declaration is bound on the first call and the\n * schema is inferred (giving you autocomplete) on the second call.\n *\n * @typeParam E - The envs shape describing required/optional environments.\n *\n * @example\n * ```ts\n * type MyEnvs = {\n * dev?: unknown\n * staging: unknown\n * production: unknown\n * }\n * const config = createEnvironmentConfig<MyEnvs>()('dev', {\n * port: { doc: 'Port', format: Number, value: 3000 },\n * })\n * config.port // number\n * ```\n *\n * @example Fallback environments\n * ```ts\n * // When running in `dev`, any entry that does not declare a `dev` value\n * // falls back to the entry's `integ` value.\n * const config = createEnvironmentConfig<MyEnvs>()(\n * 'dev',\n * {\n * apiUrl: {\n * doc: 'API URL',\n * format: 'url',\n * integ: 'https://integ.example.com',\n * staging: 'https://staging.example.com',\n * production: 'https://api.example.com',\n * },\n * },\n * { fallbacks: { dev: 'integ' } },\n * )\n * ```\n */\nexport function createEnvironmentConfig<E extends EnvsShape>() {\n return <const G extends ConfigGroup<E>>(\n env: EnvName<E>,\n inputConfig: ValidateSchema<G, E>,\n options?: CreateConfigOptions<E>,\n ): ResolveConfigGroup<G> & { env: EnvName<E> } =>\n buildConfig<E, G>(env, inputConfig as unknown as G, options)\n}\n\nfunction buildConfig<E extends EnvsShape, G extends ConfigGroup<E>>(\n env: EnvName<E>,\n inputConfig: G,\n options?: CreateConfigOptions<E>,\n): ResolveConfigGroup<G> & { env: EnvName<E> } {\n const errors: string[] = []\n\n // Resolve the per-environment lookup chain once for the active env.\n // Throws synchronously on a circular fallback chain.\n const envChain = resolveFallbackChain<E>(env, options?.fallbacks)\n\n function processConfig(\n config: ConfigGroup<E>,\n keyPrefix: string,\n ): Record<string, any> {\n const output: Record<string, any> = {}\n\n for (const [key, entry] of Object.entries(config)) {\n if (key === \"env\") {\n throw new Error(\n `Config key \"env\" is reserved and cannot be used. It will already be present by default.`,\n )\n }\n\n const fullKey = keyPrefix ? `${keyPrefix}.${key}` : key\n\n if (!(\"doc\" in entry)) {\n output[key] = processConfig(entry as ConfigGroup<E>, fullKey)\n continue\n }\n\n const configEntry = entry as any\n\n // Value resolution — sources are evaluated in ascending priority order.\n // The highest-priority source that resolves to a defined value wins.\n //\n // Priority │ Source\n // ─────────┼────────────────────────────────────────────────────────────────\n // 1 (low) │ Static `value` — same value across all environments\n // 2 │ Per-environment field, walking the fallback chain\n // │ — overrides the static value for that specific environment\n // 3 (high) │ Runtime env var via `processEnv` or `importMetaEnv`\n // │ — always wins; intended for secrets and local dev overrides\n\n // Priority 1: static value (lowest precedence)\n let value: any = \"value\" in configEntry ? configEntry.value : undefined\n\n // Priority 2: per-environment value, walking the fallback chain.\n // The first env in the chain with a defined value wins.\n for (const candidateEnv of envChain) {\n const envValue = configEntry[candidateEnv]\n if (envValue !== undefined) {\n value = envValue\n break\n }\n }\n\n // Priority 3: runtime env var (highest precedence — always wins when defined)\n if (\"processEnv\" in configEntry) {\n const runtimeOverride =\n // @ts-expect-error process may not be defined in browser builds\n typeof process !== \"undefined\" && process.env\n ? // @ts-expect-error process may not be defined in browser builds\n process.env[configEntry.processEnv as string]\n : undefined\n if (runtimeOverride !== undefined) value = runtimeOverride\n } else if (\"importMetaEnv\" in configEntry) {\n const runtimeOverride =\n // @ts-expect-error import.meta.env may not be defined in Node builds\n typeof import.meta !== \"undefined\" && import.meta.env\n ? // @ts-expect-error import.meta.env may not be defined in Node builds\n import.meta.env[configEntry.importMetaEnv as string]\n : undefined\n if (runtimeOverride !== undefined) value = runtimeOverride\n }\n\n const hasValueSource =\n value !== undefined ||\n \"processEnv\" in configEntry ||\n \"importMetaEnv\" in configEntry ||\n configEntry.optional\n\n if (value === undefined && !hasValueSource) {\n errors.push(\n `${fullKey}: No value source declared and \"optional\" is not set.`,\n )\n continue\n }\n\n if (value === undefined) {\n if (configEntry.optional) {\n value = configEntry.default\n if (value === undefined) {\n output[key] = undefined\n continue\n }\n } else {\n errors.push(\n `${fullKey}: Missing required config value in environment ${env}`,\n )\n continue\n }\n }\n\n //\n // Format validation and coercion\n //\n value = validateAndCoerce(value, configEntry.format, fullKey, errors)\n\n output[key] = value\n }\n\n return output\n }\n\n let outputConfig = processConfig(inputConfig, \"\")\n\n if (errors.length > 0) {\n console.error(\"Environment config validation failed\", errors)\n throw new Error(\n `Environment config validation failed:\\n${errors.join(\"\\n\")}`,\n )\n }\n\n outputConfig = {\n env,\n ...outputConfig,\n }\n return outputConfig as ResolveConfigGroup<G> & { env: EnvName<E> }\n}\n\n/**\n * Build the ordered list of environments to consult for per-environment\n * value resolution. The active env is always first; each subsequent entry\n * is the fallback target declared for the previous env. Throws if the\n * chain is cyclic.\n */\nfunction resolveFallbackChain<E extends EnvsShape>(\n env: EnvName<E>,\n fallbacks: Fallbacks<E> | undefined,\n): EnvName<E>[] {\n const chain: EnvName<E>[] = [env]\n if (!fallbacks) return chain\n\n const seen = new Set<string>([env])\n let current: EnvName<E> = env\n while (fallbacks[current] !== undefined) {\n const next = fallbacks[current] as EnvName<E>\n if (seen.has(next)) {\n throw new Error(\n `Circular fallback chain detected: ${[...chain, next].join(\" -> \")}`,\n )\n }\n seen.add(next)\n chain.push(next)\n current = next\n }\n return chain\n}\n","import { createEnvironmentConfig } from \"./create-config.js\"\nimport type {\n ConfigGroup,\n ResolveConfigGroup,\n ValidateSchema,\n} from \"./types.js\"\nimport type { CreateConfigOptions, EnvName, EnvsShape } from \"./util-types.js\"\n\n/**\n * Like {@link createEnvironmentConfig}, but binds the schema first and the\n * environment later. Useful when the active environment is not known at\n * schema-definition time.\n *\n * @typeParam E - The envs shape describing required/optional environments.\n *\n * @example\n * ```ts\n * type MyEnvs = {\n * dev?: unknown\n * staging: unknown\n * production: unknown\n * }\n * const buildConfig = defineEnvironmentConfig<MyEnvs>()({\n * port: { doc: 'Port', format: Number, value: 3000 },\n * })\n * const config = buildConfig('dev')\n * ```\n *\n * @example Fallback environments\n * ```ts\n * const buildConfig = defineEnvironmentConfig<MyEnvs>()(\n * { apiUrl: { doc: 'API URL', format: 'url', staging: 'https://staging' } },\n * { fallbacks: { dev: 'staging' } },\n * )\n * const config = buildConfig('dev') // apiUrl resolved from `staging`\n * ```\n */\nexport function defineEnvironmentConfig<E extends EnvsShape>() {\n const create = createEnvironmentConfig<E>()\n return <const G extends ConfigGroup<E>>(\n inputConfig: ValidateSchema<G, E>,\n options?: CreateConfigOptions<E>,\n ): ((env: EnvName<E>) => ResolveConfigGroup<G> & { env: EnvName<E> }) =>\n (env: EnvName<E>) =>\n create(env, inputConfig as any, options)\n}\n"],"mappings":";AAEA,SAAgB,kBACd,OACA,QACA,SACA,QACK;CACL,QAAQ,QAAR;EACE,KAAK;GACH,IAAI,OAAO,UAAU,UACnB,OAAO,KAAK,GAAG,QAAQ,mBAAmB;GAE5C;EACF,KAAK;GACH,QAAQ,OAAO,KAAK;GACpB,IAAI,MAAM,KAAK,GACb,OAAO,KAAK,GAAG,QAAQ,mBAAmB;GAC5C;EACF,KAAK;GACH,IACE,OAAO,UAAU,aACjB,UAAU,UACV,UAAU,WACV,UAAU,KACV,UAAU,GAEV,OAAO,KAAK,GAAG,QAAQ,oBAAoB;GAE7C,QACE,UAAU,SAAS,OAAO,UAAU,UAAU,QAAQ,QAAQ,KAAK;GACrE;EACF,KAAK;GACH,IAAI,CAAC,MAAM,QAAQ,KAAK,GACtB,OAAO,KAAK,GAAG,QAAQ,mBAAmB;GAE5C;EACF,KAAK;GACH,IAAI;IACF,IAAI,IAAI,KAAK;GACf,QAAQ;IACN,OAAO,KACL,GAAG,QAAQ,gCAAgC,MAAM,EACnD;GACF;GACA;EACF,SACE,IAAI,kBAAkB;OAChB,CAAC,OAAO,SAAS,KAAK,GACxB,OAAO,KACL,GAAG,QAAQ,qBAAqB,OAAO,KAAK,IAAI,EAAE,EACpD;EAAA;CAGR;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACFA,SAAgB,0BAA+C;CAC7D,QACE,KACA,aACA,YAEA,YAAkB,KAAK,aAA6B,OAAO;AAC/D;AAEA,SAAS,YACP,KACA,aACA,SAC6C;CAC7C,MAAM,SAAmB,CAAC;CAI1B,MAAM,WAAW,qBAAwB,KAAK,SAAS,SAAS;CAEhE,SAAS,cACP,QACA,WACqB;EACrB,MAAM,SAA8B,CAAC;EAErC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;GACjD,IAAI,QAAQ,OACV,MAAM,IAAI,MACR,yFACF;GAGF,MAAM,UAAU,YAAY,GAAG,UAAU,GAAG,QAAQ;GAEpD,IAAI,EAAE,SAAS,QAAQ;IACrB,OAAO,OAAO,cAAc,OAAyB,OAAO;IAC5D;GACF;GAEA,MAAM,cAAc;GAcpB,IAAI,QAAa,WAAW,cAAc,YAAY,QAAQ,KAAA;GAI9D,KAAK,MAAM,gBAAgB,UAAU;IACnC,MAAM,WAAW,YAAY;IAC7B,IAAI,aAAa,KAAA,GAAW;KAC1B,QAAQ;KACR;IACF;GACF;GAGA,IAAI,gBAAgB,aAAa;IAC/B,MAAM,kBAEJ,OAAO,YAAY,eAAe,QAAQ,MAEtC,QAAQ,IAAI,YAAY,cACxB,KAAA;IACN,IAAI,oBAAoB,KAAA,GAAW,QAAQ;GAC7C,OAAO,IAAI,mBAAmB,aAAa;IACzC,MAAM,kBAEJ,OAAO,OAAO,SAAS,eAAe,OAAO,KAAK,MAE9C,OAAO,KAAK,IAAI,YAAY,iBAC5B,KAAA;IACN,IAAI,oBAAoB,KAAA,GAAW,QAAQ;GAC7C;GAEA,MAAM,iBACJ,UAAU,KAAA,KACV,gBAAgB,eAChB,mBAAmB,eACnB,YAAY;GAEd,IAAI,UAAU,KAAA,KAAa,CAAC,gBAAgB;IAC1C,OAAO,KACL,GAAG,QAAQ,sDACb;IACA;GACF;GAEA,IAAI,UAAU,KAAA,GACZ,IAAI,YAAY,UAAU;IACxB,QAAQ,YAAY;IACpB,IAAI,UAAU,KAAA,GAAW;KACvB,OAAO,OAAO,KAAA;KACd;IACF;GACF,OAAO;IACL,OAAO,KACL,GAAG,QAAQ,iDAAiD,KAC9D;IACA;GACF;GAMF,QAAQ,kBAAkB,OAAO,YAAY,QAAQ,SAAS,MAAM;GAEpE,OAAO,OAAO;EAChB;EAEA,OAAO;CACT;CAEA,IAAI,eAAe,cAAc,aAAa,EAAE;CAEhD,IAAI,OAAO,SAAS,GAAG;EACrB,QAAQ,MAAM,wCAAwC,MAAM;EAC5D,MAAM,IAAI,MACR,0CAA0C,OAAO,KAAK,IAAI,GAC5D;CACF;CAEA,eAAe;EACb;EACA,GAAG;CACL;CACA,OAAO;AACT;;;;;;;AAQA,SAAS,qBACP,KACA,WACc;CACd,MAAM,QAAsB,CAAC,GAAG;CAChC,IAAI,CAAC,WAAW,OAAO;CAEvB,MAAM,OAAO,IAAI,IAAY,CAAC,GAAG,CAAC;CAClC,IAAI,UAAsB;CAC1B,OAAO,UAAU,aAAa,KAAA,GAAW;EACvC,MAAM,OAAO,UAAU;EACvB,IAAI,KAAK,IAAI,IAAI,GACf,MAAM,IAAI,MACR,qCAAqC,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC,KAAK,MAAM,GACnE;EAEF,KAAK,IAAI,IAAI;EACb,MAAM,KAAK,IAAI;EACf,UAAU;CACZ;CACA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACzLA,SAAgB,0BAA+C;CAC7D,MAAM,SAAS,wBAA2B;CAC1C,QACE,aACA,aAEC,QACC,OAAO,KAAK,aAAoB,OAAO;AAC7C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "konfeeg",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Build a validated, strongly-typed config object, and use it everywhere.",
|
|
5
5
|
"author": "Zach Olivare <https://github.com/0livare>",
|
|
6
6
|
"license": "ISC",
|
|
@@ -9,13 +9,33 @@
|
|
|
9
9
|
"url": "https://github.com/0livare/konfeeg"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
|
-
"main": "dist/index.
|
|
13
|
-
"module": "dist/index.mjs",
|
|
12
|
+
"main": "./dist/index.cjs",
|
|
13
|
+
"module": "./dist/index.mjs",
|
|
14
|
+
"types": "./dist/index.d.mts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": {
|
|
18
|
+
"types": "./dist/index.d.mts",
|
|
19
|
+
"default": "./dist/index.mjs"
|
|
20
|
+
},
|
|
21
|
+
"require": {
|
|
22
|
+
"types": "./dist/index.d.cts",
|
|
23
|
+
"default": "./dist/index.cjs"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
14
27
|
"files": [
|
|
15
28
|
"dist"
|
|
16
29
|
],
|
|
30
|
+
"nx": {
|
|
31
|
+
"targets": {
|
|
32
|
+
"types": {},
|
|
33
|
+
"format": {},
|
|
34
|
+
"lint": {}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
17
37
|
"devDependencies": {
|
|
18
|
-
"
|
|
38
|
+
"@biomejs/biome": "^2.4.16",
|
|
19
39
|
"tsdown": "^0.22.2",
|
|
20
40
|
"typescript": "^6.0.3",
|
|
21
41
|
"vitest": "^4.1.8"
|
|
@@ -31,10 +51,9 @@
|
|
|
31
51
|
"node"
|
|
32
52
|
],
|
|
33
53
|
"scripts": {
|
|
34
|
-
"
|
|
54
|
+
"dev": "tsdown --watch",
|
|
55
|
+
"build": "tsdown",
|
|
35
56
|
"test": "vitest --run",
|
|
36
|
-
"
|
|
37
|
-
"format": "prettier --write .",
|
|
38
|
-
"pr": "tsc && prettier --check . && pnpm run test && pnpm run build"
|
|
57
|
+
"pub": "npx bumpp && pnpm publish"
|
|
39
58
|
}
|
|
40
59
|
}
|