@meltstudio/config-loader 1.0.4 → 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 +330 -175
  2. package/dist/index.d.ts +135 -84
  3. package/dist/index.js +605 -283
  4. package/package.json +39 -28
package/README.md CHANGED
@@ -1,99 +1,88 @@
1
- # Config Loader
2
- > ⚠️ **WARNING**: This project is in beta, so some features may change in the future. Use at your own discretion
1
+ # @meltstudio/config-loader
3
2
 
4
- ## Project Description
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.
5
4
 
6
- 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.
5
+ ## Why config-loader?
7
6
 
8
- ## Features
9
- - Retrieve and collect variables from one or multiple files in your project.
10
- - YAML file support (support for other file types coming soon.)
11
- - Data can also be retrieved from CLI or environment variables .
12
- - Compatible with TypeScript/JavaScript environments, making it suitable for Node.js projects.
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:
13
8
 
14
- ## Table of Contents
9
+ ```typescript
10
+ import c from "@meltstudio/config-loader";
15
11
 
16
- - [Installation](#installation)
17
- - [Usage](#usage)
18
- - [License](#license)
19
- - [Acknowledgements](#acknowledgements)
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
+ });
20
41
 
21
- ## Installation
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
+ ```
22
49
 
23
- To install the project, you can use the following steps:
50
+ No separate interface to maintain. No `as` casts. The types flow from the schema.
24
51
 
25
- 1. Ensure that you have [Node.js](https://nodejs.org/) installed on your machine.
26
- 2. Open a terminal or command prompt.
27
- 3. Run the following command to install the project and its dependencies via npm:
28
- ```bash
29
- $ npm install @meltstudio/config-loader
30
- ```
31
- ```bash
32
- $ yarn add @meltstudio/config-loader
33
- ```
52
+ ## Features
34
53
 
35
- ## Usage
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
36
62
 
37
- Here's an example of how to use the `@meltstudio/config-loader` package in a TypeScript project:
63
+ ## Requirements
38
64
 
39
- ```typescript
40
- import path from "path";
65
+ - Node.js >= 20
41
66
 
42
- import Settings, { option } from "@/src";
67
+ ## Installation
43
68
 
44
- const run = (): void => {
45
- const settings = new Settings(
46
- {
47
- version: option.string({ required: true, cli: true }),
48
- website: {
49
- title: option.string({ required: true }),
50
- url: option.string({
51
- required: false,
52
- defaultValue: "www.mywebsite.dev",
53
- }),
54
- description: option.string({ required: true }),
55
- isProduction: option.bool({ required: true }),
56
- },
57
- database: {
58
- host: option.string({ required: true }),
59
- port: option.number({ required: true }),
60
- credentials: {
61
- username: option.string(),
62
- password: option.string(),
63
- },
64
- },
65
- socialMedia: option.array({
66
- required: true,
67
- item: option.string({ required: true }),
68
- }),
69
- features: option.array({
70
- required: true,
71
- item: {
72
- name: option.string(),
73
- enabled: option.bool(),
74
- },
75
- }),
76
- },
77
- {
78
- env: false,
79
- args: true,
80
- files: path.join(__dirname, "./config.yaml"),
81
- }
82
- );
83
- const config = settings.get();
84
- console.log(JSON.stringify(config, null, 2));
85
- };
69
+ ```bash
70
+ npm install @meltstudio/config-loader
71
+ ```
86
72
 
87
- run();
73
+ ```bash
74
+ yarn add @meltstudio/config-loader
88
75
  ```
89
76
 
90
- With a config.yaml file with the following contents:
77
+ ## Quick Start
78
+
79
+ **config.yaml:**
80
+
91
81
  ```yaml
92
82
  version: 1.0.0
93
83
  website:
94
84
  title: My Website
95
85
  description: A simple and elegant website
96
- port: 3000
97
86
  isProduction: false
98
87
 
99
88
  database:
@@ -110,13 +99,65 @@ features:
110
99
  enabled: true
111
100
  - name: Admin
112
101
  enabled: false
102
+ ```
113
103
 
114
- apiKeys:
115
- googleMaps: ${GOOGLE_MAPS_API_KEY}
116
- sendGrid: ${SENDGRID_API_KEY}
104
+ **index.ts:**
105
+
106
+ ```typescript
107
+ import path from "path";
108
+ import c from "@meltstudio/config-loader";
109
+
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));
117
157
  ```
118
158
 
119
- The expected output would be:
159
+ Output:
160
+
120
161
  ```json
121
162
  {
122
163
  "version": "1.0.0",
@@ -139,125 +180,239 @@ The expected output would be:
139
180
  "https://instagram.com/example"
140
181
  ],
141
182
  "features": [
142
- {
143
- "name": "Store",
144
- "enabled": true
145
- },
146
- {
147
- "name": "Admin",
148
- "enabled": false
149
- }
183
+ { "name": "Store", "enabled": true },
184
+ { "name": "Admin", "enabled": false }
150
185
  ]
151
186
  }
152
187
  ```
153
188
 
154
- You can try executing our example in your project by following these steps with the command:
189
+ ## Schema API
155
190
 
156
- ```bash
157
- 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 });
158
202
  ```
159
- ### Usage with CLI
160
- When using our package with cli, it is important to have the cli attribute set to true.
161
- This will allow values to be sent when running the package from the command line.
203
+
204
+ ### Objects
205
+
206
+ Use `c.object()` to declare nested object schemas:
207
+
162
208
  ```typescript
163
- import path from "path";
209
+ c.object({
210
+ item: {
211
+ host: c.string(),
212
+ port: c.number(),
213
+ },
214
+ });
215
+ ```
164
216
 
165
- import Settings, { option } from "@/src";
217
+ Objects can be nested arbitrarily deep:
166
218
 
167
- const run = (): void => {
168
- const settings = new Settings(
169
- {
170
- version: option.string({
171
- required: true,
172
- cli: true, 👈
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
+ },
173
230
  }),
174
231
  },
175
- {
176
- env: false,
177
- args: true,
178
- files: path.join(__dirname, "./config.yaml"),
179
- }
180
- );
181
- const config = settings.get();
182
- console.log(JSON.stringify(config, null, 2));
183
- };
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.
184
237
 
185
- run();
238
+ ### Arrays
239
+
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 }[]
186
248
  ```
187
- now for use it you need to send the property name on the command line with the new value
188
- ```bash
189
- yarn example:run --version 2.0.0
249
+
250
+ ## Loading Sources
251
+
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
+ })
190
264
  ```
191
- Having the following config.yaml file:
192
- ```yaml
193
- version: 1.0.0
265
+
266
+ Both YAML (`.yaml`, `.yml`) and JSON (`.json`) files are supported. The format is detected automatically from the file extension.
267
+
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"
194
302
  ```
195
- The expected output would be:
196
- ```json
197
- {
198
- "version": "2.0.0",
303
+
304
+ This is useful for debugging configuration resolution, building admin UIs that show where each setting originated, or auditing which sources are active.
305
+
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
+ }
199
326
  }
200
327
  ```
201
- You can see that the CLI variable overrode the yaml file variable
202
- ### Usage with Environment Variables
203
- 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**
328
+
329
+ For CLI tools that prefer the old exit-on-error behavior:
330
+
204
331
  ```typescript
205
- import path from "path";
332
+ .load({ env: true, args: true, files: "./config.yaml", exitOnError: true })
333
+ ```
206
334
 
207
- import Settings, { option } from "@/src";
208
-
209
- const run = (): void => {
210
- const settings = new Settings(
211
- {
212
- database: {
213
- host: option.string({ required: true }),
214
- port: option.number({ required: true }),
215
- credentials: {
216
- username: option.string(),
217
- password: option.string({
218
- env: "DB_PASSWORD",
219
- cli: true,
220
- }),
221
- },
222
- },
223
- },
224
- {
225
- env: true, 👈
226
- args: true,
227
- files: path.join(__dirname, "./config.yaml"),
228
- }
229
- );
230
- const config = settings.get();
231
- console.log(JSON.stringify(config, null, 2));
232
- };
335
+ ## CLI Arguments
336
+
337
+ Set `cli: true` on an option to allow overriding via command line:
233
338
 
234
- run();
339
+ ```typescript
340
+ c.schema({
341
+ version: c.string({ required: true, cli: true }),
342
+ });
235
343
  ```
236
- With the following config.yaml file:
237
- ```yaml
238
- database:
239
- host: localhost
240
- port: 5432
241
- credentials:
242
- username: admin
243
- password: IGNORED_PASSWORD
344
+
345
+ ```bash
346
+ node app.js --version 2.0.0
244
347
  ```
348
+
349
+ ## Environment Variables
350
+
351
+ Set `env: "VAR_NAME"` on an option and `env: true` in the load options:
352
+
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" });
361
+ ```
362
+
363
+ ## `.env` File Support
364
+
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:**
368
+
245
369
  ```bash
246
- 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
247
375
  ```
248
- If you have the environment variable `DB_PASSWORD=ENV_USED_PASSWORD`, the expected output would be:
249
- ```json
250
- {
251
- "database": {
252
- "host": "localhost",
253
- "port": 5432,
254
- "credentials": {
255
- "username": "admin",
256
- "password": "ENV_USED_PASSWORD"
257
- }
258
- }
259
- }
376
+
377
+ **Usage:**
378
+
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
+ })
260
403
  ```
261
- You can notice that the environment variable overrode the value in the config.yaml file
404
+
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.
415
+
262
416
  ## License
263
- This package is licensed under the Apache License 2.0. For more information, please see the [LICENSE](./LICENSE) file.
417
+
418
+ This package is licensed under the Apache License 2.0. See the [LICENSE](./LICENSE) file for details.