@wgtechlabs/config-engine 0.1.0-staging.d0d9e7f
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/LICENSE +21 -0
- package/README.md +306 -0
- package/dist/cache.d.ts +36 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/chunk-gs6j6hrj.js +35 -0
- package/dist/chunk-gs6j6hrj.js.map +10 -0
- package/dist/dot-prop.d.ts +35 -0
- package/dist/dot-prop.d.ts.map +1 -0
- package/dist/encryption.d.ts +31 -0
- package/dist/encryption.d.ts.map +1 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +967 -0
- package/dist/index.js.map +17 -0
- package/dist/migrations.d.ts +22 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/platform.d.ts +23 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/store.d.ts +48 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/validation.d.ts +28 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +10 -0
- package/dist/validation.js.map +9 -0
- package/package.json +82 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__require,
|
|
3
|
+
createZodValidator,
|
|
4
|
+
resolveValidator
|
|
5
|
+
} from "./chunk-gs6j6hrj.js";
|
|
6
|
+
|
|
7
|
+
// src/index.ts
|
|
8
|
+
import { mkdirSync } from "node:fs";
|
|
9
|
+
import { dirname } from "node:path";
|
|
10
|
+
import { watch } from "node:fs";
|
|
11
|
+
|
|
12
|
+
// src/cache.ts
|
|
13
|
+
class ConfigCache {
|
|
14
|
+
#store;
|
|
15
|
+
#strategy;
|
|
16
|
+
#data = new Map;
|
|
17
|
+
#dirty = new Set;
|
|
18
|
+
#deleted = new Set;
|
|
19
|
+
#flushScheduled = false;
|
|
20
|
+
#flushPromise = null;
|
|
21
|
+
constructor(store, strategy = "batched") {
|
|
22
|
+
this.#store = store;
|
|
23
|
+
this.#strategy = strategy;
|
|
24
|
+
}
|
|
25
|
+
load(initial) {
|
|
26
|
+
this.#data.clear();
|
|
27
|
+
this.#dirty.clear();
|
|
28
|
+
this.#deleted.clear();
|
|
29
|
+
const stored = initial ?? this.#store.getAll();
|
|
30
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
31
|
+
this.#data.set(key, value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
get(key) {
|
|
35
|
+
return this.#data.get(key);
|
|
36
|
+
}
|
|
37
|
+
has(key) {
|
|
38
|
+
return this.#data.has(key);
|
|
39
|
+
}
|
|
40
|
+
get size() {
|
|
41
|
+
return this.#data.size;
|
|
42
|
+
}
|
|
43
|
+
getAll() {
|
|
44
|
+
const obj = Object.create(null);
|
|
45
|
+
for (const [key, value] of this.#data) {
|
|
46
|
+
obj[key] = value;
|
|
47
|
+
}
|
|
48
|
+
return obj;
|
|
49
|
+
}
|
|
50
|
+
entries() {
|
|
51
|
+
return this.#data.entries();
|
|
52
|
+
}
|
|
53
|
+
set(key, value) {
|
|
54
|
+
this.#data.set(key, value);
|
|
55
|
+
this.#dirty.add(key);
|
|
56
|
+
this.#deleted.delete(key);
|
|
57
|
+
this.#scheduleFlush();
|
|
58
|
+
}
|
|
59
|
+
setMany(entries) {
|
|
60
|
+
for (const [key, value] of entries) {
|
|
61
|
+
this.#data.set(key, value);
|
|
62
|
+
this.#dirty.add(key);
|
|
63
|
+
this.#deleted.delete(key);
|
|
64
|
+
}
|
|
65
|
+
this.#scheduleFlush();
|
|
66
|
+
}
|
|
67
|
+
delete(key) {
|
|
68
|
+
const existed = this.#data.delete(key);
|
|
69
|
+
if (existed) {
|
|
70
|
+
this.#dirty.delete(key);
|
|
71
|
+
this.#deleted.add(key);
|
|
72
|
+
this.#scheduleFlush();
|
|
73
|
+
}
|
|
74
|
+
return existed;
|
|
75
|
+
}
|
|
76
|
+
clear() {
|
|
77
|
+
for (const key of this.#data.keys()) {
|
|
78
|
+
this.#deleted.add(key);
|
|
79
|
+
}
|
|
80
|
+
this.#data.clear();
|
|
81
|
+
this.#dirty.clear();
|
|
82
|
+
this.#scheduleFlush();
|
|
83
|
+
}
|
|
84
|
+
replaceAll(data) {
|
|
85
|
+
for (const key of this.#data.keys()) {
|
|
86
|
+
if (!(key in data)) {
|
|
87
|
+
this.#deleted.add(key);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
this.#data.clear();
|
|
91
|
+
this.#dirty.clear();
|
|
92
|
+
for (const [key, value] of Object.entries(data)) {
|
|
93
|
+
this.#data.set(key, value);
|
|
94
|
+
this.#dirty.add(key);
|
|
95
|
+
this.#deleted.delete(key);
|
|
96
|
+
}
|
|
97
|
+
this.#scheduleFlush();
|
|
98
|
+
}
|
|
99
|
+
#scheduleFlush() {
|
|
100
|
+
if (this.#strategy === "manual")
|
|
101
|
+
return;
|
|
102
|
+
if (this.#strategy === "immediate") {
|
|
103
|
+
this.flushSync();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!this.#flushScheduled) {
|
|
107
|
+
this.#flushScheduled = true;
|
|
108
|
+
this.#flushPromise = Promise.resolve().then(() => {
|
|
109
|
+
this.flushSync();
|
|
110
|
+
this.#flushScheduled = false;
|
|
111
|
+
this.#flushPromise = null;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
flushSync() {
|
|
116
|
+
if (this.#dirty.size === 0 && this.#deleted.size === 0)
|
|
117
|
+
return;
|
|
118
|
+
this.#store.transaction(() => {
|
|
119
|
+
if (this.#dirty.size > 0) {
|
|
120
|
+
const entries = [];
|
|
121
|
+
for (const key of this.#dirty) {
|
|
122
|
+
entries.push([key, this.#data.get(key)]);
|
|
123
|
+
}
|
|
124
|
+
this.#store.setMany(entries);
|
|
125
|
+
}
|
|
126
|
+
for (const key of this.#deleted) {
|
|
127
|
+
this.#store.deleteOne(key);
|
|
128
|
+
}
|
|
129
|
+
this.#store.touchLastWrite();
|
|
130
|
+
});
|
|
131
|
+
this.#dirty.clear();
|
|
132
|
+
this.#deleted.clear();
|
|
133
|
+
}
|
|
134
|
+
async flush() {
|
|
135
|
+
if (this.#flushPromise) {
|
|
136
|
+
await this.#flushPromise;
|
|
137
|
+
} else if (this.#dirty.size > 0 || this.#deleted.size > 0) {
|
|
138
|
+
this.flushSync();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
reload() {
|
|
142
|
+
this.#dirty.clear();
|
|
143
|
+
this.#deleted.clear();
|
|
144
|
+
this.load();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/dot-prop.ts
|
|
149
|
+
function getByPath(obj, path) {
|
|
150
|
+
if (typeof obj !== "object" || obj === null)
|
|
151
|
+
return;
|
|
152
|
+
const keys = path.split(".");
|
|
153
|
+
let current = obj;
|
|
154
|
+
for (const key of keys) {
|
|
155
|
+
if (typeof current !== "object" || current === null)
|
|
156
|
+
return;
|
|
157
|
+
current = current[key];
|
|
158
|
+
}
|
|
159
|
+
return current;
|
|
160
|
+
}
|
|
161
|
+
function setByPath(obj, path, value) {
|
|
162
|
+
const keys = path.split(".");
|
|
163
|
+
if (keys.length === 0)
|
|
164
|
+
return obj;
|
|
165
|
+
const result = { ...obj };
|
|
166
|
+
let current = result;
|
|
167
|
+
for (let i = 0;i < keys.length - 1; i++) {
|
|
168
|
+
const key = keys[i];
|
|
169
|
+
if (typeof current[key] !== "object" || current[key] === null) {
|
|
170
|
+
current[key] = {};
|
|
171
|
+
} else {
|
|
172
|
+
current[key] = { ...current[key] };
|
|
173
|
+
}
|
|
174
|
+
current = current[key];
|
|
175
|
+
}
|
|
176
|
+
const lastKey = keys[keys.length - 1];
|
|
177
|
+
current[lastKey] = value;
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
function hasByPath(obj, path) {
|
|
181
|
+
if (typeof obj !== "object" || obj === null)
|
|
182
|
+
return false;
|
|
183
|
+
const keys = path.split(".");
|
|
184
|
+
let current = obj;
|
|
185
|
+
for (let i = 0;i < keys.length; i++) {
|
|
186
|
+
if (typeof current !== "object" || current === null)
|
|
187
|
+
return false;
|
|
188
|
+
const key = keys[i];
|
|
189
|
+
if (!Object.prototype.hasOwnProperty.call(current, key))
|
|
190
|
+
return false;
|
|
191
|
+
current = current[key];
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
function deleteByPath(obj, path) {
|
|
196
|
+
const keys = path.split(".");
|
|
197
|
+
if (keys.length === 0)
|
|
198
|
+
return obj;
|
|
199
|
+
const result = { ...obj };
|
|
200
|
+
let current = result;
|
|
201
|
+
for (let i = 0;i < keys.length - 1; i++) {
|
|
202
|
+
const key = keys[i];
|
|
203
|
+
if (typeof current[key] !== "object" || current[key] === null) {
|
|
204
|
+
return obj;
|
|
205
|
+
}
|
|
206
|
+
current[key] = { ...current[key] };
|
|
207
|
+
current = current[key];
|
|
208
|
+
}
|
|
209
|
+
const lastKey = keys[keys.length - 1];
|
|
210
|
+
delete current[lastKey];
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/encryption.ts
|
|
215
|
+
class SecretsEngineEncryptor {
|
|
216
|
+
#keyName;
|
|
217
|
+
#derivedKey = null;
|
|
218
|
+
#secrets = null;
|
|
219
|
+
constructor(keyName) {
|
|
220
|
+
this.#keyName = keyName;
|
|
221
|
+
}
|
|
222
|
+
async init() {
|
|
223
|
+
let SecretsEngine;
|
|
224
|
+
try {
|
|
225
|
+
const mod = await import("@wgtechlabs/secrets-engine");
|
|
226
|
+
SecretsEngine = mod.SecretsEngine ?? mod.default?.SecretsEngine ?? mod;
|
|
227
|
+
} catch {
|
|
228
|
+
throw new Error('Encryption requires "@wgtechlabs/secrets-engine" as a peer dependency. Install it with: bun add @wgtechlabs/secrets-engine');
|
|
229
|
+
}
|
|
230
|
+
this.#secrets = await SecretsEngine.open();
|
|
231
|
+
const hasKey = await this.#secrets.has(this.#keyName);
|
|
232
|
+
if (!hasKey) {
|
|
233
|
+
const keyBytes = crypto.getRandomValues(new Uint8Array(32));
|
|
234
|
+
const keyHex2 = Array.from(keyBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
235
|
+
await this.#secrets.set(this.#keyName, keyHex2);
|
|
236
|
+
}
|
|
237
|
+
const keyHex = await this.#secrets.get(this.#keyName);
|
|
238
|
+
const keyBuffer = hexToUint8Array(keyHex);
|
|
239
|
+
this.#derivedKey = await crypto.subtle.importKey("raw", keyBuffer.buffer, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
240
|
+
}
|
|
241
|
+
async encrypt(plaintext) {
|
|
242
|
+
if (!this.#derivedKey)
|
|
243
|
+
throw new Error("Encryptor not initialized. Call init() first.");
|
|
244
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
245
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
246
|
+
const cipherBuffer = await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv.buffer }, this.#derivedKey, encoded);
|
|
247
|
+
const ivB64 = uint8ArrayToBase64(iv);
|
|
248
|
+
const cipherB64 = uint8ArrayToBase64(new Uint8Array(cipherBuffer));
|
|
249
|
+
return `${ivB64}:${cipherB64}`;
|
|
250
|
+
}
|
|
251
|
+
async decrypt(ciphertext) {
|
|
252
|
+
if (!this.#derivedKey)
|
|
253
|
+
throw new Error("Encryptor not initialized. Call init() first.");
|
|
254
|
+
const [ivB64, cipherB64] = ciphertext.split(":");
|
|
255
|
+
if (!ivB64 || !cipherB64)
|
|
256
|
+
throw new Error("Invalid encrypted data format.");
|
|
257
|
+
const iv = base64ToUint8Array(ivB64);
|
|
258
|
+
const cipherBuffer = base64ToUint8Array(cipherB64);
|
|
259
|
+
const plainBuffer = await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv.buffer }, this.#derivedKey, cipherBuffer.buffer);
|
|
260
|
+
return new TextDecoder().decode(plainBuffer);
|
|
261
|
+
}
|
|
262
|
+
async close() {
|
|
263
|
+
if (this.#secrets) {
|
|
264
|
+
this.#secrets.close();
|
|
265
|
+
this.#secrets = null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function hexToUint8Array(hex) {
|
|
270
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
271
|
+
for (let i = 0;i < hex.length; i += 2) {
|
|
272
|
+
bytes[i / 2] = Number.parseInt(hex.substring(i, i + 2), 16);
|
|
273
|
+
}
|
|
274
|
+
return bytes;
|
|
275
|
+
}
|
|
276
|
+
function uint8ArrayToBase64(bytes) {
|
|
277
|
+
if (typeof Buffer !== "undefined") {
|
|
278
|
+
return Buffer.from(bytes).toString("base64");
|
|
279
|
+
}
|
|
280
|
+
let binary = "";
|
|
281
|
+
for (const byte of bytes) {
|
|
282
|
+
binary += String.fromCharCode(byte);
|
|
283
|
+
}
|
|
284
|
+
return btoa(binary);
|
|
285
|
+
}
|
|
286
|
+
function base64ToUint8Array(b64) {
|
|
287
|
+
if (typeof Buffer !== "undefined") {
|
|
288
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
289
|
+
}
|
|
290
|
+
const binary = atob(b64);
|
|
291
|
+
const bytes = new Uint8Array(binary.length);
|
|
292
|
+
for (let i = 0;i < binary.length; i++) {
|
|
293
|
+
bytes[i] = binary.charCodeAt(i);
|
|
294
|
+
}
|
|
295
|
+
return bytes;
|
|
296
|
+
}
|
|
297
|
+
async function resolveEncryptor(encryptionKey) {
|
|
298
|
+
if (!encryptionKey)
|
|
299
|
+
return;
|
|
300
|
+
if (typeof encryptionKey === "string") {
|
|
301
|
+
const enc = new SecretsEngineEncryptor(encryptionKey);
|
|
302
|
+
await enc.init();
|
|
303
|
+
return enc;
|
|
304
|
+
}
|
|
305
|
+
return encryptionKey;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/migrations.ts
|
|
309
|
+
var META_KEY = "schema_version";
|
|
310
|
+
var INITIAL_VERSION = "0.0.0";
|
|
311
|
+
async function runMigrations(options) {
|
|
312
|
+
const { store, migrations, projectVersion, beforeEachMigration, afterEachMigration } = options;
|
|
313
|
+
if (migrations.length === 0)
|
|
314
|
+
return;
|
|
315
|
+
const currentVersion = store.getMeta(META_KEY) ?? INITIAL_VERSION;
|
|
316
|
+
const versions = migrations.map((m) => m.version);
|
|
317
|
+
const finalVersion = projectVersion;
|
|
318
|
+
const pending = migrations.filter((m) => compareVersions(m.version, currentVersion) > 0);
|
|
319
|
+
if (pending.length === 0) {
|
|
320
|
+
store.setMeta(META_KEY, projectVersion);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
pending.sort((a, b) => compareVersions(a.version, b.version));
|
|
324
|
+
const ctx = createMigrationContext(store);
|
|
325
|
+
const snapshot = store.getAll();
|
|
326
|
+
const snapshotVersion = currentVersion;
|
|
327
|
+
try {
|
|
328
|
+
for (const migration of pending) {
|
|
329
|
+
const hookCtx = {
|
|
330
|
+
fromVersion: currentVersion,
|
|
331
|
+
toVersion: migration.version,
|
|
332
|
+
finalVersion,
|
|
333
|
+
versions
|
|
334
|
+
};
|
|
335
|
+
if (beforeEachMigration) {
|
|
336
|
+
await beforeEachMigration(hookCtx);
|
|
337
|
+
}
|
|
338
|
+
await migration.up(ctx);
|
|
339
|
+
if (afterEachMigration) {
|
|
340
|
+
await afterEachMigration(hookCtx);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
store.setMeta(META_KEY, projectVersion);
|
|
344
|
+
store.touchLastWrite();
|
|
345
|
+
} catch (error) {
|
|
346
|
+
store.transaction(() => {
|
|
347
|
+
store.deleteAll();
|
|
348
|
+
const entries = Object.entries(snapshot);
|
|
349
|
+
if (entries.length > 0) {
|
|
350
|
+
store.setMany(entries);
|
|
351
|
+
}
|
|
352
|
+
store.setMeta(META_KEY, snapshotVersion);
|
|
353
|
+
});
|
|
354
|
+
throw new Error(`Migration to version "${pending.find(() => true)?.version}" failed. ` + `All changes have been rolled back. Original error: ${error instanceof Error ? error.message : String(error)}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function createMigrationContext(store) {
|
|
358
|
+
return {
|
|
359
|
+
get(key) {
|
|
360
|
+
return store.getOne(key);
|
|
361
|
+
},
|
|
362
|
+
set(key, value) {
|
|
363
|
+
store.setOne(key, value);
|
|
364
|
+
},
|
|
365
|
+
has(key) {
|
|
366
|
+
return store.has(key);
|
|
367
|
+
},
|
|
368
|
+
delete(key) {
|
|
369
|
+
return store.deleteOne(key);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function compareVersions(a, b) {
|
|
374
|
+
const partsA = a.split(".").map(Number);
|
|
375
|
+
const partsB = b.split(".").map(Number);
|
|
376
|
+
for (let i = 0;i < 3; i++) {
|
|
377
|
+
const va = partsA[i] ?? 0;
|
|
378
|
+
const vb = partsB[i] ?? 0;
|
|
379
|
+
if (va !== vb)
|
|
380
|
+
return va - vb;
|
|
381
|
+
}
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/platform.ts
|
|
386
|
+
import { homedir, platform } from "node:os";
|
|
387
|
+
import { join } from "node:path";
|
|
388
|
+
function resolveConfigDir(projectName) {
|
|
389
|
+
const home = homedir();
|
|
390
|
+
const os = platform();
|
|
391
|
+
switch (os) {
|
|
392
|
+
case "darwin":
|
|
393
|
+
return join(home, "Library", "Preferences", projectName);
|
|
394
|
+
case "win32":
|
|
395
|
+
return join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), projectName);
|
|
396
|
+
default:
|
|
397
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(home, ".config"), projectName);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function resolveConfigPath(options) {
|
|
401
|
+
const dir = options.cwd ?? resolveConfigDir(options.projectName);
|
|
402
|
+
const name = options.configName ?? "config";
|
|
403
|
+
return join(dir, `${name}.db`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/runtime.ts
|
|
407
|
+
function isBun() {
|
|
408
|
+
return typeof globalThis.Bun !== "undefined";
|
|
409
|
+
}
|
|
410
|
+
function openDatabase(filepath) {
|
|
411
|
+
if (isBun()) {
|
|
412
|
+
return openBunDatabase(filepath);
|
|
413
|
+
}
|
|
414
|
+
return openNodeDatabase(filepath);
|
|
415
|
+
}
|
|
416
|
+
function openBunDatabase(filepath) {
|
|
417
|
+
const { Database } = __require("bun:sqlite");
|
|
418
|
+
const db = new Database(filepath);
|
|
419
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
420
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
421
|
+
return {
|
|
422
|
+
prepare(sql) {
|
|
423
|
+
const stmt = db.prepare(sql);
|
|
424
|
+
return {
|
|
425
|
+
run(...params) {
|
|
426
|
+
stmt.run(...params);
|
|
427
|
+
},
|
|
428
|
+
get(...params) {
|
|
429
|
+
return stmt.get(...params);
|
|
430
|
+
},
|
|
431
|
+
all(...params) {
|
|
432
|
+
return stmt.all(...params);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
},
|
|
436
|
+
exec(sql) {
|
|
437
|
+
db.exec(sql);
|
|
438
|
+
},
|
|
439
|
+
close() {
|
|
440
|
+
db.close();
|
|
441
|
+
},
|
|
442
|
+
transaction(fn) {
|
|
443
|
+
return db.transaction(fn);
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function openNodeDatabase(filepath) {
|
|
448
|
+
let BetterSqlite3;
|
|
449
|
+
try {
|
|
450
|
+
BetterSqlite3 = __require("better-sqlite3");
|
|
451
|
+
} catch {
|
|
452
|
+
throw new Error('config-engine requires "better-sqlite3" as a peer dependency when running on Node.js. ' + "Install it with: npm install better-sqlite3");
|
|
453
|
+
}
|
|
454
|
+
const db = new BetterSqlite3(filepath);
|
|
455
|
+
db.pragma("journal_mode = WAL");
|
|
456
|
+
db.pragma("busy_timeout = 5000");
|
|
457
|
+
return {
|
|
458
|
+
prepare(sql) {
|
|
459
|
+
const stmt = db.prepare(sql);
|
|
460
|
+
return {
|
|
461
|
+
run(...params) {
|
|
462
|
+
stmt.run(...params);
|
|
463
|
+
},
|
|
464
|
+
get(...params) {
|
|
465
|
+
return stmt.get(...params);
|
|
466
|
+
},
|
|
467
|
+
all(...params) {
|
|
468
|
+
return stmt.all(...params);
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
},
|
|
472
|
+
exec(sql) {
|
|
473
|
+
db.exec(sql);
|
|
474
|
+
},
|
|
475
|
+
close() {
|
|
476
|
+
db.close();
|
|
477
|
+
},
|
|
478
|
+
transaction(fn) {
|
|
479
|
+
return db.transaction(fn);
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/store.ts
|
|
485
|
+
class ConfigStore {
|
|
486
|
+
#db;
|
|
487
|
+
constructor(db) {
|
|
488
|
+
this.#db = db;
|
|
489
|
+
this.#initialize();
|
|
490
|
+
}
|
|
491
|
+
#initialize() {
|
|
492
|
+
this.#db.exec(`
|
|
493
|
+
CREATE TABLE IF NOT EXISTS config (
|
|
494
|
+
key TEXT PRIMARY KEY,
|
|
495
|
+
value TEXT NOT NULL
|
|
496
|
+
);
|
|
497
|
+
`);
|
|
498
|
+
this.#db.exec(`
|
|
499
|
+
CREATE TABLE IF NOT EXISTS _meta (
|
|
500
|
+
key TEXT PRIMARY KEY,
|
|
501
|
+
value TEXT NOT NULL
|
|
502
|
+
);
|
|
503
|
+
`);
|
|
504
|
+
}
|
|
505
|
+
getAll() {
|
|
506
|
+
const rows = this.#db.prepare("SELECT key, value FROM config").all();
|
|
507
|
+
const result = Object.create(null);
|
|
508
|
+
for (const row of rows) {
|
|
509
|
+
result[row.key] = JSON.parse(row.value);
|
|
510
|
+
}
|
|
511
|
+
return result;
|
|
512
|
+
}
|
|
513
|
+
getOne(key) {
|
|
514
|
+
const row = this.#db.prepare("SELECT value FROM config WHERE key = ?").get(key);
|
|
515
|
+
return row ? JSON.parse(row.value) : undefined;
|
|
516
|
+
}
|
|
517
|
+
setOne(key, value) {
|
|
518
|
+
this.#db.prepare("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)").run(key, JSON.stringify(value));
|
|
519
|
+
}
|
|
520
|
+
setMany(entries) {
|
|
521
|
+
const stmt = this.#db.prepare("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)");
|
|
522
|
+
const runTransaction = this.#db.transaction(() => {
|
|
523
|
+
for (const [key, value] of entries) {
|
|
524
|
+
stmt.run(key, JSON.stringify(value));
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
runTransaction();
|
|
528
|
+
}
|
|
529
|
+
deleteOne(key) {
|
|
530
|
+
const before = this.count();
|
|
531
|
+
this.#db.prepare("DELETE FROM config WHERE key = ?").run(key);
|
|
532
|
+
return this.count() < before;
|
|
533
|
+
}
|
|
534
|
+
deleteAll() {
|
|
535
|
+
this.#db.exec("DELETE FROM config;");
|
|
536
|
+
}
|
|
537
|
+
has(key) {
|
|
538
|
+
const row = this.#db.prepare("SELECT 1 FROM config WHERE key = ? LIMIT 1").get(key);
|
|
539
|
+
return row !== undefined && row !== null;
|
|
540
|
+
}
|
|
541
|
+
count() {
|
|
542
|
+
const row = this.#db.prepare("SELECT COUNT(*) as cnt FROM config").get();
|
|
543
|
+
return row.cnt;
|
|
544
|
+
}
|
|
545
|
+
getMeta(key) {
|
|
546
|
+
const row = this.#db.prepare("SELECT value FROM _meta WHERE key = ?").get(key);
|
|
547
|
+
return row?.value;
|
|
548
|
+
}
|
|
549
|
+
setMeta(key, value) {
|
|
550
|
+
this.#db.prepare("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)").run(key, value);
|
|
551
|
+
}
|
|
552
|
+
hasMeta(key) {
|
|
553
|
+
const row = this.#db.prepare("SELECT 1 FROM _meta WHERE key = ? LIMIT 1").get(key);
|
|
554
|
+
return row !== undefined && row !== null;
|
|
555
|
+
}
|
|
556
|
+
transaction(fn) {
|
|
557
|
+
const wrapped = this.#db.transaction(fn);
|
|
558
|
+
return wrapped();
|
|
559
|
+
}
|
|
560
|
+
touchLastWrite() {
|
|
561
|
+
this.setMeta("last_write", Date.now().toString());
|
|
562
|
+
}
|
|
563
|
+
getLastWrite() {
|
|
564
|
+
const val = this.getMeta("last_write");
|
|
565
|
+
return val ? Number(val) : 0;
|
|
566
|
+
}
|
|
567
|
+
close() {
|
|
568
|
+
this.#db.close();
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/index.ts
|
|
573
|
+
class ConfigEngineError extends Error {
|
|
574
|
+
name = "ConfigEngineError";
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
class ValidationError extends ConfigEngineError {
|
|
578
|
+
name = "ValidationError";
|
|
579
|
+
errors;
|
|
580
|
+
constructor(errors) {
|
|
581
|
+
super(`Config validation failed:
|
|
582
|
+
- ${errors.join(`
|
|
583
|
+
- `)}`);
|
|
584
|
+
this.errors = errors;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
class ConfigEngine {
|
|
589
|
+
#store;
|
|
590
|
+
#cache;
|
|
591
|
+
#validator;
|
|
592
|
+
#encryptor;
|
|
593
|
+
#defaults;
|
|
594
|
+
#dotNotation;
|
|
595
|
+
#filePath;
|
|
596
|
+
#listeners = new Map;
|
|
597
|
+
#anyListeners = new Set;
|
|
598
|
+
#watcher = null;
|
|
599
|
+
#lastKnownWrite = 0;
|
|
600
|
+
#closed = false;
|
|
601
|
+
constructor(store, cache, options) {
|
|
602
|
+
this.#store = store;
|
|
603
|
+
this.#cache = cache;
|
|
604
|
+
this.#validator = options.validator;
|
|
605
|
+
this.#encryptor = options.encryptor;
|
|
606
|
+
this.#defaults = options.defaults;
|
|
607
|
+
this.#dotNotation = options.dotNotation;
|
|
608
|
+
this.#filePath = options.filePath;
|
|
609
|
+
this.#lastKnownWrite = store.getLastWrite();
|
|
610
|
+
if (options.watch) {
|
|
611
|
+
this.#startWatching();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
static async open(options) {
|
|
615
|
+
const {
|
|
616
|
+
projectName,
|
|
617
|
+
projectVersion,
|
|
618
|
+
cwd,
|
|
619
|
+
configName,
|
|
620
|
+
defaults = {},
|
|
621
|
+
encryptionKey,
|
|
622
|
+
migrations,
|
|
623
|
+
beforeEachMigration,
|
|
624
|
+
afterEachMigration,
|
|
625
|
+
flushStrategy = "batched",
|
|
626
|
+
accessPropertiesByDotNotation = true,
|
|
627
|
+
watch: enableWatch = false,
|
|
628
|
+
clearInvalidConfig = false
|
|
629
|
+
} = options;
|
|
630
|
+
const filePath = resolveConfigPath({ projectName, cwd, configName });
|
|
631
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
632
|
+
const db = openDatabase(filePath);
|
|
633
|
+
const store = new ConfigStore(db);
|
|
634
|
+
const encryptor = await resolveEncryptor(encryptionKey);
|
|
635
|
+
const validator = resolveValidator(options);
|
|
636
|
+
let storedData;
|
|
637
|
+
try {
|
|
638
|
+
storedData = store.getAll();
|
|
639
|
+
if (encryptor) {
|
|
640
|
+
storedData = await decryptStore(storedData, encryptor);
|
|
641
|
+
}
|
|
642
|
+
} catch (error) {
|
|
643
|
+
if (clearInvalidConfig) {
|
|
644
|
+
store.deleteAll();
|
|
645
|
+
storedData = {};
|
|
646
|
+
} else {
|
|
647
|
+
store.close();
|
|
648
|
+
throw new ConfigEngineError(`Failed to load config: ${error instanceof Error ? error.message : String(error)}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const merged = { ...defaults, ...storedData };
|
|
652
|
+
if (validator) {
|
|
653
|
+
const result = validator.validate(merged);
|
|
654
|
+
if (!result.success) {
|
|
655
|
+
if (clearInvalidConfig) {
|
|
656
|
+
store.deleteAll();
|
|
657
|
+
const freshData = { ...defaults };
|
|
658
|
+
const freshResult = validator.validate(freshData);
|
|
659
|
+
if (!freshResult.success) {
|
|
660
|
+
store.close();
|
|
661
|
+
throw new ValidationError(freshResult.errors);
|
|
662
|
+
}
|
|
663
|
+
const entries = Object.entries(freshData);
|
|
664
|
+
if (encryptor) {
|
|
665
|
+
const encrypted = await encryptStore(freshData, encryptor);
|
|
666
|
+
store.setMany(Object.entries(encrypted));
|
|
667
|
+
} else {
|
|
668
|
+
store.setMany(entries);
|
|
669
|
+
}
|
|
670
|
+
store.touchLastWrite();
|
|
671
|
+
const cache2 = new ConfigCache(store, flushStrategy);
|
|
672
|
+
cache2.load(freshData);
|
|
673
|
+
return new ConfigEngine(store, cache2, {
|
|
674
|
+
validator,
|
|
675
|
+
encryptor,
|
|
676
|
+
defaults,
|
|
677
|
+
dotNotation: accessPropertiesByDotNotation,
|
|
678
|
+
filePath,
|
|
679
|
+
watch: enableWatch
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
store.close();
|
|
683
|
+
throw new ValidationError(result.errors);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const hasNewDefaults = Object.keys(defaults).some((key) => !(key in storedData));
|
|
687
|
+
if (hasNewDefaults) {
|
|
688
|
+
if (encryptor) {
|
|
689
|
+
const encrypted = await encryptStore(merged, encryptor);
|
|
690
|
+
store.setMany(Object.entries(encrypted));
|
|
691
|
+
} else {
|
|
692
|
+
store.setMany(Object.entries(merged));
|
|
693
|
+
}
|
|
694
|
+
store.touchLastWrite();
|
|
695
|
+
}
|
|
696
|
+
if (migrations && migrations.length > 0) {
|
|
697
|
+
if (!projectVersion) {
|
|
698
|
+
store.close();
|
|
699
|
+
throw new ConfigEngineError('"projectVersion" is required when "migrations" is provided.');
|
|
700
|
+
}
|
|
701
|
+
await runMigrations({
|
|
702
|
+
store,
|
|
703
|
+
migrations,
|
|
704
|
+
projectVersion,
|
|
705
|
+
beforeEachMigration,
|
|
706
|
+
afterEachMigration
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
const cache = new ConfigCache(store, flushStrategy);
|
|
710
|
+
let finalData;
|
|
711
|
+
if (encryptor) {
|
|
712
|
+
finalData = await decryptStore(store.getAll(), encryptor);
|
|
713
|
+
} else {
|
|
714
|
+
finalData = store.getAll();
|
|
715
|
+
}
|
|
716
|
+
const cacheData = { ...defaults, ...finalData };
|
|
717
|
+
cache.load(cacheData);
|
|
718
|
+
return new ConfigEngine(store, cache, {
|
|
719
|
+
validator,
|
|
720
|
+
encryptor,
|
|
721
|
+
defaults,
|
|
722
|
+
dotNotation: accessPropertiesByDotNotation,
|
|
723
|
+
filePath,
|
|
724
|
+
watch: enableWatch
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
get(key, defaultValue) {
|
|
728
|
+
this.#ensureOpen();
|
|
729
|
+
if (this.#dotNotation && key.includes(".")) {
|
|
730
|
+
const full = this.#cache.getAll();
|
|
731
|
+
const value2 = getByPath(full, key);
|
|
732
|
+
return value2 !== undefined ? value2 : defaultValue;
|
|
733
|
+
}
|
|
734
|
+
const value = this.#cache.get(key);
|
|
735
|
+
return value !== undefined ? value : defaultValue;
|
|
736
|
+
}
|
|
737
|
+
has(key) {
|
|
738
|
+
this.#ensureOpen();
|
|
739
|
+
if (this.#dotNotation && key.includes(".")) {
|
|
740
|
+
return hasByPath(this.#cache.getAll(), key);
|
|
741
|
+
}
|
|
742
|
+
return this.#cache.has(key);
|
|
743
|
+
}
|
|
744
|
+
get store() {
|
|
745
|
+
this.#ensureOpen();
|
|
746
|
+
return this.#cache.getAll();
|
|
747
|
+
}
|
|
748
|
+
set store(value) {
|
|
749
|
+
this.#ensureOpen();
|
|
750
|
+
this.#validateFull(value);
|
|
751
|
+
const oldStore = this.#cache.getAll();
|
|
752
|
+
this.#cache.replaceAll(value);
|
|
753
|
+
this.#notifyAnyChange(value, oldStore);
|
|
754
|
+
}
|
|
755
|
+
get size() {
|
|
756
|
+
this.#ensureOpen();
|
|
757
|
+
return this.#cache.size;
|
|
758
|
+
}
|
|
759
|
+
get path() {
|
|
760
|
+
return this.#filePath;
|
|
761
|
+
}
|
|
762
|
+
set(keyOrObject, value) {
|
|
763
|
+
this.#ensureOpen();
|
|
764
|
+
if (typeof keyOrObject === "string") {
|
|
765
|
+
this.#setKey(keyOrObject, value);
|
|
766
|
+
} else {
|
|
767
|
+
const entries = Object.entries(keyOrObject);
|
|
768
|
+
const oldStore = this.#cache.getAll();
|
|
769
|
+
for (const [key, val] of entries) {
|
|
770
|
+
if (this.#dotNotation && key.includes(".")) {
|
|
771
|
+
const full = this.#cache.getAll();
|
|
772
|
+
const updated = setByPath(full, key, val);
|
|
773
|
+
this.#validateFull(updated);
|
|
774
|
+
this.#cache.replaceAll(updated);
|
|
775
|
+
} else {
|
|
776
|
+
this.#cache.set(key, val);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
this.#validateFull(this.#cache.getAll());
|
|
780
|
+
this.#notifyAnyChange(this.#cache.getAll(), oldStore);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
#setKey(key, value) {
|
|
784
|
+
const oldStore = this.#cache.getAll();
|
|
785
|
+
const oldValue = this.get(key);
|
|
786
|
+
if (this.#dotNotation && key.includes(".")) {
|
|
787
|
+
const full = this.#cache.getAll();
|
|
788
|
+
const updated = setByPath(full, key, value);
|
|
789
|
+
this.#validateFull(updated);
|
|
790
|
+
this.#cache.replaceAll(updated);
|
|
791
|
+
} else {
|
|
792
|
+
const testStore = { ...this.#cache.getAll(), [key]: value };
|
|
793
|
+
this.#validateFull(testStore);
|
|
794
|
+
this.#cache.set(key, value);
|
|
795
|
+
}
|
|
796
|
+
this.#notifyKeyChange(key, value, oldValue);
|
|
797
|
+
this.#notifyAnyChange(this.#cache.getAll(), oldStore);
|
|
798
|
+
}
|
|
799
|
+
delete(key) {
|
|
800
|
+
this.#ensureOpen();
|
|
801
|
+
const oldStore = this.#cache.getAll();
|
|
802
|
+
const oldValue = this.get(key);
|
|
803
|
+
if (this.#dotNotation && key.includes(".")) {
|
|
804
|
+
const full = this.#cache.getAll();
|
|
805
|
+
const updated = deleteByPath(full, key);
|
|
806
|
+
this.#cache.replaceAll(updated);
|
|
807
|
+
} else {
|
|
808
|
+
this.#cache.delete(key);
|
|
809
|
+
}
|
|
810
|
+
this.#notifyKeyChange(key, undefined, oldValue);
|
|
811
|
+
this.#notifyAnyChange(this.#cache.getAll(), oldStore);
|
|
812
|
+
}
|
|
813
|
+
clear() {
|
|
814
|
+
this.#ensureOpen();
|
|
815
|
+
const oldStore = this.#cache.getAll();
|
|
816
|
+
this.#cache.clear();
|
|
817
|
+
if (this.#defaults && Object.keys(this.#defaults).length > 0) {
|
|
818
|
+
this.#cache.setMany(Object.entries(this.#defaults));
|
|
819
|
+
}
|
|
820
|
+
this.#notifyAnyChange(this.#cache.getAll(), oldStore);
|
|
821
|
+
}
|
|
822
|
+
reset(...keys) {
|
|
823
|
+
this.#ensureOpen();
|
|
824
|
+
const oldStore = this.#cache.getAll();
|
|
825
|
+
for (const key of keys) {
|
|
826
|
+
if (key in this.#defaults) {
|
|
827
|
+
this.#cache.set(key, this.#defaults[key]);
|
|
828
|
+
} else {
|
|
829
|
+
this.#cache.delete(key);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
this.#notifyAnyChange(this.#cache.getAll(), oldStore);
|
|
833
|
+
}
|
|
834
|
+
onDidChange(key, callback) {
|
|
835
|
+
if (!this.#listeners.has(key)) {
|
|
836
|
+
this.#listeners.set(key, new Set);
|
|
837
|
+
}
|
|
838
|
+
const set = this.#listeners.get(key);
|
|
839
|
+
set.add(callback);
|
|
840
|
+
return () => {
|
|
841
|
+
set.delete(callback);
|
|
842
|
+
if (set.size === 0)
|
|
843
|
+
this.#listeners.delete(key);
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
onDidAnyChange(callback) {
|
|
847
|
+
this.#anyListeners.add(callback);
|
|
848
|
+
return () => {
|
|
849
|
+
this.#anyListeners.delete(callback);
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
async flush() {
|
|
853
|
+
this.#ensureOpen();
|
|
854
|
+
await this.#cache.flush();
|
|
855
|
+
}
|
|
856
|
+
flushSync() {
|
|
857
|
+
this.#ensureOpen();
|
|
858
|
+
this.#cache.flushSync();
|
|
859
|
+
}
|
|
860
|
+
close() {
|
|
861
|
+
if (this.#closed)
|
|
862
|
+
return;
|
|
863
|
+
this.#stopWatching();
|
|
864
|
+
this.#cache.flushSync();
|
|
865
|
+
this.#store.close();
|
|
866
|
+
this.#closed = true;
|
|
867
|
+
}
|
|
868
|
+
*[Symbol.iterator]() {
|
|
869
|
+
this.#ensureOpen();
|
|
870
|
+
yield* this.#cache.entries();
|
|
871
|
+
}
|
|
872
|
+
#ensureOpen() {
|
|
873
|
+
if (this.#closed) {
|
|
874
|
+
throw new ConfigEngineError("This ConfigEngine instance has been closed. Create a new one with ConfigEngine.open().");
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
#validateFull(data) {
|
|
878
|
+
if (!this.#validator)
|
|
879
|
+
return;
|
|
880
|
+
const result = this.#validator.validate(data);
|
|
881
|
+
if (!result.success) {
|
|
882
|
+
throw new ValidationError(result.errors);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
#notifyKeyChange(key, newValue, oldValue) {
|
|
886
|
+
if (newValue === oldValue)
|
|
887
|
+
return;
|
|
888
|
+
if (typeof newValue === "object" && typeof oldValue === "object" && JSON.stringify(newValue) === JSON.stringify(oldValue)) {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const listeners = this.#listeners.get(key);
|
|
892
|
+
if (listeners) {
|
|
893
|
+
for (const cb of listeners) {
|
|
894
|
+
cb(newValue, oldValue);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
#notifyAnyChange(newStore, oldStore) {
|
|
899
|
+
if (JSON.stringify(newStore) === JSON.stringify(oldStore))
|
|
900
|
+
return;
|
|
901
|
+
for (const cb of this.#anyListeners) {
|
|
902
|
+
cb(newStore, oldStore);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
#startWatching() {
|
|
906
|
+
try {
|
|
907
|
+
this.#watcher = watch(dirname(this.#filePath), (_event, filename) => {
|
|
908
|
+
if (!filename)
|
|
909
|
+
return;
|
|
910
|
+
const expectedName = this.#filePath.split(/[\\/]/).pop();
|
|
911
|
+
if (filename !== expectedName)
|
|
912
|
+
return;
|
|
913
|
+
const currentWrite = this.#store.getLastWrite();
|
|
914
|
+
if (currentWrite > this.#lastKnownWrite) {
|
|
915
|
+
this.#lastKnownWrite = currentWrite;
|
|
916
|
+
const oldStore = this.#cache.getAll();
|
|
917
|
+
this.#cache.reload();
|
|
918
|
+
const newStore = this.#cache.getAll();
|
|
919
|
+
this.#notifyAnyChange(newStore, oldStore);
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
if (this.#watcher && "unref" in this.#watcher) {
|
|
923
|
+
this.#watcher.unref();
|
|
924
|
+
}
|
|
925
|
+
} catch {}
|
|
926
|
+
}
|
|
927
|
+
#stopWatching() {
|
|
928
|
+
if (this.#watcher) {
|
|
929
|
+
this.#watcher.close();
|
|
930
|
+
this.#watcher = null;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
async function encryptStore(data, encryptor) {
|
|
935
|
+
const result = {};
|
|
936
|
+
for (const [key, value] of Object.entries(data)) {
|
|
937
|
+
result[key] = await encryptor.encrypt(JSON.stringify(value));
|
|
938
|
+
}
|
|
939
|
+
return result;
|
|
940
|
+
}
|
|
941
|
+
async function decryptStore(data, encryptor) {
|
|
942
|
+
const result = {};
|
|
943
|
+
for (const [key, value] of Object.entries(data)) {
|
|
944
|
+
if (typeof value === "string") {
|
|
945
|
+
try {
|
|
946
|
+
const decrypted = await encryptor.decrypt(value);
|
|
947
|
+
result[key] = JSON.parse(decrypted);
|
|
948
|
+
} catch {
|
|
949
|
+
result[key] = value;
|
|
950
|
+
}
|
|
951
|
+
} else {
|
|
952
|
+
result[key] = value;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return result;
|
|
956
|
+
}
|
|
957
|
+
export {
|
|
958
|
+
resolveValidator,
|
|
959
|
+
resolveConfigPath,
|
|
960
|
+
resolveConfigDir,
|
|
961
|
+
createZodValidator,
|
|
962
|
+
ValidationError,
|
|
963
|
+
ConfigEngineError,
|
|
964
|
+
ConfigEngine
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
//# debugId=61711D0C75DC415164756E2164756E21
|