@jayalfredprufrock/confetti 0.1.2 → 0.2.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 +243 -0
- package/dist/index.cjs +163 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -28
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +88 -28
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +150 -86
- package/dist/index.mjs.map +1 -1
- package/package.json +22 -22
package/README.md
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./confetti-logo.png" alt="confetti" width="240" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">confetti</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Type-safe, multi-environment configuration where <strong>your config file is the source of truth</strong>.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Why
|
|
14
|
+
|
|
15
|
+
- **Ultra type-safe.** Paths and return values are fully inferred — `get('nested.prop')` gives you autocomplete and the correct type, no casting.
|
|
16
|
+
- **Environments, side-by-side.** Staging and prod values live next to each other in the config, not scattered across files.
|
|
17
|
+
- **Source of truth**. Making something configurable only requires touching one file. Start with a default across environments and easily override per environment later.
|
|
18
|
+
- **Consumers resolve special values.** `confetti` tracks _what_ a value is (an env var, a remote secret, a default) and hands that metadata to the consumer — it doesn't dial AWS for you.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm add @jayalfredprufrock/confetti
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Basic usage
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { makeConfig, DEFAULT, ENV, DATA, TYPE } from "@jayalfredprufrock/confetti";
|
|
30
|
+
|
|
31
|
+
const config = makeConfig({
|
|
32
|
+
appName: "my-app",
|
|
33
|
+
port: 3000,
|
|
34
|
+
|
|
35
|
+
feature: {
|
|
36
|
+
enabled: true,
|
|
37
|
+
limit: 50,
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
apiUrl: {
|
|
41
|
+
[DEFAULT]: "http://localhost:3000",
|
|
42
|
+
staging: "https://api.staging.example.com",
|
|
43
|
+
prod: "https://api.example.com",
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
dbPassword: {
|
|
47
|
+
[ENV]: "DB_PASSWORD",
|
|
48
|
+
[DATA]: "db/password",
|
|
49
|
+
[DEFAULT]: "",
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
maxConnections: {
|
|
53
|
+
[TYPE]: "number",
|
|
54
|
+
[ENV]: "MAX_CONNECTIONS",
|
|
55
|
+
[DEFAULT]: 10,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Pick an environment, then read values.
|
|
60
|
+
const cfg = config("prod");
|
|
61
|
+
|
|
62
|
+
// paths and types fully inferred
|
|
63
|
+
cfg.get("appName"); // => 'my-app'
|
|
64
|
+
cfg.get("apiUrl"); // => 'https://api.example.com'
|
|
65
|
+
cfg.get("port"); // => 3000 (typed as number)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Factory/Function pattern
|
|
69
|
+
|
|
70
|
+
Use the factory form when it's more convenient to produce multi-environment default values based on a naming convention:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const config = makeConfig((env: string) => ({
|
|
74
|
+
serviceName: `my-app-${env}`,
|
|
75
|
+
dbPassword: {
|
|
76
|
+
[ENV]: "DB_PASSWORD",
|
|
77
|
+
[DATA]: `${env}/password`,
|
|
78
|
+
[DEFAULT]: "",
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Individual values can also be functions, which also provides an escape hatch if you need to provide an object as a config leaf value.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const config = makeConfig({
|
|
87
|
+
serviceName: (env) => `my-app-${env}`,
|
|
88
|
+
objValue: (env) => ({ enabled: true, value: 42 }),
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Sync reads with `get`
|
|
93
|
+
|
|
94
|
+
`get(path)` is synchronous. Per-env values are selected by precedence:
|
|
95
|
+
|
|
96
|
+
1. explicit value (no multi-env object used)
|
|
97
|
+
2. `process.env[ENV]` if set (coerced per `[TYPE]` — see below)
|
|
98
|
+
3. the explicit per-env value (`cfg.get('apiUrl')` in `prod` returns the `prod` value)
|
|
99
|
+
4. `[DEFAULT]`, if present
|
|
100
|
+
5. otherwise, throw
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
cfg.get("apiUrl"); // 'https://api.example.com'
|
|
104
|
+
cfg.get("feature.enabled"); // true (typed as boolean)
|
|
105
|
+
|
|
106
|
+
// Entire subtrees are fine too — everything resolves synchronously.
|
|
107
|
+
cfg.get("feature"); // { enabled: true, limit: 50 }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
_If a leaf cannot be resolved syncronously, `get` throws. See `resolve` below for handling async configuration._
|
|
111
|
+
|
|
112
|
+
### Declaring leaf types with `[TYPE]`
|
|
113
|
+
|
|
114
|
+
External values (env vars, fetched secrets) are strings by nature but your config likely wants them typed. Use `[TYPE]` to declare the runtime shape and drive both TypeScript inference and automatic coercion.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const config = makeConfig({
|
|
118
|
+
port: { [TYPE]: "number", [ENV]: "PORT", [DEFAULT]: 3000 },
|
|
119
|
+
featureFlag: { [TYPE]: "boolean", [DATA]: "flags/checkout-v2" },
|
|
120
|
+
allowedOrigins: { [TYPE]: "string[]", [ENV]: "ALLOWED_ORIGINS", [DEFAULT]: [] },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const cfg = config("prod");
|
|
124
|
+
cfg.get("port"); // typed as number — env var "8080" coerced to 8080
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Supported tags: `"string" | "number" | "boolean" | "string[]" | "number[]" | "boolean[]"`.
|
|
128
|
+
|
|
129
|
+
**Coercion rules (env vars and string fetcher returns):**
|
|
130
|
+
|
|
131
|
+
| Tag | Expected raw |
|
|
132
|
+
| --------- | --------------------------------------------------- |
|
|
133
|
+
| `string` | as-is |
|
|
134
|
+
| `number` | `Number(raw)`; empty or `NaN` throws |
|
|
135
|
+
| `boolean` | exactly `"true"` or `"false"`; anything else throws |
|
|
136
|
+
| `T[]` | `JSON.parse` + array check + element type check |
|
|
137
|
+
|
|
138
|
+
`[TYPE]` also constrains `[DEFAULT]` and per-env values at compile time — `{ [TYPE]: "number", [DEFAULT]: "nope" }` is a type error.
|
|
139
|
+
|
|
140
|
+
**When `[ENV]` or `[DATA]` are present without `[TYPE]`:** values are required to be strings (both `[DEFAULT]` and per-env overrides). The fetcher must also return a string. If you need a non-string here, add `[TYPE]`.
|
|
141
|
+
|
|
142
|
+
### Async reads with `resolve`
|
|
143
|
+
|
|
144
|
+
`resolve(path, fetcher)` hands off to your code whenever a leaf can't be satisfied synchronously. You decide how to resolve it — read AWS Secrets Manager, call Vault, hit Parameter Store, whatever.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const password = await cfg.resolve("dbPassword", async (ctx) => {
|
|
148
|
+
// ctx = { env: 'prod', envVar: 'DB_PASSWORD', data: 'prod/db/password', default: '' }
|
|
149
|
+
const secret = await secretsClient.getSecretValue({ SecretId: ctx.data });
|
|
150
|
+
return secret.SecretString;
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Rules:
|
|
155
|
+
|
|
156
|
+
- Explicit values and env overrides still win — the fetcher is only called when there's no sync value.
|
|
157
|
+
- Return `undefined` from the fetcher to fall back to `[DEFAULT]`.
|
|
158
|
+
- If `[TYPE]` is declared, a string return is coerced; a non-string must match `[TYPE]` exactly or it throws.
|
|
159
|
+
- Resolving a subtree calls the fetcher once per leaf that needs it; static leaves pass through untouched.
|
|
160
|
+
|
|
161
|
+
### Walking the config with `entries`
|
|
162
|
+
|
|
163
|
+
`entries()` returns a lazy iterator that yields `[path, entry]` for every leaf. Use it to drive downstream tooling — synthesize IaC secret resources, check for unset values in CI, etc.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
for (const [path, entry] of cfg.entries()) {
|
|
167
|
+
if (entry.envVar) {
|
|
168
|
+
console.log(`${path} ← process.env.${entry.envVar}`);
|
|
169
|
+
}
|
|
170
|
+
if (entry.data) {
|
|
171
|
+
console.log(`${path} ← secret @ ${entry.data}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Each entry has the shape:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
{ path: string; value?: unknown; default?: unknown; envVar?: string; data?: unknown; type?: TypeTag }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`value` is present if a syncronous value is available — useful for distinguishing "already known" from "needs fetching" without resolving anything.
|
|
183
|
+
|
|
184
|
+
Pass a subtree path to scope iteration to that part of the config. Paths are typed — only subtree paths compile, leaves are rejected.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
for (const [path, entry] of cfg.entries("feature")) {
|
|
188
|
+
// path is e.g. "feature.enabled", "feature.limit"
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Real-world use cases
|
|
193
|
+
|
|
194
|
+
### Generate a required env var list for your deploy pipeline
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const required = Array.from(cfg.entries())
|
|
198
|
+
.filter(([, entry]) => entry.envVar && entry.value === undefined)
|
|
199
|
+
.map(([, entry]) => entry.envVar!);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Synthesize Terraform / Pulumi secret resources
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
for (const [path, entry] of cfg.entries()) {
|
|
206
|
+
if (!entry.data) continue;
|
|
207
|
+
new aws.secretsmanager.Secret(path, { name: entry.data as string });
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Fetch everything you need at boot
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const secrets = await cfg.resolve("secrets", async ({ data }) => {
|
|
215
|
+
return await secretsClient
|
|
216
|
+
.getSecretValue({ SecretId: data as string })
|
|
217
|
+
.then((r) => r.SecretString);
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Validate config is ready before starting
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
for (const [path, entry] of cfg.entries()) {
|
|
225
|
+
if (entry.value === undefined && entry.default === undefined && !entry.data) {
|
|
226
|
+
throw new Error(`Config '${path}' has no resolvable value.`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## API
|
|
232
|
+
|
|
233
|
+
| Method | Signature | Notes |
|
|
234
|
+
| ------------------ | --------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
235
|
+
| `makeConfig` | `(config \| (env) => config) => Confetti` | Accepts a config object or factory fn. |
|
|
236
|
+
| `confetti(env)` | `(env: string) => Accessor` | Binds an environment. |
|
|
237
|
+
| `accessor.get` | `(path) => value` | Sync. Throws if async resolution is required. |
|
|
238
|
+
| `accessor.resolve` | `(path, fetcher) => Promise<value>` | Async. Fetcher invoked per leaf that needs it. |
|
|
239
|
+
| `accessor.entries` | `(startPath?) => IterableIterator<[string, Entry]>` | Lazy iterator of every leaf with its metadata; optionally scoped to a subtree. |
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -2,115 +2,179 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
//#region src/symbols.ts
|
|
3
3
|
const CONFETTI = Symbol.for("CONFETTI");
|
|
4
4
|
const DEFAULT = Symbol.for("CONFETTI_DEFAULT");
|
|
5
|
-
const
|
|
5
|
+
const ENV = Symbol.for("CONFETTI_ENV");
|
|
6
6
|
const DATA = Symbol.for("CONFETTI_DATA");
|
|
7
|
+
const TYPE = Symbol.for("CONFETTI_TYPE");
|
|
7
8
|
//#endregion
|
|
8
9
|
//#region src/util.ts
|
|
9
10
|
const isObj = (val) => {
|
|
10
11
|
return val !== null && typeof val === "object" && !Array.isArray(val);
|
|
11
12
|
};
|
|
12
13
|
const isConfigValPerEnv = (val) => {
|
|
13
|
-
return isObj(val) && (DEFAULT in val || DATA in val ||
|
|
14
|
+
return isObj(val) && (DEFAULT in val || DATA in val || ENV in val || TYPE in val);
|
|
15
|
+
};
|
|
16
|
+
const isNestedConfig = (val) => {
|
|
17
|
+
return isObj(val) && !isConfigValPerEnv(val);
|
|
14
18
|
};
|
|
15
19
|
const isConfetti = (val) => {
|
|
16
20
|
return typeof val === "function" && Object.hasOwn(val, CONFETTI);
|
|
17
21
|
};
|
|
18
|
-
const
|
|
19
|
-
return
|
|
20
|
-
const path = `${_currentPath}${key}`;
|
|
21
|
-
if (isObj(value)) {
|
|
22
|
-
if (isConfigValPerEnv(value)) {
|
|
23
|
-
let unresolvedValue;
|
|
24
|
-
if (value[ENV_VAR] && process.env[value[ENV_VAR]] !== void 0) try {
|
|
25
|
-
unresolvedValue = JSON.parse(process.env[value[ENV_VAR]]);
|
|
26
|
-
} catch {
|
|
27
|
-
unresolvedValue = process.env[value[ENV_VAR]];
|
|
28
|
-
}
|
|
29
|
-
else if (value[env] !== void 0) unresolvedValue = value[env];
|
|
30
|
-
else if (value[DEFAULT] !== void 0) unresolvedValue = value[DEFAULT];
|
|
31
|
-
if (unresolvedValue === void 0) throw new Error(`Unable to find '${env}' config value at '${path}`);
|
|
32
|
-
return map({
|
|
33
|
-
path,
|
|
34
|
-
unresolvedValue,
|
|
35
|
-
envVar: value[ENV_VAR],
|
|
36
|
-
data: value[DATA]
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
return configFlatMap(env, value, map, `${path}.`);
|
|
40
|
-
}
|
|
41
|
-
return map({
|
|
42
|
-
path,
|
|
43
|
-
unresolvedValue: value
|
|
44
|
-
});
|
|
45
|
-
});
|
|
22
|
+
const isThenable = (v) => {
|
|
23
|
+
return v !== null && typeof v === "object" && "then" in v && typeof v.then === "function";
|
|
46
24
|
};
|
|
47
|
-
const
|
|
25
|
+
const getAtPath = (config, path) => {
|
|
26
|
+
if (!path) return config;
|
|
48
27
|
const segments = path.split(".");
|
|
49
|
-
let
|
|
50
|
-
for (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (!isObj(objAtSeg[segment])) objAtSeg[segment] = {};
|
|
55
|
-
objAtSeg = objAtSeg[segment];
|
|
56
|
-
}
|
|
28
|
+
let node = config;
|
|
29
|
+
for (const segment of segments) {
|
|
30
|
+
if (!isNestedConfig(node)) throw new Error(`Invalid config path '${path}'.`);
|
|
31
|
+
node = node[segment];
|
|
32
|
+
if (node === void 0) throw new Error(`Invalid config path '${path}'.`);
|
|
57
33
|
}
|
|
34
|
+
return node;
|
|
35
|
+
};
|
|
36
|
+
const matchesType = (val, type) => {
|
|
37
|
+
if (type === "string") return typeof val === "string";
|
|
38
|
+
if (type === "number") return typeof val === "number" && !Number.isNaN(val);
|
|
39
|
+
if (type === "boolean") return typeof val === "boolean";
|
|
40
|
+
if (!Array.isArray(val)) return false;
|
|
41
|
+
const elem = type.slice(0, -2);
|
|
42
|
+
return val.every((x) => typeof x === elem);
|
|
43
|
+
};
|
|
44
|
+
const coerceFromString = (raw, type, path) => {
|
|
45
|
+
if (type === "string") return raw;
|
|
46
|
+
if (type === "number") {
|
|
47
|
+
if (raw === "") throw new Error(`Cannot coerce empty string to number at '${path}'.`);
|
|
48
|
+
const n = Number(raw);
|
|
49
|
+
if (Number.isNaN(n)) throw new Error(`Cannot coerce '${raw}' to number at '${path}'.`);
|
|
50
|
+
return n;
|
|
51
|
+
}
|
|
52
|
+
if (type === "boolean") {
|
|
53
|
+
if (raw === "true") return true;
|
|
54
|
+
if (raw === "false") return false;
|
|
55
|
+
throw new Error(`Cannot coerce '${raw}' to boolean at '${path}' (expected 'true' or 'false').`);
|
|
56
|
+
}
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(raw);
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(`Cannot coerce '${raw}' to ${type} at '${path}' (invalid JSON).`);
|
|
62
|
+
}
|
|
63
|
+
if (!Array.isArray(parsed)) throw new Error(`Cannot coerce '${raw}' to ${type} at '${path}' (not an array).`);
|
|
64
|
+
const elem = type.slice(0, -2);
|
|
65
|
+
if (!parsed.every((x) => typeof x === elem)) throw new Error(`Cannot coerce '${raw}' to ${type} at '${path}' (element type mismatch).`);
|
|
66
|
+
return parsed;
|
|
67
|
+
};
|
|
68
|
+
const coerceFetched = (fetched, type, path) => {
|
|
69
|
+
if (type === void 0) {
|
|
70
|
+
if (typeof fetched !== "string") throw new Error(`Fetcher for '${path}' must return a string (add [TYPE] to use non-string values).`);
|
|
71
|
+
return fetched;
|
|
72
|
+
}
|
|
73
|
+
if (typeof fetched === "string") return coerceFromString(fetched, type, path);
|
|
74
|
+
if (matchesType(fetched, type)) return fetched;
|
|
75
|
+
throw new Error(`Fetcher for '${path}' returned value that doesn't match [TYPE] '${type}'.`);
|
|
76
|
+
};
|
|
77
|
+
const buildEntry = (node, path, env) => {
|
|
78
|
+
if (!isConfigValPerEnv(node)) return {
|
|
79
|
+
path,
|
|
80
|
+
value: node
|
|
81
|
+
};
|
|
82
|
+
const envVar = node[ENV];
|
|
83
|
+
const data = node[DATA];
|
|
84
|
+
const defaultVal = node[DEFAULT];
|
|
85
|
+
const typeTag = node[TYPE];
|
|
86
|
+
const entry = { path };
|
|
87
|
+
if (envVar && process.env[envVar] !== void 0) {
|
|
88
|
+
const raw = process.env[envVar];
|
|
89
|
+
entry.value = typeTag ? coerceFromString(raw, typeTag, path) : raw;
|
|
90
|
+
} else if (node[env] !== void 0) entry.value = node[env];
|
|
91
|
+
if (defaultVal !== void 0) entry.default = defaultVal;
|
|
92
|
+
if (envVar) entry.envVar = envVar;
|
|
93
|
+
if (data !== void 0) entry.data = data;
|
|
94
|
+
if (typeTag !== void 0) entry.type = typeTag;
|
|
95
|
+
return entry;
|
|
96
|
+
};
|
|
97
|
+
function* entriesIter(config, env, pathPrefix = "") {
|
|
98
|
+
for (const [key, value] of Object.entries(config)) {
|
|
99
|
+
const path = pathPrefix + key;
|
|
100
|
+
if (isNestedConfig(value)) yield* entriesIter(value, env, `${path}.`);
|
|
101
|
+
else yield [path, buildEntry(value, path, env)];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const invokeSync = (source, path, env) => {
|
|
105
|
+
if (typeof source !== "function") return source;
|
|
106
|
+
const result = source(env);
|
|
107
|
+
if (isThenable(result)) throw new Error(`Config at '${path}' requires async resolution (use resolve).`);
|
|
108
|
+
return result;
|
|
109
|
+
};
|
|
110
|
+
const invokeAsync = async (source, env) => {
|
|
111
|
+
return typeof source === "function" ? await source(env) : source;
|
|
112
|
+
};
|
|
113
|
+
const resolveEntrySync = (entry, env) => {
|
|
114
|
+
if ("value" in entry) return invokeSync(entry.value, entry.path, env);
|
|
115
|
+
if ("default" in entry) return invokeSync(entry.default, entry.path, env);
|
|
116
|
+
throw new Error(`Config at '${entry.path}' requires async resolution (use resolve).`);
|
|
117
|
+
};
|
|
118
|
+
const resolveEntryAsync = async (entry, env, fetcher) => {
|
|
119
|
+
if ("value" in entry) return invokeAsync(entry.value, env);
|
|
120
|
+
if (entry.envVar !== void 0 || entry.data !== void 0) {
|
|
121
|
+
const fetched = await fetcher({
|
|
122
|
+
env,
|
|
123
|
+
default: entry.default,
|
|
124
|
+
envVar: entry.envVar,
|
|
125
|
+
data: entry.data,
|
|
126
|
+
type: entry.type
|
|
127
|
+
});
|
|
128
|
+
if (fetched !== void 0) return coerceFetched(fetched, entry.type, entry.path);
|
|
129
|
+
}
|
|
130
|
+
if ("default" in entry) return invokeAsync(entry.default, env);
|
|
131
|
+
throw new Error(`Unable to resolve config value at '${entry.path}'.`);
|
|
132
|
+
};
|
|
133
|
+
const resolveSubtreeSync = (node, prefix, env) => {
|
|
134
|
+
const result = {};
|
|
135
|
+
for (const [key, child] of Object.entries(node)) {
|
|
136
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
137
|
+
result[key] = isNestedConfig(child) ? resolveSubtreeSync(child, path, env) : resolveEntrySync(buildEntry(child, path, env), env);
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
};
|
|
141
|
+
const resolveSubtreeAsync = async (node, prefix, env, fetcher) => {
|
|
142
|
+
const keys = Object.keys(node);
|
|
143
|
+
const values = await Promise.all(keys.map((key) => {
|
|
144
|
+
const child = node[key];
|
|
145
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
146
|
+
return isNestedConfig(child) ? resolveSubtreeAsync(child, path, env, fetcher) : resolveEntryAsync(buildEntry(child, path, env), env, fetcher);
|
|
147
|
+
}));
|
|
148
|
+
const result = {};
|
|
149
|
+
keys.forEach((key, i) => {
|
|
150
|
+
result[key] = values[i];
|
|
151
|
+
});
|
|
152
|
+
return result;
|
|
58
153
|
};
|
|
59
154
|
//#endregion
|
|
60
155
|
//#region src/make-config.ts
|
|
61
|
-
const makeConfig = (
|
|
156
|
+
const makeConfig = (input) => {
|
|
157
|
+
const factory = typeof input === "function" ? input : () => input;
|
|
62
158
|
const confetti = (env) => {
|
|
63
|
-
const config =
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const context = contextByPath[path];
|
|
68
|
-
if (!context) throw new Error(`Invalid config path '${path}'.`);
|
|
69
|
-
const { unresolvedValue } = context;
|
|
70
|
-
const value = typeof unresolvedValue === "function" ? await unresolvedValue(context) : unresolvedValue;
|
|
71
|
-
if (value === void 0) throw new Error(`Config value at '${path}' resolved to undefined.`);
|
|
72
|
-
return value;
|
|
73
|
-
};
|
|
74
|
-
const resolveValueSync = (path) => {
|
|
75
|
-
const context = contextByPath[path];
|
|
76
|
-
if (!context) throw new Error(`Invalid config path '${path}'.`);
|
|
77
|
-
const { unresolvedValue } = context;
|
|
78
|
-
if (typeof unresolvedValue === "function") throw new Error(`Cannot resolve config value at "${path}" synchronously.`);
|
|
79
|
-
return unresolvedValue;
|
|
80
|
-
};
|
|
81
|
-
const resolve = async () => {
|
|
82
|
-
const resolvedConfig = {};
|
|
83
|
-
const promises = Object.keys(contextByPath).map(async (path) => {
|
|
84
|
-
return resolveValue(path).then((resolvedValue) => setAtPath(resolvedConfig, path, resolvedValue));
|
|
85
|
-
});
|
|
86
|
-
await Promise.all(promises);
|
|
87
|
-
return resolvedConfig;
|
|
159
|
+
const config = factory(env);
|
|
160
|
+
const get = (path) => {
|
|
161
|
+
const node = getAtPath(config, path);
|
|
162
|
+
return isNestedConfig(node) ? resolveSubtreeSync(node, path, env) : resolveEntrySync(buildEntry(node, path, env), env);
|
|
88
163
|
};
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
return resolvedConfig;
|
|
164
|
+
const resolve = async (path, fetcher) => {
|
|
165
|
+
const node = getAtPath(config, path);
|
|
166
|
+
return isNestedConfig(node) ? await resolveSubtreeAsync(node, path, env, fetcher) : await resolveEntryAsync(buildEntry(node, path, env), env, fetcher);
|
|
93
167
|
};
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!envVar) return [];
|
|
99
|
-
return resolveValue(context.path).then((resolvedValue) => {
|
|
100
|
-
envVars[envVar] = JSON.stringify(resolvedValue);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
await Promise.all(promises);
|
|
104
|
-
return envVars;
|
|
168
|
+
const entries = (startPath) => {
|
|
169
|
+
const root = startPath ? getAtPath(config, startPath) : config;
|
|
170
|
+
if (!isNestedConfig(root)) throw new Error(`'${startPath}' is not a config subtree.`);
|
|
171
|
+
return entriesIter(root, env, startPath ? `${startPath}.` : "");
|
|
105
172
|
};
|
|
106
173
|
return {
|
|
107
174
|
config,
|
|
108
|
-
|
|
109
|
-
resolveValue,
|
|
110
|
-
resolveValueSync,
|
|
175
|
+
get,
|
|
111
176
|
resolve,
|
|
112
|
-
|
|
113
|
-
resolveEnv
|
|
177
|
+
entries
|
|
114
178
|
};
|
|
115
179
|
};
|
|
116
180
|
confetti[CONFETTI] = "CONFETTI";
|
|
@@ -120,12 +184,23 @@ const makeConfig = (makeConfig) => {
|
|
|
120
184
|
exports.CONFETTI = CONFETTI;
|
|
121
185
|
exports.DATA = DATA;
|
|
122
186
|
exports.DEFAULT = DEFAULT;
|
|
123
|
-
exports.
|
|
124
|
-
exports.
|
|
187
|
+
exports.ENV = ENV;
|
|
188
|
+
exports.TYPE = TYPE;
|
|
189
|
+
exports.buildEntry = buildEntry;
|
|
190
|
+
exports.coerceFromString = coerceFromString;
|
|
191
|
+
exports.entriesIter = entriesIter;
|
|
192
|
+
exports.getAtPath = getAtPath;
|
|
193
|
+
exports.invokeAsync = invokeAsync;
|
|
194
|
+
exports.invokeSync = invokeSync;
|
|
125
195
|
exports.isConfetti = isConfetti;
|
|
126
196
|
exports.isConfigValPerEnv = isConfigValPerEnv;
|
|
197
|
+
exports.isNestedConfig = isNestedConfig;
|
|
127
198
|
exports.isObj = isObj;
|
|
199
|
+
exports.isThenable = isThenable;
|
|
128
200
|
exports.makeConfig = makeConfig;
|
|
129
|
-
exports.
|
|
201
|
+
exports.resolveEntryAsync = resolveEntryAsync;
|
|
202
|
+
exports.resolveEntrySync = resolveEntrySync;
|
|
203
|
+
exports.resolveSubtreeAsync = resolveSubtreeAsync;
|
|
204
|
+
exports.resolveSubtreeSync = resolveSubtreeSync;
|
|
130
205
|
|
|
131
206
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/symbols.ts","../src/util.ts","../src/make-config.ts"],"sourcesContent":["export const CONFETTI: unique symbol = Symbol.for(\"CONFETTI\");\nexport const DEFAULT: unique symbol = Symbol.for(\"CONFETTI_DEFAULT\");\nexport const ENV_VAR: unique symbol = Symbol.for(\"CONFETTI_ENV_VAR\");\nexport const DATA: unique symbol = Symbol.for(\"CONFETTI_DATA\");\n","import { CONFETTI, DATA, DEFAULT, ENV_VAR } from \"./symbols\";\nimport type { Confetti, ConfigFlatMapContext, ConfigVal, ConfigValPerEnv, Obj } from \"./types\";\n\nexport const isObj = (val: unknown): val is Obj => {\n return val !== null && typeof val === \"object\" && !Array.isArray(val);\n};\n\nexport const isConfigValPerEnv = (val: unknown): val is ConfigValPerEnv => {\n return isObj(val) && (DEFAULT in val || DATA in val || ENV_VAR in val);\n};\n\nexport const isConfetti = (val: unknown): val is Confetti<any> => {\n return typeof val === \"function\" && Object.hasOwn(val, CONFETTI);\n};\n\nexport const configFlatMap = <T>(\n env: string,\n obj: Obj,\n map: (context: ConfigFlatMapContext) => T | T[],\n _currentPath = \"\",\n): T[] => {\n return Object.entries(obj).flatMap(([key, value]) => {\n const path = `${_currentPath}${key}`;\n\n if (isObj(value)) {\n if (isConfigValPerEnv(value)) {\n let unresolvedValue: ConfigVal | undefined;\n\n // first resolve environment variable if it exists\n if (value[ENV_VAR] && process.env[value[ENV_VAR]] !== undefined) {\n try {\n unresolvedValue = JSON.parse(process.env[value[ENV_VAR]]!);\n } catch {\n unresolvedValue = process.env[value[ENV_VAR]];\n }\n }\n // second resolve any explicit environment value\n else if (value[env] !== undefined) {\n unresolvedValue = value[env];\n }\n // finally look for a default value\n else if (value[DEFAULT] !== undefined) {\n unresolvedValue = value[DEFAULT];\n }\n\n if (unresolvedValue === undefined) {\n throw new Error(`Unable to find '${env}' config value at '${path}`);\n }\n\n return map({ path, unresolvedValue, envVar: value[ENV_VAR], data: value[DATA] });\n }\n\n return configFlatMap(env, value, map, `${path}.`);\n }\n\n return map({ path, unresolvedValue: value as ConfigVal });\n });\n};\n\nexport const setAtPath = (obj: Obj, path: string, value: unknown) => {\n const segments = path.split(\".\");\n\n let objAtSeg: any = obj;\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i] ?? \"\";\n if (segments.length === i + 1) {\n objAtSeg[segment] = value;\n } else {\n if (!isObj(objAtSeg[segment])) {\n objAtSeg[segment] = {};\n }\n objAtSeg = objAtSeg[segment];\n }\n }\n};\n","import { CONFETTI } from \"./symbols\";\nimport type { Confetti, Config, ConfigFlatMapContext, Obj, ResolvedConfig } from \"./types\";\nimport { configFlatMap, setAtPath } from \"./util\";\n\nexport const makeConfig = <C extends Config>(makeConfig: (env: string) => C): Confetti<C> => {\n const confetti = (env: string) => {\n const config = makeConfig(env);\n\n const flatMap = <T>(transform: (context: ConfigFlatMapContext) => T | T[]) =>\n configFlatMap<T>(env, config, transform);\n\n const contextByPath = Object.fromEntries(flatMap((context) => [[context.path, context]]));\n\n const resolveValue = async (path: string): Promise<any> => {\n const context = contextByPath[path];\n if (!context) throw new Error(`Invalid config path '${path}'.`);\n const { unresolvedValue } = context;\n const value =\n typeof unresolvedValue === \"function\" ? await unresolvedValue(context) : unresolvedValue;\n if (value === undefined) {\n throw new Error(`Config value at '${path}' resolved to undefined.`);\n }\n return value;\n };\n\n const resolveValueSync = (path: string): any => {\n const context = contextByPath[path];\n if (!context) throw new Error(`Invalid config path '${path}'.`);\n const { unresolvedValue } = context;\n if (typeof unresolvedValue === \"function\") {\n throw new Error(`Cannot resolve config value at \"${path}\" synchronously.`);\n }\n return unresolvedValue;\n };\n\n const resolve = async (): Promise<any> => {\n const resolvedConfig: Obj = {};\n const promises = Object.keys(contextByPath).map(async (path) => {\n return resolveValue(path).then((resolvedValue) =>\n setAtPath(resolvedConfig, path, resolvedValue),\n );\n });\n\n await Promise.all(promises);\n\n return resolvedConfig;\n };\n\n const resolveSync = (): any => {\n const resolvedConfig: Obj = {};\n for (const path of Object.keys(contextByPath)) {\n setAtPath(resolvedConfig, path, resolveValueSync(path));\n }\n return resolvedConfig as ResolvedConfig<C>;\n };\n\n const resolveEnv = async (): Promise<any> => {\n const envVars: Obj<string> = {};\n const promises = Object.values(contextByPath).flatMap(async (context) => {\n const { envVar } = context;\n if (!envVar) return [];\n return resolveValue(context.path).then((resolvedValue) => {\n envVars[envVar] = JSON.stringify(resolvedValue);\n });\n });\n\n await Promise.all(promises);\n\n return envVars;\n };\n\n return {\n config,\n flatMap,\n resolveValue,\n resolveValueSync,\n resolve,\n resolveSync,\n resolveEnv,\n };\n };\n\n confetti[CONFETTI] = \"CONFETTI\" as const;\n\n return confetti;\n};\n"],"mappings":";;AAAA,MAAa,WAA0B,OAAO,IAAI,WAAW;AAC7D,MAAa,UAAyB,OAAO,IAAI,mBAAmB;AACpE,MAAa,UAAyB,OAAO,IAAI,mBAAmB;AACpE,MAAa,OAAsB,OAAO,IAAI,gBAAgB;;;ACA9D,MAAa,SAAS,QAA6B;AACjD,QAAO,QAAQ,QAAQ,OAAO,QAAQ,YAAY,CAAC,MAAM,QAAQ,IAAI;;AAGvE,MAAa,qBAAqB,QAAyC;AACzE,QAAO,MAAM,IAAI,KAAK,WAAW,OAAO,QAAQ,OAAO,WAAW;;AAGpE,MAAa,cAAc,QAAuC;AAChE,QAAO,OAAO,QAAQ,cAAc,OAAO,OAAO,KAAK,SAAS;;AAGlE,MAAa,iBACX,KACA,KACA,KACA,eAAe,OACP;AACR,QAAO,OAAO,QAAQ,IAAI,CAAC,SAAS,CAAC,KAAK,WAAW;EACnD,MAAM,OAAO,GAAG,eAAe;AAE/B,MAAI,MAAM,MAAM,EAAE;AAChB,OAAI,kBAAkB,MAAM,EAAE;IAC5B,IAAI;AAGJ,QAAI,MAAM,YAAY,QAAQ,IAAI,MAAM,cAAc,KAAA,EACpD,KAAI;AACF,uBAAkB,KAAK,MAAM,QAAQ,IAAI,MAAM,UAAW;YACpD;AACN,uBAAkB,QAAQ,IAAI,MAAM;;aAI/B,MAAM,SAAS,KAAA,EACtB,mBAAkB,MAAM;aAGjB,MAAM,aAAa,KAAA,EAC1B,mBAAkB,MAAM;AAG1B,QAAI,oBAAoB,KAAA,EACtB,OAAM,IAAI,MAAM,mBAAmB,IAAI,qBAAqB,OAAO;AAGrE,WAAO,IAAI;KAAE;KAAM;KAAiB,QAAQ,MAAM;KAAU,MAAM,MAAM;KAAO,CAAC;;AAGlF,UAAO,cAAc,KAAK,OAAO,KAAK,GAAG,KAAK,GAAG;;AAGnD,SAAO,IAAI;GAAE;GAAM,iBAAiB;GAAoB,CAAC;GACzD;;AAGJ,MAAa,aAAa,KAAU,MAAc,UAAmB;CACnE,MAAM,WAAW,KAAK,MAAM,IAAI;CAEhC,IAAI,WAAgB;AACpB,MAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;EACxC,MAAM,UAAU,SAAS,MAAM;AAC/B,MAAI,SAAS,WAAW,IAAI,EAC1B,UAAS,WAAW;OACf;AACL,OAAI,CAAC,MAAM,SAAS,SAAS,CAC3B,UAAS,WAAW,EAAE;AAExB,cAAW,SAAS;;;;;;ACnE1B,MAAa,cAAgC,eAAgD;CAC3F,MAAM,YAAY,QAAgB;EAChC,MAAM,SAAS,WAAW,IAAI;EAE9B,MAAM,WAAc,cAClB,cAAiB,KAAK,QAAQ,UAAU;EAE1C,MAAM,gBAAgB,OAAO,YAAY,SAAS,YAAY,CAAC,CAAC,QAAQ,MAAM,QAAQ,CAAC,CAAC,CAAC;EAEzF,MAAM,eAAe,OAAO,SAA+B;GACzD,MAAM,UAAU,cAAc;AAC9B,OAAI,CAAC,QAAS,OAAM,IAAI,MAAM,wBAAwB,KAAK,IAAI;GAC/D,MAAM,EAAE,oBAAoB;GAC5B,MAAM,QACJ,OAAO,oBAAoB,aAAa,MAAM,gBAAgB,QAAQ,GAAG;AAC3E,OAAI,UAAU,KAAA,EACZ,OAAM,IAAI,MAAM,oBAAoB,KAAK,0BAA0B;AAErE,UAAO;;EAGT,MAAM,oBAAoB,SAAsB;GAC9C,MAAM,UAAU,cAAc;AAC9B,OAAI,CAAC,QAAS,OAAM,IAAI,MAAM,wBAAwB,KAAK,IAAI;GAC/D,MAAM,EAAE,oBAAoB;AAC5B,OAAI,OAAO,oBAAoB,WAC7B,OAAM,IAAI,MAAM,mCAAmC,KAAK,kBAAkB;AAE5E,UAAO;;EAGT,MAAM,UAAU,YAA0B;GACxC,MAAM,iBAAsB,EAAE;GAC9B,MAAM,WAAW,OAAO,KAAK,cAAc,CAAC,IAAI,OAAO,SAAS;AAC9D,WAAO,aAAa,KAAK,CAAC,MAAM,kBAC9B,UAAU,gBAAgB,MAAM,cAAc,CAC/C;KACD;AAEF,SAAM,QAAQ,IAAI,SAAS;AAE3B,UAAO;;EAGT,MAAM,oBAAyB;GAC7B,MAAM,iBAAsB,EAAE;AAC9B,QAAK,MAAM,QAAQ,OAAO,KAAK,cAAc,CAC3C,WAAU,gBAAgB,MAAM,iBAAiB,KAAK,CAAC;AAEzD,UAAO;;EAGT,MAAM,aAAa,YAA0B;GAC3C,MAAM,UAAuB,EAAE;GAC/B,MAAM,WAAW,OAAO,OAAO,cAAc,CAAC,QAAQ,OAAO,YAAY;IACvE,MAAM,EAAE,WAAW;AACnB,QAAI,CAAC,OAAQ,QAAO,EAAE;AACtB,WAAO,aAAa,QAAQ,KAAK,CAAC,MAAM,kBAAkB;AACxD,aAAQ,UAAU,KAAK,UAAU,cAAc;MAC/C;KACF;AAEF,SAAM,QAAQ,IAAI,SAAS;AAE3B,UAAO;;AAGT,SAAO;GACL;GACA;GACA;GACA;GACA;GACA;GACA;GACD;;AAGH,UAAS,YAAY;AAErB,QAAO"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/symbols.ts","../src/util.ts","../src/make-config.ts"],"sourcesContent":["export const CONFETTI: unique symbol = Symbol.for(\"CONFETTI\");\nexport const DEFAULT: unique symbol = Symbol.for(\"CONFETTI_DEFAULT\");\nexport const ENV: unique symbol = Symbol.for(\"CONFETTI_ENV\");\nexport const DATA: unique symbol = Symbol.for(\"CONFETTI_DATA\");\nexport const TYPE: unique symbol = Symbol.for(\"CONFETTI_TYPE\");\n","import { CONFETTI, DATA, DEFAULT, ENV, TYPE } from \"./symbols\";\nimport type {\n ConfigEntry,\n Confetti,\n ConfigValPerEnv,\n Fetcher,\n FetcherContext,\n Obj,\n TypeTag,\n} from \"./types\";\n\nexport const isObj = (val: unknown): val is Obj => {\n return val !== null && typeof val === \"object\" && !Array.isArray(val);\n};\n\nexport const isConfigValPerEnv = (val: unknown): val is ConfigValPerEnv => {\n return isObj(val) && (DEFAULT in val || DATA in val || ENV in val || TYPE in val);\n};\n\nexport const isNestedConfig = (val: unknown): val is Obj => {\n return isObj(val) && !isConfigValPerEnv(val);\n};\n\nexport const isConfetti = (val: unknown): val is Confetti<any> => {\n return typeof val === \"function\" && Object.hasOwn(val, CONFETTI);\n};\n\nexport const isThenable = (v: unknown): v is PromiseLike<unknown> => {\n return (\n v !== null &&\n typeof v === \"object\" &&\n \"then\" in v &&\n typeof (v as { then: unknown }).then === \"function\"\n );\n};\n\nexport const getAtPath = (config: Obj, path: string): unknown => {\n if (!path) return config;\n const segments = path.split(\".\");\n let node: unknown = config;\n for (const segment of segments) {\n if (!isNestedConfig(node)) {\n throw new Error(`Invalid config path '${path}'.`);\n }\n node = node[segment];\n if (node === undefined) {\n throw new Error(`Invalid config path '${path}'.`);\n }\n }\n return node;\n};\n\nconst matchesType = (val: unknown, type: TypeTag): boolean => {\n if (type === \"string\") return typeof val === \"string\";\n if (type === \"number\") return typeof val === \"number\" && !Number.isNaN(val);\n if (type === \"boolean\") return typeof val === \"boolean\";\n if (!Array.isArray(val)) return false;\n const elem = type.slice(0, -2);\n return val.every((x) => typeof x === elem);\n};\n\nexport const coerceFromString = (raw: string, type: TypeTag, path: string): unknown => {\n if (type === \"string\") return raw;\n if (type === \"number\") {\n if (raw === \"\") throw new Error(`Cannot coerce empty string to number at '${path}'.`);\n const n = Number(raw);\n if (Number.isNaN(n)) throw new Error(`Cannot coerce '${raw}' to number at '${path}'.`);\n return n;\n }\n if (type === \"boolean\") {\n if (raw === \"true\") return true;\n if (raw === \"false\") return false;\n throw new Error(`Cannot coerce '${raw}' to boolean at '${path}' (expected 'true' or 'false').`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(`Cannot coerce '${raw}' to ${type} at '${path}' (invalid JSON).`);\n }\n if (!Array.isArray(parsed)) {\n throw new Error(`Cannot coerce '${raw}' to ${type} at '${path}' (not an array).`);\n }\n const elem = type.slice(0, -2);\n if (!parsed.every((x) => typeof x === elem)) {\n throw new Error(`Cannot coerce '${raw}' to ${type} at '${path}' (element type mismatch).`);\n }\n return parsed;\n};\n\nconst coerceFetched = (fetched: unknown, type: TypeTag | undefined, path: string): unknown => {\n if (type === undefined) {\n if (typeof fetched !== \"string\") {\n throw new Error(\n `Fetcher for '${path}' must return a string (add [TYPE] to use non-string values).`,\n );\n }\n return fetched;\n }\n if (typeof fetched === \"string\") return coerceFromString(fetched, type, path);\n if (matchesType(fetched, type)) return fetched;\n throw new Error(`Fetcher for '${path}' returned value that doesn't match [TYPE] '${type}'.`);\n};\n\nexport const buildEntry = (node: unknown, path: string, env: string): ConfigEntry => {\n if (!isConfigValPerEnv(node)) {\n return { path, value: node };\n }\n\n const envVar = node[ENV];\n const data = node[DATA];\n const defaultVal = node[DEFAULT];\n const typeTag = node[TYPE];\n const entry: ConfigEntry = { path };\n\n if (envVar && process.env[envVar] !== undefined) {\n const raw = process.env[envVar]!;\n entry.value = typeTag ? coerceFromString(raw, typeTag, path) : raw;\n } else if (node[env] !== undefined) {\n entry.value = node[env];\n }\n\n if (defaultVal !== undefined) entry.default = defaultVal;\n if (envVar) entry.envVar = envVar;\n if (data !== undefined) entry.data = data;\n if (typeTag !== undefined) entry.type = typeTag;\n\n return entry;\n};\n\nexport function* entriesIter(\n config: Obj,\n env: string,\n pathPrefix = \"\",\n): IterableIterator<[string, ConfigEntry]> {\n for (const [key, value] of Object.entries(config)) {\n const path = pathPrefix + key;\n if (isNestedConfig(value)) {\n yield* entriesIter(value, env, `${path}.`);\n } else {\n yield [path, buildEntry(value, path, env)];\n }\n }\n}\n\nexport const invokeSync = (source: unknown, path: string, env: string): unknown => {\n if (typeof source !== \"function\") return source;\n const result = (source as (env: string) => unknown)(env);\n if (isThenable(result)) {\n throw new Error(`Config at '${path}' requires async resolution (use resolve).`);\n }\n return result;\n};\n\nexport const invokeAsync = async (source: unknown, env: string): Promise<unknown> => {\n return typeof source === \"function\" ? await (source as (env: string) => unknown)(env) : source;\n};\n\nexport const resolveEntrySync = (entry: ConfigEntry, env: string): unknown => {\n if (\"value\" in entry) return invokeSync(entry.value, entry.path, env);\n if (\"default\" in entry) return invokeSync(entry.default, entry.path, env);\n throw new Error(`Config at '${entry.path}' requires async resolution (use resolve).`);\n};\n\nexport const resolveEntryAsync = async (\n entry: ConfigEntry,\n env: string,\n fetcher: Fetcher,\n): Promise<unknown> => {\n if (\"value\" in entry) return invokeAsync(entry.value, env);\n\n if (entry.envVar !== undefined || entry.data !== undefined) {\n const ctx: FetcherContext = {\n env,\n default: entry.default,\n envVar: entry.envVar,\n data: entry.data,\n type: entry.type,\n };\n const fetched = await fetcher(ctx);\n if (fetched !== undefined) return coerceFetched(fetched, entry.type, entry.path);\n }\n\n if (\"default\" in entry) return invokeAsync(entry.default, env);\n throw new Error(`Unable to resolve config value at '${entry.path}'.`);\n};\n\nexport const resolveSubtreeSync = (node: Obj, prefix: string, env: string): Obj => {\n const result: Obj = {};\n for (const [key, child] of Object.entries(node)) {\n const path = prefix ? `${prefix}.${key}` : key;\n result[key] = isNestedConfig(child)\n ? resolveSubtreeSync(child, path, env)\n : resolveEntrySync(buildEntry(child, path, env), env);\n }\n return result;\n};\n\nexport const resolveSubtreeAsync = async (\n node: Obj,\n prefix: string,\n env: string,\n fetcher: Fetcher,\n): Promise<Obj> => {\n const keys = Object.keys(node);\n const values = await Promise.all(\n keys.map((key) => {\n const child = node[key];\n const path = prefix ? `${prefix}.${key}` : key;\n return isNestedConfig(child)\n ? resolveSubtreeAsync(child, path, env, fetcher)\n : resolveEntryAsync(buildEntry(child, path, env), env, fetcher);\n }),\n );\n const result: Obj = {};\n keys.forEach((key, i) => {\n result[key] = values[i];\n });\n return result;\n};\n","import { CONFETTI } from \"./symbols\";\nimport type {\n Confetti,\n Config,\n ConfigEntry,\n Fetcher,\n Paths,\n SubtreePaths,\n ValidateConfig,\n ValueAtPath,\n} from \"./types\";\nimport {\n buildEntry,\n entriesIter,\n getAtPath,\n isNestedConfig,\n resolveEntryAsync,\n resolveEntrySync,\n resolveSubtreeAsync,\n resolveSubtreeSync,\n} from \"./util\";\n\nexport const makeConfig = <const C extends Config>(\n input: (C & ValidateConfig<C>) | ((env: string) => C & ValidateConfig<C>),\n): Confetti<C> => {\n const factory = typeof input === \"function\" ? input : () => input;\n\n const confetti = (env: string) => {\n const config = factory(env);\n\n const get = <P extends Paths<C> & string>(path: P): ValueAtPath<C, P> => {\n const node = getAtPath(config, path);\n const resolved = isNestedConfig(node)\n ? resolveSubtreeSync(node, path, env)\n : resolveEntrySync(buildEntry(node, path, env), env);\n return resolved as ValueAtPath<C, P>;\n };\n\n const resolve = async <P extends Paths<C> & string>(\n path: P,\n fetcher: Fetcher,\n ): Promise<ValueAtPath<C, P>> => {\n const node = getAtPath(config, path);\n const resolved = isNestedConfig(node)\n ? await resolveSubtreeAsync(node, path, env, fetcher)\n : await resolveEntryAsync(buildEntry(node, path, env), env, fetcher);\n return resolved as ValueAtPath<C, P>;\n };\n\n const entries = (\n startPath?: SubtreePaths<C> & string,\n ): IterableIterator<[string, ConfigEntry]> => {\n const root = startPath ? getAtPath(config, startPath) : config;\n if (!isNestedConfig(root)) {\n throw new Error(`'${startPath}' is not a config subtree.`);\n }\n return entriesIter(root, env, startPath ? `${startPath}.` : \"\");\n };\n\n return { config, get, resolve, entries };\n };\n\n confetti[CONFETTI] = \"CONFETTI\" as const;\n\n return confetti as Confetti<C>;\n};\n"],"mappings":"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"}
|