@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.
- package/README.md +19 -773
- package/dist/index.js +52 -1
- 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,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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
+
See the **[full documentation](https://meltstudio.github.io/config-loader/)** for:
|
|
829
73
|
|
|
830
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|