@effected/config-file 0.9.0 → 0.10.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/ConfigFile.js +38 -10
- package/ConfigResolver.js +73 -37
- package/README.md +1 -1
- package/index.d.ts +59 -1
- package/package.json +1 -1
package/ConfigFile.js
CHANGED
|
@@ -14,11 +14,26 @@ import { Context, DateTime, Effect, FileSystem, Layer, Option, Path, PubSub, Sch
|
|
|
14
14
|
*
|
|
15
15
|
* @public
|
|
16
16
|
*/
|
|
17
|
-
var ConfigFileNotFoundError = class extends Schema.TaggedError()("ConfigFileNotFoundError", {
|
|
18
|
-
/** The names of the resolvers that were probed, in order. */
|
|
19
|
-
searched: Schema.Array(Schema.String)
|
|
17
|
+
var ConfigFileNotFoundError = class extends Schema.TaggedError()("ConfigFileNotFoundError", {
|
|
18
|
+
/** The names of the resolvers that were probed, in order. */
|
|
19
|
+
searched: Schema.Array(Schema.String),
|
|
20
|
+
/**
|
|
21
|
+
* The candidate paths the chain actually checked on disk, in probe order
|
|
22
|
+
* across every resolver — the failure-path mirror of {@link ConfigMatch}.
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* One `searched` name can hide many paths: an `upwardWalk` with a
|
|
26
|
+
* `filenames` list probes every name at every ancestor. Resolvers that omit
|
|
27
|
+
* the optional `resolveProbe` member contribute nothing here, so the list
|
|
28
|
+
* can be shorter than the true search (or empty for a fully hand-rolled
|
|
29
|
+
* chain) — `searched` remains the complete resolver list either way.
|
|
30
|
+
*/
|
|
31
|
+
candidates: Schema.Array(Schema.String)
|
|
32
|
+
}) {
|
|
20
33
|
get message() {
|
|
21
|
-
|
|
34
|
+
const count = this.candidates.length;
|
|
35
|
+
const probed = count === 0 ? "" : ` — ${count} candidate path${count === 1 ? "" : "s"} checked`;
|
|
36
|
+
return `No config file found (searched: ${this.searched.join(", ")}${probed})`;
|
|
22
37
|
}
|
|
23
38
|
};
|
|
24
39
|
/**
|
|
@@ -196,8 +211,15 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
196
211
|
});
|
|
197
212
|
const discover = Effect.fn("ConfigFile.discover")(function* () {
|
|
198
213
|
const sources = [];
|
|
214
|
+
const candidates = [];
|
|
199
215
|
for (const resolver of options.resolvers) {
|
|
200
|
-
|
|
216
|
+
let found;
|
|
217
|
+
if (resolver.resolveProbe !== void 0) {
|
|
218
|
+
const probe = yield* Effect.provide(resolver.resolveProbe, resolverEnv);
|
|
219
|
+
found = probe.match;
|
|
220
|
+
if (Option.isNone(found)) candidates.push(...probe.probed);
|
|
221
|
+
} else if (resolver.resolveMatch !== void 0) found = yield* Effect.provide(resolver.resolveMatch, resolverEnv);
|
|
222
|
+
else found = Option.map(yield* Effect.provide(resolver.resolve, resolverEnv), (path) => ({ path }));
|
|
201
223
|
if (Option.isSome(found)) {
|
|
202
224
|
const match = found.value;
|
|
203
225
|
const target = match.path;
|
|
@@ -214,7 +236,10 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
214
236
|
});
|
|
215
237
|
}
|
|
216
238
|
}
|
|
217
|
-
return
|
|
239
|
+
return {
|
|
240
|
+
sources,
|
|
241
|
+
candidates
|
|
242
|
+
};
|
|
218
243
|
});
|
|
219
244
|
const searched = options.resolvers.map((r) => r.name);
|
|
220
245
|
/**
|
|
@@ -239,15 +264,18 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
239
264
|
return value;
|
|
240
265
|
});
|
|
241
266
|
const load = Effect.fn("ConfigFile.load")(function* () {
|
|
242
|
-
const sources = yield* discover();
|
|
267
|
+
const { sources, candidates } = yield* discover();
|
|
243
268
|
if (sources.length === 0) {
|
|
244
269
|
yield* emit({ _tag: "NotFound" });
|
|
245
|
-
return yield* Effect.fail(new ConfigFileNotFoundError({
|
|
270
|
+
return yield* Effect.fail(new ConfigFileNotFoundError({
|
|
271
|
+
searched,
|
|
272
|
+
candidates
|
|
273
|
+
}));
|
|
246
274
|
}
|
|
247
275
|
return yield* mergeAndEmit(sources);
|
|
248
276
|
});
|
|
249
277
|
const loadOrDefault = Effect.fn("ConfigFile.loadOrDefault")(function* (defaultValue) {
|
|
250
|
-
const sources = yield* discover();
|
|
278
|
+
const { sources } = yield* discover();
|
|
251
279
|
if (sources.length === 0) {
|
|
252
280
|
yield* emit({ _tag: "NotFound" });
|
|
253
281
|
return defaultValue;
|
|
@@ -361,7 +389,7 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
361
389
|
return {
|
|
362
390
|
load: load(),
|
|
363
391
|
loadFrom,
|
|
364
|
-
discover: discover(),
|
|
392
|
+
discover: Effect.map(discover(), (found) => found.sources),
|
|
365
393
|
loadOrDefault,
|
|
366
394
|
validate,
|
|
367
395
|
encode,
|
package/ConfigResolver.js
CHANGED
|
@@ -3,33 +3,44 @@ import { Walker } from "@effected/walker";
|
|
|
3
3
|
|
|
4
4
|
//#region src/ConfigResolver.ts
|
|
5
5
|
/**
|
|
6
|
-
* Build a resolver from a
|
|
6
|
+
* Build a resolver from a probe-reporting lookup, deriving `resolveMatch` and
|
|
7
|
+
* `resolve` from it.
|
|
7
8
|
*
|
|
8
9
|
* @remarks
|
|
9
|
-
* Every built-in goes through here, so `resolve
|
|
10
|
-
* implementation and cannot drift.
|
|
10
|
+
* Every built-in goes through here, so `resolve`, `resolveMatch` and
|
|
11
|
+
* `resolveProbe` are one implementation and cannot drift.
|
|
11
12
|
*/
|
|
12
|
-
const
|
|
13
|
+
const fromProbe = (name, resolveProbe) => ({
|
|
13
14
|
name,
|
|
14
|
-
resolve: Effect.map(
|
|
15
|
-
resolveMatch
|
|
15
|
+
resolve: Effect.map(resolveProbe, (probe) => Option.map(probe.match, (match) => match.path)),
|
|
16
|
+
resolveMatch: Effect.map(resolveProbe, (probe) => probe.match),
|
|
17
|
+
resolveProbe
|
|
16
18
|
});
|
|
17
|
-
/** Absorb any failure into
|
|
18
|
-
const absorb = (effect) => Effect.catch(effect, () => Effect.succeed(
|
|
19
|
+
/** Absorb any failure into an empty probe — the resolver contract. */
|
|
20
|
+
const absorb = (effect) => Effect.catch(effect, () => Effect.succeed({
|
|
21
|
+
match: Option.none(),
|
|
22
|
+
probed: []
|
|
23
|
+
}));
|
|
19
24
|
const cwdOf = (given) => given ?? globalThis.process?.cwd?.() ?? "/";
|
|
20
|
-
const explicitPath = (target) =>
|
|
21
|
-
return
|
|
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
|
+
};
|
|
22
30
|
})));
|
|
23
|
-
const staticDir = (options) =>
|
|
31
|
+
const staticDir = (options) => fromProbe("static", absorb(Effect.gen(function* () {
|
|
24
32
|
const fs = yield* FileSystem.FileSystem;
|
|
25
33
|
const candidate = (yield* Path.Path).join(options.dir, options.filename);
|
|
26
|
-
return
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
};
|
|
31
42
|
})));
|
|
32
|
-
const upwardWalk = (options) =>
|
|
43
|
+
const upwardWalk = (options) => fromProbe(options.name ?? "walk", Effect.gen(function* () {
|
|
33
44
|
const fs = yield* FileSystem.FileSystem;
|
|
34
45
|
const path = yield* Path.Path;
|
|
35
46
|
const subpaths = options.subpaths ?? ["."];
|
|
@@ -42,31 +53,50 @@ const upwardWalk = (options) => fromMatch(options.name ?? "walk", Effect.gen(fun
|
|
|
42
53
|
filename,
|
|
43
54
|
...options.subpaths !== void 0 && { subpath: sub }
|
|
44
55
|
});
|
|
45
|
-
const
|
|
46
|
-
|
|
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
|
+
};
|
|
47
66
|
}));
|
|
48
67
|
/**
|
|
49
68
|
* Ascend from `cwd` looking for the first directory where `isRoot` reports
|
|
50
69
|
* true, then probe `subpaths` under it. Shared by `gitRoot` and
|
|
51
70
|
* `workspaceRoot`, which differ only in how a "root" is detected.
|
|
52
71
|
*/
|
|
53
|
-
const rootAnchored = (name, isRoot, options) =>
|
|
72
|
+
const rootAnchored = (name, isRoot, options) => fromProbe(name, Effect.gen(function* () {
|
|
54
73
|
const fs = yield* FileSystem.FileSystem;
|
|
55
74
|
const path = yield* Path.Path;
|
|
56
75
|
const dirs = yield* Walker.ascend(cwdOf(options.cwd));
|
|
57
76
|
const root = yield* Walker.findRoot(dirs, isRoot);
|
|
58
|
-
if (Option.isNone(root)) return
|
|
77
|
+
if (Option.isNone(root)) return {
|
|
78
|
+
match: Option.none(),
|
|
79
|
+
probed: []
|
|
80
|
+
};
|
|
59
81
|
const subpaths = options.subpaths ?? ["."];
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
dir: root.value,
|
|
66
|
-
filename: options.filename,
|
|
67
|
-
...matchedSubpath !== void 0 && { subpath: matchedSubpath }
|
|
68
|
-
};
|
|
82
|
+
const paths = subpaths.map((sub) => path.join(root.value, sub, options.filename));
|
|
83
|
+
const found = yield* Walker.firstMatch(paths, (candidate) => fs.exists(candidate));
|
|
84
|
+
const probed = Option.match(found, {
|
|
85
|
+
onNone: () => paths,
|
|
86
|
+
onSome: (target) => paths.slice(0, paths.indexOf(target) + 1)
|
|
69
87
|
});
|
|
88
|
+
return {
|
|
89
|
+
match: Option.map(found, (target) => {
|
|
90
|
+
const matchedSubpath = options.subpaths === void 0 ? void 0 : subpaths.find((sub) => path.join(root.value, sub, options.filename) === target);
|
|
91
|
+
return {
|
|
92
|
+
path: target,
|
|
93
|
+
dir: root.value,
|
|
94
|
+
filename: options.filename,
|
|
95
|
+
...matchedSubpath !== void 0 && { subpath: matchedSubpath }
|
|
96
|
+
};
|
|
97
|
+
}),
|
|
98
|
+
probed
|
|
99
|
+
};
|
|
70
100
|
}));
|
|
71
101
|
/** `.git` may be a directory (a normal repo) or a file (a worktree pointing at the real repo). */
|
|
72
102
|
const isGitRoot = (dir) => Effect.gen(function* () {
|
|
@@ -90,18 +120,24 @@ const isWorkspaceRoot = (dir) => Effect.gen(function* () {
|
|
|
90
120
|
});
|
|
91
121
|
const gitRoot = (options) => rootAnchored("git", isGitRoot, options);
|
|
92
122
|
const workspaceRoot = (options) => rootAnchored("workspace", isWorkspaceRoot, options);
|
|
93
|
-
const systemEtc = (options) =>
|
|
94
|
-
if (globalThis.process?.platform === "win32") return
|
|
123
|
+
const systemEtc = (options) => fromProbe("system", absorb(Effect.gen(function* () {
|
|
124
|
+
if (globalThis.process?.platform === "win32") return {
|
|
125
|
+
match: Option.none(),
|
|
126
|
+
probed: []
|
|
127
|
+
};
|
|
95
128
|
const fs = yield* FileSystem.FileSystem;
|
|
96
129
|
const path = yield* Path.Path;
|
|
97
130
|
const base = options.dir ?? "/etc";
|
|
98
131
|
const dir = path.join(base, options.app);
|
|
99
132
|
const candidate = path.join(dir, options.filename);
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
133
|
+
return {
|
|
134
|
+
match: (yield* fs.exists(candidate)) ? Option.some({
|
|
135
|
+
path: candidate,
|
|
136
|
+
dir,
|
|
137
|
+
filename: options.filename
|
|
138
|
+
}) : Option.none(),
|
|
139
|
+
probed: [candidate]
|
|
140
|
+
};
|
|
105
141
|
})));
|
|
106
142
|
/**
|
|
107
143
|
* Built-in resolvers, in the order a typical chain uses them.
|
package/README.md
CHANGED
|
@@ -149,7 +149,7 @@ Every failure is a tagged error you route on with `Effect.catchTag`. The tags ex
|
|
|
149
149
|
|
|
150
150
|
| Tag | Means | Recovery |
|
|
151
151
|
| --- | --- | --- |
|
|
152
|
-
| `ConfigFileNotFoundError` | The resolver chain matched nothing. Carries `searched`, the resolver names probed. | Fall back to defaults — the one failure that is often not an error. `loadOrDefault` handles it for you. |
|
|
152
|
+
| `ConfigFileNotFoundError` | The resolver chain matched nothing. Carries `searched`, the resolver names probed, and `candidates`, the paths they actually checked on disk. | Fall back to defaults — the one failure that is often not an error. `loadOrDefault` handles it for you. |
|
|
153
153
|
| `ConfigFileReadError` | A file was found but could not be read. Carries `path` and the structural `cause`. | Usually fatal: the file exists and the process cannot read it. Check permissions. |
|
|
154
154
|
| `ConfigFileWriteError` | A file could not be written. Carries `path` and the structural `cause`. | Retry elsewhere, or surface to the user. |
|
|
155
155
|
| `ConfigDefaultPathMissingError` | `save` or `update` was called on a service configured without a `defaultPath`. | A wiring bug, not a data condition. Fix the layer, or call `write` with an explicit path. |
|
package/index.d.ts
CHANGED
|
@@ -281,6 +281,29 @@ export interface ConfigResolver<R = never> {
|
|
|
281
281
|
* tail.
|
|
282
282
|
*/
|
|
283
283
|
readonly resolveMatch?: Effect.Effect<Option.Option<ConfigMatch>, never, R>;
|
|
284
|
+
/**
|
|
285
|
+
* The same lookup, additionally reporting every candidate path actually
|
|
286
|
+
* checked on disk, in probe order.
|
|
287
|
+
*
|
|
288
|
+
* @remarks
|
|
289
|
+
* Optional, and optional forever: a hand-rolled resolver that omits it is a
|
|
290
|
+
* complete `ConfigResolver`, and the pipeline falls back to `resolveMatch`
|
|
291
|
+
* (then `resolve`), contributing nothing to
|
|
292
|
+
* `ConfigFileNotFoundError.candidates` — the failure-path mirror of
|
|
293
|
+
* `ConfigSource.match` degrading to a bare path. Every built-in implements
|
|
294
|
+
* it, and derives the other two from it, so the three can never disagree.
|
|
295
|
+
*
|
|
296
|
+
* It exists because a resolver's `name` under-reports a search once one
|
|
297
|
+
* resolver probes several candidates: after the `filenames`/`subpaths`
|
|
298
|
+
* forms of `upwardWalk`, one `searched` entry can hide dozens of probed
|
|
299
|
+
* paths, and "nothing found, here is what I looked for" becomes less
|
|
300
|
+
* informative than the chain actually is.
|
|
301
|
+
*
|
|
302
|
+
* `probed` reports what the lookup CHECKED, not everything it could have:
|
|
303
|
+
* a short-circuiting walk lists the prefix ending at the match, and an
|
|
304
|
+
* absorbed filesystem failure lists nothing.
|
|
305
|
+
*/
|
|
306
|
+
readonly resolveProbe?: Effect.Effect<ConfigProbe, never, R>;
|
|
284
307
|
}
|
|
285
308
|
/**
|
|
286
309
|
* How a resolver found a config file, not merely where.
|
|
@@ -305,6 +328,29 @@ interface ConfigMatch {
|
|
|
305
328
|
/** The `filenames`/`filename` entry that matched, when the resolver takes one. */
|
|
306
329
|
readonly filename?: string;
|
|
307
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* The full report of one resolver lookup: what matched, and what was checked.
|
|
333
|
+
*
|
|
334
|
+
* @remarks
|
|
335
|
+
* `probed` carries the candidate paths the resolver actually asked the
|
|
336
|
+
* filesystem about, in probe order — for `upwardWalk`, the prefix of its
|
|
337
|
+
* directory-major candidate list ending at the match, or the full list when
|
|
338
|
+
* nothing matched. It exists so the failure path can be as informative as the
|
|
339
|
+
* success path: {@link ConfigMatch} says which candidate won and anchored
|
|
340
|
+
* where; `probed` says what lost, and where the search looked.
|
|
341
|
+
*
|
|
342
|
+
* @public
|
|
343
|
+
*/
|
|
344
|
+
interface ConfigProbe {
|
|
345
|
+
/** The match, when a candidate existed on disk. */
|
|
346
|
+
readonly match: Option.Option<ConfigMatch>;
|
|
347
|
+
/**
|
|
348
|
+
* Candidate paths checked on disk, in probe order. Empty when the lookup
|
|
349
|
+
* produced no candidate to report — no root found, a platform short-circuit,
|
|
350
|
+
* or a filesystem failure absorbed across the whole probe.
|
|
351
|
+
*/
|
|
352
|
+
readonly probed: ReadonlyArray<string>;
|
|
353
|
+
}
|
|
308
354
|
/**
|
|
309
355
|
* Options for `ConfigResolver.upwardWalk`.
|
|
310
356
|
*
|
|
@@ -486,6 +532,18 @@ export declare const MergeStrategy: {
|
|
|
486
532
|
declare const ConfigFileNotFoundError_base: Schema.Class<ConfigFileNotFoundError, Schema.TaggedStruct<"ConfigFileNotFoundError", {
|
|
487
533
|
/** The names of the resolvers that were probed, in order. */
|
|
488
534
|
readonly searched: Schema.$Array<Schema.String>;
|
|
535
|
+
/**
|
|
536
|
+
* The candidate paths the chain actually checked on disk, in probe order
|
|
537
|
+
* across every resolver — the failure-path mirror of {@link ConfigMatch}.
|
|
538
|
+
*
|
|
539
|
+
* @remarks
|
|
540
|
+
* One `searched` name can hide many paths: an `upwardWalk` with a
|
|
541
|
+
* `filenames` list probes every name at every ancestor. Resolvers that omit
|
|
542
|
+
* the optional `resolveProbe` member contribute nothing here, so the list
|
|
543
|
+
* can be shorter than the true search (or empty for a fully hand-rolled
|
|
544
|
+
* chain) — `searched` remains the complete resolver list either way.
|
|
545
|
+
*/
|
|
546
|
+
readonly candidates: Schema.$Array<Schema.String>;
|
|
489
547
|
}>, import("effect/Cause").YieldableError>;
|
|
490
548
|
/**
|
|
491
549
|
* Indicates that the resolver chain produced no configuration source.
|
|
@@ -1275,5 +1333,5 @@ export declare const TomlCodec: ConfigCodec;
|
|
|
1275
1333
|
*/
|
|
1276
1334
|
export declare const YamlCodec: ConfigCodec;
|
|
1277
1335
|
//#endregion
|
|
1278
|
-
export type { ConfigCodec, ConfigEncodeError, ConfigEncodeOptions, ConfigEventsShape, ConfigFileMigration, ConfigFileOptions, ConfigFileShape, ConfigFileTestOptions, ConfigLoadError, ConfigMatch, ConfigMigrationOptions, ConfigReadError, ConfigReadOptions, ConfigSaveError, ConfigSource, ConfigUpdateError, ConfigWriteError, LayerConfigProviderOptions, NonEmptySources, UpwardWalkOptions };
|
|
1336
|
+
export type { ConfigCodec, ConfigEncodeError, ConfigEncodeOptions, ConfigEventsShape, ConfigFileMigration, ConfigFileOptions, ConfigFileShape, ConfigFileTestOptions, ConfigLoadError, ConfigMatch, ConfigMigrationOptions, ConfigProbe, ConfigReadError, ConfigReadOptions, ConfigSaveError, ConfigSource, ConfigUpdateError, ConfigWriteError, LayerConfigProviderOptions, NonEmptySources, UpwardWalkOptions };
|
|
1279
1337
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED