@meltstudio/config-loader 2.0.1 → 3.0.1
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 +205 -8
- package/dist/index.d.ts +70 -4
- package/dist/index.js +72 -16
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
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.
|
|
4
4
|
|
|
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
|
+
|
|
7
|
+
**[Full documentation](https://meltstudio.github.io/config-loader/)**
|
|
8
|
+
|
|
5
9
|
## Why config-loader?
|
|
6
10
|
|
|
7
11
|
Most config libraries give you `Record<string, unknown>` and leave you to cast or validate manually. config-loader infers TypeScript types directly from your schema definition:
|
|
@@ -56,7 +60,8 @@ No separate interface to maintain. No `as` casts. The types flow from the schema
|
|
|
56
60
|
- **Priority resolution** — CLI > process.env > `.env` files > Config files > Defaults
|
|
57
61
|
- **`.env` file support** — load environment variables from `.env` files with automatic line tracking
|
|
58
62
|
- **Nested objects and arrays** — deeply nested configs with full type safety
|
|
59
|
-
- **Structured errors** — typed `ConfigLoadError` with per-field error details
|
|
63
|
+
- **Structured errors** — typed `ConfigLoadError` with per-field error details and warnings
|
|
64
|
+
- **Strict mode** — promote warnings to errors for production safety
|
|
60
65
|
- **Default values** — static or computed (via functions)
|
|
61
66
|
- **Multiple files / directory loading** — load from a list of files or an entire directory
|
|
62
67
|
|
|
@@ -274,7 +279,7 @@ Use `loadExtended()` instead of `load()` to get each value wrapped in a `ConfigN
|
|
|
274
279
|
```typescript
|
|
275
280
|
import c from "@meltstudio/config-loader";
|
|
276
281
|
|
|
277
|
-
const
|
|
282
|
+
const { data, warnings } = c
|
|
278
283
|
.schema({
|
|
279
284
|
port: c.number({ required: true, env: "PORT" }),
|
|
280
285
|
host: c.string({ defaultValue: "localhost" }),
|
|
@@ -285,7 +290,12 @@ const extended = c
|
|
|
285
290
|
files: "./config.yaml",
|
|
286
291
|
});
|
|
287
292
|
|
|
288
|
-
//
|
|
293
|
+
// `warnings` is a string[] of non-fatal issues (e.g. type coercions, unused env mappings)
|
|
294
|
+
if (warnings.length > 0) {
|
|
295
|
+
warnings.forEach((w) => console.warn(w));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Each leaf in `data` is a ConfigNode with:
|
|
289
299
|
// {
|
|
290
300
|
// value: 3000,
|
|
291
301
|
// path: "port",
|
|
@@ -296,9 +306,9 @@ const extended = c
|
|
|
296
306
|
// line: 5 | null, // source line (1-based) for YAML, JSON, and .env files; null for env/args/default
|
|
297
307
|
// column: 3 | null // source column (1-based) for YAML, JSON, and .env files; null for env/args/default
|
|
298
308
|
// }
|
|
299
|
-
console.log(
|
|
300
|
-
console.log(
|
|
301
|
-
console.log(
|
|
309
|
+
console.log(data.port.value); // 3000
|
|
310
|
+
console.log(data.port.sourceType); // "env"
|
|
311
|
+
console.log(data.port.variableName); // "PORT"
|
|
302
312
|
```
|
|
303
313
|
|
|
304
314
|
This is useful for debugging configuration resolution, building admin UIs that show where each setting originated, or auditing which sources are active.
|
|
@@ -326,12 +336,23 @@ try {
|
|
|
326
336
|
}
|
|
327
337
|
```
|
|
328
338
|
|
|
329
|
-
|
|
339
|
+
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.
|
|
340
|
+
|
|
341
|
+
### Strict Mode
|
|
342
|
+
|
|
343
|
+
Enable `strict: true` to promote all warnings to errors, causing `ConfigLoadError` to be thrown for any ambiguous or lossy configuration:
|
|
330
344
|
|
|
331
345
|
```typescript
|
|
332
|
-
.load({
|
|
346
|
+
.load({
|
|
347
|
+
env: true,
|
|
348
|
+
args: false,
|
|
349
|
+
files: "./config.yaml",
|
|
350
|
+
strict: true,
|
|
351
|
+
})
|
|
333
352
|
```
|
|
334
353
|
|
|
354
|
+
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.
|
|
355
|
+
|
|
335
356
|
## CLI Arguments
|
|
336
357
|
|
|
337
358
|
Set `cli: true` on an option to allow overriding via command line:
|
|
@@ -413,6 +434,182 @@ The `.env` parser supports:
|
|
|
413
434
|
|
|
414
435
|
When using `loadExtended()`, values from `.env` files have `sourceType: "envFile"` with `file`, `line`, and `column` metadata pointing to the `.env` file location.
|
|
415
436
|
|
|
437
|
+
## Common Patterns
|
|
438
|
+
|
|
439
|
+
### Load from YAML with env overrides
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
import c from "@meltstudio/config-loader";
|
|
443
|
+
|
|
444
|
+
const config = c
|
|
445
|
+
.schema({
|
|
446
|
+
port: c.number({ required: true, env: "PORT", defaultValue: 3000 }),
|
|
447
|
+
host: c.string({ required: true, env: "HOST", defaultValue: "localhost" }),
|
|
448
|
+
})
|
|
449
|
+
.load({ env: true, args: false, files: "./config.yaml" });
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Strict mode for production
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
const config = c
|
|
456
|
+
.schema({
|
|
457
|
+
port: c.number({ required: true, env: "PORT" }),
|
|
458
|
+
dbUrl: c.string({ required: true, env: "DATABASE_URL" }),
|
|
459
|
+
})
|
|
460
|
+
.load({
|
|
461
|
+
env: true,
|
|
462
|
+
args: false,
|
|
463
|
+
files: "./config.yaml",
|
|
464
|
+
strict: true, // any type coercion or ambiguity throws an error
|
|
465
|
+
});
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Catch and inspect errors
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
import c, { ConfigLoadError } from "@meltstudio/config-loader";
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
const config = c
|
|
475
|
+
.schema({ port: c.number({ required: true }) })
|
|
476
|
+
.load({ env: false, args: false, files: "./config.yaml" });
|
|
477
|
+
} catch (err) {
|
|
478
|
+
if (err instanceof ConfigLoadError) {
|
|
479
|
+
for (const entry of err.errors) {
|
|
480
|
+
console.error(`[${entry.kind}] ${entry.path}: ${entry.message}`);
|
|
481
|
+
}
|
|
482
|
+
// err.warnings contains non-fatal issues
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Load from a directory of config files
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
const config = c
|
|
491
|
+
.schema({
|
|
492
|
+
port: c.number({ required: true }),
|
|
493
|
+
host: c.string({ required: true }),
|
|
494
|
+
})
|
|
495
|
+
.load({ env: false, args: false, dir: "./config.d/" });
|
|
496
|
+
// All YAML/JSON files in the directory are loaded and merged (sorted by filename)
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Access source metadata with loadExtended
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
const { data, warnings } = c
|
|
503
|
+
.schema({
|
|
504
|
+
port: c.number({ required: true, env: "PORT" }),
|
|
505
|
+
})
|
|
506
|
+
.loadExtended({ env: true, args: false, files: "./config.yaml" });
|
|
507
|
+
|
|
508
|
+
const portNode = data.port; // ConfigNode
|
|
509
|
+
console.log(portNode.value); // 3000
|
|
510
|
+
console.log(portNode.sourceType); // "env" | "file" | "default" | "args" | "envFile"
|
|
511
|
+
console.log(portNode.file); // "./config.yaml" or null
|
|
512
|
+
console.log(portNode.line); // source line number or null
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Combine .env files with process.env
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
const config = c
|
|
519
|
+
.schema({
|
|
520
|
+
apiKey: c.string({ required: true, env: "API_KEY" }),
|
|
521
|
+
debug: c.bool({ env: "DEBUG", defaultValue: false }),
|
|
522
|
+
})
|
|
523
|
+
.load({
|
|
524
|
+
env: true, // reads process.env
|
|
525
|
+
args: false,
|
|
526
|
+
envFile: ["./.env", "./.env.local"], // .env.local overrides .env
|
|
527
|
+
});
|
|
528
|
+
// Priority: process.env > .env.local > .env > defaults
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
## Common Mistakes
|
|
532
|
+
|
|
533
|
+
### Forgetting `item` in `c.object()`
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
// WRONG — fields are passed directly
|
|
537
|
+
c.object({ host: c.string(), port: c.number() });
|
|
538
|
+
|
|
539
|
+
// CORRECT — fields must be inside `item`
|
|
540
|
+
c.object({ item: { host: c.string(), port: c.number() } });
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Setting `env` on an option but not enabling env loading
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
// WRONG — env: "PORT" is set but env loading is disabled
|
|
547
|
+
c.schema({ port: c.number({ env: "PORT" }) }).load({
|
|
548
|
+
env: false,
|
|
549
|
+
args: false,
|
|
550
|
+
files: "./config.yaml",
|
|
551
|
+
});
|
|
552
|
+
// This emits a warning: "Options [port] have env mappings but env loading is disabled"
|
|
553
|
+
|
|
554
|
+
// CORRECT — set env: true in load options
|
|
555
|
+
c.schema({ port: c.number({ env: "PORT" }) }).load({
|
|
556
|
+
env: true,
|
|
557
|
+
args: false,
|
|
558
|
+
files: "./config.yaml",
|
|
559
|
+
});
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Expecting `.env` files to work without `envFile`
|
|
563
|
+
|
|
564
|
+
```typescript
|
|
565
|
+
// WRONG — .env files are not loaded by default
|
|
566
|
+
c.schema({ key: c.string({ env: "API_KEY" }) }).load({
|
|
567
|
+
env: true,
|
|
568
|
+
args: false,
|
|
569
|
+
});
|
|
570
|
+
// This only reads process.env, not .env files
|
|
571
|
+
|
|
572
|
+
// CORRECT — explicitly pass envFile
|
|
573
|
+
c.schema({ key: c.string({ env: "API_KEY" }) }).load({
|
|
574
|
+
env: true,
|
|
575
|
+
args: false,
|
|
576
|
+
envFile: "./.env",
|
|
577
|
+
});
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### Not catching `ConfigLoadError`
|
|
581
|
+
|
|
582
|
+
```typescript
|
|
583
|
+
// WRONG — unhandled error crashes the process with an unhelpful stack trace
|
|
584
|
+
const config = c
|
|
585
|
+
.schema({ port: c.number({ required: true }) })
|
|
586
|
+
.load({ env: false, args: false });
|
|
587
|
+
|
|
588
|
+
// CORRECT — catch and handle structured errors
|
|
589
|
+
try {
|
|
590
|
+
const config = c
|
|
591
|
+
.schema({ port: c.number({ required: true }) })
|
|
592
|
+
.load({ env: false, args: false });
|
|
593
|
+
} catch (err) {
|
|
594
|
+
if (err instanceof ConfigLoadError) {
|
|
595
|
+
console.error(err.errors); // structured error details
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### Using `required: true` on nested fields without the parent object
|
|
601
|
+
|
|
602
|
+
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.
|
|
603
|
+
|
|
604
|
+
## Documentation for AI Agents
|
|
605
|
+
|
|
606
|
+
This project provides machine-readable documentation for AI coding agents at the docs site:
|
|
607
|
+
|
|
608
|
+
- **[llms.txt](https://meltstudio.github.io/config-loader/llms.txt)** — structured index of documentation pages
|
|
609
|
+
- **[llms-full.txt](https://meltstudio.github.io/config-loader/llms-full.txt)** — full documentation in a single Markdown file
|
|
610
|
+
|
|
611
|
+
These files follow the [llms.txt standard](https://llmstxt.org/) and are generated automatically at build time. They are designed to be consumed by AI tools like Claude Code, Cursor, GitHub Copilot, and other LLM-based development assistants.
|
|
612
|
+
|
|
416
613
|
## License
|
|
417
614
|
|
|
418
615
|
This package is licensed under the Apache License 2.0. See the [LICENSE](./LICENSE) file for details.
|
package/dist/index.d.ts
CHANGED
|
@@ -14,19 +14,30 @@ interface EnvFileResult {
|
|
|
14
14
|
filePath: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** A single configuration validation error with optional location metadata. */
|
|
17
18
|
interface ConfigErrorEntry {
|
|
19
|
+
/** Human-readable error description. */
|
|
18
20
|
message: string;
|
|
21
|
+
/** Dot-separated path to the offending config key (e.g. `"db.port"`). */
|
|
19
22
|
path?: string;
|
|
23
|
+
/** The source where the error originated (e.g. file path, `"env"`, `"cli"`). */
|
|
20
24
|
source?: string;
|
|
21
|
-
|
|
25
|
+
/** Classification of the error. */
|
|
26
|
+
kind?: "required" | "type_conversion" | "invalid_path" | "invalid_state" | "file_validation" | "null_value" | "strict";
|
|
27
|
+
/** Line number in the config file where the error occurred, if applicable. */
|
|
22
28
|
line?: number;
|
|
29
|
+
/** Column number in the config file where the error occurred, if applicable. */
|
|
23
30
|
column?: number;
|
|
24
31
|
}
|
|
32
|
+
/** Thrown when configuration loading fails validation. Contains structured error entries and warnings. */
|
|
25
33
|
declare class ConfigLoadError extends Error {
|
|
34
|
+
/** All validation errors that caused the load to fail. */
|
|
26
35
|
readonly errors: ConfigErrorEntry[];
|
|
36
|
+
/** Non-fatal warnings collected during loading. */
|
|
27
37
|
readonly warnings: string[];
|
|
28
38
|
constructor(errors: ConfigErrorEntry[], warnings: string[]);
|
|
29
39
|
}
|
|
40
|
+
/** Thrown when a configuration file cannot be read or parsed. */
|
|
30
41
|
declare class ConfigFileError extends ConfigLoadError {
|
|
31
42
|
constructor(message: string);
|
|
32
43
|
}
|
|
@@ -93,21 +104,37 @@ declare class PrimitiveOption<T extends PrimitiveKind = PrimitiveKind> extends O
|
|
|
93
104
|
type NodeTree = {
|
|
94
105
|
[key: string]: NodeTree | ConfigNode;
|
|
95
106
|
};
|
|
107
|
+
/** Result returned by `SettingsBuilder.loadExtended()`, including raw node data and warnings. */
|
|
108
|
+
type ExtendedResult = {
|
|
109
|
+
/** Tree of `ConfigNode` objects preserving source metadata for each resolved value. */
|
|
110
|
+
data: NodeTree;
|
|
111
|
+
/** Non-fatal warnings collected during loading. */
|
|
112
|
+
warnings: string[];
|
|
113
|
+
};
|
|
96
114
|
type RecursivePartial<T> = {
|
|
97
115
|
[K in keyof T]?: RecursivePartial<T[K]>;
|
|
98
116
|
};
|
|
117
|
+
/** Configuration sources passed to `SettingsBuilder.load()` / `loadExtended()`. Priority: CLI > Env > Files > Defaults. */
|
|
99
118
|
type SettingsSources<T> = {
|
|
119
|
+
/** Whether to read values from `process.env`. */
|
|
100
120
|
env: boolean;
|
|
121
|
+
/** Whether to parse CLI arguments via Commander. */
|
|
101
122
|
args: boolean;
|
|
123
|
+
/** YAML/JSON file path(s) to load, or `false` to skip. */
|
|
102
124
|
files?: string | string[] | false;
|
|
125
|
+
/** Directory to scan for config files, or `false` to skip. */
|
|
103
126
|
dir?: string | false;
|
|
127
|
+
/** `.env` file path(s) to load, or `false` to skip. */
|
|
104
128
|
envFile?: string | string[] | false;
|
|
129
|
+
/** Partial default values applied at the lowest priority. */
|
|
105
130
|
defaults?: RecursivePartial<T>;
|
|
106
|
-
|
|
131
|
+
/** When `true`, unknown keys in config files cause errors. */
|
|
132
|
+
strict?: boolean;
|
|
107
133
|
};
|
|
108
134
|
type OptionKind = "boolean" | "string" | "number" | "array" | "object";
|
|
109
135
|
type PrimitiveKind = Extract<OptionKind, "boolean" | "string" | "number">;
|
|
110
136
|
type TypeOfPrimitiveKind<T extends PrimitiveKind> = T extends "boolean" ? boolean : T extends "string" ? string : T extends "number" ? number : never;
|
|
137
|
+
/** Recursively infers the plain TypeScript type from a schema definition. Maps option nodes to their resolved value types. */
|
|
111
138
|
type SchemaValue<T extends OptionBase | Node> = T extends OptionBase ? T extends ArrayOption<OptionTypes> ? SchemaValue<T["item"]>[] : T extends ObjectOption<infer R> ? {
|
|
112
139
|
[K in keyof R]: SchemaValue<R[K]>;
|
|
113
140
|
} : T extends PrimitiveOption<infer R> ? TypeOfPrimitiveKind<R> : never : T extends Node ? {
|
|
@@ -146,29 +173,68 @@ declare class ObjectOption<T extends Node = Node> extends OptionBase<"object"> {
|
|
|
146
173
|
|
|
147
174
|
type OptionTypes = PrimitiveOption | ArrayOption<OptionTypes> | ObjectOption<Node>;
|
|
148
175
|
|
|
176
|
+
/** Fluent builder that takes a schema and resolves configuration from multiple sources. */
|
|
149
177
|
declare class SettingsBuilder<T extends Node> {
|
|
150
178
|
private readonly schema;
|
|
151
179
|
constructor(schema: T);
|
|
180
|
+
/**
|
|
181
|
+
* Loads and validates configuration, returning a fully-typed plain object.
|
|
182
|
+
* @param sources - Which sources to read (env, args, files, etc.).
|
|
183
|
+
* @returns The resolved configuration object matching the schema type.
|
|
184
|
+
* @throws {ConfigLoadError} If validation fails (missing required fields, type errors, etc.).
|
|
185
|
+
*/
|
|
152
186
|
load(sources: SettingsSources<SchemaValue<T>>): SchemaValue<T>;
|
|
153
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Loads configuration and returns raw node data with source metadata alongside warnings.
|
|
189
|
+
* @param sources - Which sources to read (env, args, files, etc.).
|
|
190
|
+
* @returns An `ExtendedResult` containing the node tree and any warnings.
|
|
191
|
+
* @throws {ConfigLoadError} If validation fails.
|
|
192
|
+
*/
|
|
193
|
+
loadExtended(sources: SettingsSources<SchemaValue<T>>): ExtendedResult;
|
|
154
194
|
}
|
|
155
195
|
|
|
196
|
+
/** Options for configuring a primitive (`string`, `number`, `bool`) schema field. */
|
|
156
197
|
interface OptionPropsArgs<T> {
|
|
198
|
+
/** Whether the field must be present in at least one source. */
|
|
157
199
|
required?: boolean;
|
|
200
|
+
/** Environment variable name to read from, or `null` to disable. */
|
|
158
201
|
env?: string | null;
|
|
202
|
+
/** Whether to expose this field as a CLI argument via Commander. */
|
|
159
203
|
cli?: boolean;
|
|
204
|
+
/** Static default value or factory function returning one. */
|
|
160
205
|
defaultValue?: T | (() => T);
|
|
206
|
+
/** Help text shown in CLI `--help` output. */
|
|
161
207
|
help?: string;
|
|
162
208
|
}
|
|
209
|
+
/** Options for configuring an `array` schema field. */
|
|
163
210
|
interface ArrayOptionPropsArgs<T extends OptionTypes> {
|
|
211
|
+
/** Whether the field must be present in at least one source. */
|
|
164
212
|
required?: boolean;
|
|
213
|
+
/** Schema definition for each item in the array. */
|
|
165
214
|
item: T;
|
|
215
|
+
/** Static default value or factory function returning one. */
|
|
166
216
|
defaultValue?: SchemaValue<T>[] | (() => SchemaValue<T>[]);
|
|
167
217
|
}
|
|
218
|
+
/** Options for configuring a nested `object` schema field. */
|
|
168
219
|
interface ObjectOptionPropsArgs<T extends Node> {
|
|
220
|
+
/** Whether the field must be present in at least one source. */
|
|
169
221
|
required?: boolean;
|
|
222
|
+
/** Schema definition for the nested object's shape. */
|
|
170
223
|
item: T;
|
|
171
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Config-loader entry point. Provides factory functions to define a typed configuration schema.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```ts
|
|
230
|
+
* import c from "@meltstudio/config-loader";
|
|
231
|
+
*
|
|
232
|
+
* const config = c.schema({
|
|
233
|
+
* port: c.number({ env: "PORT", defaultValue: 3000 }),
|
|
234
|
+
* host: c.string({ env: "HOST" }),
|
|
235
|
+
* }).load({ env: true, args: false });
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
172
238
|
declare const option: {
|
|
173
239
|
string: (opts?: OptionPropsArgs<string>) => PrimitiveOption<"string">;
|
|
174
240
|
number: (opts?: OptionPropsArgs<number>) => PrimitiveOption<"number">;
|
|
@@ -178,4 +244,4 @@ declare const option: {
|
|
|
178
244
|
schema: <T extends Node>(theSchema: T) => SettingsBuilder<T>;
|
|
179
245
|
};
|
|
180
246
|
|
|
181
|
-
export { type ConfigErrorEntry, ConfigFileError, ConfigLoadError, option as default };
|
|
247
|
+
export { type ConfigErrorEntry, ConfigFileError, ConfigLoadError, type ExtendedResult, option as default };
|
package/dist/index.js
CHANGED
|
@@ -41,7 +41,9 @@ var import_commander = require("commander");
|
|
|
41
41
|
|
|
42
42
|
// src/errors.ts
|
|
43
43
|
var ConfigLoadError = class extends Error {
|
|
44
|
+
/** All validation errors that caused the load to fail. */
|
|
44
45
|
errors;
|
|
46
|
+
/** Non-fatal warnings collected during loading. */
|
|
45
47
|
warnings;
|
|
46
48
|
constructor(errors, warnings) {
|
|
47
49
|
const message = `Configuration loading failed with ${errors.length} error${errors.length === 1 ? "" : "s"}`;
|
|
@@ -286,7 +288,7 @@ var OptionBase = class {
|
|
|
286
288
|
if (this.params.env && env) {
|
|
287
289
|
if (this.params.env in env) {
|
|
288
290
|
const val = env[this.params.env];
|
|
289
|
-
if (val) {
|
|
291
|
+
if (val !== void 0) {
|
|
290
292
|
const envFileSource = findEnvFileSource(
|
|
291
293
|
this.params.env,
|
|
292
294
|
val,
|
|
@@ -448,6 +450,12 @@ var OptionBase = class {
|
|
|
448
450
|
);
|
|
449
451
|
return val.toString();
|
|
450
452
|
}
|
|
453
|
+
if (typeof val === "boolean") {
|
|
454
|
+
errors?.warnings.push(
|
|
455
|
+
`The option ${ident} is stated as a string but is provided as a boolean`
|
|
456
|
+
);
|
|
457
|
+
return val.toString();
|
|
458
|
+
}
|
|
451
459
|
errors?.errors.push({
|
|
452
460
|
message: `Cannot convert value '${valueToString(
|
|
453
461
|
val
|
|
@@ -460,10 +468,11 @@ var OptionBase = class {
|
|
|
460
468
|
}
|
|
461
469
|
if (this.params.kind === "boolean") {
|
|
462
470
|
if (typeof val !== "boolean" && typeof val !== "object") {
|
|
463
|
-
|
|
471
|
+
const normalized = typeof val === "string" ? val.toLowerCase() : val;
|
|
472
|
+
if ([1, "1", "true", "yes"].includes(normalized)) {
|
|
464
473
|
return true;
|
|
465
474
|
}
|
|
466
|
-
if ([0, "0", "false"].includes(
|
|
475
|
+
if ([0, "0", "false", "no"].includes(normalized)) {
|
|
467
476
|
return false;
|
|
468
477
|
}
|
|
469
478
|
}
|
|
@@ -521,13 +530,33 @@ var OptionBase = class {
|
|
|
521
530
|
});
|
|
522
531
|
return new InvalidValue();
|
|
523
532
|
}
|
|
524
|
-
if (val
|
|
533
|
+
if (val === void 0) {
|
|
534
|
+
return new InvalidValue();
|
|
535
|
+
}
|
|
536
|
+
if (val === null) {
|
|
537
|
+
errors?.errors.push({
|
|
538
|
+
message: `Option '${path2.join(".")}' is null \u2014 expected an object to traverse into`,
|
|
539
|
+
kind: "null_value"
|
|
540
|
+
});
|
|
525
541
|
return new InvalidValue();
|
|
526
542
|
}
|
|
527
543
|
return this.findInObject(val, rest, errors);
|
|
528
544
|
}
|
|
529
545
|
if (path2.length === 1) {
|
|
530
546
|
const val = obj[path2[0]];
|
|
547
|
+
if (val === null) {
|
|
548
|
+
if (this.params.required) {
|
|
549
|
+
errors?.errors.push({
|
|
550
|
+
message: `Option '${path2.join(".")}' is null \u2014 expected a ${this.params.kind}`,
|
|
551
|
+
kind: "null_value"
|
|
552
|
+
});
|
|
553
|
+
} else {
|
|
554
|
+
errors?.warnings.push(
|
|
555
|
+
`Option '${path2.join(".")}' is null and will be treated as unset`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
return new InvalidValue();
|
|
559
|
+
}
|
|
531
560
|
if (!Array.isArray(val) && typeof val === "object" && val || typeof val === "string" || typeof val === "number" || typeof val === "boolean" || typeof val === "undefined") {
|
|
532
561
|
return val;
|
|
533
562
|
}
|
|
@@ -754,8 +783,23 @@ var Settings = class {
|
|
|
754
783
|
this.envFileResults = envFileResults;
|
|
755
784
|
this.envData = mergedEnvData;
|
|
756
785
|
}
|
|
786
|
+
checkEnvMappingsWithoutEnvLoading() {
|
|
787
|
+
if (this.sources.env || this.sources.envFile) return;
|
|
788
|
+
const envMappedOptions = [];
|
|
789
|
+
this.traverseOptions(this.schema, [], (node, path2) => {
|
|
790
|
+
if (node.params.env) {
|
|
791
|
+
envMappedOptions.push(path2.join("."));
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
if (envMappedOptions.length > 0) {
|
|
795
|
+
this.errors.warnings.push(
|
|
796
|
+
`Options [${envMappedOptions.join(", ")}] have env mappings but env loading is disabled. Set 'env: true' in load options to read from environment variables.`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
757
800
|
load() {
|
|
758
801
|
this.validateAndLoadFiles();
|
|
802
|
+
this.checkEnvMappingsWithoutEnvLoading();
|
|
759
803
|
if (this.sources.env) {
|
|
760
804
|
this.envData = { ...process.env };
|
|
761
805
|
}
|
|
@@ -780,18 +824,13 @@ var Settings = class {
|
|
|
780
824
|
errors: this.errors
|
|
781
825
|
})
|
|
782
826
|
);
|
|
783
|
-
if (this.errors.warnings.length > 0) {
|
|
784
|
-
for (
|
|
785
|
-
|
|
827
|
+
if (this.sources.strict && this.errors.warnings.length > 0) {
|
|
828
|
+
for (const warning of this.errors.warnings) {
|
|
829
|
+
this.errors.errors.push({ message: warning, kind: "strict" });
|
|
786
830
|
}
|
|
831
|
+
this.errors.warnings = [];
|
|
787
832
|
}
|
|
788
833
|
if (this.errors.errors.length > 0) {
|
|
789
|
-
if (this.sources.exitOnError) {
|
|
790
|
-
for (let index = 0; index < this.errors.errors.length; index += 1) {
|
|
791
|
-
console.error(`[Error]: ${this.errors.errors[index].message}`);
|
|
792
|
-
}
|
|
793
|
-
process.exit(1);
|
|
794
|
-
}
|
|
795
834
|
throw new ConfigLoadError(
|
|
796
835
|
[...this.errors.errors],
|
|
797
836
|
[...this.errors.warnings]
|
|
@@ -922,8 +961,7 @@ var Settings = class {
|
|
|
922
961
|
return node.value;
|
|
923
962
|
}
|
|
924
963
|
return Object.entries(node).reduce(
|
|
925
|
-
(acc,
|
|
926
|
-
const [key, value] = item;
|
|
964
|
+
(acc, [key, value]) => {
|
|
927
965
|
acc[key] = this.getValuesFromTree(value);
|
|
928
966
|
return acc;
|
|
929
967
|
},
|
|
@@ -938,6 +976,9 @@ var Settings = class {
|
|
|
938
976
|
getExtended() {
|
|
939
977
|
return this.optionsTree;
|
|
940
978
|
}
|
|
979
|
+
getWarnings() {
|
|
980
|
+
return [...this.errors.warnings];
|
|
981
|
+
}
|
|
941
982
|
};
|
|
942
983
|
var settings_default = Settings;
|
|
943
984
|
|
|
@@ -947,13 +988,28 @@ var SettingsBuilder = class {
|
|
|
947
988
|
constructor(schema2) {
|
|
948
989
|
this.schema = schema2;
|
|
949
990
|
}
|
|
991
|
+
/**
|
|
992
|
+
* Loads and validates configuration, returning a fully-typed plain object.
|
|
993
|
+
* @param sources - Which sources to read (env, args, files, etc.).
|
|
994
|
+
* @returns The resolved configuration object matching the schema type.
|
|
995
|
+
* @throws {ConfigLoadError} If validation fails (missing required fields, type errors, etc.).
|
|
996
|
+
*/
|
|
950
997
|
load(sources) {
|
|
951
998
|
const settings = new settings_default(this.schema, sources);
|
|
952
999
|
return settings.get();
|
|
953
1000
|
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Loads configuration and returns raw node data with source metadata alongside warnings.
|
|
1003
|
+
* @param sources - Which sources to read (env, args, files, etc.).
|
|
1004
|
+
* @returns An `ExtendedResult` containing the node tree and any warnings.
|
|
1005
|
+
* @throws {ConfigLoadError} If validation fails.
|
|
1006
|
+
*/
|
|
954
1007
|
loadExtended(sources) {
|
|
955
1008
|
const settings = new settings_default(this.schema, sources);
|
|
956
|
-
return
|
|
1009
|
+
return {
|
|
1010
|
+
data: settings.getExtended(),
|
|
1011
|
+
warnings: settings.getWarnings()
|
|
1012
|
+
};
|
|
957
1013
|
}
|
|
958
1014
|
};
|
|
959
1015
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meltstudio/config-loader",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Melt Studio's tool for loading configurations into a Node.js application.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"replace-tspaths": "./scripts/replace-tspaths/index.mjs",
|
|
31
31
|
"example:run": "ts-node -r tsconfig-paths/register ./example/index.ts",
|
|
32
32
|
"prepare": "husky",
|
|
33
|
-
"docs:build": "docusaurus build",
|
|
33
|
+
"docs:build": "docusaurus build && node scripts/fix-llms-urls.mjs",
|
|
34
34
|
"docs:serve": "docusaurus serve",
|
|
35
35
|
"docs:start": "docusaurus start"
|
|
36
36
|
},
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"@types/jest": "^30.0.0",
|
|
54
54
|
"@types/js-yaml": "^4.0.5",
|
|
55
55
|
"@types/node": "^22.0.0",
|
|
56
|
+
"docusaurus-plugin-llms": "^0.3.0",
|
|
56
57
|
"eslint": "^10.0.0",
|
|
57
58
|
"eslint-config-prettier": "^10.0.0",
|
|
58
59
|
"eslint-plugin-jest": "^29.0.0",
|