@nwire/config 0.13.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/LICENSE +21 -0
- package/dist/config-env.d.ts +85 -0
- package/dist/config-env.js +200 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.js +31 -0
- package/dist/define-config.d.ts +41 -0
- package/dist/define-config.js +25 -0
- package/dist/load-config.d.ts +62 -0
- package/dist/load-config.js +62 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Gefter / 200apps Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed environment reader.
|
|
3
|
+
*
|
|
4
|
+
* Reads from `process.env` (or a supplied override — useful for tests).
|
|
5
|
+
* Values are coerced from strings; mismatches are collected across every
|
|
6
|
+
* read call and surfaced all-at-once when `env.assertValid()` is called
|
|
7
|
+
* (or automatically by `loadConfig`). That way a developer with three
|
|
8
|
+
* mis-named vars sees all three in one boot failure instead of one at a time.
|
|
9
|
+
*
|
|
10
|
+
* The instance is intentionally created once per `loadConfig` call so the
|
|
11
|
+
* aggregated error is scoped to one configuration load, not the process
|
|
12
|
+
* lifetime.
|
|
13
|
+
*/
|
|
14
|
+
export interface EnvError {
|
|
15
|
+
key: string;
|
|
16
|
+
message: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class ConfigEnv {
|
|
19
|
+
private readonly source;
|
|
20
|
+
private readonly errors;
|
|
21
|
+
constructor(source?: NodeJS.ProcessEnv);
|
|
22
|
+
/**
|
|
23
|
+
* Read a string value. Returns `defaultValue` when the key is absent.
|
|
24
|
+
* Undefined `defaultValue` means the key is optional — absent is fine.
|
|
25
|
+
*/
|
|
26
|
+
string(key: string, defaultValue?: string): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Read an integer value. Coerces from the string representation found in
|
|
29
|
+
* `process.env`. Records an error when the value is present but not a valid
|
|
30
|
+
* integer; returns `defaultValue` otherwise.
|
|
31
|
+
*/
|
|
32
|
+
number(key: string, defaultValue?: number): number | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Read a boolean. Accepts `"true"` / `"1"` / `"yes"` as truthy and
|
|
35
|
+
* `"false"` / `"0"` / `"no"` as falsy (case-insensitive). Any other
|
|
36
|
+
* non-empty value is recorded as an error.
|
|
37
|
+
*/
|
|
38
|
+
bool(key: string, defaultValue?: boolean): boolean | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Read a value constrained to a fixed set of strings. Records an error when
|
|
41
|
+
* the value is present but not in `allowed`.
|
|
42
|
+
*/
|
|
43
|
+
enum<const T extends string>(key: string, allowed: readonly T[], defaultValue?: T): T | undefined;
|
|
44
|
+
readonly required: {
|
|
45
|
+
/**
|
|
46
|
+
* Read a required string. Records an error when the key is absent or empty.
|
|
47
|
+
* Returns an empty string as a safe stand-in so downstream code can
|
|
48
|
+
* continue collecting errors rather than throw immediately.
|
|
49
|
+
*/
|
|
50
|
+
readonly string: (key: string) => string;
|
|
51
|
+
/**
|
|
52
|
+
* Read a required integer.
|
|
53
|
+
*/
|
|
54
|
+
readonly number: (key: string) => number;
|
|
55
|
+
/**
|
|
56
|
+
* Read a required boolean.
|
|
57
|
+
*/
|
|
58
|
+
readonly bool: (key: string) => boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Read a required enum value.
|
|
61
|
+
*/
|
|
62
|
+
readonly enum: <const T extends string>(key: string, allowed: readonly T[]) => T;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Return a snapshot of all errors collected so far. Empty when everything
|
|
66
|
+
* was valid.
|
|
67
|
+
*/
|
|
68
|
+
collectErrors(): readonly EnvError[];
|
|
69
|
+
/**
|
|
70
|
+
* Throw an aggregated `ConfigEnvError` if any validation errors were
|
|
71
|
+
* recorded. Called once by `loadConfig` after all slices have been
|
|
72
|
+
* evaluated, so the developer sees every problem in one shot.
|
|
73
|
+
*/
|
|
74
|
+
assertValid(): void;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Thrown by `assertValid()` / `loadConfig()` when one or more environment
|
|
78
|
+
* variables are missing or malformed. The `errors` property lists every
|
|
79
|
+
* problem so the developer can fix them all at once rather than restarting
|
|
80
|
+
* the process per error.
|
|
81
|
+
*/
|
|
82
|
+
export declare class ConfigEnvError extends Error {
|
|
83
|
+
readonly errors: readonly EnvError[];
|
|
84
|
+
constructor(message: string, errors: readonly EnvError[]);
|
|
85
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed environment reader.
|
|
3
|
+
*
|
|
4
|
+
* Reads from `process.env` (or a supplied override — useful for tests).
|
|
5
|
+
* Values are coerced from strings; mismatches are collected across every
|
|
6
|
+
* read call and surfaced all-at-once when `env.assertValid()` is called
|
|
7
|
+
* (or automatically by `loadConfig`). That way a developer with three
|
|
8
|
+
* mis-named vars sees all three in one boot failure instead of one at a time.
|
|
9
|
+
*
|
|
10
|
+
* The instance is intentionally created once per `loadConfig` call so the
|
|
11
|
+
* aggregated error is scoped to one configuration load, not the process
|
|
12
|
+
* lifetime.
|
|
13
|
+
*/
|
|
14
|
+
export class ConfigEnv {
|
|
15
|
+
source;
|
|
16
|
+
errors = [];
|
|
17
|
+
constructor(source = process.env) {
|
|
18
|
+
this.source = source;
|
|
19
|
+
}
|
|
20
|
+
// -------------------------------------------------------------------------
|
|
21
|
+
// Optional readers (return undefined when key is absent and no default)
|
|
22
|
+
// -------------------------------------------------------------------------
|
|
23
|
+
/**
|
|
24
|
+
* Read a string value. Returns `defaultValue` when the key is absent.
|
|
25
|
+
* Undefined `defaultValue` means the key is optional — absent is fine.
|
|
26
|
+
*/
|
|
27
|
+
string(key, defaultValue) {
|
|
28
|
+
const raw = this.source[key];
|
|
29
|
+
if (raw === undefined || raw === "") {
|
|
30
|
+
return defaultValue;
|
|
31
|
+
}
|
|
32
|
+
return raw;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read an integer value. Coerces from the string representation found in
|
|
36
|
+
* `process.env`. Records an error when the value is present but not a valid
|
|
37
|
+
* integer; returns `defaultValue` otherwise.
|
|
38
|
+
*/
|
|
39
|
+
number(key, defaultValue) {
|
|
40
|
+
const raw = this.source[key];
|
|
41
|
+
if (raw === undefined || raw === "") {
|
|
42
|
+
return defaultValue;
|
|
43
|
+
}
|
|
44
|
+
const n = Number(raw);
|
|
45
|
+
if (!Number.isFinite(n)) {
|
|
46
|
+
this.errors.push({ key, message: `expected a finite number, got "${raw}"` });
|
|
47
|
+
return defaultValue;
|
|
48
|
+
}
|
|
49
|
+
return n;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Read a boolean. Accepts `"true"` / `"1"` / `"yes"` as truthy and
|
|
53
|
+
* `"false"` / `"0"` / `"no"` as falsy (case-insensitive). Any other
|
|
54
|
+
* non-empty value is recorded as an error.
|
|
55
|
+
*/
|
|
56
|
+
bool(key, defaultValue) {
|
|
57
|
+
const raw = this.source[key];
|
|
58
|
+
if (raw === undefined || raw === "") {
|
|
59
|
+
return defaultValue;
|
|
60
|
+
}
|
|
61
|
+
const lower = raw.toLowerCase();
|
|
62
|
+
if (lower === "true" || lower === "1" || lower === "yes")
|
|
63
|
+
return true;
|
|
64
|
+
if (lower === "false" || lower === "0" || lower === "no")
|
|
65
|
+
return false;
|
|
66
|
+
this.errors.push({
|
|
67
|
+
key,
|
|
68
|
+
message: `expected a boolean ("true"/"false"/"1"/"0"/"yes"/"no"), got "${raw}"`,
|
|
69
|
+
});
|
|
70
|
+
return defaultValue;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Read a value constrained to a fixed set of strings. Records an error when
|
|
74
|
+
* the value is present but not in `allowed`.
|
|
75
|
+
*/
|
|
76
|
+
enum(key, allowed, defaultValue) {
|
|
77
|
+
const raw = this.source[key];
|
|
78
|
+
if (raw === undefined || raw === "") {
|
|
79
|
+
return defaultValue;
|
|
80
|
+
}
|
|
81
|
+
if (allowed.includes(raw)) {
|
|
82
|
+
return raw;
|
|
83
|
+
}
|
|
84
|
+
this.errors.push({
|
|
85
|
+
key,
|
|
86
|
+
message: `expected one of [${allowed.map((v) => `"${v}"`).join(", ")}], got "${raw}"`,
|
|
87
|
+
});
|
|
88
|
+
return defaultValue;
|
|
89
|
+
}
|
|
90
|
+
// -------------------------------------------------------------------------
|
|
91
|
+
// Required readers (no default — missing is always an error)
|
|
92
|
+
// -------------------------------------------------------------------------
|
|
93
|
+
required = {
|
|
94
|
+
/**
|
|
95
|
+
* Read a required string. Records an error when the key is absent or empty.
|
|
96
|
+
* Returns an empty string as a safe stand-in so downstream code can
|
|
97
|
+
* continue collecting errors rather than throw immediately.
|
|
98
|
+
*/
|
|
99
|
+
string: (key) => {
|
|
100
|
+
const raw = this.source[key];
|
|
101
|
+
if (raw === undefined || raw === "") {
|
|
102
|
+
this.errors.push({ key, message: "required but not set" });
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
return raw;
|
|
106
|
+
},
|
|
107
|
+
/**
|
|
108
|
+
* Read a required integer.
|
|
109
|
+
*/
|
|
110
|
+
number: (key) => {
|
|
111
|
+
const raw = this.source[key];
|
|
112
|
+
if (raw === undefined || raw === "") {
|
|
113
|
+
this.errors.push({ key, message: "required but not set" });
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
const n = Number(raw);
|
|
117
|
+
if (!Number.isFinite(n)) {
|
|
118
|
+
this.errors.push({ key, message: `required; expected a finite number, got "${raw}"` });
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
return n;
|
|
122
|
+
},
|
|
123
|
+
/**
|
|
124
|
+
* Read a required boolean.
|
|
125
|
+
*/
|
|
126
|
+
bool: (key) => {
|
|
127
|
+
const raw = this.source[key];
|
|
128
|
+
if (raw === undefined || raw === "") {
|
|
129
|
+
this.errors.push({ key, message: "required but not set" });
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const lower = raw.toLowerCase();
|
|
133
|
+
if (lower === "true" || lower === "1" || lower === "yes")
|
|
134
|
+
return true;
|
|
135
|
+
if (lower === "false" || lower === "0" || lower === "no")
|
|
136
|
+
return false;
|
|
137
|
+
this.errors.push({
|
|
138
|
+
key,
|
|
139
|
+
message: `required; expected a boolean ("true"/"false"/"1"/"0"/"yes"/"no"), got "${raw}"`,
|
|
140
|
+
});
|
|
141
|
+
return false;
|
|
142
|
+
},
|
|
143
|
+
/**
|
|
144
|
+
* Read a required enum value.
|
|
145
|
+
*/
|
|
146
|
+
enum: (key, allowed) => {
|
|
147
|
+
const raw = this.source[key];
|
|
148
|
+
if (raw === undefined || raw === "") {
|
|
149
|
+
this.errors.push({
|
|
150
|
+
key,
|
|
151
|
+
message: `required; expected one of [${allowed.map((v) => `"${v}"`).join(", ")}]`,
|
|
152
|
+
});
|
|
153
|
+
return allowed[0];
|
|
154
|
+
}
|
|
155
|
+
if (allowed.includes(raw)) {
|
|
156
|
+
return raw;
|
|
157
|
+
}
|
|
158
|
+
this.errors.push({
|
|
159
|
+
key,
|
|
160
|
+
message: `required; expected one of [${allowed.map((v) => `"${v}"`).join(", ")}], got "${raw}"`,
|
|
161
|
+
});
|
|
162
|
+
return allowed[0];
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
// -------------------------------------------------------------------------
|
|
166
|
+
// Error collection
|
|
167
|
+
// -------------------------------------------------------------------------
|
|
168
|
+
/**
|
|
169
|
+
* Return a snapshot of all errors collected so far. Empty when everything
|
|
170
|
+
* was valid.
|
|
171
|
+
*/
|
|
172
|
+
collectErrors() {
|
|
173
|
+
return [...this.errors];
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Throw an aggregated `ConfigEnvError` if any validation errors were
|
|
177
|
+
* recorded. Called once by `loadConfig` after all slices have been
|
|
178
|
+
* evaluated, so the developer sees every problem in one shot.
|
|
179
|
+
*/
|
|
180
|
+
assertValid() {
|
|
181
|
+
if (this.errors.length === 0)
|
|
182
|
+
return;
|
|
183
|
+
const lines = this.errors.map((e) => ` ${e.key}: ${e.message}`);
|
|
184
|
+
throw new ConfigEnvError(`Invalid environment — fix these before booting:\n${lines.join("\n")}`, [...this.errors]);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Thrown by `assertValid()` / `loadConfig()` when one or more environment
|
|
189
|
+
* variables are missing or malformed. The `errors` property lists every
|
|
190
|
+
* problem so the developer can fix them all at once rather than restarting
|
|
191
|
+
* the process per error.
|
|
192
|
+
*/
|
|
193
|
+
export class ConfigEnvError extends Error {
|
|
194
|
+
errors;
|
|
195
|
+
constructor(message, errors) {
|
|
196
|
+
super(message);
|
|
197
|
+
this.errors = errors;
|
|
198
|
+
this.name = "ConfigEnvError";
|
|
199
|
+
}
|
|
200
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nwire/config — typed configuration layer.
|
|
3
|
+
*
|
|
4
|
+
* Three building blocks:
|
|
5
|
+
*
|
|
6
|
+
* `env` — a stand-alone `ConfigEnv` instance backed by `process.env`.
|
|
7
|
+
* Use it inside `defineConfig` factories. For aggregated
|
|
8
|
+
* fail-fast validation, pass slices to `loadConfig` instead
|
|
9
|
+
* of calling `env` methods ad-hoc.
|
|
10
|
+
*
|
|
11
|
+
* `defineConfig` — declare a named configuration slice (factory + name).
|
|
12
|
+
*
|
|
13
|
+
* `loadConfig` — evaluate all slices, aggregate env errors, return a
|
|
14
|
+
* deeply-frozen typed config object.
|
|
15
|
+
*
|
|
16
|
+
* Quick-start:
|
|
17
|
+
*
|
|
18
|
+
* import { defineConfig, loadConfig } from '@nwire/config'
|
|
19
|
+
*
|
|
20
|
+
* const http = defineConfig('http', (env) => ({
|
|
21
|
+
* port: env.number('PORT', 3000),
|
|
22
|
+
* prefix: env.string('HTTP_PREFIX', '/api'),
|
|
23
|
+
* }))
|
|
24
|
+
*
|
|
25
|
+
* export const config = loadConfig([http])
|
|
26
|
+
* // config.http.port — number
|
|
27
|
+
* // config.http.prefix — string | undefined
|
|
28
|
+
*/
|
|
29
|
+
export { ConfigEnv, ConfigEnvError } from "./config-env.js";
|
|
30
|
+
export type { EnvError } from "./config-env.js";
|
|
31
|
+
export { defineConfig } from "./define-config.js";
|
|
32
|
+
export type { ConfigDefinition, ConfigFactory } from "./define-config.js";
|
|
33
|
+
export { loadConfig } from "./load-config.js";
|
|
34
|
+
export type { LoadConfigOptions } from "./load-config.js";
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nwire/config — typed configuration layer.
|
|
3
|
+
*
|
|
4
|
+
* Three building blocks:
|
|
5
|
+
*
|
|
6
|
+
* `env` — a stand-alone `ConfigEnv` instance backed by `process.env`.
|
|
7
|
+
* Use it inside `defineConfig` factories. For aggregated
|
|
8
|
+
* fail-fast validation, pass slices to `loadConfig` instead
|
|
9
|
+
* of calling `env` methods ad-hoc.
|
|
10
|
+
*
|
|
11
|
+
* `defineConfig` — declare a named configuration slice (factory + name).
|
|
12
|
+
*
|
|
13
|
+
* `loadConfig` — evaluate all slices, aggregate env errors, return a
|
|
14
|
+
* deeply-frozen typed config object.
|
|
15
|
+
*
|
|
16
|
+
* Quick-start:
|
|
17
|
+
*
|
|
18
|
+
* import { defineConfig, loadConfig } from '@nwire/config'
|
|
19
|
+
*
|
|
20
|
+
* const http = defineConfig('http', (env) => ({
|
|
21
|
+
* port: env.number('PORT', 3000),
|
|
22
|
+
* prefix: env.string('HTTP_PREFIX', '/api'),
|
|
23
|
+
* }))
|
|
24
|
+
*
|
|
25
|
+
* export const config = loadConfig([http])
|
|
26
|
+
* // config.http.port — number
|
|
27
|
+
* // config.http.prefix — string | undefined
|
|
28
|
+
*/
|
|
29
|
+
export { ConfigEnv, ConfigEnvError } from "./config-env.js";
|
|
30
|
+
export { defineConfig } from "./define-config.js";
|
|
31
|
+
export { loadConfig } from "./load-config.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `defineConfig` — declares a single named configuration slice.
|
|
3
|
+
*
|
|
4
|
+
* Each slice is a factory that receives the `ConfigEnv` reader and returns a
|
|
5
|
+
* typed settings object. The factory is evaluated lazily by `loadConfig`;
|
|
6
|
+
* the returned definition carries only the name and factory, never the
|
|
7
|
+
* resolved value.
|
|
8
|
+
*
|
|
9
|
+
* const http = defineConfig('http', (env) => ({
|
|
10
|
+
* port: env.number('PORT', 3000),
|
|
11
|
+
* prefix: env.string('HTTP_PREFIX', '/api'),
|
|
12
|
+
* }))
|
|
13
|
+
*
|
|
14
|
+
* The name must be unique within any `loadConfig` call — duplicates throw at
|
|
15
|
+
* load time.
|
|
16
|
+
*/
|
|
17
|
+
import type { ConfigEnv } from "./config-env.js";
|
|
18
|
+
export type ConfigFactory<T> = (env: ConfigEnv) => T;
|
|
19
|
+
/**
|
|
20
|
+
* An opaque handle produced by `defineConfig`. Pass it to `loadConfig` to
|
|
21
|
+
* include the slice in the frozen config object.
|
|
22
|
+
*
|
|
23
|
+
* The `_phantom` field is a compile-time marker that carries the resolved
|
|
24
|
+
* type `T` through to `loadConfig`'s return type without ever existing at
|
|
25
|
+
* runtime.
|
|
26
|
+
*/
|
|
27
|
+
export interface ConfigDefinition<Name extends string, T> {
|
|
28
|
+
/** Slice name — used as the key on the config object returned by `loadConfig`. */
|
|
29
|
+
readonly name: Name;
|
|
30
|
+
/** Evaluated once by `loadConfig`. Not called directly. */
|
|
31
|
+
readonly factory: ConfigFactory<T>;
|
|
32
|
+
/** Compile-time phantom type marker. Never set at runtime. */
|
|
33
|
+
readonly _phantom?: T;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Declare a typed configuration slice.
|
|
37
|
+
*
|
|
38
|
+
* @param name The key under which this slice appears in the loaded config.
|
|
39
|
+
* @param factory A function that reads from `env` and returns the slice value.
|
|
40
|
+
*/
|
|
41
|
+
export declare function defineConfig<const Name extends string, T>(name: Name, factory: ConfigFactory<T>): ConfigDefinition<Name, T>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `defineConfig` — declares a single named configuration slice.
|
|
3
|
+
*
|
|
4
|
+
* Each slice is a factory that receives the `ConfigEnv` reader and returns a
|
|
5
|
+
* typed settings object. The factory is evaluated lazily by `loadConfig`;
|
|
6
|
+
* the returned definition carries only the name and factory, never the
|
|
7
|
+
* resolved value.
|
|
8
|
+
*
|
|
9
|
+
* const http = defineConfig('http', (env) => ({
|
|
10
|
+
* port: env.number('PORT', 3000),
|
|
11
|
+
* prefix: env.string('HTTP_PREFIX', '/api'),
|
|
12
|
+
* }))
|
|
13
|
+
*
|
|
14
|
+
* The name must be unique within any `loadConfig` call — duplicates throw at
|
|
15
|
+
* load time.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Declare a typed configuration slice.
|
|
19
|
+
*
|
|
20
|
+
* @param name The key under which this slice appears in the loaded config.
|
|
21
|
+
* @param factory A function that reads from `env` and returns the slice value.
|
|
22
|
+
*/
|
|
23
|
+
export function defineConfig(name, factory) {
|
|
24
|
+
return { name, factory };
|
|
25
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `loadConfig` — evaluates all declared slices, aggregates env errors, and
|
|
3
|
+
* returns a deeply-frozen typed config object.
|
|
4
|
+
*
|
|
5
|
+
* Evaluation order:
|
|
6
|
+
* 1. Create a single `ConfigEnv` that collects all validation errors.
|
|
7
|
+
* 2. Call every slice factory, collecting results by name.
|
|
8
|
+
* 3. After all slices are evaluated, call `env.assertValid()` — this throws
|
|
9
|
+
* a single `ConfigEnvError` listing every bad or missing variable if
|
|
10
|
+
* any were encountered. The developer sees every problem in one boot
|
|
11
|
+
* failure instead of one at a time.
|
|
12
|
+
* 4. Deep-freeze the result and return it.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
*
|
|
16
|
+
* import { env, defineConfig, loadConfig } from '@nwire/config'
|
|
17
|
+
*
|
|
18
|
+
* const http = defineConfig('http', (e) => ({ port: e.number('PORT', 3000) }))
|
|
19
|
+
* const mail = defineConfig('mail', (e) => ({ host: e.required.string('MAIL_HOST') }))
|
|
20
|
+
*
|
|
21
|
+
* export const config = loadConfig([http, mail])
|
|
22
|
+
* // config.http.port — number
|
|
23
|
+
* // config.mail.host — string
|
|
24
|
+
*/
|
|
25
|
+
import type { ConfigDefinition } from "./define-config.js";
|
|
26
|
+
/** Extract the resolved value type from a `ConfigDefinition`. */
|
|
27
|
+
type DefinitionValue<D> = D extends ConfigDefinition<string, infer T> ? T : never;
|
|
28
|
+
/** Extract the name literal from a `ConfigDefinition`. */
|
|
29
|
+
type DefinitionName<D> = D extends ConfigDefinition<infer N, unknown> ? N : never;
|
|
30
|
+
/**
|
|
31
|
+
* Given a tuple of `ConfigDefinition` instances, produce the record type
|
|
32
|
+
* that `loadConfig` returns. Each slice name becomes a key.
|
|
33
|
+
*
|
|
34
|
+
* Example:
|
|
35
|
+
* [ConfigDefinition<'http', { port: number }>, ConfigDefinition<'mail', { host: string }>]
|
|
36
|
+
* → { readonly http: { port: number }; readonly mail: { host: string } }
|
|
37
|
+
*/
|
|
38
|
+
type LoadedConfig<Defs extends ReadonlyArray<ConfigDefinition<string, unknown>>> = {
|
|
39
|
+
readonly [D in Defs[number] as DefinitionName<D>]: DefinitionValue<D>;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Options for `loadConfig`.
|
|
43
|
+
*/
|
|
44
|
+
export interface LoadConfigOptions {
|
|
45
|
+
/**
|
|
46
|
+
* A source to read environment variables from. Defaults to `process.env`.
|
|
47
|
+
* Useful for tests that need to supply a synthetic environment without
|
|
48
|
+
* mutating `process.env`.
|
|
49
|
+
*
|
|
50
|
+
* loadConfig([http], { source: { PORT: '8080' } })
|
|
51
|
+
*/
|
|
52
|
+
source?: NodeJS.ProcessEnv;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Evaluate all config slices, validate the environment in aggregate, and
|
|
56
|
+
* return a deeply-frozen typed config object.
|
|
57
|
+
*
|
|
58
|
+
* @param defs One or more `ConfigDefinition` values produced by `defineConfig`.
|
|
59
|
+
* @param opts Optional overrides (e.g. `source` for testing).
|
|
60
|
+
*/
|
|
61
|
+
export declare function loadConfig<const Defs extends ReadonlyArray<ConfigDefinition<string, unknown>>>(defs: Defs, opts?: LoadConfigOptions): LoadedConfig<Defs>;
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `loadConfig` — evaluates all declared slices, aggregates env errors, and
|
|
3
|
+
* returns a deeply-frozen typed config object.
|
|
4
|
+
*
|
|
5
|
+
* Evaluation order:
|
|
6
|
+
* 1. Create a single `ConfigEnv` that collects all validation errors.
|
|
7
|
+
* 2. Call every slice factory, collecting results by name.
|
|
8
|
+
* 3. After all slices are evaluated, call `env.assertValid()` — this throws
|
|
9
|
+
* a single `ConfigEnvError` listing every bad or missing variable if
|
|
10
|
+
* any were encountered. The developer sees every problem in one boot
|
|
11
|
+
* failure instead of one at a time.
|
|
12
|
+
* 4. Deep-freeze the result and return it.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
*
|
|
16
|
+
* import { env, defineConfig, loadConfig } from '@nwire/config'
|
|
17
|
+
*
|
|
18
|
+
* const http = defineConfig('http', (e) => ({ port: e.number('PORT', 3000) }))
|
|
19
|
+
* const mail = defineConfig('mail', (e) => ({ host: e.required.string('MAIL_HOST') }))
|
|
20
|
+
*
|
|
21
|
+
* export const config = loadConfig([http, mail])
|
|
22
|
+
* // config.http.port — number
|
|
23
|
+
* // config.mail.host — string
|
|
24
|
+
*/
|
|
25
|
+
import { ConfigEnv } from "./config-env.js";
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Deep-freeze helper
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
function deepFreeze(obj) {
|
|
30
|
+
if (obj === null || typeof obj !== "object")
|
|
31
|
+
return obj;
|
|
32
|
+
Object.getOwnPropertyNames(obj).forEach((name) => {
|
|
33
|
+
const val = obj[name];
|
|
34
|
+
if (val && typeof val === "object")
|
|
35
|
+
deepFreeze(val);
|
|
36
|
+
});
|
|
37
|
+
return Object.freeze(obj);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Evaluate all config slices, validate the environment in aggregate, and
|
|
41
|
+
* return a deeply-frozen typed config object.
|
|
42
|
+
*
|
|
43
|
+
* @param defs One or more `ConfigDefinition` values produced by `defineConfig`.
|
|
44
|
+
* @param opts Optional overrides (e.g. `source` for testing).
|
|
45
|
+
*/
|
|
46
|
+
export function loadConfig(defs, opts = {}) {
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
for (const def of defs) {
|
|
49
|
+
if (seen.has(def.name)) {
|
|
50
|
+
throw new Error(`@nwire/config: duplicate slice name "${def.name}". Each defineConfig name must be unique within a loadConfig call.`);
|
|
51
|
+
}
|
|
52
|
+
seen.add(def.name);
|
|
53
|
+
}
|
|
54
|
+
const env = new ConfigEnv(opts.source ?? process.env);
|
|
55
|
+
const result = {};
|
|
56
|
+
for (const def of defs) {
|
|
57
|
+
result[def.name] = def.factory(env);
|
|
58
|
+
}
|
|
59
|
+
// Aggregate — throws once with every env error listed
|
|
60
|
+
env.assertValid();
|
|
61
|
+
return deepFreeze(result);
|
|
62
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nwire/config",
|
|
3
|
+
"version": "0.13.2",
|
|
4
|
+
"description": "Nwire — typed configuration layer. env reader with coercion + aggregated fail-fast, defineConfig slices, loadConfig registry. Laravel/Adonis-grade configuration for @nwire apps.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"config",
|
|
7
|
+
"env",
|
|
8
|
+
"environment",
|
|
9
|
+
"nwire"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./dist/config.js",
|
|
19
|
+
"types": "./dist/config.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": "./dist/config.js",
|
|
23
|
+
"types": "./dist/config.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"zod": "^4.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.19.9",
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"vitest": "^4.0.18"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc && node ../../scripts/fix-dist-extensions.mjs dist",
|
|
39
|
+
"dev": "tsc --watch",
|
|
40
|
+
"typecheck": "tsc --noEmit"
|
|
41
|
+
}
|
|
42
|
+
}
|