@meltstudio/config-loader 3.4.0 → 3.6.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 +20 -719
- package/dist/index.d.ts +24 -2
- package/dist/index.js +131 -3
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @meltstudio/config-loader
|
|
2
2
|
|
|
3
|
-
A type-safe configuration loader for Node.js. Define your schema once, load from YAML or
|
|
3
|
+
A type-safe configuration loader for Node.js. Define your schema once, load from YAML, JSON, or TOML files, `.env` files, environment variables, and CLI arguments — and get a fully typed result with zero manual type annotations.
|
|
4
4
|
|
|
5
5
|
> **Upgrading from v1?** v1.x is deprecated. v2 includes breaking changes to the public API, object schema syntax, and requires Node.js >= 20. Install the latest version with `npm install @meltstudio/config-loader@latest` or `yarn add @meltstudio/config-loader@latest`.
|
|
6
6
|
|
|
@@ -16,61 +16,45 @@ import c from "@meltstudio/config-loader";
|
|
|
16
16
|
const config = c
|
|
17
17
|
.schema({
|
|
18
18
|
port: c.number({ required: true, env: "PORT" }),
|
|
19
|
+
host: c.string({ env: "HOST", defaultValue: "localhost" }),
|
|
20
|
+
env: c.string({
|
|
21
|
+
env: "NODE_ENV",
|
|
22
|
+
defaultValue: "development",
|
|
23
|
+
oneOf: ["development", "staging", "production"],
|
|
24
|
+
}),
|
|
25
|
+
apiKey: c.string({ env: "API_KEY", sensitive: true }),
|
|
19
26
|
database: c.object({
|
|
20
27
|
item: {
|
|
21
28
|
host: c.string({ required: true }),
|
|
22
|
-
|
|
23
|
-
item: {
|
|
24
|
-
username: c.string(),
|
|
25
|
-
password: c.string({ env: "DB_PASSWORD" }),
|
|
26
|
-
},
|
|
27
|
-
}),
|
|
29
|
+
password: c.string({ env: "DB_PASSWORD", sensitive: true }),
|
|
28
30
|
},
|
|
29
31
|
}),
|
|
30
|
-
features: c.array({
|
|
31
|
-
required: true,
|
|
32
|
-
item: c.object({
|
|
33
|
-
item: {
|
|
34
|
-
name: c.string(),
|
|
35
|
-
enabled: c.bool(),
|
|
36
|
-
},
|
|
37
|
-
}),
|
|
38
|
-
}),
|
|
39
32
|
})
|
|
40
33
|
.load({
|
|
41
34
|
env: true,
|
|
42
35
|
args: true,
|
|
43
36
|
files: "./config.yaml",
|
|
37
|
+
envFile: "./.env",
|
|
44
38
|
});
|
|
45
39
|
|
|
46
|
-
// config is fully typed
|
|
47
|
-
// {
|
|
48
|
-
// port: number;
|
|
49
|
-
// database: { host: string; credentials: { username: string; password: string } };
|
|
50
|
-
// features: { name: string; enabled: boolean }[];
|
|
51
|
-
// }
|
|
40
|
+
// config is fully typed — no `as` casts, no separate interfaces
|
|
52
41
|
```
|
|
53
42
|
|
|
54
|
-
No separate interface to maintain. No `as` casts. The types flow from the schema.
|
|
55
|
-
|
|
56
43
|
## Features
|
|
57
44
|
|
|
58
45
|
- **Full type inference** — schema definition produces typed output automatically
|
|
59
|
-
- **Multiple sources** — YAML
|
|
46
|
+
- **Multiple sources** — YAML, JSON, TOML files, `.env` files, environment variables, CLI arguments
|
|
60
47
|
- **Priority resolution** — CLI > process.env > `.env` files > Config files > Defaults
|
|
61
48
|
- **`.env` file support** — load environment variables from `.env` files with automatic line tracking
|
|
62
49
|
- **Nested objects and arrays** — deeply nested configs with full type safety
|
|
63
50
|
- **Structured errors** — typed `ConfigLoadError` with per-field error details and warnings
|
|
64
51
|
- **Enum constraints** — restrict values to a fixed set with `oneOf`, with full type narrowing
|
|
52
|
+
- **Sensitive fields** — mark fields with `sensitive: true` to auto-mask in `printConfig()` and `maskSecrets()`
|
|
65
53
|
- **Schema validation** — optional per-field validation via [Standard Schema](https://github.com/standard-schema/standard-schema) (Zod, Valibot, ArkType, or custom)
|
|
66
54
|
- **Strict mode** — promote warnings to errors for production safety
|
|
67
55
|
- **Default values** — static or computed (via functions)
|
|
68
56
|
- **Multiple files / directory loading** — load from a list of files or an entire directory
|
|
69
57
|
|
|
70
|
-
## Requirements
|
|
71
|
-
|
|
72
|
-
- Node.js >= 20
|
|
73
|
-
|
|
74
58
|
## Installation
|
|
75
59
|
|
|
76
60
|
```bash
|
|
@@ -81,698 +65,15 @@ npm install @meltstudio/config-loader
|
|
|
81
65
|
yarn add @meltstudio/config-loader
|
|
82
66
|
```
|
|
83
67
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
**config.yaml:**
|
|
87
|
-
|
|
88
|
-
```yaml
|
|
89
|
-
version: 1.0.0
|
|
90
|
-
website:
|
|
91
|
-
title: My Website
|
|
92
|
-
description: A simple and elegant website
|
|
93
|
-
isProduction: false
|
|
94
|
-
|
|
95
|
-
database:
|
|
96
|
-
host: localhost
|
|
97
|
-
port: 5432
|
|
98
|
-
credentials:
|
|
99
|
-
username: admin
|
|
100
|
-
password: secret
|
|
101
|
-
|
|
102
|
-
socialMedia: [https://twitter.com/example, https://instagram.com/example]
|
|
103
|
-
|
|
104
|
-
features:
|
|
105
|
-
- name: Store
|
|
106
|
-
enabled: true
|
|
107
|
-
- name: Admin
|
|
108
|
-
enabled: false
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
**index.ts:**
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
import path from "path";
|
|
115
|
-
import c from "@meltstudio/config-loader";
|
|
116
|
-
|
|
117
|
-
const config = c
|
|
118
|
-
.schema({
|
|
119
|
-
version: c.string({ required: true, cli: true }),
|
|
120
|
-
website: c.object({
|
|
121
|
-
item: {
|
|
122
|
-
title: c.string({ required: true }),
|
|
123
|
-
url: c.string({
|
|
124
|
-
required: false,
|
|
125
|
-
defaultValue: "www.mywebsite.dev",
|
|
126
|
-
}),
|
|
127
|
-
description: c.string({ required: true }),
|
|
128
|
-
isProduction: c.bool({ required: true }),
|
|
129
|
-
},
|
|
130
|
-
}),
|
|
131
|
-
database: c.object({
|
|
132
|
-
item: {
|
|
133
|
-
host: c.string({ required: true }),
|
|
134
|
-
port: c.number({ required: true }),
|
|
135
|
-
credentials: c.object({
|
|
136
|
-
item: {
|
|
137
|
-
username: c.string(),
|
|
138
|
-
password: c.string(),
|
|
139
|
-
},
|
|
140
|
-
}),
|
|
141
|
-
},
|
|
142
|
-
}),
|
|
143
|
-
socialMedia: c.array({
|
|
144
|
-
required: true,
|
|
145
|
-
item: c.string({ required: true }),
|
|
146
|
-
}),
|
|
147
|
-
features: c.array({
|
|
148
|
-
required: true,
|
|
149
|
-
item: c.object({
|
|
150
|
-
item: {
|
|
151
|
-
name: c.string(),
|
|
152
|
-
enabled: c.bool(),
|
|
153
|
-
},
|
|
154
|
-
}),
|
|
155
|
-
}),
|
|
156
|
-
})
|
|
157
|
-
.load({
|
|
158
|
-
env: false,
|
|
159
|
-
args: true,
|
|
160
|
-
files: path.join(__dirname, "./config.yaml"),
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
console.log(JSON.stringify(config, null, 2));
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
Output:
|
|
167
|
-
|
|
168
|
-
```json
|
|
169
|
-
{
|
|
170
|
-
"version": "1.0.0",
|
|
171
|
-
"website": {
|
|
172
|
-
"title": "My Website",
|
|
173
|
-
"url": "www.mywebsite.dev",
|
|
174
|
-
"description": "A simple and elegant website",
|
|
175
|
-
"isProduction": false
|
|
176
|
-
},
|
|
177
|
-
"database": {
|
|
178
|
-
"host": "localhost",
|
|
179
|
-
"port": 5432,
|
|
180
|
-
"credentials": {
|
|
181
|
-
"username": "admin",
|
|
182
|
-
"password": "secret"
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
"socialMedia": [
|
|
186
|
-
"https://twitter.com/example",
|
|
187
|
-
"https://instagram.com/example"
|
|
188
|
-
],
|
|
189
|
-
"features": [
|
|
190
|
-
{ "name": "Store", "enabled": true },
|
|
191
|
-
{ "name": "Admin", "enabled": false }
|
|
192
|
-
]
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
## Schema API
|
|
197
|
-
|
|
198
|
-
### Primitives
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
c.string({
|
|
202
|
-
required: true,
|
|
203
|
-
env: "MY_VAR",
|
|
204
|
-
cli: true,
|
|
205
|
-
defaultValue: "fallback",
|
|
206
|
-
});
|
|
207
|
-
c.number({ required: true, env: "PORT" });
|
|
208
|
-
c.bool({ env: "DEBUG", defaultValue: false });
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### Objects
|
|
212
|
-
|
|
213
|
-
Use `c.object()` to declare nested object schemas:
|
|
214
|
-
|
|
215
|
-
```typescript
|
|
216
|
-
c.object({
|
|
217
|
-
item: {
|
|
218
|
-
host: c.string(),
|
|
219
|
-
port: c.number(),
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
Objects can be nested arbitrarily deep:
|
|
225
|
-
|
|
226
|
-
```typescript
|
|
227
|
-
c.schema({
|
|
228
|
-
database: c.object({
|
|
229
|
-
item: {
|
|
230
|
-
host: c.string(),
|
|
231
|
-
port: c.number(),
|
|
232
|
-
credentials: c.object({
|
|
233
|
-
item: {
|
|
234
|
-
username: c.string(),
|
|
235
|
-
password: c.string({ env: "DB_PASSWORD" }),
|
|
236
|
-
},
|
|
237
|
-
}),
|
|
238
|
-
},
|
|
239
|
-
}),
|
|
240
|
-
});
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
`c.object()` accepts a `required` option (defaults to `false`). When the entire subtree is absent from all sources, child `required` options will trigger errors through normal validation.
|
|
244
|
-
|
|
245
|
-
### Arrays
|
|
246
|
-
|
|
247
|
-
```typescript
|
|
248
|
-
c.array({ required: true, item: c.string() }); // string[]
|
|
249
|
-
c.array({ required: true, item: c.number() }); // number[]
|
|
250
|
-
c.array({
|
|
251
|
-
item: c.object({
|
|
252
|
-
item: { name: c.string(), age: c.number() },
|
|
253
|
-
}),
|
|
254
|
-
}); // { name: string; age: number }[]
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
## Enum Constraints (`oneOf`)
|
|
258
|
-
|
|
259
|
-
Use `oneOf` to restrict a field to a fixed set of allowed values. The check runs after type coercion and before any `validate` schema:
|
|
260
|
-
|
|
261
|
-
```typescript
|
|
262
|
-
const config = c
|
|
263
|
-
.schema({
|
|
264
|
-
env: c.string({
|
|
265
|
-
env: "NODE_ENV",
|
|
266
|
-
defaultValue: "development",
|
|
267
|
-
oneOf: ["development", "staging", "production"],
|
|
268
|
-
}),
|
|
269
|
-
logLevel: c.number({
|
|
270
|
-
env: "LOG_LEVEL",
|
|
271
|
-
defaultValue: 1,
|
|
272
|
-
oneOf: [0, 1, 2, 3],
|
|
273
|
-
}),
|
|
274
|
-
})
|
|
275
|
-
.load({ env: true, args: false });
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
If a value is not in the allowed set, a `ConfigLoadError` is thrown with `kind: "validation"`.
|
|
279
|
-
|
|
280
|
-
### Type Narrowing
|
|
281
|
-
|
|
282
|
-
When `oneOf` is provided, the inferred type is automatically narrowed to the union of the allowed values:
|
|
283
|
-
|
|
284
|
-
```typescript
|
|
285
|
-
const config = c
|
|
286
|
-
.schema({
|
|
287
|
-
env: c.string({ oneOf: ["dev", "staging", "prod"] }),
|
|
288
|
-
})
|
|
289
|
-
.load({ env: false, args: false });
|
|
290
|
-
|
|
291
|
-
// config.env is typed as "dev" | "staging" | "prod", not string
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
When used with `cli: true`, the `--help` output automatically lists the allowed values.
|
|
295
|
-
|
|
296
|
-
## Validation
|
|
297
|
-
|
|
298
|
-
Add per-field validation using the `validate` option. config-loader accepts any [Standard Schema v1](https://github.com/standard-schema/standard-schema) implementation — including **Zod**, **Valibot**, and **ArkType** — or a custom validator.
|
|
299
|
-
|
|
300
|
-
Validation runs **after** type coercion, so validators see the final typed value (e.g., the number `3000`, not the string `"3000"` from an env var).
|
|
301
|
-
|
|
302
|
-
### With Zod
|
|
303
|
-
|
|
304
|
-
```typescript
|
|
305
|
-
import c from "@meltstudio/config-loader";
|
|
306
|
-
import { z } from "zod";
|
|
307
|
-
|
|
308
|
-
const config = c
|
|
309
|
-
.schema({
|
|
310
|
-
port: c.number({
|
|
311
|
-
required: true,
|
|
312
|
-
env: "PORT",
|
|
313
|
-
validate: z.number().min(1).max(65535),
|
|
314
|
-
}),
|
|
315
|
-
host: c.string({
|
|
316
|
-
required: true,
|
|
317
|
-
validate: z.string().url(),
|
|
318
|
-
}),
|
|
319
|
-
env: c.string({
|
|
320
|
-
defaultValue: "development",
|
|
321
|
-
validate: z.enum(["development", "staging", "production"]),
|
|
322
|
-
}),
|
|
323
|
-
})
|
|
324
|
-
.load({ env: true, args: false, files: "./config.yaml" });
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
### With a custom validator
|
|
328
|
-
|
|
329
|
-
Any object with a `~standard.validate()` method works:
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
const portValidator = {
|
|
333
|
-
"~standard": {
|
|
334
|
-
version: 1,
|
|
335
|
-
vendor: "my-app",
|
|
336
|
-
validate(value: unknown) {
|
|
337
|
-
if (typeof value === "number" && value >= 1 && value <= 65535) {
|
|
338
|
-
return { value };
|
|
339
|
-
}
|
|
340
|
-
return { issues: [{ message: "must be a valid port (1-65535)" }] };
|
|
341
|
-
},
|
|
342
|
-
},
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
c.number({ required: true, env: "PORT", validate: portValidator });
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
Validation errors are collected alongside other config errors and thrown as `ConfigLoadError` with `kind: "validation"`:
|
|
349
|
-
|
|
350
|
-
```typescript
|
|
351
|
-
try {
|
|
352
|
-
const config = c.schema({ ... }).load({ ... });
|
|
353
|
-
} catch (err) {
|
|
354
|
-
if (err instanceof ConfigLoadError) {
|
|
355
|
-
for (const entry of err.errors) {
|
|
356
|
-
if (entry.kind === "validation") {
|
|
357
|
-
console.error(`Validation: ${entry.path} — ${entry.message}`);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
## Loading Sources
|
|
365
|
-
|
|
366
|
-
```typescript
|
|
367
|
-
.load({
|
|
368
|
-
env: true, // Read from process.env
|
|
369
|
-
args: true, // Read from CLI arguments (--database.port 3000)
|
|
370
|
-
files: "./config.yaml", // Single YAML file
|
|
371
|
-
files: "./config.json", // Single JSON file
|
|
372
|
-
files: ["./base.yaml", "./overrides.json"], // Mix YAML and JSON (first takes priority)
|
|
373
|
-
dir: "./config.d/", // All files in a directory (sorted)
|
|
374
|
-
envFile: "./.env", // Single .env file
|
|
375
|
-
envFile: ["./.env", "./.env.local"], // Multiple .env files (later overrides earlier)
|
|
376
|
-
defaults: { port: 3000 }, // Programmatic defaults
|
|
377
|
-
})
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
Both YAML (`.yaml`, `.yml`) and JSON (`.json`) files are supported. The format is detected automatically from the file extension.
|
|
381
|
-
|
|
382
|
-
**Priority order:** CLI arguments > `process.env` > `.env` files > Config files > Defaults
|
|
383
|
-
|
|
384
|
-
## Extended Loading (Source Metadata)
|
|
385
|
-
|
|
386
|
-
Use `loadExtended()` instead of `load()` to get each value wrapped in a `ConfigNode` that includes source metadata — where the value came from, which file, environment variable, or CLI argument provided it.
|
|
387
|
-
|
|
388
|
-
```typescript
|
|
389
|
-
import c from "@meltstudio/config-loader";
|
|
390
|
-
|
|
391
|
-
const { data, warnings } = c
|
|
392
|
-
.schema({
|
|
393
|
-
port: c.number({ required: true, env: "PORT" }),
|
|
394
|
-
host: c.string({ defaultValue: "localhost" }),
|
|
395
|
-
})
|
|
396
|
-
.loadExtended({
|
|
397
|
-
env: true,
|
|
398
|
-
args: false,
|
|
399
|
-
files: "./config.yaml",
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
// `warnings` is a string[] of non-fatal issues (e.g. type coercions, unused env mappings)
|
|
403
|
-
if (warnings.length > 0) {
|
|
404
|
-
warnings.forEach((w) => console.warn(w));
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Each leaf in `data` is a ConfigNode with:
|
|
408
|
-
// {
|
|
409
|
-
// value: 3000,
|
|
410
|
-
// path: "port",
|
|
411
|
-
// sourceType: "env" | "envFile" | "file" | "args" | "default",
|
|
412
|
-
// file: "./config.yaml" | "./.env" | null,
|
|
413
|
-
// variableName: "PORT" | null,
|
|
414
|
-
// argName: null,
|
|
415
|
-
// line: 5 | null, // source line (1-based) for YAML, JSON, and .env files; null for env/args/default
|
|
416
|
-
// column: 3 | null // source column (1-based) for YAML, JSON, and .env files; null for env/args/default
|
|
417
|
-
// }
|
|
418
|
-
console.log(data.port.value); // 3000
|
|
419
|
-
console.log(data.port.sourceType); // "env"
|
|
420
|
-
console.log(data.port.variableName); // "PORT"
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
This is useful for debugging configuration resolution, building admin UIs that show where each setting originated, or auditing which sources are active.
|
|
424
|
-
|
|
425
|
-
### Debug Helper
|
|
426
|
-
|
|
427
|
-
Use `printConfig()` to format the result of `loadExtended()` as a readable table:
|
|
428
|
-
|
|
429
|
-
```typescript
|
|
430
|
-
import c, { printConfig } from "@meltstudio/config-loader";
|
|
431
|
-
|
|
432
|
-
const result = c
|
|
433
|
-
.schema({
|
|
434
|
-
host: c.string({ defaultValue: "localhost" }),
|
|
435
|
-
port: c.number({ env: "PORT" }),
|
|
436
|
-
debug: c.bool({ cli: true }),
|
|
437
|
-
})
|
|
438
|
-
.loadExtended({ env: true, args: true, files: "./config.yaml" });
|
|
439
|
-
|
|
440
|
-
printConfig(result);
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
Output:
|
|
444
|
-
|
|
445
|
-
```
|
|
446
|
-
┌───────┬───────────┬─────────┬────────────────┐
|
|
447
|
-
│ Path │ Value │ Source │ Detail │
|
|
448
|
-
├───────┼───────────┼─────────┼────────────────┤
|
|
449
|
-
│ host │ localhost │ default │ │
|
|
450
|
-
│ port │ 8080 │ env │ PORT │
|
|
451
|
-
│ debug │ true │ args │ --debug │
|
|
452
|
-
└───────┴───────────┴─────────┴────────────────┘
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
Options:
|
|
456
|
-
|
|
457
|
-
- `printConfig(result, { silent: true })` — returns the string without printing to console
|
|
458
|
-
- `printConfig(result, { maxValueLength: 30 })` — truncate long values (default: 50)
|
|
459
|
-
|
|
460
|
-
## Error Handling
|
|
461
|
-
|
|
462
|
-
When validation fails, config-loader throws a `ConfigLoadError` with structured error details:
|
|
463
|
-
|
|
464
|
-
```typescript
|
|
465
|
-
import c, { ConfigLoadError } from "@meltstudio/config-loader";
|
|
466
|
-
|
|
467
|
-
try {
|
|
468
|
-
const config = c.schema({ port: c.number({ required: true }) }).load({
|
|
469
|
-
env: false,
|
|
470
|
-
args: false,
|
|
471
|
-
files: "./config.yaml",
|
|
472
|
-
});
|
|
473
|
-
} catch (err) {
|
|
474
|
-
if (err instanceof ConfigLoadError) {
|
|
475
|
-
for (const entry of err.errors) {
|
|
476
|
-
console.error(`[${entry.kind}] ${entry.message}`);
|
|
477
|
-
// e.g. [required] Required option 'port' not provided.
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
Warnings (non-fatal issues like type coercions) are never printed to the console. Use `loadExtended()` to access them, or they are included in `ConfigLoadError.warnings` when errors occur.
|
|
484
|
-
|
|
485
|
-
### Strict Mode
|
|
486
|
-
|
|
487
|
-
Enable `strict: true` to promote all warnings to errors, causing `ConfigLoadError` to be thrown for any ambiguous or lossy configuration:
|
|
488
|
-
|
|
489
|
-
```typescript
|
|
490
|
-
.load({
|
|
491
|
-
env: true,
|
|
492
|
-
args: false,
|
|
493
|
-
files: "./config.yaml",
|
|
494
|
-
strict: true,
|
|
495
|
-
})
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
This is useful in production environments where you want to catch type coercions, null values, and other ambiguous config early rather than silently accepting them.
|
|
499
|
-
|
|
500
|
-
## TypeScript Utilities
|
|
501
|
-
|
|
502
|
-
config-loader exports several types for advanced use cases:
|
|
503
|
-
|
|
504
|
-
```typescript
|
|
505
|
-
import c, {
|
|
506
|
-
type SchemaValue, // Infer the resolved config type from a schema
|
|
507
|
-
type SettingsSources, // Type for the sources object passed to load()
|
|
508
|
-
type ExtendedResult, // Return type of loadExtended()
|
|
509
|
-
type NodeTree, // Tree of ConfigNode objects (ExtendedResult.data)
|
|
510
|
-
ConfigNode, // Class representing a resolved value with source metadata
|
|
511
|
-
ConfigNodeArray, // Class representing an array of ConfigNode values
|
|
512
|
-
type RecursivePartial, // Deep partial utility used by the defaults option
|
|
513
|
-
type StandardSchemaV1, // Standard Schema v1 interface for validators
|
|
514
|
-
} from "@meltstudio/config-loader";
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
The most commonly needed is `SchemaValue`, which infers the plain TypeScript type from a schema:
|
|
518
|
-
|
|
519
|
-
```typescript
|
|
520
|
-
const mySchema = {
|
|
521
|
-
port: c.number({ env: "PORT" }),
|
|
522
|
-
db: c.object({ item: { host: c.string(), port: c.number() } }),
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
type MyConfig = SchemaValue<typeof mySchema>;
|
|
526
|
-
// { port: number; db: { host: string; port: number } }
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
## CLI Arguments
|
|
530
|
-
|
|
531
|
-
Set `cli: true` on an option to allow overriding via command line:
|
|
532
|
-
|
|
533
|
-
```typescript
|
|
534
|
-
c.schema({
|
|
535
|
-
version: c.string({ required: true, cli: true }),
|
|
536
|
-
});
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
```bash
|
|
540
|
-
node app.js --version 2.0.0
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
## Environment Variables
|
|
544
|
-
|
|
545
|
-
Set `env: "VAR_NAME"` on an option and `env: true` in the load options:
|
|
546
|
-
|
|
547
|
-
```typescript
|
|
548
|
-
c.schema({
|
|
549
|
-
database: c.object({
|
|
550
|
-
item: {
|
|
551
|
-
password: c.string({ env: "DB_PASSWORD" }),
|
|
552
|
-
},
|
|
553
|
-
}),
|
|
554
|
-
}).load({ env: true, args: false, files: "./config.yaml" });
|
|
555
|
-
```
|
|
556
|
-
|
|
557
|
-
## `.env` File Support
|
|
558
|
-
|
|
559
|
-
Load environment variables from `.env` files using the `envFile` option. Options with an `env` mapping automatically pick up values from `.env` files — no new syntax needed on individual fields.
|
|
560
|
-
|
|
561
|
-
**.env:**
|
|
562
|
-
|
|
563
|
-
```bash
|
|
564
|
-
DB_HOST=localhost
|
|
565
|
-
DB_PORT=5432
|
|
566
|
-
DB_PASSWORD="s3cret"
|
|
567
|
-
APP_NAME='My App'
|
|
568
|
-
# This is a comment
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
**Usage:**
|
|
572
|
-
|
|
573
|
-
```typescript
|
|
574
|
-
const config = c
|
|
575
|
-
.schema({
|
|
576
|
-
host: c.string({ env: "DB_HOST" }),
|
|
577
|
-
port: c.number({ env: "DB_PORT" }),
|
|
578
|
-
password: c.string({ env: "DB_PASSWORD" }),
|
|
579
|
-
})
|
|
580
|
-
.load({
|
|
581
|
-
env: true,
|
|
582
|
-
args: false,
|
|
583
|
-
envFile: "./.env",
|
|
584
|
-
});
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
`process.env` always takes precedence over `.env` file values. This means you can use `.env` files for development defaults while overriding them in production via real environment variables.
|
|
588
|
-
|
|
589
|
-
**Multiple `.env` files:**
|
|
590
|
-
|
|
591
|
-
```typescript
|
|
592
|
-
.load({
|
|
593
|
-
env: true,
|
|
594
|
-
args: false,
|
|
595
|
-
envFile: ["./.env", "./.env.local"], // .env.local overrides .env
|
|
596
|
-
})
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
When using multiple files, later files override earlier ones for the same key.
|
|
600
|
-
|
|
601
|
-
The `.env` parser supports:
|
|
602
|
-
|
|
603
|
-
- `KEY=VALUE` pairs (whitespace trimmed)
|
|
604
|
-
- Comments (lines starting with `#`)
|
|
605
|
-
- Quoted values (double `"..."` or single `'...'` quotes stripped)
|
|
606
|
-
- Empty values (`KEY=`)
|
|
607
|
-
|
|
608
|
-
When using `loadExtended()`, values from `.env` files have `sourceType: "envFile"` with `file`, `line`, and `column` metadata pointing to the `.env` file location.
|
|
609
|
-
|
|
610
|
-
## Common Patterns
|
|
68
|
+
Requires Node.js >= 20.
|
|
611
69
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
```typescript
|
|
615
|
-
import c from "@meltstudio/config-loader";
|
|
616
|
-
|
|
617
|
-
const config = c
|
|
618
|
-
.schema({
|
|
619
|
-
port: c.number({ required: true, env: "PORT", defaultValue: 3000 }),
|
|
620
|
-
host: c.string({ required: true, env: "HOST", defaultValue: "localhost" }),
|
|
621
|
-
})
|
|
622
|
-
.load({ env: true, args: false, files: "./config.yaml" });
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
### Strict mode for production
|
|
626
|
-
|
|
627
|
-
```typescript
|
|
628
|
-
const config = c
|
|
629
|
-
.schema({
|
|
630
|
-
port: c.number({ required: true, env: "PORT" }),
|
|
631
|
-
dbUrl: c.string({ required: true, env: "DATABASE_URL" }),
|
|
632
|
-
})
|
|
633
|
-
.load({
|
|
634
|
-
env: true,
|
|
635
|
-
args: false,
|
|
636
|
-
files: "./config.yaml",
|
|
637
|
-
strict: true, // any type coercion or ambiguity throws an error
|
|
638
|
-
});
|
|
639
|
-
```
|
|
640
|
-
|
|
641
|
-
### Catch and inspect errors
|
|
642
|
-
|
|
643
|
-
```typescript
|
|
644
|
-
import c, { ConfigLoadError } from "@meltstudio/config-loader";
|
|
645
|
-
|
|
646
|
-
try {
|
|
647
|
-
const config = c
|
|
648
|
-
.schema({ port: c.number({ required: true }) })
|
|
649
|
-
.load({ env: false, args: false, files: "./config.yaml" });
|
|
650
|
-
} catch (err) {
|
|
651
|
-
if (err instanceof ConfigLoadError) {
|
|
652
|
-
for (const entry of err.errors) {
|
|
653
|
-
console.error(`[${entry.kind}] ${entry.path}: ${entry.message}`);
|
|
654
|
-
}
|
|
655
|
-
// err.warnings contains non-fatal issues
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
```
|
|
659
|
-
|
|
660
|
-
### Load from a directory of config files
|
|
661
|
-
|
|
662
|
-
```typescript
|
|
663
|
-
const config = c
|
|
664
|
-
.schema({
|
|
665
|
-
port: c.number({ required: true }),
|
|
666
|
-
host: c.string({ required: true }),
|
|
667
|
-
})
|
|
668
|
-
.load({ env: false, args: false, dir: "./config.d/" });
|
|
669
|
-
// All YAML/JSON files in the directory are loaded and merged (sorted by filename)
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
### Access source metadata with loadExtended
|
|
673
|
-
|
|
674
|
-
```typescript
|
|
675
|
-
const { data, warnings } = c
|
|
676
|
-
.schema({
|
|
677
|
-
port: c.number({ required: true, env: "PORT" }),
|
|
678
|
-
})
|
|
679
|
-
.loadExtended({ env: true, args: false, files: "./config.yaml" });
|
|
680
|
-
|
|
681
|
-
const portNode = data.port; // ConfigNode
|
|
682
|
-
console.log(portNode.value); // 3000
|
|
683
|
-
console.log(portNode.sourceType); // "env" | "file" | "default" | "args" | "envFile"
|
|
684
|
-
console.log(portNode.file); // "./config.yaml" or null
|
|
685
|
-
console.log(portNode.line); // source line number or null
|
|
686
|
-
```
|
|
687
|
-
|
|
688
|
-
### Combine .env files with process.env
|
|
689
|
-
|
|
690
|
-
```typescript
|
|
691
|
-
const config = c
|
|
692
|
-
.schema({
|
|
693
|
-
apiKey: c.string({ required: true, env: "API_KEY" }),
|
|
694
|
-
debug: c.bool({ env: "DEBUG", defaultValue: false }),
|
|
695
|
-
})
|
|
696
|
-
.load({
|
|
697
|
-
env: true, // reads process.env
|
|
698
|
-
args: false,
|
|
699
|
-
envFile: ["./.env", "./.env.local"], // .env.local overrides .env
|
|
700
|
-
});
|
|
701
|
-
// Priority: process.env > .env.local > .env > defaults
|
|
702
|
-
```
|
|
703
|
-
|
|
704
|
-
## Common Mistakes
|
|
705
|
-
|
|
706
|
-
### Forgetting `item` in `c.object()`
|
|
707
|
-
|
|
708
|
-
```typescript
|
|
709
|
-
// WRONG — fields are passed directly
|
|
710
|
-
c.object({ host: c.string(), port: c.number() });
|
|
711
|
-
|
|
712
|
-
// CORRECT — fields must be inside `item`
|
|
713
|
-
c.object({ item: { host: c.string(), port: c.number() } });
|
|
714
|
-
```
|
|
715
|
-
|
|
716
|
-
### Setting `env` on an option but not enabling env loading
|
|
717
|
-
|
|
718
|
-
```typescript
|
|
719
|
-
// WRONG — env: "PORT" is set but env loading is disabled
|
|
720
|
-
c.schema({ port: c.number({ env: "PORT" }) }).load({
|
|
721
|
-
env: false,
|
|
722
|
-
args: false,
|
|
723
|
-
files: "./config.yaml",
|
|
724
|
-
});
|
|
725
|
-
// This emits a warning: "Options [port] have env mappings but env loading is disabled"
|
|
726
|
-
|
|
727
|
-
// CORRECT — set env: true in load options
|
|
728
|
-
c.schema({ port: c.number({ env: "PORT" }) }).load({
|
|
729
|
-
env: true,
|
|
730
|
-
args: false,
|
|
731
|
-
files: "./config.yaml",
|
|
732
|
-
});
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
### Expecting `.env` files to work without `envFile`
|
|
736
|
-
|
|
737
|
-
```typescript
|
|
738
|
-
// WRONG — .env files are not loaded by default
|
|
739
|
-
c.schema({ key: c.string({ env: "API_KEY" }) }).load({
|
|
740
|
-
env: true,
|
|
741
|
-
args: false,
|
|
742
|
-
});
|
|
743
|
-
// This only reads process.env, not .env files
|
|
744
|
-
|
|
745
|
-
// CORRECT — explicitly pass envFile
|
|
746
|
-
c.schema({ key: c.string({ env: "API_KEY" }) }).load({
|
|
747
|
-
env: true,
|
|
748
|
-
args: false,
|
|
749
|
-
envFile: "./.env",
|
|
750
|
-
});
|
|
751
|
-
```
|
|
752
|
-
|
|
753
|
-
### Not catching `ConfigLoadError`
|
|
754
|
-
|
|
755
|
-
```typescript
|
|
756
|
-
// WRONG — unhandled error crashes the process with an unhelpful stack trace
|
|
757
|
-
const config = c
|
|
758
|
-
.schema({ port: c.number({ required: true }) })
|
|
759
|
-
.load({ env: false, args: false });
|
|
760
|
-
|
|
761
|
-
// CORRECT — catch and handle structured errors
|
|
762
|
-
try {
|
|
763
|
-
const config = c
|
|
764
|
-
.schema({ port: c.number({ required: true }) })
|
|
765
|
-
.load({ env: false, args: false });
|
|
766
|
-
} catch (err) {
|
|
767
|
-
if (err instanceof ConfigLoadError) {
|
|
768
|
-
console.error(err.errors); // structured error details
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
```
|
|
70
|
+
## Documentation
|
|
772
71
|
|
|
773
|
-
|
|
72
|
+
See the **[full documentation](https://meltstudio.github.io/config-loader/)** for:
|
|
774
73
|
|
|
775
|
-
|
|
74
|
+
- [Schema API](https://meltstudio.github.io/config-loader/schema-api) — primitives, objects, arrays, `oneOf`, `sensitive`, validation
|
|
75
|
+
- [Loading & Sources](https://meltstudio.github.io/config-loader/loading-and-sources) — `load()`, `loadExtended()`, file/env/CLI/.env sources, `printConfig()`, `maskSecrets()`, error handling, strict mode
|
|
76
|
+
- [TypeScript Utilities](https://meltstudio.github.io/config-loader/typescript-utilities) — `SchemaValue`, exported types, type narrowing
|
|
776
77
|
|
|
777
78
|
## Documentation for AI Agents
|
|
778
79
|
|
|
@@ -785,4 +86,4 @@ These files follow the [llms.txt standard](https://llmstxt.org/) and are generat
|
|
|
785
86
|
|
|
786
87
|
## License
|
|
787
88
|
|
|
788
|
-
|
|
89
|
+
Built by [Melt Studio](https://meltstudio.co). Licensed under the [MIT License](./LICENSE).
|
package/dist/index.d.ts
CHANGED
|
@@ -60,6 +60,7 @@ interface OptionClassParams<T extends OptionKind> {
|
|
|
60
60
|
env: string | null;
|
|
61
61
|
cli: boolean;
|
|
62
62
|
help: string;
|
|
63
|
+
sensitive?: boolean;
|
|
63
64
|
defaultValue?: TypedDefaultValue<T>;
|
|
64
65
|
oneOf?: ReadonlyArray<string | number | boolean>;
|
|
65
66
|
validate?: StandardSchemaV1;
|
|
@@ -100,7 +101,8 @@ declare class ConfigNode {
|
|
|
100
101
|
argName: string | null;
|
|
101
102
|
line: number | null;
|
|
102
103
|
column: number | null;
|
|
103
|
-
|
|
104
|
+
sensitive: boolean;
|
|
105
|
+
constructor(value: Value | ArrayValue, path: string, sourceType: SourceTypes, file: string | null, variableName: string | null, argName: string | null, line?: number | null, column?: number | null, sensitive?: boolean);
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
declare class PrimitiveOption<T extends PrimitiveKind = PrimitiveKind, Narrowed = TypeOfPrimitiveKind<T>> extends OptionBase<T> {
|
|
@@ -225,6 +227,24 @@ declare class SettingsBuilder<T extends Node> {
|
|
|
225
227
|
loadExtended(sources: SettingsSources<SchemaValue<T>>): ExtendedResult;
|
|
226
228
|
}
|
|
227
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Masks sensitive values in an `ExtendedResult` from `loadExtended()`.
|
|
232
|
+
* Fields marked `sensitive: true` have their values replaced with `"***"`.
|
|
233
|
+
*
|
|
234
|
+
* @param result - The `ExtendedResult` returned by `loadExtended()`.
|
|
235
|
+
* @returns A new `ExtendedResult` with sensitive values masked.
|
|
236
|
+
*/
|
|
237
|
+
declare function maskSecrets(result: ExtendedResult): ExtendedResult;
|
|
238
|
+
/**
|
|
239
|
+
* Masks sensitive values in a plain config object from `load()`.
|
|
240
|
+
* Fields marked `sensitive: true` in the schema have their values replaced with `"***"`.
|
|
241
|
+
*
|
|
242
|
+
* @param config - The plain config object returned by `load()`.
|
|
243
|
+
* @param schema - The schema definition used to identify sensitive fields.
|
|
244
|
+
* @returns A new object with sensitive values masked.
|
|
245
|
+
*/
|
|
246
|
+
declare function maskSecrets<T extends Record<string, unknown>>(config: T, schema: Node): T;
|
|
247
|
+
|
|
228
248
|
declare class ConfigNodeArray {
|
|
229
249
|
arrayValues: ConfigNode[];
|
|
230
250
|
constructor(arrayValues: ConfigNode[]);
|
|
@@ -262,6 +282,8 @@ interface OptionPropsArgs<T> {
|
|
|
262
282
|
defaultValue?: T | (() => T);
|
|
263
283
|
/** Help text shown in CLI `--help` output. */
|
|
264
284
|
help?: string;
|
|
285
|
+
/** Mark this field as sensitive. Sensitive values are masked by `printConfig()` and `maskSecrets()`. */
|
|
286
|
+
sensitive?: boolean;
|
|
265
287
|
/** Restrict the value to a fixed set of allowed values. Checked after type coercion, before `validate`. */
|
|
266
288
|
oneOf?: readonly T[];
|
|
267
289
|
/** Standard Schema validator run after type coercion. Accepts Zod, Valibot, ArkType, or any Standard Schema v1 implementation. */
|
|
@@ -341,4 +363,4 @@ declare const option: {
|
|
|
341
363
|
schema: <T extends Node>(theSchema: T) => SettingsBuilder<T>;
|
|
342
364
|
};
|
|
343
365
|
|
|
344
|
-
export { type ConfigErrorEntry, ConfigFileError, ConfigLoadError, ConfigNode, ConfigNodeArray, type ExtendedResult, type NodeTree, type RecursivePartial, type SchemaValue, type SettingsSources, type StandardSchemaV1, option as default, printConfig };
|
|
366
|
+
export { type ConfigErrorEntry, ConfigFileError, ConfigLoadError, ConfigNode, ConfigNodeArray, type ExtendedResult, type NodeTree, type RecursivePartial, type SchemaValue, type SettingsSources, type StandardSchemaV1, option as default, maskSecrets, printConfig };
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,7 @@ __export(index_exports, {
|
|
|
35
35
|
ConfigNode: () => configNode_default,
|
|
36
36
|
ConfigNodeArray: () => configNodeArray_default,
|
|
37
37
|
default: () => index_default,
|
|
38
|
+
maskSecrets: () => maskSecrets,
|
|
38
39
|
printConfig: () => printConfig
|
|
39
40
|
});
|
|
40
41
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -81,7 +82,8 @@ var ConfigNode = class {
|
|
|
81
82
|
argName;
|
|
82
83
|
line;
|
|
83
84
|
column;
|
|
84
|
-
|
|
85
|
+
sensitive;
|
|
86
|
+
constructor(value, path2, sourceType, file, variableName, argName, line = null, column = null, sensitive = false) {
|
|
85
87
|
this.value = value;
|
|
86
88
|
this.path = path2;
|
|
87
89
|
this.sourceType = sourceType;
|
|
@@ -90,6 +92,7 @@ var ConfigNode = class {
|
|
|
90
92
|
this.argName = argName;
|
|
91
93
|
this.line = line;
|
|
92
94
|
this.column = column;
|
|
95
|
+
this.sensitive = sensitive;
|
|
93
96
|
}
|
|
94
97
|
};
|
|
95
98
|
var configNode_default = ConfigNode;
|
|
@@ -123,6 +126,7 @@ var fs = __toESM(require("fs"));
|
|
|
123
126
|
var import_js_yaml = __toESM(require("js-yaml"));
|
|
124
127
|
var import_js_yaml_source_map = __toESM(require("js-yaml-source-map"));
|
|
125
128
|
var path = __toESM(require("path"));
|
|
129
|
+
var import_smol_toml = require("smol-toml");
|
|
126
130
|
var fileCache = /* @__PURE__ */ new Map();
|
|
127
131
|
var JsonSourceMap = class {
|
|
128
132
|
locations = /* @__PURE__ */ new Map();
|
|
@@ -185,6 +189,47 @@ var JsonSourceMap = class {
|
|
|
185
189
|
return this.locations.get(key);
|
|
186
190
|
}
|
|
187
191
|
};
|
|
192
|
+
function escapeRegex(str) {
|
|
193
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
194
|
+
}
|
|
195
|
+
function walkTomlObject(obj, prefix, lines, locations) {
|
|
196
|
+
for (const key of Object.keys(obj)) {
|
|
197
|
+
const fullPath = [...prefix, key].join(".");
|
|
198
|
+
for (let i = 0; i < lines.length; i++) {
|
|
199
|
+
const line = lines[i];
|
|
200
|
+
const keyPattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=`);
|
|
201
|
+
if (keyPattern.test(line)) {
|
|
202
|
+
const idx = line.indexOf(key);
|
|
203
|
+
locations.set(fullPath, {
|
|
204
|
+
line: i + 1,
|
|
205
|
+
column: idx + 1,
|
|
206
|
+
position: 0
|
|
207
|
+
});
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const val = obj[key];
|
|
212
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
213
|
+
walkTomlObject(
|
|
214
|
+
val,
|
|
215
|
+
[...prefix, key],
|
|
216
|
+
lines,
|
|
217
|
+
locations
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function buildTomlSourceMap(content, data) {
|
|
223
|
+
const locations = /* @__PURE__ */ new Map();
|
|
224
|
+
const lines = content.split("\n");
|
|
225
|
+
walkTomlObject(data, [], lines, locations);
|
|
226
|
+
return {
|
|
227
|
+
lookup(lookupPath) {
|
|
228
|
+
const key = Array.isArray(lookupPath) ? lookupPath.join(".") : lookupPath;
|
|
229
|
+
return locations.get(key);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
188
233
|
function loadConfigFile(filePath) {
|
|
189
234
|
const cached = fileCache.get(filePath);
|
|
190
235
|
if (cached) return cached;
|
|
@@ -199,6 +244,15 @@ function loadConfigFile(filePath) {
|
|
|
199
244
|
fileCache.set(filePath, result2);
|
|
200
245
|
return result2;
|
|
201
246
|
}
|
|
247
|
+
if (ext === ".toml") {
|
|
248
|
+
const data2 = (0, import_smol_toml.parse)(content);
|
|
249
|
+
const result2 = {
|
|
250
|
+
data: data2,
|
|
251
|
+
sourceMap: buildTomlSourceMap(content, data2)
|
|
252
|
+
};
|
|
253
|
+
fileCache.set(filePath, result2);
|
|
254
|
+
return result2;
|
|
255
|
+
}
|
|
202
256
|
const sourceMap = new import_js_yaml_source_map.default();
|
|
203
257
|
const data = import_js_yaml.default.load(content, { listener: sourceMap.listen() });
|
|
204
258
|
const result = { data, sourceMap };
|
|
@@ -292,6 +346,9 @@ var OptionBase = class {
|
|
|
292
346
|
envFileResults,
|
|
293
347
|
errors
|
|
294
348
|
);
|
|
349
|
+
if (resolved && this.params.sensitive) {
|
|
350
|
+
resolved.sensitive = true;
|
|
351
|
+
}
|
|
295
352
|
if (resolved && this.params.oneOf) {
|
|
296
353
|
const passed = this.runOneOfCheck(resolved, path2, errors);
|
|
297
354
|
if (!passed) return resolved;
|
|
@@ -1127,6 +1184,76 @@ var SettingsBuilder = class {
|
|
|
1127
1184
|
}
|
|
1128
1185
|
};
|
|
1129
1186
|
|
|
1187
|
+
// src/maskSecrets.ts
|
|
1188
|
+
var MASK = "***";
|
|
1189
|
+
function maskNodeTree(tree) {
|
|
1190
|
+
const result = {};
|
|
1191
|
+
for (const [key, entry] of Object.entries(tree)) {
|
|
1192
|
+
if (entry instanceof configNode_default) {
|
|
1193
|
+
if (entry.sensitive) {
|
|
1194
|
+
const masked = new configNode_default(
|
|
1195
|
+
MASK,
|
|
1196
|
+
entry.path,
|
|
1197
|
+
entry.sourceType,
|
|
1198
|
+
entry.file,
|
|
1199
|
+
entry.variableName,
|
|
1200
|
+
entry.argName,
|
|
1201
|
+
entry.line,
|
|
1202
|
+
entry.column,
|
|
1203
|
+
entry.sensitive
|
|
1204
|
+
);
|
|
1205
|
+
if (entry.value instanceof configNodeArray_default) {
|
|
1206
|
+
masked.value = entry.value;
|
|
1207
|
+
}
|
|
1208
|
+
result[key] = masked;
|
|
1209
|
+
} else {
|
|
1210
|
+
result[key] = entry;
|
|
1211
|
+
}
|
|
1212
|
+
} else {
|
|
1213
|
+
result[key] = maskNodeTree(entry);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return result;
|
|
1217
|
+
}
|
|
1218
|
+
function maskPlainObject(obj, schema2) {
|
|
1219
|
+
const result = {};
|
|
1220
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1221
|
+
const schemaNode = schema2[key];
|
|
1222
|
+
if (!schemaNode) {
|
|
1223
|
+
result[key] = value;
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
if (schemaNode instanceof ObjectOption) {
|
|
1227
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1228
|
+
result[key] = maskPlainObject(
|
|
1229
|
+
value,
|
|
1230
|
+
schemaNode.item
|
|
1231
|
+
);
|
|
1232
|
+
} else {
|
|
1233
|
+
result[key] = value;
|
|
1234
|
+
}
|
|
1235
|
+
} else if (schemaNode instanceof OptionBase) {
|
|
1236
|
+
result[key] = schemaNode.params.sensitive ? MASK : value;
|
|
1237
|
+
} else {
|
|
1238
|
+
result[key] = value;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
return result;
|
|
1242
|
+
}
|
|
1243
|
+
function maskSecrets(resultOrConfig, schema2) {
|
|
1244
|
+
if ("data" in resultOrConfig && "warnings" in resultOrConfig && !schema2) {
|
|
1245
|
+
const extended = resultOrConfig;
|
|
1246
|
+
return {
|
|
1247
|
+
data: maskNodeTree(extended.data),
|
|
1248
|
+
warnings: [...extended.warnings]
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
if (schema2) {
|
|
1252
|
+
return maskPlainObject(resultOrConfig, schema2);
|
|
1253
|
+
}
|
|
1254
|
+
return resultOrConfig;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1130
1257
|
// src/printConfig.ts
|
|
1131
1258
|
function truncate(str, max) {
|
|
1132
1259
|
if (str.length <= max) return str;
|
|
@@ -1134,7 +1261,7 @@ function truncate(str, max) {
|
|
|
1134
1261
|
}
|
|
1135
1262
|
function formatValue(val) {
|
|
1136
1263
|
if (val === null || val === void 0) return "";
|
|
1137
|
-
if (typeof val === "object") return JSON.stringify(val)
|
|
1264
|
+
if (typeof val === "object") return JSON.stringify(val);
|
|
1138
1265
|
if (typeof val === "string") return val;
|
|
1139
1266
|
return `${val}`;
|
|
1140
1267
|
}
|
|
@@ -1183,7 +1310,7 @@ function flattenTree(tree, prefix = "") {
|
|
|
1183
1310
|
} else {
|
|
1184
1311
|
rows.push({
|
|
1185
1312
|
path: path2,
|
|
1186
|
-
value: formatValue(entry.value),
|
|
1313
|
+
value: entry.sensitive ? "***" : formatValue(entry.value),
|
|
1187
1314
|
source: entry.sourceType,
|
|
1188
1315
|
detail: formatDetail(entry)
|
|
1189
1316
|
});
|
|
@@ -1306,5 +1433,6 @@ var index_default = option;
|
|
|
1306
1433
|
ConfigLoadError,
|
|
1307
1434
|
ConfigNode,
|
|
1308
1435
|
ConfigNodeArray,
|
|
1436
|
+
maskSecrets,
|
|
1309
1437
|
printConfig
|
|
1310
1438
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meltstudio/config-loader",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Type-safe configuration loader with full TypeScript inference. Load from YAML, JSON, .env, environment variables, and CLI args.",
|
|
3
|
+
"version": "3.6.0",
|
|
4
|
+
"description": "Type-safe configuration loader with full TypeScript inference. Load from YAML, JSON, TOML, .env, environment variables, and CLI args.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"repository": "https://github.com/MeltStudio/config-loader",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"config",
|
|
16
16
|
"yaml",
|
|
17
17
|
"json",
|
|
18
|
+
"toml",
|
|
18
19
|
"loader",
|
|
19
20
|
"env",
|
|
20
21
|
"environment",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"commander": "^14.0.0",
|
|
49
50
|
"js-yaml": "^4.1.0",
|
|
50
51
|
"js-yaml-source-map": "^0.2.2",
|
|
52
|
+
"smol-toml": "^1.6.0",
|
|
51
53
|
"tslib": "^2.3.0"
|
|
52
54
|
},
|
|
53
55
|
"devDependencies": {
|