@effected/config-file 0.6.0 → 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 +141 -7
- package/package.json +3 -3
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,6 +26,15 @@ 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
40
|
export declare class ConfigCodecError extends ConfigCodecError_base {
|
|
@@ -242,7 +262,103 @@ export declare class ConfigEvents extends ConfigEvents_base {
|
|
|
242
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
|
*
|
|
@@ -260,13 +376,19 @@ export 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
|
}
|
|
@@ -1097,5 +1231,5 @@ export declare const TomlCodec: ConfigCodec;
|
|
|
1097
1231
|
*/
|
|
1098
1232
|
export declare const YamlCodec: ConfigCodec;
|
|
1099
1233
|
//#endregion
|
|
1100
|
-
export type { ConfigCodec, ConfigEventsShape, ConfigFileMigration, ConfigFileOptions, ConfigFileShape, ConfigFileTestOptions, ConfigLoadError, ConfigMigrationOptions, ConfigReadError, ConfigReadOptions, ConfigSaveError, ConfigSource, ConfigUpdateError, ConfigWriteError, LayerConfigProviderOptions, NonEmptySources };
|
|
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": [
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@effected/jsonc": "^0.9.0",
|
|
47
47
|
"@effected/toml": "^0.6.0",
|
|
48
|
-
"@effected/walker": "^0.
|
|
49
|
-
"@effected/yaml": "^0.
|
|
48
|
+
"@effected/walker": "^0.7.0",
|
|
49
|
+
"@effected/yaml": "^0.14.0",
|
|
50
50
|
"effect": "4.0.0-rc.112"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|