@effected/config-file 0.19.1 → 0.20.1
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/ConfigFile.js +41 -45
- package/ConfigMigration.js +18 -19
- package/ConfigResolver.js +63 -69
- package/MergeStrategy.js +34 -35
- package/README.md +20 -13
- package/index.d.ts +27 -55
- package/index.js +1 -4
- package/jsonc.d.ts +17 -0
- package/jsonc.js +3 -0
- package/package.json +29 -3
- package/toml.d.ts +21 -0
- package/toml.js +3 -0
- package/tsdoc-metadata.json +1 -1
- package/yaml.d.ts +16 -0
- package/yaml.js +3 -0
package/ConfigFile.js
CHANGED
|
@@ -114,7 +114,6 @@ var ConfigValidationError = class extends Schema.TaggedError()("ConfigValidation
|
|
|
114
114
|
})}`;
|
|
115
115
|
}
|
|
116
116
|
};
|
|
117
|
-
const Service = () => (id) => Context.Service()(id);
|
|
118
117
|
/**
|
|
119
118
|
* Re-raise a codec failure with the file it came from attached.
|
|
120
119
|
*
|
|
@@ -401,46 +400,6 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
401
400
|
update
|
|
402
401
|
};
|
|
403
402
|
};
|
|
404
|
-
const layer = (tag, options) => Layer.effect(tag, Effect.gen(function* () {
|
|
405
|
-
const fs = yield* FileSystem.FileSystem;
|
|
406
|
-
const path = yield* Path.Path;
|
|
407
|
-
const resolverEnv = yield* Effect.context();
|
|
408
|
-
return makeImpl(options, fs, path, resolverEnv);
|
|
409
|
-
}));
|
|
410
|
-
const testLayer = (tag, options) => Layer.effect(tag, Effect.gen(function* () {
|
|
411
|
-
const fs = yield* FileSystem.FileSystem;
|
|
412
|
-
const path = yield* Path.Path;
|
|
413
|
-
const resolverEnv = yield* Effect.context();
|
|
414
|
-
const dir = yield* fs.makeTempDirectory({ prefix: "effected-config-file-" }).pipe(Effect.orDie);
|
|
415
|
-
yield* Effect.addFinalizer(() => fs.remove(dir, { recursive: true }).pipe(Effect.orDie));
|
|
416
|
-
for (const [name, content] of Object.entries(options.files)) {
|
|
417
|
-
const target = path.join(dir, name);
|
|
418
|
-
yield* fs.makeDirectory(path.dirname(target), { recursive: true }).pipe(Effect.orDie);
|
|
419
|
-
yield* fs.writeFileString(target, content).pipe(Effect.orDie);
|
|
420
|
-
}
|
|
421
|
-
const resolvers = Object.keys(options.files).map((name) => ConfigResolver.staticDir({
|
|
422
|
-
dir,
|
|
423
|
-
filename: name
|
|
424
|
-
}));
|
|
425
|
-
return makeImpl({
|
|
426
|
-
schema: options.schema,
|
|
427
|
-
codec: options.codec,
|
|
428
|
-
strategy: options.strategy,
|
|
429
|
-
resolvers,
|
|
430
|
-
...options.validate !== void 0 && { validate: options.validate }
|
|
431
|
-
}, fs, path, resolverEnv);
|
|
432
|
-
}));
|
|
433
|
-
const read = (path, options) => Effect.gen(function* () {
|
|
434
|
-
const raw = yield* (yield* FileSystem.FileSystem).readFileString(path).pipe(Effect.mapError((cause) => new ConfigFileReadError({
|
|
435
|
-
path,
|
|
436
|
-
cause
|
|
437
|
-
})));
|
|
438
|
-
const parsed = yield* options.codec.parse(raw).pipe(Effect.mapError((error) => withCodecPath(error, path)));
|
|
439
|
-
return yield* Schema.decodeUnknownEffect(options.schema)(parsed, options.parseOptions).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new ConfigValidationError({
|
|
440
|
-
path: Option.some(path),
|
|
441
|
-
issue: error.issue
|
|
442
|
-
}))));
|
|
443
|
-
}).pipe(Effect.withSpan("ConfigFile.read", { attributes: { path } }));
|
|
444
403
|
/**
|
|
445
404
|
* The config file service: a per-schema service factory, its layers, and the
|
|
446
405
|
* one-shot {@link ConfigFile.read}.
|
|
@@ -471,7 +430,7 @@ var ConfigFile = class {
|
|
|
471
430
|
* >()("app/Secrets") {}
|
|
472
431
|
* ```
|
|
473
432
|
*/
|
|
474
|
-
static Service = Service;
|
|
433
|
+
static Service = () => (id) => Context.Service()(id);
|
|
475
434
|
/**
|
|
476
435
|
* Build the live layer for a config service class.
|
|
477
436
|
*
|
|
@@ -509,7 +468,12 @@ var ConfigFile = class {
|
|
|
509
468
|
* });
|
|
510
469
|
* ```
|
|
511
470
|
*/
|
|
512
|
-
static layer =
|
|
471
|
+
static layer = (tag, options) => Layer.effect(tag, Effect.gen(function* () {
|
|
472
|
+
const fs = yield* FileSystem.FileSystem;
|
|
473
|
+
const path = yield* Path.Path;
|
|
474
|
+
const resolverEnv = yield* Effect.context();
|
|
475
|
+
return makeImpl(options, fs, path, resolverEnv);
|
|
476
|
+
}));
|
|
513
477
|
/**
|
|
514
478
|
* A scoped layer that seeds `files` into a temp directory, wires the **real**
|
|
515
479
|
* live implementation over them, and removes the directory when the scope
|
|
@@ -543,7 +507,29 @@ var ConfigFile = class {
|
|
|
543
507
|
* }).pipe(Layer.provide(NodeServices.layer));
|
|
544
508
|
* ```
|
|
545
509
|
*/
|
|
546
|
-
static testLayer =
|
|
510
|
+
static testLayer = (tag, options) => Layer.effect(tag, Effect.gen(function* () {
|
|
511
|
+
const fs = yield* FileSystem.FileSystem;
|
|
512
|
+
const path = yield* Path.Path;
|
|
513
|
+
const resolverEnv = yield* Effect.context();
|
|
514
|
+
const dir = yield* fs.makeTempDirectory({ prefix: "effected-config-file-" }).pipe(Effect.orDie);
|
|
515
|
+
yield* Effect.addFinalizer(() => fs.remove(dir, { recursive: true }).pipe(Effect.orDie));
|
|
516
|
+
for (const [name, content] of Object.entries(options.files)) {
|
|
517
|
+
const target = path.join(dir, name);
|
|
518
|
+
yield* fs.makeDirectory(path.dirname(target), { recursive: true }).pipe(Effect.orDie);
|
|
519
|
+
yield* fs.writeFileString(target, content).pipe(Effect.orDie);
|
|
520
|
+
}
|
|
521
|
+
const resolvers = Object.keys(options.files).map((name) => ConfigResolver.staticDir({
|
|
522
|
+
dir,
|
|
523
|
+
filename: name
|
|
524
|
+
}));
|
|
525
|
+
return makeImpl({
|
|
526
|
+
schema: options.schema,
|
|
527
|
+
codec: options.codec,
|
|
528
|
+
strategy: options.strategy,
|
|
529
|
+
resolvers,
|
|
530
|
+
...options.validate !== void 0 && { validate: options.validate }
|
|
531
|
+
}, fs, path, resolverEnv);
|
|
532
|
+
}));
|
|
547
533
|
/**
|
|
548
534
|
* Read, decode and validate one explicit path — no service, no layer, no tag.
|
|
549
535
|
*
|
|
@@ -581,7 +567,17 @@ var ConfigFile = class {
|
|
|
581
567
|
* });
|
|
582
568
|
* ```
|
|
583
569
|
*/
|
|
584
|
-
static read =
|
|
570
|
+
static read = (path, options) => Effect.gen(function* () {
|
|
571
|
+
const raw = yield* (yield* FileSystem.FileSystem).readFileString(path).pipe(Effect.mapError((cause) => new ConfigFileReadError({
|
|
572
|
+
path,
|
|
573
|
+
cause
|
|
574
|
+
})));
|
|
575
|
+
const parsed = yield* options.codec.parse(raw).pipe(Effect.mapError((error) => withCodecPath(error, path)));
|
|
576
|
+
return yield* Schema.decodeUnknownEffect(options.schema)(parsed, options.parseOptions).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new ConfigValidationError({
|
|
577
|
+
path: Option.some(path),
|
|
578
|
+
issue: error.issue
|
|
579
|
+
}))));
|
|
580
|
+
}).pipe(Effect.withSpan("ConfigFile.read", { attributes: { path } }));
|
|
585
581
|
};
|
|
586
582
|
|
|
587
583
|
export { ConfigDefaultPathMissingError, ConfigFile, ConfigFileNotFoundError, ConfigFileReadError, ConfigFileWriteError, ConfigValidationError };
|
package/ConfigMigration.js
CHANGED
|
@@ -60,24 +60,6 @@ const runPhase = (phase, version, name, run) => Effect.suspend(run).pipe(Effect.
|
|
|
60
60
|
phase,
|
|
61
61
|
cause
|
|
62
62
|
})));
|
|
63
|
-
const make = (options) => {
|
|
64
|
-
const access = options.versionAccess ?? VersionAccess.default;
|
|
65
|
-
const sorted = [...options.migrations].sort((a, b) => a.version - b.version);
|
|
66
|
-
return {
|
|
67
|
-
name: options.codec.name,
|
|
68
|
-
stringify: options.codec.stringify,
|
|
69
|
-
parse: (raw) => Effect.gen(function* () {
|
|
70
|
-
let parsed = yield* options.codec.parse(raw);
|
|
71
|
-
if (sorted.length === 0) return parsed;
|
|
72
|
-
const current = yield* runPhase("read-version", 0, "", () => access.get(parsed));
|
|
73
|
-
for (const migration of sorted.filter((m) => m.version > current)) {
|
|
74
|
-
parsed = yield* runPhase("apply", migration.version, migration.name, () => migration.up(parsed));
|
|
75
|
-
parsed = yield* runPhase("write-version", migration.version, migration.name, () => access.set(parsed, migration.version));
|
|
76
|
-
}
|
|
77
|
-
return parsed;
|
|
78
|
-
})
|
|
79
|
-
};
|
|
80
|
-
};
|
|
81
63
|
/** Versioned migration support for config codecs. @public */
|
|
82
64
|
var ConfigMigration = class {
|
|
83
65
|
constructor() {}
|
|
@@ -111,7 +93,24 @@ var ConfigMigration = class {
|
|
|
111
93
|
* });
|
|
112
94
|
* ```
|
|
113
95
|
*/
|
|
114
|
-
static make =
|
|
96
|
+
static make = (options) => {
|
|
97
|
+
const access = options.versionAccess ?? VersionAccess.default;
|
|
98
|
+
const sorted = [...options.migrations].sort((a, b) => a.version - b.version);
|
|
99
|
+
return {
|
|
100
|
+
name: options.codec.name,
|
|
101
|
+
stringify: options.codec.stringify,
|
|
102
|
+
parse: (raw) => Effect.gen(function* () {
|
|
103
|
+
let parsed = yield* options.codec.parse(raw);
|
|
104
|
+
if (sorted.length === 0) return parsed;
|
|
105
|
+
const current = yield* runPhase("read-version", 0, "", () => access.get(parsed));
|
|
106
|
+
for (const migration of sorted.filter((m) => m.version > current)) {
|
|
107
|
+
parsed = yield* runPhase("apply", migration.version, migration.name, () => migration.up(parsed));
|
|
108
|
+
parsed = yield* runPhase("write-version", migration.version, migration.name, () => access.set(parsed, migration.version));
|
|
109
|
+
}
|
|
110
|
+
return parsed;
|
|
111
|
+
})
|
|
112
|
+
};
|
|
113
|
+
};
|
|
115
114
|
};
|
|
116
115
|
|
|
117
116
|
export { ConfigMigration, ConfigMigrationError, VersionAccess };
|
package/ConfigResolver.js
CHANGED
|
@@ -22,48 +22,6 @@ const absorb = (effect) => Effect.catch(effect, () => Effect.succeed({
|
|
|
22
22
|
probed: []
|
|
23
23
|
}));
|
|
24
24
|
const cwdOf = (given) => given ?? globalThis.process?.cwd?.() ?? "/";
|
|
25
|
-
const explicitPath = (target) => fromProbe("explicit", absorb(Effect.gen(function* () {
|
|
26
|
-
return {
|
|
27
|
-
match: (yield* (yield* FileSystem.FileSystem).exists(target)) ? Option.some({ path: target }) : Option.none(),
|
|
28
|
-
probed: [target]
|
|
29
|
-
};
|
|
30
|
-
})));
|
|
31
|
-
const staticDir = (options) => fromProbe("static", absorb(Effect.gen(function* () {
|
|
32
|
-
const fs = yield* FileSystem.FileSystem;
|
|
33
|
-
const candidate = (yield* Path.Path).join(options.dir, options.filename);
|
|
34
|
-
return {
|
|
35
|
-
match: (yield* fs.exists(candidate)) ? Option.some({
|
|
36
|
-
path: candidate,
|
|
37
|
-
dir: options.dir,
|
|
38
|
-
filename: options.filename
|
|
39
|
-
}) : Option.none(),
|
|
40
|
-
probed: [candidate]
|
|
41
|
-
};
|
|
42
|
-
})));
|
|
43
|
-
const upwardWalk = (options) => fromProbe(options.name ?? "walk", Effect.gen(function* () {
|
|
44
|
-
const fs = yield* FileSystem.FileSystem;
|
|
45
|
-
const path = yield* Path.Path;
|
|
46
|
-
const subpaths = options.subpaths ?? ["."];
|
|
47
|
-
const filenames = options.filenames ?? [options.filename];
|
|
48
|
-
const dirs = yield* Walker.ascend(cwdOf(options.cwd), { ...options.stopAt !== void 0 && { stopAt: options.stopAt } });
|
|
49
|
-
const candidates = [];
|
|
50
|
-
for (const dir of dirs) for (const sub of subpaths) for (const filename of filenames) candidates.push({
|
|
51
|
-
path: path.join(dir, sub, filename),
|
|
52
|
-
dir,
|
|
53
|
-
filename,
|
|
54
|
-
...options.subpaths !== void 0 && { subpath: sub }
|
|
55
|
-
});
|
|
56
|
-
const paths = candidates.map((candidate) => candidate.path);
|
|
57
|
-
const found = yield* Walker.firstMatch(paths, (candidate) => fs.exists(candidate));
|
|
58
|
-
const probed = Option.match(found, {
|
|
59
|
-
onNone: () => paths,
|
|
60
|
-
onSome: (target) => paths.slice(0, paths.indexOf(target) + 1)
|
|
61
|
-
});
|
|
62
|
-
return {
|
|
63
|
-
match: Option.flatMap(found, (target) => Option.fromNullishOr(candidates.find((candidate) => candidate.path === target))),
|
|
64
|
-
probed
|
|
65
|
-
};
|
|
66
|
-
}));
|
|
67
25
|
/**
|
|
68
26
|
* Ascend from `cwd` looking for the first directory where `isRoot` reports
|
|
69
27
|
* true, then probe `subpaths` under it. Shared by `gitRoot` and
|
|
@@ -125,27 +83,6 @@ const isWorkspaceRoot = (dir) => Effect.gen(function* () {
|
|
|
125
83
|
if (yield* fs.exists(pkgPath)) return Option.isSome(decodeWorkspacesManifest(yield* fs.readFileString(pkgPath)));
|
|
126
84
|
return false;
|
|
127
85
|
});
|
|
128
|
-
const gitRoot = (options) => rootAnchored("git", isGitRoot, options);
|
|
129
|
-
const workspaceRoot = (options) => rootAnchored("workspace", isWorkspaceRoot, options);
|
|
130
|
-
const systemEtc = (options) => fromProbe("system", absorb(Effect.gen(function* () {
|
|
131
|
-
if (globalThis.process?.platform === "win32") return {
|
|
132
|
-
match: Option.none(),
|
|
133
|
-
probed: []
|
|
134
|
-
};
|
|
135
|
-
const fs = yield* FileSystem.FileSystem;
|
|
136
|
-
const path = yield* Path.Path;
|
|
137
|
-
const base = options.dir ?? "/etc";
|
|
138
|
-
const dir = path.join(base, options.app);
|
|
139
|
-
const candidate = path.join(dir, options.filename);
|
|
140
|
-
return {
|
|
141
|
-
match: (yield* fs.exists(candidate)) ? Option.some({
|
|
142
|
-
path: candidate,
|
|
143
|
-
dir,
|
|
144
|
-
filename: options.filename
|
|
145
|
-
}) : Option.none(),
|
|
146
|
-
probed: [candidate]
|
|
147
|
-
};
|
|
148
|
-
})));
|
|
149
86
|
/**
|
|
150
87
|
* Built-in resolvers, in the order a typical chain uses them.
|
|
151
88
|
*
|
|
@@ -154,9 +91,25 @@ const systemEtc = (options) => fromProbe("system", absorb(Effect.gen(function* (
|
|
|
154
91
|
var ConfigResolver = class {
|
|
155
92
|
constructor() {}
|
|
156
93
|
/** Resolves to `target` when it exists on disk, or `Option.none()` when it does not — no walking, no filename convention. */
|
|
157
|
-
static explicitPath =
|
|
94
|
+
static explicitPath = (target) => fromProbe("explicit", absorb(Effect.gen(function* () {
|
|
95
|
+
return {
|
|
96
|
+
match: (yield* (yield* FileSystem.FileSystem).exists(target)) ? Option.some({ path: target }) : Option.none(),
|
|
97
|
+
probed: [target]
|
|
98
|
+
};
|
|
99
|
+
})));
|
|
158
100
|
/** Resolves `path.join(dir, filename)` when it exists on disk, or `Option.none()` when it does not. */
|
|
159
|
-
static staticDir =
|
|
101
|
+
static staticDir = (options) => fromProbe("static", absorb(Effect.gen(function* () {
|
|
102
|
+
const fs = yield* FileSystem.FileSystem;
|
|
103
|
+
const candidate = (yield* Path.Path).join(options.dir, options.filename);
|
|
104
|
+
return {
|
|
105
|
+
match: (yield* fs.exists(candidate)) ? Option.some({
|
|
106
|
+
path: candidate,
|
|
107
|
+
dir: options.dir,
|
|
108
|
+
filename: options.filename
|
|
109
|
+
}) : Option.none(),
|
|
110
|
+
probed: [candidate]
|
|
111
|
+
};
|
|
112
|
+
})));
|
|
160
113
|
/**
|
|
161
114
|
* Ascends from `cwd` (or the process cwd) toward `stopAt`, resolving the
|
|
162
115
|
* first `subpaths/filename` combination found at each level.
|
|
@@ -172,24 +125,65 @@ var ConfigResolver = class {
|
|
|
172
125
|
* distinguishable; `ConfigSource.match` identifies the matching candidate
|
|
173
126
|
* more precisely still.
|
|
174
127
|
*/
|
|
175
|
-
static upwardWalk =
|
|
128
|
+
static upwardWalk = (options) => fromProbe(options.name ?? "walk", Effect.gen(function* () {
|
|
129
|
+
const fs = yield* FileSystem.FileSystem;
|
|
130
|
+
const path = yield* Path.Path;
|
|
131
|
+
const subpaths = options.subpaths ?? ["."];
|
|
132
|
+
const filenames = options.filenames ?? [options.filename];
|
|
133
|
+
const dirs = yield* Walker.ascend(cwdOf(options.cwd), { ...options.stopAt !== void 0 && { stopAt: options.stopAt } });
|
|
134
|
+
const candidates = [];
|
|
135
|
+
for (const dir of dirs) for (const sub of subpaths) for (const filename of filenames) candidates.push({
|
|
136
|
+
path: path.join(dir, sub, filename),
|
|
137
|
+
dir,
|
|
138
|
+
filename,
|
|
139
|
+
...options.subpaths !== void 0 && { subpath: sub }
|
|
140
|
+
});
|
|
141
|
+
const paths = candidates.map((candidate) => candidate.path);
|
|
142
|
+
const found = yield* Walker.firstMatch(paths, (candidate) => fs.exists(candidate));
|
|
143
|
+
const probed = Option.match(found, {
|
|
144
|
+
onNone: () => paths,
|
|
145
|
+
onSome: (target) => paths.slice(0, paths.indexOf(target) + 1)
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
match: Option.flatMap(found, (target) => Option.fromNullishOr(candidates.find((candidate) => candidate.path === target))),
|
|
149
|
+
probed
|
|
150
|
+
};
|
|
151
|
+
}));
|
|
176
152
|
/**
|
|
177
153
|
* Ascends from `cwd` to the nearest workspace root — a directory with a
|
|
178
154
|
* `pnpm-workspace.yaml`, or a `package.json` carrying a `workspaces` field —
|
|
179
155
|
* then resolves the first `subpaths/filename` combination found under it.
|
|
180
156
|
*/
|
|
181
|
-
static workspaceRoot =
|
|
157
|
+
static workspaceRoot = (options) => rootAnchored("workspace", isWorkspaceRoot, options);
|
|
182
158
|
/**
|
|
183
159
|
* Ascends from `cwd` to the nearest git root — a directory containing a
|
|
184
160
|
* `.git` entry, directory or file (the latter for a worktree) — then
|
|
185
161
|
* resolves the first `subpaths/filename` combination found under it.
|
|
186
162
|
*/
|
|
187
|
-
static gitRoot =
|
|
163
|
+
static gitRoot = (options) => rootAnchored("git", isGitRoot, options);
|
|
188
164
|
/**
|
|
189
165
|
* Resolves `<dir>/<app>/<filename>` under the system config root (`/etc` by
|
|
190
166
|
* default). Always `Option.none()` on Windows, where `/etc` has no meaning.
|
|
191
167
|
*/
|
|
192
|
-
static systemEtc =
|
|
168
|
+
static systemEtc = (options) => fromProbe("system", absorb(Effect.gen(function* () {
|
|
169
|
+
if (globalThis.process?.platform === "win32") return {
|
|
170
|
+
match: Option.none(),
|
|
171
|
+
probed: []
|
|
172
|
+
};
|
|
173
|
+
const fs = yield* FileSystem.FileSystem;
|
|
174
|
+
const path = yield* Path.Path;
|
|
175
|
+
const base = options.dir ?? "/etc";
|
|
176
|
+
const dir = path.join(base, options.app);
|
|
177
|
+
const candidate = path.join(dir, options.filename);
|
|
178
|
+
return {
|
|
179
|
+
match: (yield* fs.exists(candidate)) ? Option.some({
|
|
180
|
+
path: candidate,
|
|
181
|
+
dir,
|
|
182
|
+
filename: options.filename
|
|
183
|
+
}) : Option.none(),
|
|
184
|
+
probed: [candidate]
|
|
185
|
+
};
|
|
186
|
+
})));
|
|
193
187
|
};
|
|
194
188
|
|
|
195
189
|
export { ConfigResolver };
|
package/MergeStrategy.js
CHANGED
|
@@ -2,39 +2,6 @@ import { canMerge, deepMerge } from "./internal/deepMerge.js";
|
|
|
2
2
|
import { Effect } from "effect";
|
|
3
3
|
|
|
4
4
|
// src/MergeStrategy.ts
|
|
5
|
-
const firstMatch = () => ({
|
|
6
|
-
name: "first-match",
|
|
7
|
-
resolve: (sources) => Effect.succeed(sources[0].value)
|
|
8
|
-
});
|
|
9
|
-
/**
|
|
10
|
-
* Deep-merge every contributing source, higher priority winning on conflict.
|
|
11
|
-
*
|
|
12
|
-
* @remarks
|
|
13
|
-
* Two values merge only when both are record-like and share a prototype, so a
|
|
14
|
-
* document decoded through `Schema.Class` merges with another of the same class
|
|
15
|
-
* and **survives as a real instance** — `instanceof` holds and its getters
|
|
16
|
-
* still work. Everything else is atomic: a nested `Date`, `Map`, `Set`,
|
|
17
|
-
* `RegExp`, array, or class instance is taken whole from the highest-priority
|
|
18
|
-
* source that defines it, never reshaped field-by-field.
|
|
19
|
-
*
|
|
20
|
-
* The alternative — spreading each value into a fresh object — would make
|
|
21
|
-
* `load`'s declared `Effect<A>` a lie, handing back a structurally-equal plain
|
|
22
|
-
* object whose class methods are gone and whose `Date` fields have decayed to
|
|
23
|
-
* `{}`.
|
|
24
|
-
*
|
|
25
|
-
* Nested *plain* objects (a `Schema.Struct` section) still merge field-wise.
|
|
26
|
-
*/
|
|
27
|
-
const layeredMerge = () => ({
|
|
28
|
-
name: "layered-merge",
|
|
29
|
-
resolve: (sources) => {
|
|
30
|
-
let merged = sources[sources.length - 1]?.value;
|
|
31
|
-
for (let i = sources.length - 2; i >= 0; i--) {
|
|
32
|
-
const higher = sources[i]?.value;
|
|
33
|
-
merged = canMerge(merged, higher) ? deepMerge(higher, merged) : higher;
|
|
34
|
-
}
|
|
35
|
-
return Effect.succeed(merged);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
5
|
/**
|
|
39
6
|
* Built-in merge strategies: `firstMatch` (the highest-priority source wins
|
|
40
7
|
* whole) and `layeredMerge` (every source deep-merged, higher priority winning).
|
|
@@ -53,8 +20,40 @@ const layeredMerge = () => ({
|
|
|
53
20
|
* @public
|
|
54
21
|
*/
|
|
55
22
|
const MergeStrategy = {
|
|
56
|
-
|
|
57
|
-
|
|
23
|
+
/** Take the highest-priority source whole. */
|
|
24
|
+
firstMatch: () => ({
|
|
25
|
+
name: "first-match",
|
|
26
|
+
resolve: (sources) => Effect.succeed(sources[0].value)
|
|
27
|
+
}),
|
|
28
|
+
/**
|
|
29
|
+
* Deep-merge every contributing source, higher priority winning on conflict.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* Two values merge only when both are record-like and share a prototype, so a
|
|
33
|
+
* document decoded through `Schema.Class` merges with another of the same class
|
|
34
|
+
* and **survives as a real instance** — `instanceof` holds and its getters
|
|
35
|
+
* still work. Everything else is atomic: a nested `Date`, `Map`, `Set`,
|
|
36
|
+
* `RegExp`, array, or class instance is taken whole from the highest-priority
|
|
37
|
+
* source that defines it, never reshaped field-by-field.
|
|
38
|
+
*
|
|
39
|
+
* The alternative — spreading each value into a fresh object — would make
|
|
40
|
+
* `load`'s declared `Effect<A>` a lie, handing back a structurally-equal plain
|
|
41
|
+
* object whose class methods are gone and whose `Date` fields have decayed to
|
|
42
|
+
* `{}`.
|
|
43
|
+
*
|
|
44
|
+
* Nested *plain* objects (a `Schema.Struct` section) still merge field-wise.
|
|
45
|
+
*/
|
|
46
|
+
layeredMerge: () => ({
|
|
47
|
+
name: "layered-merge",
|
|
48
|
+
resolve: (sources) => {
|
|
49
|
+
let merged = sources[sources.length - 1]?.value;
|
|
50
|
+
for (let i = sources.length - 2; i >= 0; i--) {
|
|
51
|
+
const higher = sources[i]?.value;
|
|
52
|
+
merged = canMerge(merged, higher) ? deepMerge(higher, merged) : higher;
|
|
53
|
+
}
|
|
54
|
+
return Effect.succeed(merged);
|
|
55
|
+
}
|
|
56
|
+
})
|
|
58
57
|
};
|
|
59
58
|
|
|
60
59
|
export { MergeStrategy };
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://nodejs.org/)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
7
|
|
|
8
|
-
Composable config file loading for Effect. Declare a resolver chain — an explicit path, an upward walk from the cwd, the workspace or git root, `/etc` — decode every discovered file through an Effect `Schema`, and combine the results with a merge strategy. JSON
|
|
8
|
+
Composable config file loading for Effect. Declare a resolver chain — an explicit path, an upward walk from the cwd, the workspace or git root, `/etc` — decode every discovered file through an Effect `Schema`, and combine the results with a merge strategy. JSON decodes out of the box; JSONC, YAML and TOML each take one optional engine package. Codecs, resolvers and merge strategies are pluggable seams, and failures arrive as tagged errors carrying structured payloads rather than prose, so "no config anywhere" is routable separately from "the config I found is broken".
|
|
9
9
|
|
|
10
10
|
> **Pre-`1.0.0`.** This package is part of the `@effected/*` kit, built on stable
|
|
11
11
|
> Effect v4 (`effect` `^4.0.0`) and still in `0.x` development. Stable Effect
|
|
@@ -33,11 +33,17 @@ npm install @effected/config-file effect @effect/platform-node
|
|
|
33
33
|
pnpm add @effected/config-file effect @effect/platform-node
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Requires Node.js >=24.11.0.
|
|
36
|
+
Requires Node.js >=24.11.0. That install covers JSON. To read JSONC, YAML or TOML, add the matching engine:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm add @effected/jsonc # for @effected/config-file/jsonc
|
|
40
|
+
pnpm add @effected/yaml # for @effected/config-file/yaml
|
|
41
|
+
pnpm add @effected/toml # for @effected/config-file/toml
|
|
42
|
+
```
|
|
37
43
|
|
|
38
44
|
All `@effected/*` packages are ESM-only: the exports maps publish only `import` conditions, so `require()` — including tools that resolve in CJS mode — fails with Node's `ERR_PACKAGE_PATH_NOT_EXPORTED` rather than loading a CJS build that does not exist. Import from an ES module.
|
|
39
45
|
|
|
40
|
-
`effect` v4 is a peer dependency, and so are `@effected/
|
|
46
|
+
`effect` v4 is a peer dependency, and so are `@effected/walker` and `@effected/glob` — the traversal primitive the `upwardWalk`, `workspaceRoot` and `gitRoot` resolvers are built on, and the matcher it peers on. Package managers that install peers automatically will pull them in; add them to your manifest explicitly if yours does not. `@effected/jsonc`, `@effected/yaml` and `@effected/toml` are **optional** peers: only the `./jsonc`, `./yaml` and `./toml` subpaths load them, and the package root never does, so a JSON-only program installs and loads none of them. The package declares no runtime dependencies of its own, so nothing it drags into your tree comes from outside `effect` and `@effected/*`.
|
|
41
47
|
|
|
42
48
|
Reading and writing files needs a `FileSystem` and a `Path` implementation, provided once at the edge — from `@effect/platform-node` on Node.
|
|
43
49
|
|
|
@@ -80,7 +86,8 @@ Effect.runPromise(program.pipe(Effect.provide(AppConfigLive), Effect.provide(Pla
|
|
|
80
86
|
Resolvers are consulted in priority order, highest first. `MergeStrategy.firstMatch` takes the winner; `MergeStrategy.layeredMerge` deep-merges every source that matched, with higher-priority keys overwriting lower ones:
|
|
81
87
|
|
|
82
88
|
```ts
|
|
83
|
-
import { ConfigFile, ConfigResolver, MergeStrategy
|
|
89
|
+
import { ConfigFile, ConfigResolver, MergeStrategy } from "@effected/config-file";
|
|
90
|
+
import { YamlCodec } from "@effected/config-file/yaml";
|
|
84
91
|
import { Schema } from "effect";
|
|
85
92
|
|
|
86
93
|
class Settings extends Schema.Class<Settings>("Settings")({ port: Schema.Number }) {}
|
|
@@ -176,16 +183,16 @@ export const loadOrFallback = (config: ConfigFileShape<AppShape>) =>
|
|
|
176
183
|
|
|
177
184
|
## Codecs
|
|
178
185
|
|
|
179
|
-
Four codecs ship in the package, each a free-standing named export:
|
|
186
|
+
Four codecs ship in the package, each a free-standing named export. `JsonCodec` comes from the package root; the other three each have their own subpath and an optional engine peer to install beside it:
|
|
180
187
|
|
|
181
|
-
| Codec | Format | Engine |
|
|
182
|
-
| ----- | ------ | ------ |
|
|
183
|
-
| `JsonCodec` | JSON | the host `JSON` global, no parser at all |
|
|
184
|
-
| `JsoncCodec` | JSONC | `@effected/jsonc` |
|
|
185
|
-
| `YamlCodec` | YAML | `@effected/yaml` |
|
|
186
|
-
| `TomlCodec` | TOML | `@effected/toml` |
|
|
188
|
+
| Codec | Format | Import from | Engine |
|
|
189
|
+
| ----- | ------ | ----------- | ------ |
|
|
190
|
+
| `JsonCodec` | JSON | `@effected/config-file` | the host `JSON` global, no parser at all |
|
|
191
|
+
| `JsoncCodec` | JSONC | `@effected/config-file/jsonc` | `@effected/jsonc` |
|
|
192
|
+
| `YamlCodec` | YAML | `@effected/config-file/yaml` | `@effected/yaml` |
|
|
193
|
+
| `TomlCodec` | TOML | `@effected/config-file/toml` | `@effected/toml` |
|
|
187
194
|
|
|
188
|
-
|
|
195
|
+
You install and load only the parser you name. The root never reaches a subpath, so a JSON-only application installs no parser and ships none. Each subpath reaches only its own engine, and the codecs are never collected into a namespace object, so importing `TomlCodec` never references the YAML or JSONC bindings.
|
|
189
196
|
|
|
190
197
|
Codecs compose. `EncryptedCodec` wraps any codec with AES-GCM, and `ConfigMigration.make` wraps any codec so parsed content is brought up to the latest version. Each *widens* the error channel rather than flattening its failures into the inner codec's error, and a decorated codec plugs into `ConfigFile.layer`, `ConfigFile.testLayer` and `ConfigFile.read` like any other: their error unions widen with it. A service key names the widened error as its third type argument:
|
|
191
198
|
|
|
@@ -242,7 +249,7 @@ Leave the third type argument off and the `ConfigFile.layer` call is a compile e
|
|
|
242
249
|
- `encode` / `write` / `save` / `update` — the write path. `encode(value, options?)` returns the serialized text a `write` would put on disk without writing, emitting an event or naming a path in its errors: the primitive behind a `--dry-run` flag. `write` takes an explicit path the caller vouches for; `save` and `update` resolve the layer's `defaultPath` and create its parent directory. `encode` and `write` accept `{ header }`, text prepended verbatim above the document with exactly one newline between — a `#:schema https://…` directive atop a TOML file, say. The caller owns its validity in the target format; JSON has no comment syntax, so a header on `JsonCodec` yields an unparseable file by construction.
|
|
243
250
|
- `ConfigResolver` — `explicitPath`, `staticDir`, `upwardWalk`, `workspaceRoot`, `gitRoot` and `systemEtc`. A resolver's error channel is `never` by contract: every filesystem failure becomes `Option.none()`, so one unreadable tier never aborts the chain.
|
|
244
251
|
- `MergeStrategy` — `firstMatch` and `layeredMerge`, combining discovered sources in priority order.
|
|
245
|
-
- `JsonCodec
|
|
252
|
+
- `JsonCodec` (root), `JsoncCodec` (`./jsonc`), `YamlCodec` (`./yaml`), `TomlCodec` (`./toml`) — JSON, JSONC, YAML and TOML, exported free-standing and split by subpath so an unused format's engine is neither installed nor loaded.
|
|
246
253
|
- `ConfigCodec` / `EncryptedCodec` / `ConfigMigration` — a pluggable codec seam, generic in its error type so decorators widen rather than flatten. `ConfigCodec` is the interface: bring your own format by satisfying it.
|
|
247
254
|
- `ConfigEvents` — an opt-in `PubSub` of `ConfigEvent`, honestly zero-cost when omitted: no `events` option means no context lookup at all. Failure events carry the structured typed error, never a `reason` string.
|
|
248
255
|
- `asConfigProvider` / `layerConfigProvider` — expose a loaded, validated document as a v4 `ConfigProvider`, layered beneath the ambient one so an environment variable overrides the file it was deployed with.
|
package/index.d.ts
CHANGED
|
@@ -48,13 +48,14 @@ export declare class ConfigCodecError extends ConfigCodecError_base {
|
|
|
48
48
|
* decorator codecs such as `EncryptedCodec` and `ConfigMigration.make` widen it
|
|
49
49
|
* rather than flattening their own failures into a string.
|
|
50
50
|
*
|
|
51
|
-
* The four built-in codecs
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
51
|
+
* The four built-in codecs are free-standing named exports, one per module,
|
|
52
|
+
* and are deliberately never collected into a namespace object. `JsonCodec`
|
|
53
|
+
* ships from the package root; `JsoncCodec`, `YamlCodec` and `TomlCodec` ship
|
|
54
|
+
* from the `./jsonc`, `./yaml` and `./toml` subpaths, whose engines are
|
|
55
|
+
* optional peers the root never reaches. Collecting them would make this module
|
|
56
|
+
* a dispatch table: referencing it at all would reach every codec, and every
|
|
57
|
+
* codec would reach its parsing engine, so importing the JSON codec alone would
|
|
58
|
+
* drag the JSONC, YAML and TOML engines into the bundle.
|
|
58
59
|
*
|
|
59
60
|
* @public
|
|
60
61
|
*/
|
|
@@ -575,7 +576,26 @@ export interface MergeStrategy<A> {
|
|
|
575
576
|
* @public
|
|
576
577
|
*/
|
|
577
578
|
export declare const MergeStrategy: {
|
|
579
|
+
/** Take the highest-priority source whole. */
|
|
578
580
|
readonly firstMatch: <A>() => MergeStrategy<A>;
|
|
581
|
+
/**
|
|
582
|
+
* Deep-merge every contributing source, higher priority winning on conflict.
|
|
583
|
+
*
|
|
584
|
+
* @remarks
|
|
585
|
+
* Two values merge only when both are record-like and share a prototype, so a
|
|
586
|
+
* document decoded through `Schema.Class` merges with another of the same class
|
|
587
|
+
* and **survives as a real instance** — `instanceof` holds and its getters
|
|
588
|
+
* still work. Everything else is atomic: a nested `Date`, `Map`, `Set`,
|
|
589
|
+
* `RegExp`, array, or class instance is taken whole from the highest-priority
|
|
590
|
+
* source that defines it, never reshaped field-by-field.
|
|
591
|
+
*
|
|
592
|
+
* The alternative — spreading each value into a fresh object — would make
|
|
593
|
+
* `load`'s declared `Effect<A>` a lie, handing back a structurally-equal plain
|
|
594
|
+
* object whose class methods are gone and whose `Date` fields have decayed to
|
|
595
|
+
* `{}`.
|
|
596
|
+
*
|
|
597
|
+
* Nested *plain* objects (a `Schema.Struct` section) still merge field-wise.
|
|
598
|
+
*/
|
|
579
599
|
readonly layeredMerge: <A>() => MergeStrategy<A>;
|
|
580
600
|
};
|
|
581
601
|
declare const ConfigFileNotFoundError_base: Schema.Class<ConfigFileNotFoundError, Schema.TaggedStruct<"ConfigFileNotFoundError", {
|
|
@@ -1464,53 +1484,5 @@ export declare function EncryptedCodec<E>(inner: ConfigCodec<E>, keySource: Encr
|
|
|
1464
1484
|
* @public
|
|
1465
1485
|
*/
|
|
1466
1486
|
export declare const JsonCodec: ConfigCodec;
|
|
1467
|
-
/**
|
|
1468
|
-
* A `ConfigCodec` backed by `@effected/jsonc`: JSON with comments and
|
|
1469
|
-
* trailing commas.
|
|
1470
|
-
*
|
|
1471
|
-
* @remarks
|
|
1472
|
-
* `@effected/jsonc` does not expose a `stringify` — its schema layer's encode
|
|
1473
|
-
* direction is `JSON.stringify` (comments never survive a round-trip encode;
|
|
1474
|
-
* see `Jsonc.fromString`'s remarks), so `stringify` here calls `JSON.stringify`
|
|
1475
|
-
* directly and wraps a thrown defect the same way `JsonCodec` does.
|
|
1476
|
-
* Both directions preserve the underlying failure structurally in `cause` —
|
|
1477
|
-
* never stringified.
|
|
1478
|
-
*
|
|
1479
|
-
* @public
|
|
1480
|
-
*/
|
|
1481
|
-
export declare const JsoncCodec: ConfigCodec;
|
|
1482
|
-
/**
|
|
1483
|
-
* A `ConfigCodec` backed by `@effected/toml`.
|
|
1484
|
-
*
|
|
1485
|
-
* @remarks
|
|
1486
|
-
* `@effected/toml`'s input hardening — a nesting-depth cap on arrays and
|
|
1487
|
-
* inline tables, enforced independently on both the parse and stringify
|
|
1488
|
-
* sides — fails through the typed error channel, so a hostile config file
|
|
1489
|
-
* surfaces as a `ConfigCodecError` rather than crashing the process. Both
|
|
1490
|
-
* directions preserve the underlying failure structurally in `cause` —
|
|
1491
|
-
* never stringified.
|
|
1492
|
-
*
|
|
1493
|
-
* Stringify is genuinely fallible beyond hostile input: TOML has no null,
|
|
1494
|
-
* so a document carrying `null` (or any other unrepresentable value, an
|
|
1495
|
-
* out-of-int64-range `bigint`, a circular reference) fails with a
|
|
1496
|
-
* `ConfigCodecError` whose `cause` is the structured `TomlStringifyError`.
|
|
1497
|
-
*
|
|
1498
|
-
* @public
|
|
1499
|
-
*/
|
|
1500
|
-
export declare const TomlCodec: ConfigCodec;
|
|
1501
|
-
/**
|
|
1502
|
-
* A `ConfigCodec` backed by `@effected/yaml`.
|
|
1503
|
-
*
|
|
1504
|
-
* @remarks
|
|
1505
|
-
* `@effected/yaml`'s input hardening — an alias-expansion budget guarding
|
|
1506
|
-
* against "billion laughs" alias bombs, and a collection-nesting depth cap —
|
|
1507
|
-
* fails through the typed error channel, so a hostile config file surfaces
|
|
1508
|
-
* as a `ConfigCodecError` rather than crashing the process. Both directions
|
|
1509
|
-
* preserve the underlying failure structurally in `cause` — never
|
|
1510
|
-
* stringified.
|
|
1511
|
-
*
|
|
1512
|
-
* @public
|
|
1513
|
-
*/
|
|
1514
|
-
export declare const YamlCodec: ConfigCodec;
|
|
1515
1487
|
export type { ConfigCodec, ConfigEncodeError, ConfigEncodeOptions, ConfigEventsShape, ConfigFileMigration, ConfigFileOptions, ConfigFileShape, ConfigFileTestOptions, ConfigLoadError, ConfigMatch, ConfigMigrationOptions, ConfigProbe, ConfigReadError, ConfigReadOptions, ConfigSaveError, ConfigSource, ConfigUpdateError, ConfigWriteError, LayerConfigProviderOptions, NonEmptySources, UpwardWalkOptions };
|
|
1516
1488
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -6,9 +6,6 @@ import { ConfigMigration, ConfigMigrationError, VersionAccess } from "./ConfigMi
|
|
|
6
6
|
import { asConfigProvider, layerConfigProvider } from "./ConfigProvider.js";
|
|
7
7
|
import { ConfigEncryptionError, EncryptedCodec, EncryptedCodecKey } from "./EncryptedCodec.js";
|
|
8
8
|
import { JsonCodec } from "./JsonCodec.js";
|
|
9
|
-
import { JsoncCodec } from "./JsoncCodec.js";
|
|
10
9
|
import { MergeStrategy } from "./MergeStrategy.js";
|
|
11
|
-
import { TomlCodec } from "./TomlCodec.js";
|
|
12
|
-
import { YamlCodec } from "./YamlCodec.js";
|
|
13
10
|
|
|
14
|
-
export { ConfigCodecError, ConfigDefaultPathMissingError, ConfigEncryptionError, ConfigEvent, ConfigEventPayload, ConfigEvents, ConfigFile, ConfigFileNotFoundError, ConfigFileReadError, ConfigFileWriteError, ConfigMigration, ConfigMigrationError, ConfigResolver, ConfigSourceRef, ConfigValidationError, EncryptedCodec, EncryptedCodecKey, JsonCodec,
|
|
11
|
+
export { ConfigCodecError, ConfigDefaultPathMissingError, ConfigEncryptionError, ConfigEvent, ConfigEventPayload, ConfigEvents, ConfigFile, ConfigFileNotFoundError, ConfigFileReadError, ConfigFileWriteError, ConfigMigration, ConfigMigrationError, ConfigResolver, ConfigSourceRef, ConfigValidationError, EncryptedCodec, EncryptedCodecKey, JsonCodec, MergeStrategy, VersionAccess, asConfigProvider, layerConfigProvider };
|
package/jsonc.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ConfigCodec } from "@effected/config-file";
|
|
2
|
+
/**
|
|
3
|
+
* A `ConfigCodec` backed by `@effected/jsonc`: JSON with comments and
|
|
4
|
+
* trailing commas.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* `@effected/jsonc` does not expose a `stringify` — its schema layer's encode
|
|
8
|
+
* direction is `JSON.stringify` (comments never survive a round-trip encode;
|
|
9
|
+
* see `Jsonc.fromString`'s remarks), so `stringify` here calls `JSON.stringify`
|
|
10
|
+
* directly and wraps a thrown defect the same way `JsonCodec` does.
|
|
11
|
+
* Both directions preserve the underlying failure structurally in `cause` —
|
|
12
|
+
* never stringified.
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export declare const JsoncCodec: ConfigCodec;
|
|
17
|
+
//# sourceMappingURL=jsonc.d.ts.map
|
package/jsonc.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/config-file",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Composable config file loading for Effect: JSON, JSONC, YAML and TOML codecs, resolution strategies, and merge behaviors.",
|
|
6
6
|
"keywords": [
|
|
@@ -40,16 +40,42 @@
|
|
|
40
40
|
"import": "./index.js",
|
|
41
41
|
"default": "./index.js"
|
|
42
42
|
},
|
|
43
|
+
"./jsonc": {
|
|
44
|
+
"types": "./jsonc.d.ts",
|
|
45
|
+
"import": "./jsonc.js",
|
|
46
|
+
"default": "./jsonc.js"
|
|
47
|
+
},
|
|
48
|
+
"./toml": {
|
|
49
|
+
"types": "./toml.d.ts",
|
|
50
|
+
"import": "./toml.js",
|
|
51
|
+
"default": "./toml.js"
|
|
52
|
+
},
|
|
53
|
+
"./yaml": {
|
|
54
|
+
"types": "./yaml.d.ts",
|
|
55
|
+
"import": "./yaml.js",
|
|
56
|
+
"default": "./yaml.js"
|
|
57
|
+
},
|
|
43
58
|
"./package.json": "./package.json"
|
|
44
59
|
},
|
|
45
60
|
"peerDependencies": {
|
|
46
61
|
"@effected/glob": "^0.14.0",
|
|
47
62
|
"@effected/jsonc": "^0.20.0",
|
|
48
63
|
"@effected/toml": "^0.16.0",
|
|
49
|
-
"@effected/walker": "^0.
|
|
50
|
-
"@effected/yaml": "^0.25.
|
|
64
|
+
"@effected/walker": "^0.20.1",
|
|
65
|
+
"@effected/yaml": "^0.25.1",
|
|
51
66
|
"effect": "^4.0.0"
|
|
52
67
|
},
|
|
68
|
+
"peerDependenciesMeta": {
|
|
69
|
+
"@effected/jsonc": {
|
|
70
|
+
"optional": true
|
|
71
|
+
},
|
|
72
|
+
"@effected/toml": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
75
|
+
"@effected/yaml": {
|
|
76
|
+
"optional": true
|
|
77
|
+
}
|
|
78
|
+
},
|
|
53
79
|
"engines": {
|
|
54
80
|
"node": ">=24.11.0"
|
|
55
81
|
}
|
package/toml.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ConfigCodec } from "@effected/config-file";
|
|
2
|
+
/**
|
|
3
|
+
* A `ConfigCodec` backed by `@effected/toml`.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* `@effected/toml`'s input hardening — a nesting-depth cap on arrays and
|
|
7
|
+
* inline tables, enforced independently on both the parse and stringify
|
|
8
|
+
* sides — fails through the typed error channel, so a hostile config file
|
|
9
|
+
* surfaces as a `ConfigCodecError` rather than crashing the process. Both
|
|
10
|
+
* directions preserve the underlying failure structurally in `cause` —
|
|
11
|
+
* never stringified.
|
|
12
|
+
*
|
|
13
|
+
* Stringify is genuinely fallible beyond hostile input: TOML has no null,
|
|
14
|
+
* so a document carrying `null` (or any other unrepresentable value, an
|
|
15
|
+
* out-of-int64-range `bigint`, a circular reference) fails with a
|
|
16
|
+
* `ConfigCodecError` whose `cause` is the structured `TomlStringifyError`.
|
|
17
|
+
*
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
export declare const TomlCodec: ConfigCodec;
|
|
21
|
+
//# sourceMappingURL=toml.d.ts.map
|
package/toml.js
ADDED
package/tsdoc-metadata.json
CHANGED
package/yaml.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ConfigCodec } from "@effected/config-file";
|
|
2
|
+
/**
|
|
3
|
+
* A `ConfigCodec` backed by `@effected/yaml`.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* `@effected/yaml`'s input hardening — an alias-expansion budget guarding
|
|
7
|
+
* against "billion laughs" alias bombs, and a collection-nesting depth cap —
|
|
8
|
+
* fails through the typed error channel, so a hostile config file surfaces
|
|
9
|
+
* as a `ConfigCodecError` rather than crashing the process. Both directions
|
|
10
|
+
* preserve the underlying failure structurally in `cause` — never
|
|
11
|
+
* stringified.
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export declare const YamlCodec: ConfigCodec;
|
|
16
|
+
//# sourceMappingURL=yaml.d.ts.map
|
package/yaml.js
ADDED