@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 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 JSON files, `.env` files, environment variables, and CLI arguments — and get a fully typed result with zero manual type annotations.
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
- credentials: c.object({
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 files, JSON files, `.env` files, environment variables, CLI arguments
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
- ## Quick Start
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
- ### Load from YAML with env overrides
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
- ### Using `required: true` on nested fields without the parent object
72
+ See the **[full documentation](https://meltstudio.github.io/config-loader/)** for:
774
73
 
775
- If the parent object is entirely absent from all sources, child `required` fields will still trigger errors. Use `required` on the parent `c.object()` only if the entire subtree must be present.
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
- This package is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
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
- constructor(value: Value | ArrayValue, path: string, sourceType: SourceTypes, file: string | null, variableName: string | null, argName: string | null, line?: number | null, column?: number | null);
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
- constructor(value, path2, sourceType, file, variableName, argName, line = null, column = null) {
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.0",
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": {