@meltstudio/config-loader 1.1.0 → 2.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 +306 -169
- package/dist/index.d.ts +138 -68
- package/dist/index.js +570 -259
- package/package.json +39 -28
package/README.md
CHANGED
|
@@ -1,101 +1,88 @@
|
|
|
1
1
|
# @meltstudio/config-loader
|
|
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
|
-
##
|
|
5
|
+
## Why config-loader?
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
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:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
```typescript
|
|
10
|
+
import c from "@meltstudio/config-loader";
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const config = c
|
|
13
|
+
.schema({
|
|
14
|
+
port: c.number({ required: true, env: "PORT" }),
|
|
15
|
+
database: c.object({
|
|
16
|
+
item: {
|
|
17
|
+
host: c.string({ required: true }),
|
|
18
|
+
credentials: c.object({
|
|
19
|
+
item: {
|
|
20
|
+
username: c.string(),
|
|
21
|
+
password: c.string({ env: "DB_PASSWORD" }),
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
features: c.array({
|
|
27
|
+
required: true,
|
|
28
|
+
item: c.object({
|
|
29
|
+
item: {
|
|
30
|
+
name: c.string(),
|
|
31
|
+
enabled: c.bool(),
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
}),
|
|
35
|
+
})
|
|
36
|
+
.load({
|
|
37
|
+
env: true,
|
|
38
|
+
args: true,
|
|
39
|
+
files: "./config.yaml",
|
|
40
|
+
});
|
|
15
41
|
|
|
16
|
-
|
|
42
|
+
// config is fully typed:
|
|
43
|
+
// {
|
|
44
|
+
// port: number;
|
|
45
|
+
// database: { host: string; credentials: { username: string; password: string } };
|
|
46
|
+
// features: { name: string; enabled: boolean }[];
|
|
47
|
+
// }
|
|
48
|
+
```
|
|
17
49
|
|
|
18
|
-
|
|
19
|
-
- [Usage](#usage)
|
|
20
|
-
- [License](#license)
|
|
21
|
-
- [Acknowledgements](#acknowledgements)
|
|
50
|
+
No separate interface to maintain. No `as` casts. The types flow from the schema.
|
|
22
51
|
|
|
23
|
-
##
|
|
52
|
+
## Features
|
|
24
53
|
|
|
25
|
-
|
|
54
|
+
- **Full type inference** — schema definition produces typed output automatically
|
|
55
|
+
- **Multiple sources** — YAML files, JSON files, `.env` files, environment variables, CLI arguments
|
|
56
|
+
- **Priority resolution** — CLI > process.env > `.env` files > Config files > Defaults
|
|
57
|
+
- **`.env` file support** — load environment variables from `.env` files with automatic line tracking
|
|
58
|
+
- **Nested objects and arrays** — deeply nested configs with full type safety
|
|
59
|
+
- **Structured errors** — typed `ConfigLoadError` with per-field error details instead of `process.exit(1)`
|
|
60
|
+
- **Default values** — static or computed (via functions)
|
|
61
|
+
- **Multiple files / directory loading** — load from a list of files or an entire directory
|
|
26
62
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- Node.js >= 20
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
30
68
|
|
|
31
69
|
```bash
|
|
32
|
-
|
|
70
|
+
npm install @meltstudio/config-loader
|
|
33
71
|
```
|
|
34
72
|
|
|
35
73
|
```bash
|
|
36
|
-
|
|
74
|
+
yarn add @meltstudio/config-loader
|
|
37
75
|
```
|
|
38
76
|
|
|
39
|
-
##
|
|
40
|
-
|
|
41
|
-
Here's an example of how to use the `@meltstudio/config-loader` package in a TypeScript project:
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
import path from "path";
|
|
77
|
+
## Quick Start
|
|
45
78
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const run = (): void => {
|
|
49
|
-
const settings = c.schema({
|
|
50
|
-
version: c.string({ required: true, cli: true }),
|
|
51
|
-
website: {
|
|
52
|
-
title: c.string({ required: true }),
|
|
53
|
-
url: c.string({
|
|
54
|
-
required: false,
|
|
55
|
-
defaultValue: "www.mywebsite.dev",
|
|
56
|
-
}),
|
|
57
|
-
description: c.string({ required: true }),
|
|
58
|
-
isProduction: c.bool({ required: true }),
|
|
59
|
-
},
|
|
60
|
-
database: {
|
|
61
|
-
host: c.string({ required: true }),
|
|
62
|
-
port: c.number({ required: true }),
|
|
63
|
-
credentials: {
|
|
64
|
-
username: c.string(),
|
|
65
|
-
password: c.string(),
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
socialMedia: c.array({
|
|
69
|
-
required: true,
|
|
70
|
-
item: c.string({ required: true }),
|
|
71
|
-
}),
|
|
72
|
-
features: c.array({
|
|
73
|
-
required: true,
|
|
74
|
-
item: {
|
|
75
|
-
name: c.string(),
|
|
76
|
-
enabled: c.bool(),
|
|
77
|
-
},
|
|
78
|
-
}),
|
|
79
|
-
});
|
|
80
|
-
const config = settings.load({
|
|
81
|
-
env: false,
|
|
82
|
-
args: true,
|
|
83
|
-
files: path.join(__dirname, "./config.yaml"),
|
|
84
|
-
});
|
|
85
|
-
console.log(JSON.stringify(config, null, 2));
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
run();
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
With a config.yaml file with the following contents:
|
|
79
|
+
**config.yaml:**
|
|
92
80
|
|
|
93
81
|
```yaml
|
|
94
82
|
version: 1.0.0
|
|
95
83
|
website:
|
|
96
84
|
title: My Website
|
|
97
85
|
description: A simple and elegant website
|
|
98
|
-
port: 3000
|
|
99
86
|
isProduction: false
|
|
100
87
|
|
|
101
88
|
database:
|
|
@@ -112,13 +99,64 @@ features:
|
|
|
112
99
|
enabled: true
|
|
113
100
|
- name: Admin
|
|
114
101
|
enabled: false
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**index.ts:**
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import path from "path";
|
|
108
|
+
import c from "@meltstudio/config-loader";
|
|
115
109
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
110
|
+
const config = c
|
|
111
|
+
.schema({
|
|
112
|
+
version: c.string({ required: true, cli: true }),
|
|
113
|
+
website: c.object({
|
|
114
|
+
item: {
|
|
115
|
+
title: c.string({ required: true }),
|
|
116
|
+
url: c.string({
|
|
117
|
+
required: false,
|
|
118
|
+
defaultValue: "www.mywebsite.dev",
|
|
119
|
+
}),
|
|
120
|
+
description: c.string({ required: true }),
|
|
121
|
+
isProduction: c.bool({ required: true }),
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
database: c.object({
|
|
125
|
+
item: {
|
|
126
|
+
host: c.string({ required: true }),
|
|
127
|
+
port: c.number({ required: true }),
|
|
128
|
+
credentials: c.object({
|
|
129
|
+
item: {
|
|
130
|
+
username: c.string(),
|
|
131
|
+
password: c.string(),
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
socialMedia: c.array({
|
|
137
|
+
required: true,
|
|
138
|
+
item: c.string({ required: true }),
|
|
139
|
+
}),
|
|
140
|
+
features: c.array({
|
|
141
|
+
required: true,
|
|
142
|
+
item: c.object({
|
|
143
|
+
item: {
|
|
144
|
+
name: c.string(),
|
|
145
|
+
enabled: c.bool(),
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
}),
|
|
149
|
+
})
|
|
150
|
+
.load({
|
|
151
|
+
env: false,
|
|
152
|
+
args: true,
|
|
153
|
+
files: path.join(__dirname, "./config.yaml"),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
console.log(JSON.stringify(config, null, 2));
|
|
119
157
|
```
|
|
120
158
|
|
|
121
|
-
|
|
159
|
+
Output:
|
|
122
160
|
|
|
123
161
|
```json
|
|
124
162
|
{
|
|
@@ -142,140 +180,239 @@ The expected output would be:
|
|
|
142
180
|
"https://instagram.com/example"
|
|
143
181
|
],
|
|
144
182
|
"features": [
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
"enabled": true
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
"name": "Admin",
|
|
151
|
-
"enabled": false
|
|
152
|
-
}
|
|
183
|
+
{ "name": "Store", "enabled": true },
|
|
184
|
+
{ "name": "Admin", "enabled": false }
|
|
153
185
|
]
|
|
154
186
|
}
|
|
155
187
|
```
|
|
156
188
|
|
|
157
|
-
|
|
189
|
+
## Schema API
|
|
158
190
|
|
|
159
|
-
|
|
160
|
-
|
|
191
|
+
### Primitives
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
c.string({
|
|
195
|
+
required: true,
|
|
196
|
+
env: "MY_VAR",
|
|
197
|
+
cli: true,
|
|
198
|
+
defaultValue: "fallback",
|
|
199
|
+
});
|
|
200
|
+
c.number({ required: true, env: "PORT" });
|
|
201
|
+
c.bool({ env: "DEBUG", defaultValue: false });
|
|
161
202
|
```
|
|
162
203
|
|
|
163
|
-
###
|
|
204
|
+
### Objects
|
|
164
205
|
|
|
165
|
-
|
|
166
|
-
This will allow values to be sent when running the package from the command line.
|
|
206
|
+
Use `c.object()` to declare nested object schemas:
|
|
167
207
|
|
|
168
208
|
```typescript
|
|
169
|
-
|
|
209
|
+
c.object({
|
|
210
|
+
item: {
|
|
211
|
+
host: c.string(),
|
|
212
|
+
port: c.number(),
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
```
|
|
170
216
|
|
|
171
|
-
|
|
217
|
+
Objects can be nested arbitrarily deep:
|
|
172
218
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
219
|
+
```typescript
|
|
220
|
+
c.schema({
|
|
221
|
+
database: c.object({
|
|
222
|
+
item: {
|
|
223
|
+
host: c.string(),
|
|
224
|
+
port: c.number(),
|
|
225
|
+
credentials: c.object({
|
|
226
|
+
item: {
|
|
227
|
+
username: c.string(),
|
|
228
|
+
password: c.string({ env: "DB_PASSWORD" }),
|
|
229
|
+
},
|
|
230
|
+
}),
|
|
231
|
+
},
|
|
232
|
+
}),
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`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.
|
|
237
|
+
|
|
238
|
+
### Arrays
|
|
187
239
|
|
|
188
|
-
|
|
240
|
+
```typescript
|
|
241
|
+
c.array({ required: true, item: c.string() }); // string[]
|
|
242
|
+
c.array({ required: true, item: c.number() }); // number[]
|
|
243
|
+
c.array({
|
|
244
|
+
item: c.object({
|
|
245
|
+
item: { name: c.string(), age: c.number() },
|
|
246
|
+
}),
|
|
247
|
+
}); // { name: string; age: number }[]
|
|
189
248
|
```
|
|
190
249
|
|
|
191
|
-
|
|
250
|
+
## Loading Sources
|
|
192
251
|
|
|
193
|
-
```
|
|
194
|
-
|
|
252
|
+
```typescript
|
|
253
|
+
.load({
|
|
254
|
+
env: true, // Read from process.env
|
|
255
|
+
args: true, // Read from CLI arguments (--database.port 3000)
|
|
256
|
+
files: "./config.yaml", // Single YAML file
|
|
257
|
+
files: "./config.json", // Single JSON file
|
|
258
|
+
files: ["./base.yaml", "./overrides.json"], // Mix YAML and JSON (first takes priority)
|
|
259
|
+
dir: "./config.d/", // All files in a directory (sorted)
|
|
260
|
+
envFile: "./.env", // Single .env file
|
|
261
|
+
envFile: ["./.env", "./.env.local"], // Multiple .env files (later overrides earlier)
|
|
262
|
+
defaults: { port: 3000 }, // Programmatic defaults
|
|
263
|
+
})
|
|
195
264
|
```
|
|
196
265
|
|
|
197
|
-
|
|
266
|
+
Both YAML (`.yaml`, `.yml`) and JSON (`.json`) files are supported. The format is detected automatically from the file extension.
|
|
198
267
|
|
|
199
|
-
|
|
200
|
-
|
|
268
|
+
**Priority order:** CLI arguments > `process.env` > `.env` files > Config files > Defaults
|
|
269
|
+
|
|
270
|
+
## Extended Loading (Source Metadata)
|
|
271
|
+
|
|
272
|
+
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.
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import c from "@meltstudio/config-loader";
|
|
276
|
+
|
|
277
|
+
const extended = c
|
|
278
|
+
.schema({
|
|
279
|
+
port: c.number({ required: true, env: "PORT" }),
|
|
280
|
+
host: c.string({ defaultValue: "localhost" }),
|
|
281
|
+
})
|
|
282
|
+
.loadExtended({
|
|
283
|
+
env: true,
|
|
284
|
+
args: false,
|
|
285
|
+
files: "./config.yaml",
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Each leaf is a ConfigNode with:
|
|
289
|
+
// {
|
|
290
|
+
// value: 3000,
|
|
291
|
+
// path: "port",
|
|
292
|
+
// sourceType: "env" | "envFile" | "file" | "args" | "default",
|
|
293
|
+
// file: "./config.yaml" | "./.env" | null,
|
|
294
|
+
// variableName: "PORT" | null,
|
|
295
|
+
// argName: null,
|
|
296
|
+
// line: 5 | null, // source line (1-based) for YAML, JSON, and .env files; null for env/args/default
|
|
297
|
+
// column: 3 | null // source column (1-based) for YAML, JSON, and .env files; null for env/args/default
|
|
298
|
+
// }
|
|
299
|
+
console.log(extended.port.value); // 3000
|
|
300
|
+
console.log(extended.port.sourceType); // "env"
|
|
301
|
+
console.log(extended.port.variableName); // "PORT"
|
|
201
302
|
```
|
|
202
303
|
|
|
203
|
-
|
|
304
|
+
This is useful for debugging configuration resolution, building admin UIs that show where each setting originated, or auditing which sources are active.
|
|
204
305
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
306
|
+
## Error Handling
|
|
307
|
+
|
|
308
|
+
When validation fails, config-loader throws a `ConfigLoadError` with structured error details:
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import c, { ConfigLoadError } from "@meltstudio/config-loader";
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const config = c.schema({ port: c.number({ required: true }) }).load({
|
|
315
|
+
env: false,
|
|
316
|
+
args: false,
|
|
317
|
+
files: "./config.yaml",
|
|
318
|
+
});
|
|
319
|
+
} catch (err) {
|
|
320
|
+
if (err instanceof ConfigLoadError) {
|
|
321
|
+
for (const entry of err.errors) {
|
|
322
|
+
console.error(`[${entry.kind}] ${entry.message}`);
|
|
323
|
+
// e.g. [required] Required option 'port' not provided.
|
|
324
|
+
}
|
|
325
|
+
}
|
|
208
326
|
}
|
|
209
327
|
```
|
|
210
328
|
|
|
211
|
-
|
|
329
|
+
For CLI tools that prefer the old exit-on-error behavior:
|
|
212
330
|
|
|
213
|
-
|
|
331
|
+
```typescript
|
|
332
|
+
.load({ env: true, args: true, files: "./config.yaml", exitOnError: true })
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## CLI Arguments
|
|
214
336
|
|
|
215
|
-
|
|
337
|
+
Set `cli: true` on an option to allow overriding via command line:
|
|
216
338
|
|
|
217
339
|
```typescript
|
|
218
|
-
|
|
340
|
+
c.schema({
|
|
341
|
+
version: c.string({ required: true, cli: true }),
|
|
342
|
+
});
|
|
343
|
+
```
|
|
219
344
|
|
|
220
|
-
|
|
345
|
+
```bash
|
|
346
|
+
node app.js --version 2.0.0
|
|
347
|
+
```
|
|
221
348
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
host: c.string({ required: true }),
|
|
226
|
-
port: c.number({ required: true }),
|
|
227
|
-
credentials: {
|
|
228
|
-
username: c.string(),
|
|
229
|
-
password: c.string({
|
|
230
|
-
env: "DB_PASSWORD",
|
|
231
|
-
cli: true,
|
|
232
|
-
}),
|
|
233
|
-
},
|
|
234
|
-
},
|
|
235
|
-
});
|
|
236
|
-
const config = settings.load({
|
|
237
|
-
env: true, 👈
|
|
238
|
-
args: true,
|
|
239
|
-
files: path.join(__dirname, "./config.yaml"),
|
|
240
|
-
});
|
|
241
|
-
console.log(JSON.stringify(config, null, 2));
|
|
242
|
-
};
|
|
349
|
+
## Environment Variables
|
|
350
|
+
|
|
351
|
+
Set `env: "VAR_NAME"` on an option and `env: true` in the load options:
|
|
243
352
|
|
|
244
|
-
|
|
353
|
+
```typescript
|
|
354
|
+
c.schema({
|
|
355
|
+
database: c.object({
|
|
356
|
+
item: {
|
|
357
|
+
password: c.string({ env: "DB_PASSWORD" }),
|
|
358
|
+
},
|
|
359
|
+
}),
|
|
360
|
+
}).load({ env: true, args: false, files: "./config.yaml" });
|
|
245
361
|
```
|
|
246
362
|
|
|
247
|
-
|
|
363
|
+
## `.env` File Support
|
|
248
364
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
port: 5432
|
|
253
|
-
credentials:
|
|
254
|
-
username: admin
|
|
255
|
-
password: IGNORED_PASSWORD
|
|
256
|
-
```
|
|
365
|
+
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.
|
|
366
|
+
|
|
367
|
+
**.env:**
|
|
257
368
|
|
|
258
369
|
```bash
|
|
259
|
-
|
|
370
|
+
DB_HOST=localhost
|
|
371
|
+
DB_PORT=5432
|
|
372
|
+
DB_PASSWORD="s3cret"
|
|
373
|
+
APP_NAME='My App'
|
|
374
|
+
# This is a comment
|
|
260
375
|
```
|
|
261
376
|
|
|
262
|
-
|
|
377
|
+
**Usage:**
|
|
263
378
|
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
"
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
379
|
+
```typescript
|
|
380
|
+
const config = c
|
|
381
|
+
.schema({
|
|
382
|
+
host: c.string({ env: "DB_HOST" }),
|
|
383
|
+
port: c.number({ env: "DB_PORT" }),
|
|
384
|
+
password: c.string({ env: "DB_PASSWORD" }),
|
|
385
|
+
})
|
|
386
|
+
.load({
|
|
387
|
+
env: true,
|
|
388
|
+
args: false,
|
|
389
|
+
envFile: "./.env",
|
|
390
|
+
});
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
`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.
|
|
394
|
+
|
|
395
|
+
**Multiple `.env` files:**
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
.load({
|
|
399
|
+
env: true,
|
|
400
|
+
args: false,
|
|
401
|
+
envFile: ["./.env", "./.env.local"], // .env.local overrides .env
|
|
402
|
+
})
|
|
275
403
|
```
|
|
276
404
|
|
|
277
|
-
|
|
405
|
+
When using multiple files, later files override earlier ones for the same key.
|
|
406
|
+
|
|
407
|
+
The `.env` parser supports:
|
|
408
|
+
|
|
409
|
+
- `KEY=VALUE` pairs (whitespace trimmed)
|
|
410
|
+
- Comments (lines starting with `#`)
|
|
411
|
+
- Quoted values (double `"..."` or single `'...'` quotes stripped)
|
|
412
|
+
- Empty values (`KEY=`)
|
|
413
|
+
|
|
414
|
+
When using `loadExtended()`, values from `.env` files have `sourceType: "envFile"` with `file`, `line`, and `column` metadata pointing to the `.env` file location.
|
|
278
415
|
|
|
279
416
|
## License
|
|
280
417
|
|
|
281
|
-
This package is licensed under the Apache License 2.0.
|
|
418
|
+
This package is licensed under the Apache License 2.0. See the [LICENSE](./LICENSE) file for details.
|