@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 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 target = found.value;
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
- name: "explicit",
10
- resolve: absorb(Effect.gen(function* () {
11
- return (yield* (yield* FileSystem.FileSystem).exists(target)) ? Option.some(target) : Option.none();
12
- }))
13
- });
14
- const staticDir = (options) => ({
15
- name: "static",
16
- resolve: absorb(Effect.gen(function* () {
17
- const fs = yield* FileSystem.FileSystem;
18
- const candidate = (yield* Path.Path).join(options.dir, options.filename);
19
- return (yield* fs.exists(candidate)) ? Option.some(candidate) : Option.none();
20
- }))
21
- });
22
- const upwardWalk = (options) => ({
23
- name: "walk",
24
- resolve: Effect.gen(function* () {
25
- const path = yield* Path.Path;
26
- const subpaths = options.subpaths ?? ["."];
27
- const dirs = yield* Walker.ascend(cwdOf(options.cwd), { ...options.stopAt !== void 0 && { stopAt: options.stopAt } });
28
- return yield* Walker.findUpward(dirs, (dir) => subpaths.map((sub) => path.join(dir, sub, options.filename)));
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
- name,
38
- resolve: Effect.gen(function* () {
39
- const path = yield* Path.Path;
40
- const dirs = yield* Walker.ascend(cwdOf(options.cwd));
41
- const root = yield* Walker.findRoot(dirs, isRoot);
42
- if (Option.isNone(root)) return Option.none();
43
- const subpaths = options.subpaths ?? ["."];
44
- return yield* Walker.findUpward([root.value], (dir) => subpaths.map((sub) => path.join(dir, sub, options.filename)));
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
- name: "system",
71
- resolve: absorb(Effect.gen(function* () {
72
- if (globalThis.process?.platform === "win32") return Option.none();
73
- const fs = yield* FileSystem.FileSystem;
74
- const path = yield* Path.Path;
75
- const base = options.dir ?? "/etc";
76
- const candidate = path.join(base, options.app, options.filename);
77
- return (yield* fs.exists(candidate)) ? Option.some(candidate) : Option.none();
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 { type ConfigCodec, ConfigCodecError, ConfigDefaultPathMissingError, ConfigEncryptionError, ConfigEvent, ConfigEventPayload, ConfigEvents, type ConfigEventsShape, ConfigFile, type ConfigFileMigration, ConfigFileNotFoundError, type ConfigFileOptions, ConfigFileReadError, type ConfigFileShape, type ConfigFileTestOptions, ConfigFileWriteError, type ConfigLoadError, ConfigMigration, ConfigMigrationError, type ConfigMigrationOptions, type ConfigReadError, type ConfigReadOptions, ConfigResolver, type ConfigSaveError, type ConfigSource, ConfigSourceRef, type ConfigUpdateError, ConfigValidationError, type ConfigWriteError, EncryptedCodec, EncryptedCodecKey, JsonCodec, JsoncCodec, type LayerConfigProviderOptions, MergeStrategy, type NonEmptySources, TomlCodec, VersionAccess, YamlCodec, asConfigProvider, layerConfigProvider };
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.5.2",
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.8.0",
47
- "@effected/toml": "^0.5.0",
48
- "@effected/walker": "^0.5.0",
49
- "@effected/yaml": "^0.12.0",
50
- "effect": "4.0.0-rc.109"
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"