konfeeg 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # konfeeg
2
+
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
+
5
+ > **Note:** Throws at startup if any required value is missing or fails format validation — broken `.env` files surface immediately, not at runtime.
6
+
7
+ ---
8
+
9
+ ## Quick start
10
+
11
+ ```ts
12
+ import { createEnvironmentConfig } from "konfeeg"
13
+
14
+ // 1. Declare your environments (required vs optional)
15
+ type MyEnvs = {
16
+ dev?: unknown // optional (?) = per-env value may be omitted
17
+ staging: unknown // required = must supply a value
18
+ production: unknown
19
+ }
20
+
21
+ // 2. Build the config — note the extra () 👇 (curried for TS inference)
22
+ const config = createEnvironmentConfig<MyEnvs>()("staging", {
23
+ apiUrl: {
24
+ doc: "Base URL for the API",
25
+ format: "url", // Will error if the value isn't a valid URL
26
+ processEnv: "API_URL", // runtime override (highest priority)
27
+ dev: "http://localhost:3000",
28
+ staging: "https://staging-api.example.com",
29
+ production: "https://api.example.com",
30
+ },
31
+ mongo: {
32
+ dbName: {
33
+ doc: "Mongo database name",
34
+ format: String, // Will error if the value isn't a string
35
+ processEnv: "MONGO_DB_NAME",
36
+ value: "my-app-db", // static fallback (lowest priority)
37
+ },
38
+ password: {
39
+ doc: "Mongo database password",
40
+ format: String,
41
+ // runtime-only, no static fallback
42
+ importMetaEnv: "MONGO_PASSWORD", // uses import.meta.env instead of process.env (e.g. for Vite)
43
+ },
44
+ },
45
+ })
46
+
47
+ config.env // "staging"
48
+ config.apiUrl // string (validated as URL)
49
+ config.mongo.dbName // string
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Schema fields
55
+
56
+ | Field | Required | Description |
57
+ | ---------------------------- | -------- | ---------------------------------------------------------------------- |
58
+ | `doc` | required | Human-readable description |
59
+ | `format` | required | Validation format — see below |
60
+ | `value` | optional | Constant shared across all environments (lowest priority) |
61
+ | `processEnv` | optional | `process.env` key — runtime override (highest priority) |
62
+ | `importMetaEnv` | optional | `import.meta.env` key — runtime override (highest priority) |
63
+ | `optional` | optional | When `true`, missing value resolves to `undefined` instead of throwing |
64
+ | `default` | optional | Fallback when `optional: true` and no value is found |
65
+ | env keys (`dev`, `staging`…) | optional | Per-environment value overrides |
66
+
67
+ ---
68
+
69
+ ## Formats
70
+
71
+ Validate that the resolved value matches the declared format. Coercion is applied where reasonable (e.g. numeric strings → numbers, `'true'`/`'false'` → booleans).
72
+
73
+ | Format | Resolved type | Notes |
74
+ | ------------ | ------------- | ------------------------------------------ |
75
+ | `String` | `string` | Value must be a string |
76
+ | `Number` | `number` | Numeric strings are coerced |
77
+ | `Boolean` | `boolean` | `'true'`/`'false'` and `0`/`1` are coerced |
78
+ | `Array` | `any[]` | Value must be an array |
79
+ | `'url'` | `string` | Must parse as a valid URL |
80
+ | `['a', 'b']` | `'a' \| 'b'` | Value must be one of the listed literals |
81
+
82
+ ---
83
+
84
+ ## Value resolution order
85
+
86
+ When multiple sources are declared on the same entry, the highest-priority source wins.
87
+
88
+ | Priority | Source | Use for |
89
+ | ----------- | ---------------------------------- | -------------------------------- |
90
+ | 3 — highest | `processEnv` / `importMetaEnv` | Secrets, local overrides |
91
+ | 2 | Per-env fields (`dev`, `staging`…) | Environment-specific values |
92
+ | 1 — lowest | `value` | Constants shared across all envs |
93
+
94
+ ---
95
+
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
+ ## Fallbacks
112
+
113
+ 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.
114
+
115
+ ```ts
116
+ const config = createEnvironmentConfig<MyEnvs>()(
117
+ "dev",
118
+ {
119
+ apiUrl: {
120
+ doc: "API URL",
121
+ format: "url",
122
+ // no `dev` field — falls back to `integ`
123
+ integ: "https://integ.example.com",
124
+ staging: "https://staging.example.com",
125
+ production: "https://api.example.com",
126
+ },
127
+ },
128
+ {
129
+ fallbacks: {
130
+ dev: "integ", // dev → integ
131
+ integ: "staging", // integ → staging (chained)
132
+ },
133
+ },
134
+ )
135
+
136
+ config.apiUrl // "https://integ.example.com"
137
+ ```
138
+
139
+ A circular fallback chain (e.g. `{ dev: 'integ', integ: 'dev' }`) throws synchronously with the cycle path in the error message.
140
+
141
+ ---
142
+
143
+ ## `defineEnvironmentConfig`
144
+
145
+ Same as `createEnvironmentConfig`, but binds the schema first and the environment later — useful when the environment isn't known at schema-definition time.
146
+
147
+ ```ts
148
+ import { defineEnvironmentConfig } from "konfeeg"
149
+
150
+ const buildConfig = defineEnvironmentConfig<MyEnvs>()({
151
+ /* schema */
152
+ })
153
+
154
+ const config = buildConfig(process.env.APP_ENV as any)
155
+ ```
@@ -0,0 +1,233 @@
1
+ //#region lib/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 lib/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
+ type ValueSource<T, E extends EnvsShape> = (PerEnv<E, T> & RuntimeSourceOptional<T>) | (NoEnvKeys<E> & ValueSourceRequired<T>);
100
+ type ConfigEntryBase<T, E extends EnvsShape> = {
101
+ doc: string;
102
+ optional?: boolean;
103
+ } & ValueSource<T, E>;
104
+ type ConfigGroup<E extends EnvsShape> = {
105
+ [key: string]: ConfigEntry<any, E> | ConfigGroup<E>;
106
+ };
107
+ type ResolveEntryType<E> = E extends {
108
+ format: StringConstructor;
109
+ } ? string : E extends {
110
+ format: NumberConstructor;
111
+ } ? number : E extends {
112
+ format: BooleanConstructor;
113
+ } ? boolean : E extends {
114
+ format: 'url';
115
+ } ? string : E extends {
116
+ format: (infer F)[];
117
+ } ? F : E extends {
118
+ format: ArrayConstructor;
119
+ } ? any[] : any;
120
+ type ResolveConfigGroup<G> = { [K in keyof G]: G[K] extends {
121
+ doc: string;
122
+ } ? ResolveEntryType<G[K]> : ResolveConfigGroup<G[K]> };
123
+ type ConfigEntry<T, E extends EnvsShape> = UntypedEntry<E> | StringEntry<E> | NumberEntry<E> | BooleanEntry<E> | ArrayEntry<T, E> | EnumEntry<T, E> | UrlEntry<E>;
124
+ type StringEntry<E extends EnvsShape> = ConfigEntryBase<string, E> & {
125
+ format: StringConstructor;
126
+ default?: string;
127
+ };
128
+ type NumberEntry<E extends EnvsShape> = ConfigEntryBase<number, E> & {
129
+ format: NumberConstructor;
130
+ default?: number;
131
+ };
132
+ type BooleanEntry<E extends EnvsShape> = ConfigEntryBase<boolean, E> & {
133
+ format: BooleanConstructor;
134
+ default?: boolean;
135
+ };
136
+ type ArrayEntry<T, E extends EnvsShape> = ConfigEntryBase<T[], E> & {
137
+ format: ArrayConstructor;
138
+ default?: T[];
139
+ };
140
+ type EnumEntry<T, E extends EnvsShape> = ConfigEntryBase<T, E> & {
141
+ format: T[];
142
+ default?: T;
143
+ };
144
+ type UrlEntry<E extends EnvsShape> = ConfigEntryBase<string, E> & {
145
+ format: "url";
146
+ default?: string;
147
+ };
148
+ type UntypedEntry<E extends EnvsShape> = ConfigEntryBase<any, E> & {
149
+ format?: never;
150
+ default?: any;
151
+ };
152
+ //#endregion
153
+ //#region lib/create-config.d.ts
154
+ /**
155
+ * Create a resolved, validated config for the given environment.
156
+ *
157
+ * Curried so the envs declaration is bound on the first call and the
158
+ * schema is inferred (giving you autocomplete) on the second call.
159
+ *
160
+ * @typeParam E - The envs shape describing required/optional environments.
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * type MyEnvs = {
165
+ * dev?: unknown
166
+ * staging: unknown
167
+ * production: unknown
168
+ * }
169
+ * const config = createEnvironmentConfig<MyEnvs>()('dev', {
170
+ * port: { doc: 'Port', format: Number, value: 3000 },
171
+ * })
172
+ * config.port // number
173
+ * ```
174
+ *
175
+ * @example Fallback environments
176
+ * ```ts
177
+ * // When running in `dev`, any entry that does not declare a `dev` value
178
+ * // falls back to the entry's `integ` value.
179
+ * const config = createEnvironmentConfig<MyEnvs>()(
180
+ * 'dev',
181
+ * {
182
+ * apiUrl: {
183
+ * doc: 'API URL',
184
+ * format: 'url',
185
+ * integ: 'https://integ.example.com',
186
+ * staging: 'https://staging.example.com',
187
+ * production: 'https://api.example.com',
188
+ * },
189
+ * },
190
+ * { fallbacks: { dev: 'integ' } },
191
+ * )
192
+ * ```
193
+ */
194
+ declare function createEnvironmentConfig<E extends EnvsShape>(): <G extends ConfigGroup<E>>(env: EnvName<E>, inputConfig: G, options?: CreateConfigOptions<E>) => ResolveConfigGroup<G> & {
195
+ env: EnvName<E>;
196
+ };
197
+ //#endregion
198
+ //#region lib/define-config.d.ts
199
+ /**
200
+ * Like {@link createEnvironmentConfig}, but binds the schema first and the
201
+ * environment later. Useful when the active environment is not known at
202
+ * schema-definition time.
203
+ *
204
+ * @typeParam E - The envs shape describing required/optional environments.
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * type MyEnvs = {
209
+ * dev?: unknown
210
+ * staging: unknown
211
+ * production: unknown
212
+ * }
213
+ * const buildConfig = defineEnvironmentConfig<MyEnvs>()({
214
+ * port: { doc: 'Port', format: Number, value: 3000 },
215
+ * })
216
+ * const config = buildConfig('dev')
217
+ * ```
218
+ *
219
+ * @example Fallback environments
220
+ * ```ts
221
+ * const buildConfig = defineEnvironmentConfig<MyEnvs>()(
222
+ * { apiUrl: { doc: 'API URL', format: 'url', staging: 'https://staging' } },
223
+ * { fallbacks: { dev: 'staging' } },
224
+ * )
225
+ * const config = buildConfig('dev') // apiUrl resolved from `staging`
226
+ * ```
227
+ */
228
+ declare function defineEnvironmentConfig<E extends EnvsShape>(): <G extends ConfigGroup<E>>(inputConfig: G, options?: CreateConfigOptions<E>) => ((env: EnvName<E>) => ResolveConfigGroup<G> & {
229
+ env: EnvName<E>;
230
+ });
231
+ //#endregion
232
+ export { type CreateConfigOptions, type EnvName, type EnvsShape, type Fallbacks, type PerEnv, createEnvironmentConfig, defineEnvironmentConfig };
233
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../lib/util-types.ts","../lib/types.ts","../lib/create-config.ts","../lib/define-config.ts"],"mappings":"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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,190 @@
1
+ //#region lib/format.ts
2
+ function validateAndCoerce(value, format, fullKey, errors) {
3
+ switch (format) {
4
+ case String:
5
+ if (typeof value !== "string") errors.push(`Config value for ${fullKey} must be a string`);
6
+ break;
7
+ case Number:
8
+ value = Number(value);
9
+ if (isNaN(value)) errors.push(`Config value for ${fullKey} must be a number`);
10
+ break;
11
+ case Boolean:
12
+ if (typeof value !== "boolean" && value !== "true" && value !== "false" && value !== 1 && value !== 0) errors.push(`Config value for ${fullKey} must be a boolean`);
13
+ value = value === "true" ? true : value === "false" ? false : Boolean(value);
14
+ break;
15
+ case Array:
16
+ if (!Array.isArray(value)) errors.push(`Config value for ${fullKey} must be an array`);
17
+ break;
18
+ case "url":
19
+ try {
20
+ new URL(value);
21
+ } catch {
22
+ errors.push(`Config value for ${fullKey} must be a valid URL; found "${value}"`);
23
+ }
24
+ break;
25
+ default: if (format instanceof Array) {
26
+ if (!format.includes(value)) errors.push(`Config value for ${fullKey} must be one of: [${format.join(", ")}]`);
27
+ }
28
+ }
29
+ return value;
30
+ }
31
+ //#endregion
32
+ //#region lib/create-config.ts
33
+ /**
34
+ * Create a resolved, validated config for the given environment.
35
+ *
36
+ * Curried so the envs declaration is bound on the first call and the
37
+ * schema is inferred (giving you autocomplete) on the second call.
38
+ *
39
+ * @typeParam E - The envs shape describing required/optional environments.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * type MyEnvs = {
44
+ * dev?: unknown
45
+ * staging: unknown
46
+ * production: unknown
47
+ * }
48
+ * const config = createEnvironmentConfig<MyEnvs>()('dev', {
49
+ * port: { doc: 'Port', format: Number, value: 3000 },
50
+ * })
51
+ * config.port // number
52
+ * ```
53
+ *
54
+ * @example Fallback environments
55
+ * ```ts
56
+ * // When running in `dev`, any entry that does not declare a `dev` value
57
+ * // falls back to the entry's `integ` value.
58
+ * const config = createEnvironmentConfig<MyEnvs>()(
59
+ * 'dev',
60
+ * {
61
+ * apiUrl: {
62
+ * doc: 'API URL',
63
+ * format: 'url',
64
+ * integ: 'https://integ.example.com',
65
+ * staging: 'https://staging.example.com',
66
+ * production: 'https://api.example.com',
67
+ * },
68
+ * },
69
+ * { fallbacks: { dev: 'integ' } },
70
+ * )
71
+ * ```
72
+ */
73
+ function createEnvironmentConfig() {
74
+ return (env, inputConfig, options) => buildConfig(env, inputConfig, options);
75
+ }
76
+ function buildConfig(env, inputConfig, options) {
77
+ const errors = [];
78
+ const envChain = resolveFallbackChain(env, options?.fallbacks);
79
+ function processConfig(config, keyPrefix) {
80
+ const output = {};
81
+ for (const [key, entry] of Object.entries(config)) {
82
+ if (key === "env") throw new Error(`Config key "env" is reserved and cannot be used. It will already be present by default.`);
83
+ const fullKey = keyPrefix ? `${keyPrefix}.${key}` : key;
84
+ if (!("doc" in entry)) {
85
+ output[key] = processConfig(entry, fullKey);
86
+ continue;
87
+ }
88
+ const configEntry = entry;
89
+ let value = "value" in configEntry ? configEntry.value : void 0;
90
+ for (const candidateEnv of envChain) {
91
+ const envValue = configEntry[candidateEnv];
92
+ if (envValue !== void 0) {
93
+ value = envValue;
94
+ break;
95
+ }
96
+ }
97
+ if ("processEnv" in configEntry) {
98
+ const runtimeOverride = typeof process !== "undefined" && process.env ? process.env[configEntry.processEnv] : void 0;
99
+ if (runtimeOverride !== void 0) value = runtimeOverride;
100
+ } else if ("importMetaEnv" in configEntry) {
101
+ const runtimeOverride = typeof import.meta !== "undefined" && import.meta.env ? import.meta.env[configEntry.importMetaEnv] : void 0;
102
+ if (runtimeOverride !== void 0) value = runtimeOverride;
103
+ }
104
+ const hasValueSource = value !== void 0 || "processEnv" in configEntry || "importMetaEnv" in configEntry;
105
+ if (value === void 0 && !hasValueSource) {
106
+ errors.push(`No value source declared for ${fullKey}. Supply a value using environment names, "value", "processEnv", or "importMetaEnv".`);
107
+ continue;
108
+ }
109
+ if (value === void 0) if (configEntry.optional) {
110
+ value = configEntry.default;
111
+ if (value === void 0) {
112
+ output[key] = void 0;
113
+ continue;
114
+ }
115
+ } else {
116
+ errors.push(`Missing required config value for ${fullKey} in environment ${env}`);
117
+ continue;
118
+ }
119
+ value = validateAndCoerce(value, configEntry.format, fullKey, errors);
120
+ output[key] = value;
121
+ }
122
+ return output;
123
+ }
124
+ const outputConfig = processConfig(inputConfig, "");
125
+ if (errors.length > 0) {
126
+ console.error("Environment config validation failed", errors);
127
+ throw new Error(`Environment config validation failed:\n${errors.join("\n")}`);
128
+ }
129
+ outputConfig.env = env;
130
+ return outputConfig;
131
+ }
132
+ /**
133
+ * Build the ordered list of environments to consult for per-environment
134
+ * value resolution. The active env is always first; each subsequent entry
135
+ * is the fallback target declared for the previous env. Throws if the
136
+ * chain is cyclic.
137
+ */
138
+ function resolveFallbackChain(env, fallbacks) {
139
+ const chain = [env];
140
+ if (!fallbacks) return chain;
141
+ const seen = new Set([env]);
142
+ let current = env;
143
+ while (fallbacks[current] !== void 0) {
144
+ const next = fallbacks[current];
145
+ if (seen.has(next)) throw new Error(`Circular fallback chain detected: ${[...chain, next].join(" -> ")}`);
146
+ seen.add(next);
147
+ chain.push(next);
148
+ current = next;
149
+ }
150
+ return chain;
151
+ }
152
+ //#endregion
153
+ //#region lib/define-config.ts
154
+ /**
155
+ * Like {@link createEnvironmentConfig}, but binds the schema first and the
156
+ * environment later. Useful when the active environment is not known at
157
+ * schema-definition time.
158
+ *
159
+ * @typeParam E - The envs shape describing required/optional environments.
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * type MyEnvs = {
164
+ * dev?: unknown
165
+ * staging: unknown
166
+ * production: unknown
167
+ * }
168
+ * const buildConfig = defineEnvironmentConfig<MyEnvs>()({
169
+ * port: { doc: 'Port', format: Number, value: 3000 },
170
+ * })
171
+ * const config = buildConfig('dev')
172
+ * ```
173
+ *
174
+ * @example Fallback environments
175
+ * ```ts
176
+ * const buildConfig = defineEnvironmentConfig<MyEnvs>()(
177
+ * { apiUrl: { doc: 'API URL', format: 'url', staging: 'https://staging' } },
178
+ * { fallbacks: { dev: 'staging' } },
179
+ * )
180
+ * const config = buildConfig('dev') // apiUrl resolved from `staging`
181
+ * ```
182
+ */
183
+ function defineEnvironmentConfig() {
184
+ const create = createEnvironmentConfig();
185
+ return (inputConfig, options) => (env) => create(env, inputConfig, options);
186
+ }
187
+ //#endregion
188
+ export { createEnvironmentConfig, defineEnvironmentConfig };
189
+
190
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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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"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "konfeeg",
3
+ "version": "0.0.0",
4
+ "description": "Build a validated, strongly-typed config object, and use it everywhere.",
5
+ "author": "Zach Olivare <https://github.com/0livare>",
6
+ "license": "ISC",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/0livare/konfeeg"
10
+ },
11
+ "type": "module",
12
+ "main": "dist/index.mjs",
13
+ "module": "dist/index.mjs",
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "devDependencies": {
18
+ "prettier": "^3.8.4",
19
+ "tsdown": "^0.22.2",
20
+ "typescript": "^6.0.3",
21
+ "vitest": "^4.1.8"
22
+ },
23
+ "keywords": [
24
+ "environment",
25
+ "config",
26
+ "configuration",
27
+ "typescript",
28
+ "type-safe",
29
+ "validation",
30
+ "browser",
31
+ "node"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsdown lib/index.ts",
35
+ "test": "vitest --run",
36
+ "types": "tsc",
37
+ "format": "prettier --write .",
38
+ "pr": "tsc && prettier --check . && pnpm run test && pnpm run build"
39
+ }
40
+ }