@jondotsoy/configs 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonathan Delgado <hi@jon.soy> (https://jon.soy)
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.
package/README.md ADDED
@@ -0,0 +1,296 @@
1
+ # @jondotsoy/configs
2
+
3
+ A tool for all your configurations.
4
+
5
+ - **Reactive configs** — every field is a live `Store`; subscribe to it and get notified whenever an upstream source changes.
6
+ - **Lightweight** — no dependencies, just a thin layer over plain objects and stores.
7
+ - **Typed with TS check** — schemas are statically checked, so `cfg.port.get()` is inferred as `number | null` (or `number` when a `default` is set), not `any`.
8
+
9
+ ```ts
10
+ import { configs, envSource, mapKey } from "@jondotsoy/configs";
11
+
12
+ // SERVER_PORT=3000 SERVER_HOST=localhost → { server: { port: "3000", host: "localhost" } }
13
+ const source = envSource({ mapKey: mapKey.snakeCase() });
14
+
15
+ const serverConfigs = await configs.create(
16
+ {
17
+ server: configs.create({
18
+ port: { type: "number", summary: "HTTP port", default: 3000 },
19
+ host: { type: "string", summary: "bind host", default: "localhost" },
20
+ }),
21
+ tls: configs.create({
22
+ key: { type: "string", summary: "TLS key path" },
23
+ cert: { type: "string", summary: "TLS cert path" },
24
+ }),
25
+ },
26
+ { sources: [source] },
27
+ );
28
+
29
+ // React to changes
30
+ serverConfigs.server.port.subscribe((port) => {
31
+ console.log(`listening on port ${port}`);
32
+ });
33
+
34
+ console.log(serverConfigs.server.port.get());
35
+ // 3000
36
+ ```
37
+
38
+ ## Table of contents
39
+
40
+ - [Install](#install)
41
+ - [Guide](#guide)
42
+ - [`Source` — building a custom source](#source--building-a-custom-source)
43
+ - [`envSource` — environment variables](#envsource--environment-variables)
44
+ - [`fetchSource` — a JSON endpoint over HTTP](#fetchsource--a-json-endpoint-over-http)
45
+ - [`sseSource` — live updates over Server-Sent Events](#ssesource--live-updates-over-server-sent-events)
46
+ - [`fileSource` — a local `.json` or `.env` file](#filesource--a-local-json-or-env-file)
47
+ - [`literalSource` — a static value](#literalsource--a-static-value)
48
+ - [Reacting to changes — restarting a periodic task](#reacting-to-changes--restarting-a-periodic-task)
49
+ - [Closing a config tree](#closing-a-config-tree)
50
+
51
+ ## Install
52
+
53
+ ```sh
54
+ npm install @jondotsoy/configs
55
+ ```
56
+
57
+ ## Guide
58
+
59
+ ### `Source` — building a custom source
60
+
61
+ The building block behind `envSource`, `fetchSource`, `sseSource`, `fileSource`, and
62
+ `literalSource`. It takes an
63
+ object with `start(control)` and an optional `close()`, mirroring `ReadableStream`'s
64
+ `UnderlyingSource`: `start` runs once and pushes snapshots via `control.set(value)`, while `close`
65
+ — called from within `start` via `control.close()`, or from the outside via the `Source`'s own
66
+ `close()` — is where you release whatever `start` set up, like a timer or an in-flight request.
67
+
68
+ ```ts
69
+ import { Source } from "@jondotsoy/configs";
70
+
71
+ function pollingSource(url: string, intervalMs: number): Source<{ port: number }> {
72
+ let timer: ReturnType<typeof setInterval>;
73
+
74
+ return new Source({
75
+ async start(control) {
76
+ const poll = async () => control.set((await (await fetch(url)).json()) as { port: number });
77
+ await poll();
78
+ timer = setInterval(poll, intervalMs);
79
+ },
80
+ close() {
81
+ clearInterval(timer);
82
+ },
83
+ });
84
+ }
85
+ ```
86
+
87
+ An optional `reduce(incoming, previous)` runs every `control.set(incoming)` call through it (along
88
+ with the last published value, `null` before the first `set()`) instead of publishing `incoming`
89
+ as-is — so a source whose `start()` only ever produces a partial patch (like `sseSource`, which
90
+ hands `control.set` one SSE message at a time) can publish the merged result without keeping its
91
+ own accumulator variable around:
92
+
93
+ ```ts
94
+ const source = new Source<{ port?: number; host?: string }>({
95
+ start(control) {
96
+ control.set({ port: 3000 }); // -> reduce({ port: 3000 }, null)
97
+ control.set({ host: "x" }); // -> reduce({ host: "x" }, { port: 3000 })
98
+ },
99
+ reduce: (patch, previous) => ({ ...(previous ?? {}), ...patch }),
100
+ });
101
+ // published: { port: 3000, host: "x" }
102
+ ```
103
+
104
+ ### `envSource` — environment variables
105
+
106
+ Reads `process.env` (or any object you pass as `env`) into the config tree. `mapKey` decides how
107
+ each key maps to a path; the default is the identity mapping, `"FOO_TAR" => ["FOO_TAR"]`.
108
+
109
+ Built-in strategies live under the `mapKey` namespace, each a factory returning an `EnvKeyMapper`:
110
+
111
+ - **`mapKey.snakeCase(options?)`** — splits a `SCREAMING_SNAKE_CASE` key into a lowercase nested
112
+ path on `separator` (default `"_"`): `"FOO_TAR" => ["foo", "tar"]`. Pass a different `separator`
113
+ (e.g. `"__"`) to keep a single underscore inside a segment from splitting it:
114
+ `mapKey.snakeCase({ separator: "__" })` maps `"API_KEY_V2__ENABLED"` to
115
+ `["api_key_v2", "enabled"]` instead of splitting on every `_`.
116
+ - **`mapKey.identity()`** — passes each key through unchanged, as a single-segment path:
117
+ `"FOO_TAR" => ["FOO_TAR"]`. Same as omitting `mapKey`, spelled out explicitly.
118
+ - **`mapKey.camelCase()`** — maps a key to a single camelCase segment instead of nesting it:
119
+ `"FOO_TAR" => ["fooTar"]`.
120
+ - **`mapKey.lookup(table, fallback?)`** — maps specific keys to explicit paths via a
121
+ `Record<string, string[]>` lookup table; a key not in `table` falls back to `fallback` (default:
122
+ the identity mapping). Handy when most keys follow no consistent naming, or when a few need an
123
+ exception to whatever strategy the rest use.
124
+
125
+ ```ts
126
+ import { envSource, mapKey } from "@jondotsoy/configs";
127
+
128
+ // SERVER_PORT=3000 SERVER_HOST=localhost → { server: { port: "3000", host: "localhost" } }
129
+ const source = envSource({ mapKey: mapKey.snakeCase() });
130
+
131
+ // PORT=3000 HOST=localhost → { server: { port: "3000" }, HOST: "localhost" }
132
+ const source2 = envSource({ mapKey: mapKey.lookup({ PORT: ["server", "port"] }) });
133
+ ```
134
+
135
+ You can also pass your own `EnvKeyMapper` instead of a built-in strategy — it's just a
136
+ `(key: string) => string[]` function:
137
+
138
+ ```ts
139
+ const source = envSource({
140
+ mapKey: (key) => (key === "PORT" ? ["server", "port"] : [key]),
141
+ });
142
+ ```
143
+
144
+ ### `fetchSource` — a JSON endpoint over HTTP
145
+
146
+ Fetches a JSON snapshot from `url` (with `method` and `headers`, if needed). Only JSON is
147
+ supported — a non-JSON `Content-Type` still gets a fallback parse attempt. `attempts` retries the
148
+ download on a network error or a non-`ok` response (default `1`, no retry). If the download never
149
+ succeeds, or the body isn't valid JSON, it logs a `console.error` and leaves the store empty
150
+ instead of throwing.
151
+
152
+ ```ts
153
+ import { fetchSource } from "@jondotsoy/configs";
154
+
155
+ const source = fetchSource<{ port: number }>({
156
+ url: "https://config-service.internal/app",
157
+ method: "GET",
158
+ headers: { authorization: `Bearer ${process.env.CONFIG_TOKEN}` },
159
+ attempts: 3,
160
+ });
161
+ ```
162
+
163
+ ### `sseSource` — live updates over Server-Sent Events
164
+
165
+ Connects to an SSE endpoint (`url`, `method`, `headers`). Every message tries to parse as JSON and,
166
+ if it's a plain object, is applied as a **patch** on top of what was already received — fields add
167
+ up and overwrite, the tree is never replaced wholesale:
168
+
169
+ ```ts
170
+ import { sseSource } from "@jondotsoy/configs";
171
+
172
+ const source = sseSource<{ port?: number; host?: string }>({
173
+ url: "https://config-service.internal/app/events",
174
+ });
175
+
176
+ // message: {"port":3000} => Store<{ port: 3000 }>
177
+ // message: {"host":"10.0.0.1"} => Store<{ port: 3000, host: "10.0.0.1" }>
178
+ ```
179
+
180
+ A message that isn't valid JSON, or doesn't parse to a plain object, is logged via
181
+ `console.error` and skipped — it never resets what was already received.
182
+
183
+ Opening the source waits for the first message (so the `Store` you get back already has data,
184
+ not `null`), then keeps the connection alive in the background, applying further messages as
185
+ patches until the resource closes the stream.
186
+
187
+ ### `fileSource` — a local `.json` or `.env` file
188
+
189
+ Reads a config tree from `path`, parsed by its extension: `.json` or `.env` (matched by extension,
190
+ or by the bare `.env` filename itself — a `.env` file always parses to a flat string map, one
191
+ entry per `KEY=VALUE` line, blank lines and `#`-comments skipped). `watch` defaults to `true`: the
192
+ file is re-read and the store updated live on every change; `watch: false` reads it once. A
193
+ missing file, an unrecognized extension, or a parse failure (including on a later watched change)
194
+ is logged via `console.error` and leaves the store empty instead of throwing — a parse error on a
195
+ later change keeps the last good value instead.
196
+
197
+ ```ts
198
+ import { fileSource } from "@jondotsoy/configs";
199
+
200
+ const source = fileSource<{ port: number; host: string }>("./config.json");
201
+ // config.json: { "port": 3000, "host": "localhost" }
202
+ ```
203
+
204
+ ### `literalSource` — a static value
205
+
206
+ Publishes a plain, already-in-hand value as a snapshot immediately, then closes. No I/O, no
207
+ options — just wraps `value` in a `Source` so it can sit in a `sources` array alongside the rest.
208
+ Handy as a static fallback tree (put it last so real sources win), a hardcoded default for a
209
+ single environment, or a stand-in source in a test.
210
+
211
+ ```ts
212
+ import { configs, envSource, literalSource, mapKey } from "@jondotsoy/configs";
213
+
214
+ const cfg = await configs.create(
215
+ {
216
+ port: { type: "number", required: true },
217
+ host: { type: "string", required: true },
218
+ },
219
+ {
220
+ sources: [
221
+ envSource({ mapKey: mapKey.snakeCase() }),
222
+ literalSource({ port: 3000, host: "localhost" }), // fallback if env vars are unset
223
+ ],
224
+ },
225
+ );
226
+ ```
227
+
228
+ ### Reacting to changes — restarting a periodic task
229
+
230
+ Because every field is a live `Store`, `.subscribe()` is the hook point for keeping something
231
+ else in sync with the config — for example, restarting a `setInterval` job whenever its period
232
+ changes. This only really happens at runtime with a live source like `sseSource`; an
233
+ `envSource` resolves once and never changes:
234
+
235
+ ```ts
236
+ import { configs, sseSource } from "@jondotsoy/configs";
237
+
238
+ async function cleanupTempFiles() {
239
+ // ...
240
+ }
241
+
242
+ const cfg = await configs.create(
243
+ {
244
+ service: configs.create({
245
+ cleanupIntervalMs: { type: "number", summary: "cleanup interval", default: 60_000 },
246
+ }),
247
+ },
248
+ { sources: [sseSource({ url: "https://config-service.internal/app/events" })] },
249
+ );
250
+
251
+ let timer: ReturnType<typeof setInterval> | undefined;
252
+
253
+ const unsubscribe = cfg.service.cleanupIntervalMs.subscribe((intervalMs) => {
254
+ clearInterval(timer);
255
+ timer = setInterval(cleanupTempFiles, intervalMs);
256
+ return () => clearInterval(timer); // runs when `unsubscribe()` is called, not on the next change
257
+ });
258
+ ```
259
+
260
+ `subscribe` fires immediately with the current value (starting the first timer) and again on every
261
+ subsequent change — `clearInterval(timer)` cancels the previous one before `setInterval` starts the
262
+ new one, so there's never more than one timer running for this field. The callback's returned
263
+ cleanup only fires once, when `unsubscribe()` itself is called, so it's the right place to stop the
264
+ last timer for good — it's not a substitute for the `clearInterval` at the top of the callback.
265
+
266
+ ### Closing a config tree
267
+
268
+ `configs.create(...)` results (and their nested groups) expose `close()`, which closes every
269
+ source backing them — for `sseSource`, this aborts the live connection instead of leaving
270
+ it open in the background:
271
+
272
+ ```ts
273
+ import { configs, sseSource } from "@jondotsoy/configs";
274
+
275
+ const source = sseSource({ url: "https://config-service.internal/app/events" });
276
+ const serverConfigs = await configs.create({ port: { type: "number" } }, { sources: [source] });
277
+
278
+ await serverConfigs.close();
279
+ ```
280
+
281
+ It also implements `Symbol.asyncDispose`, so `await using` closes it automatically at the end of
282
+ the scope — including when the scope throws:
283
+
284
+ ```ts
285
+ import { configs, sseSource } from "@jondotsoy/configs";
286
+
287
+ async function run() {
288
+ await using serverConfigs = await configs.create(
289
+ { port: { type: "number" } },
290
+ { sources: [sseSource({ url: "https://config-service.internal/app/events" })] },
291
+ );
292
+
293
+ console.log(serverConfigs.port.get());
294
+ // closed automatically here, no explicit serverConfigs.close() needed
295
+ }
296
+ ```
@@ -0,0 +1,177 @@
1
+ import { Source } from "./sources/source";
2
+ import { Store, type Subscriber, type Unsubscribe } from "./utils/store";
3
+ export type FieldType = "string" | "number" | "boolean";
4
+ interface BaseFieldSchema {
5
+ summary?: string;
6
+ required?: boolean;
7
+ /** Freezes the field at its first resolved value: later source updates no longer reach `.get()`. */
8
+ readonly?: boolean;
9
+ }
10
+ export type FieldSchema = (BaseFieldSchema & {
11
+ type: "string";
12
+ pattern?: RegExp;
13
+ default?: string;
14
+ }) | (BaseFieldSchema & {
15
+ type: "number";
16
+ default?: number;
17
+ }) | (BaseFieldSchema & {
18
+ type: "boolean";
19
+ default?: boolean;
20
+ });
21
+ export interface CreateOptions {
22
+ sources?: Source<any>[];
23
+ /**
24
+ * Static, lowest-priority fallback tree — checked after every source, before a field's own `default`.
25
+ * @deprecated No longer read by `configs.create()`.
26
+ */
27
+ defaultValues?: unknown;
28
+ }
29
+ /**
30
+ * Discriminates a nested group from a leaf field inside a `SchemaShape`. Deliberately not generic
31
+ * (unlike `SchemaGroup<S>` below): a generic member here — even `SchemaGroup<any>` — becomes the
32
+ * contextual type TypeScript propagates into a nested `configs.create(...)` call written inline as
33
+ * a shape property, and `any` there widens every literal `type: "string"` inside it to `string`,
34
+ * breaking every field's inferred type. This plain marker carries no such poison.
35
+ */
36
+ interface SchemaGroupNode {
37
+ readonly shape: SchemaShape;
38
+ }
39
+ export type SchemaNode = FieldSchema | SchemaGroupNode;
40
+ export type SchemaShape = Record<string, SchemaNode>;
41
+ type PrimitiveOfField<F extends FieldSchema> = F extends {
42
+ type: "number";
43
+ } ? number : F extends {
44
+ type: "string";
45
+ } ? string : F extends {
46
+ type: "boolean";
47
+ } ? boolean : never;
48
+ /**
49
+ * A field only ever resolves to `null` when no source has it and it has no `default` — data comes
50
+ * from async sources that may simply have nothing, so `required` documents intent but can't be enforced
51
+ * at runtime and doesn't affect this type.
52
+ */
53
+ type HasDefault<F extends FieldSchema> = F extends {
54
+ default: any;
55
+ } ? true : false;
56
+ type InferFieldValue<F extends FieldSchema> = HasDefault<F> extends true ? PrimitiveOfField<F> : PrimitiveOfField<F> | null;
57
+ type InferField<F extends SchemaNode> = F extends ConfigNode<infer S> ? InferShape<S> : F extends FieldSchema ? InferFieldValue<F> : never;
58
+ export type InferShape<S extends SchemaShape> = {
59
+ [K in keyof S]: InferField<S[K]>;
60
+ };
61
+ /** A read-only view of a `Store<T>`: exposes `get`/`subscribe`/`listen` but never `set`. */
62
+ export interface ReadOnlyStore<T> {
63
+ get(): T;
64
+ /** Calls `subscriber` immediately with the current value, then on every subsequent update. */
65
+ subscribe(subscriber: Subscriber<T>): Unsubscribe;
66
+ /** Calls `subscriber` only on subsequent updates, not with the current value. */
67
+ listen(subscriber: Subscriber<T>): Unsubscribe;
68
+ }
69
+ type InferReadOnlyAccessor<F extends SchemaNode> = F extends ConfigNode<infer S> ? SchemaGroup<S> : F extends FieldSchema ? ReadOnlyStore<InferFieldValue<F>> : never;
70
+ /** Shape of the live proxy returned by `configs.create()`: a leaf field is a read-only `ReadOnlyStore<T>`, a nested group is a `SchemaGroup`. */
71
+ export type InferReadOnlyAccessors<S extends SchemaShape> = {
72
+ [K in keyof S]: InferReadOnlyAccessor<S[K]>;
73
+ };
74
+ /** A leaf config field, live as a `Store<T>`: read-only — `set()` always throws. */
75
+ declare class ConfigField<T> extends Store<T> {
76
+ set(): void;
77
+ /** Pushes a recomputed value when an upstream source changes, bypassing the public read-only `set()`. */
78
+ _update(value: T): void;
79
+ }
80
+ /**
81
+ * The engine behind a `ConfigNode`/`ConfigNodeResolved`: resolves fields from `rootStores` (in
82
+ * priority order), caches field/child-group instances, and recomputes them live as `rootStores`
83
+ * change. A nested group embedded in a parent (via `configs.create(shape)` with no `options`)
84
+ * shares its parent's `rootStores`; one created with its own `sources` (root form) resolves
85
+ * independently, tracked via `ownsResolution`.
86
+ */
87
+ declare class ConfigNodeState<S extends SchemaShape> {
88
+ readonly shape: S;
89
+ private readonly basePath;
90
+ private readonly ownSources;
91
+ readonly ownsResolution: boolean;
92
+ /** The sources this node's (or an ancestor's) `close()` actually closes. */
93
+ private readonly closableSources;
94
+ private rootStores;
95
+ private readonly fields;
96
+ private readonly children;
97
+ private snapshotStore;
98
+ readonly readyPromise: Promise<void>;
99
+ constructor(shape: S, rootStores: Store<any>[], basePath: string[], ownSources: Source<any>[], ownsResolution: boolean,
100
+ /** The sources this node's (or an ancestor's) `close()` actually closes. */
101
+ closableSources: Source<any>[]);
102
+ private wireLiveUpdates;
103
+ private resolveField;
104
+ private refreshFields;
105
+ private refreshSnapshot;
106
+ fieldFor(key: string): ConfigField<any>;
107
+ childNode(key: string): object;
108
+ get(): InferShape<S>;
109
+ set(key: string): never;
110
+ private ensureSnapshotStore;
111
+ subscribe(subscriber: Subscriber<InferShape<S>>): Unsubscribe;
112
+ listen(subscriber: Subscriber<InferShape<S>>): Unsubscribe;
113
+ close(): Promise<void>;
114
+ }
115
+ /**
116
+ * Shared, non-thenable base behind both `ConfigNode` and `ConfigNodeResolved`: a resolved node
117
+ * must not itself be thenable (awaiting a thenable whose `then()` resolves to itself is a
118
+ * chaining cycle, both at runtime per the Promise spec and in TypeScript's `Awaited<T>`, which
119
+ * rejects the type outright with a "referenced directly or indirectly in the fulfillment
120
+ * callback of its own `then` method" error) — so `then()` lives only on `ConfigNode`, added by a
121
+ * sibling subclass rather than inherited.
122
+ */
123
+ declare class ConfigNodeCore<S extends SchemaShape> {
124
+ readonly _state: ConfigNodeState<S>;
125
+ constructor(_state: ConfigNodeState<S>);
126
+ get shape(): S;
127
+ /** Snapshot of every field's current value, recursing into nested groups. */
128
+ get(): InferShape<S>;
129
+ /** Always throws: config values are read-only. */
130
+ set<K extends keyof S & string>(key: K, _value: InferShape<S>[K]): void;
131
+ /** Calls `subscriber` immediately with the current snapshot (per `get()`), then again on every subsequent change to it. */
132
+ subscribe(subscriber: Subscriber<InferShape<S>>): Unsubscribe;
133
+ /** Calls `subscriber` only on a subsequent change to the snapshot, not with the current one. */
134
+ listen(subscriber: Subscriber<InferShape<S>>): Unsubscribe;
135
+ /** Closes every source backing this config tree, including nested groups' own. A no-op for a nested-form group of its own. */
136
+ close(): Promise<void>;
137
+ /** Enables `await using s = await configs.create(...)`: disposal closes the config tree. */
138
+ [Symbol.asyncDispose](): Promise<void>;
139
+ }
140
+ /** A `ConfigNode` once every source behind it has published its first snapshot. Synchronous only — no longer thenable. */
141
+ export declare class ConfigNodeResolved<S extends SchemaShape> extends ConfigNodeCore<S> {
142
+ }
143
+ /**
144
+ * `configs.create()`'s return value: synchronous and read-only, but also a `PromiseLike` —
145
+ * awaiting it resolves once every source has published its first snapshot, yielding a
146
+ * `ConfigNodeResolved`. A nested group (`configs.create(shape)` with no `options`) never needs
147
+ * awaiting: it shares its parent's already-resolved values unless given sources of its own.
148
+ */
149
+ export declare class ConfigNode<S extends SchemaShape> extends ConfigNodeCore<S> implements PromiseLike<ConfigNodeResolved<S> & InferReadOnlyAccessors<S>> {
150
+ /**
151
+ * `ConfigNodeResolved` is a sibling class, not a subclass (see the note on `ConfigNodeCore`),
152
+ * so it fails a plain `instanceof ConfigNode` check. Custom `Symbol.hasInstance` widens that
153
+ * check to "any `ConfigNodeCore`" instead, which both classes satisfy — without giving
154
+ * `ConfigNodeResolved` a real `then()` and reintroducing the chaining cycle.
155
+ */
156
+ static [Symbol.hasInstance](instance: unknown): boolean;
157
+ then<TResult1 = ConfigNodeResolved<S> & InferReadOnlyAccessors<S>, TResult2 = never>(onfulfilled?: ((value: ConfigNodeResolved<S> & InferReadOnlyAccessors<S>) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | undefined | null): PromiseLike<TResult1 | TResult2>;
158
+ }
159
+ /**
160
+ * A nested config group embedded in a parent shape: either shares the parent's resolved values,
161
+ * or — when created with its own `sources` — resolves independently of it.
162
+ */
163
+ export type SchemaGroup<S extends SchemaShape = SchemaShape> = ConfigNode<S> & InferReadOnlyAccessors<S>;
164
+ /**
165
+ * Nested-group form (no `options`): synchronous, no sources of its own — inherits the
166
+ * parent's resolved values when embedded, unless `options` gives it its own.
167
+ *
168
+ * Root form (`options` given): opens every source. Returns synchronously as a `ConfigNode`
169
+ * and is awaitable — for each field, the first source (in array order) whose snapshot has
170
+ * that field wins; a field missing everywhere falls back to the field's own `default`, else `null`.
171
+ */
172
+ export declare function createConfigNode<S extends SchemaShape>(shape: S, options?: CreateOptions): ConfigNode<S> & InferReadOnlyAccessors<S>;
173
+ /** The type of the `configs` namespace object. */
174
+ export interface configs {
175
+ create<S extends SchemaShape>(shape: S, options?: CreateOptions): ConfigNode<S> & InferReadOnlyAccessors<S>;
176
+ }
177
+ export {};
@@ -0,0 +1,21 @@
1
+ import type { configs as ConfigsApi } from "./config.types";
2
+ export type { CreateOptions, FieldSchema, FieldType, InferReadOnlyAccessors, InferShape, ReadOnlyStore, SchemaGroup, SchemaNode, SchemaShape, } from "./config.types";
3
+ export type { SourceControl, UnderlyingSource } from "./types";
4
+ export type { DataType, DataTypeName } from "./utils/data-types";
5
+ export type { EnvSourceOptions, EnvKeyMapper } from "./sources/env";
6
+ export type { FetchSourceOptions } from "./sources/fetch";
7
+ export type { SseSourceOptions } from "./sources/sse";
8
+ export type { FileSourceOptions } from "./sources/file";
9
+ export { ConfigNode, ConfigNodeResolved } from "./config.types";
10
+ export { Source } from "./sources/source";
11
+ export { envSource, mapKey } from "./sources/env";
12
+ export { fetchSource } from "./sources/fetch";
13
+ export { sseSource } from "./sources/sse";
14
+ export { fileSource } from "./sources/file";
15
+ export { literalSource } from "./sources/literal";
16
+ export { Store } from "./utils/store";
17
+ export { DataTypes } from "./utils/data-types";
18
+ export { DotEnv } from "./utils/dotenv";
19
+ export { ConfigError } from "./errors";
20
+ export declare const configs: ConfigsApi;
21
+ export default configs;