@effected/config-file 0.5.2 → 0.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/ConfigCodec.js +21 -1
- package/ConfigFile.js +26 -5
- package/ConfigResolver.js +82 -45
- package/index.d.ts +171 -37
- package/package.json +6 -6
package/ConfigCodec.js
CHANGED
|
@@ -8,6 +8,15 @@ import { Schema } from "effect";
|
|
|
8
8
|
* The underlying failure is preserved structurally in `cause` — it is never
|
|
9
9
|
* stringified. Route on the `"ConfigCodecError"` tag with `Effect.catchTag`.
|
|
10
10
|
*
|
|
11
|
+
* `path` names the offending file when the error came through `ConfigFile`,
|
|
12
|
+
* which is what makes a failure during multi-candidate discovery reportable.
|
|
13
|
+
*
|
|
14
|
+
* `message` deliberately does NOT name the path — read `path` when you need it.
|
|
15
|
+
* A wrapper that renders its own message almost always names the file too, so
|
|
16
|
+
* a message carrying the path would compose into a rendering that prints the
|
|
17
|
+
* same file twice. The doubling is the default outcome, not an unlucky one:
|
|
18
|
+
* the field is the API, and the message is a bare summary.
|
|
19
|
+
*
|
|
11
20
|
* @public
|
|
12
21
|
*/
|
|
13
22
|
var ConfigCodecError = class extends Schema.TaggedError()("ConfigCodecError", {
|
|
@@ -16,7 +25,18 @@ var ConfigCodecError = class extends Schema.TaggedError()("ConfigCodecError", {
|
|
|
16
25
|
/** Which direction failed. */
|
|
17
26
|
operation: Schema.Literals(["parse", "stringify"]),
|
|
18
27
|
/** The underlying failure, preserved structurally. */
|
|
19
|
-
cause: Schema.Defect()
|
|
28
|
+
cause: Schema.Defect(),
|
|
29
|
+
/**
|
|
30
|
+
* The file the content came from, when a caller knew it.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* A codec sees a string, never a path, so it cannot fill this in itself.
|
|
34
|
+
* `ConfigFile`'s read/write pipeline does: every site that feeds a codec a
|
|
35
|
+
* path it resolved re-raises the error with `path` attached, so a discovery
|
|
36
|
+
* pass over several candidates still names the file that failed. Absent
|
|
37
|
+
* only when a codec was driven directly, outside that pipeline.
|
|
38
|
+
*/
|
|
39
|
+
path: Schema.optionalKey(Schema.String)
|
|
20
40
|
}) {
|
|
21
41
|
get message() {
|
|
22
42
|
return `${this.codec} ${this.operation} failed`;
|
package/ConfigFile.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConfigCodecError } from "./ConfigCodec.js";
|
|
1
2
|
import { ConfigEvent } from "./ConfigEvent.js";
|
|
2
3
|
import { ConfigResolver } from "./ConfigResolver.js";
|
|
3
4
|
import { Context, DateTime, Effect, FileSystem, Layer, Option, Path, PubSub, Schema, Semaphore } from "effect";
|
|
@@ -101,6 +102,24 @@ var ConfigValidationError = class extends Schema.TaggedError()("ConfigValidation
|
|
|
101
102
|
}
|
|
102
103
|
};
|
|
103
104
|
const Service = () => (id) => Context.Service()(id);
|
|
105
|
+
/**
|
|
106
|
+
* Re-raise a codec failure with the file it came from attached.
|
|
107
|
+
*
|
|
108
|
+
* @remarks
|
|
109
|
+
* A codec is handed a string and never a path, so `ConfigCodecError.path` can
|
|
110
|
+
* only be filled in here, where the resolved target is in scope. An error that
|
|
111
|
+
* already carries a path is left alone — a decorator codec that knew better
|
|
112
|
+
* wins — and anything that is not a `ConfigCodecError` passes through
|
|
113
|
+
* untouched, which is why the cast is sound: the returned value is either the
|
|
114
|
+
* argument itself or a `ConfigCodecError`, and the only way a `ConfigCodecError`
|
|
115
|
+
* reaches here is if `E` admits one.
|
|
116
|
+
*/
|
|
117
|
+
const withCodecPath = (error, target) => error instanceof ConfigCodecError && error.path === void 0 ? new ConfigCodecError({
|
|
118
|
+
codec: error.codec,
|
|
119
|
+
operation: error.operation,
|
|
120
|
+
cause: error.cause,
|
|
121
|
+
path: target
|
|
122
|
+
}) : error;
|
|
104
123
|
const makeImpl = (options, fs, path, resolverEnv) => {
|
|
105
124
|
/**
|
|
106
125
|
* Publish one event, or nothing at all.
|
|
@@ -143,7 +162,7 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
143
162
|
path: target,
|
|
144
163
|
cause
|
|
145
164
|
})));
|
|
146
|
-
const parsed = yield* options.codec.parse(raw).pipe(Effect.tapError((error) => emit({
|
|
165
|
+
const parsed = yield* options.codec.parse(raw).pipe(Effect.mapError((error) => withCodecPath(error, target)), Effect.tapError((error) => emit({
|
|
147
166
|
_tag: "ParseFailed",
|
|
148
167
|
path: target,
|
|
149
168
|
codec: options.codec.name,
|
|
@@ -171,9 +190,10 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
171
190
|
const discover = Effect.fn("ConfigFile.discover")(function* () {
|
|
172
191
|
const sources = [];
|
|
173
192
|
for (const resolver of options.resolvers) {
|
|
174
|
-
const found = yield* Effect.provide(resolver.resolve, resolverEnv);
|
|
193
|
+
const found = resolver.resolveMatch === void 0 ? Option.map(yield* Effect.provide(resolver.resolve, resolverEnv), (path) => ({ path })) : yield* Effect.provide(resolver.resolveMatch, resolverEnv);
|
|
175
194
|
if (Option.isSome(found)) {
|
|
176
|
-
const
|
|
195
|
+
const match = found.value;
|
|
196
|
+
const target = match.path;
|
|
177
197
|
yield* emit({
|
|
178
198
|
_tag: "Discovered",
|
|
179
199
|
path: target,
|
|
@@ -182,6 +202,7 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
182
202
|
sources.push({
|
|
183
203
|
path: target,
|
|
184
204
|
resolver: resolver.name,
|
|
205
|
+
match,
|
|
185
206
|
value: yield* loadFrom(target)
|
|
186
207
|
});
|
|
187
208
|
}
|
|
@@ -239,7 +260,7 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
239
260
|
path: Option.some(target),
|
|
240
261
|
issue: error.issue
|
|
241
262
|
}))));
|
|
242
|
-
const serialized = yield* options.codec.stringify(encoded).pipe(Effect.tapError((error) => emit({
|
|
263
|
+
const serialized = yield* options.codec.stringify(encoded).pipe(Effect.mapError((error) => withCodecPath(error, target)), Effect.tapError((error) => emit({
|
|
243
264
|
_tag: "StringifyFailed",
|
|
244
265
|
codec: options.codec.name,
|
|
245
266
|
error
|
|
@@ -353,7 +374,7 @@ const read = (path, options) => Effect.gen(function* () {
|
|
|
353
374
|
path,
|
|
354
375
|
cause
|
|
355
376
|
})));
|
|
356
|
-
const parsed = yield* options.codec.parse(raw);
|
|
377
|
+
const parsed = yield* options.codec.parse(raw).pipe(Effect.mapError((error) => withCodecPath(error, path)));
|
|
357
378
|
return yield* Schema.decodeUnknownEffect(options.schema)(parsed, options.parseOptions).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new ConfigValidationError({
|
|
358
379
|
path: Option.some(path),
|
|
359
380
|
issue: error.issue
|
package/ConfigResolver.js
CHANGED
|
@@ -2,48 +2,72 @@ import { Effect, FileSystem, Option, Path } from "effect";
|
|
|
2
2
|
import { Walker } from "@effected/walker";
|
|
3
3
|
|
|
4
4
|
//#region src/ConfigResolver.ts
|
|
5
|
+
/**
|
|
6
|
+
* Build a resolver from a match-reporting lookup, deriving `resolve` from it.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Every built-in goes through here, so `resolve` and `resolveMatch` are one
|
|
10
|
+
* implementation and cannot drift.
|
|
11
|
+
*/
|
|
12
|
+
const fromMatch = (name, resolveMatch) => ({
|
|
13
|
+
name,
|
|
14
|
+
resolve: Effect.map(resolveMatch, Option.map((match) => match.path)),
|
|
15
|
+
resolveMatch
|
|
16
|
+
});
|
|
5
17
|
/** Absorb any failure into `Option.none()` — the resolver contract. */
|
|
6
18
|
const absorb = (effect) => Effect.catch(effect, () => Effect.succeed(Option.none()));
|
|
7
19
|
const cwdOf = (given) => given ?? globalThis.process?.cwd?.() ?? "/";
|
|
8
|
-
const explicitPath = (target) => ({
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
const explicitPath = (target) => fromMatch("explicit", absorb(Effect.gen(function* () {
|
|
21
|
+
return (yield* (yield* FileSystem.FileSystem).exists(target)) ? Option.some({ path: target }) : Option.none();
|
|
22
|
+
})));
|
|
23
|
+
const staticDir = (options) => fromMatch("static", absorb(Effect.gen(function* () {
|
|
24
|
+
const fs = yield* FileSystem.FileSystem;
|
|
25
|
+
const candidate = (yield* Path.Path).join(options.dir, options.filename);
|
|
26
|
+
return (yield* fs.exists(candidate)) ? Option.some({
|
|
27
|
+
path: candidate,
|
|
28
|
+
dir: options.dir,
|
|
29
|
+
filename: options.filename
|
|
30
|
+
}) : Option.none();
|
|
31
|
+
})));
|
|
32
|
+
const upwardWalk = (options) => fromMatch(options.name ?? "walk", Effect.gen(function* () {
|
|
33
|
+
const fs = yield* FileSystem.FileSystem;
|
|
34
|
+
const path = yield* Path.Path;
|
|
35
|
+
const subpaths = options.subpaths ?? ["."];
|
|
36
|
+
const filenames = options.filenames ?? [options.filename];
|
|
37
|
+
const dirs = yield* Walker.ascend(cwdOf(options.cwd), { ...options.stopAt !== void 0 && { stopAt: options.stopAt } });
|
|
38
|
+
const candidates = [];
|
|
39
|
+
for (const dir of dirs) for (const sub of subpaths) for (const filename of filenames) candidates.push({
|
|
40
|
+
path: path.join(dir, sub, filename),
|
|
41
|
+
dir,
|
|
42
|
+
filename,
|
|
43
|
+
...options.subpaths !== void 0 && { subpath: sub }
|
|
44
|
+
});
|
|
45
|
+
const found = yield* Walker.firstMatch(candidates.map((candidate) => candidate.path), (candidate) => fs.exists(candidate));
|
|
46
|
+
return Option.flatMap(found, (target) => Option.fromNullishOr(candidates.find((candidate) => candidate.path === target)));
|
|
47
|
+
}));
|
|
31
48
|
/**
|
|
32
49
|
* Ascend from `cwd` looking for the first directory where `isRoot` reports
|
|
33
50
|
* true, then probe `subpaths` under it. Shared by `gitRoot` and
|
|
34
51
|
* `workspaceRoot`, which differ only in how a "root" is detected.
|
|
35
52
|
*/
|
|
36
|
-
const rootAnchored = (name, isRoot, options) => ({
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
const rootAnchored = (name, isRoot, options) => fromMatch(name, Effect.gen(function* () {
|
|
54
|
+
const fs = yield* FileSystem.FileSystem;
|
|
55
|
+
const path = yield* Path.Path;
|
|
56
|
+
const dirs = yield* Walker.ascend(cwdOf(options.cwd));
|
|
57
|
+
const root = yield* Walker.findRoot(dirs, isRoot);
|
|
58
|
+
if (Option.isNone(root)) return Option.none();
|
|
59
|
+
const subpaths = options.subpaths ?? ["."];
|
|
60
|
+
const found = yield* Walker.firstMatch(subpaths.map((sub) => path.join(root.value, sub, options.filename)), (candidate) => fs.exists(candidate));
|
|
61
|
+
return Option.map(found, (target) => {
|
|
62
|
+
const matchedSubpath = options.subpaths === void 0 ? void 0 : subpaths.find((sub) => path.join(root.value, sub, options.filename) === target);
|
|
63
|
+
return {
|
|
64
|
+
path: target,
|
|
65
|
+
dir: root.value,
|
|
66
|
+
filename: options.filename,
|
|
67
|
+
...matchedSubpath !== void 0 && { subpath: matchedSubpath }
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}));
|
|
47
71
|
/** `.git` may be a directory (a normal repo) or a file (a worktree pointing at the real repo). */
|
|
48
72
|
const isGitRoot = (dir) => Effect.gen(function* () {
|
|
49
73
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -66,17 +90,19 @@ const isWorkspaceRoot = (dir) => Effect.gen(function* () {
|
|
|
66
90
|
});
|
|
67
91
|
const gitRoot = (options) => rootAnchored("git", isGitRoot, options);
|
|
68
92
|
const workspaceRoot = (options) => rootAnchored("workspace", isWorkspaceRoot, options);
|
|
69
|
-
const systemEtc = (options) => ({
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
93
|
+
const systemEtc = (options) => fromMatch("system", absorb(Effect.gen(function* () {
|
|
94
|
+
if (globalThis.process?.platform === "win32") return Option.none();
|
|
95
|
+
const fs = yield* FileSystem.FileSystem;
|
|
96
|
+
const path = yield* Path.Path;
|
|
97
|
+
const base = options.dir ?? "/etc";
|
|
98
|
+
const dir = path.join(base, options.app);
|
|
99
|
+
const candidate = path.join(dir, options.filename);
|
|
100
|
+
return (yield* fs.exists(candidate)) ? Option.some({
|
|
101
|
+
path: candidate,
|
|
102
|
+
dir,
|
|
103
|
+
filename: options.filename
|
|
104
|
+
}) : Option.none();
|
|
105
|
+
})));
|
|
80
106
|
/**
|
|
81
107
|
* Built-in resolvers, in the order a typical chain uses them.
|
|
82
108
|
*
|
|
@@ -91,6 +117,17 @@ var ConfigResolver = class {
|
|
|
91
117
|
/**
|
|
92
118
|
* Ascends from `cwd` (or the process cwd) toward `stopAt`, resolving the
|
|
93
119
|
* first `subpaths/filename` combination found at each level.
|
|
120
|
+
*
|
|
121
|
+
* @remarks
|
|
122
|
+
* `filenames` replaces `filename` with a per-directory candidate list: every
|
|
123
|
+
* name is probed at one ancestor before the walk ascends, so a child's
|
|
124
|
+
* second candidate still beats a parent's first. Registering one resolver
|
|
125
|
+
* per filename cannot do this — the chain runs a resolver to the filesystem
|
|
126
|
+
* root before starting the next.
|
|
127
|
+
*
|
|
128
|
+
* `name` overrides the reported `"walk"` so two walks in one chain are
|
|
129
|
+
* distinguishable; `ConfigSource.match` identifies the matching candidate
|
|
130
|
+
* more precisely still.
|
|
94
131
|
*/
|
|
95
132
|
static upwardWalk = upwardWalk;
|
|
96
133
|
/**
|
package/index.d.ts
CHANGED
|
@@ -7,6 +7,17 @@ declare const ConfigCodecError_base: Schema.Class<ConfigCodecError, Schema.Tagge
|
|
|
7
7
|
readonly operation: Schema.Literals<readonly ["parse", "stringify"]>;
|
|
8
8
|
/** The underlying failure, preserved structurally. */
|
|
9
9
|
readonly cause: Schema.Defect;
|
|
10
|
+
/**
|
|
11
|
+
* The file the content came from, when a caller knew it.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* A codec sees a string, never a path, so it cannot fill this in itself.
|
|
15
|
+
* `ConfigFile`'s read/write pipeline does: every site that feeds a codec a
|
|
16
|
+
* path it resolved re-raises the error with `path` attached, so a discovery
|
|
17
|
+
* pass over several candidates still names the file that failed. Absent
|
|
18
|
+
* only when a codec was driven directly, outside that pipeline.
|
|
19
|
+
*/
|
|
20
|
+
readonly path: Schema.optionalKey<Schema.String>;
|
|
10
21
|
}>, import("effect/Cause").YieldableError>;
|
|
11
22
|
/**
|
|
12
23
|
* Indicates that a codec failed to parse or stringify configuration content.
|
|
@@ -15,9 +26,18 @@ declare const ConfigCodecError_base: Schema.Class<ConfigCodecError, Schema.Tagge
|
|
|
15
26
|
* The underlying failure is preserved structurally in `cause` — it is never
|
|
16
27
|
* stringified. Route on the `"ConfigCodecError"` tag with `Effect.catchTag`.
|
|
17
28
|
*
|
|
29
|
+
* `path` names the offending file when the error came through `ConfigFile`,
|
|
30
|
+
* which is what makes a failure during multi-candidate discovery reportable.
|
|
31
|
+
*
|
|
32
|
+
* `message` deliberately does NOT name the path — read `path` when you need it.
|
|
33
|
+
* A wrapper that renders its own message almost always names the file too, so
|
|
34
|
+
* a message carrying the path would compose into a rendering that prints the
|
|
35
|
+
* same file twice. The doubling is the default outcome, not an unlucky one:
|
|
36
|
+
* the field is the API, and the message is a bare summary.
|
|
37
|
+
*
|
|
18
38
|
* @public
|
|
19
39
|
*/
|
|
20
|
-
declare class ConfigCodecError extends ConfigCodecError_base {
|
|
40
|
+
export declare class ConfigCodecError extends ConfigCodecError_base {
|
|
21
41
|
get message(): string;
|
|
22
42
|
}
|
|
23
43
|
/**
|
|
@@ -55,7 +75,7 @@ interface ConfigCodec<E = ConfigCodecError> {
|
|
|
55
75
|
*
|
|
56
76
|
* @public
|
|
57
77
|
*/
|
|
58
|
-
declare const ConfigSourceRef: Schema.Struct<{
|
|
78
|
+
export declare const ConfigSourceRef: Schema.Struct<{
|
|
59
79
|
/** The filesystem path the value was read from. */
|
|
60
80
|
readonly path: Schema.String;
|
|
61
81
|
/** The name of the resolver that found it. */
|
|
@@ -78,7 +98,7 @@ declare const ConfigSourceRef: Schema.Struct<{
|
|
|
78
98
|
*
|
|
79
99
|
* @public
|
|
80
100
|
*/
|
|
81
|
-
declare const ConfigEventPayload: Schema.Union<readonly [Schema.TaggedStruct<"Discovered", {
|
|
101
|
+
export declare const ConfigEventPayload: Schema.Union<readonly [Schema.TaggedStruct<"Discovered", {
|
|
82
102
|
readonly path: Schema.String;
|
|
83
103
|
readonly resolver: Schema.String;
|
|
84
104
|
}>, Schema.TaggedStruct<"NotFound", {}>, Schema.TaggedStruct<"Parsed", {
|
|
@@ -124,7 +144,7 @@ declare const ConfigEventPayload: Schema.Union<readonly [Schema.TaggedStruct<"Di
|
|
|
124
144
|
*
|
|
125
145
|
* @public
|
|
126
146
|
*/
|
|
127
|
-
type ConfigEventPayload = typeof ConfigEventPayload.Type;
|
|
147
|
+
export type ConfigEventPayload = typeof ConfigEventPayload.Type;
|
|
128
148
|
declare const ConfigEvent_base: Schema.Class<ConfigEvent, Schema.Struct<{
|
|
129
149
|
/** When the event occurred. */
|
|
130
150
|
readonly timestamp: Schema.DateTimeUtc;
|
|
@@ -175,7 +195,7 @@ declare const ConfigEvent_base: Schema.Class<ConfigEvent, Schema.Struct<{
|
|
|
175
195
|
*
|
|
176
196
|
* @public
|
|
177
197
|
*/
|
|
178
|
-
declare class ConfigEvent extends ConfigEvent_base {}
|
|
198
|
+
export declare class ConfigEvent extends ConfigEvent_base {}
|
|
179
199
|
/**
|
|
180
200
|
* The service shape {@link ConfigEvents} provides.
|
|
181
201
|
*
|
|
@@ -209,7 +229,7 @@ declare const ConfigEvents_base: Context.ServiceClass<ConfigEvents, "@effected/c
|
|
|
209
229
|
*
|
|
210
230
|
* @public
|
|
211
231
|
*/
|
|
212
|
-
declare class ConfigEvents extends ConfigEvents_base {
|
|
232
|
+
export declare class ConfigEvents extends ConfigEvents_base {
|
|
213
233
|
/**
|
|
214
234
|
* An unbounded PubSub of config events.
|
|
215
235
|
*
|
|
@@ -239,16 +259,112 @@ declare class ConfigEvents extends ConfigEvents_base {
|
|
|
239
259
|
*
|
|
240
260
|
* @public
|
|
241
261
|
*/
|
|
242
|
-
interface ConfigResolver<R = never> {
|
|
262
|
+
export interface ConfigResolver<R = never> {
|
|
243
263
|
readonly name: string;
|
|
244
264
|
readonly resolve: Effect.Effect<Option.Option<string>, never, R>;
|
|
265
|
+
/**
|
|
266
|
+
* The same lookup, reporting **how** the file was found rather than only
|
|
267
|
+
* where.
|
|
268
|
+
*
|
|
269
|
+
* @remarks
|
|
270
|
+
* Optional, and optional forever: a hand-rolled resolver that omits it is a
|
|
271
|
+
* complete `ConfigResolver`, and the pipeline falls back to `resolve`,
|
|
272
|
+
* reporting a bare `{ path }` match. Every built-in implements it, and
|
|
273
|
+
* derives `resolve` from it, so the two can never disagree.
|
|
274
|
+
*
|
|
275
|
+
* It exists because a resolver's `name` cannot identify *which* candidate
|
|
276
|
+
* matched once one resolver probes several — `upwardWalk` with a
|
|
277
|
+
* `filenames` list, or with `subpaths`, is one resolver with many
|
|
278
|
+
* candidates. A consumer that needs the anchor directory (a CLI computing
|
|
279
|
+
* its project root from the discovered config) reads
|
|
280
|
+
* {@link ConfigMatch.dir} instead of string-matching the discovered path's
|
|
281
|
+
* tail.
|
|
282
|
+
*/
|
|
283
|
+
readonly resolveMatch?: Effect.Effect<Option.Option<ConfigMatch>, never, R>;
|
|
245
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* How a resolver found a config file, not merely where.
|
|
287
|
+
*
|
|
288
|
+
* @remarks
|
|
289
|
+
* `path` is always present; the rest describe the candidate that matched and
|
|
290
|
+
* are populated only when the resolver knows them. `dir` is the **anchor**: the
|
|
291
|
+
* directory the candidate was resolved against — the ancestor `upwardWalk`
|
|
292
|
+
* stopped at, the root `gitRoot`/`workspaceRoot` found, `staticDir`'s `dir`.
|
|
293
|
+
* `subpath` and `filename` are the `subpaths`/`filenames` entries that matched,
|
|
294
|
+
* exactly as the caller spelled them.
|
|
295
|
+
*
|
|
296
|
+
* @public
|
|
297
|
+
*/
|
|
298
|
+
interface ConfigMatch {
|
|
299
|
+
/** The filesystem path that matched. */
|
|
300
|
+
readonly path: string;
|
|
301
|
+
/** The directory the matching candidate was resolved against, when the resolver has one. */
|
|
302
|
+
readonly dir?: string;
|
|
303
|
+
/** The `subpaths` entry that matched, when the resolver takes `subpaths`. */
|
|
304
|
+
readonly subpath?: string;
|
|
305
|
+
/** The `filenames`/`filename` entry that matched, when the resolver takes one. */
|
|
306
|
+
readonly filename?: string;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Options for `ConfigResolver.upwardWalk`.
|
|
310
|
+
*
|
|
311
|
+
* @remarks
|
|
312
|
+
* Exactly one of `filename` and `filenames` is given. `filename` is the
|
|
313
|
+
* original one-name form and keeps working unchanged; `filenames` is the
|
|
314
|
+
* per-directory candidate list — every name is probed at each ancestor before
|
|
315
|
+
* the walk ascends, which is the "config-dir" convention (`.app.toml`, then
|
|
316
|
+
* `app.toml`, then `.config/app.toml`, all at one level) that separate
|
|
317
|
+
* `upwardWalk` entries cannot express, because the chain exhausts one resolver
|
|
318
|
+
* to the filesystem root before starting the next.
|
|
319
|
+
*
|
|
320
|
+
* A `filenames` entry may itself carry a relative directory
|
|
321
|
+
* (`".config/app.toml"`), which is how a mixed convention is spelled without a
|
|
322
|
+
* `subpaths` cross product.
|
|
323
|
+
*
|
|
324
|
+
* @public
|
|
325
|
+
*/
|
|
326
|
+
type UpwardWalkOptions = {
|
|
327
|
+
/** The single file name probed at each ancestor directory. */
|
|
328
|
+
readonly filename: string;
|
|
329
|
+
readonly filenames?: undefined;
|
|
330
|
+
/** Where the walk starts. Defaults to the process cwd. */
|
|
331
|
+
readonly cwd?: string;
|
|
332
|
+
/** An absolute ceiling the walk stops at, inclusive. */
|
|
333
|
+
readonly stopAt?: string;
|
|
334
|
+
/** Directories probed under each ancestor, in order. Defaults to `["."]`. */
|
|
335
|
+
readonly subpaths?: ReadonlyArray<string>;
|
|
336
|
+
/**
|
|
337
|
+
* The resolver's reported name. Defaults to `"walk"`.
|
|
338
|
+
*
|
|
339
|
+
* @remarks
|
|
340
|
+
* Two `upwardWalk` entries in one chain otherwise report the same
|
|
341
|
+
* `"walk"`, so `ConfigSource.resolver` cannot tell them apart. Pass a
|
|
342
|
+
* distinct name per call — or read `ConfigSource.match` instead, which
|
|
343
|
+
* identifies the matching candidate rather than the resolver.
|
|
344
|
+
*/
|
|
345
|
+
readonly name?: string;
|
|
346
|
+
} | {
|
|
347
|
+
/**
|
|
348
|
+
* The candidate file names probed at each ancestor directory, in order,
|
|
349
|
+
* before the walk ascends.
|
|
350
|
+
*/
|
|
351
|
+
readonly filenames: ReadonlyArray<string>;
|
|
352
|
+
readonly filename?: undefined;
|
|
353
|
+
/** Where the walk starts. Defaults to the process cwd. */
|
|
354
|
+
readonly cwd?: string;
|
|
355
|
+
/** An absolute ceiling the walk stops at, inclusive. */
|
|
356
|
+
readonly stopAt?: string;
|
|
357
|
+
/** Directories probed under each ancestor, in order. Defaults to `["."]`. */
|
|
358
|
+
readonly subpaths?: ReadonlyArray<string>;
|
|
359
|
+
/** The resolver's reported name. Defaults to `"walk"`. */
|
|
360
|
+
readonly name?: string;
|
|
361
|
+
};
|
|
246
362
|
/**
|
|
247
363
|
* Built-in resolvers, in the order a typical chain uses them.
|
|
248
364
|
*
|
|
249
365
|
* @public
|
|
250
366
|
*/
|
|
251
|
-
declare class ConfigResolver {
|
|
367
|
+
export declare class ConfigResolver {
|
|
252
368
|
private constructor();
|
|
253
369
|
/** Resolves to `target` when it exists on disk, or `Option.none()` when it does not — no walking, no filename convention. */
|
|
254
370
|
static readonly explicitPath: (target: string) => ConfigResolver<FileSystem.FileSystem | Path.Path>;
|
|
@@ -260,13 +376,19 @@ declare class ConfigResolver {
|
|
|
260
376
|
/**
|
|
261
377
|
* Ascends from `cwd` (or the process cwd) toward `stopAt`, resolving the
|
|
262
378
|
* first `subpaths/filename` combination found at each level.
|
|
379
|
+
*
|
|
380
|
+
* @remarks
|
|
381
|
+
* `filenames` replaces `filename` with a per-directory candidate list: every
|
|
382
|
+
* name is probed at one ancestor before the walk ascends, so a child's
|
|
383
|
+
* second candidate still beats a parent's first. Registering one resolver
|
|
384
|
+
* per filename cannot do this — the chain runs a resolver to the filesystem
|
|
385
|
+
* root before starting the next.
|
|
386
|
+
*
|
|
387
|
+
* `name` overrides the reported `"walk"` so two walks in one chain are
|
|
388
|
+
* distinguishable; `ConfigSource.match` identifies the matching candidate
|
|
389
|
+
* more precisely still.
|
|
263
390
|
*/
|
|
264
|
-
static readonly upwardWalk: (options:
|
|
265
|
-
readonly filename: string;
|
|
266
|
-
readonly cwd?: string;
|
|
267
|
-
readonly stopAt?: string;
|
|
268
|
-
readonly subpaths?: ReadonlyArray<string>;
|
|
269
|
-
}) => ConfigResolver<FileSystem.FileSystem | Path.Path>;
|
|
391
|
+
static readonly upwardWalk: (options: UpwardWalkOptions) => ConfigResolver<FileSystem.FileSystem | Path.Path>;
|
|
270
392
|
/**
|
|
271
393
|
* Ascends from `cwd` to the nearest workspace root — a directory with a
|
|
272
394
|
* `pnpm-workspace.yaml`, or a `package.json` carrying a `workspaces` field —
|
|
@@ -314,6 +436,18 @@ interface ConfigSource<A> {
|
|
|
314
436
|
readonly path: string;
|
|
315
437
|
/** The name of the resolver that found it. */
|
|
316
438
|
readonly resolver: string;
|
|
439
|
+
/**
|
|
440
|
+
* How the resolver found it — the anchor directory and the candidate that
|
|
441
|
+
* matched.
|
|
442
|
+
*
|
|
443
|
+
* @remarks
|
|
444
|
+
* Populated by `ConfigFile.discover` for every source it produces: a
|
|
445
|
+
* resolver implementing `ConfigResolver.resolveMatch` reports its own
|
|
446
|
+
* detail, and one that does not yields a bare `{ path }`. Optional on the
|
|
447
|
+
* type so a hand-built `ConfigSource` — a merge-strategy test, a synthetic
|
|
448
|
+
* source — stays valid without one.
|
|
449
|
+
*/
|
|
450
|
+
readonly match?: ConfigMatch;
|
|
317
451
|
/** The decoded, validated configuration value. */
|
|
318
452
|
readonly value: A;
|
|
319
453
|
}
|
|
@@ -330,7 +464,7 @@ type NonEmptySources<A> = readonly [ConfigSource<A>, ...ConfigSource<A>[]];
|
|
|
330
464
|
*
|
|
331
465
|
* @public
|
|
332
466
|
*/
|
|
333
|
-
interface MergeStrategy<A> {
|
|
467
|
+
export interface MergeStrategy<A> {
|
|
334
468
|
readonly name: string;
|
|
335
469
|
readonly resolve: (sources: NonEmptySources<A>) => Effect.Effect<A>;
|
|
336
470
|
}
|
|
@@ -343,7 +477,7 @@ interface MergeStrategy<A> {
|
|
|
343
477
|
*
|
|
344
478
|
* @public
|
|
345
479
|
*/
|
|
346
|
-
declare const MergeStrategy: {
|
|
480
|
+
export declare const MergeStrategy: {
|
|
347
481
|
readonly firstMatch: <A>() => MergeStrategy<A>;
|
|
348
482
|
readonly layeredMerge: <A>() => MergeStrategy<A>;
|
|
349
483
|
};
|
|
@@ -363,7 +497,7 @@ declare const ConfigFileNotFoundError_base: Schema.Class<ConfigFileNotFoundError
|
|
|
363
497
|
*
|
|
364
498
|
* @public
|
|
365
499
|
*/
|
|
366
|
-
declare class ConfigFileNotFoundError extends ConfigFileNotFoundError_base {
|
|
500
|
+
export declare class ConfigFileNotFoundError extends ConfigFileNotFoundError_base {
|
|
367
501
|
get message(): string;
|
|
368
502
|
}
|
|
369
503
|
declare const ConfigFileReadError_base: Schema.Class<ConfigFileReadError, Schema.TaggedStruct<"ConfigFileReadError", {
|
|
@@ -381,7 +515,7 @@ declare const ConfigFileReadError_base: Schema.Class<ConfigFileReadError, Schema
|
|
|
381
515
|
*
|
|
382
516
|
* @public
|
|
383
517
|
*/
|
|
384
|
-
declare class ConfigFileReadError extends ConfigFileReadError_base {
|
|
518
|
+
export declare class ConfigFileReadError extends ConfigFileReadError_base {
|
|
385
519
|
get message(): string;
|
|
386
520
|
}
|
|
387
521
|
declare const ConfigFileWriteError_base: Schema.Class<ConfigFileWriteError, Schema.TaggedStruct<"ConfigFileWriteError", {
|
|
@@ -395,7 +529,7 @@ declare const ConfigFileWriteError_base: Schema.Class<ConfigFileWriteError, Sche
|
|
|
395
529
|
*
|
|
396
530
|
* @public
|
|
397
531
|
*/
|
|
398
|
-
declare class ConfigFileWriteError extends ConfigFileWriteError_base {
|
|
532
|
+
export declare class ConfigFileWriteError extends ConfigFileWriteError_base {
|
|
399
533
|
get message(): string;
|
|
400
534
|
}
|
|
401
535
|
declare const ConfigDefaultPathMissingError_base: Schema.Class<ConfigDefaultPathMissingError, Schema.TaggedStruct<"ConfigDefaultPathMissingError", {}>, import("effect/Cause").YieldableError>;
|
|
@@ -414,7 +548,7 @@ declare const ConfigDefaultPathMissingError_base: Schema.Class<ConfigDefaultPath
|
|
|
414
548
|
*
|
|
415
549
|
* @public
|
|
416
550
|
*/
|
|
417
|
-
declare class ConfigDefaultPathMissingError extends ConfigDefaultPathMissingError_base {
|
|
551
|
+
export declare class ConfigDefaultPathMissingError extends ConfigDefaultPathMissingError_base {
|
|
418
552
|
get message(): string;
|
|
419
553
|
}
|
|
420
554
|
declare const ConfigValidationError_base: Schema.Class<ConfigValidationError, Schema.TaggedStruct<"ConfigValidationError", {
|
|
@@ -436,7 +570,7 @@ declare const ConfigValidationError_base: Schema.Class<ConfigValidationError, Sc
|
|
|
436
570
|
*
|
|
437
571
|
* @public
|
|
438
572
|
*/
|
|
439
|
-
declare class ConfigValidationError extends ConfigValidationError_base {
|
|
573
|
+
export declare class ConfigValidationError extends ConfigValidationError_base {
|
|
440
574
|
get message(): string;
|
|
441
575
|
}
|
|
442
576
|
/**
|
|
@@ -680,7 +814,7 @@ interface ConfigReadOptions<A, I> {
|
|
|
680
814
|
*
|
|
681
815
|
* @public
|
|
682
816
|
*/
|
|
683
|
-
declare class ConfigFile {
|
|
817
|
+
export declare class ConfigFile {
|
|
684
818
|
private constructor();
|
|
685
819
|
/**
|
|
686
820
|
* Create a uniquely-keyed service class for one config schema.
|
|
@@ -792,7 +926,7 @@ declare const ConfigMigrationError_base: Schema.Class<ConfigMigrationError, Sche
|
|
|
792
926
|
*
|
|
793
927
|
* @public
|
|
794
928
|
*/
|
|
795
|
-
declare class ConfigMigrationError extends ConfigMigrationError_base {
|
|
929
|
+
export declare class ConfigMigrationError extends ConfigMigrationError_base {
|
|
796
930
|
get message(): string;
|
|
797
931
|
}
|
|
798
932
|
/**
|
|
@@ -814,12 +948,12 @@ interface ConfigFileMigration {
|
|
|
814
948
|
readonly up: (raw: unknown) => Effect.Effect<unknown, unknown>;
|
|
815
949
|
}
|
|
816
950
|
/** How the version number is read from and written to the parsed config. @public */
|
|
817
|
-
interface VersionAccess {
|
|
951
|
+
export interface VersionAccess {
|
|
818
952
|
readonly get: (raw: unknown) => Effect.Effect<number, unknown>;
|
|
819
953
|
readonly set: (raw: unknown, version: number) => Effect.Effect<unknown, unknown>;
|
|
820
954
|
}
|
|
821
955
|
/** Reads and writes a top-level `version` field. @public */
|
|
822
|
-
declare const VersionAccess: {
|
|
956
|
+
export declare const VersionAccess: {
|
|
823
957
|
readonly default: VersionAccess;
|
|
824
958
|
};
|
|
825
959
|
/** Options for {@link ConfigMigration.make}. @public */
|
|
@@ -829,7 +963,7 @@ interface ConfigMigrationOptions {
|
|
|
829
963
|
readonly versionAccess?: VersionAccess;
|
|
830
964
|
}
|
|
831
965
|
/** Versioned migration support for config codecs. @public */
|
|
832
|
-
declare class ConfigMigration {
|
|
966
|
+
export declare class ConfigMigration {
|
|
833
967
|
private constructor();
|
|
834
968
|
/**
|
|
835
969
|
* Wrap a codec so that parsed content is brought up to the latest version.
|
|
@@ -894,7 +1028,7 @@ declare class ConfigMigration {
|
|
|
894
1028
|
*
|
|
895
1029
|
* @public
|
|
896
1030
|
*/
|
|
897
|
-
declare const asConfigProvider: <A>(service: ConfigFileShape<A>) => Effect.Effect<ConfigProvider.ConfigProvider, ConfigLoadError>;
|
|
1031
|
+
export declare const asConfigProvider: <A>(service: ConfigFileShape<A>) => Effect.Effect<ConfigProvider.ConfigProvider, ConfigLoadError>;
|
|
898
1032
|
/**
|
|
899
1033
|
* Options for {@link layerConfigProvider}.
|
|
900
1034
|
*
|
|
@@ -937,7 +1071,7 @@ interface LayerConfigProviderOptions {
|
|
|
937
1071
|
*
|
|
938
1072
|
* @public
|
|
939
1073
|
*/
|
|
940
|
-
declare const layerConfigProvider: <Self, A>(tag: Context.Key<Self, ConfigFileShape<A>>, options?: LayerConfigProviderOptions) => Layer.Layer<never, ConfigLoadError, Self>;
|
|
1074
|
+
export declare const layerConfigProvider: <Self, A>(tag: Context.Key<Self, ConfigFileShape<A>>, options?: LayerConfigProviderOptions) => Layer.Layer<never, ConfigLoadError, Self>;
|
|
941
1075
|
//#endregion
|
|
942
1076
|
//#region src/EncryptedCodec.d.ts
|
|
943
1077
|
declare const ConfigEncryptionError_base: Schema.Class<ConfigEncryptionError, Schema.TaggedStruct<"ConfigEncryptionError", {
|
|
@@ -958,7 +1092,7 @@ declare const ConfigEncryptionError_base: Schema.Class<ConfigEncryptionError, Sc
|
|
|
958
1092
|
*
|
|
959
1093
|
* @public
|
|
960
1094
|
*/
|
|
961
|
-
declare class ConfigEncryptionError extends ConfigEncryptionError_base {
|
|
1095
|
+
export declare class ConfigEncryptionError extends ConfigEncryptionError_base {
|
|
962
1096
|
get message(): string;
|
|
963
1097
|
}
|
|
964
1098
|
/**
|
|
@@ -971,7 +1105,7 @@ declare class ConfigEncryptionError extends ConfigEncryptionError_base {
|
|
|
971
1105
|
*
|
|
972
1106
|
* @public
|
|
973
1107
|
*/
|
|
974
|
-
type EncryptedCodecKey = {
|
|
1108
|
+
export type EncryptedCodecKey = {
|
|
975
1109
|
readonly _tag: "CryptoKey";
|
|
976
1110
|
readonly key: Effect.Effect<CryptoKey, ConfigEncryptionError>;
|
|
977
1111
|
} | {
|
|
@@ -984,7 +1118,7 @@ type EncryptedCodecKey = {
|
|
|
984
1118
|
*
|
|
985
1119
|
* @public
|
|
986
1120
|
*/
|
|
987
|
-
declare const EncryptedCodecKey: {
|
|
1121
|
+
export declare const EncryptedCodecKey: {
|
|
988
1122
|
/**
|
|
989
1123
|
* Use a pre-derived `CryptoKey` effect directly.
|
|
990
1124
|
*
|
|
@@ -1026,7 +1160,7 @@ declare const EncryptedCodecKey: {
|
|
|
1026
1160
|
*
|
|
1027
1161
|
* @public
|
|
1028
1162
|
*/
|
|
1029
|
-
declare function EncryptedCodec<E>(inner: ConfigCodec<E>, keySource: EncryptedCodecKey): ConfigCodec<E | ConfigEncryptionError>;
|
|
1163
|
+
export declare function EncryptedCodec<E>(inner: ConfigCodec<E>, keySource: EncryptedCodecKey): ConfigCodec<E | ConfigEncryptionError>;
|
|
1030
1164
|
//#endregion
|
|
1031
1165
|
//#region src/JsonCodec.d.ts
|
|
1032
1166
|
/**
|
|
@@ -1041,7 +1175,7 @@ declare function EncryptedCodec<E>(inner: ConfigCodec<E>, keySource: EncryptedCo
|
|
|
1041
1175
|
*
|
|
1042
1176
|
* @public
|
|
1043
1177
|
*/
|
|
1044
|
-
declare const JsonCodec: ConfigCodec;
|
|
1178
|
+
export declare const JsonCodec: ConfigCodec;
|
|
1045
1179
|
//#endregion
|
|
1046
1180
|
//#region src/JsoncCodec.d.ts
|
|
1047
1181
|
/**
|
|
@@ -1058,7 +1192,7 @@ declare const JsonCodec: ConfigCodec;
|
|
|
1058
1192
|
*
|
|
1059
1193
|
* @public
|
|
1060
1194
|
*/
|
|
1061
|
-
declare const JsoncCodec: ConfigCodec;
|
|
1195
|
+
export declare const JsoncCodec: ConfigCodec;
|
|
1062
1196
|
//#endregion
|
|
1063
1197
|
//#region src/TomlCodec.d.ts
|
|
1064
1198
|
/**
|
|
@@ -1079,7 +1213,7 @@ declare const JsoncCodec: ConfigCodec;
|
|
|
1079
1213
|
*
|
|
1080
1214
|
* @public
|
|
1081
1215
|
*/
|
|
1082
|
-
declare const TomlCodec: ConfigCodec;
|
|
1216
|
+
export declare const TomlCodec: ConfigCodec;
|
|
1083
1217
|
//#endregion
|
|
1084
1218
|
//#region src/YamlCodec.d.ts
|
|
1085
1219
|
/**
|
|
@@ -1095,7 +1229,7 @@ declare const TomlCodec: ConfigCodec;
|
|
|
1095
1229
|
*
|
|
1096
1230
|
* @public
|
|
1097
1231
|
*/
|
|
1098
|
-
declare const YamlCodec: ConfigCodec;
|
|
1232
|
+
export declare const YamlCodec: ConfigCodec;
|
|
1099
1233
|
//#endregion
|
|
1100
|
-
export {
|
|
1234
|
+
export type { ConfigCodec, ConfigEventsShape, ConfigFileMigration, ConfigFileOptions, ConfigFileShape, ConfigFileTestOptions, ConfigLoadError, ConfigMatch, ConfigMigrationOptions, ConfigReadError, ConfigReadOptions, ConfigSaveError, ConfigSource, ConfigUpdateError, ConfigWriteError, LayerConfigProviderOptions, NonEmptySources, UpwardWalkOptions };
|
|
1101
1235
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/config-file",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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": [
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
"./package.json": "./package.json"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@effected/jsonc": "^0.
|
|
47
|
-
"@effected/toml": "^0.
|
|
48
|
-
"@effected/walker": "^0.
|
|
49
|
-
"@effected/yaml": "^0.
|
|
50
|
-
"effect": "4.0.0-rc.
|
|
46
|
+
"@effected/jsonc": "^0.9.0",
|
|
47
|
+
"@effected/toml": "^0.6.0",
|
|
48
|
+
"@effected/walker": "^0.7.0",
|
|
49
|
+
"@effected/yaml": "^0.14.0",
|
|
50
|
+
"effect": "4.0.0-rc.112"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": ">=24.11.0"
|