@meltstudio/config-loader 3.5.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.
Files changed (3) hide show
  1. package/README.md +19 -773
  2. package/dist/index.js +52 -1
  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 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,47 +16,34 @@ 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
@@ -68,10 +55,6 @@ No separate interface to maintain. No `as` casts. The types flow from the schema
68
55
  - **Default values** — static or computed (via functions)
69
56
  - **Multiple files / directory loading** — load from a list of files or an entire directory
70
57
 
71
- ## Requirements
72
-
73
- - Node.js >= 20
74
-
75
58
  ## Installation
76
59
 
77
60
  ```bash
@@ -82,752 +65,15 @@ npm install @meltstudio/config-loader
82
65
  yarn add @meltstudio/config-loader
83
66
  ```
84
67
 
85
- ## Quick Start
86
-
87
- **config.yaml:**
88
-
89
- ```yaml
90
- version: 1.0.0
91
- website:
92
- title: My Website
93
- description: A simple and elegant website
94
- isProduction: false
95
-
96
- database:
97
- host: localhost
98
- port: 5432
99
- credentials:
100
- username: admin
101
- password: secret
102
-
103
- socialMedia: [https://twitter.com/example, https://instagram.com/example]
104
-
105
- features:
106
- - name: Store
107
- enabled: true
108
- - name: Admin
109
- enabled: false
110
- ```
111
-
112
- **index.ts:**
113
-
114
- ```typescript
115
- import path from "path";
116
- import c from "@meltstudio/config-loader";
117
-
118
- const config = c
119
- .schema({
120
- version: c.string({ required: true, cli: true }),
121
- website: c.object({
122
- item: {
123
- title: c.string({ required: true }),
124
- url: c.string({
125
- required: false,
126
- defaultValue: "www.mywebsite.dev",
127
- }),
128
- description: c.string({ required: true }),
129
- isProduction: c.bool({ required: true }),
130
- },
131
- }),
132
- database: c.object({
133
- item: {
134
- host: c.string({ required: true }),
135
- port: c.number({ required: true }),
136
- credentials: c.object({
137
- item: {
138
- username: c.string(),
139
- password: c.string(),
140
- },
141
- }),
142
- },
143
- }),
144
- socialMedia: c.array({
145
- required: true,
146
- item: c.string({ required: true }),
147
- }),
148
- features: c.array({
149
- required: true,
150
- item: c.object({
151
- item: {
152
- name: c.string(),
153
- enabled: c.bool(),
154
- },
155
- }),
156
- }),
157
- })
158
- .load({
159
- env: false,
160
- args: true,
161
- files: path.join(__dirname, "./config.yaml"),
162
- });
163
-
164
- console.log(JSON.stringify(config, null, 2));
165
- ```
166
-
167
- Output:
168
-
169
- ```json
170
- {
171
- "version": "1.0.0",
172
- "website": {
173
- "title": "My Website",
174
- "url": "www.mywebsite.dev",
175
- "description": "A simple and elegant website",
176
- "isProduction": false
177
- },
178
- "database": {
179
- "host": "localhost",
180
- "port": 5432,
181
- "credentials": {
182
- "username": "admin",
183
- "password": "secret"
184
- }
185
- },
186
- "socialMedia": [
187
- "https://twitter.com/example",
188
- "https://instagram.com/example"
189
- ],
190
- "features": [
191
- { "name": "Store", "enabled": true },
192
- { "name": "Admin", "enabled": false }
193
- ]
194
- }
195
- ```
196
-
197
- ## Schema API
198
-
199
- ### Primitives
200
-
201
- ```typescript
202
- c.string({
203
- required: true,
204
- env: "MY_VAR",
205
- cli: true,
206
- defaultValue: "fallback",
207
- });
208
- c.number({ required: true, env: "PORT" });
209
- c.bool({ env: "DEBUG", defaultValue: false });
210
- ```
211
-
212
- ### Objects
213
-
214
- Use `c.object()` to declare nested object schemas:
215
-
216
- ```typescript
217
- c.object({
218
- item: {
219
- host: c.string(),
220
- port: c.number(),
221
- },
222
- });
223
- ```
224
-
225
- Objects can be nested arbitrarily deep:
226
-
227
- ```typescript
228
- c.schema({
229
- database: c.object({
230
- item: {
231
- host: c.string(),
232
- port: c.number(),
233
- credentials: c.object({
234
- item: {
235
- username: c.string(),
236
- password: c.string({ env: "DB_PASSWORD" }),
237
- },
238
- }),
239
- },
240
- }),
241
- });
242
- ```
243
-
244
- `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.
245
-
246
- ### Arrays
247
-
248
- ```typescript
249
- c.array({ required: true, item: c.string() }); // string[]
250
- c.array({ required: true, item: c.number() }); // number[]
251
- c.array({
252
- item: c.object({
253
- item: { name: c.string(), age: c.number() },
254
- }),
255
- }); // { name: string; age: number }[]
256
- ```
257
-
258
- ## Enum Constraints (`oneOf`)
259
-
260
- Use `oneOf` to restrict a field to a fixed set of allowed values. The check runs after type coercion and before any `validate` schema:
261
-
262
- ```typescript
263
- const config = c
264
- .schema({
265
- env: c.string({
266
- env: "NODE_ENV",
267
- defaultValue: "development",
268
- oneOf: ["development", "staging", "production"],
269
- }),
270
- logLevel: c.number({
271
- env: "LOG_LEVEL",
272
- defaultValue: 1,
273
- oneOf: [0, 1, 2, 3],
274
- }),
275
- })
276
- .load({ env: true, args: false });
277
- ```
278
-
279
- If a value is not in the allowed set, a `ConfigLoadError` is thrown with `kind: "validation"`.
280
-
281
- ### Type Narrowing
282
-
283
- When `oneOf` is provided, the inferred type is automatically narrowed to the union of the allowed values:
284
-
285
- ```typescript
286
- const config = c
287
- .schema({
288
- env: c.string({ oneOf: ["dev", "staging", "prod"] }),
289
- })
290
- .load({ env: false, args: false });
291
-
292
- // config.env is typed as "dev" | "staging" | "prod", not string
293
- ```
294
-
295
- When used with `cli: true`, the `--help` output automatically lists the allowed values.
296
-
297
- ## Sensitive Fields
298
-
299
- Mark fields as `sensitive: true` to prevent their values from being exposed in logs or debug output:
300
-
301
- ```typescript
302
- const schema = {
303
- host: c.string({ defaultValue: "localhost" }),
304
- apiKey: c.string({ env: "API_KEY", sensitive: true }),
305
- db: c.object({
306
- item: {
307
- host: c.string({ defaultValue: "db.local" }),
308
- password: c.string({ env: "DB_PASS", sensitive: true }),
309
- },
310
- }),
311
- };
312
-
313
- const config = c.schema(schema).load({ env: true, args: false });
314
- ```
315
-
316
- Sensitive values load normally — `config.apiKey` returns the real value. The flag only affects masking utilities.
317
-
318
- ### `printConfig()` auto-masking
319
-
320
- `printConfig()` automatically masks sensitive fields:
321
-
322
- ```typescript
323
- const result = c.schema(schema).loadExtended({ env: true, args: false });
324
- printConfig(result);
325
- // apiKey shows "***" instead of the real value
326
- // db.password shows "***" instead of the real value
327
- ```
328
-
329
- ### `maskSecrets()`
330
-
331
- Use `maskSecrets()` to create a safe-to-log copy of your config:
332
-
333
- ```typescript
334
- import c, { maskSecrets } from "@meltstudio/config-loader";
335
-
336
- // With a plain config from load()
337
- const config = c.schema(schema).load({ env: true, args: false });
338
- console.log(maskSecrets(config, schema));
339
- // { host: "localhost", apiKey: "***", db: { host: "db.local", password: "***" } }
340
-
341
- // With an extended result from loadExtended()
342
- const result = c.schema(schema).loadExtended({ env: true, args: false });
343
- const masked = maskSecrets(result);
344
- // masked.data contains ConfigNodes with "***" for sensitive values
345
- ```
346
-
347
- The original config object is never mutated — `maskSecrets()` always returns a new copy.
348
-
349
- ## Validation
350
-
351
- 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.
352
-
353
- 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).
354
-
355
- ### With Zod
356
-
357
- ```typescript
358
- import c from "@meltstudio/config-loader";
359
- import { z } from "zod";
360
-
361
- const config = c
362
- .schema({
363
- port: c.number({
364
- required: true,
365
- env: "PORT",
366
- validate: z.number().min(1).max(65535),
367
- }),
368
- host: c.string({
369
- required: true,
370
- validate: z.string().url(),
371
- }),
372
- env: c.string({
373
- defaultValue: "development",
374
- validate: z.enum(["development", "staging", "production"]),
375
- }),
376
- })
377
- .load({ env: true, args: false, files: "./config.yaml" });
378
- ```
379
-
380
- ### With a custom validator
381
-
382
- Any object with a `~standard.validate()` method works:
383
-
384
- ```typescript
385
- const portValidator = {
386
- "~standard": {
387
- version: 1,
388
- vendor: "my-app",
389
- validate(value: unknown) {
390
- if (typeof value === "number" && value >= 1 && value <= 65535) {
391
- return { value };
392
- }
393
- return { issues: [{ message: "must be a valid port (1-65535)" }] };
394
- },
395
- },
396
- };
397
-
398
- c.number({ required: true, env: "PORT", validate: portValidator });
399
- ```
400
-
401
- Validation errors are collected alongside other config errors and thrown as `ConfigLoadError` with `kind: "validation"`:
402
-
403
- ```typescript
404
- try {
405
- const config = c.schema({ ... }).load({ ... });
406
- } catch (err) {
407
- if (err instanceof ConfigLoadError) {
408
- for (const entry of err.errors) {
409
- if (entry.kind === "validation") {
410
- console.error(`Validation: ${entry.path} — ${entry.message}`);
411
- }
412
- }
413
- }
414
- }
415
- ```
416
-
417
- ## Loading Sources
418
-
419
- ```typescript
420
- .load({
421
- env: true, // Read from process.env
422
- args: true, // Read from CLI arguments (--database.port 3000)
423
- files: "./config.yaml", // Single YAML file
424
- files: "./config.json", // Single JSON file
425
- files: ["./base.yaml", "./overrides.json"], // Mix YAML and JSON (first takes priority)
426
- dir: "./config.d/", // All files in a directory (sorted)
427
- envFile: "./.env", // Single .env file
428
- envFile: ["./.env", "./.env.local"], // Multiple .env files (later overrides earlier)
429
- defaults: { port: 3000 }, // Programmatic defaults
430
- })
431
- ```
432
-
433
- Both YAML (`.yaml`, `.yml`) and JSON (`.json`) files are supported. The format is detected automatically from the file extension.
434
-
435
- **Priority order:** CLI arguments > `process.env` > `.env` files > Config files > Defaults
436
-
437
- ## Extended Loading (Source Metadata)
438
-
439
- 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.
440
-
441
- ```typescript
442
- import c from "@meltstudio/config-loader";
443
-
444
- const { data, warnings } = c
445
- .schema({
446
- port: c.number({ required: true, env: "PORT" }),
447
- host: c.string({ defaultValue: "localhost" }),
448
- })
449
- .loadExtended({
450
- env: true,
451
- args: false,
452
- files: "./config.yaml",
453
- });
454
-
455
- // `warnings` is a string[] of non-fatal issues (e.g. type coercions, unused env mappings)
456
- if (warnings.length > 0) {
457
- warnings.forEach((w) => console.warn(w));
458
- }
459
-
460
- // Each leaf in `data` is a ConfigNode with:
461
- // {
462
- // value: 3000,
463
- // path: "port",
464
- // sourceType: "env" | "envFile" | "file" | "args" | "default",
465
- // file: "./config.yaml" | "./.env" | null,
466
- // variableName: "PORT" | null,
467
- // argName: null,
468
- // line: 5 | null, // source line (1-based) for YAML, JSON, and .env files; null for env/args/default
469
- // column: 3 | null // source column (1-based) for YAML, JSON, and .env files; null for env/args/default
470
- // }
471
- console.log(data.port.value); // 3000
472
- console.log(data.port.sourceType); // "env"
473
- console.log(data.port.variableName); // "PORT"
474
- ```
475
-
476
- This is useful for debugging configuration resolution, building admin UIs that show where each setting originated, or auditing which sources are active.
477
-
478
- ### Debug Helper
479
-
480
- Use `printConfig()` to format the result of `loadExtended()` as a readable table:
481
-
482
- ```typescript
483
- import c, { printConfig } from "@meltstudio/config-loader";
484
-
485
- const result = c
486
- .schema({
487
- host: c.string({ defaultValue: "localhost" }),
488
- port: c.number({ env: "PORT" }),
489
- debug: c.bool({ cli: true }),
490
- })
491
- .loadExtended({ env: true, args: true, files: "./config.yaml" });
492
-
493
- printConfig(result);
494
- ```
495
-
496
- Output:
497
-
498
- ```
499
- ┌───────┬───────────┬─────────┬────────────────┐
500
- │ Path │ Value │ Source │ Detail │
501
- ├───────┼───────────┼─────────┼────────────────┤
502
- │ host │ localhost │ default │ │
503
- │ port │ 8080 │ env │ PORT │
504
- │ debug │ true │ args │ --debug │
505
- └───────┴───────────┴─────────┴────────────────┘
506
- ```
507
-
508
- Options:
509
-
510
- - `printConfig(result, { silent: true })` — returns the string without printing to console
511
- - `printConfig(result, { maxValueLength: 30 })` — truncate long values (default: 50)
512
-
513
- ## Error Handling
514
-
515
- When validation fails, config-loader throws a `ConfigLoadError` with structured error details:
516
-
517
- ```typescript
518
- import c, { ConfigLoadError } from "@meltstudio/config-loader";
519
-
520
- try {
521
- const config = c.schema({ port: c.number({ required: true }) }).load({
522
- env: false,
523
- args: false,
524
- files: "./config.yaml",
525
- });
526
- } catch (err) {
527
- if (err instanceof ConfigLoadError) {
528
- for (const entry of err.errors) {
529
- console.error(`[${entry.kind}] ${entry.message}`);
530
- // e.g. [required] Required option 'port' not provided.
531
- }
532
- }
533
- }
534
- ```
535
-
536
- 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.
537
-
538
- ### Strict Mode
539
-
540
- Enable `strict: true` to promote all warnings to errors, causing `ConfigLoadError` to be thrown for any ambiguous or lossy configuration:
541
-
542
- ```typescript
543
- .load({
544
- env: true,
545
- args: false,
546
- files: "./config.yaml",
547
- strict: true,
548
- })
549
- ```
550
-
551
- 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.
552
-
553
- ## TypeScript Utilities
554
-
555
- config-loader exports several types for advanced use cases:
556
-
557
- ```typescript
558
- import c, {
559
- type SchemaValue, // Infer the resolved config type from a schema
560
- type SettingsSources, // Type for the sources object passed to load()
561
- type ExtendedResult, // Return type of loadExtended()
562
- type NodeTree, // Tree of ConfigNode objects (ExtendedResult.data)
563
- ConfigNode, // Class representing a resolved value with source metadata
564
- ConfigNodeArray, // Class representing an array of ConfigNode values
565
- type RecursivePartial, // Deep partial utility used by the defaults option
566
- type StandardSchemaV1, // Standard Schema v1 interface for validators
567
- maskSecrets, // Create a safe-to-log copy with sensitive values masked
568
- printConfig, // Format loadExtended() result as a readable table
569
- } from "@meltstudio/config-loader";
570
- ```
571
-
572
- The most commonly needed is `SchemaValue`, which infers the plain TypeScript type from a schema:
573
-
574
- ```typescript
575
- const mySchema = {
576
- port: c.number({ env: "PORT" }),
577
- db: c.object({ item: { host: c.string(), port: c.number() } }),
578
- };
579
-
580
- type MyConfig = SchemaValue<typeof mySchema>;
581
- // { port: number; db: { host: string; port: number } }
582
- ```
583
-
584
- ## CLI Arguments
585
-
586
- Set `cli: true` on an option to allow overriding via command line:
587
-
588
- ```typescript
589
- c.schema({
590
- version: c.string({ required: true, cli: true }),
591
- });
592
- ```
593
-
594
- ```bash
595
- node app.js --version 2.0.0
596
- ```
597
-
598
- ## Environment Variables
599
-
600
- Set `env: "VAR_NAME"` on an option and `env: true` in the load options:
601
-
602
- ```typescript
603
- c.schema({
604
- database: c.object({
605
- item: {
606
- password: c.string({ env: "DB_PASSWORD" }),
607
- },
608
- }),
609
- }).load({ env: true, args: false, files: "./config.yaml" });
610
- ```
611
-
612
- ## `.env` File Support
613
-
614
- 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.
615
-
616
- **.env:**
617
-
618
- ```bash
619
- DB_HOST=localhost
620
- DB_PORT=5432
621
- DB_PASSWORD="s3cret"
622
- APP_NAME='My App'
623
- # This is a comment
624
- ```
625
-
626
- **Usage:**
627
-
628
- ```typescript
629
- const config = c
630
- .schema({
631
- host: c.string({ env: "DB_HOST" }),
632
- port: c.number({ env: "DB_PORT" }),
633
- password: c.string({ env: "DB_PASSWORD" }),
634
- })
635
- .load({
636
- env: true,
637
- args: false,
638
- envFile: "./.env",
639
- });
640
- ```
641
-
642
- `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.
643
-
644
- **Multiple `.env` files:**
645
-
646
- ```typescript
647
- .load({
648
- env: true,
649
- args: false,
650
- envFile: ["./.env", "./.env.local"], // .env.local overrides .env
651
- })
652
- ```
68
+ Requires Node.js >= 20.
653
69
 
654
- When using multiple files, later files override earlier ones for the same key.
655
-
656
- The `.env` parser supports:
657
-
658
- - `KEY=VALUE` pairs (whitespace trimmed)
659
- - Comments (lines starting with `#`)
660
- - Quoted values (double `"..."` or single `'...'` quotes stripped)
661
- - Empty values (`KEY=`)
662
-
663
- When using `loadExtended()`, values from `.env` files have `sourceType: "envFile"` with `file`, `line`, and `column` metadata pointing to the `.env` file location.
664
-
665
- ## Common Patterns
666
-
667
- ### Load from YAML with env overrides
668
-
669
- ```typescript
670
- import c from "@meltstudio/config-loader";
671
-
672
- const config = c
673
- .schema({
674
- port: c.number({ required: true, env: "PORT", defaultValue: 3000 }),
675
- host: c.string({ required: true, env: "HOST", defaultValue: "localhost" }),
676
- })
677
- .load({ env: true, args: false, files: "./config.yaml" });
678
- ```
679
-
680
- ### Strict mode for production
681
-
682
- ```typescript
683
- const config = c
684
- .schema({
685
- port: c.number({ required: true, env: "PORT" }),
686
- dbUrl: c.string({ required: true, env: "DATABASE_URL" }),
687
- })
688
- .load({
689
- env: true,
690
- args: false,
691
- files: "./config.yaml",
692
- strict: true, // any type coercion or ambiguity throws an error
693
- });
694
- ```
695
-
696
- ### Catch and inspect errors
697
-
698
- ```typescript
699
- import c, { ConfigLoadError } from "@meltstudio/config-loader";
700
-
701
- try {
702
- const config = c
703
- .schema({ port: c.number({ required: true }) })
704
- .load({ env: false, args: false, files: "./config.yaml" });
705
- } catch (err) {
706
- if (err instanceof ConfigLoadError) {
707
- for (const entry of err.errors) {
708
- console.error(`[${entry.kind}] ${entry.path}: ${entry.message}`);
709
- }
710
- // err.warnings contains non-fatal issues
711
- }
712
- }
713
- ```
714
-
715
- ### Load from a directory of config files
716
-
717
- ```typescript
718
- const config = c
719
- .schema({
720
- port: c.number({ required: true }),
721
- host: c.string({ required: true }),
722
- })
723
- .load({ env: false, args: false, dir: "./config.d/" });
724
- // All YAML/JSON files in the directory are loaded and merged (sorted by filename)
725
- ```
726
-
727
- ### Access source metadata with loadExtended
728
-
729
- ```typescript
730
- const { data, warnings } = c
731
- .schema({
732
- port: c.number({ required: true, env: "PORT" }),
733
- })
734
- .loadExtended({ env: true, args: false, files: "./config.yaml" });
735
-
736
- const portNode = data.port; // ConfigNode
737
- console.log(portNode.value); // 3000
738
- console.log(portNode.sourceType); // "env" | "file" | "default" | "args" | "envFile"
739
- console.log(portNode.file); // "./config.yaml" or null
740
- console.log(portNode.line); // source line number or null
741
- ```
742
-
743
- ### Combine .env files with process.env
744
-
745
- ```typescript
746
- const config = c
747
- .schema({
748
- apiKey: c.string({ required: true, env: "API_KEY" }),
749
- debug: c.bool({ env: "DEBUG", defaultValue: false }),
750
- })
751
- .load({
752
- env: true, // reads process.env
753
- args: false,
754
- envFile: ["./.env", "./.env.local"], // .env.local overrides .env
755
- });
756
- // Priority: process.env > .env.local > .env > defaults
757
- ```
758
-
759
- ## Common Mistakes
760
-
761
- ### Forgetting `item` in `c.object()`
762
-
763
- ```typescript
764
- // WRONG — fields are passed directly
765
- c.object({ host: c.string(), port: c.number() });
766
-
767
- // CORRECT — fields must be inside `item`
768
- c.object({ item: { host: c.string(), port: c.number() } });
769
- ```
770
-
771
- ### Setting `env` on an option but not enabling env loading
772
-
773
- ```typescript
774
- // WRONG — env: "PORT" is set but env loading is disabled
775
- c.schema({ port: c.number({ env: "PORT" }) }).load({
776
- env: false,
777
- args: false,
778
- files: "./config.yaml",
779
- });
780
- // This emits a warning: "Options [port] have env mappings but env loading is disabled"
781
-
782
- // CORRECT — set env: true in load options
783
- c.schema({ port: c.number({ env: "PORT" }) }).load({
784
- env: true,
785
- args: false,
786
- files: "./config.yaml",
787
- });
788
- ```
789
-
790
- ### Expecting `.env` files to work without `envFile`
791
-
792
- ```typescript
793
- // WRONG — .env files are not loaded by default
794
- c.schema({ key: c.string({ env: "API_KEY" }) }).load({
795
- env: true,
796
- args: false,
797
- });
798
- // This only reads process.env, not .env files
799
-
800
- // CORRECT — explicitly pass envFile
801
- c.schema({ key: c.string({ env: "API_KEY" }) }).load({
802
- env: true,
803
- args: false,
804
- envFile: "./.env",
805
- });
806
- ```
807
-
808
- ### Not catching `ConfigLoadError`
809
-
810
- ```typescript
811
- // WRONG — unhandled error crashes the process with an unhelpful stack trace
812
- const config = c
813
- .schema({ port: c.number({ required: true }) })
814
- .load({ env: false, args: false });
815
-
816
- // CORRECT — catch and handle structured errors
817
- try {
818
- const config = c
819
- .schema({ port: c.number({ required: true }) })
820
- .load({ env: false, args: false });
821
- } catch (err) {
822
- if (err instanceof ConfigLoadError) {
823
- console.error(err.errors); // structured error details
824
- }
825
- }
826
- ```
70
+ ## Documentation
827
71
 
828
- ### Using `required: true` on nested fields without the parent object
72
+ See the **[full documentation](https://meltstudio.github.io/config-loader/)** for:
829
73
 
830
- 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
831
77
 
832
78
  ## Documentation for AI Agents
833
79
 
@@ -840,4 +86,4 @@ These files follow the [llms.txt standard](https://llmstxt.org/) and are generat
840
86
 
841
87
  ## License
842
88
 
843
- 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.js CHANGED
@@ -126,6 +126,7 @@ var fs = __toESM(require("fs"));
126
126
  var import_js_yaml = __toESM(require("js-yaml"));
127
127
  var import_js_yaml_source_map = __toESM(require("js-yaml-source-map"));
128
128
  var path = __toESM(require("path"));
129
+ var import_smol_toml = require("smol-toml");
129
130
  var fileCache = /* @__PURE__ */ new Map();
130
131
  var JsonSourceMap = class {
131
132
  locations = /* @__PURE__ */ new Map();
@@ -188,6 +189,47 @@ var JsonSourceMap = class {
188
189
  return this.locations.get(key);
189
190
  }
190
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
+ }
191
233
  function loadConfigFile(filePath) {
192
234
  const cached = fileCache.get(filePath);
193
235
  if (cached) return cached;
@@ -202,6 +244,15 @@ function loadConfigFile(filePath) {
202
244
  fileCache.set(filePath, result2);
203
245
  return result2;
204
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
+ }
205
256
  const sourceMap = new import_js_yaml_source_map.default();
206
257
  const data = import_js_yaml.default.load(content, { listener: sourceMap.listen() });
207
258
  const result = { data, sourceMap };
@@ -1210,7 +1261,7 @@ function truncate(str, max) {
1210
1261
  }
1211
1262
  function formatValue(val) {
1212
1263
  if (val === null || val === void 0) return "";
1213
- if (typeof val === "object") return JSON.stringify(val) ?? "";
1264
+ if (typeof val === "object") return JSON.stringify(val);
1214
1265
  if (typeof val === "string") return val;
1215
1266
  return `${val}`;
1216
1267
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meltstudio/config-loader",
3
- "version": "3.5.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": {