@meltstudio/config-loader 1.1.0 → 2.0.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 (4) hide show
  1. package/README.md +306 -169
  2. package/dist/index.d.ts +138 -68
  3. package/dist/index.js +570 -259
  4. package/package.json +39 -28
package/README.md CHANGED
@@ -1,101 +1,88 @@
1
1
  # @meltstudio/config-loader
2
2
 
3
- > ⚠️ **WARNING**: This project is in beta, so some features may change in the future. Use at your own discretion
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
- ## Project Description
5
+ ## Why config-loader?
6
6
 
7
- The Config Loader package is a powerful and user-friendly tool that simplifies the process of retrieving and collecting variables from one or multiple files for your project. It provides an efficient way to extract specific information from files and access those variables in your code. The result is a JSON object, making it easy to work with in various applications.
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
- ## Features
9
+ ```typescript
10
+ import c from "@meltstudio/config-loader";
10
11
 
11
- - Retrieve and collect variables from one or multiple files in your project.
12
- - YAML file support (support for other file types coming soon.)
13
- - Data can also be retrieved from CLI or environment variables .
14
- - Compatible with TypeScript/JavaScript environments, making it suitable for Node.js projects.
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
- ## Table of Contents
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
- - [Installation](#installation)
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
- ## Installation
52
+ ## Features
24
53
 
25
- To install the project, you can use the following steps:
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
- 1. Ensure that you have [Node.js](https://nodejs.org/) installed on your machine.
28
- 2. Open a terminal or command prompt.
29
- 3. Run the following command to install the project and its dependencies via npm:
63
+ ## Requirements
64
+
65
+ - Node.js >= 20
66
+
67
+ ## Installation
30
68
 
31
69
  ```bash
32
- $ npm install @meltstudio/config-loader
70
+ npm install @meltstudio/config-loader
33
71
  ```
34
72
 
35
73
  ```bash
36
- $ yarn add @meltstudio/config-loader
74
+ yarn add @meltstudio/config-loader
37
75
  ```
38
76
 
39
- ## Usage
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
- import c from "@meltstudio/config-loader";
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
- apiKeys:
117
- googleMaps: ${GOOGLE_MAPS_API_KEY}
118
- sendGrid: ${SENDGRID_API_KEY}
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
- The expected output would be:
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
- "name": "Store",
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
- You can try executing our example in your project by following these steps with the command:
189
+ ## Schema API
158
190
 
159
- ```bash
160
- yarn example:run
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
- ### Usage with CLI
204
+ ### Objects
164
205
 
165
- When using our package with cli, it is important to have the cli attribute set to true.
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
- import path from "path";
209
+ c.object({
210
+ item: {
211
+ host: c.string(),
212
+ port: c.number(),
213
+ },
214
+ });
215
+ ```
170
216
 
171
- import c from "@meltstudio/config-loader";
217
+ Objects can be nested arbitrarily deep:
172
218
 
173
- const run = (): void => {
174
- const settings = c.schema({
175
- version: c.string({
176
- required: true,
177
- cli: true, 👈
178
- }),
179
- });
180
- const config = settings.load({
181
- env: false,
182
- args: true,
183
- files: path.join(__dirname, "./config.yaml"),
184
- });
185
- console.log(JSON.stringify(config, null, 2));
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
- run();
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
- To use it you need to send the property name on the command line with the new value
250
+ ## Loading Sources
192
251
 
193
- ```bash
194
- yarn example:run --version 2.0.0
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
- Having the following config.yaml file:
266
+ Both YAML (`.yaml`, `.yml`) and JSON (`.json`) files are supported. The format is detected automatically from the file extension.
198
267
 
199
- ```yaml
200
- version: 1.0.0
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
- The expected output would be:
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
- ```json
206
- {
207
- "version": "2.0.0"
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
- You can see that the CLI variable overrode the yaml file variable
329
+ For CLI tools that prefer the old exit-on-error behavior:
212
330
 
213
- ### Usage with Environment Variables
331
+ ```typescript
332
+ .load({ env: true, args: true, files: "./config.yaml", exitOnError: true })
333
+ ```
334
+
335
+ ## CLI Arguments
214
336
 
215
- The Config Loader package allows you to use environment variables in your system configuration. You can specify variable names in your configuration and get them. To use this feature you need to set **env: true**
337
+ Set `cli: true` on an option to allow overriding via command line:
216
338
 
217
339
  ```typescript
218
- import path from "path";
340
+ c.schema({
341
+ version: c.string({ required: true, cli: true }),
342
+ });
343
+ ```
219
344
 
220
- import c from "@meltstudio/config-loader";
345
+ ```bash
346
+ node app.js --version 2.0.0
347
+ ```
221
348
 
222
- const run = (): void => {
223
- const settings = c.schema({
224
- database: {
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
- run();
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
- With the following config.yaml file:
363
+ ## `.env` File Support
248
364
 
249
- ```yaml
250
- database:
251
- host: localhost
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
- yarn example:run
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
- If you have the environment variable `DB_PASSWORD=ENV_USED_PASSWORD`, the expected output would be:
377
+ **Usage:**
263
378
 
264
- ```json
265
- {
266
- "database": {
267
- "host": "localhost",
268
- "port": 5432,
269
- "credentials": {
270
- "username": "admin",
271
- "password": "ENV_USED_PASSWORD"
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
- You can notice that the environment variable overrode the value in the config.yaml file
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. For more information, please see the [LICENSE](./LICENSE) file.
418
+ This package is licensed under the Apache License 2.0. See the [LICENSE](./LICENSE) file for details.