@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.
- package/README.md +330 -175
- package/dist/index.d.ts +135 -84
- package/dist/index.js +605 -283
- package/package.json +39 -28
package/README.md
CHANGED
|
@@ -1,99 +1,88 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
5
|
+
## Why config-loader?
|
|
7
6
|
|
|
8
|
-
|
|
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
|
-
|
|
9
|
+
```typescript
|
|
10
|
+
import c from "@meltstudio/config-loader";
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
+
No separate interface to maintain. No `as` casts. The types flow from the schema.
|
|
24
51
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
+
## Requirements
|
|
38
64
|
|
|
39
|
-
|
|
40
|
-
import path from "path";
|
|
65
|
+
- Node.js >= 20
|
|
41
66
|
|
|
42
|
-
|
|
67
|
+
## Installation
|
|
43
68
|
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
73
|
+
```bash
|
|
74
|
+
yarn add @meltstudio/config-loader
|
|
88
75
|
```
|
|
89
76
|
|
|
90
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
+
## Schema API
|
|
155
190
|
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
203
|
+
|
|
204
|
+
### Objects
|
|
205
|
+
|
|
206
|
+
Use `c.object()` to declare nested object schemas:
|
|
207
|
+
|
|
162
208
|
```typescript
|
|
163
|
-
|
|
209
|
+
c.object({
|
|
210
|
+
item: {
|
|
211
|
+
host: c.string(),
|
|
212
|
+
port: c.number(),
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
```
|
|
164
216
|
|
|
165
|
-
|
|
217
|
+
Objects can be nested arbitrarily deep:
|
|
166
218
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
328
|
+
|
|
329
|
+
For CLI tools that prefer the old exit-on-error behavior:
|
|
330
|
+
|
|
204
331
|
```typescript
|
|
205
|
-
|
|
332
|
+
.load({ env: true, args: true, files: "./config.yaml", exitOnError: true })
|
|
333
|
+
```
|
|
206
334
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
339
|
+
```typescript
|
|
340
|
+
c.schema({
|
|
341
|
+
version: c.string({ required: true, cli: true }),
|
|
342
|
+
});
|
|
235
343
|
```
|
|
236
|
-
|
|
237
|
-
```
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
"
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
417
|
+
|
|
418
|
+
This package is licensed under the Apache License 2.0. See the [LICENSE](./LICENSE) file for details.
|