@meltstudio/config-loader 3.6.0 → 3.7.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 +15 -2
- package/dist/index.d.ts +44 -1
- package/dist/index.js +186 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A type-safe configuration loader for Node.js. Define your schema once, load from YAML, JSON, or TOML 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
|
|
5
|
+
> **Upgrading from v1?** v1.x is deprecated and no longer maintained. Install the latest version with `npm install @meltstudio/config-loader@latest` or `yarn add @meltstudio/config-loader@latest`.
|
|
6
6
|
|
|
7
7
|
**[Full documentation](https://meltstudio.github.io/config-loader/)**
|
|
8
8
|
|
|
@@ -54,6 +54,7 @@ const config = c
|
|
|
54
54
|
- **Strict mode** — promote warnings to errors for production safety
|
|
55
55
|
- **Default values** — static or computed (via functions)
|
|
56
56
|
- **Multiple files / directory loading** — load from a list of files or an entire directory
|
|
57
|
+
- **File watching** — `watch()` reloads config on file changes with debouncing, change detection, and error recovery
|
|
57
58
|
|
|
58
59
|
## Installation
|
|
59
60
|
|
|
@@ -72,9 +73,21 @@ Requires Node.js >= 20.
|
|
|
72
73
|
See the **[full documentation](https://meltstudio.github.io/config-loader/)** for:
|
|
73
74
|
|
|
74
75
|
- [Schema API](https://meltstudio.github.io/config-loader/schema-api) — primitives, objects, arrays, `oneOf`, `sensitive`, validation
|
|
75
|
-
- [Loading & Sources](https://meltstudio.github.io/config-loader/loading-and-sources) — `load()`, `loadExtended()`, file/env/CLI/.env sources, `printConfig()`, `maskSecrets()`, error handling, strict mode
|
|
76
|
+
- [Loading & Sources](https://meltstudio.github.io/config-loader/loading-and-sources) — `load()`, `loadExtended()`, `watch()`, file/env/CLI/.env sources, `printConfig()`, `maskSecrets()`, error handling, strict mode
|
|
76
77
|
- [TypeScript Utilities](https://meltstudio.github.io/config-loader/typescript-utilities) — `SchemaValue`, exported types, type narrowing
|
|
77
78
|
|
|
79
|
+
## Examples
|
|
80
|
+
|
|
81
|
+
The [`example/`](./example) directory contains runnable examples:
|
|
82
|
+
|
|
83
|
+
- **[Basic](./example/basic)** — Schema definition, YAML file loading, nested objects and arrays, CLI arguments
|
|
84
|
+
- **[Advanced](./example/advanced)** — TOML config, `.env` files, `oneOf` constraints, `sensitive` fields, validation, `printConfig()`, `maskSecrets()`, error handling
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
yarn example:basic
|
|
88
|
+
yarn example:advanced
|
|
89
|
+
```
|
|
90
|
+
|
|
78
91
|
## Documentation for AI Agents
|
|
79
92
|
|
|
80
93
|
This project provides machine-readable documentation for AI coding agents at the docs site:
|
package/dist/index.d.ts
CHANGED
|
@@ -207,6 +207,40 @@ declare class ObjectOption<T extends Node = Node> extends OptionBase<"object"> {
|
|
|
207
207
|
|
|
208
208
|
type OptionTypes = PrimitiveOption | ArrayOption<OptionTypes> | ObjectOption<Node>;
|
|
209
209
|
|
|
210
|
+
/** A single change detected between two config loads. */
|
|
211
|
+
interface ConfigChange {
|
|
212
|
+
/** Dot-separated path to the changed key (e.g. "db.url"). */
|
|
213
|
+
path: string;
|
|
214
|
+
/** The previous value (undefined if key was added). */
|
|
215
|
+
oldValue: unknown;
|
|
216
|
+
/** The new value (undefined if key was removed). */
|
|
217
|
+
newValue: unknown;
|
|
218
|
+
/** Whether the change was an addition, removal, or modification. */
|
|
219
|
+
type: "added" | "removed" | "changed";
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Compares two plain config objects and returns a list of changes.
|
|
223
|
+
* Sensitive fields (from the schema) are masked in the output.
|
|
224
|
+
*/
|
|
225
|
+
declare function diffConfig(oldConfig: Record<string, unknown>, newConfig: Record<string, unknown>, schema?: Node): ConfigChange[];
|
|
226
|
+
|
|
227
|
+
/** Options for the `watch()` method. */
|
|
228
|
+
interface WatchOptions<T> {
|
|
229
|
+
/** Called after a successful reload with the new config, old config, and list of changes. */
|
|
230
|
+
onChange: (newConfig: T, oldConfig: T, changes: ConfigChange[]) => void;
|
|
231
|
+
/** Called when a reload fails (parse error, validation error). The previous config is retained. */
|
|
232
|
+
onError?: (error: Error) => void;
|
|
233
|
+
/** Debounce interval in milliseconds. Default: 100. */
|
|
234
|
+
debounce?: number;
|
|
235
|
+
}
|
|
236
|
+
/** Handle returned by `watch()`. Provides access to the current config and a `close()` method. */
|
|
237
|
+
interface ConfigWatcher<T> {
|
|
238
|
+
/** The current resolved configuration. Updated on each successful reload. */
|
|
239
|
+
readonly config: T;
|
|
240
|
+
/** Stop watching all files. Idempotent. */
|
|
241
|
+
close(): void;
|
|
242
|
+
}
|
|
243
|
+
|
|
210
244
|
/** Fluent builder that takes a schema and resolves configuration from multiple sources. */
|
|
211
245
|
declare class SettingsBuilder<T extends Node> {
|
|
212
246
|
private readonly schema;
|
|
@@ -225,6 +259,15 @@ declare class SettingsBuilder<T extends Node> {
|
|
|
225
259
|
* @throws {ConfigLoadError} If validation fails.
|
|
226
260
|
*/
|
|
227
261
|
loadExtended(sources: SettingsSources<SchemaValue<T>>): ExtendedResult;
|
|
262
|
+
/**
|
|
263
|
+
* Watches config files for changes and reloads automatically.
|
|
264
|
+
* File watchers are `.unref()`'d so they don't prevent the process from exiting.
|
|
265
|
+
* @param sources - Which sources to read (env, args, files, etc.).
|
|
266
|
+
* @param options - Callbacks and debounce configuration.
|
|
267
|
+
* @returns A `ConfigWatcher` with the current config and a `close()` method.
|
|
268
|
+
* @throws {ConfigLoadError} If the initial load fails.
|
|
269
|
+
*/
|
|
270
|
+
watch(sources: SettingsSources<SchemaValue<T>>, options: WatchOptions<SchemaValue<T>>): ConfigWatcher<SchemaValue<T>>;
|
|
228
271
|
}
|
|
229
272
|
|
|
230
273
|
/**
|
|
@@ -363,4 +406,4 @@ declare const option: {
|
|
|
363
406
|
schema: <T extends Node>(theSchema: T) => SettingsBuilder<T>;
|
|
364
407
|
};
|
|
365
408
|
|
|
366
|
-
export { type ConfigErrorEntry, ConfigFileError, ConfigLoadError, ConfigNode, ConfigNodeArray, type ExtendedResult, type NodeTree, type RecursivePartial, type SchemaValue, type SettingsSources, type StandardSchemaV1, option as default, maskSecrets, printConfig };
|
|
409
|
+
export { type ConfigChange, type ConfigErrorEntry, ConfigFileError, ConfigLoadError, ConfigNode, ConfigNodeArray, type ConfigWatcher, type ExtendedResult, type NodeTree, type RecursivePartial, type SchemaValue, type SettingsSources, type StandardSchemaV1, type WatchOptions, option as default, diffConfig, maskSecrets, printConfig };
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,7 @@ __export(index_exports, {
|
|
|
35
35
|
ConfigNode: () => configNode_default,
|
|
36
36
|
ConfigNodeArray: () => configNodeArray_default,
|
|
37
37
|
default: () => index_default,
|
|
38
|
+
diffConfig: () => diffConfig,
|
|
38
39
|
maskSecrets: () => maskSecrets,
|
|
39
40
|
printConfig: () => printConfig
|
|
40
41
|
});
|
|
@@ -1153,6 +1154,176 @@ var Settings = class {
|
|
|
1153
1154
|
};
|
|
1154
1155
|
var settings_default = Settings;
|
|
1155
1156
|
|
|
1157
|
+
// src/watcher.ts
|
|
1158
|
+
var fs4 = __toESM(require("fs"));
|
|
1159
|
+
|
|
1160
|
+
// src/diffConfig.ts
|
|
1161
|
+
var MASK = "***";
|
|
1162
|
+
function collectSensitivePaths(schema2, prefix = "") {
|
|
1163
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1164
|
+
for (const key of Object.keys(schema2)) {
|
|
1165
|
+
const opt = schema2[key];
|
|
1166
|
+
const fullPath = prefix ? `${prefix}.${key}` : key;
|
|
1167
|
+
if (opt instanceof PrimitiveOption && opt.params.sensitive) {
|
|
1168
|
+
paths.add(fullPath);
|
|
1169
|
+
}
|
|
1170
|
+
if (opt instanceof ObjectOption) {
|
|
1171
|
+
const childPaths = collectSensitivePaths(opt.item, fullPath);
|
|
1172
|
+
for (const p of childPaths) {
|
|
1173
|
+
paths.add(p);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return paths;
|
|
1178
|
+
}
|
|
1179
|
+
function diffObjects(oldObj, newObj, sensitivePaths, prefix = "") {
|
|
1180
|
+
const changes = [];
|
|
1181
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
|
|
1182
|
+
for (const key of allKeys) {
|
|
1183
|
+
const fullPath = prefix ? `${prefix}.${key}` : key;
|
|
1184
|
+
const isSensitive = sensitivePaths.has(fullPath);
|
|
1185
|
+
const oldVal = oldObj[key];
|
|
1186
|
+
const newVal = newObj[key];
|
|
1187
|
+
const hasOld = key in oldObj;
|
|
1188
|
+
const hasNew = key in newObj;
|
|
1189
|
+
if (hasNew && !hasOld) {
|
|
1190
|
+
changes.push({
|
|
1191
|
+
path: fullPath,
|
|
1192
|
+
type: "added",
|
|
1193
|
+
oldValue: void 0,
|
|
1194
|
+
newValue: isSensitive ? MASK : newVal
|
|
1195
|
+
});
|
|
1196
|
+
} else if (hasOld && !hasNew) {
|
|
1197
|
+
changes.push({
|
|
1198
|
+
path: fullPath,
|
|
1199
|
+
type: "removed",
|
|
1200
|
+
oldValue: isSensitive ? MASK : oldVal,
|
|
1201
|
+
newValue: void 0
|
|
1202
|
+
});
|
|
1203
|
+
} else if (oldVal !== null && newVal !== null && typeof oldVal === "object" && typeof newVal === "object" && !Array.isArray(oldVal) && !Array.isArray(newVal)) {
|
|
1204
|
+
changes.push(
|
|
1205
|
+
...diffObjects(
|
|
1206
|
+
oldVal,
|
|
1207
|
+
newVal,
|
|
1208
|
+
sensitivePaths,
|
|
1209
|
+
fullPath
|
|
1210
|
+
)
|
|
1211
|
+
);
|
|
1212
|
+
} else if (!Object.is(oldVal, newVal)) {
|
|
1213
|
+
if (Array.isArray(oldVal) && Array.isArray(newVal) && JSON.stringify(oldVal) === JSON.stringify(newVal)) {
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
changes.push({
|
|
1217
|
+
path: fullPath,
|
|
1218
|
+
type: "changed",
|
|
1219
|
+
oldValue: isSensitive ? MASK : oldVal,
|
|
1220
|
+
newValue: isSensitive ? MASK : newVal
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return changes;
|
|
1225
|
+
}
|
|
1226
|
+
function diffConfig(oldConfig, newConfig, schema2) {
|
|
1227
|
+
const sensitivePaths = schema2 ? collectSensitivePaths(schema2) : /* @__PURE__ */ new Set();
|
|
1228
|
+
return diffObjects(oldConfig, newConfig, sensitivePaths);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// src/watcher.ts
|
|
1232
|
+
function resolveFilePaths(sources) {
|
|
1233
|
+
const paths = [];
|
|
1234
|
+
const configFiles = validateFiles(
|
|
1235
|
+
sources.files ?? false,
|
|
1236
|
+
sources.dir ?? false
|
|
1237
|
+
);
|
|
1238
|
+
if (Array.isArray(configFiles)) {
|
|
1239
|
+
paths.push(...configFiles);
|
|
1240
|
+
} else if (configFiles) {
|
|
1241
|
+
paths.push(configFiles);
|
|
1242
|
+
}
|
|
1243
|
+
if (sources.envFile) {
|
|
1244
|
+
const envFiles = Array.isArray(sources.envFile) ? sources.envFile : [sources.envFile];
|
|
1245
|
+
paths.push(...envFiles);
|
|
1246
|
+
}
|
|
1247
|
+
return paths;
|
|
1248
|
+
}
|
|
1249
|
+
function createWatcher(schema2, sources, options) {
|
|
1250
|
+
const debounceMs = options.debounce ?? 100;
|
|
1251
|
+
const initialSettings = new settings_default(schema2, sources);
|
|
1252
|
+
let currentConfig = initialSettings.get();
|
|
1253
|
+
const filePaths = resolveFilePaths(sources);
|
|
1254
|
+
const watchers = [];
|
|
1255
|
+
let debounceTimer = null;
|
|
1256
|
+
let closed = false;
|
|
1257
|
+
function reload() {
|
|
1258
|
+
if (closed) return;
|
|
1259
|
+
try {
|
|
1260
|
+
clearFileCache();
|
|
1261
|
+
const settings = new settings_default(schema2, sources);
|
|
1262
|
+
const newConfig = settings.get();
|
|
1263
|
+
const changes = diffConfig(
|
|
1264
|
+
currentConfig,
|
|
1265
|
+
newConfig,
|
|
1266
|
+
schema2
|
|
1267
|
+
);
|
|
1268
|
+
if (changes.length > 0) {
|
|
1269
|
+
const oldConfig = currentConfig;
|
|
1270
|
+
currentConfig = newConfig;
|
|
1271
|
+
options.onChange(newConfig, oldConfig, changes);
|
|
1272
|
+
}
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
if (options.onError) {
|
|
1275
|
+
options.onError(err instanceof Error ? err : new Error(String(err)));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
function scheduleReload() {
|
|
1280
|
+
if (closed) return;
|
|
1281
|
+
if (debounceTimer) {
|
|
1282
|
+
clearTimeout(debounceTimer);
|
|
1283
|
+
}
|
|
1284
|
+
debounceTimer = setTimeout(reload, debounceMs);
|
|
1285
|
+
debounceTimer.unref();
|
|
1286
|
+
}
|
|
1287
|
+
for (const filePath of filePaths) {
|
|
1288
|
+
try {
|
|
1289
|
+
const watcher = fs4.watch(filePath, () => {
|
|
1290
|
+
scheduleReload();
|
|
1291
|
+
});
|
|
1292
|
+
watcher.unref();
|
|
1293
|
+
watchers.push(watcher);
|
|
1294
|
+
} catch {
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
if (sources.dir && typeof sources.dir === "string") {
|
|
1298
|
+
try {
|
|
1299
|
+
const dirWatcher = fs4.watch(sources.dir, () => {
|
|
1300
|
+
scheduleReload();
|
|
1301
|
+
});
|
|
1302
|
+
dirWatcher.unref();
|
|
1303
|
+
watchers.push(dirWatcher);
|
|
1304
|
+
} catch {
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
function close() {
|
|
1308
|
+
if (closed) return;
|
|
1309
|
+
closed = true;
|
|
1310
|
+
if (debounceTimer) {
|
|
1311
|
+
clearTimeout(debounceTimer);
|
|
1312
|
+
debounceTimer = null;
|
|
1313
|
+
}
|
|
1314
|
+
for (const watcher of watchers) {
|
|
1315
|
+
watcher.close();
|
|
1316
|
+
}
|
|
1317
|
+
watchers.length = 0;
|
|
1318
|
+
}
|
|
1319
|
+
return {
|
|
1320
|
+
get config() {
|
|
1321
|
+
return currentConfig;
|
|
1322
|
+
},
|
|
1323
|
+
close
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1156
1327
|
// src/builder/settings.ts
|
|
1157
1328
|
var SettingsBuilder = class {
|
|
1158
1329
|
schema;
|
|
@@ -1182,17 +1353,28 @@ var SettingsBuilder = class {
|
|
|
1182
1353
|
warnings: settings.getWarnings()
|
|
1183
1354
|
};
|
|
1184
1355
|
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Watches config files for changes and reloads automatically.
|
|
1358
|
+
* File watchers are `.unref()`'d so they don't prevent the process from exiting.
|
|
1359
|
+
* @param sources - Which sources to read (env, args, files, etc.).
|
|
1360
|
+
* @param options - Callbacks and debounce configuration.
|
|
1361
|
+
* @returns A `ConfigWatcher` with the current config and a `close()` method.
|
|
1362
|
+
* @throws {ConfigLoadError} If the initial load fails.
|
|
1363
|
+
*/
|
|
1364
|
+
watch(sources, options) {
|
|
1365
|
+
return createWatcher(this.schema, sources, options);
|
|
1366
|
+
}
|
|
1185
1367
|
};
|
|
1186
1368
|
|
|
1187
1369
|
// src/maskSecrets.ts
|
|
1188
|
-
var
|
|
1370
|
+
var MASK2 = "***";
|
|
1189
1371
|
function maskNodeTree(tree) {
|
|
1190
1372
|
const result = {};
|
|
1191
1373
|
for (const [key, entry] of Object.entries(tree)) {
|
|
1192
1374
|
if (entry instanceof configNode_default) {
|
|
1193
1375
|
if (entry.sensitive) {
|
|
1194
1376
|
const masked = new configNode_default(
|
|
1195
|
-
|
|
1377
|
+
MASK2,
|
|
1196
1378
|
entry.path,
|
|
1197
1379
|
entry.sourceType,
|
|
1198
1380
|
entry.file,
|
|
@@ -1233,7 +1415,7 @@ function maskPlainObject(obj, schema2) {
|
|
|
1233
1415
|
result[key] = value;
|
|
1234
1416
|
}
|
|
1235
1417
|
} else if (schemaNode instanceof OptionBase) {
|
|
1236
|
-
result[key] = schemaNode.params.sensitive ?
|
|
1418
|
+
result[key] = schemaNode.params.sensitive ? MASK2 : value;
|
|
1237
1419
|
} else {
|
|
1238
1420
|
result[key] = value;
|
|
1239
1421
|
}
|
|
@@ -1433,6 +1615,7 @@ var index_default = option;
|
|
|
1433
1615
|
ConfigLoadError,
|
|
1434
1616
|
ConfigNode,
|
|
1435
1617
|
ConfigNodeArray,
|
|
1618
|
+
diffConfig,
|
|
1436
1619
|
maskSecrets,
|
|
1437
1620
|
printConfig
|
|
1438
1621
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meltstudio/config-loader",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"description": "Type-safe configuration loader with full TypeScript inference. Load from YAML, JSON, TOML, .env, environment variables, and CLI args.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"type-check": "tsc --noEmit",
|
|
40
40
|
"test": "jest --verbose",
|
|
41
41
|
"replace-tspaths": "./scripts/replace-tspaths/index.mjs",
|
|
42
|
-
"example:
|
|
42
|
+
"example:basic": "ts-node -r tsconfig-paths/register ./example/basic/index.ts",
|
|
43
|
+
"example:advanced": "ts-node -r tsconfig-paths/register ./example/advanced/index.ts",
|
|
43
44
|
"prepare": "husky",
|
|
44
45
|
"docs:build": "docusaurus build && node scripts/fix-llms-urls.mjs",
|
|
45
46
|
"docs:serve": "docusaurus serve",
|